Browse Source

refactor VFileList

- Refine note deletion logics;
- Refine note copy/paste logics;
- Refine note sorting logics;
Le Tan 8 years ago
parent
commit
58e7cdca4b

+ 92 - 51
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();
-    }
-
-    ConfirmItemWidget(bool p_checked, const QString &p_file, QWidget *p_parent = NULL)
-        : QWidget(p_parent)
-    {
-        setupUI();
+    setupUI();
 
-        m_checkBox->setChecked(p_checked);
-        m_fileLabel->setText(p_file);
+    m_checkBox->setChecked(p_checked);
+    m_fileLabel->setText(p_file);
+    if (!p_tip.isEmpty()) {
+        m_fileLabel->setToolTip(p_tip);
     }
+}
 
-    bool isChecked() const
-    {
-        return m_checkBox->isChecked();
-    }
+void ConfirmItemWidget::setupUI()
+{
+    m_checkBox = new QCheckBox;
+    connect(m_checkBox, &QCheckBox::stateChanged,
+            this, &ConfirmItemWidget::checkStateChanged);
 
-    QString getFile() const
-    {
-        return m_fileLabel->text();
-    }
+    m_fileLabel = new QLabel;
+    QHBoxLayout *mainLayout = new QHBoxLayout;
+    mainLayout->addWidget(m_checkBox);
+    mainLayout->addWidget(m_fileLabel);
+    mainLayout->addStretch();
+    mainLayout->setContentsMargins(3, 0, 0, 0);
 
-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);
-    }
+    setLayout(mainLayout);
+}
 
-    QCheckBox *m_checkBox;
-    QLabel *m_fileLabel;
-};
+bool ConfirmItemWidget::isChecked() const
+{
+    return m_checkBox->isChecked();
+}
+
+int ConfirmItemWidget::getIndex() const
+{
+    return m_index;
+}
 
 VConfirmDeletionDialog::VConfirmDeletionDialog(const QString &p_title,
+                                               const QString &p_text,
                                                const QString &p_info,
-                                               const QVector<QString> &p_files,
+                                               const QVector<ConfirmItemInfo> &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);
+
+    initItems();
 
-    initFileItems(p_files);
+    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<QString> VConfirmDeletionDialog::getConfirmedFiles() const
+QVector<ConfirmItemInfo> VConfirmDeletionDialog::getConfirmedItems() const
 {
-    QVector<QString> files;
+    QVector<ConfirmItemInfo> 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<QString> &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<ConfirmItemWidget *>(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));
+}

+ 61 - 4
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<QString> &p_files,
+                           const QVector<ConfirmItemInfo> &p_items,
                            bool p_enableAskAgain,
                            bool p_askAgainEnabled,
                            bool p_enablePreview,
                            QWidget *p_parent = 0);
 
-    QVector<QString> getConfirmedFiles() const;
+    QVector<ConfirmItemInfo> getConfirmedItems() const;
 
     bool getAskAgainEnabled() const;
 
 private slots:
     void currentFileChanged(int p_row);
 
+    void updateCountLabel();
+
 private:
-    void setupUI(const QString &p_title, const QString &p_info);
+    void setupUI(const QString &p_title,
+                 const QString &p_text,
+                 const QString &p_info);
 
-    void initFileItems(const QVector<QString> &p_files);
+    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<ConfirmItemInfo> m_items;
 };
 
 #endif // VCONFIRMDELETIONDIALOG_H

+ 65 - 1
src/dialog/vsortdialog.cpp

@@ -2,6 +2,33 @@
 
 #include <QtWidgets>
 
+void VTreeWidget::dropEvent(QDropEvent *p_event)
+{
+    QList<QTreeWidgetItem *> 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<QVariant> VSortDialog::getSortedData() const
+{
+    int cnt = m_treeWidget->topLevelItemCount();
+    QVector<QVariant> 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;
+}

+ 26 - 1
src/dialog/vsortdialog.h

@@ -3,10 +3,32 @@
 
 #include <QDialog>
 #include <QVector>
+#include <QTreeWidget>
 
 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<QVariant> 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;

+ 57 - 20
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;
+    }
+}

