Browse Source

support attachments

Support attachments to internal note file.

- Add/Delete/Clear/Sort;
- Support custom attachment folder for each notebook (read-only);
- Support renaming attachment;
Le Tan 8 years ago
parent
commit
01788a5301
43 changed files with 1552 additions and 164 deletions
  1. 36 14
      src/dialog/vconfirmdeletiondialog.cpp
  2. 11 0
      src/dialog/vconfirmdeletiondialog.h
  3. 2 2
      src/dialog/vdirinfodialog.cpp
  4. 9 4
      src/dialog/vfileinfodialog.cpp
  5. 33 11
      src/dialog/vnewnotebookdialog.cpp
  6. 6 3
      src/dialog/vnewnotebookdialog.h
  7. 13 4
      src/dialog/vnotebookinfodialog.cpp
  8. 2 0
      src/dialog/vnotebookinfodialog.h
  9. 61 2
      src/dialog/vsettingsdialog.cpp
  10. 8 0
      src/dialog/vsettingsdialog.h
  11. 205 0
      src/dialog/vsortdialog.cpp
  12. 46 0
      src/dialog/vsortdialog.h
  13. 7 0
      src/resources/icons/add_attachment.svg
  14. 15 0
      src/resources/icons/attachment.svg
  15. 14 0
      src/resources/icons/attachment_full.svg
  16. 10 0
      src/resources/icons/clear_attachment.svg
  17. 10 0
      src/resources/icons/delete_attachment.svg
  18. 10 0
      src/resources/icons/locate_attachment.svg
  19. 12 0
      src/resources/icons/sort.svg
  20. 3 0
      src/resources/vnote.ini
  21. 6 2
      src/src.pro
  22. 43 4
      src/utils/vutils.cpp
  23. 10 0
      src/utils/vutils.h
  24. 404 0
      src/vattachmentlist.cpp
  25. 60 0
      src/vattachmentlist.h
  26. 20 66
      src/vbuttonwithwidget.cpp
  27. 31 13
      src/vbuttonwithwidget.h
  28. 8 1
      src/vconfigmanager.cpp
  29. 35 0
      src/vconfigmanager.h
  30. 2 0
      src/vconstants.h
  31. 7 0
      src/vdirectory.cpp
  32. 6 0
      src/vdirectory.h
  33. 49 1
      src/vmainwindow.cpp
  34. 13 3
      src/vmainwindow.h
  35. 4 0
      src/vmdedit.cpp
  36. 7 0
      src/vnote.qrc
  37. 42 4
      src/vnotebook.cpp
  38. 18 5
      src/vnotebook.h
  39. 13 5
      src/vnotebookselector.cpp
  40. 4 2
      src/vnotebookselector.h
  41. 178 3
      src/vnotefile.cpp
  42. 65 1
      src/vnotefile.h
  43. 14 14
      src/vvimindicator.cpp

+ 36 - 14
src/dialog/vconfirmdeletiondialog.cpp

@@ -56,8 +56,14 @@ private:
 VConfirmDeletionDialog::VConfirmDeletionDialog(const QString &p_title,
                                                const QString &p_info,
                                                const QVector<QString> &p_files,
+                                               bool p_enableAskAgain,
+                                               bool p_askAgainEnabled,
+                                               bool p_enablePreview,
                                                QWidget *p_parent)
