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