+ 23 - 1
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<QPair<QString, QString> > &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):

+ 41 - 19
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<QString> names;
+    QVector<ConfirmItemInfo> items;
     const QList<QListWidgetItem *> 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 "
-                      "<span style=\"%1\">%2</span>? "
-                      "You could find deleted files in the recycle "
-                      "bin of this notebook.<br>"
-                      "Click \"Cancel\" to leave them untouched.")
+    QString text = tr("Are you sure to delete these attachments of note "
+                      "<span style=\"%1\">%2</span>?")
                      .arg(g_config->c_dataTextStyle).arg(m_file->getName());
+
+    QString info = tr("You could find deleted files in the recycle "
+                      "bin of this note.<br>"
+                      "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<QString> 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 <span style=\"%1\">%2</span> "
+                          "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<int> 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<QVariant> data = dialog.getSortedData();
+        Q_ASSERT(data.size() == attas.size());
+        QVector<int> 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 <span style=\"%1\">%2</span>.")
+                                  .arg(g_config->c_dataTextStyle)
+                                  .arg(m_file->getName()),
+                                "",
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                this);
+        }
     }
 }
 

+ 10 - 1
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;

+ 21 - 165
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<ImageLink> 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 <span style=\"%1\">%2</span>.")
-                               .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 <span style=\"%1\">%2</span> "
-                                    "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 <span style=\"%1\">%2</span> while "
-                                           "%5 note <span style=\"%3\">%4</span>.")
-                                          .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<int> &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;
+}

+ 5 - 15
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<int> &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);

+ 2 - 2
src/vdirectorytree.cpp

@@ -533,12 +533,12 @@ void VDirectoryTree::reloadFromDisk()
         curDir = getVDirectory(curItem);
         info = tr("Are you sure to reload folder <span style=\"%1\">%2</span>?")
                  .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 <span style=\"%1\">%2</span>?")
                  .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()) {

+ 388 - 150
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<QListWidgetItem *> 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<QListWidgetItem *> VFileList::updateFileListAdded()
             }
         }
     }
-    qDebug() << ret.size() << "items added";
+
     return ret;
 }
 
-// Delete the file related to current item
-void VFileList::deleteFile()
+void VFileList::deleteSelectedFiles()
 {
     QList<QListWidgetItem *> items = fileList->selectedItems();
     Q_ASSERT(!items.isEmpty());
-    for (int i = 0; i < items.size(); ++i) {
-        deleteFile(getVFile(items.at(i)));
+
+    QVector<VNoteFile *> 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 <span style=\"%1\">%2</span>?")
-                                    .arg(g_config->c_dataTextStyle).arg(fileName),
-                                  tr("<span style=\"%1\">WARNING</span>: "
-                                     "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.<br>"
-                                     "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);
-
-        // Remove the item before deleting it totally, or p_file will be invalid.
-        QListWidgetItem *item = findItem(p_file);
-        if (item) {
-            removeFileListItem(item);
+    QVector<VNoteFile *> files(1, p_file);
+    deleteFiles(files);
+}
+
+void VFileList::deleteFiles(const QVector<VNoteFile *> &p_files)
+{
+    if (p_files.isEmpty()) {
+        return;
+    }
+
+    QVector<ConfirmItemInfo> 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("<span style=\"%1\">WARNING</span>: "
+                      "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.<br>"
+                      "Click \"Cancel\" to leave them untouched.<br>"
+                      "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<VNoteFile *> 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 <span style=\"%1\">%2</span>.<br>"
+                                       "Please check <span style=\"%1\">%3</span> 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<QString> 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<QString> &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<VNoteFile> 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 <span style=\"%1\">%2</span>.")
+                                  .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<ImageLink> 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 <span style=\"%1\">%2</span>.")
+                                          .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"),
-                                tr("Fail to copy note <span style=\"%1\">%2</span>.")
-                                  .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);
+            // Rename it to xxx_copy.md if needed.
+            fileName = VUtils::generateCopiedFileName(p_destDir->fetchPath(), fileName);
         }