-    : QDialog(p_parent)
+    : QDialog(p_parent),
+      m_enableAskAgain(p_enableAskAgain),
+      m_askAgainEnabled(p_askAgainEnabled),
+      m_enablePreview(p_enablePreview)
 {
     setupUI(p_title, p_info);
 
@@ -66,33 +72,44 @@ VConfirmDeletionDialog::VConfirmDeletionDialog(const QString &p_title,
 
 void VConfirmDeletionDialog::setupUI(const QString &p_title, const QString &p_info)
 {
-    QLabel *infoLabel = new QLabel(p_info);
+    QLabel *infoLabel = NULL;
+    if (!p_info.isEmpty()) {
+        infoLabel = new QLabel(p_info);
+        infoLabel->setWordWrap(true);
+    }
+
     m_listWidget = new QListWidget();
     connect(m_listWidget, &QListWidget::currentRowChanged,
             this, &VConfirmDeletionDialog::currentFileChanged);
 
     m_previewer = new QLabel();
 
-    m_askAgainCB = new QCheckBox(tr("Just delete them and do not ask for confirmation again"));
-    m_askAgainCB->setChecked(!g_config->getConfirmImagesCleanUp());
+    m_askAgainCB = new QCheckBox(tr("Do not ask for confirmation again"));
+    m_askAgainCB->setChecked(!m_askAgainEnabled);
+    m_askAgainCB->setVisible(m_enableAskAgain);
 
     // Ok is the default button.
     m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
-    connect(m_btnBox, &QDialogButtonBox::accepted,
-            this, [this]() {
-                g_config->setConfirmImagesCleanUp(!m_askAgainCB->isChecked());
-                QDialog::accept();
-            });
+    connect(m_btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
     connect(m_btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+    m_btnBox->button(QDialogButtonBox::Ok)->setStyleSheet(g_config->c_dangerBtnStyle);
 
     QHBoxLayout *midLayout = new QHBoxLayout;
     midLayout->addWidget(m_listWidget);
-    midLayout->addStretch();
-    midLayout->addWidget(m_previewer);
-    midLayout->addStretch();
+    if (m_enablePreview) {
+        midLayout->addStretch();
+        midLayout->addWidget(m_previewer);
+        midLayout->addStretch();
+    } else {
+        midLayout->addWidget(m_previewer);
+        m_previewer->setVisible(false);
+    }
 
     QVBoxLayout *mainLayout = new QVBoxLayout;
-    mainLayout->addWidget(infoLabel);
+    if (infoLabel) {
+        mainLayout->addWidget(infoLabel);
+    }
+
     mainLayout->addWidget(m_askAgainCB);
     mainLayout->addWidget(m_btnBox);
     mainLayout->addLayout(midLayout);
@@ -137,10 +154,15 @@ void VConfirmDeletionDialog::initFileItems(const QVector<QString> &p_files)
     m_listWidget->setCurrentRow(0);
 }
 
+bool VConfirmDeletionDialog::getAskAgainEnabled() const
+{
+    return !m_askAgainCB->isChecked();
+}
+
 void VConfirmDeletionDialog::currentFileChanged(int p_row)
 {
     bool succeed = false;
-    if (p_row > -1) {
+    if (p_row > -1 && m_enablePreview) {
         ConfirmItemWidget *widget = getItemWidget(m_listWidget->item(p_row));
         if (widget) {
             QPixmap image(widget->getFile());

+ 11 - 0
src/dialog/vconfirmdeletiondialog.h

@@ -19,10 +19,15 @@ public:
     VConfirmDeletionDialog(const QString &p_title,
                            const QString &p_info,
                            const QVector<QString> &p_files,
+                           bool p_enableAskAgain,
+                           bool p_askAgainEnabled,
+                           bool p_enablePreview,
                            QWidget *p_parent = 0);
 
     QVector<QString> getConfirmedFiles() const;
 
+    bool getAskAgainEnabled() const;
+
 private slots:
     void currentFileChanged(int p_row);
 
@@ -37,6 +42,12 @@ private:
     QLabel *m_previewer;
     QDialogButtonBox *m_btnBox;
     QCheckBox *m_askAgainCB;
+
+    bool m_enableAskAgain;
+    // Init value if m_enableAskAgain is true.
+    bool m_askAgainEnabled;
+
+    bool m_enablePreview;
 };
 
 #endif // VCONFIRMDELETIONDIALOG_H

+ 2 - 2
src/dialog/vdirinfodialog.cpp

@@ -2,6 +2,7 @@
 #include "vdirinfodialog.h"
 #include "vdirectory.h"
 #include "vconfigmanager.h"
+#include "utils/vutils.h"
 
 extern VConfigManager *g_config;
 
@@ -31,8 +32,7 @@ void VDirInfoDialog::setupUI()
     nameEdit->selectAll();
 
     // Created time.
-    QString createdTimeStr = m_directory->getCreatedTimeUtc().toLocalTime()
-                                                             .toString(Qt::DefaultLocaleLongDate);
+    QString createdTimeStr = VUtils::displayDateTime(m_directory->getCreatedTimeUtc().toLocalTime());
     QLabel *createdTimeLabel = new QLabel(createdTimeStr);
 
     QFormLayout *topLayout = new QFormLayout();

+ 9 - 4
src/dialog/vfileinfodialog.cpp

@@ -40,19 +40,24 @@ void VFileInfoDialog::setupUI(const QString &p_title, const QString &p_info)
     // Select without suffix.
     nameEdit->setSelection(baseStart, baseLength);
 
+    // Attachment folder.
+    QLineEdit *attachmentFolderEdit = new QLineEdit(m_file->getAttachmentFolder());
+    attachmentFolderEdit->setPlaceholderText(tr("Will be assigned when adding attachments"));
+    attachmentFolderEdit->setToolTip(tr("The folder to hold attachments of this note"));
+    attachmentFolderEdit->setReadOnly(true);
+
     // Created time.
-    QString createdTimeStr = m_file->getCreatedTimeUtc().toLocalTime()
-                                                        .toString(Qt::DefaultLocaleLongDate);
+    QString createdTimeStr = VUtils::displayDateTime(m_file->getCreatedTimeUtc().toLocalTime());
     QLabel *createdTimeLabel = new QLabel(createdTimeStr);
 
     // Modified time.
-    createdTimeStr = m_file->getModifiedTimeUtc().toLocalTime()
-                                                 .toString(Qt::DefaultLocaleLongDate);
+    createdTimeStr = VUtils::displayDateTime(m_file->getModifiedTimeUtc().toLocalTime());
     QLabel *modifiedTimeLabel = new QLabel(createdTimeStr);
     modifiedTimeLabel->setToolTip(tr("Last modified time within VNote"));
 
     QFormLayout *topLayout = new QFormLayout();
     topLayout->addRow(tr("Note &name:"), nameEdit);
+    topLayout->addRow(tr("Attachment folder:"), attachmentFolderEdit);
     topLayout->addRow(tr("Created time:"), createdTimeLabel);
     topLayout->addRow(tr("Modified time:"), modifiedTimeLabel);
 

+ 33 - 11
src/dialog/vnewnotebookdialog.cpp

@@ -12,11 +12,11 @@ VNewNotebookDialog::VNewNotebookDialog(const QString &title, const QString &info
                                        const QVector<VNotebook *> &p_notebooks,
                                        QWidget *parent)
     : QDialog(parent),
-      title(title), info(info), defaultName(defaultName), defaultPath(defaultPath),
+      defaultName(defaultName), defaultPath(defaultPath),
       m_importNotebook(false), m_manualPath(false), m_manualName(false),
       m_notebooks(p_notebooks)
 {
-    setupUI();
+    setupUI(title, info);
 
     connect(nameEdit, &QLineEdit::textChanged, this, &VNewNotebookDialog::handleInputChanged);
     connect(pathEdit, &QLineEdit::textChanged, this, &VNewNotebookDialog::handleInputChanged);
@@ -25,11 +25,11 @@ VNewNotebookDialog::VNewNotebookDialog(const QString &title, const QString &info
     handleInputChanged();
 }
 
-void VNewNotebookDialog::setupUI()
+void VNewNotebookDialog::setupUI(const QString &p_title, const QString &p_info)
 {
     QLabel *infoLabel = NULL;
-    if (!info.isEmpty()) {
-        infoLabel = new QLabel(info);
+    if (!p_info.isEmpty()) {
+        infoLabel = new QLabel(p_info);
         infoLabel->setWordWrap(true);
     }
 
@@ -44,16 +44,26 @@ void VNewNotebookDialog::setupUI()
 
     QLabel *imageFolderLabel = new QLabel(tr("&Image folder:"));
     m_imageFolderEdit = new QLineEdit();
+    imageFolderLabel->setBuddy(m_imageFolderEdit);
     m_imageFolderEdit->setPlaceholderText(tr("Use global configuration (%1)")
                                             .arg(g_config->getImageFolder()));
-    imageFolderLabel->setBuddy(m_imageFolderEdit);
-    QString imageFolderTip = tr("Set the name of the folder for all the notes of this notebook to store images "
-                                "(empty to use global configuration)");
-    m_imageFolderEdit->setToolTip(imageFolderTip);
-    imageFolderLabel->setToolTip(imageFolderTip);
+    m_imageFolderEdit->setToolTip(tr("Set the name of the folder to hold images of all the notes in this notebook "
+                                     "(empty to use global configuration)"));
+    imageFolderLabel->setToolTip(m_imageFolderEdit->toolTip());
     QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp), m_imageFolderEdit);
     m_imageFolderEdit->setValidator(validator);
 
+    QLabel *attachmentFolderLabel = new QLabel(tr("&Attachment folder:"));
+    m_attachmentFolderEdit = new QLineEdit();
+    attachmentFolderLabel->setBuddy(m_attachmentFolderEdit);
+    m_attachmentFolderEdit->setPlaceholderText(tr("Use global configuration (%1)")
+                                                 .arg(g_config->getAttachmentFolder()));
+    m_attachmentFolderEdit->setToolTip(tr("Set the name of the folder to hold attachments of all the notes in this notebook "
+                                          "(empty to use global configuration, read-only once created)"));
+    attachmentFolderLabel->setToolTip(m_attachmentFolderEdit->toolTip());
+    validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp), m_attachmentFolderEdit);
+    m_attachmentFolderEdit->setValidator(validator);
+
     QGridLayout *topLayout = new QGridLayout();
     topLayout->addWidget(nameLabel, 0, 0);
     topLayout->addWidget(nameEdit, 0, 1, 1, 2);
@@ -62,6 +72,8 @@ void VNewNotebookDialog::setupUI()
     topLayout->addWidget(browseBtn, 1, 2);
     topLayout->addWidget(imageFolderLabel, 2, 0);
     topLayout->addWidget(m_imageFolderEdit, 2, 1);
+    topLayout->addWidget(attachmentFolderLabel, 3, 0);
+    topLayout->addWidget(m_attachmentFolderEdit, 3, 1);
 
     // Warning label.
     m_warnLabel = new QLabel();
@@ -87,7 +99,7 @@ void VNewNotebookDialog::setupUI()
     // Will set the parent of above widgets properly.
     setLayout(mainLayout);
     mainLayout->setSizeConstraint(QLayout::SetFixedSize);
-    setWindowTitle(title);
+    setWindowTitle(p_title);
 }
 
 QString VNewNotebookDialog::getNameInput() const
@@ -111,6 +123,15 @@ QString VNewNotebookDialog::getImageFolder() const
     }
 }
 
+QString VNewNotebookDialog::getAttachmentFolder() const
+{
+    if (m_attachmentFolderEdit->isEnabled()) {
+        return m_attachmentFolderEdit->text();
+    } else {
+        return QString();
+    }
+}
+
 void VNewNotebookDialog::handleBrowseBtnClicked()
 {
     static QString defaultPath;
@@ -251,6 +272,7 @@ void VNewNotebookDialog::handleInputChanged()
     m_warnLabel->setVisible(showWarnLabel);
     m_importNotebook = configExist;
     m_imageFolderEdit->setEnabled(!m_importNotebook);
+    m_attachmentFolderEdit->setEnabled(!m_importNotebook);
 
     QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok);
     okBtn->setEnabled(nameOk && pathOk);

+ 6 - 3
src/dialog/vnewnotebookdialog.h

@@ -29,6 +29,10 @@ public:
     // Empty string indicates using global config.
     QString getImageFolder() const;
 
+    // Get the custom attachment folder for this notebook.
+    // Empty string indicates using global config.
+    QString getAttachmentFolder() const;
+
 private slots:
     void handleBrowseBtnClicked();
 
@@ -39,7 +43,7 @@ protected:
     void showEvent(QShowEvent *event) Q_DECL_OVERRIDE;
 
 private:
-    void setupUI();
+    void setupUI(const QString &p_title, const QString &p_info);
 
     // Should be called before enableOkButton() when path changed.
     void checkRootFolder(const QString &p_path);
@@ -53,10 +57,9 @@ private:
     QPushButton *browseBtn;
     QLabel *m_warnLabel;
     QLineEdit *m_imageFolderEdit;
+    QLineEdit *m_attachmentFolderEdit;
     QDialogButtonBox *m_btnBox;
 
-    QString title;
-    QString info;
     QString defaultName;
     QString defaultPath;
 

+ 13 - 4
src/dialog/vnotebookinfodialog.cpp

@@ -35,28 +35,37 @@ void VNotebookInfoDialog::setupUI(const QString &p_title, const QString &p_info)
     m_pathEdit = new QLineEdit(m_notebook->getPath());
     m_pathEdit->setReadOnly(true);
 
+    // Image folder.
     m_imageFolderEdit = new QLineEdit(m_notebook->getImageFolderConfig());
     m_imageFolderEdit->setPlaceholderText(tr("Use global configuration (%1)")
                                             .arg(g_config->getImageFolder()));
-    m_imageFolderEdit->setToolTip(tr("Set the name of the folder for all the notes of this notebook to store images "
+    m_imageFolderEdit->setToolTip(tr("Set the name of the folder to hold images of all the notes in this notebook "
                                      "(empty to use global configuration)"));
     QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp), m_imageFolderEdit);
     m_imageFolderEdit->setValidator(validator);
 
+    // Attachment folder.
+    Q_ASSERT(!m_notebook->getAttachmentFolder().isEmpty());
+    m_attachmentFolderEdit = new QLineEdit(m_notebook->getAttachmentFolder());
+    m_attachmentFolderEdit->setPlaceholderText(tr("Use global configuration (%1)")
+                                                 .arg(g_config->getAttachmentFolder()));
+    m_attachmentFolderEdit->setToolTip(tr("The folder to hold attachments of all the notes in this notebook"));
+    m_attachmentFolderEdit->setReadOnly(true);
+
     // Recycle bin folder.
     QLineEdit *recycleBinFolderEdit = new QLineEdit(m_notebook->getRecycleBinFolder());
     recycleBinFolderEdit->setReadOnly(true);
-    recycleBinFolderEdit->setToolTip(tr("The folder to hold deleted files from within VNote"));
+    recycleBinFolderEdit->setToolTip(tr("The folder to hold deleted files from within VNote of all the notes in this notebook"));
 
     // Created time.
-    QString createdTimeStr = const_cast<VNotebook *>(m_notebook)->getCreatedTimeUtc().toLocalTime()
-                                                                                     .toString(Qt::DefaultLocaleLongDate);
+    QString createdTimeStr = VUtils::displayDateTime(const_cast<VNotebook *>(m_notebook)->getCreatedTimeUtc().toLocalTime());
     QLabel *createdTimeLabel = new QLabel(createdTimeStr);
 
     QFormLayout *topLayout = new QFormLayout();
     topLayout->addRow(tr("Notebook &name:"), m_nameEdit);
     topLayout->addRow(tr("Notebook &root folder:"), m_pathEdit);
     topLayout->addRow(tr("&Image folder:"), m_imageFolderEdit);
+    topLayout->addRow(tr("Attachment folder:"), m_attachmentFolderEdit);
     topLayout->addRow(tr("Recycle bin folder:"), recycleBinFolderEdit);
     topLayout->addRow(tr("Created time:"), createdTimeLabel);
 

+ 2 - 0
src/dialog/vnotebookinfodialog.h

@@ -41,6 +41,8 @@ private:
     QLineEdit *m_nameEdit;
     QLineEdit *m_pathEdit;
     QLineEdit *m_imageFolderEdit;
+    // Read-only.
+    QLineEdit *m_attachmentFolderEdit;
     QLabel *m_warnLabel;
     QDialogButtonBox *m_btnBox;
     const QVector<VNotebook *> &m_notebooks;

+ 61 - 2
src/dialog/vsettingsdialog.cpp

@@ -295,7 +295,7 @@ VNoteManagementTab::VNoteManagementTab(QWidget *p_parent)
     // Note.
     // Image folder.
     m_customImageFolder = new QCheckBox(tr("Custom image folder"), this);
-    m_customImageFolder->setToolTip(tr("Set the global name of the image folder to store images "
+    m_customImageFolder->setToolTip(tr("Set the global name of the image folder to hold images "
                                        "of notes (restart VNote to make it work)"));
     connect(m_customImageFolder, &QCheckBox::stateChanged,
             this, &VNoteManagementTab::customImageFolderChanged);
@@ -310,14 +310,32 @@ VNoteManagementTab::VNoteManagementTab(QWidget *p_parent)
     imageFolderLayout->addWidget(m_customImageFolder);
     imageFolderLayout->addWidget(m_imageFolderEdit);
 
+    // Attachment folder.
+    m_customAttachmentFolder = new QCheckBox(tr("Custom attachment folder"), this);
+    m_customAttachmentFolder->setToolTip(tr("Set the global name of the attachment folder to hold attachments "
+                                            "of notes (restart VNote to make it work)"));
+    connect(m_customAttachmentFolder, &QCheckBox::stateChanged,
+            this, &VNoteManagementTab::customAttachmentFolderChanged);
+
+    m_attachmentFolderEdit = new QLineEdit(this);
+    m_attachmentFolderEdit->setPlaceholderText(tr("Name of the attachment folder"));
+    m_attachmentFolderEdit->setToolTip(m_customAttachmentFolder->toolTip());
+    validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp), this);
+    m_attachmentFolderEdit->setValidator(validator);
+
+    QHBoxLayout *attachmentFolderLayout = new QHBoxLayout();
+    attachmentFolderLayout->addWidget(m_customAttachmentFolder);
+    attachmentFolderLayout->addWidget(m_attachmentFolderEdit);
+
     QFormLayout *noteLayout = new QFormLayout();
     noteLayout->addRow(imageFolderLayout);
+    noteLayout->addRow(attachmentFolderLayout);
     m_noteBox->setLayout(noteLayout);
 
     // External File.
     // Image folder.
     m_customImageFolderExt = new QCheckBox(tr("Custom image folder"), this);
-    m_customImageFolderExt->setToolTip(tr("Set the path of the global image folder to store images "
+    m_customImageFolderExt->setToolTip(tr("Set the path of the global image folder to hold images "
                                           "of external files (restart VNote to make it work).\nYou "
                                           "could use both absolute or relative path here. If "
                                           "absolute path is used, VNote will not manage\nthose images, "
@@ -350,6 +368,10 @@ bool VNoteManagementTab::loadConfiguration()
         return false;
     }
 
+    if (!loadAttachmentFolder()) {
+        return false;
+    }
+
     if (!loadImageFolderExt()) {
         return false;
     }
@@ -363,6 +385,10 @@ bool VNoteManagementTab::saveConfiguration()
         return false;
     }
 
+    if (!saveAttachmentFolder()) {
+        return false;
+    }
+
     if (!saveImageFolderExt()) {
         return false;
     }
@@ -403,6 +429,39 @@ void VNoteManagementTab::customImageFolderChanged(int p_state)
     }
 }
 
+bool VNoteManagementTab::loadAttachmentFolder()
+{
+    bool isCustom = g_config->isCustomAttachmentFolder();
+
+    m_customAttachmentFolder->setChecked(isCustom);
+    m_attachmentFolderEdit->setText(g_config->getAttachmentFolder());
+    m_attachmentFolderEdit->setEnabled(isCustom);
+
+    return true;
+}
+
+bool VNoteManagementTab::saveAttachmentFolder()
+{
+    if (m_customAttachmentFolder->isChecked()) {
+        g_config->setAttachmentFolder(m_attachmentFolderEdit->text());
+    } else {
+        g_config->setAttachmentFolder("");
+    }
+
+    return true;
+}
+
+void VNoteManagementTab::customAttachmentFolderChanged(int p_state)
+{
+    if (p_state == Qt::Checked) {
+        m_attachmentFolderEdit->setEnabled(true);
+        m_attachmentFolderEdit->selectAll();
+        m_attachmentFolderEdit->setFocus();
+    } else {
+        m_attachmentFolderEdit->setEnabled(false);
+    }
+}
+
 bool VNoteManagementTab::loadImageFolderExt()
 {
     bool isCustom = g_config->isCustomImageFolderExt();

+ 8 - 0
src/dialog/vsettingsdialog.h

@@ -69,9 +69,14 @@ public:
     QCheckBox *m_customImageFolderExt;
     QLineEdit *m_imageFolderEditExt;
 
+    // Attachment folder.
+    QCheckBox *m_customAttachmentFolder;
+    QLineEdit *m_attachmentFolderEdit;
+
 private slots:
     void customImageFolderChanged(int p_state);
     void customImageFolderExtChanged(int p_state);
+    void customAttachmentFolderChanged(int p_state);
 
 private:
     bool loadImageFolder();
@@ -79,6 +84,9 @@ private:
 
     bool loadImageFolderExt();
     bool saveImageFolderExt();
+
+    bool loadAttachmentFolder();
+    bool saveAttachmentFolder();
 };
 
 class VMarkdownTab : public QWidget

+ 205 - 0
src/dialog/vsortdialog.cpp

@@ -0,0 +1,205 @@
+#include "vsortdialog.h"
+
+#include <QtWidgets>
+
+VSortDialog::VSortDialog(const QString &p_title,
+                         const QString &p_info,
+                         QWidget *p_parent)
+    : QDialog(p_parent)
+{
+    setupUI(p_title, p_info);
+}
+
+void VSortDialog::setupUI(const QString &p_title, const QString &p_info)
+{
+    QLabel *infoLabel = NULL;
+    if (!p_info.isEmpty()) {
+        infoLabel = new QLabel(p_info);
+        infoLabel->setWordWrap(true);
+    }
+
+    m_treeWidget = new QTreeWidget();
+    m_treeWidget->setRootIsDecorated(false);
+    m_treeWidget->setSelectionMode(QAbstractItemView::ContiguousSelection);
+    m_treeWidget->setDragDropMode(QAbstractItemView::InternalMove);
+
+    // Buttons for top/up/down/bottom.
+    m_topBtn = new QPushButton(tr("&Top"));
+    m_topBtn->setToolTip(tr("Move selected items to top"));
+    connect(m_topBtn, &QPushButton::clicked,
+            this, [this]() {
+                this->handleMoveOperation(MoveOperation::Top);
+            });
+
+    m_upBtn = new QPushButton(tr("&Up"));
+    m_upBtn->setToolTip(tr("Move selected items up"));
+    connect(m_upBtn, &QPushButton::clicked,
+            this, [this]() {
+                this->handleMoveOperation(MoveOperation::Up);
+            });
+
+    m_downBtn = new QPushButton(tr("&Down"));
+    m_downBtn->setToolTip(tr("Move selected items down"));
+    connect(m_downBtn, &QPushButton::clicked,
+            this, [this]() {
+                this->handleMoveOperation(MoveOperation::Down);
+            });
+
+    m_bottomBtn = new QPushButton(tr("&Bottom"));
+    m_bottomBtn->setToolTip(tr("Move selected items to bottom"));
+    connect(m_bottomBtn, &QPushButton::clicked,
+            this, [this]() {
+                this->handleMoveOperation(MoveOperation::Bottom);
+            });
+
+    QVBoxLayout *btnLayout = new QVBoxLayout;
+    btnLayout->addWidget(m_topBtn);
+    btnLayout->addWidget(m_upBtn);
+    btnLayout->addWidget(m_downBtn);
+    btnLayout->addWidget(m_bottomBtn);
+    btnLayout->addStretch();
+
+    QHBoxLayout *midLayout = new QHBoxLayout;
+    midLayout->addWidget(m_treeWidget);
+    midLayout->addLayout(btnLayout);
+
+    // Ok is the default button.
+    m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    connect(m_btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+    connect(m_btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    if (infoLabel) {
+        mainLayout->addWidget(infoLabel);
+    }
+
+    mainLayout->addLayout(midLayout);
+    mainLayout->addWidget(m_btnBox);
+
+    setLayout(mainLayout);
+    setWindowTitle(p_title);
+}
+
+void VSortDialog::treeUpdated()
+{
+    // We just need single level.
+    int cnt = m_treeWidget->topLevelItemCount();
+    for (int i = 0; i < cnt; ++i) {
+        QTreeWidgetItem *item = m_treeWidget->topLevelItem(i);
+        item->setFlags(item->flags() & ~Qt::ItemIsDropEnabled);
+    }
+}
+
+void VSortDialog::handleMoveOperation(MoveOperation p_op)
+{
+    const QList<QTreeWidgetItem *> selectedItems = m_treeWidget->selectedItems();
+    if (selectedItems.isEmpty()) {
+        return;
+    }
+
+    int first = m_treeWidget->topLevelItemCount();
+    int last = -1;
+    for (auto const & it : selectedItems) {
+        int idx = m_treeWidget->indexOfTopLevelItem(it);
+        Q_ASSERT(idx > -1);
+        if (idx < first) {
+            first = idx;
+        }
+
+        if (idx > last) {
+            last = idx;
+        }
+    }
+
+    Q_ASSERT(first <= last && (last - first + 1) == selectedItems.size());
+    QTreeWidgetItem *firstItem = NULL;
+
+    switch (p_op) {
+    case MoveOperation::Top:
+        if (first == 0) {
+            break;
+        }
+
+        m_treeWidget->clearSelection();
+
+        // Insert item[last] to index 0 repeatedly.
+        for (int i = last - first; i >= 0; --i) {
+            QTreeWidgetItem *item = m_treeWidget->takeTopLevelItem(last);
+            Q_ASSERT(item);
+            m_treeWidget->insertTopLevelItem(0, item);
+            item->setSelected(true);
+        }
+
+        firstItem = m_treeWidget->topLevelItem(0);
+
+        break;
+
+    case MoveOperation::Up:
+        if (first == 0) {
+            break;
+        }
+
+        m_treeWidget->clearSelection();
+
+        // Insert item[last] to index (first -1) repeatedly.
+        for (int i = last - first; i >= 0; --i) {
+            QTreeWidgetItem *item = m_treeWidget->takeTopLevelItem(last);
+            Q_ASSERT(item);
+            m_treeWidget->insertTopLevelItem(first - 1, item);
+            item->setSelected(true);
+        }
+
+        firstItem = m_treeWidget->topLevelItem(first - 1);
+
+        break;
+
+    case MoveOperation::Down:
+        if (last == m_treeWidget->topLevelItemCount() - 1) {
+            break;
+        }
+
+        m_treeWidget->clearSelection();
+
+        // Insert item[first] to index (last) repeatedly.
+        for (int i = last - first; i >= 0; --i) {
+            QTreeWidgetItem *item = m_treeWidget->takeTopLevelItem(first);
+            Q_ASSERT(item);
+            m_treeWidget->insertTopLevelItem(last + 1, item);
+            item->setSelected(true);
+
+            if (!firstItem) {
+                firstItem = item;
+            }
+        }
+
+        break;
+
+    case MoveOperation::Bottom:
+        if (last == m_treeWidget->topLevelItemCount() - 1) {
+            break;
+        }
+
+        m_treeWidget->clearSelection();
+
+        // Insert item[first] to the last of the tree repeatedly.
+        for (int i = last - first; i >= 0; --i) {
+            QTreeWidgetItem *item = m_treeWidget->takeTopLevelItem(first);
+            Q_ASSERT(item);
+            m_treeWidget->addTopLevelItem(item);
+            item->setSelected(true);
+
+            if (!firstItem) {
+                firstItem = item;
+            }
+        }
+
+        break;
+
+    default:
+        return;
+    }
+
+    if (firstItem) {
+        m_treeWidget->scrollToItem(firstItem);
+    }
+}

+ 46 - 0
src/dialog/vsortdialog.h

@@ -0,0 +1,46 @@
+#ifndef VSORTDIALOG_H
+#define VSORTDIALOG_H
+
+#include <QDialog>
+#include <QVector>
+
+class QPushButton;
+class QDialogButtonBox;
+class QTreeWidget;
+
+class VSortDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    VSortDialog(const QString &p_title,
+                const QString &p_info,
+                QWidget *p_parent = 0);
+
+    QTreeWidget *getTreeWidget() const;
+
+    // Called after updating the m_treeWidget.
+    void treeUpdated();
+
+private:
+    enum MoveOperation { Top, Up, Down, Bottom };
+
+private slots:
+    void handleMoveOperation(MoveOperation p_op);
+
+private:
+    void setupUI(const QString &p_title, const QString &p_info);
+
+    QTreeWidget *m_treeWidget;
+    QPushButton *m_topBtn;
+    QPushButton *m_upBtn;
+    QPushButton *m_downBtn;
+    QPushButton *m_bottomBtn;
+    QDialogButtonBox *m_btnBox;
+};
+
+inline QTreeWidget *VSortDialog::getTreeWidget() const
+{
+    return m_treeWidget;
+}
+
+#endif // VSORTDIALOG_H

+ 7 - 0
src/resources/icons/add_attachment.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<polygon points="448,224 288,224 288,64 224,64 224,224 64,224 64,288 224,288 224,448 288,448 288,288 448,288 "/>
+</svg>

+ 15 - 0
src/resources/icons/attachment.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g id="Icon_3_">
+	<g>
+		<path d="M341.334,128v234.666C341.334,409.604,302.938,448,256,448c-46.937,0-85.333-38.396-85.333-85.334V117.334
+			C170.667,87.469,194.135,64,224,64c29.864,0,53.333,23.469,53.333,53.334v245.333c0,11.729-9.605,21.333-21.334,21.333
+			c-11.729,0-21.333-9.604-21.333-21.333V160h-32v202.667C202.667,392.531,226.135,416,256,416
+			c29.865,0,53.334-23.469,53.334-53.333V117.334C309.334,70.401,270.938,32,224,32c-46.938,0-85.334,38.401-85.334,85.334v245.332
+			C138.667,427.729,190.938,480,256,480c65.062,0,117.334-52.271,117.334-117.334V128H341.334z"/>
+	</g>
+</g>
+</svg>

+ 14 - 0
src/resources/icons/attachment_full.svg

@@ -0,0 +1,14 @@
+<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <g>
+  <title>Layer 1</title>
+  <g id="Icon_3_">
+   <g id="svg_1">
+    <path d="m341.334,128l0,234.666c0,46.938 -38.396,85.334 -85.334,85.334c-46.937,0 -85.333,-38.396 -85.333,-85.334l0,-245.332c0,-29.865 23.468,-53.334 53.333,-53.334c29.864,0 53.333,23.469 53.333,53.334l0,245.333c0,11.729 -9.605,21.333 -21.334,21.333c-11.729,0 -21.333,-9.604 -21.333,-21.333l0,-202.667l-32,0l0,202.667c0.001,29.864 23.469,53.333 53.334,53.333c29.865,0 53.334,-23.469 53.334,-53.333l0,-245.333c0,-46.933 -38.396,-85.334 -85.334,-85.334c-46.938,0 -85.334,38.401 -85.334,85.334l0,245.332c0.001,65.063 52.272,117.334 117.334,117.334c65.062,0 117.334,-52.271 117.334,-117.334l0,-234.666l-32,0z" id="svg_2"/>
+   </g>
+  </g>
+ </g>
+ <g>
+  <title>Layer 2</title>
+  <circle stroke="#000000" fill="#15ae67" stroke-width="5" stroke-opacity="0" cx="435.5" cy="75.50001" r="70.05334" id="svg_3"/>
+ </g>
+</svg>

+ 10 - 0
src/resources/icons/clear_attachment.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<path style="fill:#C9302C" d="M443.6,387.1L312.4,255.4l131.5-130c5.4-5.4,5.4-14.2,0-19.6l-37.4-37.6c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4
+	L256,197.8L124.9,68.3c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4L68,105.9c-5.4,5.4-5.4,14.2,0,19.6l131.5,130L68.4,387.1
+	c-2.6,2.6-4.1,6.1-4.1,9.8c0,3.7,1.4,7.2,4.1,9.8l37.4,37.6c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1L256,313.1l130.7,131.1
+	c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1l37.4-37.6c2.6-2.6,4.1-6.1,4.1-9.8C447.7,393.2,446.2,389.7,443.6,387.1z"/>
+</svg>

+ 10 - 0
src/resources/icons/delete_attachment.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<path d="M341,128V99c0-19.1-14.5-35-34.5-35H205.4C185.5,64,171,79.9,171,99v29H80v32h9.2c0,0,5.4,0.6,8.2,3.4c2.8,2.8,3.9,9,3.9,9
+	l19,241.7c1.5,29.4,1.5,33.9,36,33.9h199.4c34.5,0,34.5-4.4,36-33.8l19-241.6c0,0,1.1-6.3,3.9-9.1c2.8-2.8,8.2-3.4,8.2-3.4h9.2v-32
+	h-91V128z M192,99c0-9.6,7.8-15,17.7-15h91.7c9.9,0,18.6,5.5,18.6,15v29H192V99z M183.5,384l-10.3-192h20.3L204,384H183.5z
+	 M267.1,384h-22V192h22V384z M328.7,384h-20.4l10.5-192h20.3L328.7,384z"/>
+</svg>

+ 10 - 0
src/resources/icons/locate_attachment.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<path d="M437.334,144H256.006l-42.668-48H74.666C51.197,96,32,115.198,32,138.667v234.666C32,396.802,51.197,416,74.666,416h362.668
+	C460.803,416,480,396.802,480,373.333V186.667C480,163.198,460.803,144,437.334,144z M448,373.333
+	c0,5.782-4.885,10.667-10.666,10.667H74.666C68.884,384,64,379.115,64,373.333V176h373.334c5.781,0,10.666,4.885,10.666,10.667
+	V373.333z"/>
+</svg>

+ 12 - 0
src/resources/icons/sort.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<g>
+	<rect x="80" y="352" width="64" height="64"/>
+	<rect x="176" y="288" width="64" height="128"/>
+	<rect x="272" y="192" width="64" height="224"/>
+	<rect x="368" y="96" width="64" height="320"/>
+</g>
+</svg>

+ 3 - 0
src/resources/vnote.ini

@@ -47,6 +47,9 @@ image_folder=_v_images
 ; Image folder name for the external files
 external_image_folder=_v_images
 
+; Attachment folder name for the notes
+attachment_folder=_v_attachments
+
 ; Enable trailing space highlight
 enable_trailing_space_highlight=true
 

+ 6 - 2
src/src.pro

@@ -75,7 +75,9 @@ SOURCES += main.cpp\
     vtextblockdata.cpp \
     utils/vpreviewutils.cpp \
     dialog/vconfirmdeletiondialog.cpp \
-    vnotefile.cpp
+    vnotefile.cpp \
+    vattachmentlist.cpp \
+    dialog/vsortdialog.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -138,7 +140,9 @@ HEADERS  += vmainwindow.h \
     vtextblockdata.h \
     utils/vpreviewutils.h \
     dialog/vconfirmdeletiondialog.h \
-    vnotefile.h
+    vnotefile.h \
+    vattachmentlist.h \
+    dialog/vsortdialog.h
 
 RESOURCES += \
     vnote.qrc \

+ 43 - 4
src/utils/vutils.cpp

@@ -123,14 +123,13 @@ QString VUtils::generateImageFileName(const QString &path, const QString &title,
     baseName = baseName + '_' + QString::number(QDateTime::currentDateTime().toTime_t());
     baseName = baseName + '_' + QString::number(qrand());
 
+    QDir dir(path);
     QString imageName = baseName + "." + format.toLower();
-    QString filePath = QDir(path).filePath(imageName);
     int index = 1;
 
-    while (QFileInfo::exists(filePath)) {
+    while (fileExists(dir, imageName, true)) {
         imageName = QString("%1_%2.%3").arg(baseName).arg(index++)
                                        .arg(format.toLower());
-        filePath = QDir(path).filePath(imageName);
     }
 
     return imageName;
@@ -191,6 +190,7 @@ QVector<ImageLink> VUtils::fetchImagesFromMarkdownFile(VFile *p_file,
         QString linkText = text.mid(reg.m_startPos, reg.m_endPos - reg.m_startPos);
         bool matched = regExp.exactMatch(linkText);
         Q_ASSERT(matched);
+        Q_UNUSED(matched);
         QString imageUrl = regExp.capturedTexts()[2].trimmed();
 
         ImageLink link;
@@ -605,11 +605,25 @@ QString VUtils::getFileNameWithSequence(const QString &p_directory,
         if (!suffix.isEmpty()) {
             fileName = fileName + "." + suffix;
         }
-    } while (dir.exists(fileName));
+    } while (fileExists(dir, fileName, true));
 
     return fileName;
 }
 
+QString VUtils::getRandomFileName(const QString &p_directory)
+{
+    Q_ASSERT(!p_directory.isEmpty());
+
+    QString name;
+    QDir dir(p_directory);
+    do {
+        name = QString::number(QDateTime::currentDateTimeUtc().toTime_t());
+        name = name + '_' + QString::number(qrand());
+    } while (fileExists(dir, name, true));
+
+    return name;
+}
+
 bool VUtils::checkPathLegal(const QString &p_path)
 {
     // Ensure every part of the p_path is a valid file name until we come to
@@ -869,3 +883,28 @@ QVector<VElementRegion> VUtils::fetchImageRegionsUsingParser(const QString &p_co
 
     return regs;
 }
+
+QString VUtils::displayDateTime(const QDateTime &p_dateTime)
+{
+    QString res = p_dateTime.date().toString(Qt::DefaultLocaleLongDate);
+    res += " " + p_dateTime.time().toString();
+    return res;
+}
+
+bool VUtils::fileExists(const QDir &p_dir, const QString &p_name, bool p_forceCaseInsensitive)
+{
+    if (!p_forceCaseInsensitive) {
+        return p_dir.exists(p_name);
+    }
+
+    QString name = p_name.toLower();
+    QStringList names = p_dir.entryList(QDir::Dirs | QDir::Files | QDir::Hidden
+                                        | QDir::NoSymLinks | QDir::NoDotAndDotDot);
+    foreach (const QString &str, names) {
+        if (str.toLower() == name) {
+            return true;
+        }
+    }
+
+    return false;
+}

+ 10 - 0
src/utils/vutils.h

@@ -7,6 +7,7 @@
 #include <QPair>
 #include <QMessageBox>
 #include <QUrl>
+#include <QDir>
 #include "vconfigmanager.h"
 #include "vconstants.h"
 
@@ -104,6 +105,9 @@ public:
     static QString getFileNameWithSequence(const QString &p_directory,
                                            const QString &p_baseFileName);
 
+    // Get an available random file name in @p_directory.
+    static QString getRandomFileName(const QString &p_directory);
+
     // Try to check if @p_path is legal.
     static bool checkPathLegal(const QString &p_path);
 
@@ -152,6 +156,12 @@ public:
     static bool deleteFile(const QString &p_path,
                            bool p_skipRecycleBin = false);
 
+    static QString displayDateTime(const QDateTime &p_dateTime);
+
+    // Check if file @p_name exists in @p_dir.
+    // @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);
+
     // Regular expression for image link.
     // ![image title]( http://github.com/tamlok/vnote.jpg "alt \" text" )
     // Captured texts (need to be trimmed):

+ 404 - 0
src/vattachmentlist.cpp

@@ -0,0 +1,404 @@
+#include "vattachmentlist.h"
+
+#include <QtWidgets>
+
+#include "vconfigmanager.h"
+#include "utils/vutils.h"
+#include "vbuttonwithwidget.h"
+#include "vnote.h"
+#include "vmainwindow.h"
+#include "dialog/vconfirmdeletiondialog.h"
+#include "dialog/vsortdialog.h"
+
+extern VConfigManager *g_config;
+extern VNote *g_vnote;
+
+VAttachmentList::VAttachmentList(QWidget *p_parent)
+    : QWidget(p_parent), m_file(NULL)
+{
+    setupUI();
+
+    initActions();
+
+    updateContent();
+}
+
+void VAttachmentList::setupUI()
+{
+    m_addBtn = new QPushButton(QIcon(":/resources/icons/add_attachment.svg"), "");
+    m_addBtn->setToolTip(tr("Add"));
+    m_addBtn->setProperty("FlatBtn", true);
+    connect(m_addBtn, &QPushButton::clicked,
+            this, &VAttachmentList::addAttachment);
+
+    m_clearBtn = new QPushButton(QIcon(":/resources/icons/clear_attachment.svg"), "");
+    m_clearBtn->setToolTip(tr("Clear"));
+    m_clearBtn->setProperty("FlatBtn", true);
+    connect(m_clearBtn, &QPushButton::clicked,
+            this, [this]() {
+                if (m_file && m_attachmentList->count() > 0) {
+                    int ret = VUtils::showMessage(QMessageBox::Warning, tr("Warning"),
+                                                  tr("Are you sure to clear attachments of note "
+                                                     "<span style=\"%1\">%2</span>?")
+                                                    .arg(g_config->c_dataTextStyle)
+                                                    .arg(m_file->getName()),
+                                                  tr("<span style=\"%1\">WARNING</span>: "
+                                                     "VNote will delete all the files in directory "
+                                                     "<span style=\"%2\">%3</span>."
+                                                     "You could find deleted files in the recycle bin "
+                                                     "of this notebook.<br>The operation is IRREVERSIBLE!")
+                                                    .arg(g_config->c_warningTextStyle)
+                                                    .arg(g_config->c_dataTextStyle)
+                                                    .arg(m_file->fetchAttachmentFolderPath()),
+                                                  QMessageBox::Ok | QMessageBox::Cancel,
+                                                  QMessageBox::Ok,
+                                                  g_vnote->getMainWindow(),
+                                                  MessageBoxType::Danger);
+                    if (ret == QMessageBox::Ok) {
+                        if (!m_file->deleteAttachments()) {
+                            VUtils::showMessage(QMessageBox::Warning,
+                                                tr("Warning"),
+                                                tr("Fail to clear attachments of note <span style=\"%1\">%2</span>.")
+                                                  .arg(g_config->c_dataTextStyle)
+                                                  .arg(m_file->getName()),
+                                                tr("Please maintain the configureation file manually."),
+                                                QMessageBox::Ok,
+                                                QMessageBox::Ok,
+                                                g_vnote->getMainWindow());
+                        }
+
+                        m_attachmentList->clear();
+                    }
+                }
+            });
+
+    m_locateBtn = new QPushButton(QIcon(":/resources/icons/locate_attachment.svg"), "");
+    m_locateBtn->setToolTip(tr("Open Folder"));
+    m_locateBtn->setProperty("FlatBtn", true);
+    connect(m_locateBtn, &QPushButton::clicked,
+            this, [this]() {
+                if (m_file && !m_file->getAttachmentFolder().isEmpty()) {
+                    QUrl url = QUrl::fromLocalFile(m_file->fetchAttachmentFolderPath());
+                    QDesktopServices::openUrl(url);
+                }
+            });
+
+    m_numLabel = new QLabel();
+
+    QHBoxLayout *btnLayout = new QHBoxLayout;
+    btnLayout->addWidget(m_addBtn);
+    btnLayout->addWidget(m_clearBtn);
+    btnLayout->addWidget(m_locateBtn);
+    btnLayout->addStretch();
+    btnLayout->addWidget(m_numLabel);
+
+    m_attachmentList = new QListWidget;
+    m_attachmentList->setContextMenuPolicy(Qt::CustomContextMenu);
+    m_attachmentList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+    m_attachmentList->setEditTriggers(QAbstractItemView::SelectedClicked);
+    connect(m_attachmentList, &QListWidget::customContextMenuRequested,
+            this, &VAttachmentList::handleContextMenuRequested);
+    connect(m_attachmentList, &QListWidget::itemActivated,
+            this, &VAttachmentList::handleItemActivated);
+    connect(m_attachmentList->itemDelegate(), &QAbstractItemDelegate::commitData,
+            this, &VAttachmentList::handleListItemCommitData);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addLayout(btnLayout);
+    mainLayout->addWidget(m_attachmentList);
+
+    setLayout(mainLayout);
+}
+
+void VAttachmentList::initActions()
+{
+    m_openAct = new QAction(tr("&Open"), this);
+    m_openAct->setToolTip(tr("Open current attachment file"));
+    connect(m_openAct, &QAction::triggered,
+            this, [this]() {
+                QListWidgetItem *item = m_attachmentList->currentItem();
+                handleItemActivated(item);
+            });
+
+    m_deleteAct = new QAction(QIcon(":/resources/icons/delete_attachment.svg"),
+                              tr("&Delete"),
+                              this);
+    m_deleteAct->setToolTip(tr("Delete selected attachments"));
+    connect(m_deleteAct, &QAction::triggered,
+            this, &VAttachmentList::deleteSelectedItems);
+
+    m_sortAct = new QAction(QIcon(":/resources/icons/sort.svg"),
+                            tr("&Sort"),
+                            this);
+    m_sortAct->setToolTip(tr("Sort attachments manually"));
+    connect(m_sortAct, &QAction::triggered,
+            this, &VAttachmentList::sortItems);
+}
+
+void VAttachmentList::setFile(VNoteFile *p_file)
+{
+    m_file = p_file;
+    updateContent();
+}
+
+void VAttachmentList::updateContent()
+{
+    bool enableAdd = true, enableDelete = true, enableClear = true, enableLocate = true;
+    m_attachmentList->clear();
+
+    if (!m_file) {
+        enableAdd = enableDelete = enableClear = enableLocate = false;
+    } else {
+        QString folder = m_file->getAttachmentFolder();
+        const QVector<VAttachment> &attas = m_file->getAttachments();
+
+        if (folder.isEmpty()) {
+            Q_ASSERT(attas.isEmpty());
+            enableDelete = enableClear = enableLocate = false;
+        } else if (attas.isEmpty()) {
+            enableDelete = enableClear = false;
+        } else {
+            fillAttachmentList(attas);
+        }
+    }
+
+    m_addBtn->setEnabled(enableAdd);
+    m_clearBtn->setEnabled(enableClear);
+    m_locateBtn->setEnabled(enableLocate);
+
+    int cnt = m_attachmentList->count();
+    if (cnt > 0) {
+        m_numLabel->setText(tr("%1 %2").arg(cnt).arg(cnt > 1 ? tr("Files") : tr("File")));
+    } else {
+        m_numLabel->setText("");
+    }
+}
+
+void VAttachmentList::fillAttachmentList(const QVector<VAttachment> &p_attachments)
+{
+    Q_ASSERT(m_attachmentList->count() == 0);
+    for (int i = 0; i < p_attachments.size(); ++i) {
+        const VAttachment &atta = p_attachments[i];
+        QListWidgetItem *item = new QListWidgetItem(atta.m_name);
+        item->setFlags(item->flags() | Qt::ItemIsEditable);
+        item->setData(Qt::UserRole, atta.m_name);
+
+        m_attachmentList->addItem(item);
+    }
+}
+
+void VAttachmentList::addAttachment()
+{
+    if (!m_file) {
+        return;
+    }
+
+    static QString lastPath = QDir::homePath();
+    QStringList files = QFileDialog::getOpenFileNames(g_vnote->getMainWindow(),
+                                                      tr("Select Files As Attachments"),
+                                                      lastPath);
+    if (files.isEmpty()) {
+        return;
+    }
+
+    // Update lastPath
+    lastPath = QFileInfo(files[0]).path();
+
+    int addedFiles = 0;
+    for (int i = 0; i < files.size(); ++i) {
+        if (!m_file->addAttachment(files[i])) {
+            VUtils::showMessage(QMessageBox::Warning,
+                                tr("Warning"),
+                                tr("Fail to add attachment %1 for note <span style=\"%2\">%3</span>.")
+                                  .arg(files[i])
+                                  .arg(g_config->c_dataTextStyle)
+                                  .arg(m_file->getName()),
+                                "",
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                g_vnote->getMainWindow());
+        } else {
+            ++addedFiles;
+        }
+    }
+
+    updateContent();
+
+    if (addedFiles > 0) {
+        g_vnote->getMainWindow()->showStatusMessage(tr("Added %1 %2 as attachments")
+                                                      .arg(addedFiles)
+                                                      .arg(addedFiles > 1 ? tr("files") : tr("file")));
+    }
+}
+
+void VAttachmentList::handleContextMenuRequested(QPoint p_pos)
+{
+    // @p_pos is the position in the coordinate of VAttachmentList, no m_attachmentList.
+    QListWidgetItem *item = m_attachmentList->itemAt(m_attachmentList->mapFromParent(p_pos));
+    QMenu menu(this);
+    menu.setToolTipsVisible(true);
+
+    if (!m_file) {
+        return;
+    }
+
+    if (item) {
+        if (!item->isSelected()) {
+            m_attachmentList->setCurrentItem(item, QItemSelectionModel::ClearAndSelect);
+        }
+
+        if (m_attachmentList->selectedItems().size() == 1) {
+            menu.addAction(m_openAct);
+        }
+
+        menu.addAction(m_deleteAct);
+    }
+
+    m_attachmentList->update();
+
+    if (m_file->getAttachments().size() > 1) {
+        if (!menu.actions().isEmpty()) {
+            menu.addSeparator();
+        }
+
+        menu.addAction(m_sortAct);
+    }
+
+    if (!menu.actions().isEmpty()) {
+        menu.exec(mapToGlobal(p_pos));
+    }
+}
+
+void VAttachmentList::handleItemActivated(QListWidgetItem *p_item)
+{
+    if (p_item) {
+        Q_ASSERT(m_file);
+
+        QString name = p_item->text();
+        QString folderPath = m_file->fetchAttachmentFolderPath();
+        QUrl url = QUrl::fromLocalFile(QDir(folderPath).filePath(name));
+        QDesktopServices::openUrl(url);
+    }
+}
+
+void VAttachmentList::deleteSelectedItems()
+{
+    QVector<QString> names;
+    const QList<QListWidgetItem *> selectedItems = m_attachmentList->selectedItems();
+
+    if (selectedItems.isEmpty()) {
+        return;
+    }
+
+    for (auto const & item : selectedItems) {
+        names.push_back(item->text());
+    }
+
+    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.")
+                     .arg(g_config->c_dataTextStyle).arg(m_file->getName());
+    VConfirmDeletionDialog dialog(tr("Confirm Deleting Attachments"),
+                                  info,
+                                  names,
+                                  false,
+                                  false,
+                                  false,
+                                  g_vnote->getMainWindow());
+    if (dialog.exec()) {
+        names = dialog.getConfirmedFiles();
+
+        if (!m_file->deleteAttachments(names)) {
+            VUtils::showMessage(QMessageBox::Warning,
+                                tr("Warning"),
+                                tr("Fail to delete attachments of note <span style=\"%1\">%2</span>.")
+                                  .arg(g_config->c_dataTextStyle)
+                                  .arg(m_file->getName()),
+                                tr("Please maintain the configureation file manually."),
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                g_vnote->getMainWindow());
+        }
+
+        updateContent();
+    }
+}
+
+void VAttachmentList::sortItems()
+{
+    const QVector<VAttachment> &attas = m_file->getAttachments();
+    if (attas.size() < 2) {
+        return;
+    }
+
+    VSortDialog dialog(tr("Sort Attachments"),
+                       tr("Sort attachments in the configuration file."),
+                       g_vnote->getMainWindow());
+    QTreeWidget *tree = dialog.getTreeWidget();
+    tree->clear();
+    tree->setColumnCount(1);
+    tree->header()->setStretchLastSection(true);
+    QStringList headers;
+    headers << tr("Name");
+    tree->setHeaderLabels(headers);
+
+    for (int i = 0; i < attas.size(); ++i) {
+        QTreeWidgetItem *item = new QTreeWidgetItem(tree, QStringList(attas[i].m_name));
+
+        item->setData(0, Qt::UserRole, i);
+    }
+
+    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();
+        }
+
+        m_file->sortAttachments(sortedIdx);
+    }
+}
+
+void VAttachmentList::handleListItemCommitData(QWidget *p_itemEdit)
+{
+    QString text = reinterpret_cast<QLineEdit *>(p_itemEdit)->text();
+    QListWidgetItem *item = m_attachmentList->currentItem();
+    Q_ASSERT(item && item->text() == text);
+
+    QString oldText = item->data(Qt::UserRole).toString();
+
+    if (oldText == text) {
+        return;
+    }
+
+    if (!(oldText.toLower() == text.toLower())
+        && m_file->findAttachment(text, false) > -1) {
+        // Name conflict.
+        // Recover to old name.
+        item->setText(oldText);
+    } else {
+        if (!m_file->renameAttachment(oldText, text)) {
+            VUtils::showMessage(QMessageBox::Information,
+                                tr("Rename Attachment"),
+                                tr("Fail to rename attachment <span style=\"%1\">%2</span>.")
+                                  .arg(g_config->c_dataTextStyle)
+                                  .arg(oldText),
+                                "",
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                this);
+            // Recover to old name.
+            item->setText(oldText);
+        } else {
+            // Change the data.
+            item->setData(Qt::UserRole, text);
+        }
+    }
+}

+ 60 - 0
src/vattachmentlist.h

@@ -0,0 +1,60 @@
+#ifndef VATTACHMENTLIST_H
+#define VATTACHMENTLIST_H
+
+#include <QWidget>
+#include <QVector>
+#include "vnotefile.h"
+
+class QPushButton;
+class QListWidget;
+class QListWidgetItem;
+class QLabel;
+class VNoteFile;
+class QAction;
+
+class VAttachmentList : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit VAttachmentList(QWidget *p_parent = 0);
+
+    void setFile(VNoteFile *p_file);
+
+private slots:
+    void addAttachment();
+
+    void handleContextMenuRequested(QPoint p_pos);
+
+    void handleItemActivated(QListWidgetItem *p_item);
+
+    void deleteSelectedItems();
+
+    void sortItems();
+
+    void handleListItemCommitData(QWidget *p_itemEdit);
+
+private:
+    void setupUI();
+
+    void initActions();
+
+    // Update attachment info of m_file.
+    void updateContent();
+
+    void fillAttachmentList(const QVector<VAttachment> &p_attachments);
+
+    QPushButton *m_addBtn;
+    QPushButton *m_clearBtn;
+    QPushButton *m_locateBtn;
+    QLabel *m_numLabel;
+
+    QListWidget *m_attachmentList;
+
+    QAction *m_openAct;
+    QAction *m_deleteAct;
+    QAction *m_sortAct;
+
+    VNoteFile *m_file;
+};
+
+#endif // VATTACHMENTLIST_H

+ 20 - 66
src/vbuttonwithwidget.cpp

@@ -1,60 +1,44 @@
 #include "vbuttonwithwidget.h"
 
-#include <QEvent>
-#include <QMouseEvent>
-#include <QRect>
+#include <QMenu>
 
-VButtonWithWidget::VButtonWithWidget(QWidget *p_parent)
-    : QPushButton(p_parent), m_popupWidget(NULL)
+VButtonWithWidget::VButtonWithWidget(QWidget *p_widget,
+                                     QWidget *p_parent)
+    : QPushButton(p_parent), m_popupWidget(p_widget)
 {
     init();
 }
 
-VButtonWithWidget::VButtonWithWidget(const QString &p_text, QWidget *p_parent)
-    : QPushButton(p_text, p_parent), m_popupWidget(NULL)
+VButtonWithWidget::VButtonWithWidget(const QString &p_text,
+                                     QWidget *p_widget,
+                                     QWidget *p_parent)
+    : QPushButton(p_text, p_parent), m_popupWidget(p_widget)
 {
     init();
 }
 
 VButtonWithWidget::VButtonWithWidget(const QIcon &p_icon,
                                      const QString &p_text,
+                                     QWidget *p_widget,
                                      QWidget *p_parent)
-    : QPushButton(p_icon, p_text, p_parent), m_popupWidget(NULL)
+    : QPushButton(p_icon, p_text, p_parent), m_popupWidget(p_widget)
 {
     init();
 }
 
-VButtonWithWidget::~VButtonWithWidget()
-{
-    if (m_popupWidget) {
-        delete m_popupWidget;
-    }
-}
-
 void VButtonWithWidget::init()
 {
-    connect(this, &QPushButton::clicked,
-            this, &VButtonWithWidget::showPopupWidget);
-}
-
-void VButtonWithWidget::setPopupWidget(QWidget *p_widget)
-{
-    if (m_popupWidget) {
-        delete m_popupWidget;
-    }
+    m_popupWidget->setParent(this);
 
-    m_popupWidget = p_widget;
-    if (m_popupWidget) {
-        m_popupWidget->hide();
-        m_popupWidget->setParent(NULL);
+    QMenu *menu = new QMenu(this);
+    VButtonWidgetAction *act = new VButtonWidgetAction(m_popupWidget, menu);
+    menu->addAction(act);
+    connect(menu, &QMenu::aboutToShow,
+            this, [this]() {
+                emit popupWidgetAboutToShow(m_popupWidget);
+            });
 
-        Qt::WindowFlags flags = Qt::Popup;
-        m_popupWidget->setWindowFlags(flags);
-        m_popupWidget->setWindowModality(Qt::NonModal);
-
-        // Let popup widget to hide itself if focus lost.
-        m_popupWidget->installEventFilter(this);
-    }
+    setMenu(menu);
 }
 
 QWidget *VButtonWithWidget::getPopupWidget() const
@@ -64,35 +48,5 @@ QWidget *VButtonWithWidget::getPopupWidget() const
 
 void VButtonWithWidget::showPopupWidget()
 {
-    if (m_popupWidget->isVisible()) {
-        m_popupWidget->hide();
-    } else {
-        emit popupWidgetAboutToShow(m_popupWidget);
-
-        // Calculate the position of the popup widget.
-        QPoint btnPos = mapToGlobal(QPoint(0, 0));
-        int btnWidth = width();
-
-        int popupWidth = btnWidth * 10;
-        int popupHeight = height() * 10;
-        int popupX = btnPos.x() + btnWidth - popupWidth;
-        int popupY = btnPos.y() - popupHeight - 10;
-
-        m_popupWidget->setGeometry(popupX, popupY, popupWidth, popupHeight);
-        m_popupWidget->show();
-    }
-}
-
-bool VButtonWithWidget::eventFilter(QObject *p_obj, QEvent *p_event)
-{
-    if (p_event->type() == QEvent::MouseButtonRelease) {
-        QMouseEvent *eve = dynamic_cast<QMouseEvent *>(p_event);
-        QPoint clickPos = eve->pos();
-        const QRect &rect = m_popupWidget->rect();
-        if (!rect.contains(clickPos)) {
-            m_popupWidget->hide();
-        }
-    }
-
-    return QPushButton::eventFilter(p_obj, p_event);
+    showMenu();
 }

+ 31 - 13
src/vbuttonwithwidget.h

@@ -4,35 +4,53 @@
 #include <QPushButton>
 #include <QString>
 #include <QIcon>
+#include <QWidgetAction>
+
+class VButtonWidgetAction : public QWidgetAction
+{
+    Q_OBJECT
+public:
+    VButtonWidgetAction(QWidget *p_widget, QWidget *p_parent)
+        : QWidgetAction(p_parent), m_widget(p_widget)
+    {
+    }
+
+    QWidget *createWidget(QWidget *p_parent)
+    {
+        m_widget->setParent(p_parent);
+        return m_widget;
+    }
+
+private:
+    QWidget *m_widget;
+};
 
 // A QPushButton with popup widget.
 class VButtonWithWidget : public QPushButton
 {
     Q_OBJECT
 public:
-    VButtonWithWidget(QWidget *p_parent = Q_NULLPTR);
-    VButtonWithWidget(const QString &p_text, QWidget *p_parent = Q_NULLPTR);
+    VButtonWithWidget(QWidget *p_widget,
+                      QWidget *p_parent = Q_NULLPTR);
+
+    VButtonWithWidget(const QString &p_text,
+                      QWidget *p_widget,
+                      QWidget *p_parent = Q_NULLPTR);
+
     VButtonWithWidget(const QIcon &p_icon,
                       const QString &p_text,
+                      QWidget *p_widget,
                       QWidget *p_parent = Q_NULLPTR);
-    ~VButtonWithWidget();
-
-    // Set the widget which will transfer the ownership to VButtonWithWidget.
-    void setPopupWidget(QWidget *p_widget);
 
     QWidget *getPopupWidget() const;
 
+    // Show the popup widget.
+    void showPopupWidget();
+
 signals:
     // Emit when popup widget is about to show.
     void popupWidgetAboutToShow(QWidget *p_widget);
 
-protected:
-    bool eventFilter(QObject *p_obj, QEvent *p_event) Q_DECL_OVERRIDE;
-
-private slots:
-    // Show the popup widget.
-    void showPopupWidget();
-
 private:
     void init();
 

+ 8 - 1
src/vconfigmanager.cpp

@@ -136,6 +136,13 @@ void VConfigManager::initialize()
     m_imageFolderExt = getConfigFromSettings("global",
                                              "external_image_folder").toString();
 
+    m_attachmentFolder = getConfigFromSettings("global",
+                                               "attachment_folder").toString();
+    if (m_attachmentFolder.isEmpty()) {
+        // Reset the default folder.
+        m_attachmentFolder = resetDefaultConfig("global", "attachment_folder").toString();
+    }
+
     m_enableTrailingSpaceHighlight = getConfigFromSettings("global",
                                                            "enable_trailing_space_highlight").toBool();
 
@@ -230,7 +237,7 @@ void VConfigManager::readNotebookFromSettings(QVector<VNotebook *> &p_notebooks,
         QString name = userSettings->value("name").toString();
         QString path = userSettings->value("path").toString();
         VNotebook *notebook = new VNotebook(name, path, parent);
-        notebook->readConfig();
+        notebook->readConfigNotebook();
         p_notebooks.append(notebook);
     }
     userSettings->endArray();

+ 35 - 0
src/vconfigmanager.h

@@ -213,6 +213,11 @@ public:
     void setImageFolderExt(const QString &p_folder);
     bool isCustomImageFolderExt() const;
 
+    const QString &getAttachmentFolder() const;
+    // Empty string to reset the default folder.
+    void setAttachmentFolder(const QString &p_folder);
+    bool isCustomAttachmentFolder() const;
+
     bool getEnableTrailingSpaceHighlight() const;
     void setEnableTrailingSapceHighlight(bool p_enabled);
 
@@ -462,6 +467,10 @@ private:
     // Each file can specify its custom folder.
     QString m_imageFolderExt;
 
+    // Global default folder name to store attachments of all the notes.
+    // Each notebook can specify its custom folder.
+    QString m_attachmentFolder;
+
     // Enable trailing-space highlight.
     bool m_enableTrailingSpaceHighlight;
 
@@ -1140,6 +1149,32 @@ inline bool VConfigManager::isCustomImageFolderExt() const
     return m_imageFolderExt != getDefaultConfig("global", "external_image_folder").toString();
 }
 
+inline const QString &VConfigManager::getAttachmentFolder() const
+{
+    return m_attachmentFolder;
+}
+
+inline void VConfigManager::setAttachmentFolder(const QString &p_folder)
+{
+    if (p_folder.isEmpty()) {
+        // Reset the default folder.
+        m_attachmentFolder = resetDefaultConfig("global", "attachment_folder").toString();
+        return;
+    }
+
+    if (m_attachmentFolder == p_folder) {
+        return;
+    }
+
+    m_attachmentFolder = p_folder;
+    setConfigToSettings("global", "attachment_folder", m_attachmentFolder);
+}
+
+inline bool VConfigManager::isCustomAttachmentFolder() const
+{
+    return m_attachmentFolder != getDefaultConfig("global", "attachment_folder").toString();
+}
+
 inline bool VConfigManager::getEnableTrailingSpaceHighlight() const
 {
     return m_enableTrailingSpaceHighlight;

+ 2 - 0
src/vconstants.h

@@ -29,7 +29,9 @@ namespace DirConfig
     static const QString c_version = "version";
     static const QString c_subDirectories = "sub_directories";
     static const QString c_files = "files";
+    static const QString c_attachments = "attachments";
     static const QString c_imageFolder = "image_folder";
+    static const QString c_attachmentFolder = "attachment_folder";
     static const QString c_recycleBinFolder = "recycle_bin_folder";
     static const QString c_name = "name";
     static const QString c_createdTime = "created_time";

+ 7 - 0
src/vdirectory.cpp

@@ -163,6 +163,13 @@ bool VDirectory::writeToConfig() const
     return writeToConfig(json);
 }
 
+bool VDirectory::updateFileConfig(const VNoteFile *p_file)
+{
+    Q_ASSERT(m_opened);
+    Q_UNUSED(p_file);
+    return writeToConfig();
+}
+
 bool VDirectory::writeToConfig(const QJsonObject &p_json) const
 {
     return VConfigManager::writeDirectoryConfig(fetchPath(), p_json);

+ 6 - 0
src/vdirectory.h

@@ -82,6 +82,9 @@ public:
     QString getNotebookName() const;
     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.
@@ -97,6 +100,9 @@ public:
     // notebook.
     bool writeToConfig() const;
 
+    // Write the config of @p_file to config file.
+    bool updateFileConfig(const VNoteFile *p_file);
+
     // Try to load file given relative path @p_filePath.
     VNoteFile *tryLoadFile(QStringList &p_filePath);
 

+ 49 - 1
src/vmainwindow.cpp

@@ -27,6 +27,8 @@
 #include "dialog/vorphanfileinfodialog.h"
 #include "vsingleinstanceguard.h"
 #include "vnotefile.h"
+#include "vbuttonwithwidget.h"
+#include "vattachmentlist.h"
 
 extern VConfigManager *g_config;
 
@@ -124,7 +126,12 @@ void VMainWindow::setupUI()
     connect(notebookSelector, &VNotebookSelector::notebookUpdated,
             editArea, &VEditArea::handleNotebookUpdated);
     connect(notebookSelector, &VNotebookSelector::notebookCreated,
-            directoryTree, &VDirectoryTree::newRootDirectory);
+            directoryTree, [this](const QString &p_name, bool p_import) {
+                Q_UNUSED(p_name);
+                if (!p_import) {
+                    directoryTree->newRootDirectory();
+                }
+            });
 
     connect(fileList, &VFileList::fileClicked,
             editArea, &VEditArea::openFile);
@@ -203,6 +210,7 @@ void VMainWindow::initToolBar()
     initFileToolBar(iconSize);
     initViewToolBar(iconSize);
     initEditToolBar(iconSize);
+    initNoteToolBar(iconSize);
 }
 
 void VMainWindow::initViewToolBar(QSize p_iconSize)
@@ -319,6 +327,38 @@ void VMainWindow::initEditToolBar(QSize p_iconSize)
     setActionsEnabled(m_editToolBar, false);
 }
 
+void VMainWindow::initNoteToolBar(QSize p_iconSize)
+{
+    QToolBar *noteToolBar = addToolBar(tr("Note Toolbar"));
+    noteToolBar->setObjectName("NoteToolBar");
+    noteToolBar->setMovable(false);
+    if (p_iconSize.isValid()) {
+        noteToolBar->setIconSize(p_iconSize);
+    }
+
+    noteToolBar->addSeparator();
+
+    // Attachment.
+    m_attachmentList = new VAttachmentList(this);
+    m_attachmentBtn = new VButtonWithWidget(QIcon(":/resources/icons/attachment.svg"),
+                                            "",
+                                            m_attachmentList,
+                                            this);
+    m_attachmentBtn->setToolTip(tr("Attachments"));
+    m_attachmentBtn->setStatusTip(tr("Manage current note's attachments"));
+    m_attachmentBtn->setProperty("CornerBtn", true);
+    m_attachmentBtn->setFocusPolicy(Qt::NoFocus);
+
+    connect(m_attachmentBtn, &VButtonWithWidget::popupWidgetAboutToShow,
+            this, [this]() {
+                m_attachmentList->setFile(dynamic_cast<VNoteFile *>(m_curFile.data()));
+            });
+
+    m_attachmentBtn->setEnabled(false);
+
+    noteToolBar->addWidget(m_attachmentBtn);
+}
+
 void VMainWindow::initFileToolBar(QSize p_iconSize)
 {
     QToolBar *fileToolBar = addToolBar(tr("Note"));
@@ -1511,6 +1551,14 @@ void VMainWindow::updateActionStateFromTabStatusChange(const VFile *p_file,
     deleteNoteAct->setEnabled(p_file && p_file->getType() == FileType::Note);
     noteInfoAct->setEnabled(p_file && !systemFile);
 
+    m_attachmentBtn->setEnabled(p_file && p_file->getType() == FileType::Note);
+    if (m_attachmentBtn->isEnabled()
+        && !dynamic_cast<const VNoteFile *>(p_file)->getAttachments().isEmpty()) {
+        m_attachmentBtn->setIcon(QIcon(":/resources/icons/attachment_full.svg"));
+    } else {
+        m_attachmentBtn->setIcon(QIcon(":/resources/icons/attachment.svg"));
+    }
+
     m_insertImageAct->setEnabled(p_file && p_editMode);
 
     setActionsEnabled(m_editToolBar, p_file && p_editMode);

+ 13 - 3
src/vmainwindow.h

@@ -35,6 +35,8 @@ class VSingleInstanceGuard;
 class QTimer;
 class QSystemTrayIcon;
 class QShortcut;
+class VButtonWithWidget;
+class VAttachmentList;
 
 class VMainWindow : public QMainWindow
 {
@@ -65,6 +67,9 @@ public:
     // Try to open @p_filePath as internal note.
     bool tryOpenInternalFile(const QString &p_filePath);
 
+    // Show a temporary message in status bar.
+    void showStatusMessage(const QString &p_msg);
+
 private slots:
     void importNoteFromFile();
     void viewSettings();
@@ -107,9 +112,6 @@ private slots:
     void printNote();
     void exportAsPDF();
 
-    // Show a temporary message in status bar.
-    void showStatusMessage(const QString &p_msg);
-
     // Handle Vim status updated.
     void handleVimStatusUpdated(const VVim *p_vim);
 
@@ -140,6 +142,8 @@ private:
     void initFileToolBar(QSize p_iconSize = QSize());
     void initViewToolBar(QSize p_iconSize = QSize());
 
+    void initNoteToolBar(QSize p_iconSize = QSize());
+
     // Init the Edit toolbar.
     void initEditToolBar(QSize p_iconSize = QSize());
 
@@ -249,6 +253,12 @@ private:
     // Edit Toolbar.
     QToolBar *m_editToolBar;
 
+    // Attachment button.
+    VButtonWithWidget *m_attachmentBtn;
+
+    // Attachment list.
+    VAttachmentList *m_attachmentList;
+
     QVector<QPixmap> predefinedColorPixmaps;
 
     // Single instance guard.

+ 4 - 0
src/vmdedit.cpp

@@ -291,9 +291,13 @@ void VMdEdit::clearUnusedImages()
             VConfirmDeletionDialog dialog(tr("Confirm Cleaning Up Unused Images"),
                                           info,
                                           unusedImages,
+                                          true,
+                                          g_config->getConfirmImagesCleanUp(),
+                                          true,
                                           this);
             if (dialog.exec()) {
                 unusedImages = dialog.getConfirmedFiles();
+                g_config->setConfirmImagesCleanUp(dialog.getAskAgainEnabled());
             } else {
                 unusedImages.clear();
             }

+ 7 - 0
src/vnote.qrc

@@ -123,5 +123,12 @@
         <file>utils/highlightjs/highlightjs-line-numbers.min.js</file>
         <file>resources/icons/recycle_bin.svg</file>
         <file>resources/icons/empty_recycle_bin.svg</file>
+        <file>resources/icons/attachment.svg</file>
+        <file>resources/icons/attachment_full.svg</file>
+        <file>resources/icons/add_attachment.svg</file>
+        <file>resources/icons/clear_attachment.svg</file>
+        <file>resources/icons/locate_attachment.svg</file>
+        <file>resources/icons/delete_attachment.svg</file>
+        <file>resources/icons/sort.svg</file>
     </qresource>
 </RCC>

+ 42 - 4
src/vnotebook.cpp

@@ -24,7 +24,7 @@ VNotebook::~VNotebook()
     delete m_rootDir;
 }
 
-bool VNotebook::readConfig()
+bool VNotebook::readConfigNotebook()
 {
     QJsonObject configJson = VConfigManager::readDirectoryConfig(m_path);
     if (configJson.isEmpty()) {
@@ -44,6 +44,20 @@ bool VNotebook::readConfig()
         m_recycleBinFolder = it.value().toString();
     }
 
+    // [attachment_folder] section.
+    // SHOULD be processed at last.
+    it = configJson.find(DirConfig::c_attachmentFolder);
+    if (it != configJson.end()) {
+        m_attachmentFolder = it.value().toString();
+    }
+
+    // We do not allow empty attachment folder.
+    if (m_attachmentFolder.isEmpty()) {
+        m_attachmentFolder = g_config->getAttachmentFolder();
+        Q_ASSERT(!m_attachmentFolder.isEmpty());
+        writeConfigNotebook();
+    }
+
     return true;
 }
 
@@ -54,6 +68,9 @@ QJsonObject VNotebook::toConfigJsonNotebook() const
     // [image_folder] section.
     json[DirConfig::c_imageFolder] = m_imageFolder;
 
+    // [attachment_folder] section.
+    json[DirConfig::c_attachmentFolder] = m_attachmentFolder;
+
     // [recycle_bin_folder] section.
     json[DirConfig::c_recycleBinFolder] = m_recycleBinFolder;
 
@@ -126,8 +143,11 @@ bool VNotebook::open()
     return m_rootDir->open();
 }
 
-VNotebook *VNotebook::createNotebook(const QString &p_name, const QString &p_path,
-                                     bool p_import, const QString &p_imageFolder,
+VNotebook *VNotebook::createNotebook(const QString &p_name,
+                                     const QString &p_path,
+                                     bool p_import,
+                                     const QString &p_imageFolder,
+                                     const QString &p_attachmentFolder,
                                      QObject *p_parent)
 {
     VNotebook *nb = new VNotebook(p_name, p_path, p_parent);
@@ -136,10 +156,18 @@ VNotebook *VNotebook::createNotebook(const QString &p_name, const QString &p_pat
     // its image folder.
     nb->setImageFolder(p_imageFolder);
 
+    // If @p_attachmentFolder is empty, use global configured folder.
+    QString attachmentFolder = p_attachmentFolder;
+    if (attachmentFolder.isEmpty()) {
+        attachmentFolder = g_config->getAttachmentFolder();
+    }
+
+    nb->setAttachmentFolder(attachmentFolder);
+
     // Check if there alread exists a config file.
     if (p_import && VConfigManager::directoryConfigExist(p_path)) {
         qDebug() << "import existing notebook";
-        nb->readConfig();
+        nb->readConfigNotebook();
         return nb;
     }
 
@@ -271,6 +299,16 @@ const QString &VNotebook::getImageFolderConfig() const
     return m_imageFolder;
 }
 
+const QString &VNotebook::getAttachmentFolder() const
+{
+    return m_attachmentFolder;
+}
+
+void VNotebook::setAttachmentFolder(const QString &p_attachmentFolder)
+{
+    m_attachmentFolder = p_attachmentFolder;
+}
+
 bool VNotebook::isOpened() const
 {
     return m_rootDir->isOpened();

+ 18 - 5
src/vnotebook.h

@@ -43,8 +43,11 @@ public:
 
     void rename(const QString &p_name);
 
-    static VNotebook *createNotebook(const QString &p_name, const QString &p_path,
-                                     bool p_import, const QString &p_imageFolder,
+    static VNotebook *createNotebook(const QString &p_name,
+                                     const QString &p_path,
+                                     bool p_import,
+                                     const QString &p_imageFolder,
+                                     const QString &p_attachmentFolder,
                                      QObject *p_parent = 0);
 
     static bool deleteNotebook(VNotebook *p_notebook, bool p_deleteFiles);
@@ -56,6 +59,11 @@ public:
     // Return m_imageFolder.
     const QString &getImageFolderConfig() const;
 
+    // Different from image folder. We could not change the attachment folder
+    // of a notebook once it has been created.
+    // Get the attachment folder for this notebook to use.
+    const QString &getAttachmentFolder() const;
+
     // Return m_recycleBinFolder.
     const QString &getRecycleBinFolder() const;
 
@@ -64,9 +72,10 @@ public:
 
     void setImageFolder(const QString &p_imageFolder);
 
-    // Read configurations (excluding "sub_directories" and "files" section)
-    // from root directory config file.
-    bool readConfig();
+    void setAttachmentFolder(const QString &p_attachmentFolder);
+
+    // Read configurations (only notebook part) directly from root directory config file.
+    bool readConfigNotebook();
 
     // Write configurations only related to notebook to root directory config file.
     bool writeConfigNotebook() const;
@@ -95,6 +104,10 @@ private:
     // Otherwise, VNote will use the global configured folder.
     QString m_imageFolder;
 
+    // Folder name to store attachments.
+    // Should not be empty and changed once a notebook is created.
+    QString m_attachmentFolder;
+
     // Folder name to store deleted files.
     // Could be relative or absolute.
     QString m_recycleBinFolder;

+ 13 - 5
src/vnotebookselector.cpp

@@ -271,9 +271,10 @@ bool VNotebookSelector::newNotebook()
         createNotebook(dialog.getNameInput(),
                        dialog.getPathInput(),
                        dialog.isImportExistingNotebook(),
-                       dialog.getImageFolder());
+                       dialog.getImageFolder(),
+                       dialog.getAttachmentFolder());
 
-        emit notebookCreated();
+        emit notebookCreated(dialog.getNameInput(), dialog.isImportExistingNotebook());
         return true;
     }
 
@@ -283,10 +284,12 @@ bool VNotebookSelector::newNotebook()
 void VNotebookSelector::createNotebook(const QString &p_name,
                                        const QString &p_path,
                                        bool p_import,
-                                       const QString &p_imageFolder)
+                                       const QString &p_imageFolder,
+                                       const QString &p_attachmentFolder)
 {
     VNotebook *nb = VNotebook::createNotebook(p_name, p_path, p_import,
-                                              p_imageFolder, m_vnote);
+                                              p_imageFolder, p_attachmentFolder,
+                                              m_vnote);
     if (!nb) {
         VUtils::showMessage(QMessageBox::Warning, tr("Warning"),
                             tr("Fail to create notebook "
@@ -383,6 +386,7 @@ void VNotebookSelector::editNotebookInfo()
                                m_notebooks, this);
     if (dialog.exec() == QDialog::Accepted) {
         bool updated = false;
+        bool configUpdated = false;
         QString name = dialog.getName();
         if (name != curName) {
             updated = true;
@@ -393,8 +397,12 @@ void VNotebookSelector::editNotebookInfo()
 
         QString imageFolder = dialog.getImageFolder();
         if (imageFolder != notebook->getImageFolderConfig()) {
-            updated = true;
+            configUpdated = true;
             notebook->setImageFolder(imageFolder);
+        }
+
+        if (configUpdated) {
+            updated = true;
             notebook->writeConfigNotebook();
         }
 

+ 4 - 2
src/vnotebookselector.h

@@ -38,7 +38,7 @@ signals:
     void notebookUpdated(const VNotebook *p_notebook);
 
     // Emit after creating a new notebook.
-    void notebookCreated();
+    void notebookCreated(const QString &p_name, bool p_import);
 
 public slots:
     bool newNotebook();
@@ -62,8 +62,10 @@ private:
 
     // If @p_import is true, we will use the existing config file.
     // If @p_imageFolder is empty, we will use the global one.
+    // If @p_attachmentFolder is empty, we will use the global one.
     void createNotebook(const QString &p_name, const QString &p_path,
-                        bool p_import, const QString &p_imageFolder);
+                        bool p_import, const QString &p_imageFolder,
+                        const QString &p_attachmentFolder);
 
     void deleteNotebook(VNotebook *p_notebook, bool p_deleteFiles);
     void addNotebookItem(const QString &p_name);

+ 178 - 3
src/vnotefile.cpp

@@ -4,6 +4,8 @@
 #include <QDebug>
 #include <QTextEdit>
 #include <QFileInfo>
+#include <QJsonObject>
+#include <QJsonArray>
 #include <QDebug>
 
 #include "utils/vutils.h"
@@ -14,8 +16,12 @@ VNoteFile::VNoteFile(VDirectory *p_directory,
                      FileType p_type,
                      bool p_modifiable,
                      QDateTime p_createdTimeUtc,
-                     QDateTime p_modifiedTimeUtc)
-    : VFile(p_directory, p_name, p_type, p_modifiable, p_createdTimeUtc, p_modifiedTimeUtc)
+                     QDateTime p_modifiedTimeUtc,
+                     const QString &p_attachmentFolder,
+                     const QVector<VAttachment> &p_attachments)
+    : VFile(p_directory, p_name, p_type, p_modifiable, p_createdTimeUtc, p_modifiedTimeUtc),
+      m_attachmentFolder(p_attachmentFolder),
+      m_attachments(p_attachments)
 {
 }
 
@@ -124,6 +130,14 @@ VNoteFile *VNoteFile::fromJson(VDirectory *p_directory,
                                FileType p_type,
                                bool p_modifiable)
 {
+    // Attachments.
+    QJsonArray attachmentJson = p_json[DirConfig::c_attachments].toArray();
+    QVector<VAttachment> attachments;
+    for (int i = 0; i < attachmentJson.size(); ++i) {
+        QJsonObject attachmentItem = attachmentJson[i].toObject();
+        attachments.push_back(VAttachment(attachmentItem[DirConfig::c_name].toString()));
+    }
+
     return new VNoteFile(p_directory,
                          p_json[DirConfig::c_name].toString(),
                          p_type,
@@ -131,7 +145,9 @@ VNoteFile *VNoteFile::fromJson(VDirectory *p_directory,
                          QDateTime::fromString(p_json[DirConfig::c_createdTime].toString(),
                                                Qt::ISODate),
                          QDateTime::fromString(p_json[DirConfig::c_modifiedTime].toString(),
-                                               Qt::ISODate));
+                                               Qt::ISODate),
+                         p_json[DirConfig::c_attachmentFolder].toString(),
+                         attachments);
 }
 
 QJsonObject VNoteFile::toConfigJson() const
@@ -140,6 +156,18 @@ QJsonObject VNoteFile::toConfigJson() const
     item[DirConfig::c_name] = m_name;
     item[DirConfig::c_createdTime] = m_createdTimeUtc.toString(Qt::ISODate);
     item[DirConfig::c_modifiedTime] = m_modifiedTimeUtc.toString(Qt::ISODate);
+    item[DirConfig::c_attachmentFolder] = m_attachmentFolder;
+
+    // Attachments.
+    QJsonArray attachmentJson;
+    for (int i = 0; i < m_attachments.size(); ++i) {
+        const VAttachment &item = m_attachments[i];
+        QJsonObject attachmentItem;
+        attachmentItem[DirConfig::c_name] = item.m_name;
+        attachmentJson.append(attachmentItem);
+    }
+
+    item[DirConfig::c_attachments] = attachmentJson;
 
     return item;
 }
@@ -185,3 +213,150 @@ void VNoteFile::deleteInternalImages()
     qDebug() << "delete" << deleted << "images for" << m_name << fetchPath();
 }
 
+bool VNoteFile::addAttachment(const QString &p_file)
+{
+    if (p_file.isEmpty() || !QFileInfo::exists(p_file)) {
+        return false;
+    }
+
+    QString folderPath = fetchAttachmentFolderPath();
+    QString name = VUtils::fileNameFromPath(p_file);
+    Q_ASSERT(!name.isEmpty());
+    name = VUtils::getFileNameWithSequence(folderPath, name);
+    QString destPath = QDir(folderPath).filePath(name);
+    if (!VUtils::copyFile(p_file, destPath, false)) {
+        return false;
+    }
+
+    m_attachments.push_back(VAttachment(name));
+
+    if (!getDirectory()->updateFileConfig(this)) {
+        qWarning() << "fail to update config of file" << m_name
+                   << "in directory" << fetchBasePath();
+        return false;
+    }
+
+    return true;
+}
+
+QString VNoteFile::fetchAttachmentFolderPath()
+{
+    QString folderPath = QDir(fetchBasePath()).filePath(getNotebook()->getAttachmentFolder());
+    if (m_attachmentFolder.isEmpty()) {
+        m_attachmentFolder = VUtils::getRandomFileName(folderPath);
+    }
+
+    folderPath = QDir(folderPath).filePath(m_attachmentFolder);
+    if (!QFileInfo::exists(folderPath)) {
+        QDir dir;
+        if (!dir.mkpath(folderPath)) {
+            qWarning() << "fail to create attachment folder of notebook" << m_name << folderPath;
+        }
+    }
+
+    return folderPath;
+}
+
+bool VNoteFile::deleteAttachments()
+{
+    if (m_attachments.isEmpty()) {
+        return true;
+    }
+
+    QVector<QString> attas;
+    for (int i = 0; i < m_attachments.size(); ++i) {
+        attas.push_back(m_attachments[i].m_name);
+    }
+
+    return deleteAttachments(attas);
+}
+
+bool VNoteFile::deleteAttachments(const QVector<QString> &p_names)
+{
+    if (p_names.isEmpty()) {
+        return true;
+    }
+
+    QDir dir(fetchAttachmentFolderPath());
+    bool ret = true;
+    for (int i = 0; i < p_names.size(); ++i) {
+        int idx = findAttachment(p_names[i]);
+        if (idx == -1) {
+            ret = false;
+            continue;
+        }
+
+        m_attachments.remove(idx);
+        if (!VUtils::deleteFile(getNotebook(), dir.filePath(p_names[i]), false)) {
+            ret = false;
+            qWarning() << "fail to delete attachment" << p_names[i]
+                       << "for note" << m_name;
+        }
+    }
+
+    if (!getDirectory()->updateFileConfig(this)) {
+        qWarning() << "fail to update config of file" << m_name
+                   << "in directory" << fetchBasePath();
+        ret = false;
+    }
+
+    return ret;
+}
+
+int VNoteFile::findAttachment(const QString &p_name, bool p_caseSensitive)
+{
+    const QString name = p_caseSensitive ? p_name : p_name.toLower();
+    for (int i = 0; i < m_attachments.size(); ++i) {
+        QString attaName = p_caseSensitive ? m_attachments[i].m_name
+                                           : m_attachments[i].m_name.toLower();
+        if (name == attaName) {
+            return i;
+        }
+    }
+
+    return -1;
+}
+
+void VNoteFile::sortAttachments(QVector<int> p_sortedIdx)
+{
+    V_ASSERT(m_opened);
+    V_ASSERT(p_sortedIdx.size() == m_attachments.size());
+
+    auto oriFiles = m_attachments;
+
+    for (int i = 0; i < p_sortedIdx.size(); ++i) {
+        m_attachments[i] = oriFiles[p_sortedIdx[i]];
+    }
+
+    if (!getDirectory()->updateFileConfig(this)) {
+        qWarning() << "fail to reorder files in config" << p_sortedIdx;
+        m_attachments = oriFiles;
+    }
+}
+
+bool VNoteFile::renameAttachment(const QString &p_oldName, const QString &p_newName)
+{
+    int idx = findAttachment(p_oldName);
+    if (idx == -1) {
+        return false;
+    }
+
+    QDir dir(fetchAttachmentFolderPath());
+    if (!dir.rename(p_oldName, p_newName)) {
+        qWarning() << "fail to rename attachment file" << p_oldName << p_newName;
+        return false;
+    }
+
+    m_attachments[idx].m_name = p_newName;
+
+    if (!getDirectory()->updateFileConfig(this)) {
+        qWarning() << "fail to rename attachment in config" << p_oldName << p_newName;
+
+        m_attachments[idx].m_name = p_oldName;
+        dir.rename(p_newName, p_oldName);
+
+        return false;
+    }
+
+    return true;
+}

+ 65 - 1
src/vnotefile.h

@@ -1,11 +1,30 @@
 #ifndef VNOTEFILE_H
 #define VNOTEFILE_H
 
+#include <QVector>
+#include <QString>
+
 #include "vfile.h"
 
 class VDirectory;
 class VNotebook;
 
+// Structure for a note attachment.
+struct VAttachment
+{
+    VAttachment()
+    {
+    }
+
+    VAttachment(const QString &p_name)
+        : m_name(p_name)
+    {
+    }
+
+    // File name of the attachment.
+    QString m_name;
+};
+
 class VNoteFile : public VFile
 {
     Q_OBJECT
@@ -15,7 +34,9 @@ public:
               FileType p_type,
               bool p_modifiable,
               QDateTime p_createdTimeUtc,
-              QDateTime p_modifiedTimeUtc);
+              QDateTime p_modifiedTimeUtc,
+              const QString &p_attachmentFolder = "",
+              const QVector<VAttachment> &p_attachments = QVector<VAttachment>());
 
     QString fetchPath() const Q_DECL_OVERRIDE;
 
@@ -58,9 +79,52 @@ public:
     // Delete this file in disk as well as all its images/attachments.
     bool deleteFile();
 
+    const QString &getAttachmentFolder() const;
+
+    const QVector<VAttachment> &getAttachments() const;
+
+    // Add @p_file as an attachment to this note.
+    bool addAttachment(const QString &p_file);
+
+    // Fetch attachment folder path.
+    // Will create it if it does not exist.
+    QString fetchAttachmentFolderPath();
+
+    // Delete all the attachments.
+    bool deleteAttachments();
+
+    // Delete attachments specified by @p_names.
+    bool deleteAttachments(const QVector<QString> &p_names);
+
+    // Reorder attachments in m_attachments by index.
+    void sortAttachments(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);
+
+    bool renameAttachment(const QString &p_oldName, const QString &p_newName);
+
 private:
     // Delete internal images of this file.
     void deleteInternalImages();
+
+    // Folder under the attachment folder of the notebook.
+    // Store all the attachments of current file.
+    QString m_attachmentFolder;
+
+    // Attachments.
+    QVector<VAttachment> m_attachments;
 };
 
