Add an advanced grouping dialog for the library. boost::multi_index ftw. Fixes issue #94.

This commit is contained in:
David Sansome 2010-03-31 23:11:45 +00:00
parent 738fdb662d
commit 134743bd29
8 changed files with 445 additions and 30 deletions

View File

@ -66,6 +66,7 @@ set(CLEMENTINE-SOURCES
backgroundthread.cpp
osdpretty.cpp
playlistdelegates.cpp
groupbydialog.cpp
)
# Header files that have Q_OBJECT in
@ -124,6 +125,7 @@ set(CLEMENTINE-MOC-HEADERS
globalshortcuts/globalshortcuts.h
osdpretty.h
playlistdelegates.h
groupbydialog.h
)
# UI files
@ -144,6 +146,7 @@ set(CLEMENTINE-UI
albumcovermanager.ui
playlistsequence.ui
osdpretty.ui
groupbydialog.ui
)
# Resource files

57
src/groupbydialog.cpp Normal file
View File

@ -0,0 +1,57 @@
/* This file is part of Clementine.
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 "groupbydialog.h"
#include <QPushButton>
GroupByDialog::GroupByDialog(QWidget *parent)
: QDialog(parent)
{
ui_.setupUi(this);
Reset();
mapping_.insert(Mapping(Library::GroupBy_None, 0));
mapping_.insert(Mapping(Library::GroupBy_Album, 1));
mapping_.insert(Mapping(Library::GroupBy_Artist, 2));
mapping_.insert(Mapping(Library::GroupBy_Composer, 3));
mapping_.insert(Mapping(Library::GroupBy_Genre, 4));
mapping_.insert(Mapping(Library::GroupBy_Year, 5));
mapping_.insert(Mapping(Library::GroupBy_YearAlbum, 6));
connect(ui_.button_box->button(QDialogButtonBox::Reset), SIGNAL(clicked()),
SLOT(Reset()));
}
void GroupByDialog::Reset() {
ui_.first->setCurrentIndex(2); // Artist
ui_.second->setCurrentIndex(1); // Album
ui_.third->setCurrentIndex(0); // None
}
void GroupByDialog::accept() {
emit Accepted(Library::Grouping(
mapping_.get<tag_index>().find(ui_.first->currentIndex())->group_by,
mapping_.get<tag_index>().find(ui_.second->currentIndex())->group_by,
mapping_.get<tag_index>().find(ui_.third->currentIndex())->group_by));
QDialog::accept();
}
void GroupByDialog::LibraryGroupingChanged(const Library::Grouping& g) {
ui_.first->setCurrentIndex(mapping_.get<tag_group_by>().find(g[0])->combo_box_index);
ui_.second->setCurrentIndex(mapping_.get<tag_group_by>().find(g[1])->combo_box_index);
ui_.third->setCurrentIndex(mapping_.get<tag_group_by>().find(g[2])->combo_box_index);
}

75
src/groupbydialog.h Normal file
View File

@ -0,0 +1,75 @@
/* This file is part of Clementine.
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 GROUPBYDIALOG_H
#define GROUPBYDIALOG_H
#include <QDialog>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/member.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include "library.h"
#include "ui_groupbydialog.h"
using boost::multi_index_container;
using boost::multi_index::indexed_by;
using boost::multi_index::ordered_unique;
using boost::multi_index::tag;
using boost::multi_index::member;
class GroupByDialog : public QDialog {
Q_OBJECT
public:
GroupByDialog(QWidget *parent = 0);
public slots:
void LibraryGroupingChanged(const Library::Grouping& g);
void accept();
signals:
void Accepted(const Library::Grouping& g);
private slots:
void Reset();
private:
struct Mapping {
Mapping(Library::GroupBy g, int i) : group_by(g), combo_box_index(i) {}
Library::GroupBy group_by;
int combo_box_index;
};
struct tag_index {};
struct tag_group_by {};
typedef multi_index_container<
Mapping,
indexed_by<
ordered_unique<tag<tag_index>,
member<Mapping, int, &Mapping::combo_box_index> >,
ordered_unique<tag<tag_group_by>,
member<Mapping, Library::GroupBy, &Mapping::group_by> >
>
> MappingContainer;
MappingContainer mapping_;
Ui_GroupByDialog ui_;
};
#endif // GROUPBYDIALOG_H

240
src/groupbydialog.ui Normal file
View File

@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GroupByDialog</class>
<widget class="QDialog" name="GroupByDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>354</width>
<height>236</height>
</rect>
</property>
<property name="windowTitle">
<string>Library advanced grouping</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>You can change the way the songs in the library are organised.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Group Library by...</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>First level</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="first">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Album</string>
</property>
</item>
<item>
<property name="text">
<string>Artist</string>
</property>
</item>
<item>
<property name="text">
<string>Composer</string>
</property>
</item>
<item>
<property name="text">
<string>Genre</string>
</property>
</item>
<item>
<property name="text">
<string>Year</string>
</property>
</item>
<item>
<property name="text">
<string>Year - Album</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Second level</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="second">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Album</string>
</property>
</item>
<item>
<property name="text">
<string>Artist</string>
</property>
</item>
<item>
<property name="text">
<string>Composer</string>
</property>
</item>
<item>
<property name="text">
<string>Genre</string>
</property>
</item>
<item>
<property name="text">
<string>Year</string>
</property>
</item>
<item>
<property name="text">
<string>Year - Album</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Third level</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="third">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Album</string>
</property>
</item>
<item>
<property name="text">
<string>Artist</string>
</property>
</item>
<item>
<property name="text">
<string>Composer</string>
</property>
</item>
<item>
<property name="text">
<string>Genre</string>
</property>
</item>
<item>
<property name="text">
<string>Year</string>
</property>
</item>
<item>
<property name="text">
<string>Year - Album</string>
</property>
</item>
</widget>
</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>11</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Reset</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>first</tabstop>
<tabstop>second</tabstop>
<tabstop>third</tabstop>
<tabstop>button_box</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>GroupByDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>GroupByDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -140,7 +140,7 @@ void Library::SongsDiscovered(const SongList& songs) {
// Find parent containers in the tree
LibraryItem* container = root_;
for (int i=0 ; i<kMaxLevels ; ++i) {
for (int i=0 ; i<3 ; ++i) {
GroupBy type = group_by_[i];
if (type == GroupBy_None) break;
@ -227,7 +227,7 @@ QString Library::DividerKey(GroupBy type, LibraryItem* item) const {
return QString(item->sort_text[0]);
case GroupBy_Year:
return QString::number(item->sort_text.toInt() / 10 * 10);
return SortTextForYear(item->sort_text.toInt() / 10 * 10);
case GroupBy_YearAlbum:
return QString::number(item->metadata.year());
@ -252,6 +252,8 @@ QString Library::DividerDisplayText(GroupBy type, const QString& key) const {
// fallthrough
case GroupBy_Year:
return QString::number(key.toInt()); // To remove leading 0s
case GroupBy_YearAlbum:
return key;
@ -400,7 +402,7 @@ void Library::LazyPopulate(LibraryItem* parent, bool signal) {
// Information about what we want the children to be
int child_level = parent->container_level + 1;
GroupBy child_type = group_by_[child_level];
GroupBy child_type = child_level >= 3 ? GroupBy_None : group_by_[child_level];
// Initialise the query. child_type says what type of thing we want (artists,
// songs, etc.)
@ -561,7 +563,7 @@ LibraryItem* Library::ItemFromQuery(GroupBy type,
case GroupBy_Year:
year = qMax(0, q.Value(0).toInt());
item->key = QString::number(year);
item->sort_text = SortTextForYear(year);
item->sort_text = SortTextForYear(year) + " ";
break;
case GroupBy_Composer:
@ -607,7 +609,7 @@ LibraryItem* Library::ItemFromSong(GroupBy type,
case GroupBy_Year:
year = qMax(0, s.year());
item->key = QString::number(year);
item->sort_text = SortTextForYear(year);
item->sort_text = SortTextForYear(year) + " ";
break;
case GroupBy_Composer: item->key = s.composer();
@ -790,13 +792,13 @@ bool Library::canFetchMore(const QModelIndex &parent) const {
return !item->lazy_loaded;
}
void Library::SetGroupBy(GroupBy g[kMaxLevels]) {
group_by_[0] = g[0];
group_by_[1] = g[1];
group_by_[2] = g[2];
void Library::SetGroupBy(const Grouping& g) {
group_by_ = g;
if (!waiting_for_threads_)
Reset();
emit GroupingChanged(g);
}
QMetaEnum Library::GroupByEnum() const {
@ -807,3 +809,23 @@ QMetaEnum Library::GroupByEnum() const {
}
return QMetaEnum();
}
const Library::GroupBy& Library::Grouping::operator [](int i) const {
switch (i) {
case 0: return first;
case 1: return second;
case 2: return third;
}
Q_ASSERT(0);
return first;
}
Library::GroupBy& Library::Grouping::operator [](int i) {
switch (i) {
case 0: return first;
case 1: return second;
case 2: return third;
}
Q_ASSERT(0);
return first;
}

View File

@ -61,9 +61,17 @@ class Library : public SimpleTreeModel<LibraryItem> {
};
QMetaEnum GroupByEnum() const;
// The tree has a maximum depth of 4 (not including the root) - 3 grouping
// levels like artist or album, and one final one for songs.
static const int kMaxLevels = 3;
struct Grouping {
Grouping() : first(GroupBy_None), second(GroupBy_None), third(GroupBy_None) {}
Grouping(GroupBy f, GroupBy s, GroupBy t) : first(f), second(s), third(t) {}
GroupBy first;
GroupBy second;
GroupBy third;
const GroupBy& operator [](int i) const;
GroupBy& operator [](int i);
};
// Useful for tests. The library takes ownership.
void set_backend_factory(BackgroundThreadFactory<LibraryBackendInterface>* factory);
@ -89,6 +97,7 @@ class Library : public SimpleTreeModel<LibraryItem> {
signals:
void Error(const QString& message);
void TotalSongCountUpdated(int count);
void GroupingChanged(const Library::Grouping& g);
void ScanStarted();
void ScanFinished();
@ -98,7 +107,7 @@ class Library : public SimpleTreeModel<LibraryItem> {
public slots:
void SetFilterAge(int age);
void SetFilterText(const QString& text);
void SetGroupBy(GroupBy g[kMaxLevels]);
void SetGroupBy(const Library::Grouping& g);
protected:
void LazyPopulate(LibraryItem* item) { LazyPopulate(item, false); }
@ -166,13 +175,13 @@ class Library : public SimpleTreeModel<LibraryItem> {
int waiting_for_threads_;
QueryOptions query_options_;
GroupBy group_by_[kMaxLevels];
Grouping group_by_;
// Keyed on database ID
QMap<int, LibraryItem*> song_nodes_;
// Keyed on whatever the key is for that level - artist, album, year, etc.
QMap<QString, LibraryItem*> container_nodes_[kMaxLevels];
QMap<QString, LibraryItem*> container_nodes_[3];
// Keyed on a letter, a year, a century, etc.
QMap<QString, LibraryItem*> divider_nodes_;

View File

@ -37,6 +37,7 @@
#include "m3uparser.h"
#include "xspfparser.h"
#include "playlistsequence.h"
#include "groupbydialog.h"
#include "globalshortcuts/globalshortcuts.h"
@ -78,6 +79,7 @@ MainWindow::MainWindow(QNetworkAccessManager* network, QWidget *parent)
settings_dialog_(new SettingsDialog(this)),
add_stream_dialog_(new AddStreamDialog(this)),
cover_manager_(new AlbumCoverManager(network, this)),
group_by_dialog_(new GroupByDialog(this)),
playlist_menu_(new QMenu(this)),
library_sort_model_(new QSortFilterProxyModel(this)),
track_position_timer_(new QTimer(this))
@ -242,7 +244,14 @@ MainWindow::MainWindow(QNetworkAccessManager* network, QWidget *parent)
group_by_group_->addAction(ui_.group_by_genre_album);
group_by_group_->addAction(ui_.group_by_genre_artist_album);
group_by_group_->addAction(ui_.group_by_advanced);
connect(group_by_group_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*)));
connect(library_, SIGNAL(GroupingChanged(Library::Grouping)),
group_by_dialog_, SLOT(LibraryGroupingChanged(Library::Grouping)));
connect(library_, SIGNAL(GroupingChanged(Library::Grouping)),
SLOT(LibraryGroupingChanged(Library::Grouping)));
connect(group_by_dialog_, SIGNAL(Accepted(Library::Grouping)),
library_, SLOT(SetGroupBy(Library::Grouping)));
// Library config menu
QMenu* library_menu = new QMenu(this);
@ -335,12 +344,10 @@ MainWindow::MainWindow(QNetworkAccessManager* network, QWidget *parent)
ui_.file_view->SetPath(settings_.value("file_path", QDir::homePath()).toString());
Library::GroupBy g[Library::kMaxLevels];
g[0] = Library::GroupBy(settings_.value("group_by1", int(Library::GroupBy_Artist)).toInt());
g[1] = Library::GroupBy(settings_.value("group_by2", int(Library::GroupBy_Album)).toInt());
g[2] = Library::GroupBy(settings_.value("group_by3", int(Library::GroupBy_None)).toInt());
library_->SetGroupBy(g);
UpdateGroupBySelection(g);
library_->SetGroupBy(Library::Grouping(
Library::GroupBy(settings_.value("group_by1", int(Library::GroupBy_Artist)).toInt()),
Library::GroupBy(settings_.value("group_by2", int(Library::GroupBy_Album)).toInt()),
Library::GroupBy(settings_.value("group_by3", int(Library::GroupBy_None)).toInt())));
bool hidden = settings_.value("hidden", false).toBool();
bool show_tray = settings_.value("showtray", true).toBool();
@ -802,33 +809,33 @@ void MainWindow::PlaylistRemoveCurrent() {
}
void MainWindow::GroupByClicked(QAction* action) {
Library::GroupBy g[Library::kMaxLevels];
for (int i=0 ; i<Library::kMaxLevels ; ++i)
g[i] = Library::GroupBy_None;
Library::Grouping g;
QStringList group_by = action->property("group_by").toStringList();
if (group_by.isEmpty()) {
qWarning() << __PRETTY_FUNCTION__ << ": Unknown action";
group_by_dialog_->show();
return;
}
for (int i=0 ; i<group_by.size() && i<Library::kMaxLevels ; ++i) {
for (int i=0 ; i<group_by.size() ; ++i) {
g[i] = Library::GroupBy(
library_->GroupByEnum().keyToValue(group_by[i].toUtf8().constData()));
}
library_->SetGroupBy(g);
}
void MainWindow::LibraryGroupingChanged(const Library::Grouping& g) {
// Save the settings
settings_.setValue("group_by1", int(g[0]));
settings_.setValue("group_by2", int(g[1]));
settings_.setValue("group_by3", int(g[2]));
}
void MainWindow::UpdateGroupBySelection(Library::GroupBy g[Library::kMaxLevels]) {
// Now make sure the correct action is checked
foreach (QAction* action, group_by_group_->actions()) {
QStringList group_by = action->property("group_by").toStringList();
bool match = true;
for (int i=0 ; i<group_by.size() && i<Library::kMaxLevels ; ++i) {
for (int i=0 ; i<group_by.size() ; ++i) {
if (g[i] != library_->GroupByEnum().keyToValue(group_by[i].toUtf8().constData())) {
match = false;
break;

View File

@ -40,6 +40,7 @@ class AddStreamDialog;
class AlbumCoverManager;
class PlaylistSequence;
class GlobalShortcuts;
class GroupByDialog;
class QSortFilterProxyModel;
class SystemTrayIcon;
@ -83,6 +84,7 @@ class MainWindow : public QMainWindow {
void LibraryDoubleClick(const QModelIndex& index);
void ClearLibraryFilter();
void GroupByClicked(QAction*);
void LibraryGroupingChanged(const Library::Grouping& g);
void VolumeWheelEvent(int delta);
void TrayClicked(QSystemTrayIcon::ActivationReason reason);
@ -107,7 +109,6 @@ class MainWindow : public QMainWindow {
private:
void SaveGeometry();
void UpdateGroupBySelection(Library::GroupBy g[Library::kMaxLevels]);
private:
static const int kStateVersion;
@ -133,6 +134,7 @@ class MainWindow : public QMainWindow {
SettingsDialog* settings_dialog_;
AddStreamDialog* add_stream_dialog_;
AlbumCoverManager* cover_manager_;
GroupByDialog* group_by_dialog_;
QMenu* playlist_menu_;
QAction* playlist_play_pause_;