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