From 58e7cdca4bf564eb2a47e6060f8ebb0835ceb1af Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 29 Sep 2017 19:56:38 +0800 Subject: [PATCH] refactor VFileList - Refine note deletion logics; - Refine note copy/paste logics; - Refine note sorting logics; --- src/dialog/vconfirmdeletiondialog.cpp | 143 ++++--- src/dialog/vconfirmdeletiondialog.h | 67 +++- src/dialog/vsortdialog.cpp | 66 +++- src/dialog/vsortdialog.h | 27 +- src/utils/vutils.cpp | 77 +++- src/utils/vutils.h | 24 +- src/vattachmentlist.cpp | 60 ++- src/vconstants.h | 11 +- src/vdirectory.cpp | 186 +-------- src/vdirectory.h | 20 +- src/vdirectorytree.cpp | 4 +- src/vfilelist.cpp | 536 +++++++++++++++++++------- src/vfilelist.h | 89 ++++- src/vmainwindow.cpp | 32 +- src/vmdedit.cpp | 32 +- src/vnotefile.cpp | 223 ++++++++++- src/vnotefile.h | 53 ++- 17 files changed, 1150 insertions(+), 500 deletions(-) diff --git a/src/dialog/vconfirmdeletiondialog.cpp b/src/dialog/vconfirmdeletiondialog.cpp index 8703e83b..adeccd06 100644 --- a/src/dialog/vconfirmdeletiondialog.cpp +++ b/src/dialog/vconfirmdeletiondialog.cpp @@ -7,55 +7,52 @@ extern VConfigManager *g_config; -class ConfirmItemWidget : public QWidget +ConfirmItemWidget::ConfirmItemWidget(bool p_checked, + const QString &p_file, + const QString &p_tip, + int p_index, + QWidget *p_parent) + : QWidget(p_parent), m_index(p_index) { -public: - explicit ConfirmItemWidget(QWidget *p_parent = NULL) - : QWidget(p_parent) - { - setupUI(); + setupUI(); + + m_checkBox->setChecked(p_checked); + m_fileLabel->setText(p_file); + if (!p_tip.isEmpty()) { + m_fileLabel->setToolTip(p_tip); } +} - ConfirmItemWidget(bool p_checked, const QString &p_file, QWidget *p_parent = NULL) - : QWidget(p_parent) - { - setupUI(); +void ConfirmItemWidget::setupUI() +{ + m_checkBox = new QCheckBox; + connect(m_checkBox, &QCheckBox::stateChanged, + this, &ConfirmItemWidget::checkStateChanged); - m_checkBox->setChecked(p_checked); - m_fileLabel->setText(p_file); - } + m_fileLabel = new QLabel; + QHBoxLayout *mainLayout = new QHBoxLayout; + mainLayout->addWidget(m_checkBox); + mainLayout->addWidget(m_fileLabel); + mainLayout->addStretch(); + mainLayout->setContentsMargins(3, 0, 0, 0); - bool isChecked() const - { - return m_checkBox->isChecked(); - } + setLayout(mainLayout); +} - QString getFile() const - { - return m_fileLabel->text(); - } +bool ConfirmItemWidget::isChecked() const +{ + return m_checkBox->isChecked(); +} -private: - void setupUI() - { - m_checkBox = new QCheckBox; - m_fileLabel = new QLabel; - QHBoxLayout *mainLayout = new QHBoxLayout; - mainLayout->addWidget(m_checkBox); - mainLayout->addWidget(m_fileLabel); - mainLayout->addStretch(); - mainLayout->setContentsMargins(3, 0, 0, 0); - - setLayout(mainLayout); - } - - QCheckBox *m_checkBox; - QLabel *m_fileLabel; -}; +int ConfirmItemWidget::getIndex() const +{ + return m_index; +} VConfirmDeletionDialog::VConfirmDeletionDialog(const QString &p_title, + const QString &p_text, const QString &p_info, - const QVector &p_files, + const QVector &p_items, bool p_enableAskAgain, bool p_askAgainEnabled, bool p_enablePreview, @@ -63,21 +60,38 @@ VConfirmDeletionDialog::VConfirmDeletionDialog(const QString &p_title, : QDialog(p_parent), m_enableAskAgain(p_enableAskAgain), m_askAgainEnabled(p_askAgainEnabled), - m_enablePreview(p_enablePreview) + m_enablePreview(p_enablePreview), + m_items(p_items) { - setupUI(p_title, p_info); + setupUI(p_title, p_text, p_info); - initFileItems(p_files); + initItems(); + + updateCountLabel(); } -void VConfirmDeletionDialog::setupUI(const QString &p_title, const QString &p_info) +void VConfirmDeletionDialog::setupUI(const QString &p_title, + const QString &p_text, + const QString &p_info) { + QLabel *textLabel = NULL; + if (!p_text.isEmpty()) { + textLabel = new QLabel(p_text); + textLabel->setWordWrap(true); + } + QLabel *infoLabel = NULL; if (!p_info.isEmpty()) { infoLabel = new QLabel(p_info); infoLabel->setWordWrap(true); } + m_countLabel = new QLabel("Items"); + QHBoxLayout *labelLayout = new QHBoxLayout; + labelLayout->addWidget(m_countLabel); + labelLayout->addStretch(); + labelLayout->setContentsMargins(0, 0, 0, 0); + m_listWidget = new QListWidget(); connect(m_listWidget, &QListWidget::currentRowChanged, this, &VConfirmDeletionDialog::currentFileChanged); @@ -106,40 +120,50 @@ void VConfirmDeletionDialog::setupUI(const QString &p_title, const QString &p_in } QVBoxLayout *mainLayout = new QVBoxLayout; + if (textLabel) { + mainLayout->addWidget(textLabel); + } + if (infoLabel) { mainLayout->addWidget(infoLabel); } mainLayout->addWidget(m_askAgainCB); mainLayout->addWidget(m_btnBox); + mainLayout->addLayout(labelLayout); mainLayout->addLayout(midLayout); setLayout(mainLayout); setWindowTitle(p_title); } -QVector VConfirmDeletionDialog::getConfirmedFiles() const +QVector VConfirmDeletionDialog::getConfirmedItems() const { - QVector files; + QVector confirmedItems; for (int i = 0; i < m_listWidget->count(); ++i) { ConfirmItemWidget *widget = getItemWidget(m_listWidget->item(i)); if (widget->isChecked()) { - files.push_back(widget->getFile()); + confirmedItems.push_back(m_items[widget->getIndex()]); } } - return files; + return confirmedItems; } -void VConfirmDeletionDialog::initFileItems(const QVector &p_files) +void VConfirmDeletionDialog::initItems() { m_listWidget->clear(); - for (int i = 0; i < p_files.size(); ++i) { + for (int i = 0; i < m_items.size(); ++i) { ConfirmItemWidget *itemWidget = new ConfirmItemWidget(true, - p_files[i], + m_items[i].m_name, + m_items[i].m_tip, + i, this); + connect(itemWidget, &ConfirmItemWidget::checkStateChanged, + this, &VConfirmDeletionDialog::updateCountLabel); + QListWidgetItem *item = new QListWidgetItem(); QSize size = itemWidget->sizeHint(); size.setHeight(size.height() * 2); @@ -165,7 +189,9 @@ void VConfirmDeletionDialog::currentFileChanged(int p_row) if (p_row > -1 && m_enablePreview) { ConfirmItemWidget *widget = getItemWidget(m_listWidget->item(p_row)); if (widget) { - QPixmap image(widget->getFile()); + int idx = widget->getIndex(); + Q_ASSERT(idx < m_items.size()); + QPixmap image(m_items[idx].m_path); if (!image.isNull()) { int width = 512 * VUtils::calculateScaleFactor(); QSize previewSize(width, width); @@ -183,3 +209,18 @@ ConfirmItemWidget *VConfirmDeletionDialog::getItemWidget(QListWidgetItem *p_item QWidget *wid = m_listWidget->itemWidget(p_item); return dynamic_cast(wid); } + +void VConfirmDeletionDialog::updateCountLabel() +{ + int total = m_listWidget->count(); + int checked = 0; + + for (int i = 0; i < total; ++i) { + ConfirmItemWidget *widget = getItemWidget(m_listWidget->item(i)); + if (widget->isChecked()) { + ++checked; + } + } + + m_countLabel->setText(tr("%1/%2 Items").arg(checked).arg(total)); +} diff --git a/src/dialog/vconfirmdeletiondialog.h b/src/dialog/vconfirmdeletiondialog.h index d78c5985..d67d74b4 100644 --- a/src/dialog/vconfirmdeletiondialog.h +++ b/src/dialog/vconfirmdeletiondialog.h @@ -12,32 +12,87 @@ class QListWidgetItem; class ConfirmItemWidget; class QCheckBox; +// Information about a deletion item needed to confirm. +struct ConfirmItemInfo +{ + ConfirmItemInfo() + : m_data(NULL) + { + } + + ConfirmItemInfo(const QString &p_name, + const QString &p_tip, + const QString &p_path, + void *p_data) + : m_name(p_name), m_tip(p_tip), m_path(p_path), m_data(p_data) + { + } + + QString m_name; + QString m_tip; + QString m_path; + void *m_data; +}; + +class ConfirmItemWidget : public QWidget +{ + Q_OBJECT +public: + ConfirmItemWidget(bool p_checked, + const QString &p_file, + const QString &p_tip, + int p_index, + QWidget *p_parent = NULL); + + bool isChecked() const; + + int getIndex() const; + +signals: + // Emit when item's check state changed. + void checkStateChanged(int p_state); + +private: + void setupUI(); + + QCheckBox *m_checkBox; + QLabel *m_fileLabel; + + int m_index; +}; + class VConfirmDeletionDialog : public QDialog { Q_OBJECT public: VConfirmDeletionDialog(const QString &p_title, + const QString &p_text, const QString &p_info, - const QVector &p_files, + const QVector &p_items, bool p_enableAskAgain, bool p_askAgainEnabled, bool p_enablePreview, QWidget *p_parent = 0); - QVector getConfirmedFiles() const; + QVector getConfirmedItems() const; bool getAskAgainEnabled() const; private slots: void currentFileChanged(int p_row); -private: - void setupUI(const QString &p_title, const QString &p_info); + void updateCountLabel(); - void initFileItems(const QVector &p_files); +private: + void setupUI(const QString &p_title, + const QString &p_text, + const QString &p_info); + + void initItems(); ConfirmItemWidget *getItemWidget(QListWidgetItem *p_item) const; + QLabel *m_countLabel; QListWidget *m_listWidget; QLabel *m_previewer; QDialogButtonBox *m_btnBox; @@ -48,6 +103,8 @@ private: bool m_askAgainEnabled; bool m_enablePreview; + + QVector m_items; }; #endif // VCONFIRMDELETIONDIALOG_H diff --git a/src/dialog/vsortdialog.cpp b/src/dialog/vsortdialog.cpp index f01149f4..9cf4854a 100644 --- a/src/dialog/vsortdialog.cpp +++ b/src/dialog/vsortdialog.cpp @@ -2,6 +2,33 @@ #include +void VTreeWidget::dropEvent(QDropEvent *p_event) +{ + QList dragItems = selectedItems(); + + int first = -1, last = -1; + QTreeWidgetItem *firstItem = NULL; + for (int i = 0; i < dragItems.size(); ++i) { + int row = indexFromItem(dragItems[i]).row(); + if (row > last) { + last = row; + } + + if (first == -1 || row < first) { + first = row; + firstItem = dragItems[i]; + } + } + + Q_ASSERT(firstItem); + + QTreeWidget::dropEvent(p_event); + + int target = indexFromItem(firstItem).row(); + emit rowsMoved(first, last, target); +} + + VSortDialog::VSortDialog(const QString &p_title, const QString &p_info, QWidget *p_parent) @@ -18,10 +45,28 @@ void VSortDialog::setupUI(const QString &p_title, const QString &p_info) infoLabel->setWordWrap(true); } - m_treeWidget = new QTreeWidget(); + m_treeWidget = new VTreeWidget(); m_treeWidget->setRootIsDecorated(false); m_treeWidget->setSelectionMode(QAbstractItemView::ContiguousSelection); m_treeWidget->setDragDropMode(QAbstractItemView::InternalMove); + connect(m_treeWidget, &VTreeWidget::rowsMoved, + this, [this](int p_first, int p_last, int p_row) { + Q_UNUSED(p_first); + Q_UNUSED(p_last); + QTreeWidgetItem *item = m_treeWidget->topLevelItem(p_row); + if (item) { + m_treeWidget->setCurrentItem(item); + + // Select all items back. + int cnt = p_last - p_first + 1; + for (int i = 0; i < cnt; ++i) { + QTreeWidgetItem *it = m_treeWidget->topLevelItem(p_row + i); + if (it) { + it->setSelected(true); + } + } + } + }); // Buttons for top/up/down/bottom. m_topBtn = new QPushButton(tr("&Top")); @@ -82,6 +127,11 @@ void VSortDialog::setupUI(const QString &p_title, const QString &p_info) void VSortDialog::treeUpdated() { + int cols = m_treeWidget->columnCount(); + for (int i = 0; i < cols; ++i) { + m_treeWidget->resizeColumnToContents(i); + } + // We just need single level. int cnt = m_treeWidget->topLevelItemCount(); for (int i = 0; i < cnt; ++i) { @@ -200,6 +250,20 @@ void VSortDialog::handleMoveOperation(MoveOperation p_op) } if (firstItem) { + m_treeWidget->setCurrentItem(firstItem); m_treeWidget->scrollToItem(firstItem); } } + +QVector VSortDialog::getSortedData() const +{ + int cnt = m_treeWidget->topLevelItemCount(); + QVector data(cnt); + for (int i = 0; i < cnt; ++i) { + QTreeWidgetItem *item = m_treeWidget->topLevelItem(i); + Q_ASSERT(item); + data[i] = item->data(0, Qt::UserRole); + } + + return data; +} diff --git a/src/dialog/vsortdialog.h b/src/dialog/vsortdialog.h index 975ad00c..5c3e448f 100644 --- a/src/dialog/vsortdialog.h +++ b/src/dialog/vsortdialog.h @@ -3,10 +3,32 @@ #include #include +#include class QPushButton; class QDialogButtonBox; class QTreeWidget; +class QDropEvent; + +// QTreeWidget won't emit the rowsMoved() signal after drag-and-drop. +// VTreeWidget will emit rowsMoved() signal. +class VTreeWidget : public QTreeWidget +{ + Q_OBJECT +public: + explicit VTreeWidget(QWidget *p_parent = 0) + : QTreeWidget(p_parent) + { + } + +protected: + void dropEvent(QDropEvent *p_event) Q_DECL_OVERRIDE; + +signals: + // Rows [@p_first, @p_last] were moved to @p_row. + void rowsMoved(int p_first, int p_last, int p_row); + +}; class VSortDialog : public QDialog { @@ -21,6 +43,9 @@ public: // Called after updating the m_treeWidget. void treeUpdated(); + // Get user data of column 0 from sorted items. + QVector getSortedData() const; + private: enum MoveOperation { Top, Up, Down, Bottom }; @@ -30,7 +55,7 @@ private slots: private: void setupUI(const QString &p_title, const QString &p_info); - QTreeWidget *m_treeWidget; + VTreeWidget *m_treeWidget; QPushButton *m_topBtn; QPushButton *m_upBtn; QPushButton *m_downBtn; diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 84105465..4a22144a 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -250,18 +250,28 @@ bool VUtils::makePath(const QString &p_path) return ret; } -ClipboardOpType VUtils::opTypeInClipboard() +QJsonObject VUtils::clipboardToJson() { QClipboard *clipboard = QApplication::clipboard(); const QMimeData *mimeData = clipboard->mimeData(); + QJsonObject obj; if (mimeData->hasText()) { QString text = mimeData->text(); - QJsonObject clip = QJsonDocument::fromJson(text.toLocal8Bit()).object(); - if (clip.contains("operation")) { - return (ClipboardOpType)clip["operation"].toInt(); - } + obj = QJsonDocument::fromJson(text.toUtf8()).object(); + qDebug() << "Json object in clipboard" << obj; } + + return obj; +} + +ClipboardOpType VUtils::operationInClipboard() +{ + QJsonObject obj = clipboardToJson(); + if (obj.contains(ClipboardConfig::c_type)) { + return (ClipboardOpType)obj[ClipboardConfig::c_type].toInt(); + } + return ClipboardOpType::Invalid; } @@ -274,6 +284,12 @@ bool VUtils::copyFile(const QString &p_srcFilePath, const QString &p_destFilePat return true; } + QDir dir; + if (!dir.mkpath(basePathFromPath(p_destFilePath))) { + qWarning() << "fail to create directory" << basePathFromPath(p_destFilePath); + return false; + } + if (p_isCut) { QFile file(srcPath); if (!file.rename(destPath)) { @@ -286,10 +302,10 @@ bool VUtils::copyFile(const QString &p_srcFilePath, const QString &p_destFilePat return false; } } + return true; } -// Copy @p_srcDirPath to be @p_destDirPath. bool VUtils::copyDirectory(const QString &p_srcDirPath, const QString &p_destDirPath, bool p_isCut) { QString srcPath = QDir::cleanPath(p_srcDirPath); @@ -298,17 +314,24 @@ bool VUtils::copyDirectory(const QString &p_srcDirPath, const QString &p_destDir return true; } - // Make a directory - QDir parentDir(VUtils::basePathFromPath(p_destDirPath)); - QString dirName = VUtils::fileNameFromPath(p_destDirPath); - if (!parentDir.mkdir(dirName)) { - qWarning() << QString("fail to create target directory %1: already exists").arg(p_destDirPath); + if (QFileInfo::exists(destPath)) { + qWarning() << QString("target directory %1 already exists").arg(destPath); return false; } - // Handle sub-dirs recursively and copy files. - QDir srcDir(p_srcDirPath); - QDir destDir(p_destDirPath); + // QDir.rename() could not move directory across drives. + + // Make sure target directory exists. + QDir destDir(destPath); + if (!destDir.exists()) { + if (!destDir.mkpath(destPath)) { + qWarning() << QString("fail to create target directory %1").arg(destPath); + return false; + } + } + + // Handle directory recursively. + QDir srcDir(srcPath); Q_ASSERT(srcDir.exists() && destDir.exists()); QFileInfoList nodes = srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoSymLinks | QDir::NoDotAndDotDot); @@ -327,13 +350,13 @@ bool VUtils::copyDirectory(const QString &p_srcDirPath, const QString &p_destDir } } - // Delete the src dir if p_isCut if (p_isCut) { - if (!srcDir.removeRecursively()) { - qWarning() << "fail to remove directory" << p_srcDirPath; + if (!destDir.rmdir(srcPath)) { + qWarning() << QString("fail to delete source directory %1 after cut").arg(srcPath); return false; } } + return true; } @@ -364,19 +387,20 @@ QString VUtils::generateCopiedFileName(const QString &p_dirPath, const QString & suffix = p_fileName.right(p_fileName.size() - dotIdx); base = p_fileName.left(dotIdx); } + QDir dir(p_dirPath); QString name = p_fileName; - QString filePath = dir.filePath(name); int index = 0; - while (QFile(filePath).exists()) { + while (dir.exists(name)) { QString seq; if (index > 0) { seq = QString::number(index); } + index++; name = QString("%1_copy%2%3").arg(base).arg(seq).arg(suffix); - filePath = dir.filePath(name); } + return name; } @@ -898,3 +922,16 @@ bool VUtils::fileExists(const QDir &p_dir, const QString &p_name, bool p_forceCa return false; } + +void VUtils::addErrMsg(QString *p_msg, const QString &p_str) +{ + if (!p_msg) { + return; + } + + if (p_msg->isEmpty()) { + *p_msg = p_str; + } else { + *p_msg = *p_msg + '\n' + p_str; + } +} diff --git a/src/utils/vutils.h b/src/utils/vutils.h index ec02e841..d0886dde 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -51,7 +51,11 @@ public: static QRgb QRgbFromString(const QString &str); static QString generateImageFileName(const QString &path, const QString &title, const QString &format = "png"); + + // Given the file name @p_fileName and directory path @p_dirPath, generate + // a file name based on @p_fileName which does not exist in @p_dirPath. static QString generateCopiedFileName(const QString &p_dirPath, const QString &p_fileName); + static QString generateCopiedDirName(const QString &p_parentDirPath, const QString &p_dirName); static void processStyle(QString &style, const QVector > &varMap); @@ -76,9 +80,24 @@ public: // @p_path could be /home/tamlok/abc, /home/tamlok/abc/. static bool makePath(const QString &p_path); - static ClipboardOpType opTypeInClipboard(); + // Return QJsonObject if there is valid Json string in clipboard. + // Return empty object if there is no valid Json string. + static QJsonObject clipboardToJson(); + + // Get the operation type in system's clipboard. + static ClipboardOpType operationInClipboard(); + + static ClipboardOpType opTypeInClipboard() { return ClipboardOpType::Invalid; } + + // Copy file @p_srcFilePath to @p_destFilePath. + // Will make necessary parent directory along the destination path. static bool copyFile(const QString &p_srcFilePath, const QString &p_destFilePath, bool p_isCut); + + // Copy @p_srcDirPath to be @p_destDirPath. + // @p_destDirPath should not exist. + // Will make necessary parent directory along the destination path. static bool copyDirectory(const QString &p_srcDirPath, const QString &p_destDirPath, bool p_isCut); + static int showMessage(QMessageBox::Icon p_icon, const QString &p_title, const QString &p_text, const QString &p_infoText, QMessageBox::StandardButtons p_buttons, QMessageBox::StandardButton p_defaultBtn, QWidget *p_parent, @@ -164,6 +183,9 @@ public: // @p_forceCaseInsensitive: if true, will check the name ignoring the case. static bool fileExists(const QDir &p_dir, const QString &p_name, bool p_forceCaseInsensitive = false); + // Assign @p_str to @p_msg if it is not NULL. + static void addErrMsg(QString *p_msg, const QString &p_str); + // Regular expression for image link. // ![image title]( http://github.com/tamlok/vnote.jpg "alt \" text" ) // Captured texts (need to be trimmed): diff --git a/src/vattachmentlist.cpp b/src/vattachmentlist.cpp index 504e2c6f..f559753b 100644 --- a/src/vattachmentlist.cpp +++ b/src/vattachmentlist.cpp @@ -242,7 +242,7 @@ void VAttachmentList::addAttachments(const QStringList &p_files) } if (addedFiles > 0) { - g_vnote->getMainWindow()->showStatusMessage(tr("Added %1 %2 as attachments") + g_vnote->getMainWindow()->showStatusMessage(tr("%1 %2 added as attachments") .arg(addedFiles) .arg(addedFiles > 1 ? tr("files") : tr("file"))); } @@ -300,7 +300,7 @@ void VAttachmentList::handleItemActivated(QListWidgetItem *p_item) void VAttachmentList::deleteSelectedItems() { - QVector names; + QVector items; const QList selectedItems = m_attachmentList->selectedItems(); if (selectedItems.isEmpty()) { @@ -308,24 +308,35 @@ void VAttachmentList::deleteSelectedItems() } for (auto const & item : selectedItems) { - names.push_back(item->text()); + items.push_back(ConfirmItemInfo(item->text(), + item->text(), + "", + NULL)); } - QString info = tr("Are you sure to delete these attachments of note " - "%2? " - "You could find deleted files in the recycle " - "bin of this notebook.
" - "Click \"Cancel\" to leave them untouched.") + QString text = tr("Are you sure to delete these attachments of note " + "%2?") .arg(g_config->c_dataTextStyle).arg(m_file->getName()); + + QString info = tr("You could find deleted files in the recycle " + "bin of this note.
" + "Click \"Cancel\" to leave them untouched."); + VConfirmDeletionDialog dialog(tr("Confirm Deleting Attachments"), + text, info, - names, + items, false, false, false, g_vnote->getMainWindow()); if (dialog.exec()) { - names = dialog.getConfirmedFiles(); + items = dialog.getConfirmedItems(); + + QVector names; + for (auto const & item : items) { + names.push_back(item.m_name); + } if (!m_file->deleteAttachments(names)) { VUtils::showMessage(QMessageBox::Warning, @@ -353,7 +364,10 @@ void VAttachmentList::sortItems() } VSortDialog dialog(tr("Sort Attachments"), - tr("Sort attachments in the configuration file."), + tr("Sort attachments of note %2 " + "in the configuration file.") + .arg(g_config->c_dataTextStyle) + .arg(m_file->getName()), g_vnote->getMainWindow()); QTreeWidget *tree = dialog.getTreeWidget(); tree->clear(); @@ -372,16 +386,24 @@ void VAttachmentList::sortItems() dialog.treeUpdated(); if (dialog.exec()) { - int cnt = tree->topLevelItemCount(); - Q_ASSERT(cnt == attas.size()); - QVector sortedIdx(cnt, -1); - for (int i = 0; i < cnt; ++i) { - QTreeWidgetItem *item = tree->topLevelItem(i); - Q_ASSERT(item); - sortedIdx[i] = item->data(0, Qt::UserRole).toInt(); + QVector data = dialog.getSortedData(); + Q_ASSERT(data.size() == attas.size()); + QVector sortedIdx(data.size(), -1); + for (int i = 0; i < data.size(); ++i) { + sortedIdx[i] = data[i].toInt(); } - m_file->sortAttachments(sortedIdx); + if (!m_file->sortAttachments(sortedIdx)) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to sort attachments of note %2.") + .arg(g_config->c_dataTextStyle) + .arg(m_file->getName()), + "", + QMessageBox::Ok, + QMessageBox::Ok, + this); + } } } diff --git a/src/vconstants.h b/src/vconstants.h index 305b7c07..6ee62039 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -11,7 +11,16 @@ enum class DocType { Html = 0, Markdown, List, Container, Unknown }; // Orphan: external file; enum class FileType { Note, Orphan }; -enum class ClipboardOpType { Invalid, CopyFile, CopyDir }; +enum class ClipboardOpType { CopyFile, CopyDir, Invalid }; + +namespace ClipboardConfig +{ + static const QString c_type = "type"; + static const QString c_magic = "magic"; + static const QString c_isCut = "is_cut"; + static const QString c_files = "files"; +} + enum class OpenFileMode {Read = 0, Edit}; static const qreal c_webZoomFactorMax = 5; diff --git a/src/vdirectory.cpp b/src/vdirectory.cpp index 213d5c86..bee8ae72 100644 --- a/src/vdirectory.cpp +++ b/src/vdirectory.cpp @@ -470,24 +470,9 @@ bool VDirectory::removeFile(VNoteFile *p_file) return false; } - qDebug() << "note" << p_file->getName() << "removed from folder" << m_name; - return true; } -void VDirectory::deleteFile(VNoteFile *p_file) -{ - removeFile(p_file); - - // Delete the file - V_ASSERT(!p_file->isOpened()); - V_ASSERT(p_file->parent()); - - p_file->deleteFile(); - - delete p_file; -} - bool VDirectory::rename(const QString &p_name) { if (m_name == p_name) { @@ -519,126 +504,6 @@ bool VDirectory::rename(const QString &p_name) return true; } -VNoteFile *VDirectory::copyFile(VDirectory *p_destDir, const QString &p_destName, - VNoteFile *p_srcFile, bool p_cut) -{ - QString srcPath = QDir::cleanPath(p_srcFile->fetchPath()); - QString destPath = QDir::cleanPath(QDir(p_destDir->fetchPath()).filePath(p_destName)); - if (VUtils::equalPath(srcPath, destPath)) { - return p_srcFile; - } - - VDirectory *srcDir = p_srcFile->getDirectory(); - DocType docType = p_srcFile->getDocType(); - DocType newDocType = VUtils::docTypeFromName(destPath); - - QVector images; - if (docType == DocType::Markdown) { - images = VUtils::fetchImagesFromMarkdownFile(p_srcFile, - ImageLink::LocalRelativeInternal); - } - - // Copy the file - if (!VUtils::copyFile(srcPath, destPath, p_cut)) { - return NULL; - } - - // Handle VDirectory and VNoteFile - int index = -1; - VNoteFile *destFile = NULL; - if (p_cut) { - // Remove the file from config - srcDir->removeFile(p_srcFile); - - p_srcFile->setName(p_destName); - - // Add the file to new dir's config - if (p_destDir->addFile(p_srcFile, index)) { - destFile = p_srcFile; - } else { - destFile = NULL; - } - } else { - destFile = p_destDir->addFile(p_destName, -1); - } - - if (!destFile) { - return NULL; - } - - Q_ASSERT(docType == newDocType); - - // We need to copy internal images when it is still markdown. - if (!images.isEmpty()) { - if (newDocType == DocType::Markdown) { - QString parentPath = destFile->fetchBasePath(); - int nrPasted = 0; - for (int i = 0; i < images.size(); ++i) { - const ImageLink &link = images[i]; - if (!QFileInfo::exists(link.m_path)) { - continue; - } - - QString errStr; - bool ret = true; - - QString imageFolder = VUtils::directoryNameFromPath(VUtils::basePathFromPath(link.m_path)); - QString destImagePath = QDir(parentPath).filePath(imageFolder); - ret = VUtils::makePath(destImagePath); - if (!ret) { - errStr = tr("Fail to create image folder %2.") - .arg(g_config->c_dataTextStyle).arg(destImagePath); - } else { - destImagePath = QDir(destImagePath).filePath(VUtils::fileNameFromPath(link.m_path)); - - // Copy or Cut the images accordingly. - if (VUtils::equalPath(destImagePath, link.m_path)) { - ret = false; - } else { - ret = VUtils::copyFile(link.m_path, destImagePath, p_cut); - } - - if (ret) { - qDebug() << (p_cut ? "Cut" : "Copy") << "image" - << link.m_path << "->" << destImagePath; - - nrPasted++; - } else { - errStr = tr("Please check if there already exists a file %2 " - "and then manually copy it and modify the note accordingly.") - .arg(g_config->c_dataTextStyle).arg(destImagePath); - } - } - - if (!ret) { - VUtils::showMessage(QMessageBox::Warning, tr("Warning"), - tr("Fail to copy image %2 while " - "%5 note %4.") - .arg(g_config->c_dataTextStyle).arg(link.m_path) - .arg(g_config->c_dataTextStyle).arg(srcPath) - .arg(p_cut ? tr("moving") : tr("copying")), - errStr, QMessageBox::Ok, QMessageBox::Ok, NULL); - } - } - - qDebug() << "pasted" << nrPasted << "images"; - } else { - // Delete the images. - int deleted = 0; - for (int i = 0; i < images.size(); ++i) { - QFile file(images[i].m_path); - if (file.remove()) { - ++deleted; - } - } - - qDebug() << "delete" << deleted << "images since it is not Markdown any more for" << srcPath; - } - } - - return destFile; -} - // Copy @p_srcDir to be a sub-directory of @p_destDir with name @p_destName. VDirectory *VDirectory::copyDirectory(VDirectory *p_destDir, const QString &p_destName, VDirectory *p_srcDir, bool p_cut) @@ -687,36 +552,6 @@ void VDirectory::setExpanded(bool p_expanded) m_expanded = p_expanded; } -void VDirectory::reorderFiles(int p_first, int p_last, int p_destStart) -{ - V_ASSERT(m_opened); - V_ASSERT(p_first <= p_last); - V_ASSERT(p_last < m_files.size()); - V_ASSERT(p_destStart < p_first || p_destStart > p_last); - V_ASSERT(p_destStart >= 0 && p_destStart <= m_files.size()); - - auto oriFiles = m_files; - - // Reorder m_files. - if (p_destStart > p_last) { - int to = p_destStart - 1; - for (int i = p_first; i <= p_last; ++i) { - // Move p_first to p_destStart every time. - m_files.move(p_first, to); - } - } else { - int to = p_destStart; - for (int i = p_first; i <= p_last; ++i) { - m_files.move(i, to++); - } - } - - if (!writeToConfig()) { - qWarning() << "fail to reorder files in config" << p_first << p_last << p_destStart; - m_files = oriFiles; - } -} - VNoteFile *VDirectory::tryLoadFile(QStringList &p_filePath) { qDebug() << "directory" << m_name << "tryLoadFile()" << p_filePath.join("/"); @@ -755,3 +590,24 @@ VNoteFile *VDirectory::tryLoadFile(QStringList &p_filePath) return file; } + +bool VDirectory::sortFiles(const QVector &p_sortedIdx) +{ + V_ASSERT(m_opened); + V_ASSERT(p_sortedIdx.size() == m_files.size()); + + auto ori = m_files; + + for (int i = 0; i < p_sortedIdx.size(); ++i) { + m_files[i] = ori[p_sortedIdx[i]]; + } + + bool ret = true; + if (!writeToConfig()) { + qWarning() << "fail to reorder files in config" << p_sortedIdx; + m_files = ori; + ret = false; + } + + return ret; +} diff --git a/src/vdirectory.h b/src/vdirectory.h index 4e9e5858..06c37618 100644 --- a/src/vdirectory.h +++ b/src/vdirectory.h @@ -52,18 +52,12 @@ public: // Return the VNoteFile if succeed. VNoteFile *addFile(const QString &p_name, int p_index); - // Delete @p_file both from disk and config, as well as its local images. - void deleteFile(VNoteFile *p_file); + // Add the file in the config and m_files. If @p_index is -1, add it at the end. + bool addFile(VNoteFile *p_file, int p_index); // Rename current directory to @p_name. bool rename(const QString &p_name); - // Copy @p_srcFile to @p_destDir, setting new name to @p_destName. - // @p_cut: copy or cut. - // Returns the dest VNoteFile. - static VNoteFile *copyFile(VDirectory *p_destDir, const QString &p_destName, - VNoteFile *p_srcFile, bool p_cut); - static VDirectory *copyDirectory(VDirectory *p_destDir, const QString &p_destName, VDirectory *p_srcDir, bool p_cut); @@ -83,10 +77,6 @@ public: bool isExpanded() const; void setExpanded(bool p_expanded); - // Reorder files in m_files by index. - // Move [@p_first, @p_last] to @p_destStart. - void reorderFiles(int p_first, int p_last, int p_destStart); - // Serialize current instance to json. // Not including sections belonging to notebook. QJsonObject toConfigJson() const; @@ -108,6 +98,9 @@ public: QDateTime getCreatedTimeUtc() const; + // Reorder files in m_files by index. + bool sortFiles(const QVector &p_sortedIdx); + private: // Get the path of @p_dir recursively QString fetchPath(const VDirectory *p_dir) const; @@ -121,9 +114,6 @@ private: // Should only be called with root directory. void addNotebookConfig(QJsonObject &p_json) const; - // Add the file in the config and m_files. If @p_index is -1, add it at the end. - bool addFile(VNoteFile *p_file, int p_index); - // Add the directory in the config and m_subDirs. If @p_index is -1, add it at the end. // Return the VDirectory if succeed. VDirectory *addSubDirectory(const QString &p_name, int p_index); diff --git a/src/vdirectorytree.cpp b/src/vdirectorytree.cpp index ae652323..7e98bd56 100644 --- a/src/vdirectorytree.cpp +++ b/src/vdirectorytree.cpp @@ -533,12 +533,12 @@ void VDirectoryTree::reloadFromDisk() curDir = getVDirectory(curItem); info = tr("Are you sure to reload folder %2?") .arg(g_config->c_dataTextStyle).arg(curDir->getName()); - msg = tr("Successfully reloaded folder %1 from disk").arg(curDir->getName()); + msg = tr("Folder %1 reloaded from disk").arg(curDir->getName()); } else { // Reload notebook. info = tr("Are you sure to reload notebook %2?") .arg(g_config->c_dataTextStyle).arg(m_notebook->getName()); - msg = tr("Successfully reloaded notebook %1 from disk").arg(m_notebook->getName()); + msg = tr("Notebook %1 reloaded from disk").arg(m_notebook->getName()); } if (g_config->getConfirmReloadFolder()) { diff --git a/src/vfilelist.cpp b/src/vfilelist.cpp index e55d0a5b..a70c162f 100644 --- a/src/vfilelist.cpp +++ b/src/vfilelist.cpp @@ -12,9 +12,13 @@ #include "vconfigmanager.h" #include "vmdedit.h" #include "vmdtab.h" +#include "dialog/vconfirmdeletiondialog.h" +#include "dialog/vsortdialog.h" +#include "vmainwindow.h" extern VConfigManager *g_config; extern VNote *g_vnote; +extern VMainWindow *g_mainWin; const QString VFileList::c_infoShortcutSequence = "F2"; const QString VFileList::c_copyShortcutSequence = "Ctrl+C"; @@ -34,7 +38,6 @@ 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; @@ -45,8 +48,6 @@ void VFileList::setupUI() this, &VFileList::contextMenuRequested); connect(fileList, &QListWidget::itemClicked, this, &VFileList::handleItemClicked); - connect(fileList->model(), &QAbstractItemModel::rowsMoved, - this, &VFileList::handleRowsMoved); setLayout(mainLayout); } @@ -78,7 +79,7 @@ void VFileList::initShortcuts() pasteShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(pasteShortcut, &QShortcut::activated, this, [this](){ - pasteFilesInCurDir(); + pasteFilesFromClipboard(); }); } @@ -136,7 +137,7 @@ void VFileList::initActions() tr("&Delete"), this); deleteFileAct->setToolTip(tr("Delete selected note")); connect(deleteFileAct, SIGNAL(triggered(bool)), - this, SLOT(deleteFile())); + this, SLOT(deleteSelectedFiles())); fileInfoAct = new QAction(QIcon(":/resources/icons/note_info.svg"), tr("&Info\t%1").arg(VUtils::getShortcutText(c_infoShortcutSequence)), this); @@ -160,12 +161,19 @@ void VFileList::initActions() 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); + this, &VFileList::pasteFilesFromClipboard); 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); + + m_sortAct = new QAction(QIcon(":/resources/icons/sort.svg"), + tr("&Sort"), + this); + m_sortAct->setToolTip(tr("Sort notes in this folder manually")); + connect(m_sortAct, &QAction::triggered, + this, &VFileList::sortItems); } void VFileList::setDirectory(VDirectory *p_directory) @@ -187,7 +195,6 @@ void VFileList::setDirectory(VDirectory *p_directory) return; } - qDebug() << "filelist set folder" << m_directory->getName(); updateFileList(); } @@ -215,10 +222,11 @@ void VFileList::fileInfo() void VFileList::openFileLocation() const { - QListWidgetItem *curItem = fileList->currentItem(); - V_ASSERT(curItem); - QUrl url = QUrl::fromLocalFile(getVFile(curItem)->fetchBasePath()); - QDesktopServices::openUrl(url); + QList items = fileList->selectedItems(); + if (items.size() == 1) { + QUrl url = QUrl::fromLocalFile(getVFile(items[0])->fetchBasePath()); + QDesktopServices::openUrl(url); + } } void VFileList::fileInfo(VNoteFile *p_file) @@ -277,15 +285,26 @@ QListWidgetItem* VFileList::insertFileListItem(VNoteFile *file, bool atFront) // 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) +void VFileList::removeFileListItem(VNoteFile *p_file) { - fileList->setCurrentRow(-1); - fileList->removeItemWidget(item); + 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(); } @@ -383,18 +402,21 @@ QVector VFileList::updateFileListAdded() } } } - qDebug() << ret.size() << "items added"; + return ret; } -// Delete the file related to current item -void VFileList::deleteFile() +void VFileList::deleteSelectedFiles() { QList items = fileList->selectedItems(); Q_ASSERT(!items.isEmpty()); - for (int i = 0; i < items.size(); ++i) { - deleteFile(getVFile(items.at(i))); + + QVector files; + for (auto const & item : items) { + files.push_back(getVFile(item)); } + + deleteFiles(files); } // @p_file may or may not be listed in VFileList @@ -404,30 +426,83 @@ void VFileList::deleteFile(VNoteFile *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: " - "VNote will delete the note as well as all " - "its images and attachments managed by VNote. " - "You could find deleted files in the recycle " - "bin of this notebook.
" - "The operation is IRREVERSIBLE!") - .arg(g_config->c_warningTextStyle), - QMessageBox::Ok | QMessageBox::Cancel, - QMessageBox::Ok, this, MessageBoxType::Danger); - if (ret == QMessageBox::Ok) { - editArea->closeFile(p_file, true); + QVector files(1, p_file); + deleteFiles(files); +} - // Remove the item before deleting it totally, or p_file will be invalid. - QListWidgetItem *item = findItem(p_file); - if (item) { - removeFileListItem(item); +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. " + "You could find deleted files 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); } - dir->deleteFile(p_file); + 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"))); + } } } @@ -456,18 +531,22 @@ void VFileList::contextMenuRequested(QPoint pos) menu.addAction(newFileAct); + if (fileList->count() > 1) { + menu.addAction(m_sortAct); + } + if (item) { - menu.addAction(deleteFileAct); menu.addSeparator(); + menu.addAction(deleteFileAct); menu.addAction(copyAct); menu.addAction(cutAct); } - if (VUtils::opTypeInClipboard() == ClipboardOpType::CopyFile - && !m_copiedFiles.isEmpty()) { + if (pasteAvailable()) { if (!item) { menu.addSeparator(); } + menu.addAction(pasteAct); } @@ -517,29 +596,59 @@ void VFileList::handleItemClicked(QListWidgetItem *currentItem) emit fileClicked(getVFile(currentItem), g_config->getNoteOpenMode()); } -bool VFileList::importFile(const QString &p_srcFilePath) +bool VFileList::importFiles(const QStringList &p_files, QString *p_errMsg) { - 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) { + if (p_files.isEmpty()) { return false; } - VNoteFile *destFile = m_directory->addFile(srcName, -1); - if (destFile) { - return insertFileListItem(destFile, false); + bool ret = true; + Q_ASSERT(m_directory && m_directory->isOpened()); + QString dirPath = m_directory->fetchPath(); + QDir dir(dirPath); + + int nrImported = 0; + 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()); + name = VUtils::getFileNameWithSequence(dirPath, name); + QString targetFilePath = dir.filePath(name); + bool ret = VUtils::copyFile(file, targetFilePath, false); + if (!ret) { + VUtils::addErrMsg(p_errMsg, tr("Fail to copy file %1 as %1.") + .arg(file) + .arg(targetFilePath)); + ret = false; + continue; + } + + VNoteFile *destFile = m_directory->addFile(name, -1); + if (destFile) { + ++nrImported; + 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; + } } - return false; + + qDebug() << "imported" << nrImported << "files"; + + updateFileList(); + + return ret; } void VFileList::copySelectedFiles(bool p_isCut) @@ -548,19 +657,29 @@ void VFileList::copySelectedFiles(bool p_isCut) if (items.isEmpty()) { return; } + QJsonArray files; - m_copiedFiles.clear(); for (int i = 0; i < items.size(); ++i) { VNoteFile *file = getVFile(items[i]); - QJsonObject fileJson; - fileJson["notebook"] = file->getNotebookName(); - fileJson["path"] = file->fetchPath(); - files.append(fileJson); - - m_copiedFiles.append(file); + files.append(file->fetchPath()); } - copyFileInfoToClipboard(files, p_isCut); + 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() @@ -568,82 +687,123 @@ void VFileList::cutSelectedFiles() copySelectedFiles(true); } -void VFileList::copyFileInfoToClipboard(const QJsonArray &p_files, bool p_isCut) +void VFileList::pasteFilesFromClipboard() { - 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()) { + if (!pasteAvailable()) { return; } - pasteFiles(m_directory); + 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); } -void VFileList::pasteFiles(VDirectory *p_destDir) +void VFileList::pasteFiles(VDirectory *p_destDir, + const QVector &p_files, + bool p_isCut) { - 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(); + if (!p_destDir || p_files.isEmpty()) { + clipboard->clear(); + return; + } int nrPasted = 0; - for (int i = 0; i < m_copiedFiles.size(); ++i) { - QPointer srcFile = m_copiedFiles[i]; - if (!srcFile) { + 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 copy 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 = 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++; + 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); } else { - VUtils::showMessage(QMessageBox::Warning, tr("Warning"), + // Rename it to xxx_copy.md if needed. + fileName = VUtils::generateCopiedFileName(p_destDir->fetchPath(), fileName); + } + + QString msg; + VNoteFile *destFile = NULL; + bool ret = VNoteFile::copyFile(p_destDir, + fileName, + file, + p_isCut, + &destFile, + &msg); + if (!ret) { + 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); + .arg(g_config->c_dataTextStyle) + .arg(p_files[i]), + msg, + QMessageBox::Ok, + QMessageBox::Ok, + this); + } + + if (destFile) { + ++nrPasted; + emit fileUpdated(destFile); } } - qDebug() << "pasted" << nrPasted << "files sucessfully"; - clipboard->clear(); - m_copiedFiles.clear(); -} - -bool VFileList::copyFile(VDirectory *p_destDir, const QString &p_destName, VNoteFile *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; + qDebug() << "copy" << nrPasted << "files"; + if (nrPasted > 0) { + g_mainWin->showStatusMessage(tr("%1 %2 pasted") + .arg(nrPasted) + .arg(nrPasted > 1 ? tr("notes") : tr("note"))); } - // DocType is not allowed to change. - Q_ASSERT(p_file->getDocType() == VUtils::docTypeFromName(destPath)); - - VNoteFile *destFile = VDirectory::copyFile(p_destDir, p_destName, p_file, p_cut); updateFileList(); - if (destFile) { - emit fileUpdated(destFile); - } - return destFile != NULL; + clipboard->clear(); + getNewMagic(); } void VFileList::keyPressEvent(QKeyEvent *event) @@ -714,30 +874,6 @@ bool VFileList::locateFile(const VNoteFile *p_file) 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; @@ -825,3 +961,105 @@ QList VFileList::getVisibleItems() const return items; } +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); + tree->header()->setStretchLastSection(true); + 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()); + QString modifiedTime = VUtils::displayDateTime(file->getModifiedTimeUtc().toLocalTime()); + 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(); + } +} diff --git a/src/vfilelist.h b/src/vfilelist.h index caeca297..35d2761e 100644 --- a/src/vfilelist.h +++ b/src/vfilelist.h @@ -27,11 +27,23 @@ class VFileList : public QWidget, public VNavigationMode Q_OBJECT public: explicit VFileList(QWidget *parent = 0); - bool importFile(const QString &p_srcFilePath); + + // Import external files @p_files to current directory. + // Only copy the files itself. + bool importFiles(const QStringList &p_files, QString *p_errMsg = NULL); + inline void setEditArea(VEditArea *editArea); + + // View and edit information of @p_file. void fileInfo(VNoteFile *p_file); + + // Delete file @p_file. + // It is not necessary that @p_file exists in the list. void deleteFile(VNoteFile *p_file); + + // Locate @p_file in the list widget. bool locateFile(const VNoteFile *p_file); + inline const VDirectory *currentDirectory() const; // Implementations for VNavigationMode. @@ -40,6 +52,13 @@ public: void hideNavigation() Q_DECL_OVERRIDE; bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE; +public slots: + // Set VFileList to display content of @p_directory directory. + void setDirectory(VDirectory *p_directory); + + // Create a note. + void newFile(); + signals: void fileClicked(VNoteFile *p_file, OpenFileMode mode = OpenFileMode::Read); void fileCreated(VNoteFile *p_file, OpenFileMode mode = OpenFileMode::Read); @@ -48,18 +67,33 @@ signals: private slots: void contextMenuRequested(QPoint pos); void handleItemClicked(QListWidgetItem *currentItem); - void fileInfo(); - void openFileLocation() const; - // m_copiedFiles will keep the files's VNoteFile. - void copySelectedFiles(bool p_isCut = false); - void cutSelectedFiles(); - void pasteFilesInCurDir(); - void deleteFile(); - void handleRowsMoved(const QModelIndex &p_parent, int p_start, int p_end, const QModelIndex &p_destination, int p_row); -public slots: - void setDirectory(VDirectory *p_directory); - void newFile(); + // View and edit information of selected file. + // Valid only when there is only one selected file. + void fileInfo(); + + // Open the folder containing selected file in system's file browser. + // Valid only when there is only one selected file. + void openFileLocation() const; + + // Copy selected files to clipboard. + // Will put a Json string into the clipboard which contains the information + // about copied files. + void copySelectedFiles(bool p_isCut = false); + + void cutSelectedFiles(); + + // Paste files from clipboard. + void pasteFilesFromClipboard(); + + // Delete selected files. + void deleteSelectedFiles(); + + // Delete files @p_files. + void deleteFiles(const QVector &p_files); + + // Sort files in this list. + void sortItems(); protected: void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; @@ -71,11 +105,16 @@ private: // Init shortcuts. void initShortcuts(); + // Clear and re-fill the list widget according to m_directory. void updateFileList(); + // Insert a new item into the list widget. + // @file: the file represented by the new item. + // @atFront: insert at the front or back of the list widget. QListWidgetItem *insertFileListItem(VNoteFile *file, bool atFront = false); - void removeFileListItem(QListWidgetItem *item); + // Remove and delete item related to @p_file from list widget. + void removeFileListItem(VNoteFile *p_file); // Init actions. void initActions(); @@ -83,25 +122,36 @@ private: // Return the corresponding QListWidgetItem of @p_file. QListWidgetItem *findItem(const VNoteFile *p_file); - void copyFileInfoToClipboard(const QJsonArray &p_files, bool p_isCut); - void pasteFiles(VDirectory *p_destDir); - bool copyFile(VDirectory *p_destDir, const QString &p_destName, VNoteFile *p_file, bool p_cut); + // Paste files given path by @p_files to destination directory @p_destDir. + void pasteFiles(VDirectory *p_destDir, + const QVector &p_files, + bool p_isCut); + // New items have been added to direcotry. Update file list accordingly. QVector updateFileListAdded(); inline QPointer getVFile(QListWidgetItem *p_item) const; - // Check if the list items match exactly the contents of the directory. - bool identicalListWithDirectory() const; QList getVisibleItems() const; // Fill the info of @p_item according to @p_file. void fillItem(QListWidgetItem *p_item, const VNoteFile *p_file); + // Generate new magic to m_magicForClipboard. + int getNewMagic(); + + // Check if @p_magic equals to m_magicForClipboard. + bool checkMagic(int p_magic) const; + + // Check if there are files in clipboard available to paste. + bool pasteAvailable() const; + VEditArea *editArea; QListWidget *fileList; QPointer m_directory; - QVector > m_copiedFiles; + + // Magic number for clipboard operations. + int m_magicForClipboard; // Actions QAction *m_openInReadAct; @@ -114,6 +164,7 @@ private: QAction *cutAct; QAction *pasteAct; QAction *m_openLocationAct; + QAction *m_sortAct; // Navigation Mode. // Map second key to QListWidgetItem. diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index 787ef81a..76cbb795 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -1039,7 +1039,7 @@ void VMainWindow::importNoteFromFile() { static QString lastPath = QDir::homePath(); QStringList files = QFileDialog::getOpenFileNames(this, - tr("Select Files (HTML or Markdown) To Create Notes"), + tr("Select Files To Create Notes"), lastPath); if (files.isEmpty()) { return; @@ -1048,23 +1048,21 @@ void VMainWindow::importNoteFromFile() // Update lastPath lastPath = QFileInfo(files[0]).path(); - int failedFiles = 0; - for (int i = 0; i < files.size(); ++i) { - bool ret = fileList->importFile(files[i]); - if (!ret) { - ++failedFiles; - } + QString msg; + if (!fileList->importFiles(files, &msg)) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to create notes for all the files."), + msg, + QMessageBox::Ok, + QMessageBox::Ok, + this); + } else { + int cnt = files.size(); + showStatusMessage(tr("%1 %2 created from external files") + .arg(cnt) + .arg(cnt > 1 ? tr("notes") : tr("note"))); } - - QMessageBox msgBox(QMessageBox::Information, tr("New Notes From Files"), - tr("Created notes: %1 succeed, %2 failed.") - .arg(files.size() - failedFiles).arg(failedFiles), - QMessageBox::Ok, this); - if (failedFiles > 0) { - msgBox.setInformativeText(tr("Fail to create notes from files maybe due to name conflicts.")); - } - - msgBox.exec(); } void VMainWindow::changeMarkdownConverter(QAction *action) diff --git a/src/vmdedit.cpp b/src/vmdedit.cpp index bac4f358..b341fc19 100644 --- a/src/vmdedit.cpp +++ b/src/vmdedit.cpp @@ -286,21 +286,39 @@ void VMdEdit::clearUnusedImages() if (!unusedImages.isEmpty()) { if (g_config->getConfirmImagesCleanUp()) { - QString info = tr("Following images seems not to be used in this note anymore. " - "Please confirm the deletion of these images.
" + QVector items; + for (auto const & img : unusedImages) { + items.push_back(ConfirmItemInfo(img, + img, + img, + NULL)); + + } + + QString text = tr("Following images seems not to be used in this note anymore. " + "Please confirm the deletion of these images."); + + QString info = tr("You could find deleted files in the recycle " + "bin of this note.
" "Click \"Cancel\" to leave them untouched."); + VConfirmDeletionDialog dialog(tr("Confirm Cleaning Up Unused Images"), + text, info, - unusedImages, + items, + true, true, - g_config->getConfirmImagesCleanUp(), true, this); + + unusedImages.clear(); if (dialog.exec()) { - unusedImages = dialog.getConfirmedFiles(); + items = dialog.getConfirmedItems(); g_config->setConfirmImagesCleanUp(dialog.getAskAgainEnabled()); - } else { - unusedImages.clear(); + + for (auto const & item : items) { + unusedImages.push_back(item.m_name); + } } } diff --git a/src/vnotefile.cpp b/src/vnotefile.cpp index 05a05c00..743bdbe7 100644 --- a/src/vnotefile.cpp +++ b/src/vnotefile.cpp @@ -53,9 +53,6 @@ QString VNoteFile::getImageFolderInLink() const void VNoteFile::setName(const QString &p_name) { - Q_ASSERT(m_name.isEmpty() - || (m_docType == VUtils::docTypeFromName(p_name))); - m_name = p_name; } @@ -80,7 +77,7 @@ bool VNoteFile::rename(const QString &p_name) m_name = p_name; // Update parent directory's config file. - if (!dir->writeToConfig()) { + if (!dir->updateFileConfig(this)) { m_name = oldName; diskDir.rename(p_name, m_name); return false; @@ -175,32 +172,41 @@ QJsonObject VNoteFile::toConfigJson() const return item; } -bool VNoteFile::deleteFile() +bool VNoteFile::deleteFile(QString *p_errMsg) { + Q_ASSERT(!m_opened); Q_ASSERT(parent()); - bool ret = false; + bool ret = true; // Delete local images if it is Markdown. if (m_docType == DocType::Markdown) { - deleteInternalImages(); + if (!deleteInternalImages()) { + ret = false; + VUtils::addErrMsg(p_errMsg, tr("Fail to delete images of this note.")); + } } - // TODO: Delete attachments. + // Delete attachments. + if (!deleteAttachments()) { + ret = false; + VUtils::addErrMsg(p_errMsg, tr("Fail to delete attachments of this note.")); + } // Delete the file. QString filePath = fetchPath(); if (VUtils::deleteFile(getNotebook(), filePath, false)) { - ret = true; qDebug() << "deleted" << m_name << filePath; } else { + ret = false; + VUtils::addErrMsg(p_errMsg, tr("Fail to delete the note file.")); qWarning() << "fail to delete" << m_name << filePath; } return ret; } -void VNoteFile::deleteInternalImages() +bool VNoteFile::deleteInternalImages() { Q_ASSERT(parent() && m_docType == DocType::Markdown); @@ -214,6 +220,8 @@ void VNoteFile::deleteInternalImages() } qDebug() << "delete" << deleted << "images for" << m_name << fetchPath(); + + return deleted == images.size(); } bool VNoteFile::addAttachment(const QString &p_file) @@ -297,10 +305,20 @@ bool VNoteFile::deleteAttachments(const QVector &p_names) } } + // Delete the attachment folder if m_attachments is empty now. + if (m_attachments.isEmpty()) { + dir.cdUp(); + if (!dir.rmdir(m_attachmentFolder)) { + ret = false; + qWarning() << "fail to delete attachment folder" << m_attachmentFolder + << "for note" << m_name; + } + } + if (!getDirectory()->updateFileConfig(this)) { + ret = false; qWarning() << "fail to update config of file" << m_name << "in directory" << fetchBasePath(); - ret = false; } return ret; @@ -320,21 +338,25 @@ int VNoteFile::findAttachment(const QString &p_name, bool p_caseSensitive) return -1; } -void VNoteFile::sortAttachments(QVector p_sortedIdx) +bool VNoteFile::sortAttachments(const QVector &p_sortedIdx) { V_ASSERT(m_opened); V_ASSERT(p_sortedIdx.size() == m_attachments.size()); - auto oriFiles = m_attachments; + auto ori = m_attachments; for (int i = 0; i < p_sortedIdx.size(); ++i) { - m_attachments[i] = oriFiles[p_sortedIdx[i]]; + m_attachments[i] = ori[p_sortedIdx[i]]; } + bool ret = true; if (!getDirectory()->updateFileConfig(this)) { - qWarning() << "fail to reorder files in config" << p_sortedIdx; - m_attachments = oriFiles; + qWarning() << "fail to reorder attachments in config" << p_sortedIdx; + m_attachments = ori; + ret = false; } + + return ret; } bool VNoteFile::renameAttachment(const QString &p_oldName, const QString &p_newName) @@ -363,3 +385,172 @@ bool VNoteFile::renameAttachment(const QString &p_oldName, const QString &p_newN return true; } + +bool VNoteFile::deleteFile(VNoteFile *p_file, QString *p_errMsg) +{ + Q_ASSERT(!p_file->isOpened()); + + bool ret = true; + QString name = p_file->getName(); + QString path = p_file->fetchPath(); + + if (!p_file->deleteFile(p_errMsg)) { + qWarning() << "fail to delete file" << name << path; + ret = false; + } + + VDirectory *dir = p_file->getDirectory(); + Q_ASSERT(dir); + if (!dir->removeFile(p_file)) { + qWarning() << "fail to remove file from directory" << name << path; + VUtils::addErrMsg(p_errMsg, tr("Fail to remove the note from the folder configuration.")); + ret = false; + } + + delete p_file; + + return ret; +} + +bool VNoteFile::copyFile(VDirectory *p_destDir, + const QString &p_destName, + VNoteFile *p_file, + bool p_isCut, + VNoteFile **p_targetFile, + QString *p_errMsg) +{ + bool ret = true; + *p_targetFile = NULL; + int nrImageCopied = 0; + bool attachmentFolderCopied = false; + + QString srcPath = QDir::cleanPath(p_file->fetchPath()); + QString destPath = QDir::cleanPath(QDir(p_destDir->fetchPath()).filePath(p_destName)); + if (VUtils::equalPath(srcPath, destPath)) { + *p_targetFile = p_file; + return false; + } + + if (!p_destDir->isOpened()) { + VUtils::addErrMsg(p_errMsg, tr("Fail to open target folder.")); + return false; + } + + QString opStr = p_isCut ? tr("cut") : tr("copy"); + VDirectory *srcDir = p_file->getDirectory(); + DocType docType = p_file->getDocType(); + + Q_ASSERT(srcDir->isOpened()); + Q_ASSERT(docType == VUtils::docTypeFromName(p_destName)); + + // Images to be copied. + QVector images; + if (docType == DocType::Markdown) { + images = VUtils::fetchImagesFromMarkdownFile(p_file, + ImageLink::LocalRelativeInternal); + } + + // Attachments to be copied. + QString attaFolder = p_file->getAttachmentFolder(); + QString attaFolderPath; + if (!attaFolder.isEmpty()) { + attaFolderPath = p_file->fetchAttachmentFolderPath(); + } + + // Copy the note file. + if (!VUtils::copyFile(srcPath, destPath, p_isCut)) { + VUtils::addErrMsg(p_errMsg, tr("Fail to %1 the note file.").arg(opStr)); + qWarning() << "fail to" << opStr << "the note file" << srcPath << "to" << destPath; + return false; + } + + // Add file to VDirectory. + VNoteFile *destFile = NULL; + if (p_isCut) { + srcDir->removeFile(p_file); + p_file->setName(p_destName); + if (p_destDir->addFile(p_file, -1)) { + destFile = p_file; + } else { + destFile = NULL; + } + } else { + destFile = p_destDir->addFile(p_destName, -1); + } + + if (!destFile) { + VUtils::addErrMsg(p_errMsg, tr("Fail to add the note to target folder's configuration.")); + return false; + } + + // Copy images. + QDir parentDir(destFile->fetchBasePath()); + for (int i = 0; i < images.size(); ++i) { + const ImageLink &link = images[i]; + if (!QFileInfo::exists(link.m_path)) { + VUtils::addErrMsg(p_errMsg, tr("Source image %1 does not exist.") + .arg(link.m_path)); + ret = false; + continue; + } + + QString imageFolder = VUtils::directoryNameFromPath(VUtils::basePathFromPath(link.m_path)); + QString destImagePath = QDir(parentDir.filePath(imageFolder)).filePath(VUtils::fileNameFromPath(link.m_path)); + + if (VUtils::equalPath(link.m_path, destImagePath)) { + VUtils::addErrMsg(p_errMsg, tr("Skip image with the same source and target path %1.") + .arg(link.m_path)); + ret = false; + continue; + } + + if (!VUtils::copyFile(link.m_path, destImagePath, p_isCut)) { + VUtils::addErrMsg(p_errMsg, tr("Fail to %1 image %2 to %3. " + "Please manually %1 it and modify the note.") + .arg(opStr).arg(link.m_path).arg(destImagePath)); + ret = false; + } else { + ++nrImageCopied; + qDebug() << opStr << "image" << link.m_path << "to" << destImagePath; + } + } + + // Copy attachment folder. + if (!attaFolderPath.isEmpty()) { + QDir dir(destFile->fetchBasePath()); + QString folderPath = dir.filePath(destFile->getNotebook()->getAttachmentFolder()); + attaFolder = VUtils::getFileNameWithSequence(folderPath, attaFolder); + folderPath = QDir(folderPath).filePath(attaFolder); + + // Copy attaFolderPath to folderPath. + if (!VUtils::copyDirectory(attaFolderPath, folderPath, p_isCut)) { + VUtils::addErrMsg(p_errMsg, tr("Fail to %1 attachments folder %2 to %3. " + "Please manually maintain it.") + .arg(opStr).arg(attaFolderPath).arg(folderPath)); + QVector emptyAttas; + destFile->setAttachments(emptyAttas); + ret = false; + } else { + attachmentFolderCopied = true; + + destFile->setAttachmentFolder(attaFolder); + if (!p_isCut) { + destFile->setAttachments(p_file->getAttachments()); + } + } + + if (!p_destDir->updateFileConfig(destFile)) { + VUtils::addErrMsg(p_errMsg, tr("Fail to update configuration of note %1.") + .arg(destFile->fetchPath())); + ret = false; + } + } + + qDebug() << "copyFile:" << p_file << "to" << destFile + << "copied_images:" << nrImageCopied + << "copied_attachments:" << attachmentFolderCopied; + + *p_targetFile = destFile; + return ret; +} + diff --git a/src/vnotefile.h b/src/vnotefile.h index 7dee3c12..92e444f1 100644 --- a/src/vnotefile.h +++ b/src/vnotefile.h @@ -67,22 +67,17 @@ public: // Get the relative path related to the notebook. QString fetchRelativePath() const; - // Create a VNoteFile from @p_json Json object. - static VNoteFile *fromJson(VDirectory *p_directory, - const QJsonObject &p_json, - FileType p_type, - bool p_modifiable); - // Create a Json object from current instance. QJsonObject toConfigJson() const; - // Delete this file in disk as well as all its images/attachments. - bool deleteFile(); - const QString &getAttachmentFolder() const; + void setAttachmentFolder(const QString &p_folder); + const QVector &getAttachments() const; + void setAttachments(const QVector &p_attas); + // Add @p_file as an attachment to this note. bool addAttachment(const QString &p_file); @@ -97,17 +92,43 @@ public: bool deleteAttachments(const QVector &p_names); // Reorder attachments in m_attachments by index. - void sortAttachments(QVector p_sortedIdx); + bool sortAttachments(const QVector &p_sortedIdx); // Return the index of @p_name in m_attachments. // -1 if not found. int findAttachment(const QString &p_name, bool p_caseSensitive = true); + // Rename attachment @p_oldName to @p_newName. bool renameAttachment(const QString &p_oldName, const QString &p_newName); + // Create a VNoteFile from @p_json Json object. + static VNoteFile *fromJson(VDirectory *p_directory, + const QJsonObject &p_json, + FileType p_type, + bool p_modifiable); + + // Delete file @p_file including removing it from parent directory configuration + // and delete the file in disk. + // @p_file: should be a normal file with parent directory. + // @p_errMsg: if not NULL, it will contain error message if this function fails. + static bool deleteFile(VNoteFile *p_file, QString *p_errMsg = NULL); + + // Copy file @p_file to @p_destDir with new name @p_destName. + // Returns a file representing the destination file after copy/cut. + static bool copyFile(VDirectory *p_destDir, + const QString &p_destName, + VNoteFile *p_file, + bool p_isCut, + VNoteFile **p_targetFile, + QString *p_errMsg = NULL); + private: // Delete internal images of this file. - void deleteInternalImages(); + // Return true only when all internal images were deleted successfully. + bool deleteInternalImages(); + + // Delete this file in disk as well as all its images/attachments. + bool deleteFile(QString *p_msg = NULL); // Folder under the attachment folder of the notebook. // Store all the attachments of current file. @@ -122,9 +143,19 @@ inline const QString &VNoteFile::getAttachmentFolder() const return m_attachmentFolder; } +inline void VNoteFile::setAttachmentFolder(const QString &p_folder) +{ + m_attachmentFolder = p_folder; +} + inline const QVector &VNoteFile::getAttachments() const { return m_attachments; } +inline void VNoteFile::setAttachments(const QVector &p_attas) +{ + m_attachments = p_attas; +} + #endif // VNOTEFILE_H