Browse Source

add recycle bin to each notebook

Le Tan 8 years ago
parent
commit
35fa0a46f6

+ 14 - 9
src/dialog/vdeletenotebookdialog.cpp

@@ -4,11 +4,12 @@
 
 extern VConfigManager *g_config;
 
-VDeleteNotebookDialog::VDeleteNotebookDialog(const QString &p_title, const QString &p_name,
-                                             const QString &p_path, QWidget *p_parent)
-    : QDialog(p_parent), m_path(p_path)
+VDeleteNotebookDialog::VDeleteNotebookDialog(const QString &p_title,
+                                             const VNotebook *p_notebook,
+                                             QWidget *p_parent)
+    : QDialog(p_parent), m_notebook(p_notebook)
 {
-    setupUI(p_title, p_name);
+    setupUI(p_title, m_notebook->getName());
 }
 
 void VDeleteNotebookDialog::setupUI(const QString &p_title, const QString &p_name)
@@ -20,7 +21,7 @@ void VDeleteNotebookDialog::setupUI(const QString &p_title, const QString &p_nam
 
     m_deleteCheck = new QCheckBox(tr("Delete files from disk"), this);
     m_deleteCheck->setChecked(false);
-    m_deleteCheck->setToolTip(tr("When checked, VNote will delete all the files within this notebook from disk"));
+    m_deleteCheck->setToolTip(tr("When checked, VNote will delete all files (including Recycle Bin) of this notebook from disk"));
     connect(m_deleteCheck, &QCheckBox::stateChanged,
             this, &VDeleteNotebookDialog::deleteCheckChanged);
 
@@ -109,12 +110,16 @@ void VDeleteNotebookDialog::deleteCheckChanged(int p_state)
 {
     if (!p_state) {
         m_warningLabel->setText(tr("VNote won't delete files in directory <span style=\"%1\">%2</span>.")
-                                  .arg(g_config->c_dataTextStyle).arg(m_path));
+                                  .arg(g_config->c_dataTextStyle).arg(m_notebook->getPath()));
     } else {
         m_warningLabel->setText(tr("<span style=\"%1\">WARNING</span>: "
-                                   "VNote may delete <b>ANY</b> files in directory <span style=\"%2\">%3</span>! "
-                                   "VNote will try to delete all the root folders within this notebook one by one. "
+                                   "VNote may delete <b>ANY</b> files in directory <span style=\"%2\">%3</span> "
+                                   "and directory <span style=\"%2\">%4</span>!<br>"
+                                   "VNote will try to delete all the root folders within this notebook one by one.<br>"
                                    "It may be UNRECOVERABLE!")
-                                  .arg(g_config->c_warningTextStyle).arg(g_config->c_dataTextStyle).arg(m_path));
+                                  .arg(g_config->c_warningTextStyle)
+                                  .arg(g_config->c_dataTextStyle)
+                                  .arg(m_notebook->getPath())
+                                  .arg(m_notebook->getRecycleBinFolderPath()));
     }
 }

+ 4 - 2
src/dialog/vdeletenotebookdialog.h

@@ -9,12 +9,14 @@ class QLineEdit;
 class QString;
 class QCheckBox;
 class QDialogButtonBox;
