1 #include "util/dnd.h"
2
3 #include <QRegExp>
4
5 #include "control/controlobject.h"
6 #include "library/parserm3u.h"
7 #include "library/parserpls.h"
8 #include "mixer/playermanager.h"
9 #include "sources/soundsourceproxy.h"
10 #include "track/track.h"
11 #include "util/sandbox.h"
12
13 namespace {
14
dragUrls(const QList<QUrl> & trackUrls,QWidget * pDragSource,const QString & sourceIdentifier)15 QDrag* dragUrls(
16 const QList<QUrl>& trackUrls,
17 QWidget* pDragSource,
18 const QString& sourceIdentifier) {
19 if (trackUrls.isEmpty()) {
20 return nullptr;
21 }
22
23 QMimeData* mimeData = new QMimeData();
24 mimeData->setUrls(trackUrls);
25 mimeData->setText(sourceIdentifier);
26
27 QDrag* drag = new QDrag(pDragSource);
28 drag->setMimeData(mimeData);
29 drag->setPixmap(QPixmap(":/images/library/ic_library_drag_and_drop.svg"));
30 drag->exec(Qt::CopyAction);
31
32 return drag;
33 }
34
addFileToList(TrackFile trackFile,QList<TrackFile> * trackFiles)35 bool addFileToList(
36 TrackFile trackFile,
37 QList<TrackFile>* trackFiles) {
38 // Since the user just dropped these files into Mixxx we have permission
39 // to touch the file. Create a security token to keep this permission
40 // across reboots.
41 Sandbox::createSecurityToken(trackFile.asFileInfo());
42
43 if (!trackFile.checkFileExists()) {
44 return false;
45 }
46
47 // Filter out invalid URLs (eg. files that aren't supported audio
48 // filetypes, etc.)
49 if (!SoundSourceProxy::isFileSupported(trackFile.asFileInfo())) {
50 return false;
51 }
52
53 trackFiles->append(std::move(trackFile));
54 return true;
55 }
56
dropEventFiles(const QMimeData & mimeData,const QString & sourceIdentifier,bool firstOnly,bool acceptPlaylists)57 QList<TrackFile> dropEventFiles(
58 const QMimeData& mimeData,
59 const QString& sourceIdentifier,
60 bool firstOnly,
61 bool acceptPlaylists) {
62 qDebug() << "dropEventFiles()" << mimeData.hasUrls() << mimeData.urls();
63 qDebug() << "mimeData.hasText()" << mimeData.hasText() << mimeData.text();
64
65 if (!mimeData.hasUrls() ||
66 (mimeData.hasText() && mimeData.text() == sourceIdentifier)) {
67 return QList<TrackFile>();
68 }
69
70 return DragAndDropHelper::supportedTracksFromUrls(
71 mimeData.urls(),
72 firstOnly,
73 acceptPlaylists);
74 }
75
76 // Allow loading to a player if the player isn't playing
77 // or the settings allow interrupting a playing player.
allowLoadToPlayer(const QString & group,UserSettingsPointer pConfig)78 bool allowLoadToPlayer(
79 const QString& group,
80 UserSettingsPointer pConfig) {
81 // Always allow loads to preview decks
82 if (PlayerManager::isPreviewDeckGroup(group)) {
83 return true;
84 }
85
86 // Allow if deck is not playing
87 if (ControlObject::get(ConfigKey(group, "play")) <= 0.0) {
88 return true;
89 }
90
91 return pConfig->getValueString(
92 ConfigKey("[Controls]",
93 "AllowTrackLoadToPlayingDeck")).toInt();
94 }
95
96 } // anonymous namespace
97
98 //static
supportedTracksFromUrls(const QList<QUrl> & urls,bool firstOnly,bool acceptPlaylists)99 QList<TrackFile> DragAndDropHelper::supportedTracksFromUrls(
100 const QList<QUrl>& urls,
101 bool firstOnly,
102 bool acceptPlaylists) {
103 QList<TrackFile> trackFiles;
104 for (const QUrl& url : urls) {
105
106 // XXX: Possible WTF alert - Previously we thought we needed
107 // toString() here but what you actually want in any case when
108 // converting a QUrl to a file system path is
109 // QUrl::toLocalFile(). This is the second time we have flip-flopped
110 // on this, but I think toLocalFile() should work in any
111 // case. toString() absolutely does not work when you pass the
112 // result to a (this comment was never finished by the original
113 // author).
114 QString file(url.toLocalFile());
115
116 // If the file is on a network share, try just converting the URL to
117 // a string...
118 if (file.isEmpty()) {
119 file = url.toString();
120 }
121
122 if (file.isEmpty()) {
123 continue;
124 }
125
126 if (acceptPlaylists && (file.endsWith(".m3u") || file.endsWith(".m3u8"))) {
127 QScopedPointer<ParserM3u> playlist_parser(new ParserM3u());
128 QList<QString> track_list = playlist_parser->parse(file);
129 foreach (const QString& playlistFile, track_list) {
130 addFileToList(TrackFile(playlistFile), &trackFiles);
131 }
132 } else if (acceptPlaylists && url.toString().endsWith(".pls")) {
133 QScopedPointer<ParserPls> playlist_parser(new ParserPls());
134 QList<QString> track_list = playlist_parser->parse(file);
135 foreach (const QString& playlistFile, track_list) {
136 addFileToList(TrackFile(playlistFile), &trackFiles);
137 }
138 } else {
139 addFileToList(TrackFile::fromUrl(url), &trackFiles);
140 }
141
142 if (firstOnly && !trackFiles.isEmpty()) {
143 break;
144 }
145 }
146
147 return trackFiles;
148 }
149
150 //static
allowDeckCloneAttempt(const QDropEvent & event,const QString & group)151 bool DragAndDropHelper::allowDeckCloneAttempt(
152 const QDropEvent& event,
153 const QString& group) {
154 // only allow clones to decks
155 if (!PlayerManager::isDeckGroup(group, nullptr)) {
156 return false;
157 }
158
159 // forbid clone if shift is pressed
160 if (event.keyboardModifiers().testFlag(Qt::ShiftModifier)) {
161 return false;
162 }
163
164 if (!event.mimeData()->hasText() ||
165 // prevent cloning to ourself
166 event.mimeData()->text() == group ||
167 // only allow clone from decks
168 !PlayerManager::isDeckGroup(event.mimeData()->text(), nullptr)) {
169 return false;
170 }
171
172 return true;
173 }
174
175 //static
dragEnterAccept(const QMimeData & mimeData,const QString & sourceIdentifier,bool firstOnly,bool acceptPlaylists)176 bool DragAndDropHelper::dragEnterAccept(
177 const QMimeData& mimeData,
178 const QString& sourceIdentifier,
179 bool firstOnly,
180 bool acceptPlaylists) {
181 // TODO(XXX): This operation blocks the UI when many
182 // files are selected!
183 QList<TrackFile> files = dropEventFiles(mimeData, sourceIdentifier, firstOnly, acceptPlaylists);
184 return !files.isEmpty();
185 }
186
187 //static
dragTrack(TrackPointer pTrack,QWidget * pDragSource,const QString & sourceIdentifier)188 QDrag* DragAndDropHelper::dragTrack(
189 TrackPointer pTrack,
190 QWidget* pDragSource,
191 const QString& sourceIdentifier) {
192 QList<QUrl> trackUrls;
193 trackUrls.append(pTrack->getFileInfo().toUrl());
194 return dragUrls(trackUrls, pDragSource, sourceIdentifier);
195 }
196
197 //static
dragTrackLocations(const QList<QString> & locations,QWidget * pDragSource,const QString & sourceIdentifier)198 QDrag* DragAndDropHelper::dragTrackLocations(
199 const QList<QString>& locations,
200 QWidget* pDragSource,
201 const QString& sourceIdentifier) {
202 QList<QUrl> trackUrls;
203 foreach (QString location, locations) {
204 trackUrls.append(TrackFile(location).toUrl());
205 }
206 return dragUrls(trackUrls, pDragSource, sourceIdentifier);
207 }
208
209 //static
handleTrackDragEnterEvent(QDragEnterEvent * event,const QString & group,UserSettingsPointer pConfig)210 void DragAndDropHelper::handleTrackDragEnterEvent(
211 QDragEnterEvent* event,
212 const QString& group,
213 UserSettingsPointer pConfig) {
214 if (allowLoadToPlayer(group, pConfig) &&
215 dragEnterAccept(*event->mimeData(), group, true, false)) {
216 event->acceptProposedAction();
217 } else {
218 qDebug() << "Ignoring drag enter event, loading not allowed";
219 event->ignore();
220 }
221 }
222
223 //static
handleTrackDropEvent(QDropEvent * event,TrackDropTarget & target,const QString & group,UserSettingsPointer pConfig)224 void DragAndDropHelper::handleTrackDropEvent(
225 QDropEvent* event,
226 TrackDropTarget& target,
227 const QString& group,
228 UserSettingsPointer pConfig) {
229 if (allowLoadToPlayer(group, pConfig)) {
230 if (allowDeckCloneAttempt(*event, group)) {
231 event->accept();
232 target.emitCloneDeck(event->mimeData()->text(), group);
233 return;
234 } else {
235 QList<TrackFile> files = dropEventFiles(
236 *event->mimeData(), group, true, false);
237 if (!files.isEmpty()) {
238 event->accept();
239 target.emitTrackDropped(files.at(0).location(), group);
240 return;
241 }
242 }
243 }
244 event->ignore();
245 }
246