1 /* DirectoryTreeView.cpp */
2 
3 /* Copyright (C) 2011-2020 Michael Lugmair (Lucio Carreras)
4  *
5  * This file is part of sayonara player
6  *
7  * This program 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  * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "DirectoryTreeView.h"
22 #include "DirectoryModel.h"
23 #include "DirectoryContextMenu.h"
24 
25 #include "Interfaces/LibraryInfoAccessor.h"
26 
27 #include "Gui/Utils/Delegates/StyledItemDelegate.h"
28 #include "Gui/Utils/PreferenceAction.h"
29 #include "Gui/Utils/MimeData/CustomMimeData.h"
30 #include "Gui/Utils/MimeData/MimeDataUtils.h"
31 #include "Gui/Utils/InputDialog/LineInputDialog.h"
32 #include "Gui/Utils/Icons.h"
33 #include "Gui/Utils/Widgets/ProgressBar.h"
34 
35 #include "Utils/MetaData/MetaDataList.h"
36 #include "Utils/Library/LibraryInfo.h"
37 #include "Utils/FileUtils.h"
38 #include "Utils/Algorithm.h"
39 #include "Utils/Language/Language.h"
40 #include "Utils/Logger/Logger.h"
41 
42 #include <QDir>
43 #include <QUrl>
44 #include <QMouseEvent>
45 #include <QDrag>
46 #include <QTimer>
47 #include <QAction>
48 #include <QDesktopServices>
49 
50 #include <algorithm>
51 
52 using Directory::TreeView;
53 
54 struct TreeView::Private
55 {
56 	LibraryInfoAccessor* libraryInfoAccessor;
57 	Directory::Model* model;
58 	Directory::ContextMenu* contextMenu = nullptr;
59 	Gui::ProgressBar* progressBar = nullptr;
60 
61 	QTimer* dragTimer;
62 	QModelIndex dragTargetIndex;
63 
64 	Private(LibraryInfoAccessor* libraryInfoAccessor, TreeView* parent) :
65 		libraryInfoAccessor {libraryInfoAccessor},
66 		model {new Directory::Model(libraryInfoAccessor, parent)},
67 		dragTimer {new QTimer {parent}}
68 	{
69 		dragTimer->setSingleShot(true);
70 		dragTimer->setInterval(750);
71 	}
72 
73 	void resetDrag()
74 	{
75 		dragTimer->stop();
76 		dragTargetIndex = QModelIndex();
77 	}
78 };
79 
80 TreeView::TreeView(QWidget* parent) :
81 	Gui::WidgetTemplate<QTreeView>(parent),
82 	InfoDialogContainer(),
83 	Gui::Dragable(this) {}
84 
85 TreeView::~TreeView() = default;
86 
87 void TreeView::init(LibraryInfoAccessor* libraryInfoAccessor, const Library::Info& info)
88 {
89 	m = Pimpl::make<Private>(libraryInfoAccessor, this);
90 
91 	setModel(m->model);
92 	connect(m->model, &Model::sigBusy, this, &TreeView::setBusy);
93 	connect(m->dragTimer, &QTimer::timeout, this, &TreeView::dragTimerTimeout);
94 
95 	this->setItemDelegate(new Gui::StyledItemDelegate(this));
96 	this->setDragDropMode(QAbstractItemView::DragDrop);
97 
98 	auto* action = new QAction(this);
99 	action->setShortcut(QKeySequence("F2"));
100 	action->setShortcutContext(Qt::WidgetShortcut);
101 	connect(action, &QAction::triggered, this, &TreeView::renameDirectoryClicked);
102 	this->addAction(action);
103 
104 	const auto index = m->model->setDataSource(info.id());
105 	if(index.isValid())
106 	{
107 		this->setRootIndex(index);
108 	}
109 }
110 
111 void TreeView::initContextMenu()
112 {
113 	if(m->contextMenu)
114 	{
115 		return;
116 	}
117 
118 	m->contextMenu = new ContextMenu(ContextMenu::Mode::Dir, m->libraryInfoAccessor, this);
119 
120 	connect(m->contextMenu, &ContextMenu::sigDeleteClicked, this, &TreeView::sigDeleteClicked);
121 	connect(m->contextMenu, &ContextMenu::sigPlayClicked, this, &TreeView::sigPlayClicked);
122 	connect(m->contextMenu, &ContextMenu::sigPlayNewTabClicked, this, &TreeView::sigPlayNewTabClicked);
123 	connect(m->contextMenu, &ContextMenu::sigPlayNextClicked, this, &TreeView::sigPlayNextClicked);
124 	connect(m->contextMenu, &ContextMenu::sigAppendClicked, this, &TreeView::sigAppendClicked);
125 	connect(m->contextMenu, &ContextMenu::sigCreateDirectoryClicked, this, &TreeView::createDirectoryClicked);
126 	connect(m->contextMenu, &ContextMenu::sigRenameClicked, this, &TreeView::renameDirectoryClicked);
127 	connect(m->contextMenu, &ContextMenu::sigCollapseAllClicked, this, &TreeView::collapseAll);
128 	connect(m->contextMenu, &ContextMenu::sigViewInFileManagerClicked, this, &TreeView::viewInFileManagerClicked);
129 	connect(m->contextMenu, &ContextMenu::sigCopyToLibrary, this, &TreeView::sigCopyToLibraryRequested);
130 	connect(m->contextMenu, &ContextMenu::sigMoveToLibrary, this, &TreeView::sigMoveToLibraryRequested);
131 
132 	connect(m->contextMenu, &ContextMenu::sigInfoClicked, this, [this]() { this->showInfo(); });
133 	connect(m->contextMenu, &ContextMenu::sigEditClicked, this, [this]() { this->showEdit(); });
134 }
135 
136 QString TreeView::directoryName(const QModelIndex& index)
137 {
138 	return m->model->filePath(index);
139 }
140 
141 QModelIndexList TreeView::selectedRows() const
142 {
143 	return selectionModel()->selectedRows();
144 }
145 
146 QStringList TreeView::selectedPaths() const
147 {
148 	QStringList paths;
149 
150 	Util::Algorithm::transform(selectedRows(), paths, [&](const auto& index) {
151 		return m->model->filePath(index);
152 	});
153 
154 	return paths;
155 }
156 
157 void TreeView::createDirectoryClicked()
158 {
159 	const auto paths = selectedPaths();
160 	if(paths.size() != 1)
161 	{
162 		return;
163 	}
164 
165 	const auto newName =
166 		Gui::LineInputDialog::getNewFilename(this, Lang::get(Lang::CreateDirectory), paths[0]);
167 
168 	if(!newName.isEmpty())
169 	{
170 		Util::File::createDir(paths[0] + "/" + newName);
171 		this->expand(m->model->indexOfPath(paths[0]));
172 	}
173 }
174 
175 void TreeView::renameDirectoryClicked()
176 {
177 	const auto paths = selectedPaths();
178 	if(paths.size() != 1)
179 	{
180 		return;
181 	}
182 
183 	const auto originalDir = QDir(paths[0]);
184 	auto parentDir = originalDir;
185 
186 	if(parentDir.cdUp())
187 	{
188 		const auto newName =
189 			Gui::LineInputDialog::getRenameFilename(this, originalDir.dirName(), parentDir.absolutePath());
190 
191 		if(!newName.isEmpty())
192 		{
193 			emit sigRenameRequested(originalDir.absolutePath(), parentDir.absoluteFilePath(newName));
194 		}
195 	}
196 }
197 
198 void TreeView::viewInFileManagerClicked()
199 {
200 	const auto paths = this->selectedPaths();
201 	for(const auto& path : paths)
202 	{
203 		const auto url = QUrl::fromLocalFile(path);
204 		QDesktopServices::openUrl(url);
205 	}
206 }
207 
208 void TreeView::setBusy(bool b)
209 {
210 	if(b)
211 	{
212 		this->setDragDropMode(DragDropMode::NoDragDrop);
213 
214 		if(!m->progressBar)
215 		{
216 			m->progressBar = new Gui::ProgressBar(this);
217 		}
218 
219 		m->progressBar->show();
220 	}
221 
222 	else
223 	{
224 		this->setDragDropMode(DragDropMode::DragDrop);
225 
226 		if(m->progressBar)
227 		{
228 			m->progressBar->hide();
229 		}
230 	}
231 }
232 
233 void TreeView::dragTimerTimeout()
234 {
235 	if(m->dragTargetIndex.isValid())
236 	{
237 		this->expand(m->dragTargetIndex);
238 		emit sigCurrentIndexChanged(m->dragTargetIndex);
239 	}
240 
241 	m->resetDrag();
242 }
243 
244 void TreeView::dragEnterEvent(QDragEnterEvent* event)
245 {
246 	m->resetDrag();
247 
248 	const auto* mimeData = event->mimeData();
249 	event->setAccepted(mimeData && mimeData->hasUrls());
250 }
251 
252 void TreeView::dragMoveEvent(QDragMoveEvent* event)
253 {
254 	Parent::dragMoveEvent(event);
255 
256 	const auto* mimeData = event->mimeData();
257 	if(!mimeData)
258 	{
259 		event->ignore();
260 		return;
261 	}
262 
263 	const auto index = this->indexAt(event->pos());
264 	if(index != m->dragTargetIndex)
265 	{
266 		m->dragTargetIndex = index;
267 
268 		if(index.isValid())
269 		{
270 			m->dragTimer->start();
271 		}
272 	}
273 
274 	const auto* cmd = Gui::MimeData::customMimedata(mimeData);
275 
276 	if(mimeData->hasUrls() || (cmd && cmd->hasSource(this)))
277 	{
278 		event->acceptProposedAction();
279 	}
280 
281 	else
282 	{
283 		event->ignore();
284 	}
285 
286 	if(event->isAccepted() && !selectionModel()->isSelected(index))
287 	{
288 		selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
289 	}
290 }
291 
292 void TreeView::dragLeaveEvent(QDragLeaveEvent* event)
293 {
294 	m->resetDrag();
295 	event->accept();
296 
297 	Parent::dragLeaveEvent(event);
298 }
299 
300 void TreeView::dropEvent(QDropEvent* event)
301 {
302 	event->accept();
303 
304 	m->dragTimer->stop();
305 	m->dragTargetIndex = QModelIndex();
306 
307 	const auto index = this->indexAt(event->pos());
308 	if(!index.isValid())
309 	{
310 		return;
311 	}
312 
313 	const auto* mimedata = event->mimeData();
314 	if(!mimedata)
315 	{
316 		return;
317 	}
318 
319 	const auto targetDirectory = m->model->filePath(index);
320 	const auto* cmd = Gui::MimeData::customMimedata(mimedata);
321 	if(cmd)
322 	{
323 		handleSayonaraDrop(cmd, targetDirectory);
324 	}
325 
326 	else if(mimedata->hasUrls())
327 	{
328 		QStringList files;
329 
330 		const auto urls = mimedata->urls();
331 		for(const auto& url : urls)
332 		{
333 			const auto localFile = url.toLocalFile();
334 			if(!localFile.isEmpty())
335 			{
336 				files << localFile;
337 			}
338 		}
339 
340 		const auto libraryId = m->model->libraryDataSource();
341 		if(libraryId >= 0)
342 		{
343 			emit sigImportRequested(libraryId, files, targetDirectory);
344 		}
345 	}
346 }
347 
348 void TreeView::handleSayonaraDrop(const Gui::CustomMimeData* cmd, const QString& targetDirectory)
349 {
350 	const auto urls = cmd->urls();
351 	QStringList sourceFiles, sourceDirectories;
352 
353 	for(const auto& url : urls)
354 	{
355 		const auto localFilename = url.toLocalFile();
356 		const auto localFile = (!localFilename.isEmpty())
357 		                       ? localFilename
358 		                       : url.toString(QUrl::PreferLocalFile);
359 
360 		if(Util::File::isDir(localFile) && Util::File::canCopyDir(localFile, targetDirectory))
361 		{
362 			sourceDirectories << localFile;
363 		}
364 
365 		else if(Util::File::isFile(localFile))
366 		{
367 			sourceFiles << localFile;
368 		}
369 	}
370 
371 	if(!sourceDirectories.isEmpty())
372 	{
373 		const auto dropAction = showDropMenu(QCursor::pos());
374 		switch(dropAction)
375 		{
376 			case TreeView::DropAction::Copy:
377 				emit sigCopyRequested(sourceDirectories, targetDirectory);
378 				break;
379 			case TreeView::DropAction::Move:
380 				emit sigMoveRequested(sourceDirectories, targetDirectory);
381 				break;
382 			default:
383 				break;
384 		}
385 	}
386 
387 	if(!sourceFiles.isEmpty())
388 	{
389 		const auto dropAction = showDropMenu(QCursor::pos());
390 		switch(dropAction)
391 		{
392 			case TreeView::DropAction::Copy:
393 				emit sigCopyRequested(sourceFiles, targetDirectory);
394 				break;
395 			case TreeView::DropAction::Move:
396 				emit sigMoveRequested(sourceFiles, targetDirectory);
397 				break;
398 			default:
399 				break;
400 		}
401 	}
402 }
403 
404 TreeView::DropAction TreeView::showDropMenu(const QPoint& pos)
405 {
406 	auto menu = QMenu(this);
407 	auto* copyAction = new QAction(tr("Copy here"), &menu);
408 	auto* moveAction = new QAction(tr("Move here"), &menu);
409 	auto* cancelAction = new QAction(Lang::get(Lang::Cancel), &menu);
410 
411 	menu.addActions(
412 		{
413 			copyAction,
414 			moveAction,
415 			menu.addSeparator(),
416 			cancelAction,
417 		});
418 
419 	auto* action = menu.exec(pos);
420 	if(action == copyAction)
421 	{
422 		return TreeView::DropAction::Copy;
423 	}
424 
425 	else if(action == moveAction)
426 	{
427 		return TreeView::DropAction::Move;
428 	}
429 
430 	return TreeView::DropAction::Cancel;
431 }
432 
433 void TreeView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected)
434 {
435 	if(!m->dragTimer->isActive())
436 	{
437 		QTreeView::selectionChanged(selected, deselected);
438 
439 		const auto index = (!selected.indexes().isEmpty())
440 		                   ? selected.indexes().first()
441 		                   : QModelIndex();
442 
443 		emit sigCurrentIndexChanged(index);
444 	}
445 }
446 
447 void TreeView::setFilterTerm(const QString& filter) { m->model->setFilter(filter); }
448 
449 MD::Interpretation TreeView::metadataInterpretation() const { return MD::Interpretation::Tracks; }
450 
451 MetaDataList TreeView::infoDialogData() const {	return MetaDataList(); }
452 
453 bool TreeView::hasMetadata() const { return false; }
454 
455 QStringList TreeView::pathlist() const
456 {
457     return this->selectedPaths();
458 }
459 
460 QWidget *TreeView::getParentWidget()
461 {
462     return this;
463 }
464 
465 void TreeView::keyPressEvent(QKeyEvent* event)
466 {
467 	switch(event->key())
468 	{
469 		case Qt::Key_Enter:
470 		case Qt::Key_Return:
471 			emit sigEnterPressed();
472 			return;
473 		case Qt::Key_Escape:
474 			this->clearSelection();
475 			return;
476 		default:
477 			Parent::keyPressEvent(event);
478 	}
479 }
480 
481 void TreeView::contextMenuEvent(QContextMenuEvent* event)
482 {
483 	if(!m->contextMenu)
484 	{
485 		initContextMenu();
486 	}
487 
488 	m->contextMenu->refresh(selectedPaths().size());
489 
490 	const auto pos = QWidget::mapToGlobal(event->pos());
491 	m->contextMenu->exec(pos);
492 }
493 
494 void TreeView::skinChanged()
495 {
496 	const auto height = this->fontMetrics().height();
497 	this->setIconSize(QSize(height, height));
498 	this->setIndentation(height);
499 }
500