+inline const QString &VNoteFile::getAttachmentFolder() const
+{
+    return m_attachmentFolder;
+}
+
+inline const QVector<VAttachment> &VNoteFile::getAttachments() const
+{
+    return m_attachments;
+}
+
 #endif // VNOTEFILE_H

+ 14 - 14
src/vvimindicator.cpp

@@ -100,35 +100,35 @@ void VVimIndicator::setupUI()
 
     m_modeLabel = new QLabel(this);
 
-    m_regBtn = new VButtonWithWidget(QIcon(":/resources/icons/arrow_dropup.svg"),
-                                     "\"",
-                                     this);
-    m_regBtn->setToolTip(tr("Registers"));
-    m_regBtn->setProperty("StatusBtn", true);
-    m_regBtn->setFocusPolicy(Qt::NoFocus);
     QTreeWidget *regTree = new QTreeWidget(this);
     regTree->setColumnCount(2);
     regTree->header()->setStretchLastSection(true);
     QStringList headers;
     headers << tr("Register") << tr("Value");
     regTree->setHeaderLabels(headers);
-    m_regBtn->setPopupWidget(regTree);
+
+    m_regBtn = new VButtonWithWidget("\"",
+                                     regTree,
+                                     this);
+    m_regBtn->setToolTip(tr("Registers"));
+    m_regBtn->setProperty("StatusBtn", true);
+    m_regBtn->setFocusPolicy(Qt::NoFocus);
     connect(m_regBtn, &VButtonWithWidget::popupWidgetAboutToShow,
             this, &VVimIndicator::updateRegistersTree);
 
-    m_markBtn = new VButtonWithWidget(QIcon(":/resources/icons/arrow_dropup.svg"),
-                                      "[]",
-                                      this);
-    m_markBtn->setToolTip(tr("Marks"));
-    m_markBtn->setProperty("StatusBtn", true);
-    m_markBtn->setFocusPolicy(Qt::NoFocus);
     QTreeWidget *markTree = new QTreeWidget(this);
     markTree->setColumnCount(4);
     markTree->header()->setStretchLastSection(true);
     headers.clear();
     headers << tr("Mark") << tr("Line") << tr("Column") << tr("Text");
     markTree->setHeaderLabels(headers);
-    m_markBtn->setPopupWidget(markTree);
+
+    m_markBtn = new VButtonWithWidget("[]",
+                                      markTree,
+                                      this);
+    m_markBtn->setToolTip(tr("Marks"));
+    m_markBtn->setProperty("StatusBtn", true);
+    m_markBtn->setFocusPolicy(Qt::NoFocus);
     connect(m_markBtn, &VButtonWithWidget::popupWidgetAboutToShow,
             this, &VVimIndicator::updateMarksTree);