vfilelist.cpp 39 KB

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