1 /*
2 * Strawberry Music Player
3 * This file was part of Clementine.
4 * Copyright 2010, David Sansome <me@davidsansome.com>
5 * Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
6 *
7 * Strawberry is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Strawberry is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
19 *
20 */
21
22 #include "config.h"
23
24 #include <QObject>
25 #include <QFlags>
26 #include <QMimeData>
27 #include <QMap>
28 #include <QString>
29 #include <QStringList>
30 #include <QIcon>
31 #include <QStandardItemModel>
32 #include <QAbstractItemModel>
33
34 #include "playlistlistmodel.h"
35
PlaylistListModel(QObject * parent)36 PlaylistListModel::PlaylistListModel(QObject *parent) : QStandardItemModel(parent), dropping_rows_(false) {
37
38 QObject::connect(this, &PlaylistListModel::dataChanged, this, &PlaylistListModel::RowsChanged);
39 QObject::connect(this, &PlaylistListModel::rowsAboutToBeRemoved, this, &PlaylistListModel::RowsAboutToBeRemoved);
40 QObject::connect(this, &PlaylistListModel::rowsInserted, this, &PlaylistListModel::RowsInserted);
41
42 }
43
SetIcons(const QIcon & playlist_icon,const QIcon & folder_icon)44 void PlaylistListModel::SetIcons(const QIcon &playlist_icon, const QIcon &folder_icon) {
45 playlist_icon_ = playlist_icon;
46 folder_icon_ = folder_icon;
47 }
48
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)49 bool PlaylistListModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) {
50
51 dropping_rows_ = true;
52 bool ret = QStandardItemModel::dropMimeData(data, action, row, column, parent);
53 dropping_rows_ = false;
54
55 return ret;
56
57 }
58
ItemPath(const QStandardItem * item)59 QString PlaylistListModel::ItemPath(const QStandardItem *item) {
60
61 QStringList components;
62
63 const QStandardItem *current = item;
64 while (current) {
65 if (current->data(Role_Type).toInt() == Type_Folder) {
66 components.insert(0, current->data(Qt::DisplayRole).toString());
67 }
68 current = current->parent();
69 }
70
71 return components.join("/");
72
73 }
74
RowsChanged(const QModelIndex & begin,const QModelIndex & end)75 void PlaylistListModel::RowsChanged(const QModelIndex &begin, const QModelIndex &end) {
76 AddRowMappings(begin, end);
77 }
78
RowsInserted(const QModelIndex & parent,const int start,const int end)79 void PlaylistListModel::RowsInserted(const QModelIndex &parent, const int start, const int end) {
80
81 // RowsChanged will take care of these when dropping.
82 if (!dropping_rows_) {
83 AddRowMappings(index(start, 0, parent), index(end, 0, parent));
84 }
85
86 }
87
AddRowMappings(const QModelIndex & begin,const QModelIndex & end)88 void PlaylistListModel::AddRowMappings(const QModelIndex &begin, const QModelIndex &end) {
89
90 const QString parent_path = ItemPath(itemFromIndex(begin));
91
92 for (int i = begin.row(); i <= end.row(); ++i) {
93 const QModelIndex index = begin.sibling(i, 0);
94 QStandardItem *item = itemFromIndex(index);
95 AddRowItem(item, parent_path);
96 }
97
98 }
99
AddRowItem(QStandardItem * item,const QString & parent_path)100 void PlaylistListModel::AddRowItem(QStandardItem *item, const QString &parent_path) {
101
102 switch (item->data(Role_Type).toInt()) {
103 case Type_Playlist: {
104 const int id = item->data(Role_PlaylistId).toInt();
105
106 playlists_by_id_[id] = item;
107 if (dropping_rows_) {
108 emit PlaylistPathChanged(id, parent_path);
109 }
110
111 break;
112 }
113
114 case Type_Folder:
115 for (int j = 0; j < item->rowCount(); ++j) {
116 QStandardItem *child_item = item->child(j);
117 AddRowItem(child_item, parent_path);
118 }
119 break;
120 }
121
122 }
123
RowsAboutToBeRemoved(const QModelIndex & parent,const int start,const int end)124 void PlaylistListModel::RowsAboutToBeRemoved(const QModelIndex &parent, const int start, const int end) {
125
126 for (int i = start; i <= end; ++i) {
127 const QModelIndex idx = index(i, 0, parent);
128 const QStandardItem *item = itemFromIndex(idx);
129
130 switch (idx.data(Role_Type).toInt()) {
131 case Type_Playlist: {
132 const int id = idx.data(Role_PlaylistId).toInt();
133 QMap<int, QStandardItem*>::iterator it = playlists_by_id_.find(id);
134 if (it != playlists_by_id_.end() && it.value() == item) {
135 playlists_by_id_.erase(it); // clazy:exclude=strict-iterators
136 }
137 break;
138 }
139
140 case Type_Folder:
141 break;
142 }
143 }
144
145 }
146
PlaylistById(const int id) const147 QStandardItem *PlaylistListModel::PlaylistById(const int id) const {
148 return playlists_by_id_[id];
149 }
150
FolderByPath(const QString & path)151 QStandardItem *PlaylistListModel::FolderByPath(const QString &path) {
152
153 if (path.isEmpty()) {
154 return invisibleRootItem();
155 }
156
157 // Walk down from the root until we find the target folder. This is pretty
158 // inefficient but maintaining a path -> item map is difficult.
159 QStandardItem *parent = invisibleRootItem();
160
161 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
162 const QStringList parts = path.split('/', Qt::SkipEmptyParts);
163 #else
164 const QStringList parts = path.split('/', QString::SkipEmptyParts);
165 #endif
166
167 for (const QString &part : parts) {
168 QStandardItem *matching_child = nullptr;
169
170 const int child_count = parent->rowCount();
171 for (int i = 0; i < child_count; ++i) {
172 if (parent->child(i)->data(Qt::DisplayRole).toString() == part) {
173 matching_child = parent->child(i);
174 break;
175 }
176 }
177
178 // Does this folder exist already?
179 if (matching_child) {
180 parent = matching_child;
181 }
182 else {
183 QStandardItem *child = NewFolder(part);
184 parent->appendRow(child);
185 parent = child;
186 }
187 }
188
189 return parent;
190
191 }
192
NewFolder(const QString & name) const193 QStandardItem *PlaylistListModel::NewFolder(const QString &name) const {
194
195 QStandardItem *ret = new QStandardItem;
196 ret->setText(name);
197 ret->setData(PlaylistListModel::Type_Folder, PlaylistListModel::Role_Type);
198 ret->setIcon(folder_icon_);
199 ret->setFlags(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
200 return ret;
201
202 }
203
NewPlaylist(const QString & name,const int id) const204 QStandardItem *PlaylistListModel::NewPlaylist(const QString &name, const int id) const {
205
206 QStandardItem *ret = new QStandardItem;
207 ret->setText(name);
208 ret->setData(PlaylistListModel::Type_Playlist, PlaylistListModel::Role_Type);
209 ret->setData(id, PlaylistListModel::Role_PlaylistId);
210 ret->setIcon(playlist_icon_);
211 ret->setFlags(Qt::ItemIsDragEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
212 return ret;
213
214 }
215
setData(const QModelIndex & idx,const QVariant & value,int role)216 bool PlaylistListModel::setData(const QModelIndex &idx, const QVariant &value, int role) {
217
218 if (!QStandardItemModel::setData(idx, value, role)) {
219 return false;
220 }
221
222 switch (idx.data(Role_Type).toInt()) {
223 case Type_Playlist:
224 emit PlaylistRenamed(idx.data(Role_PlaylistId).toInt(), value.toString());
225 break;
226
227 case Type_Folder:
228 // Walk all the children and modify their paths.
229 UpdatePathsRecursive(idx);
230 break;
231 }
232
233 return true;
234
235 }
236
UpdatePathsRecursive(const QModelIndex & parent)237 void PlaylistListModel::UpdatePathsRecursive(const QModelIndex &parent) {
238
239 switch (parent.data(Role_Type).toInt()) {
240 case Type_Playlist:
241 emit PlaylistPathChanged(parent.data(Role_PlaylistId).toInt(), ItemPath(itemFromIndex(parent)));
242 break;
243
244 case Type_Folder:
245 for (int i = 0; i < rowCount(parent); ++i) {
246 UpdatePathsRecursive(index(i, 0, parent));
247 }
248 break;
249 }
250
251 }
252