-    }
 
-    qDebug() << "pasted" << nrPasted << "files sucessfully";
-    clipboard->clear();
-    m_copiedFiles.clear();
-}
+        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 <span style=\"%1\">%2</span>.")
+                                  .arg(g_config->c_dataTextStyle)
+                                  .arg(p_files[i]),
+                                msg,
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                this);
+        }
 
-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;
+        if (destFile) {
+            ++nrPasted;
+            emit fileUpdated(destFile);
+        }
     }
 
-    // DocType is not allowed to change.
-    Q_ASSERT(p_file->getDocType() == VUtils::docTypeFromName(destPath));
+    qDebug() << "copy" << nrPasted << "files";
+    if (nrPasted > 0) {
+        g_mainWin->showStatusMessage(tr("%1 %2 pasted")
+                                       .arg(nrPasted)
+                                       .arg(nrPasted > 1 ? tr("notes") : tr("note")));
+    }
 
-    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<VNoteFile *> 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<QListWidgetItem *> 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<VNoteFile *> &files = m_directory->getFiles();
+    if (files.size() < 2) {
+        return;
+    }
+
+    VSortDialog dialog(tr("Sort Notes"),
+                       tr("Sort notes in folder <span style=\"%1\">%2</span> "
+                          "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<QVariant> data = dialog.getSortedData();
+        Q_ASSERT(data.size() == files.size());
+        QVector<int> 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 <span style=\"%1\">%2</span>.")
+                                  .arg(g_config->c_dataTextStyle)
+                                  .arg(m_directory->getName()),
+                                "",
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                this);
+        }
+
+        updateFileList();
+    }
+}

+ 66 - 15
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);
+
+    // 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;
-    // m_copiedFiles will keep the files's VNoteFile.
+
+    // 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();
-    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();
+    // Paste files from clipboard.
+    void pasteFilesFromClipboard();
+
+    // Delete selected files.
+    void deleteSelectedFiles();
+
+    // Delete files @p_files.
+    void deleteFiles(const QVector<VNoteFile *> &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<QString> &p_files,
+                    bool p_isCut);
+
     // New items have been added to direcotry. Update file list accordingly.
     QVector<QListWidgetItem *> updateFileListAdded();
 
     inline QPointer<VNoteFile> getVFile(QListWidgetItem *p_item) const;
 
-    // Check if the list items match exactly the contents of the directory.
-    bool identicalListWithDirectory() const;
     QList<QListWidgetItem *> 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<VDirectory> m_directory;
-    QVector<QPointer<VNoteFile> > 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.

+ 15 - 17
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;
-        }
-    }
-
-    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."));
+    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")));
     }
-
-    msgBox.exec();
 }
 
 void VMainWindow::changeMarkdownConverter(QAction *action)

+ 25 - 7
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.<br>"
+            QVector<ConfirmItemInfo> 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.<br>"
                               "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);
+                }
             }
         }
 

+ 207 - 16
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<QString> &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<int> p_sortedIdx)
+bool VNoteFile::sortAttachments(const QVector<int> &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<ImageLink> 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<VAttachment> 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;
+}
+

+ 42 - 11
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<VAttachment> &getAttachments() const;
 
+    void setAttachments(const QVector<VAttachment> &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<QString> &p_names);
 
     // Reorder attachments in m_attachments by index.
-    void sortAttachments(QVector<int> p_sortedIdx);
+    bool sortAttachments(const QVector<int> &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<VAttachment> &VNoteFile::getAttachments() const
 {
     return m_attachments;
 }
 
+inline void VNoteFile::setAttachments(const QVector<VAttachment> &p_attas)
+{
+    m_attachments = p_attas;
+}
+
 #endif // VNOTEFILE_H