#include #include #include #include #include "vfilelist.h" #include "vconfigmanager.h" #include "dialog/vnewfiledialog.h" #include "dialog/vfileinfodialog.h" #include "vnote.h" #include "veditarea.h" #include "utils/vutils.h" #include "vnotefile.h" #include "vconfigmanager.h" #include "vmdeditor.h" #include "vmdtab.h" #include "dialog/vconfirmdeletiondialog.h" #include "dialog/vsortdialog.h" #include "vmainwindow.h" #include "utils/viconutils.h" #include "dialog/vtipsdialog.h" #include "vcart.h" #include "vhistorylist.h" extern VConfigManager *g_config; extern VNote *g_vnote; extern VMainWindow *g_mainWin; VFileList::VFileList(QWidget *parent) : QWidget(parent), VNavigationMode(), m_openWithMenu(NULL), m_itemClicked(NULL), m_fileToCloseInSingleClick(NULL) { setupUI(); initShortcuts(); m_clickTimer = new QTimer(this); m_clickTimer->setSingleShot(true); m_clickTimer->setInterval(QApplication::doubleClickInterval()); // When timer timeouts, we need to close the previous tab to simulate the // effect as opening file in current tab. connect(m_clickTimer, &QTimer::timeout, this, [this]() { m_itemClicked = NULL; VFile *file = m_fileToCloseInSingleClick; m_fileToCloseInSingleClick = NULL; if (file) { editArea->closeFile(file, false); fileList->setFocus(); } }); updateNumberLabel(); } void VFileList::setupUI() { QLabel *titleLabel = new QLabel(tr("Notes"), this); titleLabel->setProperty("TitleLabel", true); QPushButton *viewBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/view.svg"), "", this); viewBtn->setToolTip(tr("View")); viewBtn->setProperty("CornerBtn", true); viewBtn->setFocusPolicy(Qt::NoFocus); QMenu *viewMenu = new QMenu(this); connect(viewMenu, &QMenu::aboutToShow, this, [this, viewMenu]() { updateViewMenu(viewMenu); }); viewBtn->setMenu(viewMenu); m_splitBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/split_window.svg"), "", this); m_splitBtn->setToolTip(tr("Split")); m_splitBtn->setCheckable(true); m_splitBtn->setProperty("CornerBtn", true); m_splitBtn->setFocusPolicy(Qt::NoFocus); connect(m_splitBtn, &QPushButton::clicked, this, [this](bool p_checked) { emit requestSplitOut(p_checked); }); m_numLabel = new QLabel(this); QHBoxLayout *titleLayout = new QHBoxLayout(); titleLayout->addWidget(titleLabel); titleLayout->addWidget(viewBtn); titleLayout->addWidget(m_splitBtn); titleLayout->addStretch(); titleLayout->addWidget(m_numLabel); titleLayout->setContentsMargins(0, 0, 0, 0); fileList = new VFileListWidget(this); fileList->setContextMenuPolicy(Qt::CustomContextMenu); fileList->setObjectName("FileList"); fileList->setAttribute(Qt::WA_MacShowFocusRect, false); fileList->setMimeDataGetter([this](const QString &p_format, const QList &p_items) { return getMimeData(p_format, p_items); }); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->addLayout(titleLayout); mainLayout->addWidget(fileList); mainLayout->setContentsMargins(0, 0, 0, 0); connect(fileList, &QListWidget::customContextMenuRequested, this, &VFileList::contextMenuRequested); connect(fileList, &QListWidget::itemClicked, this, &VFileList::handleItemClicked); connect(fileList, &QListWidget::itemEntered, this, &VFileList::showStatusTipAboutItem); connect(fileList, &QListWidget::currentItemChanged, this, [this](QListWidgetItem *p_cur, QListWidgetItem *p_pre) { Q_UNUSED(p_pre); if (p_cur) { showStatusTipAboutItem(p_cur); } }); setLayout(mainLayout); } void VFileList::initShortcuts() { QShortcut *infoShortcut = new QShortcut(QKeySequence(Shortcut::c_info), this); infoShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(infoShortcut, &QShortcut::activated, this, [this](){ fileInfo(); }); QShortcut *copyShortcut = new QShortcut(QKeySequence(Shortcut::c_copy), this); copyShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(copyShortcut, &QShortcut::activated, this, [this](){ copySelectedFiles(); }); QShortcut *cutShortcut = new QShortcut(QKeySequence(Shortcut::c_cut), this); cutShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(cutShortcut, &QShortcut::activated, this, [this](){ cutSelectedFiles(); }); QShortcut *pasteShortcut = new QShortcut(QKeySequence(Shortcut::c_paste), this); pasteShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(pasteShortcut, &QShortcut::activated, this, [this](){ pasteFilesFromClipboard(); }); QKeySequence seq(g_config->getShortcutKeySequence("OpenViaDefaultProgram")); if (!seq.isEmpty()) { QShortcut *defaultProgramShortcut = new QShortcut(seq, this); defaultProgramShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(defaultProgramShortcut, &QShortcut::activated, this, &VFileList::openCurrentItemViaDefaultProgram); } } void VFileList::setDirectory(VDirectory *p_directory) { // QPointer will be set to NULL automatically once the directory was deleted. // If the last directory is deleted, m_directory and p_directory will both // be NULL. if (m_directory == p_directory) { if (!m_directory) { fileList->clearAll(); updateNumberLabel(); } return; } m_directory = p_directory; if (!m_directory) { fileList->clearAll(); updateNumberLabel(); return; } updateFileList(); } void VFileList::updateFileList() { fileList->clearAll(); if (!m_directory->open()) { return; } QVector files = m_directory->getFiles(); sortFiles(files, (ViewOrder)g_config->getNoteListViewOrder()); for (int i = 0; i < files.size(); ++i) { VNoteFile *file = files[i]; insertFileListItem(file); } updateNumberLabel(); } void VFileList::fileInfo() { QList items = fileList->selectedItems(); if (items.size() == 1) { fileInfo(getVFile(items[0])); } } void VFileList::openFileLocation() const { QList items = fileList->selectedItems(); if (items.size() == 1) { QUrl url = QUrl::fromLocalFile(getVFile(items[0])->fetchBasePath()); QDesktopServices::openUrl(url); } } void VFileList::addFileToCart() const { QList items = fileList->selectedItems(); VCart *cart = g_mainWin->getCart(); for (int i = 0; i < items.size(); ++i) { cart->addFile(getVFile(items[i])->fetchPath()); } g_mainWin->showStatusMessage(tr("%1 %2 added to Cart") .arg(items.size()) .arg(items.size() > 1 ? tr("notes") : tr("note"))); } void VFileList::pinFileToHistory() const { QList items = fileList->selectedItems(); QStringList files; for (int i = 0; i < items.size(); ++i) { files << getVFile(items[i])->fetchPath(); } g_mainWin->getHistoryList()->pinFiles(files); g_mainWin->showStatusMessage(tr("%1 %2 pinned to History") .arg(items.size()) .arg(items.size() > 1 ? tr("notes") : tr("note"))); } void VFileList::setFileQuickAccess() const { QList items = fileList->selectedItems(); if (items.size() == 1) { QString fp(getVFile(items[0])->fetchPath()); g_config->setQuickAccess(fp); g_mainWin->showStatusMessage(tr("Quick access: %1").arg(fp)); } } void VFileList::fileInfo(VNoteFile *p_file) { if (!p_file) { return; } VDirectory *dir = p_file->getDirectory(); QString curName = p_file->getName(); VFileInfoDialog dialog(tr("Note Information"), "", dir, p_file, this); if (dialog.exec() == QDialog::Accepted) { QString name = dialog.getNameInput(); if (name == curName) { return; } if (!p_file->rename(name)) { VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to rename note %2.") .arg(g_config->c_dataTextStyle).arg(curName), "", QMessageBox::Ok, QMessageBox::Ok, this); return; } QListWidgetItem *item = findItem(p_file); if (item) { fillItem(item, p_file); } emit fileUpdated(p_file, UpdateAction::InfoChanged); } } void VFileList::fillItem(QListWidgetItem *p_item, const VNoteFile *p_file) { qulonglong ptr = (qulonglong)p_file; p_item->setData(Qt::UserRole, ptr); p_item->setText(p_file->getName()); QString createdTime = VUtils::displayDateTime(p_file->getCreatedTimeUtc().toLocalTime()); QString modifiedTime = VUtils::displayDateTime(p_file->getModifiedTimeUtc().toLocalTime()); QString tooltip = tr("%1\n\nCreated Time: %2\nModified Time: %3") .arg(p_file->getName()) .arg(createdTime) .arg(modifiedTime); p_item->setToolTip(tooltip); V_ASSERT(sizeof(p_file) <= sizeof(ptr)); } QListWidgetItem* VFileList::insertFileListItem(VNoteFile *file, bool atFront) { V_ASSERT(file); QListWidgetItem *item = new QListWidgetItem(); fillItem(item, file); if (atFront) { fileList->insertItem(0, item); } else { fileList->addItem(item); } // Qt seems not to update the QListWidget correctly. Manually force it to repaint. fileList->update(); return item; } void VFileList::removeFileListItem(VNoteFile *p_file) { if (!p_file) { return; } QListWidgetItem *item = findItem(p_file); if (!item) { return; } int row = fileList->row(item); Q_ASSERT(row >= 0); fileList->takeItem(row); delete item; // Qt seems not to update the QListWidget correctly. Manually force it to repaint. fileList->update(); } void VFileList::newFile() { if (!m_directory) { return; } QList suffixes = g_config->getDocSuffixes()[(int)DocType::Markdown]; QString defaultSuf; QString suffixStr; for (auto const & suf : suffixes) { suffixStr += (suffixStr.isEmpty() ? suf : "/" + suf); if (defaultSuf.isEmpty() || suf == "md") { defaultSuf = suf; } } QString info = tr("Create a note in %2.") .arg(g_config->c_dataTextStyle).arg(m_directory->getName()); info = info + "
" + tr("Note with name ending with \"%1\" will be treated as Markdown type.") .arg(suffixStr); QString defaultName = QString("new_note.%1").arg(defaultSuf); defaultName = VUtils::getFileNameWithSequence(m_directory->fetchPath(), defaultName, true); VNewFileDialog dialog(tr("Create Note"), info, defaultName, m_directory, this); if (dialog.exec() == QDialog::Accepted) { VNoteFile *file = m_directory->createFile(dialog.getNameInput(), g_config->getInsertNewNoteInFront()); if (!file) { VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to create note %2.") .arg(g_config->c_dataTextStyle).arg(dialog.getNameInput()), "", QMessageBox::Ok, QMessageBox::Ok, this); return; } // Whether need to move the cursor to the end. bool moveCursorEnd = false; // Content needed to insert into the new file, title/template. QString insertContent; if (dialog.getInsertTitleInput() && file->getDocType() == DocType::Markdown) { // Insert title. insertContent = QString("# %1\n").arg(QFileInfo(file->getName()).completeBaseName()); } if (dialog.isTemplateUsed()) { Q_ASSERT(insertContent.isEmpty()); insertContent = dialog.getTemplate(); } if (!insertContent.isEmpty()) { if (!file->open()) { qWarning() << "fail to open newly-created note" << file->getName(); } else { Q_ASSERT(file->getContent().isEmpty()); file->setContent(insertContent); if (!file->save()) { qWarning() << "fail to write to newly-created note" << file->getName(); } else { if (dialog.getInsertTitleInput()) { moveCursorEnd = true; } } file->close(); } } updateFileList(); locateFile(file); // Open it in edit mode emit fileCreated(file, OpenFileMode::Edit, true); // Move cursor down if content has been inserted. if (moveCursorEnd) { const VMdTab *tab = dynamic_cast(editArea->getCurrentTab()); if (tab) { VMdEditor *edit = tab->getEditor(); if (edit && edit->getFile() == file) { QTextCursor cursor = edit->textCursor(); cursor.movePosition(QTextCursor::End); edit->setTextCursor(cursor); } } } } } void VFileList::deleteSelectedFiles() { QList items = fileList->selectedItems(); Q_ASSERT(!items.isEmpty()); QVector files; for (auto const & item : items) { files.push_back(getVFile(item)); } deleteFiles(files); } // @p_file may or may not be listed in VFileList void VFileList::deleteFile(VNoteFile *p_file) { if (!p_file) { return; } QVector files(1, p_file); deleteFiles(files); } void VFileList::deleteFiles(const QVector &p_files) { if (p_files.isEmpty()) { return; } QVector items; for (auto const & file : p_files) { items.push_back(ConfirmItemInfo(file->getName(), file->fetchPath(), file->fetchPath(), (void *)file)); } QString text = tr("Are you sure to delete these notes?"); QString info = tr("WARNING: " "VNote will delete notes as well as all " "their images and attachments managed by VNote. " "Deleted files could be found in the recycle " "bin of these notes.
" "Click \"Cancel\" to leave them untouched.
" "The operation is IRREVERSIBLE!") .arg(g_config->c_warningTextStyle); VConfirmDeletionDialog dialog(tr("Confirm Deleting Notes"), text, info, items, false, false, false, this); if (dialog.exec()) { items = dialog.getConfirmedItems(); QVector files; for (auto const & item : items) { files.push_back((VNoteFile *)item.m_data); } int nrDeleted = 0; for (auto file : files) { editArea->closeFile(file, true); // Remove the item before deleting it totally, or file will be invalid. removeFileListItem(file); QString errMsg; QString fileName = file->getName(); QString filePath = file->fetchPath(); if (!VNoteFile::deleteFile(file, &errMsg)) { VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to delete note %2.
" "Please check %3 and manually delete it.") .arg(g_config->c_dataTextStyle) .arg(fileName) .arg(filePath), errMsg, QMessageBox::Ok, QMessageBox::Ok, this); } else { Q_ASSERT(errMsg.isEmpty()); ++nrDeleted; } } if (nrDeleted > 0) { g_mainWin->showStatusMessage(tr("%1 %2 deleted") .arg(nrDeleted) .arg(nrDeleted > 1 ? tr("notes") : tr("note"))); updateNumberLabel(); } } } void VFileList::contextMenuRequested(QPoint pos) { if (!m_directory) { return; } QListWidgetItem *item = fileList->itemAt(pos); QMenu menu(this); menu.setToolTipsVisible(true); int selectedSize = fileList->selectedItems().size(); if (item && selectedSize == 1) { VNoteFile *file = getVFile(item); if (file) { if (file->getDocType() == DocType::Markdown) { QAction *openAct = new QAction(VIconUtils::menuIcon(":/resources/icons/editing.svg"), tr("Open"), &menu); openAct->setToolTip(tr("Open and edit current note")); connect(openAct, &QAction::triggered, this, [this]() { QListWidgetItem *item = fileList->currentItem(); if (item) { emit fileClicked(getVFile(item), OpenFileMode::Edit, true); } }); menu.addAction(openAct); } menu.addMenu(getOpenWithMenu()); menu.addSeparator(); } } QAction *newFileAct = new QAction(VIconUtils::menuIcon(":/resources/icons/create_note.svg"), tr("&New Note"), &menu); VUtils::fixTextWithShortcut(newFileAct, "NewNote"); newFileAct->setToolTip(tr("Create a note in current folder")); connect(newFileAct, SIGNAL(triggered(bool)), this, SLOT(newFile())); menu.addAction(newFileAct); if (fileList->count() > 1) { QAction *sortAct = new QAction(VIconUtils::menuIcon(":/resources/icons/sort.svg"), tr("&Sort"), &menu); sortAct->setToolTip(tr("Sort notes in this folder manually")); connect(sortAct, &QAction::triggered, this, &VFileList::sortItems); menu.addAction(sortAct); } if (item) { menu.addSeparator(); QAction *deleteFileAct = new QAction(VIconUtils::menuDangerIcon(":/resources/icons/delete_note.svg"), tr("&Delete"), &menu); deleteFileAct->setToolTip(tr("Delete selected note")); connect(deleteFileAct, SIGNAL(triggered(bool)), this, SLOT(deleteSelectedFiles())); menu.addAction(deleteFileAct); QAction *copyAct = new QAction(VIconUtils::menuIcon(":/resources/icons/copy.svg"), tr("&Copy\t%1").arg(VUtils::getShortcutText(Shortcut::c_copy)), &menu); copyAct->setToolTip(tr("Copy selected notes")); connect(copyAct, &QAction::triggered, this, &VFileList::copySelectedFiles); menu.addAction(copyAct); QAction *cutAct = new QAction(VIconUtils::menuIcon(":/resources/icons/cut.svg"), tr("C&ut\t%1").arg(VUtils::getShortcutText(Shortcut::c_cut)), &menu); cutAct->setToolTip(tr("Cut selected notes")); connect(cutAct, &QAction::triggered, this, &VFileList::cutSelectedFiles); menu.addAction(cutAct); } if (pasteAvailable()) { if (!item) { menu.addSeparator(); } QAction *pasteAct = new QAction(VIconUtils::menuIcon(":/resources/icons/paste.svg"), tr("&Paste\t%1").arg(VUtils::getShortcutText(Shortcut::c_paste)), &menu); pasteAct->setToolTip(tr("Paste notes in current folder")); connect(pasteAct, &QAction::triggered, this, &VFileList::pasteFilesFromClipboard); menu.addAction(pasteAct); } if (item) { menu.addSeparator(); if (selectedSize == 1) { QAction *openLocationAct = new QAction(VIconUtils::menuIcon(":/resources/icons/open_location.svg"), tr("Open Note &Location"), &menu); openLocationAct->setToolTip(tr("Explore the folder containing this note in operating system")); connect(openLocationAct, &QAction::triggered, this, &VFileList::openFileLocation); menu.addAction(openLocationAct); QAction *copyPathAct = new QAction(tr("Copy File &Path"), &menu); connect(copyPathAct, &QAction::triggered, this, [this]() { QList items = fileList->selectedItems(); if (items.size() == 1) { QString filePath = getVFile(items[0])->fetchPath(); QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(filePath); g_mainWin->showStatusMessage(tr("File path copied %1").arg(filePath)); } }); menu.addAction(copyPathAct); } QAction *addToCartAct = new QAction(VIconUtils::menuIcon(":/resources/icons/cart.svg"), tr("Add To Cart"), &menu); addToCartAct->setToolTip(tr("Add selected notes to Cart for further processing")); connect(addToCartAct, &QAction::triggered, this, &VFileList::addFileToCart); menu.addAction(addToCartAct); QAction *pinToHistoryAct = new QAction(VIconUtils::menuIcon(":/resources/icons/pin.svg"), tr("Pin To History"), &menu); pinToHistoryAct->setToolTip(tr("Pin selected notes to History")); connect(pinToHistoryAct, &QAction::triggered, this, &VFileList::pinFileToHistory); menu.addAction(pinToHistoryAct); QAction *quickAccessAct = new QAction(VIconUtils::menuIcon(":/resources/icons/quick_access.svg"), tr("Set As Quick Access"), &menu); quickAccessAct->setToolTip(tr("Set current note as quick access")); connect(quickAccessAct, &QAction::triggered, this, &VFileList::setFileQuickAccess); menu.addAction(quickAccessAct); if (selectedSize == 1) { QAction *fileInfoAct = new QAction(VIconUtils::menuIcon(":/resources/icons/note_info.svg"), tr("&Info (Rename)\t%1").arg(VUtils::getShortcutText(Shortcut::c_info)), &menu); fileInfoAct->setToolTip(tr("View and edit current note's information")); connect(fileInfoAct, SIGNAL(triggered(bool)), this, SLOT(fileInfo())); menu.addAction(fileInfoAct); } } menu.exec(fileList->mapToGlobal(pos)); } QListWidgetItem* VFileList::findItem(const VNoteFile *p_file) { if (!p_file || p_file->getDirectory() != m_directory) { return NULL; } int nrChild = fileList->count(); for (int i = 0; i < nrChild; ++i) { QListWidgetItem *item = fileList->item(i); if (p_file == getVFile(item)) { return item; } } return NULL; } void VFileList::handleItemClicked(QListWidgetItem *p_item) { Q_ASSERT(p_item); Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers(); if (modifiers != Qt::NoModifier) { return; } m_clickTimer->stop(); if (m_itemClicked) { // Timer will not trigger. if (m_itemClicked == p_item) { // Double clicked. m_itemClicked = NULL; m_fileToCloseInSingleClick = NULL; return; } else { // Handle previous clicked item as single click. m_itemClicked = NULL; if (m_fileToCloseInSingleClick) { editArea->closeFile(m_fileToCloseInSingleClick, false); m_fileToCloseInSingleClick = NULL; } } } // Pending @p_item. bool singleClickClose = g_config->getSingleClickClosePreviousTab(); if (singleClickClose) { VFile *file = getVFile(p_item); Q_ASSERT(file); if (editArea->isFileOpened(file)) { // File already opened. activateItem(p_item, true); return; } // EditArea will open Unknown file using system's default program, in which // case we should now close current file even after single click. if (file->getDocType() == DocType::Unknown) { m_fileToCloseInSingleClick = NULL; } else { // Get current tab which will be closed if click timer timeouts. VEditTab *tab = editArea->getCurrentTab(); if (tab) { m_fileToCloseInSingleClick = tab->getFile(); } else { m_fileToCloseInSingleClick = NULL; } } } // Activate it. activateItem(p_item, true); if (singleClickClose) { m_itemClicked = p_item; m_clickTimer->start(); } } void VFileList::showStatusTipAboutItem(QListWidgetItem *p_item) { Q_ASSERT(p_item); const VNoteFile *file = getVFile(p_item); const QStringList &tags = file->getTags(); QString tag; for (int i = 0; i < tags.size(); ++i) { if (i == 0) { tag = "\t[" + tags[i] + "]"; } else { tag += " [" + tags[i] + "]"; } } QString tip(file->getName() + tag); g_mainWin->showStatusMessage(tip); } void VFileList::activateItem(QListWidgetItem *p_item, bool p_restoreFocus) { if (!p_item) { emit fileClicked(NULL); return; } // Qt seems not to update the QListWidget correctly. Manually force it to repaint. fileList->update(); // emit fileClicked(getVFile(p_item), g_config->getNoteOpenMode()); emit fileClicked(getVFile(p_item), OpenFileMode::Edit); if (p_restoreFocus) { fileList->setFocus(); } } bool VFileList::importFiles(const QStringList &p_files, QString *p_errMsg) { if (p_files.isEmpty()) { return false; } bool ret = true; Q_ASSERT(m_directory && m_directory->isOpened()); QString dirPath = m_directory->fetchPath(); QDir dir(dirPath); QVector importedFiles; for (int i = 0; i < p_files.size(); ++i) { const QString &file = p_files[i]; QFileInfo fi(file); if (!fi.exists() || !fi.isFile()) { VUtils::addErrMsg(p_errMsg, tr("Skip importing non-exist file %1.") .arg(file)); ret = false; continue; } QString name = VUtils::fileNameFromPath(file); Q_ASSERT(!name.isEmpty()); bool copyNeeded = true; if (VUtils::equalPath(dirPath, fi.absolutePath())) { // Check if it is already a note. if (m_directory->findFile(name, false)) { VUtils::addErrMsg(p_errMsg, tr("Skip importing file %1. " "A note with the same name (case-insensitive) " "in the same directory already exists.") .arg(file)); ret = false; continue; } qDebug() << "skip cpoy file" << file << "locates in" << dirPath; copyNeeded = false; } QString targetFilePath; if (copyNeeded) { name = VUtils::getFileNameWithSequence(dirPath, name, true); targetFilePath = dir.filePath(name); bool ret = VUtils::copyFile(file, targetFilePath, false); if (!ret) { VUtils::addErrMsg(p_errMsg, tr("Fail to copy file %1 as %2.") .arg(file) .arg(targetFilePath)); ret = false; continue; } } else { targetFilePath = file; } VNoteFile *destFile = m_directory->addFile(name, -1); if (destFile) { importedFiles.append(destFile); qDebug() << "imported" << file << "as" << targetFilePath; } else { VUtils::addErrMsg(p_errMsg, tr("Fail to add the note %1 to target folder's configuration.") .arg(file)); ret = false; continue; } } qDebug() << "imported" << importedFiles.size() << "files"; updateFileList(); selectFiles(importedFiles); return ret; } void VFileList::copySelectedFiles(bool p_isCut) { QList items = fileList->selectedItems(); if (items.isEmpty()) { return; } QJsonArray files; for (int i = 0; i < items.size(); ++i) { VNoteFile *file = getVFile(items[i]); files.append(file->fetchPath()); } QJsonObject clip; clip[ClipboardConfig::c_magic] = getNewMagic(); clip[ClipboardConfig::c_type] = (int)ClipboardOpType::CopyFile; clip[ClipboardConfig::c_isCut] = p_isCut; clip[ClipboardConfig::c_files] = files; QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(QJsonDocument(clip).toJson(QJsonDocument::Compact)); qDebug() << "copied files info" << clipboard->text(); int cnt = files.size(); g_mainWin->showStatusMessage(tr("%1 %2 %3") .arg(cnt) .arg(cnt > 1 ? tr("notes") : tr("note")) .arg(p_isCut ? tr("cut") : tr("copied"))); } void VFileList::cutSelectedFiles() { copySelectedFiles(true); } void VFileList::pasteFilesFromClipboard() { if (!pasteAvailable()) { return; } QJsonObject obj = VUtils::clipboardToJson(); QJsonArray files = obj[ClipboardConfig::c_files].toArray(); bool isCut = obj[ClipboardConfig::c_isCut].toBool(); QVector filesToPaste(files.size()); for (int i = 0; i < files.size(); ++i) { filesToPaste[i] = files[i].toString(); } pasteFiles(m_directory, filesToPaste, isCut); QClipboard *clipboard = QApplication::clipboard(); clipboard->clear(); } void VFileList::pasteFiles(VDirectory *p_destDir, const QVector &p_files, bool p_isCut) { if (!p_destDir || p_files.isEmpty()) { return; } int nrPasted = 0; for (int i = 0; i < p_files.size(); ++i) { VNoteFile *file = g_vnote->getInternalFile(p_files[i]); if (!file) { qWarning() << "Copied file is not an internal note" << p_files[i]; VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to paste note %2.") .arg(g_config->c_dataTextStyle) .arg(p_files[i]), tr("VNote could not find this note in any notebook."), QMessageBox::Ok, QMessageBox::Ok, this); continue; } QString fileName = file->getName(); if (file->getDirectory() == p_destDir) { if (p_isCut) { qDebug() << "skip one note to cut and paste in the same folder" << fileName; continue; } // Copy and paste in the same folder. // We do not allow this if the note contains local images. if (file->getDocType() == DocType::Markdown) { QVector images = VUtils::fetchImagesFromMarkdownFile(file, ImageLink::LocalRelativeInternal); if (!images.isEmpty()) { qDebug() << "skip one note with internal images to copy and paste in the same folder" << fileName; VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to copy note %2.") .arg(g_config->c_dataTextStyle) .arg(p_files[i]), tr("VNote does not allow copy and paste notes with internal images " "in the same folder."), QMessageBox::Ok, QMessageBox::Ok, this); continue; } } // Rename it to xxx_copy.md. fileName = VUtils::generateCopiedFileName(file->fetchBasePath(), fileName, true); } else { // Rename it to xxx_copy.md if needed. fileName = VUtils::generateCopiedFileName(p_destDir->fetchPath(), fileName, true); } QString msg; VNoteFile *destFile = NULL; bool ret = VNoteFile::copyFile(p_destDir, fileName, file, p_isCut, g_config->getInsertNewNoteInFront() ? 0 : -1, &destFile, &msg); if (!ret) { VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to copy note %2.") .arg(g_config->c_dataTextStyle) .arg(p_files[i]), msg, QMessageBox::Ok, QMessageBox::Ok, this); } if (destFile) { ++nrPasted; emit fileUpdated(destFile, p_isCut ? UpdateAction::Moved : UpdateAction::InfoChanged); } } qDebug() << "pasted" << nrPasted << "files"; if (nrPasted > 0) { g_mainWin->showStatusMessage(tr("%1 %2 pasted") .arg(nrPasted) .arg(nrPasted > 1 ? tr("notes") : tr("note"))); } updateFileList(); getNewMagic(); } void VFileList::keyPressEvent(QKeyEvent *p_event) { switch (p_event->key()) { case Qt::Key_Enter: case Qt::Key_Return: { QListWidgetItem *item = fileList->currentItem(); if (item) { VFile *fileToClose = NULL; VFile *file = getVFile(item); Q_ASSERT(file); if (p_event->modifiers() == Qt::NoModifier && g_config->getSingleClickClosePreviousTab() && file->getDocType() != DocType::Unknown) { if (!editArea->isFileOpened(file)) { VEditTab *tab = editArea->getCurrentTab(); if (tab) { fileToClose = tab->getFile(); } } } activateItem(item, false); if (fileToClose) { editArea->closeFile(fileToClose, false); } } break; } default: break; } QWidget::keyPressEvent(p_event); } void VFileList::focusInEvent(QFocusEvent * /* p_event */) { fileList->setFocus(); } bool VFileList::locateFile(const VNoteFile *p_file) { if (p_file) { if (p_file->getDirectory() != m_directory) { return false; } QListWidgetItem *item = findItem(p_file); if (item) { fileList->setCurrentItem(item, QItemSelectionModel::ClearAndSelect); return true; } } return false; } void VFileList::showNavigation() { VNavigationMode::showNavigation(fileList); } bool VFileList::handleKeyNavigation(int p_key, bool &p_succeed) { return VNavigationMode::handleKeyNavigation(fileList, p_key, p_succeed); } int VFileList::getNewMagic() { m_magicForClipboard = (int)QDateTime::currentDateTime().toTime_t(); m_magicForClipboard |= qrand(); return m_magicForClipboard; } bool VFileList::checkMagic(int p_magic) const { return m_magicForClipboard == p_magic; } bool VFileList::pasteAvailable() const { QJsonObject obj = VUtils::clipboardToJson(); if (obj.isEmpty()) { return false; } if (!obj.contains(ClipboardConfig::c_type)) { return false; } ClipboardOpType type = (ClipboardOpType)obj[ClipboardConfig::c_type].toInt(); if (type != ClipboardOpType::CopyFile) { return false; } if (!obj.contains(ClipboardConfig::c_magic) || !obj.contains(ClipboardConfig::c_isCut) || !obj.contains(ClipboardConfig::c_files)) { return false; } int magic = obj[ClipboardConfig::c_magic].toInt(); if (!checkMagic(magic)) { return false; } QJsonArray files = obj[ClipboardConfig::c_files].toArray(); return !files.isEmpty(); } void VFileList::sortItems() { const QVector &files = m_directory->getFiles(); if (files.size() < 2) { return; } VSortDialog dialog(tr("Sort Notes"), tr("Sort notes in folder %2 " "in the configuration file.") .arg(g_config->c_dataTextStyle) .arg(m_directory->getName()), this); QTreeWidget *tree = dialog.getTreeWidget(); tree->clear(); tree->setColumnCount(3); QStringList headers; headers << tr("Name") << tr("Created Time") << tr("Modified Time"); tree->setHeaderLabels(headers); for (int i = 0; i < files.size(); ++i) { const VNoteFile *file = files[i]; QString createdTime = VUtils::displayDateTime(file->getCreatedTimeUtc().toLocalTime(), true); QString modifiedTime = VUtils::displayDateTime(file->getModifiedTimeUtc().toLocalTime(), true); QStringList cols; cols << file->getName() << createdTime << modifiedTime; QTreeWidgetItem *item = new QTreeWidgetItem(tree, cols); item->setData(0, Qt::UserRole, i); } dialog.treeUpdated(); if (dialog.exec()) { QVector data = dialog.getSortedData(); Q_ASSERT(data.size() == files.size()); QVector sortedIdx(data.size(), -1); for (int i = 0; i < data.size(); ++i) { sortedIdx[i] = data[i].toInt(); } qDebug() << "sort files" << sortedIdx; if (!m_directory->sortFiles(sortedIdx)) { VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to sort notes in folder %2.") .arg(g_config->c_dataTextStyle) .arg(m_directory->getName()), "", QMessageBox::Ok, QMessageBox::Ok, this); } updateFileList(); } } QMenu *VFileList::getOpenWithMenu() { if (m_openWithMenu) { return m_openWithMenu; } m_openWithMenu = new QMenu(tr("Open With"), this); m_openWithMenu->setToolTipsVisible(true); auto programs = g_config->getExternalEditors(); for (auto const & pa : programs) { QKeySequence seq = QKeySequence(pa.m_shortcut); QString name = pa.m_name; if (!seq.isEmpty()) { name = QString("%1\t%2").arg(pa.m_name) .arg(VUtils::getShortcutText(pa.m_shortcut)); } QAction *act = new QAction(name, this); act->setToolTip(tr("Open current note with %1").arg(pa.m_name)); act->setStatusTip(pa.m_cmd); act->setData(pa.m_cmd); if (!seq.isEmpty()) { QShortcut *shortcut = new QShortcut(seq, this); shortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(shortcut, &QShortcut::activated, this, [act](){ act->trigger(); }); } connect(act, &QAction::triggered, this, &VFileList::handleOpenWithActionTriggered); m_openWithMenu->addAction(act); } QKeySequence seq(g_config->getShortcutKeySequence("OpenViaDefaultProgram")); QString name = tr("System's Default Program"); if (!seq.isEmpty()) { name = QString("%1\t%2").arg(name) .arg(VUtils::getShortcutText(g_config->getShortcutKeySequence("OpenViaDefaultProgram"))); } QAction *defaultAct = new QAction(name, this); defaultAct->setToolTip(tr("Open current note with system's default program")); connect(defaultAct, &QAction::triggered, this, &VFileList::openCurrentItemViaDefaultProgram); m_openWithMenu->addAction(defaultAct); QAction *addAct = new QAction(VIconUtils::menuIcon(":/resources/icons/add_program.svg"), tr("Add External Program"), this); addAct->setToolTip(tr("Add external program")); connect(addAct, &QAction::triggered, this, [this]() { VTipsDialog dialog(VUtils::getDocFile("tips_external_program.md"), tr("Add External Program"), []() { #if defined(Q_OS_MACOS) || defined(Q_OS_MAC) // On macOS, it seems that we could not open that ini file directly. QUrl url = QUrl::fromLocalFile(g_config->getConfigFolder()); #else QUrl url = QUrl::fromLocalFile(g_config->getConfigFilePath()); #endif QDesktopServices::openUrl(url); }, this); dialog.exec(); }); m_openWithMenu->addAction(addAct); return m_openWithMenu; } void VFileList::handleOpenWithActionTriggered() { QAction *act = static_cast(sender()); QString cmd = act->data().toString(); QListWidgetItem *item = fileList->currentItem(); if (item) { VNoteFile *file = getVFile(item); if (file && (!g_config->getCloseBeforeExternalEditor() || !editArea->isFileOpened(file) || editArea->closeFile(file, false))) { cmd.replace("%0", file->fetchPath()); QProcess *process = new QProcess(this); connect(process, static_cast(&QProcess::finished), process, &QProcess::deleteLater); process->start(cmd); } } } void VFileList::updateNumberLabel() const { int cnt = fileList->count(); m_numLabel->setText(tr("%1 %2").arg(cnt) .arg(cnt > 1 ? tr("Items") : tr("Item"))); } void VFileList::updateViewMenu(QMenu *p_menu) { if (p_menu->isEmpty()) { QActionGroup *ag = new QActionGroup(p_menu); QAction *act = new QAction(tr("View By Configuration File"), ag); act->setCheckable(true); act->setData(ViewOrder::Config); act->setChecked(true); p_menu->addAction(act); act = new QAction(tr("View By Name"), ag); act->setCheckable(true); act->setData(ViewOrder::Name); p_menu->addAction(act); act = new QAction(tr("View By Name (Reverse)"), ag); act->setCheckable(true); act->setData(ViewOrder::NameReverse); p_menu->addAction(act); act = new QAction(tr("View By Created Time"), ag); act->setCheckable(true); act->setData(ViewOrder::CreatedTime); p_menu->addAction(act); act = new QAction(tr("View By Created Time (Reverse)"), ag); act->setCheckable(true); act->setData(ViewOrder::CreatedTimeReverse); p_menu->addAction(act); act = new QAction(tr("View By Modified Time"), ag); act->setCheckable(true); act->setData(ViewOrder::ModifiedTime); p_menu->addAction(act); act = new QAction(tr("View By Modified Time (Reverse)"), ag); act->setCheckable(true); act->setData(ViewOrder::ModifiedTimeReverse); p_menu->addAction(act); int config = g_config->getNoteListViewOrder(); for (auto act : ag->actions()) { if (act->data().toInt() == config) { act->setChecked(true); } } connect(ag, &QActionGroup::triggered, this, [this](QAction *p_action) { int order = p_action->data().toInt(); g_config->setNoteListViewOrder(order); sortFileList((ViewOrder)order); }); } } void VFileList::sortFileList(ViewOrder p_order) { if (!m_directory) { return; } QVector files = m_directory->getFiles(); if (files.isEmpty()) { return; } sortFiles(files, p_order); int cnt = files.size(); Q_ASSERT(cnt == fileList->count()); for (int i = 0; i < cnt; ++i) { int row = -1; for (int j = i; j < cnt; ++j) { QListWidgetItem *item = fileList->item(j); if (files[i] == getVFile(item)) { row = j; break; } } Q_ASSERT(row > -1); QListWidgetItem *item = fileList->takeItem(row); fileList->insertItem(i, item); } } void VFileList::sortFiles(QVector &p_files, ViewOrder p_order) { bool reverse = false; switch (p_order) { case ViewOrder::Config: break; case ViewOrder::NameReverse: reverse = true; V_FALLTHROUGH; case ViewOrder::Name: std::sort(p_files.begin(), p_files.end(), [reverse](const VNoteFile *p_a, const VNoteFile *p_b) { if (reverse) { return p_b->getName() < p_a->getName(); } else { return p_a->getName() < p_b->getName(); } }); break; case ViewOrder::CreatedTimeReverse: reverse = true; V_FALLTHROUGH; case ViewOrder::CreatedTime: std::sort(p_files.begin(), p_files.end(), [reverse](const VNoteFile *p_a, const VNoteFile *p_b) { if (reverse) { return p_b->getCreatedTimeUtc() < p_a->getCreatedTimeUtc(); } else { return p_a->getCreatedTimeUtc() < p_b->getCreatedTimeUtc(); } }); break; case ViewOrder::ModifiedTimeReverse: reverse = true; V_FALLTHROUGH; case ViewOrder::ModifiedTime: std::sort(p_files.begin(), p_files.end(), [reverse](const VNoteFile *p_a, const VNoteFile *p_b) { if (reverse) { return p_b->getModifiedTimeUtc() < p_a->getModifiedTimeUtc(); } else { return p_a->getModifiedTimeUtc() < p_b->getModifiedTimeUtc(); } }); break; default: break; } } void VFileList::selectFiles(const QVector &p_files) { bool first = true; for (auto const & file : p_files) { QListWidgetItem *item = findItem(file); if (item) { if (first) { first = false; fileList->setCurrentItem(item, QItemSelectionModel::ClearAndSelect); } else { item->setSelected(true); } } } } QByteArray VFileList::getMimeData(const QString &p_format, const QList &p_items) const { Q_UNUSED(p_format); Q_ASSERT(p_format ==ClipboardConfig::c_format); QJsonArray files; for (int i = 0; i < p_items.size(); ++i) { VNoteFile *file = getVFile(p_items[i]); files.append(file->fetchPath()); } QJsonObject obj; obj[ClipboardConfig::c_type] = (int)ClipboardOpType::CopyFile; obj[ClipboardConfig::c_files] = files; return QJsonDocument(obj).toJson(QJsonDocument::Compact); } void VFileList::setEnableSplitOut(bool p_enabled) { m_splitBtn->setChecked(p_enabled); } void VFileList::openCurrentItemViaDefaultProgram() { QListWidgetItem *item = fileList->currentItem(); if (item) { VNoteFile *file = getVFile(item); if (file && (!g_config->getCloseBeforeExternalEditor() || !editArea->isFileOpened(file) || editArea->closeFile(file, false))) { QUrl url = QUrl::fromLocalFile(file->fetchPath()); QDesktopServices::openUrl(url); } } }