vfilelist.cpp 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  1. #include <QtDebug>
  2. #include <QtWidgets>
  3. #include <QUrl>
  4. #include "vfilelist.h"
  5. #include "vconfigmanager.h"
  6. #include "dialog/vnewfiledialog.h"
  7. #include "dialog/vfileinfodialog.h"
  8. #include "vnote.h"
  9. #include "veditarea.h"
  10. #include "utils/vutils.h"
  11. #include "vnotefile.h"
  12. #include "vconfigmanager.h"
  13. #include "vmdedit.h"
  14. #include "vmdtab.h"
  15. #include "dialog/vconfirmdeletiondialog.h"
  16. #include "dialog/vsortdialog.h"
  17. #include "vmainwindow.h"
  18. extern VConfigManager *g_config;
  19. extern VNote *g_vnote;
  20. extern VMainWindow *g_mainWin;
  21. const QString VFileList::c_infoShortcutSequence = "F2";
  22. const QString VFileList::c_copyShortcutSequence = "Ctrl+C";
  23. const QString VFileList::c_cutShortcutSequence = "Ctrl+X";
  24. const QString VFileList::c_pasteShortcutSequence = "Ctrl+V";
  25. VFileList::VFileList(QWidget *parent)
  26. : QWidget(parent), VNavigationMode()
  27. {
  28. setupUI();
  29. initShortcuts();
  30. initActions();
  31. }
  32. void VFileList::setupUI()
  33. {
  34. fileList = new QListWidget(this);
  35. fileList->setContextMenuPolicy(Qt::CustomContextMenu);
  36. fileList->setSelectionMode(QAbstractItemView::ExtendedSelection);
  37. fileList->setObjectName("FileList");
  38. QVBoxLayout *mainLayout = new QVBoxLayout;
  39. mainLayout->addWidget(fileList);
  40. mainLayout->setContentsMargins(0, 0, 0, 0);
  41. connect(fileList, &QListWidget::customContextMenuRequested,
  42. this, &VFileList::contextMenuRequested);
  43. connect(fileList, &QListWidget::itemClicked,
  44. this, &VFileList::handleItemClicked);
  45. setLayout(mainLayout);
  46. }
  47. void VFileList::initShortcuts()
  48. {
  49. QShortcut *infoShortcut = new QShortcut(QKeySequence(c_infoShortcutSequence), this);
  50. infoShortcut->setContext(Qt::WidgetWithChildrenShortcut);
  51. connect(infoShortcut, &QShortcut::activated,
  52. this, [this](){
  53. fileInfo();
  54. });
  55. QShortcut *copyShortcut = new QShortcut(QKeySequence(c_copyShortcutSequence), this);
  56. copyShortcut->setContext(Qt::WidgetWithChildrenShortcut);
  57. connect(copyShortcut, &QShortcut::activated,
  58. this, [this](){
  59. copySelectedFiles();
  60. });
  61. QShortcut *cutShortcut = new QShortcut(QKeySequence(c_cutShortcutSequence), this);
  62. cutShortcut->setContext(Qt::WidgetWithChildrenShortcut);
  63. connect(cutShortcut, &QShortcut::activated,
  64. this, [this](){
  65. cutSelectedFiles();
  66. });
  67. QShortcut *pasteShortcut = new QShortcut(QKeySequence(c_pasteShortcutSequence), this);
  68. pasteShortcut->setContext(Qt::WidgetWithChildrenShortcut);
  69. connect(pasteShortcut, &QShortcut::activated,
  70. this, [this](){
  71. pasteFilesFromClipboard();
  72. });
  73. }
  74. void VFileList::initActions()
  75. {
  76. newFileAct = new QAction(QIcon(":/resources/icons/create_note.svg"),
  77. tr("&New Note"), this);
  78. QString shortcutStr = VUtils::getShortcutText(g_config->getShortcutKeySequence("NewNote"));
  79. if (!shortcutStr.isEmpty()) {
  80. newFileAct->setText(tr("&New Note\t%1").arg(shortcutStr));
  81. }
  82. newFileAct->setToolTip(tr("Create a note in current folder"));
  83. connect(newFileAct, SIGNAL(triggered(bool)),
  84. this, SLOT(newFile()));
  85. m_openInReadAct = new QAction(QIcon(":/resources/icons/reading.svg"),
  86. tr("&Open In Read Mode"), this);
  87. m_openInReadAct->setToolTip(tr("Open current note in read mode"));
  88. connect(m_openInReadAct, &QAction::triggered,
  89. this, [this]() {
  90. QListWidgetItem *item = fileList->currentItem();
  91. if (item) {
  92. emit fileClicked(getVFile(item), OpenFileMode::Read);
  93. }
  94. });
  95. m_openInEditAct = new QAction(QIcon(":/resources/icons/editing.svg"),
  96. tr("Open In &Edit Mode"), this);
  97. m_openInEditAct->setToolTip(tr("Open current note in edit mode"));
  98. connect(m_openInEditAct, &QAction::triggered,
  99. this, [this]() {
  100. QListWidgetItem *item = fileList->currentItem();
  101. if (item) {
  102. emit fileClicked(getVFile(item), OpenFileMode::Edit);
  103. }
  104. });
  105. m_openExternalAct = new QAction(tr("Open Via External Program"), this);
  106. m_openExternalAct->setToolTip(tr("Open current note via external program"));
  107. connect(m_openExternalAct, &QAction::triggered,
  108. this, [this]() {
  109. QListWidgetItem *item = fileList->currentItem();
  110. if (item) {
  111. VNoteFile *file = getVFile(item);
  112. if (file
  113. && (!editArea->isFileOpened(file) || editArea->closeFile(file, false))) {
  114. QUrl url = QUrl::fromLocalFile(file->fetchPath());
  115. QDesktopServices::openUrl(url);
  116. }
  117. }
  118. });
  119. deleteFileAct = new QAction(QIcon(":/resources/icons/delete_note.svg"),
  120. tr("&Delete"), this);
  121. deleteFileAct->setToolTip(tr("Delete selected note"));
  122. connect(deleteFileAct, SIGNAL(triggered(bool)),
  123. this, SLOT(deleteSelectedFiles()));
  124. fileInfoAct = new QAction(QIcon(":/resources/icons/note_info.svg"),
  125. tr("&Info\t%1").arg(VUtils::getShortcutText(c_infoShortcutSequence)), this);
  126. fileInfoAct->setToolTip(tr("View and edit current note's information"));
  127. connect(fileInfoAct, SIGNAL(triggered(bool)),
  128. this, SLOT(fileInfo()));
  129. copyAct = new QAction(QIcon(":/resources/icons/copy.svg"),
  130. tr("&Copy\t%1").arg(VUtils::getShortcutText(c_copyShortcutSequence)), this);
  131. copyAct->setToolTip(tr("Copy selected notes"));
  132. connect(copyAct, &QAction::triggered,
  133. this, &VFileList::copySelectedFiles);
  134. cutAct = new QAction(QIcon(":/resources/icons/cut.svg"),
  135. tr("C&ut\t%1").arg(VUtils::getShortcutText(c_cutShortcutSequence)), this);
  136. cutAct->setToolTip(tr("Cut selected notes"));
  137. connect(cutAct, &QAction::triggered,
  138. this, &VFileList::cutSelectedFiles);
  139. pasteAct = new QAction(QIcon(":/resources/icons/paste.svg"),
  140. tr("&Paste\t%1").arg(VUtils::getShortcutText(c_pasteShortcutSequence)), this);
  141. pasteAct->setToolTip(tr("Paste notes in current folder"));
  142. connect(pasteAct, &QAction::triggered,
  143. this, &VFileList::pasteFilesFromClipboard);
  144. m_openLocationAct = new QAction(tr("&Open Note Location"), this);
  145. m_openLocationAct->setToolTip(tr("Open the folder containing this note in operating system"));
  146. connect(m_openLocationAct, &QAction::triggered,
  147. this, &VFileList::openFileLocation);
  148. m_sortAct = new QAction(QIcon(":/resources/icons/sort.svg"),
  149. tr("&Sort"),
  150. this);
  151. m_sortAct->setToolTip(tr("Sort notes in this folder manually"));
  152. connect(m_sortAct, &QAction::triggered,
  153. this, &VFileList::sortItems);
  154. }
  155. void VFileList::setDirectory(VDirectory *p_directory)
  156. {
  157. // QPointer will be set to NULL automatically once the directory was deleted.
  158. // If the last directory is deleted, m_directory and p_directory will both
  159. // be NULL.
  160. if (m_directory == p_directory) {
  161. if (!m_directory) {
  162. fileList->clear();
  163. }
  164. return;
  165. }
  166. m_directory = p_directory;
  167. if (!m_directory) {
  168. fileList->clear();
  169. return;
  170. }
  171. updateFileList();
  172. }
  173. void VFileList::updateFileList()
  174. {
  175. fileList->clear();
  176. if (!m_directory->open()) {
  177. return;
  178. }
  179. const QVector<VNoteFile *> &files = m_directory->getFiles();
  180. for (int i = 0; i < files.size(); ++i) {
  181. VNoteFile *file = files[i];
  182. insertFileListItem(file);
  183. }
  184. }
  185. void VFileList::fileInfo()
  186. {
  187. QList<QListWidgetItem *> items = fileList->selectedItems();
  188. if (items.size() == 1) {
  189. fileInfo(getVFile(items[0]));
  190. }
  191. }
  192. void VFileList::openFileLocation() const
  193. {
  194. QList<QListWidgetItem *> items = fileList->selectedItems();
  195. if (items.size() == 1) {
  196. QUrl url = QUrl::fromLocalFile(getVFile(items[0])->fetchBasePath());
  197. QDesktopServices::openUrl(url);
  198. }
  199. }
  200. void VFileList::fileInfo(VNoteFile *p_file)
  201. {
  202. if (!p_file) {
  203. return;
  204. }
  205. VDirectory *dir = p_file->getDirectory();
  206. QString curName = p_file->getName();
  207. VFileInfoDialog dialog(tr("Note Information"), "", dir, p_file, this);
  208. if (dialog.exec() == QDialog::Accepted) {
  209. QString name = dialog.getNameInput();
  210. if (name == curName) {
  211. return;
  212. }
  213. if (!p_file->rename(name)) {
  214. VUtils::showMessage(QMessageBox::Warning, tr("Warning"),
  215. tr("Fail to rename note <span style=\"%1\">%2</span>.")
  216. .arg(g_config->c_dataTextStyle).arg(curName), "",
  217. QMessageBox::Ok, QMessageBox::Ok, this);
  218. return;
  219. }
  220. QListWidgetItem *item = findItem(p_file);
  221. if (item) {
  222. fillItem(item, p_file);
  223. }
  224. emit fileUpdated(p_file);
  225. }
  226. }
  227. void VFileList::fillItem(QListWidgetItem *p_item, const VNoteFile *p_file)
  228. {
  229. unsigned long long ptr = (long long)p_file;
  230. p_item->setData(Qt::UserRole, ptr);
  231. p_item->setToolTip(p_file->getName());
  232. p_item->setText(p_file->getName());
  233. V_ASSERT(sizeof(p_file) <= sizeof(ptr));
  234. }
  235. QListWidgetItem* VFileList::insertFileListItem(VNoteFile *file, bool atFront)
  236. {
  237. V_ASSERT(file);
  238. QListWidgetItem *item = new QListWidgetItem();
  239. fillItem(item, file);
  240. if (atFront) {
  241. fileList->insertItem(0, item);
  242. } else {
  243. fileList->addItem(item);
  244. }
  245. // Qt seems not to update the QListWidget correctly. Manually force it to repaint.
  246. fileList->update();
  247. return item;
  248. }
  249. void VFileList::removeFileListItem(VNoteFile *p_file)
  250. {
  251. if (!p_file) {
  252. return;
  253. }
  254. QListWidgetItem *item = findItem(p_file);
  255. if (!item) {
  256. return;
  257. }
  258. int row = fileList->row(item);
  259. Q_ASSERT(row >= 0);
  260. fileList->takeItem(row);
  261. delete item;
  262. // Qt seems not to update the QListWidget correctly. Manually force it to repaint.
  263. fileList->update();
  264. }
  265. void VFileList::newFile()
  266. {
  267. if (!m_directory) {
  268. return;
  269. }
  270. QList<QString> suffixes = g_config->getDocSuffixes()[(int)DocType::Markdown];
  271. QString defaultSuf;
  272. QString suffixStr;
  273. for (auto const & suf : suffixes) {
  274. suffixStr += (suffixStr.isEmpty() ? suf : "/" + suf);
  275. if (defaultSuf.isEmpty() || suf == "md") {
  276. defaultSuf = suf;
  277. }
  278. }
  279. QString info = tr("Create a note in <span style=\"%1\">%2</span>.")
  280. .arg(g_config->c_dataTextStyle).arg(m_directory->getName());
  281. info = info + "<br>" + tr("Note with name ending with \"%1\" will be treated as Markdown type.")
  282. .arg(suffixStr);
  283. QString defaultName = QString("new_note.%1").arg(defaultSuf);
  284. defaultName = VUtils::getFileNameWithSequence(m_directory->fetchPath(), defaultName);
  285. VNewFileDialog dialog(tr("Create Note"), info, defaultName, m_directory, this);
  286. if (dialog.exec() == QDialog::Accepted) {
  287. VNoteFile *file = m_directory->createFile(dialog.getNameInput());
  288. if (!file) {
  289. VUtils::showMessage(QMessageBox::Warning, tr("Warning"),
  290. tr("Fail to create note <span style=\"%1\">%2</span>.")
  291. .arg(g_config->c_dataTextStyle).arg(dialog.getNameInput()), "",
  292. QMessageBox::Ok, QMessageBox::Ok, this);
  293. return;
  294. }
  295. // Write title if needed.
  296. bool contentInserted = false;
  297. if (dialog.getInsertTitleInput() && file->getDocType() == DocType::Markdown) {
  298. if (!file->open()) {
  299. qWarning() << "fail to open newly-created note" << file->getName();
  300. } else {
  301. Q_ASSERT(file->getContent().isEmpty());
  302. QString content = QString("# %1\n").arg(QFileInfo(file->getName()).baseName());
  303. file->setContent(content);
  304. if (!file->save()) {
  305. qWarning() << "fail to write to newly-created note" << file->getName();
  306. } else {
  307. contentInserted = true;
  308. }
  309. file->close();
  310. }
  311. }
  312. QVector<QListWidgetItem *> items = updateFileListAdded();
  313. Q_ASSERT(items.size() == 1);
  314. fileList->setCurrentItem(items[0], QItemSelectionModel::ClearAndSelect);
  315. // Qt seems not to update the QListWidget correctly. Manually force it to repaint.
  316. fileList->update();
  317. // Open it in edit mode
  318. emit fileCreated(file, OpenFileMode::Edit);
  319. // Move cursor down if content has been inserted.
  320. if (contentInserted) {
  321. const VMdTab *tab = dynamic_cast<VMdTab *>(editArea->currentEditTab());
  322. if (tab) {
  323. VMdEdit *edit = dynamic_cast<VMdEdit *>(tab->getEditor());
  324. if (edit && edit->getFile() == file) {
  325. QTextCursor cursor = edit->textCursor();
  326. cursor.movePosition(QTextCursor::End);
  327. edit->setTextCursor(cursor);
  328. }
  329. }
  330. }
  331. }
  332. }
  333. QVector<QListWidgetItem *> VFileList::updateFileListAdded()
  334. {
  335. QVector<QListWidgetItem *> ret;
  336. const QVector<VNoteFile *> &files = m_directory->getFiles();
  337. for (int i = 0; i < files.size(); ++i) {
  338. VNoteFile *file = files[i];
  339. if (i >= fileList->count()) {
  340. QListWidgetItem *item = insertFileListItem(file, false);
  341. ret.append(item);
  342. } else {
  343. VNoteFile *itemFile = getVFile(fileList->item(i));
  344. if (itemFile != file) {
  345. QListWidgetItem *item = insertFileListItem(file, false);
  346. ret.append(item);
  347. }
  348. }
  349. }
  350. return ret;
  351. }
  352. void VFileList::deleteSelectedFiles()
  353. {
  354. QList<QListWidgetItem *> items = fileList->selectedItems();
  355. Q_ASSERT(!items.isEmpty());
  356. QVector<VNoteFile *> files;
  357. for (auto const & item : items) {
  358. files.push_back(getVFile(item));
  359. }
  360. deleteFiles(files);
  361. }
  362. // @p_file may or may not be listed in VFileList
  363. void VFileList::deleteFile(VNoteFile *p_file)
  364. {
  365. if (!p_file) {
  366. return;
  367. }
  368. QVector<VNoteFile *> files(1, p_file);
  369. deleteFiles(files);
  370. }
  371. void VFileList::deleteFiles(const QVector<VNoteFile *> &p_files)
  372. {
  373. if (p_files.isEmpty()) {
  374. return;
  375. }
  376. QVector<ConfirmItemInfo> items;
  377. for (auto const & file : p_files) {
  378. items.push_back(ConfirmItemInfo(file->getName(),
  379. file->fetchPath(),
  380. file->fetchPath(),
  381. (void *)file));
  382. }
  383. QString text = tr("Are you sure to delete these notes?");
  384. QString info = tr("<span style=\"%1\">WARNING</span>: "
  385. "VNote will delete notes as well as all "
  386. "their images and attachments managed by VNote. "
  387. "You could find deleted files in the recycle "
  388. "bin of these notes.<br>"
  389. "Click \"Cancel\" to leave them untouched.<br>"
  390. "The operation is IRREVERSIBLE!")
  391. .arg(g_config->c_warningTextStyle);
  392. VConfirmDeletionDialog dialog(tr("Confirm Deleting Notes"),
  393. text,
  394. info,
  395. items,
  396. false,
  397. false,
  398. false,
  399. this);
  400. if (dialog.exec()) {
  401. items = dialog.getConfirmedItems();
  402. QVector<VNoteFile *> files;
  403. for (auto const & item : items) {
  404. files.push_back((VNoteFile *)item.m_data);
  405. }
  406. int nrDeleted = 0;
  407. for (auto file : files) {
  408. editArea->closeFile(file, true);
  409. // Remove the item before deleting it totally, or file will be invalid.
  410. removeFileListItem(file);
  411. QString errMsg;
  412. QString fileName = file->getName();
  413. QString filePath = file->fetchPath();
  414. if (!VNoteFile::deleteFile(file, &errMsg)) {
  415. VUtils::showMessage(QMessageBox::Warning,
  416. tr("Warning"),
  417. tr("Fail to delete note <span style=\"%1\">%2</span>.<br>"
  418. "Please check <span style=\"%1\">%3</span> and manually delete it.")
  419. .arg(g_config->c_dataTextStyle)
  420. .arg(fileName)
  421. .arg(filePath),
  422. errMsg,
  423. QMessageBox::Ok,
  424. QMessageBox::Ok,
  425. this);
  426. } else {
  427. Q_ASSERT(errMsg.isEmpty());
  428. ++nrDeleted;
  429. }
  430. }
  431. if (nrDeleted > 0) {
  432. g_mainWin->showStatusMessage(tr("%1 %2 deleted")
  433. .arg(nrDeleted)
  434. .arg(nrDeleted > 1 ? tr("notes") : tr("note")));
  435. }
  436. }
  437. }
  438. void VFileList::contextMenuRequested(QPoint pos)
  439. {
  440. QListWidgetItem *item = fileList->itemAt(pos);
  441. QMenu menu(this);
  442. menu.setToolTipsVisible(true);
  443. if (!m_directory) {
  444. return;
  445. }
  446. if (item && fileList->selectedItems().size() == 1) {
  447. VNoteFile *file = getVFile(item);
  448. if (file) {
  449. if (file->getDocType() == DocType::Markdown) {
  450. menu.addAction(m_openInReadAct);
  451. menu.addAction(m_openInEditAct);
  452. }
  453. menu.addAction(m_openExternalAct);
  454. menu.addSeparator();
  455. }
  456. }
  457. menu.addAction(newFileAct);
  458. if (fileList->count() > 1) {
  459. menu.addAction(m_sortAct);
  460. }
  461. if (item) {
  462. menu.addSeparator();
  463. menu.addAction(deleteFileAct);
  464. menu.addAction(copyAct);
  465. menu.addAction(cutAct);
  466. }
  467. if (pasteAvailable()) {
  468. if (!item) {
  469. menu.addSeparator();
  470. }
  471. menu.addAction(pasteAct);
  472. }
  473. if (item) {
  474. menu.addSeparator();
  475. menu.addAction(m_openLocationAct);
  476. if (fileList->selectedItems().size() == 1) {
  477. menu.addAction(fileInfoAct);
  478. }
  479. }
  480. menu.exec(fileList->mapToGlobal(pos));
  481. }
  482. QListWidgetItem* VFileList::findItem(const VNoteFile *p_file)
  483. {
  484. if (!p_file || p_file->getDirectory() != m_directory) {
  485. return NULL;
  486. }
  487. int nrChild = fileList->count();
  488. for (int i = 0; i < nrChild; ++i) {
  489. QListWidgetItem *item = fileList->item(i);
  490. if (p_file == getVFile(item)) {
  491. return item;
  492. }
  493. }
  494. return NULL;
  495. }
  496. void VFileList::handleItemClicked(QListWidgetItem *currentItem)
  497. {
  498. Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
  499. if (modifiers != Qt::NoModifier) {
  500. return;
  501. }
  502. if (!currentItem) {
  503. emit fileClicked(NULL);
  504. return;
  505. }
  506. // Qt seems not to update the QListWidget correctly. Manually force it to repaint.
  507. fileList->update();
  508. emit fileClicked(getVFile(currentItem), g_config->getNoteOpenMode());
  509. }
  510. bool VFileList::importFiles(const QStringList &p_files, QString *p_errMsg)
  511. {
  512. if (p_files.isEmpty()) {
  513. return false;
  514. }
  515. bool ret = true;
  516. Q_ASSERT(m_directory && m_directory->isOpened());
  517. QString dirPath = m_directory->fetchPath();
  518. QDir dir(dirPath);
  519. int nrImported = 0;
  520. for (int i = 0; i < p_files.size(); ++i) {
  521. const QString &file = p_files[i];
  522. QFileInfo fi(file);
  523. if (!fi.exists() || !fi.isFile()) {
  524. VUtils::addErrMsg(p_errMsg, tr("Skip importing non-exist file %1.")
  525. .arg(file));
  526. ret = false;
  527. continue;
  528. }
  529. QString name = VUtils::fileNameFromPath(file);
  530. Q_ASSERT(!name.isEmpty());
  531. name = VUtils::getFileNameWithSequence(dirPath, name);
  532. QString targetFilePath = dir.filePath(name);
  533. bool ret = VUtils::copyFile(file, targetFilePath, false);
  534. if (!ret) {
  535. VUtils::addErrMsg(p_errMsg, tr("Fail to copy file %1 as %1.")
  536. .arg(file)
  537. .arg(targetFilePath));
  538. ret = false;
  539. continue;
  540. }
  541. VNoteFile *destFile = m_directory->addFile(name, -1);
  542. if (destFile) {
  543. ++nrImported;
  544. qDebug() << "imported" << file << "as" << targetFilePath;
  545. } else {
  546. VUtils::addErrMsg(p_errMsg, tr("Fail to add the note %1 to target folder's configuration.")
  547. .arg(file));
  548. ret = false;
  549. continue;
  550. }
  551. }
  552. qDebug() << "imported" << nrImported << "files";
  553. updateFileList();
  554. return ret;
  555. }
  556. void VFileList::copySelectedFiles(bool p_isCut)
  557. {
  558. QList<QListWidgetItem *> items = fileList->selectedItems();
  559. if (items.isEmpty()) {
  560. return;
  561. }
  562. QJsonArray files;
  563. for (int i = 0; i < items.size(); ++i) {
  564. VNoteFile *file = getVFile(items[i]);
  565. files.append(file->fetchPath());
  566. }
  567. QJsonObject clip;
  568. clip[ClipboardConfig::c_magic] = getNewMagic();
  569. clip[ClipboardConfig::c_type] = (int)ClipboardOpType::CopyFile;
  570. clip[ClipboardConfig::c_isCut] = p_isCut;
  571. clip[ClipboardConfig::c_files] = files;
  572. QClipboard *clipboard = QApplication::clipboard();
  573. clipboard->setText(QJsonDocument(clip).toJson(QJsonDocument::Compact));
  574. qDebug() << "copied files info" << clipboard->text();
  575. int cnt = files.size();
  576. g_mainWin->showStatusMessage(tr("%1 %2 %3")
  577. .arg(cnt)
  578. .arg(cnt > 1 ? tr("notes") : tr("note"))
  579. .arg(p_isCut ? tr("cut") : tr("copied")));
  580. }
  581. void VFileList::cutSelectedFiles()
  582. {
  583. copySelectedFiles(true);
  584. }
  585. void VFileList::pasteFilesFromClipboard()
  586. {
  587. if (!pasteAvailable()) {
  588. return;
  589. }
  590. QJsonObject obj = VUtils::clipboardToJson();
  591. QJsonArray files = obj[ClipboardConfig::c_files].toArray();
  592. bool isCut = obj[ClipboardConfig::c_isCut].toBool();
  593. QVector<QString> filesToPaste(files.size());
  594. for (int i = 0; i < files.size(); ++i) {
  595. filesToPaste[i] = files[i].toString();
  596. }
  597. pasteFiles(m_directory, filesToPaste, isCut);
  598. }
  599. void VFileList::pasteFiles(VDirectory *p_destDir,
  600. const QVector<QString> &p_files,
  601. bool p_isCut)
  602. {
  603. QClipboard *clipboard = QApplication::clipboard();
  604. if (!p_destDir || p_files.isEmpty()) {
  605. clipboard->clear();
  606. return;
  607. }
  608. int nrPasted = 0;
  609. for (int i = 0; i < p_files.size(); ++i) {
  610. VNoteFile *file = g_vnote->getInternalFile(p_files[i]);
  611. if (!file) {
  612. qWarning() << "Copied file is not an internal note" << p_files[i];
  613. VUtils::showMessage(QMessageBox::Warning,
  614. tr("Warning"),
  615. tr("Fail to copy note <span style=\"%1\">%2</span>.")
  616. .arg(g_config->c_dataTextStyle)
  617. .arg(p_files[i]),
  618. tr("VNote could not find this note in any notebook."),
  619. QMessageBox::Ok,
  620. QMessageBox::Ok,
  621. this);
  622. continue;
  623. }
  624. QString fileName = file->getName();
  625. if (file->getDirectory() == p_destDir) {
  626. if (p_isCut) {
  627. qDebug() << "skip one note to cut and paste in the same folder" << fileName;
  628. continue;
  629. }
  630. // Copy and paste in the same folder.
  631. // We do not allow this if the note contains local images.
  632. if (file->getDocType() == DocType::Markdown) {
  633. QVector<ImageLink> images = VUtils::fetchImagesFromMarkdownFile(file,
  634. ImageLink::LocalRelativeInternal);
  635. if (!images.isEmpty()) {
  636. qDebug() << "skip one note with internal images to copy and paste in the same folder"
  637. << fileName;
  638. VUtils::showMessage(QMessageBox::Warning,
  639. tr("Warning"),
  640. tr("Fail to copy note <span style=\"%1\">%2</span>.")
  641. .arg(g_config->c_dataTextStyle)
  642. .arg(p_files[i]),
  643. tr("VNote does not allow copy and paste notes with internal images "
  644. "in the same folder."),
  645. QMessageBox::Ok,
  646. QMessageBox::Ok,
  647. this);
  648. continue;
  649. }
  650. }
  651. // Rename it to xxx_copy.md.
  652. fileName = VUtils::generateCopiedFileName(file->fetchBasePath(), fileName);
  653. } else {
  654. // Rename it to xxx_copy.md if needed.
  655. fileName = VUtils::generateCopiedFileName(p_destDir->fetchPath(), fileName);
  656. }
  657. QString msg;
  658. VNoteFile *destFile = NULL;
  659. bool ret = VNoteFile::copyFile(p_destDir,
  660. fileName,
  661. file,
  662. p_isCut,
  663. &destFile,
  664. &msg);
  665. if (!ret) {
  666. VUtils::showMessage(QMessageBox::Warning,
  667. tr("Warning"),
  668. tr("Fail to copy note <span style=\"%1\">%2</span>.")
  669. .arg(g_config->c_dataTextStyle)
  670. .arg(p_files[i]),
  671. msg,
  672. QMessageBox::Ok,
  673. QMessageBox::Ok,
  674. this);
  675. }
  676. if (destFile) {
  677. ++nrPasted;
  678. emit fileUpdated(destFile);
  679. }
  680. }
  681. qDebug() << "copy" << nrPasted << "files";
  682. if (nrPasted > 0) {
  683. g_mainWin->showStatusMessage(tr("%1 %2 pasted")
  684. .arg(nrPasted)
  685. .arg(nrPasted > 1 ? tr("notes") : tr("note")));
  686. }
  687. updateFileList();
  688. clipboard->clear();
  689. getNewMagic();
  690. }
  691. void VFileList::keyPressEvent(QKeyEvent *event)
  692. {
  693. int key = event->key();
  694. int modifiers = event->modifiers();
  695. switch (key) {
  696. case Qt::Key_Return:
  697. {
  698. QListWidgetItem *item = fileList->currentItem();
  699. if (item) {
  700. handleItemClicked(item);
  701. }
  702. break;
  703. }
  704. case Qt::Key_J:
  705. {
  706. if (modifiers == Qt::ControlModifier) {
  707. event->accept();
  708. QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down,
  709. Qt::NoModifier);
  710. QCoreApplication::postEvent(fileList, downEvent);
  711. return;
  712. }
  713. break;
  714. }
  715. case Qt::Key_K:
  716. {
  717. if (modifiers == Qt::ControlModifier) {
  718. event->accept();
  719. QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up,
  720. Qt::NoModifier);
  721. QCoreApplication::postEvent(fileList, upEvent);
  722. return;
  723. }
  724. break;
  725. }
  726. default:
  727. break;
  728. }
  729. QWidget::keyPressEvent(event);
  730. }
  731. void VFileList::focusInEvent(QFocusEvent * /* p_event */)
  732. {
  733. fileList->setFocus();
  734. }
  735. bool VFileList::locateFile(const VNoteFile *p_file)
  736. {
  737. if (p_file) {
  738. if (p_file->getDirectory() != m_directory) {
  739. return false;
  740. }
  741. QListWidgetItem *item = findItem(p_file);
  742. if (item) {
  743. fileList->setCurrentItem(item, QItemSelectionModel::ClearAndSelect);
  744. return true;
  745. }
  746. }
  747. return false;
  748. }
  749. void VFileList::registerNavigation(QChar p_majorKey)
  750. {
  751. m_majorKey = p_majorKey;
  752. V_ASSERT(m_keyMap.empty());
  753. V_ASSERT(m_naviLabels.empty());
  754. }
  755. void VFileList::showNavigation()
  756. {
  757. // Clean up.
  758. m_keyMap.clear();
  759. for (auto label : m_naviLabels) {
  760. delete label;
  761. }
  762. m_naviLabels.clear();
  763. if (!isVisible()) {
  764. return;
  765. }
  766. // Generate labels for visible items.
  767. auto items = getVisibleItems();
  768. int itemWidth = rect().width();
  769. for (int i = 0; i < 26 && i < items.size(); ++i) {
  770. QChar key('a' + i);
  771. m_keyMap[key] = items[i];
  772. QString str = QString(m_majorKey) + key;
  773. QLabel *label = new QLabel(str, this);
  774. label->setStyleSheet(g_vnote->getNavigationLabelStyle(str));
  775. label->show();
  776. QRect rect = fileList->visualItemRect(items[i]);
  777. // Display the label at the end to show the file name.
  778. label->move(rect.x() + itemWidth - label->width(), rect.y());
  779. m_naviLabels.append(label);
  780. }
  781. }
  782. void VFileList::hideNavigation()
  783. {
  784. m_keyMap.clear();
  785. for (auto label : m_naviLabels) {
  786. delete label;
  787. }
  788. m_naviLabels.clear();
  789. }
  790. bool VFileList::handleKeyNavigation(int p_key, bool &p_succeed)
  791. {
  792. static bool secondKey = false;
  793. bool ret = false;
  794. p_succeed = false;
  795. QChar keyChar = VUtils::keyToChar(p_key);
  796. if (secondKey && !keyChar.isNull()) {
  797. secondKey = false;
  798. p_succeed = true;
  799. ret = true;
  800. auto it = m_keyMap.find(keyChar);
  801. if (it != m_keyMap.end()) {
  802. fileList->setCurrentItem(it.value(), QItemSelectionModel::ClearAndSelect);
  803. fileList->setFocus();
  804. }
  805. } else if (keyChar == m_majorKey) {
  806. // Major key pressed.
  807. // Need second key if m_keyMap is not empty.
  808. if (m_keyMap.isEmpty()) {
  809. p_succeed = true;
  810. } else {
  811. secondKey = true;
  812. }
  813. ret = true;
  814. }
  815. return ret;
  816. }
  817. QList<QListWidgetItem *> VFileList::getVisibleItems() const
  818. {
  819. QList<QListWidgetItem *> items;
  820. for (int i = 0; i < fileList->count(); ++i) {
  821. QListWidgetItem *item = fileList->item(i);
  822. if (!item->isHidden()) {
  823. items.append(item);
  824. }
  825. }
  826. return items;
  827. }
  828. int VFileList::getNewMagic()
  829. {
  830. m_magicForClipboard = (int)QDateTime::currentDateTime().toTime_t();
  831. m_magicForClipboard |= qrand();
  832. return m_magicForClipboard;
  833. }
  834. bool VFileList::checkMagic(int p_magic) const
  835. {
  836. return m_magicForClipboard == p_magic;
  837. }
  838. bool VFileList::pasteAvailable() const
  839. {
  840. QJsonObject obj = VUtils::clipboardToJson();
  841. if (obj.isEmpty()) {
  842. return false;
  843. }
  844. if (!obj.contains(ClipboardConfig::c_type)) {
  845. return false;
  846. }
  847. ClipboardOpType type = (ClipboardOpType)obj[ClipboardConfig::c_type].toInt();
  848. if (type != ClipboardOpType::CopyFile) {
  849. return false;
  850. }
  851. if (!obj.contains(ClipboardConfig::c_magic)
  852. || !obj.contains(ClipboardConfig::c_isCut)
  853. || !obj.contains(ClipboardConfig::c_files)) {
  854. return false;
  855. }
  856. int magic = obj[ClipboardConfig::c_magic].toInt();
  857. if (!checkMagic(magic)) {
  858. return false;
  859. }
  860. QJsonArray files = obj[ClipboardConfig::c_files].toArray();
  861. return !files.isEmpty();
  862. }
  863. void VFileList::sortItems()
  864. {
  865. const QVector<VNoteFile *> &files = m_directory->getFiles();
  866. if (files.size() < 2) {
  867. return;
  868. }
  869. VSortDialog dialog(tr("Sort Notes"),
  870. tr("Sort notes in folder <span style=\"%1\">%2</span> "
  871. "in the configuration file.")
  872. .arg(g_config->c_dataTextStyle)
  873. .arg(m_directory->getName()),
  874. this);
  875. QTreeWidget *tree = dialog.getTreeWidget();
  876. tree->clear();
  877. tree->setColumnCount(3);
  878. tree->header()->setStretchLastSection(true);
  879. QStringList headers;
  880. headers << tr("Name") << tr("Created Time") << tr("Modified Time");
  881. tree->setHeaderLabels(headers);
  882. for (int i = 0; i < files.size(); ++i) {
  883. const VNoteFile *file = files[i];
  884. QString createdTime = VUtils::displayDateTime(file->getCreatedTimeUtc().toLocalTime());
  885. QString modifiedTime = VUtils::displayDateTime(file->getModifiedTimeUtc().toLocalTime());
  886. QStringList cols;
  887. cols << file->getName() << createdTime << modifiedTime;
  888. QTreeWidgetItem *item = new QTreeWidgetItem(tree, cols);
  889. item->setData(0, Qt::UserRole, i);
  890. }
  891. dialog.treeUpdated();
  892. if (dialog.exec()) {
  893. QVector<QVariant> data = dialog.getSortedData();
  894. Q_ASSERT(data.size() == files.size());
  895. QVector<int> sortedIdx(data.size(), -1);
  896. for (int i = 0; i < data.size(); ++i) {
  897. sortedIdx[i] = data[i].toInt();
  898. }
  899. qDebug() << "sort files" << sortedIdx;
  900. if (!m_directory->sortFiles(sortedIdx)) {
  901. VUtils::showMessage(QMessageBox::Warning,
  902. tr("Warning"),
  903. tr("Fail to sort notes in folder <span style=\"%1\">%2</span>.")
  904. .arg(g_config->c_dataTextStyle)
  905. .arg(m_directory->getName()),
  906. "",
  907. QMessageBox::Ok,
  908. QMessageBox::Ok,
  909. this);
  910. }
  911. updateFileList();
  912. }
  913. }