diff --git a/src/dialog/vconfirmdeletiondialog.cpp b/src/dialog/vconfirmdeletiondialog.cpp index 0510ccbe..8703e83b 100644 --- a/src/dialog/vconfirmdeletiondialog.cpp +++ b/src/dialog/vconfirmdeletiondialog.cpp @@ -56,8 +56,14 @@ private: VConfirmDeletionDialog::VConfirmDeletionDialog(const QString &p_title, const QString &p_info, const QVector &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 &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()); diff --git a/src/dialog/vconfirmdeletiondialog.h b/src/dialog/vconfirmdeletiondialog.h index 0a73ce0b..d78c5985 100644 --- a/src/dialog/vconfirmdeletiondialog.h +++ b/src/dialog/vconfirmdeletiondialog.h @@ -19,10 +19,15 @@ public: VConfirmDeletionDialog(const QString &p_title, const QString &p_info, const QVector &p_files, + bool p_enableAskAgain, + bool p_askAgainEnabled, + bool p_enablePreview, QWidget *p_parent = 0); QVector 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 diff --git a/src/dialog/vdirinfodialog.cpp b/src/dialog/vdirinfodialog.cpp index e29c94b7..63a9d477 100644 --- a/src/dialog/vdirinfodialog.cpp +++ b/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(); diff --git a/src/dialog/vfileinfodialog.cpp b/src/dialog/vfileinfodialog.cpp index 88435242..59ad3ed0 100644 --- a/src/dialog/vfileinfodialog.cpp +++ b/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); diff --git a/src/dialog/vnewnotebookdialog.cpp b/src/dialog/vnewnotebookdialog.cpp index d385bff9..f19d8ca8 100644 --- a/src/dialog/vnewnotebookdialog.cpp +++ b/src/dialog/vnewnotebookdialog.cpp @@ -12,11 +12,11 @@ VNewNotebookDialog::VNewNotebookDialog(const QString &title, const QString &info const QVector &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); diff --git a/src/dialog/vnewnotebookdialog.h b/src/dialog/vnewnotebookdialog.h index e470330b..d1f38105 100644 --- a/src/dialog/vnewnotebookdialog.h +++ b/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; diff --git a/src/dialog/vnotebookinfodialog.cpp b/src/dialog/vnotebookinfodialog.cpp index 7383856f..d52eec1d 100644 --- a/src/dialog/vnotebookinfodialog.cpp +++ b/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(m_notebook)->getCreatedTimeUtc().toLocalTime() - .toString(Qt::DefaultLocaleLongDate); + QString createdTimeStr = VUtils::displayDateTime(const_cast(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); diff --git a/src/dialog/vnotebookinfodialog.h b/src/dialog/vnotebookinfodialog.h index 5e9eaf88..68d02fbd 100644 --- a/src/dialog/vnotebookinfodialog.h +++ b/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 &m_notebooks; diff --git a/src/dialog/vsettingsdialog.cpp b/src/dialog/vsettingsdialog.cpp index 4caa4add..e52f1ea5 100644 --- a/src/dialog/vsettingsdialog.cpp +++ b/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(); diff --git a/src/dialog/vsettingsdialog.h b/src/dialog/vsettingsdialog.h index 6dbf7c30..d9b1eeeb 100644 --- a/src/dialog/vsettingsdialog.h +++ b/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 diff --git a/src/dialog/vsortdialog.cpp b/src/dialog/vsortdialog.cpp new file mode 100644 index 00000000..f01149f4 --- /dev/null +++ b/src/dialog/vsortdialog.cpp @@ -0,0 +1,205 @@ +#include "vsortdialog.h" + +#include + +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 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); + } +} diff --git a/src/dialog/vsortdialog.h b/src/dialog/vsortdialog.h new file mode 100644 index 00000000..975ad00c --- /dev/null +++ b/src/dialog/vsortdialog.h @@ -0,0 +1,46 @@ +#ifndef VSORTDIALOG_H +#define VSORTDIALOG_H + +#include +#include + +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 diff --git a/src/resources/icons/add_attachment.svg b/src/resources/icons/add_attachment.svg new file mode 100644 index 00000000..c4b273c4 --- /dev/null +++ b/src/resources/icons/add_attachment.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/resources/icons/attachment.svg b/src/resources/icons/attachment.svg new file mode 100644 index 00000000..a5ffa065 --- /dev/null +++ b/src/resources/icons/attachment.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/resources/icons/attachment_full.svg b/src/resources/icons/attachment_full.svg new file mode 100644 index 00000000..7e400019 --- /dev/null +++ b/src/resources/icons/attachment_full.svg @@ -0,0 +1,14 @@ + + + Layer 1 + + + + + + + + Layer 2 + + + diff --git a/src/resources/icons/clear_attachment.svg b/src/resources/icons/clear_attachment.svg new file mode 100644 index 00000000..59af5e14 --- /dev/null +++ b/src/resources/icons/clear_attachment.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/resources/icons/delete_attachment.svg b/src/resources/icons/delete_attachment.svg new file mode 100644 index 00000000..1631e74d --- /dev/null +++ b/src/resources/icons/delete_attachment.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/resources/icons/locate_attachment.svg b/src/resources/icons/locate_attachment.svg new file mode 100644 index 00000000..19545aa6 --- /dev/null +++ b/src/resources/icons/locate_attachment.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/resources/icons/sort.svg b/src/resources/icons/sort.svg new file mode 100644 index 00000000..14f9bc25 --- /dev/null +++ b/src/resources/icons/sort.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index a43a6a9a..fb72cbd9 100644 --- a/src/resources/vnote.ini +++ b/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 diff --git a/src/src.pro b/src/src.pro index 6e8279c5..39730061 100644 --- a/src/src.pro +++ b/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 \ diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 5277e76b..46f16005 100644 --- a/src/utils/vutils.cpp +++ b/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 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 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; +} diff --git a/src/utils/vutils.h b/src/utils/vutils.h index a5999d3f..2c228de7 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -7,6 +7,7 @@ #include #include #include +#include #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): diff --git a/src/vattachmentlist.cpp b/src/vattachmentlist.cpp new file mode 100644 index 00000000..4fa2a563 --- /dev/null +++ b/src/vattachmentlist.cpp @@ -0,0 +1,404 @@ +#include "vattachmentlist.h" + +#include + +#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 " + "%2?") + .arg(g_config->c_dataTextStyle) + .arg(m_file->getName()), + tr("WARNING: " + "VNote will delete all the files in directory " + "%3." + "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(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 %2.") + .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 &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 &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 %3.") + .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 names; + const QList 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 " + "%2? " + "You could find deleted files in the recycle " + "bin of this notebook.
" + "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 %2.") + .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 &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 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(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 %2.") + .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); + } + } +} diff --git a/src/vattachmentlist.h b/src/vattachmentlist.h new file mode 100644 index 00000000..27d7709c --- /dev/null +++ b/src/vattachmentlist.h @@ -0,0 +1,60 @@ +#ifndef VATTACHMENTLIST_H +#define VATTACHMENTLIST_H + +#include +#include +#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 &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 diff --git a/src/vbuttonwithwidget.cpp b/src/vbuttonwithwidget.cpp index 30dfe558..53170d10 100644 --- a/src/vbuttonwithwidget.cpp +++ b/src/vbuttonwithwidget.cpp @@ -1,60 +1,44 @@ #include "vbuttonwithwidget.h" -#include -#include -#include +#include -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); -} + m_popupWidget->setParent(this); -void VButtonWithWidget::setPopupWidget(QWidget *p_widget) -{ - if (m_popupWidget) { - delete m_popupWidget; - } + 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); + }); - m_popupWidget = p_widget; - if (m_popupWidget) { - m_popupWidget->hide(); - m_popupWidget->setParent(NULL); - - 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(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(); } diff --git a/src/vbuttonwithwidget.h b/src/vbuttonwithwidget.h index bd43b043..42e1516e 100644 --- a/src/vbuttonwithwidget.h +++ b/src/vbuttonwithwidget.h @@ -4,35 +4,53 @@ #include #include #include +#include + +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(); diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index 68910e77..991b72a2 100644 --- a/src/vconfigmanager.cpp +++ b/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 &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(); diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index cde163c0..862d16e9 100644 --- a/src/vconfigmanager.h +++ b/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; diff --git a/src/vconstants.h b/src/vconstants.h index c0d60908..2b21275e 100644 --- a/src/vconstants.h +++ b/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"; diff --git a/src/vdirectory.cpp b/src/vdirectory.cpp index f3c64f0a..213d5c86 100644 --- a/src/vdirectory.cpp +++ b/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); diff --git a/src/vdirectory.h b/src/vdirectory.h index 061d379f..4e9e5858 100644 --- a/src/vdirectory.h +++ b/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); diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index b527768a..39049bba 100644 --- a/src/vmainwindow.cpp +++ b/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(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(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); diff --git a/src/vmainwindow.h b/src/vmainwindow.h index 0f2476d2..bc505bf4 100644 --- a/src/vmainwindow.h +++ b/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 predefinedColorPixmaps; // Single instance guard. diff --git a/src/vmdedit.cpp b/src/vmdedit.cpp index 4f0c8ed2..54611f0d 100644 --- a/src/vmdedit.cpp +++ b/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(); } diff --git a/src/vnote.qrc b/src/vnote.qrc index 1a9e9539..4293ae18 100644 --- a/src/vnote.qrc +++ b/src/vnote.qrc @@ -123,5 +123,12 @@ utils/highlightjs/highlightjs-line-numbers.min.js resources/icons/recycle_bin.svg resources/icons/empty_recycle_bin.svg + resources/icons/attachment.svg + resources/icons/attachment_full.svg + resources/icons/add_attachment.svg + resources/icons/clear_attachment.svg + resources/icons/locate_attachment.svg + resources/icons/delete_attachment.svg + resources/icons/sort.svg diff --git a/src/vnotebook.cpp b/src/vnotebook.cpp index b5b39329..da911183 100644 --- a/src/vnotebook.cpp +++ b/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(); diff --git a/src/vnotebook.h b/src/vnotebook.h index ccff4e66..bd99bd26 100644 --- a/src/vnotebook.h +++ b/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; diff --git a/src/vnotebookselector.cpp b/src/vnotebookselector.cpp index 46abe1eb..00605e80 100644 --- a/src/vnotebookselector.cpp +++ b/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(); } diff --git a/src/vnotebookselector.h b/src/vnotebookselector.h index dab19d7e..9f5797b5 100644 --- a/src/vnotebookselector.h +++ b/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); diff --git a/src/vnotefile.cpp b/src/vnotefile.cpp index 0307cd20..e9c6ad0b 100644 --- a/src/vnotefile.cpp +++ b/src/vnotefile.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #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 &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 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 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 &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 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; +} diff --git a/src/vnotefile.h b/src/vnotefile.h index f49001b6..7dee3c12 100644 --- a/src/vnotefile.h +++ b/src/vnotefile.h @@ -1,11 +1,30 @@ #ifndef VNOTEFILE_H #define VNOTEFILE_H +#include +#include + #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 &p_attachments = QVector()); 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 &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 &p_names); + + // Reorder attachments in m_attachments by index. + void sortAttachments(QVector p_sortedIdx); + + // Return the index of @p_name in m_attachments. + // -1 if not found. + int findAttachment(const QString &p_name, bool p_caseSensitive = true); + + 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 m_attachments; }; +inline const QString &VNoteFile::getAttachmentFolder() const +{ + return m_attachmentFolder; +} + +inline const QVector &VNoteFile::getAttachments() const +{ + return m_attachments; +} + #endif // VNOTEFILE_H diff --git a/src/vvimindicator.cpp b/src/vvimindicator.cpp index 648dbf91..609cdf27 100644 --- a/src/vvimindicator.cpp +++ b/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);