vfilelist.cpp 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064
  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, true);
  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, true);
  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, true);
  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. "Deleted files could be found 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 %2.")
  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. QClipboard *clipboard = QApplication::clipboard();
  599. clipboard->clear();
  600. }
  601. void VFileList::pasteFiles(VDirectory *p_destDir,
  602. const QVector<QString> &p_files,
  603. bool p_isCut)
  604. {
  605. if (!p_destDir || p_files.isEmpty()) {
  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 paste 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() << "pasted" << 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. getNewMagic();
  689. }
  690. void VFileList::keyPressEvent(QKeyEvent *event)
  691. {
  692. int key = event->key();
  693. int modifiers = event->modifiers();
  694. switch (key) {
  695. case Qt::Key_Return:
  696. {
  697. QListWidgetItem *item = fileList->currentItem();
  698. if (item) {
  699. handleItemClicked(item);
  700. }
  701. break;
  702. }
  703. case Qt::Key_J:
  704. {
  705. if (modifiers == Qt::ControlModifier) {
  706. event->accept();
  707. QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down,
  708. Qt::NoModifier);
  709. QCoreApplication::postEvent(fileList, downEvent);
  710. return;
  711. }
  712. break;
  713. }
  714. case Qt::Key_K:
  715. {
  716. if (modifiers == Qt::ControlModifier) {
  717. event->accept();
  718. QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up,
  719. Qt::NoModifier);
  720. QCoreApplication::postEvent(fileList, upEvent);
  721. return;
  722. }
  723. break;
  724. }
  725. default:
  726. break;
  727. }
  728. QWidget::keyPressEvent(event);
  729. }
  730. void VFileList::focusInEvent(QFocusEvent * /* p_event */)
  731. {
  732. fileList->setFocus();
  733. }
  734. bool VFileList::locateFile(const VNoteFile *p_file)
  735. {
  736. if (p_file) {
  737. if (p_file->getDirectory() != m_directory) {
  738. return false;
  739. }
  740. QListWidgetItem *item = findItem(p_file);
  741. if (item) {
  742. fileList->setCurrentItem(item, QItemSelectionModel::ClearAndSelect);
  743. return true;
  744. }
  745. }
  746. return false;
  747. }
  748. void VFileList::registerNavigation(QChar p_majorKey)
  749. {
  750. m_majorKey = p_majorKey;
  751. V_ASSERT(m_keyMap.empty());
  752. V_ASSERT(m_naviLabels.empty());
  753. }
  754. void VFileList::showNavigation()
  755. {
  756. // Clean up.
  757. m_keyMap.clear();
  758. for (auto label : m_naviLabels) {
  759. delete label;
  760. }
  761. m_naviLabels.clear();
  762. if (!isVisible()) {
  763. return;
  764. }
  765. // Generate labels for visible items.
  766. auto items = getVisibleItems();
  767. int itemWidth = rect().width();
  768. for (int i = 0; i < 26 && i < items.size(); ++i) {
  769. QChar key('a' + i);
  770. m_keyMap[key] = items[i];
  771. QString str = QString(m_majorKey) + key;
  772. QLabel *label = new QLabel(str, this);
  773. label->setStyleSheet(g_vnote->getNavigationLabelStyle(str));
  774. label->show();
  775. QRect rect = fileList->visualItemRect(items[i]);
  776. // Display the label at the end to show the file name.
  777. label->move(rect.x() + itemWidth - label->width(), rect.y());
  778. m_naviLabels.append(label);
  779. }
  780. }
  781. void VFileList::hideNavigation()
  782. {
  783. m_keyMap.clear();
  784. for (auto label : m_naviLabels) {
  785. delete label;
  786. }
  787. m_naviLabels.clear();
  788. }
  789. bool VFileList::handleKeyNavigation(int p_key, bool &p_succeed)
  790. {
  791. static bool secondKey = false;
  792. bool ret = false;
  793. p_succeed = false;
  794. QChar keyChar = VUtils::keyToChar(p_key);
  795. if (secondKey && !keyChar.isNull()) {
  796. secondKey = false;
  797. p_succeed = true;
  798. ret = true;
  799. auto it = m_keyMap.find(keyChar);
  800. if (it != m_keyMap.end()) {
  801. fileList->setCurrentItem(it.value(), QItemSelectionModel::ClearAndSelect);
  802. fileList->setFocus();
  803. }
  804. } else if (keyChar == m_majorKey) {
  805. // Major key pressed.
  806. // Need second key if m_keyMap is not empty.
  807. if (m_keyMap.isEmpty()) {
  808. p_succeed = true;
  809. } else {
  810. secondKey = true;
  811. }
  812. ret = true;
  813. }
  814. return ret;
  815. }
  816. QList<QListWidgetItem *> VFileList::getVisibleItems() const
  817. {
  818. QList<QListWidgetItem *> items;
  819. for (int i = 0; i < fileList->count(); ++i) {
  820. QListWidgetItem *item = fileList->item(i);
  821. if (!item->isHidden()) {
  822. items.append(item);
  823. }
  824. }
  825. return items;
  826. }
  827. int VFileList::getNewMagic()
  828. {
  829. m_magicForClipboard = (int)QDateTime::currentDateTime().toTime_t();
  830. m_magicForClipboard |= qrand();
  831. return m_magicForClipboard;
  832. }
  833. bool VFileList::checkMagic(int p_magic) const
  834. {
  835. return m_magicForClipboard == p_magic;
  836. }
  837. bool VFileList::pasteAvailable() const
  838. {
  839. QJsonObject obj = VUtils::clipboardToJson();
  840. if (obj.isEmpty()) {
  841. return false;
  842. }
  843. if (!obj.contains(ClipboardConfig::c_type)) {
  844. return false;
  845. }
  846. ClipboardOpType type = (ClipboardOpType)obj[ClipboardConfig::c_type].toInt();
  847. if (type != ClipboardOpType::CopyFile) {
  848. return false;
  849. }
  850. if (!obj.contains(ClipboardConfig::c_magic)
  851. || !obj.contains(ClipboardConfig::c_isCut)
  852. || !obj.contains(ClipboardConfig::c_files)) {
  853. return false;
  854. }
  855. int magic = obj[ClipboardConfig::c_magic].toInt();
  856. if (!checkMagic(magic)) {
  857. return false;
  858. }
  859. QJsonArray files = obj[ClipboardConfig::c_files].toArray();
  860. return !files.isEmpty();
  861. }
  862. void VFileList::sortItems()
  863. {
  864. const QVector<VNoteFile *> &files = m_directory->getFiles();
  865. if (files.size() < 2) {
  866. return;
  867. }
  868. VSortDialog dialog(tr("Sort Notes"),
  869. tr("Sort notes in folder <span style=\"%1\">%2</span> "
  870. "in the configuration file.")
  871. .arg(g_config->c_dataTextStyle)
  872. .arg(m_directory->getName()),
  873. this);
  874. QTreeWidget *tree = dialog.getTreeWidget();
  875. tree->clear();
  876. tree->setColumnCount(3);
  877. QStringList headers;
  878. headers << tr("Name") << tr("Created Time") << tr("Modified Time");
  879. tree->setHeaderLabels(headers);
  880. for (int i = 0; i < files.size(); ++i) {
  881. const VNoteFile *file = files[i];
  882. QString createdTime = VUtils::displayDateTime(file->getCreatedTimeUtc().toLocalTime());
  883. QString modifiedTime = VUtils::displayDateTime(file->getModifiedTimeUtc().toLocalTime());
  884. QStringList cols;
  885. cols << file->getName() << createdTime << modifiedTime;
  886. QTreeWidgetItem *item = new QTreeWidgetItem(tree, cols);
  887. item->setData(0, Qt::UserRole, i);
  888. }
  889. dialog.treeUpdated();
  890. if (dialog.exec()) {
  891. QVector<QVariant> data = dialog.getSortedData();
  892. Q_ASSERT(data.size() == files.size());
  893. QVector<int> sortedIdx(data.size(), -1);
  894. for (int i = 0; i < data.size(); ++i) {
  895. sortedIdx[i] = data[i].toInt();
  896. }
  897. qDebug() << "sort files" << sortedIdx;
  898. if (!m_directory->sortFiles(sortedIdx)) {
  899. VUtils::showMessage(QMessageBox::Warning,
  900. tr("Warning"),
  901. tr("Fail to sort notes in folder <span style=\"%1\">%2</span>.")
  902. .arg(g_config->c_dataTextStyle)
  903. .arg(m_directory->getName()),
  904. "",
  905. QMessageBox::Ok,
  906. QMessageBox::Ok,
  907. this);
  908. }
  909. updateFileList();
  910. }
  911. }