Add contextual searches to playlists, library and global search. (#5649)

* Closes #5567: Contextual album/artist search in library search, global search, and playlists.

* Change artist/album search to 'search for this' in globalsearch

* Change artist/album search to 'search for this' in library

* Applying patch from @Fat-Zer to allow search incl year

* Re-adding missing schema definition
This commit is contained in:
Jacob Henner 2017-08-09 09:12:36 -04:00 committed by John Maguire
parent 4aee29982e
commit 203ec76973
12 changed files with 161 additions and 56 deletions

View File

@ -471,6 +471,7 @@
<file>schema/schema-4.sql</file>
<file>schema/schema-5.sql</file>
<file>schema/schema-50.sql</file>
<file>schema/schema-51.sql</file>
<file>schema/schema-6.sql</file>
<file>schema/schema-7.sql</file>
<file>schema/schema-8.sql</file>

15
data/schema/schema-51.sql Normal file
View File

@ -0,0 +1,15 @@
DELETE FROM %allsongstables_fts;
DROP TABLE %allsongstables_fts;
CREATE VIRTUAL TABLE %allsongstables_fts USING fts3( ftstitle, ftsalbum, ftsartist, ftsalbumartist,
ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment, ftsyear,
tokenize=unicode
);
INSERT INTO %allsongstables_fts ( ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist,
ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment, ftsyear)
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment, year
FROM %allsongstables;
UPDATE schema_version SET version=51;

View File

@ -47,7 +47,7 @@
#include <QVariant>
const char* Database::kDatabaseFilename = "clementine.db";
const int Database::kSchemaVersion = 50;
const int Database::kSchemaVersion = 51;
const char* Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;

View File

@ -130,7 +130,8 @@ const QStringList Song::kFtsColumns = QStringList() << "ftstitle"
<< "ftsperformer"
<< "ftsgrouping"
<< "ftsgenre"
<< "ftscomment";
<< "ftscomment"
<< "ftsyear";
const QString Song::kFtsColumnSpec = Song::kFtsColumns.join(", ");
const QString Song::kFtsBindSpec =
@ -996,6 +997,7 @@ void Song::BindToFtsQuery(QSqlQuery* query) const {
query->bindValue(":ftsgrouping", d->grouping_);
query->bindValue(":ftsgenre", d->genre_);
query->bindValue(":ftscomment", d->comment_);
query->bindValue(":ftsyear", d->year_);
}
#ifdef HAVE_LIBLASTFM

View File

@ -36,9 +36,11 @@
#include "core/logging.h"
#include "core/mimedata.h"
#include "core/timeconstants.h"
#include "internet/core/internetsongmimedata.h"
#include "library/libraryfilterwidget.h"
#include "library/librarymodel.h"
#include "library/groupbydialog.h"
#include "playlist/songmimedata.h"
using std::placeholders::_1;
using std::placeholders::_2;
@ -446,34 +448,38 @@ bool GlobalSearchView::SearchKeyEvent(QKeyEvent* event) {
}
bool GlobalSearchView::ResultsContextMenuEvent(QContextMenuEvent* event) {
if (!context_menu_) {
context_menu_ = new QMenu(this);
context_actions_ << context_menu_->addAction(
IconLoader::Load("media-playback-start", IconLoader::Base),
tr("Append to current playlist"), this,
SLOT(AddSelectedToPlaylist()));
context_actions_ << context_menu_->addAction(
IconLoader::Load("media-playback-start", IconLoader::Base),
tr("Replace current playlist"), this,
SLOT(LoadSelected()));
context_actions_ << context_menu_->addAction(
IconLoader::Load("document-new", IconLoader::Base),
tr("Open in new playlist"), this,
SLOT(OpenSelectedInNewPlaylist()));
context_menu_ = new QMenu(this);
context_actions_ << context_menu_->addAction(
IconLoader::Load("media-playback-start", IconLoader::Base),
tr("Append to current playlist"), this, SLOT(AddSelectedToPlaylist()));
context_actions_ << context_menu_->addAction(
IconLoader::Load("media-playback-start", IconLoader::Base),
tr("Replace current playlist"), this, SLOT(LoadSelected()));
context_actions_ << context_menu_->addAction(
IconLoader::Load("document-new", IconLoader::Base),
tr("Open in new playlist"), this, SLOT(OpenSelectedInNewPlaylist()));
context_menu_->addSeparator();
context_actions_ << context_menu_->addAction(
IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"),
this, SLOT(AddSelectedToPlaylistEnqueue()));
context_menu_->addSeparator();
context_actions_ << context_menu_->addAction(
IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"), this,
SLOT(AddSelectedToPlaylistEnqueue()));
context_menu_->addSeparator();
context_menu_->addMenu(tr("Group by"))
->addActions(group_by_actions_->actions());
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
tr("Configure global search..."), this,
SLOT(OpenSettingsDialog()));
context_menu_->addSeparator();
if (ui_->results->selectionModel() &&
ui_->results->selectionModel()->selectedRows().length() == 1) {
context_actions_ << context_menu_->addAction(
IconLoader::Load("system-search", IconLoader::Base),
tr("Search for this"), this, SLOT(SearchForThis()));
}
context_menu_->addSeparator();
context_menu_->addMenu(tr("Group by"))
->addActions(group_by_actions_->actions());
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
tr("Configure global search..."), this,
SLOT(OpenSettingsDialog()));
const bool enable_context_actions =
ui_->results->selectionModel() &&
ui_->results->selectionModel()->hasSelection();
@ -515,6 +521,11 @@ void GlobalSearchView::OpenSelectedInNewPlaylist() {
emit AddToPlaylist(data);
}
void GlobalSearchView::SearchForThis() {
StartSearch(
ui_->results->selectionModel()->selectedRows().first().data().toString());
}
void GlobalSearchView::showEvent(QShowEvent* e) {
if (show_suggestions_) {
UpdateSuggestions();
@ -560,9 +571,12 @@ void GlobalSearchView::GroupByClicked(QAction* action) {
}
void GlobalSearchView::SetGroupBy(const LibraryModel::Grouping& g) {
// Clear requests: changing "group by" on the models will cause all the items to be removed/added
// again, so all the QModelIndex here will become invalid. New requests will be created for those
// songs when they will be displayed again anyway (when GlobalSearchItemDelegate::paint will call
// Clear requests: changing "group by" on the models will cause all the items
// to be removed/added
// again, so all the QModelIndex here will become invalid. New requests will
// be created for those
// songs when they will be displayed again anyway (when
// GlobalSearchItemDelegate::paint will call
// LazyLoadArt)
art_requests_.clear();
// Update the models

View File

@ -21,6 +21,7 @@
#include "searchprovider.h"
#include "library/librarymodel.h"
#include "ui/settingsdialog.h"
#include "playlist/playlistmanager.h"
#include <QWidget>
@ -82,6 +83,8 @@ signals:
void OpenSelectedInNewPlaylist();
void AddSelectedToPlaylistEnqueue();
void SearchForThis();
void GroupByClicked(QAction* action);
void SetGroupBy(const LibraryModel::Grouping& grouping);

View File

@ -656,6 +656,9 @@ QVariant LibraryModel::data(const LibraryItem* item, int role) const {
case Role_SortText:
return item->SortText();
case Role_DisplayText:
return item->DisplayText();
}
return QVariant();
}

View File

@ -64,6 +64,7 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
Role_Type = Qt::UserRole + 1,
Role_ContainerType,
Role_SortText,
Role_DisplayText,
Role_Key,
Role_Artist,
Role_IsDivider,

View File

@ -103,7 +103,7 @@ void LibraryItemDelegate::paint(QPainter* painter,
// Draw the line under the item
QColor line_color = opt.palette.color(QPalette::Text);
QLinearGradient grad_color(opt.rect.bottomLeft(), opt.rect.bottomRight());
const double fade_start_end = (opt.rect.width()/3.0)/opt.rect.width();
const double fade_start_end = (opt.rect.width() / 3.0) / opt.rect.width();
line_color.setAlphaF(0.0);
grad_color.setColorAt(0, line_color);
line_color.setAlphaF(0.5);
@ -193,15 +193,17 @@ LibraryView::~LibraryView() {}
void LibraryView::SaveFocus() {
QModelIndex current = currentIndex();
QVariant type = model()->data(current, LibraryModel::Role_Type);
if (!type.isValid() || !(type.toInt() == LibraryItem::Type_Song ||
type.toInt() == LibraryItem::Type_Container ||
type.toInt() == LibraryItem::Type_Divider)) {
if (!type.isValid() ||
!(type.toInt() == LibraryItem::Type_Song ||
type.toInt() == LibraryItem::Type_Container ||
type.toInt() == LibraryItem::Type_Divider)) {
return;
}
last_selected_path_.clear();
last_selected_song_ = Song();
last_selected_container_ = QString();
last_selected_text_ = QString();
switch (type.toInt()) {
case LibraryItem::Type_Song: {
@ -210,6 +212,7 @@ void LibraryView::SaveFocus() {
SongList songs = app_->library_model()->GetChildSongs(index);
if (!songs.isEmpty()) {
last_selected_song_ = songs.last();
last_selected_text_ = songs.last().title();
}
break;
}
@ -217,8 +220,9 @@ void LibraryView::SaveFocus() {
case LibraryItem::Type_Container:
case LibraryItem::Type_Divider: {
QString text =
model()->data(current, LibraryModel::Role_SortText).toString();
model()->data(current, LibraryModel::Role_Key).toString();
last_selected_container_ = text;
last_selected_text_ = model()->data(current, LibraryModel::Role_DisplayText).toString();
break;
}
@ -232,8 +236,9 @@ void LibraryView::SaveFocus() {
void LibraryView::SaveContainerPath(const QModelIndex& child) {
QModelIndex current = model()->parent(child);
QVariant type = model()->data(current, LibraryModel::Role_Type);
if (!type.isValid() || !(type.toInt() == LibraryItem::Type_Container ||
type.toInt() == LibraryItem::Type_Divider)) {
if (!type.isValid() ||
!(type.toInt() == LibraryItem::Type_Container ||
type.toInt() == LibraryItem::Type_Divider)) {
return;
}
@ -276,7 +281,7 @@ bool LibraryView::RestoreLevelFocus(const QModelIndex& parent) {
case LibraryItem::Type_Container:
case LibraryItem::Type_Divider: {
QString text =
model()->data(current, LibraryModel::Role_SortText).toString();
model()->data(current, LibraryModel::Role_Key).toString();
if (!last_selected_container_.isEmpty() &&
last_selected_container_ == text) {
emit expand(current);
@ -384,11 +389,13 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist()));
context_menu_->addSeparator();
add_to_playlist_enqueue_ =
context_menu_->addAction(IconLoader::Load("go-next", IconLoader::Base),
tr("Queue track"), this,
SLOT(AddToPlaylistEnqueue()));
add_to_playlist_enqueue_ = context_menu_->addAction(
IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"), this,
SLOT(AddToPlaylistEnqueue()));
context_menu_->addSeparator();
search_for_this_ = context_menu_->addAction(
IconLoader::Load("system-search", IconLoader::Base),
tr("Search for this"), this, SLOT(SearchForThis()));
context_menu_->addSeparator();
new_smart_playlist_ = context_menu_->addAction(
IconLoader::Load("document-new", IconLoader::Base),
@ -401,23 +408,23 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
tr("Delete smart playlist"), this, SLOT(DeleteSmartPlaylist()));
context_menu_->addSeparator();
organise_ = context_menu_->addAction(IconLoader::Load("edit-copy", IconLoader::Base),
tr("Organise files..."), this,
SLOT(Organise()));
organise_ = context_menu_->addAction(
IconLoader::Load("edit-copy", IconLoader::Base),
tr("Organise files..."), this, SLOT(Organise()));
copy_to_device_ = context_menu_->addAction(
IconLoader::Load("multimedia-player-ipod-mini-blue", IconLoader::Base),
tr("Copy to device..."), this, SLOT(CopyToDevice()));
delete_ = context_menu_->addAction(IconLoader::Load("edit-delete", IconLoader::Base),
tr("Delete from disk..."), this,
SLOT(Delete()));
delete_ = context_menu_->addAction(
IconLoader::Load("edit-delete", IconLoader::Base),
tr("Delete from disk..."), this, SLOT(Delete()));
context_menu_->addSeparator();
edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename", IconLoader::Base),
tr("Edit track information..."),
this, SLOT(EditTracks()));
edit_tracks_ = context_menu_->addAction(IconLoader::Load("edit-rename", IconLoader::Base),
tr("Edit tracks information..."),
this, SLOT(EditTracks()));
edit_track_ = context_menu_->addAction(
IconLoader::Load("edit-rename", IconLoader::Base),
tr("Edit track information..."), this, SLOT(EditTracks()));
edit_tracks_ = context_menu_->addAction(
IconLoader::Load("edit-rename", IconLoader::Base),
tr("Edit tracks information..."), this, SLOT(EditTracks()));
show_in_browser_ = context_menu_->addAction(
IconLoader::Load("document-open-folder", IconLoader::Base),
tr("Show in file browser..."), this, SLOT(ShowInBrowser()));
@ -458,6 +465,8 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
int regular_elements = 0;
// number of editable non smart playlists selected
int regular_editable = 0;
// number of container elements selected
int container_elements = 0;
for (const QModelIndex& index : selected_indexes) {
int type =
@ -467,6 +476,10 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
smart_playlists++;
} else if (type == LibraryItem::Type_PlaylistContainer) {
smart_playlists_header++;
} else if (type == LibraryItem::Type_Container) {
container_elements++;
// To preserve expected behavior, since a container is "regular"
regular_elements++;
} else {
regular_elements++;
}
@ -487,6 +500,10 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
songs_selected == smart_playlists + smart_playlists_header;
const bool only_smart_playlist_selected =
smart_playlists == 1 && songs_selected == 1;
const bool one_regular_song_only =
regular_elements_only && container_elements == 0 && regular_elements == 1;
const bool one_container_only =
container_elements == 1 && songs_selected == 1;
// in all modes
load_->setEnabled(songs_selected);
@ -509,6 +526,9 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
show_in_various_->setVisible(regular_elements_only);
no_show_in_various_->setVisible(regular_elements_only);
// only when a single container or one song is selected exclusively
search_for_this_->setVisible(one_container_only || one_regular_song_only);
// only when all selected items are editable
organise_->setEnabled(regular_elements == regular_editable);
copy_to_device_->setEnabled(regular_elements == regular_editable);
@ -714,6 +734,13 @@ void LibraryView::FilterReturnPressed() {
emit doubleClicked(currentIndex());
}
void LibraryView::SearchForThis() {
SaveFocus();
if (!last_selected_text_.isEmpty()) {
filter_->ShowInLibrary(last_selected_text_.simplified());
}
}
void LibraryView::NewSmartPlaylist() {
Wizard* wizard = new Wizard(app_, app_->library_backend(), this);
wizard->setAttribute(Qt::WA_DeleteOnClose);

View File

@ -101,6 +101,8 @@ signals:
void ShowInVarious();
void NoShowInVarious();
void SearchForThis();
void NewSmartPlaylist();
void EditSmartPlaylist();
void DeleteSmartPlaylist();
@ -139,6 +141,8 @@ signals:
QAction* show_in_various_;
QAction* no_show_in_various_;
QAction* search_for_this_;
QAction* new_smart_playlist_;
QAction* edit_smart_playlist_;
QAction* delete_smart_playlist_;
@ -151,6 +155,7 @@ signals:
// Save focus
Song last_selected_song_;
QString last_selected_container_;
QString last_selected_text_;
QSet<QString> last_selected_path_;
};

View File

@ -676,7 +676,13 @@ MainWindow::MainWindow(Application* app, SystemTrayIcon* tray_icon, OSD* osd,
ui_->playlist->addAction(playlist_queue_);
playlist_skip_ = playlist_menu_->addAction("", this, SLOT(PlaylistSkip()));
ui_->playlist->addAction(playlist_skip_);
playlist_menu_->addSeparator();
search_for_artist_ = playlist_menu_->addAction(
IconLoader::Load("system-search", IconLoader::Base),
tr("Search for artist"), this, SLOT(SearchForArtist()));
search_for_album_ = playlist_menu_->addAction(
IconLoader::Load("system-search", IconLoader::Base),
tr("Search for album"), this, SLOT(SearchForAlbum()));
playlist_menu_->addSeparator();
playlist_menu_->addAction(ui_->action_remove_from_playlist);
playlist_undoredo_ = playlist_menu_->addSeparator();
@ -1733,6 +1739,9 @@ void MainWindow::PlaylistRightClick(const QPoint& global_pos,
playlist_delete_->setVisible(false);
playlist_copy_to_device_->setVisible(false);
search_for_artist_->setVisible(all == 1);
search_for_album_->setVisible(all == 1);
if (in_queue == 1 && not_in_queue == 0)
playlist_queue_->setText(tr("Dequeue track"));
else if (in_queue > 1 && not_in_queue == 0)
@ -2484,6 +2493,26 @@ void MainWindow::PlaylistCopyToDevice() {
}
}
void MainWindow::SearchForArtist() {
PlaylistItemPtr item(
app_->playlist_manager()->current()->item_at(playlist_menu_index_.row()));
Song song = item->Metadata();
if (!song.albumartist().isEmpty()) {
DoGlobalSearch(song.albumartist().simplified());
} else if (!song.artist().isEmpty()) {
DoGlobalSearch(song.artist().simplified());
}
}
void MainWindow::SearchForAlbum() {
PlaylistItemPtr item(
app_->playlist_manager()->current()->item_at(playlist_menu_index_.row()));
Song song = item->Metadata();
if (!song.album().isEmpty()) {
DoGlobalSearch(song.album().simplified());
}
}
void MainWindow::ChangeLibraryQueryMode(QAction* action) {
if (action == library_show_duplicates_) {
library_view_->filter()->SetQueryMode(QueryOptions::QueryMode_Duplicates);

View File

@ -147,7 +147,6 @@ signals:
void StopAfterToggled(bool stop);
void IntroPointReached();
private slots:
void FilePathChanged(const QString& path);
@ -178,6 +177,9 @@ signals:
void PlaylistUndoRedoChanged(QAction* undo, QAction* redo);
void AddFilesToTranscoder();
void SearchForArtist();
void SearchForAlbum();
void PlaylistCopyToLibrary();
void PlaylistMoveToLibrary();
void PlaylistCopyToDevice();
@ -365,6 +367,9 @@ signals:
QAction* playlistitem_actions_separator_;
QModelIndex playlist_menu_index_;
QAction* search_for_artist_;
QAction* search_for_album_;
QSortFilterProxyModel* library_sort_model_;
QTimer* track_position_timer_;