#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 "vfile.h" #include "vconfigmanager.h" #include "vmdedit.h" extern VConfigManager *g_config; extern VNote *g_vnote; const QString VFileList::c_infoShortcutSequence = "F2"; const QString VFileList::c_copyShortcutSequence = "Ctrl+C"; const QString VFileList::c_cutShortcutSequence = "Ctrl+X"; const QString VFileList::c_pasteShortcutSequence = "Ctrl+V"; VFileList::VFileList(QWidget *parent) : QWidget(parent), VNavigationMode() { setupUI(); initShortcuts(); initActions(); } void VFileList::setupUI() { fileList = new QListWidget(this); fileList->setContextMenuPolicy(Qt::CustomContextMenu); fileList->setSelectionMode(QAbstractItemView::ExtendedSelection); fileList->setDragDropMode(QAbstractItemView::InternalMove); fileList->setObjectName("FileList"); QVBoxLayout *mainLayout = new QVBoxLayout; 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->model(), &QAbstractItemModel::rowsMoved, this, &VFileList::handleRowsMoved); setLayout(mainLayout); } void VFileList::initShortcuts() { QShortcut *infoShortcut = new QShortcut(QKeySequence(c_infoShortcutSequence), this); infoShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(infoShortcut, &QShortcut::activated, this, [this](){ fileInfo(); }); QShortcut *copyShortcut = new QShortcut(QKeySequence(c_copyShortcutSequence), this); copyShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(copyShortcut, &QShortcut::activated, this, [this](){ copySelectedFiles(); }); QShortcut *cutShortcut = new QShortcut(QKeySequence(c_cutShortcutSequence), this); cutShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(cutShortcut, &QShortcut::activated, this, [this](){ cutSelectedFiles(); }); QShortcut *pasteShortcut = new QShortcut(QKeySequence(c_pasteShortcutSequence), this); pasteShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(pasteShortcut, &QShortcut::activated, this, [this](){ pasteFilesInCurDir(); }); } void VFileList::initActions() { newFileAct = new QAction(QIcon(":/resources/icons/create_note.svg"), tr("&New Note"), this); QString shortcutStr = VUtils::getShortcutText(g_config->getShortcutKeySequence("NewNote")); if (!shortcutStr.isEmpty()) { newFileAct->setText(tr("&New Note\t%1").arg(shortcutStr)); } newFileAct->setToolTip(tr("Create a note in current folder")); connect(newFileAct, SIGNAL(triggered(bool)), this, SLOT(newFile())); deleteFileAct = new QAction(QIcon(":/resources/icons/delete_note.svg"), tr("&Delete"), this); deleteFileAct->setToolTip(tr("Delete selected note")); connect(deleteFileAct, SIGNAL(triggered(bool)), this, SLOT(deleteFile())); fileInfoAct = new QAction(QIcon(":/resources/icons/note_info.svg"), tr("&Info\t%1").arg(VUtils::getShortcutText(c_infoShortcutSequence)), this); fileInfoAct->setToolTip(tr("View and edit current note's information")); connect(fileInfoAct, SIGNAL(triggered(bool)), this, SLOT(fileInfo())); copyAct = new QAction(QIcon(":/resources/icons/copy.svg"), tr("&Copy\t%1").arg(VUtils::getShortcutText(c_copyShortcutSequence)), this); copyAct->setToolTip(tr("Copy selected notes")); connect(copyAct, &QAction::triggered, this, &VFileList::copySelectedFiles); cutAct = new QAction(QIcon(":/resources/icons/cut.svg"), tr("C&ut\t%1").arg(VUtils::getShortcutText(c_cutShortcutSequence)), this); cutAct->setToolTip(tr("Cut selected notes")); connect(cutAct, &QAction::triggered, this, &VFileList::cutSelectedFiles); pasteAct = new QAction(QIcon(":/resources/icons/paste.svg"), tr("&Paste\t%1").arg(VUtils::getShortcutText(c_pasteShortcutSequence)), this); pasteAct->setToolTip(tr("Paste notes in current folder")); connect(pasteAct, &QAction::triggered, this, &VFileList::pasteFilesInCurDir); m_openLocationAct = new QAction(tr("&Open Note Location"), this); m_openLocationAct->setToolTip(tr("Open the folder containing this note in operating system")); connect(m_openLocationAct, &QAction::triggered, this, &VFileList::openFileLocation); } 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->clear(); } return; } m_directory = p_directory; if (!m_directory) { fileList->clear(); return; } qDebug() << "filelist set folder" << m_directory->getName(); updateFileList(); } void VFileList::updateFileList() { fileList->clear(); if (!m_directory->open()) { return; } const QVector &files = m_directory->getFiles(); for (int i = 0; i < files.size(); ++i) { VFile *file = files[i]; insertFileListItem(file); } } void VFileList::fileInfo() { QList items = fileList->selectedItems(); if (items.size() == 1) { fileInfo(getVFile(items[0])); } } void VFileList::openFileLocation() const { QListWidgetItem *curItem = fileList->currentItem(); V_ASSERT(curItem); QUrl url = QUrl::fromLocalFile(getVFile(curItem)->fetchBasePath()); QDesktopServices::openUrl(url); } void VFileList::fileInfo(VFile *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 (!promptForDocTypeChange(p_file, QDir(p_file->fetchBasePath()).filePath(name))) { 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); } } void VFileList::fillItem(QListWidgetItem *p_item, const VFile *p_file) { unsigned long long ptr = (long long)p_file; p_item->setData(Qt::UserRole, ptr); p_item->setToolTip(p_file->getName()); p_item->setText(p_file->getName()); V_ASSERT(sizeof(p_file) <= sizeof(ptr)); } QListWidgetItem* VFileList::insertFileListItem(VFile *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(); qDebug() << "VFileList adds" << file->getName(); return item; } void VFileList::removeFileListItem(QListWidgetItem *item) { fileList->setCurrentRow(-1); fileList->removeItemWidget(item); 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); VNewFileDialog dialog(tr("Create Note"), info, defaultName, m_directory, this); if (dialog.exec() == QDialog::Accepted) { VFile *file = m_directory->createFile(dialog.getNameInput()); 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; } // Write title if needed. bool contentInserted = false; if (dialog.getInsertTitleInput() && file->getDocType() == DocType::Markdown) { if (!file->open()) { qWarning() << "fail to open newly-created note" << file->getName(); } else { Q_ASSERT(file->getContent().isEmpty()); QString content = QString("# %1\n").arg(QFileInfo(file->getName()).baseName()); file->setContent(content); if (!file->save()) { qWarning() << "fail to write to newly-created note" << file->getName(); } else { contentInserted = true; } file->close(); } } QVector items = updateFileListAdded(); Q_ASSERT(items.size() == 1); fileList->setCurrentItem(items[0], QItemSelectionModel::ClearAndSelect); // Qt seems not to update the QListWidget correctly. Manually force it to repaint. fileList->update(); // Open it in edit mode emit fileCreated(file, OpenFileMode::Edit); // Move cursor down if content has been inserted. if (contentInserted) { QWidget *wid = QApplication::focusWidget(); VMdEdit *edit = dynamic_cast(wid); if (edit && edit->getFile() == file) { QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier); QCoreApplication::postEvent(edit, downEvent); } } } } QVector VFileList::updateFileListAdded() { QVector ret; const QVector &files = m_directory->getFiles(); for (int i = 0; i < files.size(); ++i) { VFile *file = files[i]; if (i >= fileList->count()) { QListWidgetItem *item = insertFileListItem(file, false); ret.append(item); } else { VFile *itemFile = getVFile(fileList->item(i)); if (itemFile != file) { QListWidgetItem *item = insertFileListItem(file, false); ret.append(item); } } } qDebug() << ret.size() << "items added"; return ret; } // Delete the file related to current item void VFileList::deleteFile() { QList items = fileList->selectedItems(); Q_ASSERT(!items.isEmpty()); for (int i = 0; i < items.size(); ++i) { deleteFile(getVFile(items.at(i))); } } // @p_file may or may not be listed in VFileList void VFileList::deleteFile(VFile *p_file) { if (!p_file) { return; } VDirectory *dir = p_file->getDirectory(); QString fileName = p_file->getName(); int ret = VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Are you sure to delete note %2?") .arg(g_config->c_dataTextStyle).arg(fileName), tr("WARNING: The files (including images) " "deleted may be UNRECOVERABLE!") .arg(g_config->c_warningTextStyle), QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok, this, MessageBoxType::Danger); if (ret == QMessageBox::Ok) { editArea->closeFile(p_file, true); // Remove the item before deleting it totally, or p_file will be invalid. QListWidgetItem *item = findItem(p_file); if (item) { removeFileListItem(item); } dir->deleteFile(p_file); } } void VFileList::contextMenuRequested(QPoint pos) { QListWidgetItem *item = fileList->itemAt(pos); QMenu menu(this); menu.setToolTipsVisible(true); if (!m_directory) { return; } menu.addAction(newFileAct); if (item) { menu.addAction(deleteFileAct); menu.addSeparator(); menu.addAction(copyAct); menu.addAction(cutAct); } if (VUtils::opTypeInClipboard() == ClipboardOpType::CopyFile && !m_copiedFiles.isEmpty()) { if (!item) { menu.addSeparator(); } menu.addAction(pasteAct); } if (item) { menu.addSeparator(); menu.addAction(m_openLocationAct); if (fileList->selectedItems().size() == 1) { menu.addAction(fileInfoAct); } } menu.exec(fileList->mapToGlobal(pos)); } QListWidgetItem* VFileList::findItem(const VFile *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 *currentItem) { Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers(); if (modifiers != Qt::NoModifier) { return; } if (!currentItem) { emit fileClicked(NULL); return; } // Qt seems not to update the QListWidget correctly. Manually force it to repaint. fileList->update(); emit fileClicked(getVFile(currentItem), OpenFileMode::Read); } bool VFileList::importFile(const QString &p_srcFilePath) { if (p_srcFilePath.isEmpty()) { return false; } Q_ASSERT(m_directory); // Copy file @name to current directory QString targetPath = m_directory->fetchPath(); QString srcName = VUtils::fileNameFromPath(p_srcFilePath); if (srcName.isEmpty()) { return false; } QString targetFilePath = QDir(targetPath).filePath(srcName); bool ret = VUtils::copyFile(p_srcFilePath, targetFilePath, false); if (!ret) { return false; } VFile *destFile = m_directory->addFile(srcName, -1); if (destFile) { return insertFileListItem(destFile, false); } return false; } void VFileList::copySelectedFiles(bool p_isCut) { QList items = fileList->selectedItems(); if (items.isEmpty()) { return; } QJsonArray files; m_copiedFiles.clear(); for (int i = 0; i < items.size(); ++i) { VFile *file = getVFile(items[i]); QJsonObject fileJson; fileJson["notebook"] = file->getNotebookName(); fileJson["path"] = file->fetchPath(); files.append(fileJson); m_copiedFiles.append(file); } copyFileInfoToClipboard(files, p_isCut); } void VFileList::cutSelectedFiles() { copySelectedFiles(true); } void VFileList::copyFileInfoToClipboard(const QJsonArray &p_files, bool p_isCut) { QJsonObject clip; clip["operation"] = (int)ClipboardOpType::CopyFile; clip["is_cut"] = p_isCut; clip["sources"] = p_files; QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(QJsonDocument(clip).toJson(QJsonDocument::Compact)); } void VFileList::pasteFilesInCurDir() { if (m_copiedFiles.isEmpty()) { return; } pasteFiles(m_directory); } void VFileList::pasteFiles(VDirectory *p_destDir) { qDebug() << "paste files to" << p_destDir->getName(); QClipboard *clipboard = QApplication::clipboard(); QString text = clipboard->text(); QJsonObject clip = QJsonDocument::fromJson(text.toLocal8Bit()).object(); Q_ASSERT(!clip.isEmpty() && clip["operation"] == (int)ClipboardOpType::CopyFile); bool isCut = clip["is_cut"].toBool(); int nrPasted = 0; for (int i = 0; i < m_copiedFiles.size(); ++i) { QPointer srcFile = m_copiedFiles[i]; if (!srcFile) { continue; } QString fileName = srcFile->getName(); VDirectory *srcDir = srcFile->getDirectory(); if (srcDir == p_destDir && !isCut) { // Copy and paste in the same directory. // Rename it to xx_copy.md fileName = VUtils::generateCopiedFileName(srcDir->fetchPath(), fileName); } if (copyFile(p_destDir, fileName, srcFile, isCut)) { nrPasted++; } else { VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to copy note %2.") .arg(g_config->c_dataTextStyle).arg(srcFile->getName()), tr("Please check if there already exists a file with the same name in the target folder."), QMessageBox::Ok, QMessageBox::Ok, this); } } qDebug() << "pasted" << nrPasted << "files sucessfully"; clipboard->clear(); m_copiedFiles.clear(); } bool VFileList::copyFile(VDirectory *p_destDir, const QString &p_destName, VFile *p_file, bool p_cut) { QString srcPath = QDir::cleanPath(p_file->fetchPath()); QString destPath = QDir::cleanPath(QDir(p_destDir->fetchPath()).filePath(p_destName)); if (VUtils::equalPath(srcPath, destPath)) { return true; } // If change the file type, we need to close it first if (!promptForDocTypeChange(p_file, destPath)) { return false; } VFile *destFile = VDirectory::copyFile(p_destDir, p_destName, p_file, p_cut); updateFileList(); if (destFile) { emit fileUpdated(destFile); } return destFile != NULL; } bool VFileList::promptForDocTypeChange(const VFile *p_file, const QString &p_newFilePath) { DocType docType = p_file->getDocType(); DocType newDocType = VUtils::docTypeFromName(p_newFilePath); if (docType != newDocType) { if (editArea->isFileOpened(p_file)) { int ret = VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("The renaming will change the note type."), tr("You should close the note %2 before continue.") .arg(g_config->c_dataTextStyle).arg(p_file->getName()), QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok, this); if (QMessageBox::Ok == ret) { if (!editArea->closeFile(p_file, false)) { return false; } } else { return false; } } } return true; } void VFileList::keyPressEvent(QKeyEvent *event) { int key = event->key(); int modifiers = event->modifiers(); switch (key) { case Qt::Key_Return: { QListWidgetItem *item = fileList->currentItem(); if (item) { handleItemClicked(item); } break; } case Qt::Key_J: { if (modifiers == Qt::ControlModifier) { event->accept(); QKeyEvent *downEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier); QCoreApplication::postEvent(fileList, downEvent); return; } break; } case Qt::Key_K: { if (modifiers == Qt::ControlModifier) { event->accept(); QKeyEvent *upEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier); QCoreApplication::postEvent(fileList, upEvent); return; } break; } default: break; } QWidget::keyPressEvent(event); } void VFileList::focusInEvent(QFocusEvent * /* p_event */) { fileList->setFocus(); } bool VFileList::locateFile(const VFile *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::handleRowsMoved(const QModelIndex &p_parent, int p_start, int p_end, const QModelIndex &p_destination, int p_row) { if (p_parent == p_destination) { // Items[p_start, p_end] are moved to p_row. m_directory->reorderFiles(p_start, p_end, p_row); Q_ASSERT(identicalListWithDirectory()); } } bool VFileList::identicalListWithDirectory() const { const QVector files = m_directory->getFiles(); int nrItems = fileList->count(); if (nrItems != files.size()) { return false; } for (int i = 0; i < nrItems; ++i) { if (getVFile(fileList->item(i)) != files.at(i)) { return false; } } return true; } void VFileList::registerNavigation(QChar p_majorKey) { m_majorKey = p_majorKey; V_ASSERT(m_keyMap.empty()); V_ASSERT(m_naviLabels.empty()); } void VFileList::showNavigation() { // Clean up. m_keyMap.clear(); for (auto label : m_naviLabels) { delete label; } m_naviLabels.clear(); if (!isVisible()) { return; } // Generate labels for visible items. auto items = getVisibleItems(); int itemWidth = rect().width(); for (int i = 0; i < 26 && i < items.size(); ++i) { QChar key('a' + i); m_keyMap[key] = items[i]; QString str = QString(m_majorKey) + key; QLabel *label = new QLabel(str, this); label->setStyleSheet(g_vnote->getNavigationLabelStyle(str)); label->show(); QRect rect = fileList->visualItemRect(items[i]); // Display the label at the end to show the file name. label->move(rect.x() + itemWidth - label->width(), rect.y()); m_naviLabels.append(label); } } void VFileList::hideNavigation() { m_keyMap.clear(); for (auto label : m_naviLabels) { delete label; } m_naviLabels.clear(); } bool VFileList::handleKeyNavigation(int p_key, bool &p_succeed) { static bool secondKey = false; bool ret = false; p_succeed = false; QChar keyChar = VUtils::keyToChar(p_key); if (secondKey && !keyChar.isNull()) { secondKey = false; p_succeed = true; ret = true; auto it = m_keyMap.find(keyChar); if (it != m_keyMap.end()) { fileList->setCurrentItem(it.value(), QItemSelectionModel::ClearAndSelect); fileList->setFocus(); } } else if (keyChar == m_majorKey) { // Major key pressed. // Need second key if m_keyMap is not empty. if (m_keyMap.isEmpty()) { p_succeed = true; } else { secondKey = true; } ret = true; } return ret; } QList VFileList::getVisibleItems() const { QList items; for (int i = 0; i < fileList->count(); ++i) { QListWidgetItem *item = fileList->item(i); if (!item->isHidden()) { items.append(item); } } return items; }