diff --git a/src/dialog/vdeletenotebookdialog.cpp b/src/dialog/vdeletenotebookdialog.cpp index 555ff4b7..ffab9fce 100644 --- a/src/dialog/vdeletenotebookdialog.cpp +++ b/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 %2.") - .arg(g_config->c_dataTextStyle).arg(m_path)); + .arg(g_config->c_dataTextStyle).arg(m_notebook->getPath())); } else { m_warningLabel->setText(tr("WARNING: " - "VNote may delete ANY files in directory %3! " - "VNote will try to delete all the root folders within this notebook one by one. " + "VNote may delete ANY files in directory %3 " + "and directory %4!
" + "VNote will try to delete all the root folders within this notebook one by one.
" "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())); } } diff --git a/src/dialog/vdeletenotebookdialog.h b/src/dialog/vdeletenotebookdialog.h index 34850689..d88baac8 100644 --- a/src/dialog/vdeletenotebookdialog.h +++ b/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; diff --git a/src/dialog/vdirinfodialog.cpp b/src/dialog/vdirinfodialog.cpp index b384021e..e29c94b7 100644 --- a/src/dialog/vdirinfodialog.cpp +++ b/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("WARNING: Name (case-insensitive) already exists. " diff --git a/src/dialog/vfileinfodialog.cpp b/src/dialog/vfileinfodialog.cpp index 94e95019..1413a12f 100644 --- a/src/dialog/vfileinfodialog.cpp +++ b/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("WARNING: Name (case-insensitive) already exists. " diff --git a/src/dialog/vnotebookinfodialog.cpp b/src/dialog/vnotebookinfodialog.cpp index 15f650c4..7383856f 100644 --- a/src/dialog/vnotebookinfodialog.cpp +++ b/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(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("WARNING: Name (case-insensitive) already exists. " diff --git a/src/resources/icons/empty_recycle_bin.svg b/src/resources/icons/empty_recycle_bin.svg new file mode 100644 index 00000000..a2200617 --- /dev/null +++ b/src/resources/icons/empty_recycle_bin.svg @@ -0,0 +1,9 @@ + + + Layer 1 + + + + Layer 1 copy + + diff --git a/src/resources/icons/recycle_bin.svg b/src/resources/icons/recycle_bin.svg new file mode 100644 index 00000000..89b5feec --- /dev/null +++ b/src/resources/icons/recycle_bin.svg @@ -0,0 +1,10 @@ + + + Layer 1 + + + + Layer 1 copy + + + diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index 329f24dd..b531d149 100644 --- a/src/resources/vnote.ini +++ b/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 diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index c7485d56..1f311009 100644 --- a/src/utils/vutils.cpp +++ b/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; + } +} diff --git a/src/utils/vutils.h b/src/utils/vutils.h index 84cbdc0a..7f106f89 100644 --- a/src/utils/vutils.h +++ b/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): diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index 0b67331b..fad62c66 100644 --- a/src/vconfigmanager.cpp +++ b/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() diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 78921ea3..30ec89f6 100644 --- a/src/vconfigmanager.h +++ b/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 diff --git a/src/vconstants.h b/src/vconstants.h index 61eafaf7..44da5975 100644 --- a/src/vconstants.h +++ b/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"; diff --git a/src/vdirectory.cpp b/src/vdirectory.cpp index 514411d9..0f6628ff 100644 --- a/src/vdirectory.cpp +++ b/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; diff --git a/src/vdirectory.h b/src/vdirectory.h index e0c40ba1..a10fab58 100644 --- a/src/vdirectory.h +++ b/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. diff --git a/src/vdirectorytree.cpp b/src/vdirectorytree.cpp index ba2daa21..6f51f7f7 100644 --- a/src/vdirectorytree.cpp +++ b/src/vdirectorytree.cpp @@ -447,9 +447,10 @@ void VDirectoryTree::deleteDirectory() tr("Are you sure to delete folder %2?") .arg(g_config->c_dataTextStyle).arg(curDir->getName()), tr("WARNING: " - "VNote will delete the whole directory (ANY files) " + "VNote will delete the whole directory " "%3." - "
It may be UNRECOVERABLE!") + "You could find deleted files in the recycle bin " + "of this notebook.
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); diff --git a/src/vfile.cpp b/src/vfile.cpp index 292fe7d8..e3aacb80 100644 --- a/src/vfile.cpp +++ b/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; } } diff --git a/src/vfilelist.cpp b/src/vfilelist.cpp index 1f4816de..03806f8c 100644 --- a/src/vfilelist.cpp +++ b/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 %2?") .arg(g_config->c_dataTextStyle).arg(fileName), - tr("WARNING: The files (including images) " - "deleted may be UNRECOVERABLE!") + tr("WARNING: " + "VNote will delete the note as well as all " + "its images and attachments managed by VNote. " + "You could find deleted files in the recycle " + "bin of this notebook.
" + "The operation is IRREVERSIBLE!") .arg(g_config->c_warningTextStyle), QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok, this, MessageBoxType::Danger); diff --git a/src/vmainwindow.h b/src/vmainwindow.h index f940b1b6..0f2476d2 100644 --- a/src/vmainwindow.h +++ b/src/vmainwindow.h @@ -34,6 +34,7 @@ class VTabIndicator; class VSingleInstanceGuard; class QTimer; class QSystemTrayIcon; +class QShortcut; class VMainWindow : public QMainWindow { diff --git a/src/vmdedit.cpp b/src/vmdedit.cpp index 262e0966..76d986f3 100644 --- a/src/vmdedit.cpp +++ b/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; diff --git a/src/vnote.qrc b/src/vnote.qrc index cc1266ba..1a9e9539 100644 --- a/src/vnote.qrc +++ b/src/vnote.qrc @@ -121,5 +121,7 @@ resources/docs/markdown_guide_en.md resources/docs/markdown_guide_zh.md utils/highlightjs/highlightjs-line-numbers.min.js + resources/icons/recycle_bin.svg + resources/icons/empty_recycle_bin.svg diff --git a/src/vnotebook.cpp b/src/vnotebook.cpp index c5aeb972..3d5d4a0e 100644 --- a/src/vnotebook.cpp +++ b/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 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); + } +} diff --git a/src/vnotebook.h b/src/vnotebook.h index 3bc0501a..1348cea6 100644 --- a/src/vnotebook.h +++ b/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 diff --git a/src/vnotebookselector.cpp b/src/vnotebookselector.cpp index b1c67437..46abe1eb 100644 --- a/src/vnotebookselector.cpp +++ b/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 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 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 " + "%2?") + .arg(g_config->c_dataTextStyle) + .arg(notebook->getName()), + tr("WARNING: " + "VNote will delete all the files in directory " + "%3." + "
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 " + "%2!") + .arg(g_config->c_dataTextStyle) + .arg(notebook->getName()); + } else { + info = tr("Fail to empty recycle bin of notebook " + "%2!") + .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); diff --git a/src/vnotebookselector.h b/src/vnotebookselector.h index eddcfa7c..dab19d7e 100644 --- a/src/vnotebookselector.h +++ b/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.