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