support attachments

Support attachments to internal note file.

- Add/Delete/Clear/Sort;
- Support custom attachment folder for each notebook (read-only);
- Support renaming attachment;
This commit is contained in:
Le Tan 2017-09-21 19:41:31 +08:00
parent a64d01ea86
commit 01788a5301
43 changed files with 1552 additions and 164 deletions

View File

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

View File

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

View File

@ -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();

View File

@ -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);

View File

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

View File

@ -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;

View File

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

View File

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

View File

@ -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();

View File

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

205
src/dialog/vsortdialog.cpp Normal file
View File

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

46
src/dialog/vsortdialog.h Normal file
View File

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

View File

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

After

Width:  |  Height:  |  Size: 597 B

View File

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

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1001 B

View File

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

After

Width:  |  Height:  |  Size: 995 B

View File

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

After

Width:  |  Height:  |  Size: 934 B

View File

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

After

Width:  |  Height:  |  Size: 840 B

View File

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

After

Width:  |  Height:  |  Size: 698 B

View File

@ -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

View File

@ -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 \

View File

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

View File

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

404
src/vattachmentlist.cpp Normal file
View File

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

60
src/vattachmentlist.h Normal file
View File

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

View File

@ -1,60 +1,44 @@
#include "vbuttonwithwidget.h"
#include <QEvent>
#include <QMouseEvent>
#include <QRect>
#include <QMenu>
VButtonWithWidget::VButtonWithWidget(QWidget *p_parent)
: QPushButton(p_parent), m_popupWidget(NULL)
VButtonWithWidget::VButtonWithWidget(QWidget *p_widget,
QWidget *p_parent)
: QPushButton(p_parent), m_popupWidget(p_widget)
{
init();
}
VButtonWithWidget::VButtonWithWidget(const QString &p_text, QWidget *p_parent)
: QPushButton(p_text, p_parent), m_popupWidget(NULL)
VButtonWithWidget::VButtonWithWidget(const QString &p_text,
QWidget *p_widget,
QWidget *p_parent)
: QPushButton(p_text, p_parent), m_popupWidget(p_widget)
{
init();
}
VButtonWithWidget::VButtonWithWidget(const QIcon &p_icon,
const QString &p_text,
QWidget *p_widget,
QWidget *p_parent)
: QPushButton(p_icon, p_text, p_parent), m_popupWidget(NULL)
: QPushButton(p_icon, p_text, p_parent), m_popupWidget(p_widget)
{
init();
}
VButtonWithWidget::~VButtonWithWidget()
{
if (m_popupWidget) {
delete m_popupWidget;
}
}
void VButtonWithWidget::init()
{
connect(this, &QPushButton::clicked,
this, &VButtonWithWidget::showPopupWidget);
}
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<QMouseEvent *>(p_event);
QPoint clickPos = eve->pos();
const QRect &rect = m_popupWidget->rect();
if (!rect.contains(clickPos)) {
m_popupWidget->hide();
}
}
return QPushButton::eventFilter(p_obj, p_event);
showMenu();
}

View File

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

View File

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

View File

@ -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;

View File

@ -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";

View File

@ -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);

View File

@ -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);

View File

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

View File

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

View File

@ -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();
}

View File

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

View File

@ -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();

View File

@ -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;

View File

@ -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();
}

View File

@ -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);

View File

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

View File

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

View File

@ -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);