diff --git a/VNote.pro b/VNote.pro index 914d8d08..ced264f0 100644 --- a/VNote.pro +++ b/VNote.pro @@ -18,14 +18,18 @@ SOURCES += main.cpp\ vnote.cpp \ vnotebook.cpp \ vnewdirdialog.cpp \ - vconfigmanager.cpp + vconfigmanager.cpp \ + vfilelist.cpp \ + vnewfiledialog.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ vnote.h \ vnotebook.h \ vnewdirdialog.h \ - vconfigmanager.h + vconfigmanager.h \ + vfilelist.h \ + vnewfiledialog.h RESOURCES += \ vnote.qrc diff --git a/vdirectorytree.cpp b/vdirectorytree.cpp index 98814364..1502d8f2 100644 --- a/vdirectorytree.cpp +++ b/vdirectorytree.cpp @@ -1,5 +1,4 @@ #include -#include #include "vdirectorytree.h" #include "vnewdirdialog.h" #include "vconfigmanager.h" @@ -10,15 +9,17 @@ VDirectoryTree::VDirectoryTree(QWidget *parent) setColumnCount(1); setHeaderHidden(true); setContextMenuPolicy(Qt::CustomContextMenu); - initialActions(); + initActions(); connect(this, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(updateItemSubtree(QTreeWidgetItem*))); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextMenuRequested(QPoint))); + connect(this, &VDirectoryTree::currentItemChanged, + this, &VDirectoryTree::currentDirectoryItemChanged); } -void VDirectoryTree::initialActions() +void VDirectoryTree::initActions() { newRootDirAct = new QAction(tr("New &root directory"), this); newRootDirAct->setStatusTip(tr("Create a new root directory in current notebook")); @@ -51,6 +52,9 @@ void VDirectoryTree::setTreePath(const QString& path) qDebug() << "set directory tree path:" << path; updateDirectoryTree(); + if (topLevelItemCount() > 0) { + setCurrentItem(topLevelItem(0)); + } } bool VDirectoryTree::validatePath(const QString &path) @@ -180,7 +184,7 @@ void VDirectoryTree::updateDirectoryTreeOne(QTreeWidgetItem &parent, int depth) if (configJson.isEmpty()) { qDebug() << "invalid notebook configuration for directory:" << path; QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), tr("Invalid notebook directory configuration.")); - msgBox.setInformativeText(QString("Notebook path \"%1\" does not contain a valid configuration file.") + msgBox.setInformativeText(QString("Notebook directory \"%1\" does not contain a valid configuration file.") .arg(path)); msgBox.exec(); return; @@ -356,7 +360,7 @@ QTreeWidgetItem* VDirectoryTree::createDirectoryAndUpdateTree(QTreeWidgetItem *p QString path = QDir(treePath).filePath(relativePath); QDir dir(path); if (!dir.mkdir(name)) { - qDebug() << "warning: fail to create directory" << name << "under" << path; + qWarning() << "error: fail to create directory" << name << "under" << path; QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), QString("Could not create directory \"%1\" under \"%2\".") .arg(name).arg(path)); msgBox.setInformativeText(QString("Please check if there already exists a directory named \"%1\".").arg(name)); @@ -458,3 +462,12 @@ bool VDirectoryTree::isConflictNameWithChildren(const QTreeWidgetItem *parent, c } return false; } + +void VDirectoryTree::currentDirectoryItemChanged(QTreeWidgetItem *currentItem) +{ + QJsonObject itemJson = currentItem->data(0, Qt::UserRole).toJsonObject(); + Q_ASSERT(!itemJson.isEmpty()); + itemJson["root_path"] = treePath; + qDebug() << "click dir:" << itemJson; + emit currentDirectoryChanged(itemJson); +} diff --git a/vdirectorytree.h b/vdirectorytree.h index e0cfe0f9..627a794d 100644 --- a/vdirectorytree.h +++ b/vdirectorytree.h @@ -2,16 +2,16 @@ #define VDIRECTORYTREE_H #include - -class QJsonObject; +#include class VDirectoryTree : public QTreeWidget { Q_OBJECT public: - VDirectoryTree(QWidget *parent = 0); + explicit VDirectoryTree(QWidget *parent = 0); signals: + void currentDirectoryChanged(QJsonObject itemJson); public slots: void setTreePath(const QString& path); @@ -27,6 +27,7 @@ private slots: void newSubDirectory(); void newRootDirectory(); void deleteDirectory(); + void currentDirectoryItemChanged(QTreeWidgetItem *currentItem); private: // Clean and pdate the TreeWidget according to treePath @@ -41,7 +42,7 @@ private: // Fill the QTreeWidgetItem according to its QJsonObject. // @relative_path is the path related to treePath. void fillDirectoryTreeItem(QTreeWidgetItem &item, QJsonObject itemJson, const QString &relativePath); - void initialActions(); + void initActions(); QTreeWidgetItem* createDirectoryAndUpdateTree(QTreeWidgetItem *parent, const QString &name, const QString &description); void deleteDirectoryAndUpdateTree(QTreeWidgetItem *item); diff --git a/vfilelist.cpp b/vfilelist.cpp new file mode 100644 index 00000000..5a38f656 --- /dev/null +++ b/vfilelist.cpp @@ -0,0 +1,261 @@ +#include +#include +#include "vfilelist.h" +#include "vconfigmanager.h" +#include "vnewfiledialog.h" + +VFileList::VFileList(QWidget *parent) + : QListWidget(parent) +{ + setContextMenuPolicy(Qt::CustomContextMenu); + initActions(); + + connect(this, &VFileList::customContextMenuRequested, + this, &VFileList::contextMenuRequested); + connect(this, &VFileList::currentItemChanged, + this, &VFileList::currentFileItemChanged); +} + +void VFileList::initActions() +{ + newFileAct = new QAction(tr("&New note"), this); + newFileAct->setStatusTip(tr("Create a new note in current directory")); + connect(newFileAct, &QAction::triggered, + this, &VFileList::newFile); + + deleteFileAct = new QAction(tr("&Delete"), this); + deleteFileAct->setStatusTip(tr("Delete selected note")); + connect(deleteFileAct, &QAction::triggered, + this, &VFileList::deleteFile); +} + +void VFileList::setDirectory(QJsonObject dirJson) +{ + Q_ASSERT(!dirJson.isEmpty()); + directoryName = dirJson["name"].toString(); + rootPath = dirJson["root_path"].toString(); + relativePath = QDir(dirJson["relative_path"].toString()).filePath(directoryName); + qDebug() << "FileList update:" << rootPath << relativePath << directoryName; + + updateFileList(); +} + + +void VFileList::updateFileList() +{ + clear(); + QString path = QDir(rootPath).filePath(relativePath); + + if (!QDir(path).exists()) { + qDebug() << "invalid notebook directory:" << path; + QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), tr("Invalid notebook directory.")); + msgBox.setInformativeText(QString("Notebook directory \"%1\" either does not exist or is not valid.") + .arg(path)); + msgBox.exec(); + return; + } + + QJsonObject configJson = VConfigManager::readDirectoryConfig(path); + if (configJson.isEmpty()) { + qDebug() << "invalid notebook configuration for directory:" << path; + QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), tr("Invalid notebook directory configuration.")); + msgBox.setInformativeText(QString("Notebook directory \"%1\" does not contain a valid configuration file.") + .arg(path)); + msgBox.exec(); + return; + } + + // Handle files section + QJsonArray filesJson = configJson["files"].toArray(); + for (int i = 0; i < filesJson.size(); ++i) { + QJsonObject fileItem = filesJson[i].toObject(); + insertFileListItem(fileItem); + } +} + +QListWidgetItem* VFileList::insertFileListItem(QJsonObject fileJson, bool atFront) +{ + Q_ASSERT(!fileJson.isEmpty()); + QListWidgetItem *item = new QListWidgetItem(fileJson["name"].toString()); + if (!fileJson["description"].toString().isEmpty()) { + item->setToolTip(fileJson["description"].toString()); + } + item->setData(Qt::UserRole, fileJson); + + if (atFront) { + insertItem(0, item); + } else { + addItem(item); + } + qDebug() << "add new list item:" << fileJson["name"].toString(); + return item; +} + +void VFileList::removeFileListItem(QListWidgetItem *item) +{ + // Qt ensures it will be removed from QListWidget automatically + delete item; +} + +void VFileList::newFile() +{ + QString text("&Note name:"); + QString defaultText("new_note"); + QString defaultDescription(""); + do { + VNewFileDialog dialog(QString("Create a new note under %1").arg(directoryName), text, + defaultText, tr("&Description:"), defaultDescription, this); + if (dialog.exec() == QDialog::Accepted) { + QString name = dialog.getNameInput(); + QString description = dialog.getDescriptionInput(); + if (isConflictNameWithExisting(name)) { + text = "Name already exists.\nPlease choose another name:"; + defaultText = name; + defaultDescription = description; + continue; + } + QListWidgetItem *newItem = createFileAndUpdateList(name, description); + if (newItem) { + this->setCurrentItem(newItem); + } + } + break; + } while (true); +} + +void VFileList::deleteFile() +{ + QListWidgetItem *curItem = currentItem(); + QJsonObject curItemJson = curItem->data(Qt::UserRole).toJsonObject(); + QString curItemName = curItemJson["name"].toString(); + + QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), + QString("Are you sure you want to delete note \"%1\"?") + .arg(curItemName)); + msgBox.setInformativeText(tr("This may be not recoverable.")); + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Ok); + if (msgBox.exec() == QMessageBox::Ok) { + deleteFileAndUpdateList(curItem); + } +} + +void VFileList::contextMenuRequested(QPoint pos) +{ + QListWidgetItem *item = itemAt(pos); + QMenu menu(this); + + if (directoryName.isEmpty()) { + return; + } + menu.addAction(newFileAct); + if (item) { + menu.addAction(deleteFileAct); + } + + menu.exec(mapToGlobal(pos)); +} + +bool VFileList::isConflictNameWithExisting(const QString &name) +{ + int nrChild = this->count(); + for (int i = 0; i < nrChild; ++i) { + QListWidgetItem *item = this->item(i); + QJsonObject itemJson = item->data(Qt::UserRole).toJsonObject(); + Q_ASSERT(!itemJson.isEmpty()); + if (itemJson["name"].toString() == name) { + return true; + } + } + return false; +} + +QListWidgetItem* VFileList::createFileAndUpdateList(const QString &name, + const QString &description) +{ + QString path = QDir(rootPath).filePath(relativePath); + QString filePath = QDir(path).filePath(name); + QFile file(filePath); + + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "error: fail to create file:" << filePath; + QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), QString("Could not create file \"%1\" under \"%2\".") + .arg(name).arg(path)); + msgBox.setInformativeText(QString("Please check if there already exists a file named \"%1\".").arg(name)); + msgBox.exec(); + return NULL; + } + file.close(); + qDebug() << "create file:" << filePath; + + // Update current directory's config file to include this new file + QJsonObject dirJson = VConfigManager::readDirectoryConfig(path); + Q_ASSERT(!dirJson.isEmpty()); + QJsonObject fileJson; + fileJson["name"] = name; + fileJson["description"] = description; + QJsonArray fileArray = dirJson["files"].toArray(); + fileArray.push_front(fileJson); + dirJson["files"] = fileArray; + if (!VConfigManager::writeDirectoryConfig(path, dirJson)) { + qWarning() << "error: fail to update directory's configuration file to add a new file" + << name; + file.remove(); + return NULL; + } + + return insertFileListItem(fileJson, true); +} + +void VFileList::deleteFileAndUpdateList(QListWidgetItem *item) +{ + Q_ASSERT(item); + QJsonObject itemJson = item->data(Qt::UserRole).toJsonObject(); + QString path = QDir(rootPath).filePath(relativePath); + QString fileName = itemJson["name"].toString(); + QString filePath = QDir(path).filePath(fileName); + + // Update current directory's config file to exclude this file + QJsonObject dirJson = VConfigManager::readDirectoryConfig(path); + Q_ASSERT(!dirJson.isEmpty()); + QJsonArray fileArray = dirJson["files"].toArray(); + bool deleted = false; + for (int i = 0; i < fileArray.size(); ++i) { + QJsonObject ele = fileArray[i].toObject(); + if (ele["name"].toString() == fileName) { + fileArray.removeAt(i); + deleted = true; + break; + } + } + if (!deleted) { + qWarning() << "error: fail to find" << fileName << "to delete"; + return; + } + dirJson["files"] = fileArray; + if (!VConfigManager::writeDirectoryConfig(path, dirJson)) { + qWarning() << "error: fail to update directory's configuration file to delete" + << fileName; + return; + } + + + // Delete the file + QFile file(filePath); + if (!file.remove()) { + qWarning() << "error: fail to delete" << filePath; + } else { + qDebug() << "delete" << filePath; + } + + removeFileListItem(item); +} + +void VFileList::currentFileItemChanged(QListWidgetItem *currentItem) +{ + QJsonObject itemJson = currentItem->data(Qt::UserRole).toJsonObject(); + Q_ASSERT(!itemJson.isEmpty()); + itemJson["path"] = QDir::cleanPath(QDir(rootPath).filePath(relativePath)); + qDebug() << "click file:" << itemJson; + emit currentFileChanged(itemJson); +} diff --git a/vfilelist.h b/vfilelist.h new file mode 100644 index 00000000..71091bdf --- /dev/null +++ b/vfilelist.h @@ -0,0 +1,46 @@ +#ifndef VFILELIST_H +#define VFILELIST_H + +#include +#include + +class QAction; + +class VFileList : public QListWidget +{ + Q_OBJECT +public: + explicit VFileList(QWidget *parent = 0); + +signals: + void currentFileChanged(QJsonObject fileJson); + +private slots: + void newFile(); + void deleteFile(); + void contextMenuRequested(QPoint pos); + void currentFileItemChanged(QListWidgetItem *currentItem); + +public slots: + void setDirectory(QJsonObject dirJson); + +private: + void updateFileList(); + QListWidgetItem *insertFileListItem(QJsonObject fileJson, bool atFront = false); + void removeFileListItem(QListWidgetItem *item); + void initActions(); + bool isConflictNameWithExisting(const QString &name); + QListWidgetItem *createFileAndUpdateList(const QString &name, + const QString &description); + void deleteFileAndUpdateList(QListWidgetItem *item); + + QString rootPath; + QString relativePath; + QString directoryName; + + // Actions + QAction *newFileAct; + QAction *deleteFileAct; +}; + +#endif // VFILELIST_H diff --git a/vmainwindow.cpp b/vmainwindow.cpp index 7a797a53..908c41e6 100644 --- a/vmainwindow.cpp +++ b/vmainwindow.cpp @@ -2,6 +2,7 @@ #include "vmainwindow.h" #include "vdirectorytree.h" #include "vnote.h" +#include "vfilelist.h" VMainWindow::VMainWindow(QWidget *parent) : QMainWindow(parent) @@ -40,8 +41,8 @@ void VMainWindow::setupUI() nbContainer->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding); // File list widget - fileListWidget = new QListWidget(); - fileListWidget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding); + fileList = new VFileList(); + fileList->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding); // Editor tab widget editorTabWidget = new QTabWidget(); @@ -60,7 +61,7 @@ void VMainWindow::setupUI() // Main Splitter mainSplitter = new QSplitter(); mainSplitter->addWidget(nbContainer); - mainSplitter->addWidget(fileListWidget); + mainSplitter->addWidget(fileList); mainSplitter->addWidget(editorTabWidget); mainSplitter->setStretchFactor(0, 1); mainSplitter->setStretchFactor(1, 1); @@ -71,6 +72,8 @@ void VMainWindow::setupUI() SLOT(setCurNotebookIndex(int))); connect(this, SIGNAL(curNotebookIndexChanged(const QString&)), directoryTree, SLOT(setTreePath(const QString&))); + connect(directoryTree, &VDirectoryTree::currentDirectoryChanged, + fileList, &VFileList::setDirectory); setCentralWidget(mainSplitter); // Create and show the status bar diff --git a/vmainwindow.h b/vmainwindow.h index 7babb2ba..68cfc8a7 100644 --- a/vmainwindow.h +++ b/vmainwindow.h @@ -10,6 +10,7 @@ class QSplitter; class QListWidget; class QTabWidget; class VNote; +class VFileList; class VMainWindow : public QMainWindow { @@ -34,7 +35,7 @@ private: QLabel *notebookLabel; QComboBox *notebookComboBox; VDirectoryTree *directoryTree; - QListWidget *fileListWidget; + VFileList *fileList; QTabWidget *editorTabWidget; QSplitter *mainSplitter; VNote *vnote; diff --git a/vnewfiledialog.cpp b/vnewfiledialog.cpp new file mode 100644 index 00000000..cb1131de --- /dev/null +++ b/vnewfiledialog.cpp @@ -0,0 +1,64 @@ +#include +#include "vnewfiledialog.h" + +VNewFileDialog::VNewFileDialog(const QString &title, const QString &name, const QString &defaultName, + const QString &description, const QString &defaultDescription, + QWidget *parent) + : QDialog(parent), title(title), name(name), defaultName(defaultName), + description(description), defaultDescription(defaultDescription) +{ + setupUI(); + + connect(nameEdit, &QLineEdit::textChanged, this, &VNewFileDialog::enableOkButton); + connect(okBtn, &QPushButton::clicked, this, &VNewFileDialog::accept); + connect(cancelBtn, &QPushButton::clicked, this, &VNewFileDialog::reject); +} + +void VNewFileDialog::setupUI() +{ + nameLabel = new QLabel(name); + nameEdit = new QLineEdit(defaultName); + nameEdit->selectAll(); + nameLabel->setBuddy(nameEdit); + + descriptionLabel = new QLabel(description); + descriptionEdit = new QLineEdit(defaultDescription); + descriptionLabel->setBuddy(descriptionEdit); + + okBtn = new QPushButton(tr("&OK")); + okBtn->setDefault(true); + cancelBtn = new QPushButton(tr("&Cancel")); + + QVBoxLayout *topLayout = new QVBoxLayout(); + topLayout->addWidget(nameLabel); + topLayout->addWidget(nameEdit); + topLayout->addWidget(descriptionLabel); + topLayout->addWidget(descriptionEdit); + + QHBoxLayout *btmLayout = new QHBoxLayout(); + btmLayout->addStretch(); + btmLayout->addWidget(okBtn); + btmLayout->addWidget(cancelBtn); + + QVBoxLayout *mainLayout = new QVBoxLayout(); + mainLayout->addLayout(topLayout); + mainLayout->addLayout(btmLayout); + setLayout(mainLayout); + + setWindowTitle(title); +} + +void VNewFileDialog::enableOkButton(const QString &editText) +{ + okBtn->setEnabled(!editText.isEmpty()); +} + +QString VNewFileDialog::getNameInput() const +{ + return nameEdit->text(); +} + +QString VNewFileDialog::getDescriptionInput() const +{ + return descriptionEdit->text(); +} diff --git a/vnewfiledialog.h b/vnewfiledialog.h new file mode 100644 index 00000000..393c21ec --- /dev/null +++ b/vnewfiledialog.h @@ -0,0 +1,40 @@ +#ifndef VNEWFILEDIALOG_H +#define VNEWFILEDIALOG_H + +#include + +class QLabel; +class QLineEdit; +class QPushButton; +class QString; + +class VNewFileDialog : public QDialog +{ + Q_OBJECT +public: + VNewFileDialog(const QString &title, const QString &name, const QString &defaultName, + const QString &description, const QString &defaultDescription, QWidget *parent = 0); + QString getNameInput() const; + QString getDescriptionInput() const; + +private slots: + void enableOkButton(const QString &editText); + +private: + void setupUI(); + + QLabel *nameLabel; + QLabel *descriptionLabel; + QLineEdit *nameEdit; + QLineEdit *descriptionEdit; + QPushButton *okBtn; + QPushButton *cancelBtn; + + QString title; + QString name; + QString defaultName; + QString description; + QString defaultDescription; +}; + +#endif // VNEWFILEDIALOG_H