+class VNotebook;
 
 class VDeleteNotebookDialog : public QDialog
 {
     Q_OBJECT
 public:
-    VDeleteNotebookDialog(const QString &p_title, const QString &p_name, const QString &p_path,
+    VDeleteNotebookDialog(const QString &p_title,
+                          const VNotebook *p_notebook,
                           QWidget *p_parent = 0);
 
     // Whether delete files from disk.
@@ -27,7 +29,7 @@ private:
     void setupUI(const QString &p_title, const QString &p_name);
     QPixmap standardIcon(QMessageBox::Icon p_icon);
 
-    QString m_path;
+    const VNotebook *m_notebook;
     QLabel *m_warningLabel;
     QCheckBox *m_deleteCheck;
     QDialogButtonBox *m_btnBox;

+ 2 - 1
src/dialog/vdirinfodialog.cpp

@@ -72,7 +72,8 @@ void VDirInfoDialog::handleInputChanged()
     if (nameOk && name != m_directory->getName()) {
         // Check if the name conflicts with existing directory name.
         // Case-insensitive when creating note.
-        if (m_parentDirectory->findSubDirectory(name, false)) {
+        const VDirectory *directory = m_parentDirectory->findSubDirectory(name, false);
+        if (directory && directory != m_directory) {
             nameOk = false;
             showWarnLabel = true;
             QString nameConflictText = tr("<span style=\"%1\">WARNING</span>: Name (case-insensitive) already exists. "

+ 2 - 1
src/dialog/vfileinfodialog.cpp

@@ -89,7 +89,8 @@ void VFileInfoDialog::handleInputChanged()
     if (nameOk && name != m_file->getName()) {
         // Check if the name conflicts with existing note name.
         // Case-insensitive when creating note.
-        if (m_directory->findFile(name, false)) {
+        const VFile *file = m_directory->findFile(name, false);
+        if (file && file != m_file) {
             nameOk = false;
             showWarnLabel = true;
             QString nameConflictText = tr("<span style=\"%1\">WARNING</span>: Name (case-insensitive) already exists. "

+ 8 - 5
src/dialog/vnotebookinfodialog.cpp

@@ -42,9 +42,11 @@ void VNotebookInfoDialog::setupUI(const QString &p_title, const QString &p_info)
                                      "(empty to use global configuration)"));
     QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp), m_imageFolderEdit);
     m_imageFolderEdit->setValidator(validator);
-    QLabel *imageFolderLabel = new QLabel(tr("&Image folder:"));
-    imageFolderLabel->setBuddy(m_imageFolderEdit);
-    imageFolderLabel->setToolTip(m_imageFolderEdit->toolTip());
+
+    // 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"));
 
     // Created time.
     QString createdTimeStr = const_cast<VNotebook *>(m_notebook)->getCreatedTimeUtc().toLocalTime()
@@ -54,7 +56,8 @@ void VNotebookInfoDialog::setupUI(const QString &p_title, const QString &p_info)
     QFormLayout *topLayout = new QFormLayout();
     topLayout->addRow(tr("Notebook &name:"), m_nameEdit);
     topLayout->addRow(tr("Notebook &root folder:"), m_pathEdit);
-    topLayout->addRow(imageFolderLabel, m_imageFolderEdit);
+    topLayout->addRow(tr("&Image folder:"), m_imageFolderEdit);
+    topLayout->addRow(tr("Recycle bin folder:"), recycleBinFolderEdit);
     topLayout->addRow(tr("Created time:"), createdTimeLabel);
 
     // Warning label.
@@ -100,7 +103,7 @@ void VNotebookInfoDialog::handleInputChanged()
             }
         }
 
-        if (idx < m_notebooks.size()) {
+        if (idx < m_notebooks.size() && m_notebooks[idx] != m_notebook) {
             nameOk = false;
             showWarnLabel = true;
             QString nameConflictText = tr("<span style=\"%1\">WARNING</span>: Name (case-insensitive) already exists. "

+ 9 - 0
src/resources/icons/empty_recycle_bin.svg

@@ -0,0 +1,9 @@
+<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <g display="inline">
+  <title>Layer 1</title>
+  <path id="svg_6" fill-opacity="0" d="m85.75,73.84658l43.19437,360.59092l241.12296,0l43.05767,-361.68444l-327.375,1.09352z" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="30" fill="#000000" stroke="#000000"/>
+ </g>
+ <g display="inline">
+  <title>Layer 1 copy</title>
+ </g>
+</svg>

File diff suppressed because it is too large
+ 7 - 0
src/resources/icons/recycle_bin.svg


+ 3 - 0
src/resources/vnote.ini

@@ -107,6 +107,9 @@ markdownit_opt_breaks=false
 ; Auto-convert URL-like text to links
 markdownit_opt_linkify=true
 
+; Default name of the recycle bin of notebook
+recycle_bin_folder=_v_recycle_bin
+
 [session]
 tools_dock_checked=true
 

+ 98 - 0
src/utils/vutils.cpp

@@ -24,6 +24,7 @@
 
 #include "vfile.h"
 #include "vnote.h"
+#include "vnotebook.h"
 
 extern VConfigManager *g_config;
 
@@ -718,3 +719,100 @@ QString VUtils::getShortcutText(const QString &p_keySeq)
 {
     return QKeySequence(p_keySeq).toString(QKeySequence::NativeText);
 }
+
+static QString getRecycleBinSubFolderToUse(const VNotebook *p_notebook)
+{
+    QString folderPath = p_notebook->getRecycleBinFolderPath();
+    QDir dir(folderPath);
+    return QDir::cleanPath(dir.absoluteFilePath(QDateTime::currentDateTime().toString("yyyyMMdd")));
+}
+
+bool VUtils::deleteDirectory(const VNotebook *p_notebook,
+                             const QString &p_path,
+                             bool p_skipRecycleBin)
+{
+    if (p_skipRecycleBin) {
+        QDir dir(p_path);
+        return dir.removeRecursively();
+    } else {
+        // Move it to the recycle bin folder.
+        QString binPath = getRecycleBinSubFolderToUse(p_notebook);
+        QDir binDir(binPath);
+        if (!binDir.exists()) {
+            binDir.mkpath(binPath);
+            if (!binDir.exists()) {
+                return false;
+            }
+        }
+
+        QString destName = getFileNameWithSequence(binPath,
+                                                   directoryNameFromPath(p_path));
+
+        qDebug() << "try to move" << p_path << "to" << binPath << "as" << destName;
+        if (!binDir.rename(p_path, binDir.filePath(destName))) {
+            qWarning() << "fail to move directory" << p_path << "to" << binDir.filePath(destName);
+            return false;
+        }
+
+        return true;
+    }
+}
+
+bool VUtils::emptyDirectory(const VNotebook *p_notebook,
+                            const QString &p_path,
+                            bool p_skipRecycleBin)
+{
+    QDir dir(p_path);
+    if (!dir.exists()) {
+        return true;
+    }
+
+    QFileInfoList nodes = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::Hidden
+                                            | QDir::NoSymLinks | QDir::NoDotAndDotDot);
+    for (int i = 0; i < nodes.size(); ++i) {
+        const QFileInfo &fileInfo = nodes.at(i);
+        if (fileInfo.isDir()) {
+            if (!deleteDirectory(p_notebook, fileInfo.absoluteFilePath(), p_skipRecycleBin)) {
+                return false;
+            }
+        } else {
+            Q_ASSERT(fileInfo.isFile());
+            if (!deleteFile(p_notebook, fileInfo.absoluteFilePath(), p_skipRecycleBin)) {
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+bool VUtils::deleteFile(const VNotebook *p_notebook,
+                        const QString &p_path,
+                        bool p_skipRecycleBin)
+{
+    if (p_skipRecycleBin) {
+        QFile file(p_path);
+        return file.remove();
+    } else {
+        // Move it to the recycle bin folder.
+        QString binPath = getRecycleBinSubFolderToUse(p_notebook);
+        QDir binDir(binPath);
+        if (!binDir.exists()) {
+            binDir.mkpath(binPath);
+            if (!binDir.exists()) {
+                return false;
+            }
+        }
+
+        QString destName = getFileNameWithSequence(binPath,
+                                                   fileNameFromPath(p_path));
+
+        qDebug() << "try to move" << p_path << "to" << binPath << "as" << destName;
+        if (!binDir.rename(p_path, binDir.filePath(destName))) {
+            qWarning() << "fail to move file" << p_path << "to" << binDir.filePath(destName);
+            return false;
+        }
+
+        return true;
+    }
+}

+ 22 - 0
src/utils/vutils.h

@@ -12,6 +12,7 @@
 
 class QKeyEvent;
 class VFile;
+class VNotebook;
 
 #if !defined(V_ASSERT)
     #define V_ASSERT(cond) ((!(cond)) ? qt_assert(#cond, __FILE__, __LINE__) : qt_noop())
@@ -124,6 +125,27 @@ public:
     // Returns the shortcut text.
     static QString getShortcutText(const QString &p_keySeq);
 
+    // Delete directory recursively specified by @p_path.
+    // Will just move the directory to the recycle bin of @p_notebook if
+    // @p_skipRecycleBin is false.
+    static bool deleteDirectory(const VNotebook *p_notebook,
+                                const QString &p_path,
+                                bool p_skipRecycleBin = false);
+
+    // Empty all files in directory recursively specified by @p_path.
+    // Will just move files to the recycle bin of @p_notebook if
+    // @p_skipRecycleBin is false.
+    static bool emptyDirectory(const VNotebook *p_notebook,
+                               const QString &p_path,
+                               bool p_skipRecycleBin = false);
+
+    // Delete file specified by @p_path.
+    // Will just move the file to the recycle bin of @p_notebook if
+    // @p_skipRecycleBin is false.
+    static bool deleteFile(const VNotebook *p_notebook,
+                           const QString &p_path,
+                           bool p_skipRecycleBin = false);
+
     // Regular expression for image link.
     // ![image title]( http://github.com/tamlok/vnote.jpg "alt \" text" )
     // Captured texts (need to be trimmed):

+ 3 - 0
src/vconfigmanager.cpp

@@ -197,6 +197,9 @@ void VConfigManager::initialize()
 
     m_markdownitOptLinkify = getConfigFromSettings("global",
                                                    "markdownit_opt_linkify").toBool();
+
+    m_recycleBinFolder = getConfigFromSettings("global",
+                                               "recycle_bin_folder").toString();
 }
 
 void VConfigManager::readPredefinedColorsFromSettings()

+ 10 - 0
src/vconfigmanager.h

@@ -264,6 +264,8 @@ public:
     MarkdownitOption getMarkdownitOption() const;
     void setMarkdownitOption(const MarkdownitOption &p_opt);
 
+    const QString &getRecycleBinFolder() const;
+
     // Return the configured key sequence of @p_operation.
     // Return empty if there is no corresponding config.
     QString getShortcutKeySequence(const QString &p_operation) const;
@@ -531,6 +533,9 @@ private:
     // Auto-convert URL-like text to links.
     bool m_markdownitOptLinkify;
 
+    // Default name of the recycle bin folder of notebook.
+    QString m_recycleBinFolder;
+
     // The name of the config file in each directory, obsolete.
     // Use c_dirConfigFile instead.
     static const QString c_obsoleteDirConfigFile;
@@ -1377,4 +1382,9 @@ inline void VConfigManager::setMarkdownitOption(const MarkdownitOption &p_opt)
     }
 }
 
+inline const QString &VConfigManager::getRecycleBinFolder() const
+{
+    return m_recycleBinFolder;
+}
+
 #endif // VCONFIGMANAGER_H

+ 1 - 0
src/vconstants.h

@@ -30,6 +30,7 @@ namespace DirConfig
     static const QString c_subDirectories = "sub_directories";
     static const QString c_files = "files";
     static const QString c_imageFolder = "image_folder";
+    static const QString c_recycleBinFolder = "recycle_bin_folder";
     static const QString c_name = "name";
     static const QString c_createdTime = "created_time";
     static const QString c_modifiedTime = "modified_time";

+ 6 - 5
src/vdirectory.cpp

@@ -412,20 +412,21 @@ VDirectory *VDirectory::addSubDirectory(const QString &p_name, int p_index)
     return dir;
 }
 
-void VDirectory::deleteSubDirectory(VDirectory *p_subDir)
+void VDirectory::deleteSubDirectory(VDirectory *p_subDir, bool p_skipRecycleBin)
 {
+    Q_ASSERT(p_subDir->getNotebook() == m_notebook);
+
     QString dirPath = p_subDir->fetchPath();
 
     p_subDir->close();
 
     removeSubDirectory(p_subDir);
 
-    // Delete the entire directory
-    QDir dir(dirPath);
-    if (!dir.removeRecursively()) {
+    // Delete the entire directory.
+    if (!VUtils::deleteDirectory(m_notebook, dirPath, p_skipRecycleBin)) {
         qWarning() << "fail to remove directory" << dirPath << "recursively";
     } else {
-        qDebug() << "deleted" << dirPath << "from disk";
+        qDebug() << "deleted" << dirPath << (p_skipRecycleBin ? "from disk" : "to recycle bin");
     }
 
     delete p_subDir;

+ 2 - 1
src/vdirectory.h

@@ -35,7 +35,8 @@ public:
 
     VFile *createFile(const QString &p_name);
 
-    void deleteSubDirectory(VDirectory *p_subDir);
+    // Remove and delete subdirectory @p_subDir.
+    void deleteSubDirectory(VDirectory *p_subDir, bool p_skipRecycleBin = false);
 
     // Remove the file in the config and m_files without deleting it in the disk.
     // It won't change the parent of @p_file to enable it find its path.

+ 3 - 2
src/vdirectorytree.cpp

@@ -447,9 +447,10 @@ void VDirectoryTree::deleteDirectory()
                                   tr("Are you sure to delete folder <span style=\"%1\">%2</span>?")
                                     .arg(g_config->c_dataTextStyle).arg(curDir->getName()),
                                   tr("<span style=\"%1\">WARNING</span>: "
-                                     "VNote will delete the whole directory (<b>ANY</b> files) "
+                                     "VNote will delete the whole directory "
                                      "<span style=\"%2\">%3</span>."
-                                     "<br>It may be UNRECOVERABLE!")
+                                     "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(curDir->fetchPath()),
                                   QMessageBox::Ok | QMessageBox::Cancel,
                                   QMessageBox::Ok, this, MessageBoxType::Danger);

+ 2 - 4
src/vfile.cpp

@@ -64,8 +64,7 @@ void VFile::deleteDiskFile()
 
     // Delete the file
     QString filePath = fetchPath();
-    QFile file(filePath);
-    if (file.remove()) {
+    if (VUtils::deleteFile(getNotebook(), filePath, false)) {
         qDebug() << "deleted" << filePath;
     } else {
         qWarning() << "fail to delete" << filePath;
@@ -96,8 +95,7 @@ void VFile::deleteLocalImages()
                                                                     ImageLink::LocalRelativeInternal);
     int deleted = 0;
     for (int i = 0; i < images.size(); ++i) {
-        QFile file(images[i].m_path);
-        if (file.remove()) {
+        if (VUtils::deleteFile(getNotebook(), images[i].m_path, false)) {
             ++deleted;
         }
     }

+ 6 - 2
src/vfilelist.cpp

@@ -394,8 +394,12 @@ void VFileList::deleteFile(VFile *p_file)
     int ret = VUtils::showMessage(QMessageBox::Warning, tr("Warning"),
                                   tr("Are you sure to delete note <span style=\"%1\">%2</span>?")
                                     .arg(g_config->c_dataTextStyle).arg(fileName),
-                                  tr("<span style=\"%1\">WARNING</span>: The files (including images) "
-                                     "deleted may be UNRECOVERABLE!")
+                                  tr("<span style=\"%1\">WARNING</span>: "
+                                     "VNote will delete the note as well as all "
+                                     "its images and attachments managed by VNote. "
+                                     "You could find deleted files in the recycle "
+                                     "bin of this notebook.<br>"
+                                     "The operation is IRREVERSIBLE!")
                                     .arg(g_config->c_warningTextStyle),
                                   QMessageBox::Ok | QMessageBox::Cancel,
                                   QMessageBox::Ok, this, MessageBoxType::Danger);

+ 1 - 0
src/vmainwindow.h

@@ -34,6 +34,7 @@ class VTabIndicator;
 class VSingleInstanceGuard;
 class QTimer;
 class QSystemTrayIcon;
+class QShortcut;
 
 class VMainWindow : public QMainWindow
 {

+ 2 - 2
src/vmdedit.cpp

@@ -255,7 +255,7 @@ void VMdEdit::clearUnusedImages()
 
             // This inserted image is no longer in the file.
             if (j == images.size()) {
-                if (!QFile(link.m_path).remove()) {
+                if (!VUtils::deleteFile(m_file->getNotebook(), link.m_path, false)) {
                     qWarning() << "fail to delete unused inserted image" << link.m_path;
                 } else {
                     qDebug() << "delete unused inserted image" << link.m_path;
@@ -280,7 +280,7 @@ void VMdEdit::clearUnusedImages()
 
         // Original local relative image is no longer in the file.
         if (j == images.size()) {
-            if (!QFile(link.m_path).remove()) {
+            if (!VUtils::deleteFile(m_file->getNotebook(), link.m_path, false)) {
                 qWarning() << "fail to delete unused original image" << link.m_path;
             } else {
                 qDebug() << "delete unused original image" << link.m_path;

+ 2 - 0
src/vnote.qrc

@@ -121,5 +121,7 @@
         <file>resources/docs/markdown_guide_en.md</file>
         <file>resources/docs/markdown_guide_zh.md</file>
         <file>utils/highlightjs/highlightjs-line-numbers.min.js</file>
+        <file>resources/icons/recycle_bin.svg</file>
+        <file>resources/icons/empty_recycle_bin.svg</file>
     </qresource>
 </RCC>

+ 47 - 6
src/vnotebook.cpp

@@ -12,6 +12,7 @@ VNotebook::VNotebook(const QString &name, const QString &path, QObject *parent)
     : QObject(parent), m_name(name)
 {
     m_path = QDir::cleanPath(path);
+    m_recycleBinFolder = g_config->getRecycleBinFolder();
     m_rootDir = new VDirectory(this,
                                VUtils::directoryNameFromPath(path),
                                NULL,
@@ -37,6 +38,12 @@ bool VNotebook::readConfig()
         m_imageFolder = it.value().toString();
     }
 
+    // [recycle_bin_folder] section.
+    it = configJson.find(DirConfig::c_recycleBinFolder);
+    if (it != configJson.end()) {
+        m_recycleBinFolder = it.value().toString();
+    }
+
     return true;
 }
 
@@ -47,6 +54,9 @@ QJsonObject VNotebook::toConfigJsonNotebook() const
     // [image_folder] section.
     json[DirConfig::c_imageFolder] = m_imageFolder;
 
+    // [recycle_bin_folder] section.
+    json[DirConfig::c_recycleBinFolder] = m_recycleBinFolder;
+
     return json;
 }
 
@@ -69,9 +79,9 @@ bool VNotebook::writeToConfig() const
     return VConfigManager::writeDirectoryConfig(m_path, toConfigJson());
 }
 
-bool VNotebook::writeConfig() const
+bool VNotebook::writeConfigNotebook() const
 {
-    QJsonObject json = toConfigJson();
+    QJsonObject nbJson = toConfigJsonNotebook();
 
     QJsonObject configJson = VConfigManager::readDirectoryConfig(m_path);
     if (configJson.isEmpty()) {
@@ -79,10 +89,11 @@ bool VNotebook::writeConfig() const
         return false;
     }
 
-    json[DirConfig::c_subDirectories] = configJson[DirConfig::c_subDirectories];
-    json[DirConfig::c_files] = configJson[DirConfig::c_files];
+    for (auto it = nbJson.begin(); it != nbJson.end(); ++it) {
+        configJson[it.key()] = it.value();
+    }
 
-    return VConfigManager::writeDirectoryConfig(m_path, json);
+    return VConfigManager::writeDirectoryConfig(m_path, configJson);
 }
 
 const QString &VNotebook::getName() const
@@ -102,6 +113,16 @@ void VNotebook::close()
 
 bool VNotebook::open()
 {
+    QString recycleBinPath = getRecycleBinFolderPath();
+    if (!QFileInfo::exists(recycleBinPath)) {
+        QDir dir(m_path);
+        if (!dir.mkpath(recycleBinPath)) {
+            qWarning() << "fail to create recycle bin folder" << recycleBinPath
+                       << "for notebook" << m_name;
+            return false;
+        }
+    }
+
     return m_rootDir->open();
 }
 
@@ -148,10 +169,20 @@ bool VNotebook::deleteNotebook(VNotebook *p_notebook, bool p_deleteFiles)
             goto exit;
         }
 
+        // Delete sub directories.
         VDirectory *rootDir = p_notebook->getRootDir();
         QVector<VDirectory *> subdirs = rootDir->getSubDirs();
         for (auto dir : subdirs) {
-            rootDir->deleteSubDirectory(dir);
+            // Skip recycle bin.
+            rootDir->deleteSubDirectory(dir, true);
+        }
+
+        // Delete the recycle bin.
+        QDir recycleDir(p_notebook->getRecycleBinFolderPath());
+        if (!recycleDir.removeRecursively()) {
+            qWarning() << "fail to delete notebook recycle bin folder"
+                       << p_notebook->getRecycleBinFolderPath();
+            ret = false;
         }
 
         // Delete the config file.
@@ -255,3 +286,13 @@ QDateTime VNotebook::getCreatedTimeUtc()
 
     return m_rootDir->getCreatedTimeUtc();
 }
+
+QString VNotebook::getRecycleBinFolderPath() const
+{
+    QFileInfo fi(m_recycleBinFolder);
+    if (fi.isAbsolute()) {
+        return m_recycleBinFolder;
+    } else {
+        return QDir(m_path).filePath(m_recycleBinFolder);
+    }
+}

+ 18 - 3
src/vnotebook.h

@@ -13,6 +13,7 @@ class VNotebook : public QObject
     Q_OBJECT
 public:
     VNotebook(const QString &name, const QString &path, QObject *parent = 0);
+
     ~VNotebook();
 
     // Open the root directory to load contents
@@ -54,15 +55,20 @@ public:
     // Return m_imageFolder.
     const QString &getImageFolderConfig() const;
 
+    // Return m_recycleBinFolder.
+    const QString &getRecycleBinFolder() const;
+
+    // Get the recycle folder path for this notebook to use.
+    QString getRecycleBinFolderPath() const;
+
     void setImageFolder(const QString &p_imageFolder);
 
     // Read configurations (excluding "sub_directories" and "files" section)
     // from root directory config file.
     bool readConfig();
 
-    // Write configurations (excluding "sub_directories" and "files" section)
-    // to root directory config file.
-    bool writeConfig() const;
+    // Write configurations only related to notebook to root directory config file.
+    bool writeConfigNotebook() const;
 
     // Return only the info of notebook part in json.
     QJsonObject toConfigJsonNotebook() const;
@@ -88,6 +94,10 @@ private:
     // Otherwise, VNote will use the global configured folder.
     QString m_imageFolder;
 
+    // Folder name to store deleted files.
+    // Could be relative or absolute.
+    QString m_recycleBinFolder;
+
     // Parent is NULL for root directory
     VDirectory *m_rootDir;
 };
@@ -97,4 +107,9 @@ inline VDirectory *VNotebook::getRootDir() const
     return m_rootDir;
 }
 
+inline const QString &VNotebook::getRecycleBinFolder() const
+{
+    return m_recycleBinFolder;
+}
+
 #endif // VNOTEBOOK_H

+ 78 - 2
src/vnotebookselector.cpp

@@ -81,6 +81,78 @@ void VNotebookSelector::initActions()
                 QUrl url = QUrl::fromLocalFile(notebook->getPath());
                 QDesktopServices::openUrl(url);
             });
+
+    m_recycleBinAct = new QAction(QIcon(":/resources/icons/recycle_bin.svg"),
+                                  tr("&Recycle Bin"), this);
+    m_recycleBinAct->setToolTip(tr("Open the recycle bin of this notebook"));
+    connect(m_recycleBinAct, &QAction::triggered,
+            this, [this]() {
+                QList<QListWidgetItem *> items = this->m_listWidget->selectedItems();
+                if (items.isEmpty()) {
+                    return;
+                }
+
+                Q_ASSERT(items.size() == 1);
+                QListWidgetItem *item = items[0];
+                int index = this->indexOfListItem(item);
+                VNotebook *notebook = this->getNotebookFromComboIndex(index);
+                QUrl url = QUrl::fromLocalFile(notebook->getRecycleBinFolderPath());
+                QDesktopServices::openUrl(url);
+            });
+
+    m_emptyRecycleBinAct = new QAction(QIcon(":/resources/icons/empty_recycle_bin.svg"),
+                                       tr("&Empty Recycle Bin"), this);
+    m_emptyRecycleBinAct->setToolTip(tr("Empty the recycle bin of this notebook"));
+    connect(m_emptyRecycleBinAct, &QAction::triggered,
+            this, [this]() {
+                QList<QListWidgetItem *> items = this->m_listWidget->selectedItems();
+                if (items.isEmpty()) {
+                    return;
+                }
+
+                Q_ASSERT(items.size() == 1);
+                QListWidgetItem *item = items[0];
+                int index = this->indexOfListItem(item);
+                VNotebook *notebook = this->getNotebookFromComboIndex(index);
+                QString binPath = notebook->getRecycleBinFolderPath();
+
+                int ret = VUtils::showMessage(QMessageBox::Warning, tr("Warning"),
+                                              tr("Are you sure to empty recycle bin of notebook "
+                                                 "<span style=\"%1\">%2</span>?")
+                                                .arg(g_config->c_dataTextStyle)
+                                                .arg(notebook->getName()),
+                                              tr("<span style=\"%1\">WARNING</span>: "
+                                                 "VNote will delete all the files in directory "
+                                                 "<span style=\"%2\">%3</span>."
+                                                 "<br>It may be UNRECOVERABLE!")
+                                                .arg(g_config->c_warningTextStyle)
+                                                .arg(g_config->c_dataTextStyle)
+                                                .arg(binPath),
+                                              QMessageBox::Ok | QMessageBox::Cancel,
+                                              QMessageBox::Ok, this, MessageBoxType::Danger);
+                if (ret == QMessageBox::Ok) {
+                    QString info;
+                    if (VUtils::emptyDirectory(notebook, binPath, true)) {
+                        info = tr("Successfully emptied recycle bin of notebook "
+                                  "<span style=\"%1\">%2</span>!")
+                                 .arg(g_config->c_dataTextStyle)
+                                 .arg(notebook->getName());
+                    } else {
+                        info = tr("Fail to empty recycle bin of notebook "
+                                  "<span style=\"%1\">%2</span>!")
+                                 .arg(g_config->c_dataTextStyle)
+                                 .arg(notebook->getName());
+                    }
+
+                    VUtils::showMessage(QMessageBox::Information,
+                                        tr("Information"),
+                                        info,
+                                        "",
+                                        QMessageBox::Ok,
+                                        QMessageBox::Ok,
+                                        this);
+                }
+            });
 }
 
 void VNotebookSelector::updateComboBox()
@@ -244,7 +316,7 @@ void VNotebookSelector::deleteNotebook()
     VNotebook *notebook = getNotebookFromComboIndex(index);
     Q_ASSERT(notebook);
 
-    VDeleteNotebookDialog dialog(tr("Delete Notebook"), notebook->getName(), notebook->getPath(), this);
+    VDeleteNotebookDialog dialog(tr("Delete Notebook"), notebook, this);
     if (dialog.exec() == QDialog::Accepted) {
         bool deleteFiles = dialog.getDeleteFiles();
         m_editArea->closeFile(notebook, true);
@@ -323,7 +395,7 @@ void VNotebookSelector::editNotebookInfo()
         if (imageFolder != notebook->getImageFolderConfig()) {
             updated = true;
             notebook->setImageFolder(imageFolder);
-            notebook->writeConfig();
+            notebook->writeConfigNotebook();
         }
 
         if (updated) {
@@ -369,6 +441,10 @@ void VNotebookSelector::requestPopupListContextMenu(QPoint p_pos)
     QMenu menu(this);
     menu.setToolTipsVisible(true);
     menu.addAction(m_deleteNotebookAct);
+    menu.addSeparator();
+    menu.addAction(m_recycleBinAct);
+    menu.addAction(m_emptyRecycleBinAct);
+    menu.addSeparator();
     menu.addAction(m_openLocationAct);
     menu.addAction(m_notebookInfoAct);
 

+ 2 - 0
src/vnotebookselector.h

@@ -90,6 +90,8 @@ private:
     QAction *m_deleteNotebookAct;
     QAction *m_notebookInfoAct;
     QAction *m_openLocationAct;
+    QAction *m_recycleBinAct;
+    QAction *m_emptyRecycleBinAct;
 
     // We will add several special action item in the combobox. This is the start index
     // of the real notebook items related to m_notebooks.

Some files were not shown because too many files changed in this diff