Browse Source

implement VDirectoryTree logics

Signed-off-by: Le Tan <[email protected]>
Le Tan 9 years ago
parent
commit
09f6415536
8 changed files with 620 additions and 11 deletions
  1. 4 2
      VNote.pro
  2. 452 2
      vdirectorytree.cpp
  3. 47 1
      vdirectorytree.h
  4. 6 4
      vmainwindow.cpp
  5. 64 0
      vnewdirdialog.cpp
  6. 40 0
      vnewdirdialog.h
  7. 1 0
      vnote.cpp
  8. 6 2
      vnote.h

+ 4 - 2
VNote.pro

@@ -16,12 +16,14 @@ SOURCES += main.cpp\
         vmainwindow.cpp \
     vdirectorytree.cpp \
     vnote.cpp \
-    vnotebook.cpp
+    vnotebook.cpp \
+    vnewdirdialog.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
     vnote.h \
-    vnotebook.h
+    vnotebook.h \
+    vnewdirdialog.h
 
 RESOURCES += \
     vnote.qrc

+ 452 - 2
vdirectorytree.cpp

@@ -1,12 +1,462 @@
-#include <QtGui>
+#include <QtWidgets>
+#include <QJsonObject>
 #include "vdirectorytree.h"
+#include "vnewdirdialog.h"
 
-VDirectoryTree::VDirectoryTree(QWidget *parent) : QTreeWidget(parent)
+VDirectoryTree::VDirectoryTree(const QString &dirConfigFileName, QWidget *parent)
+    : QTreeWidget(parent), dirConfigFileName(dirConfigFileName)
 {
+    setColumnCount(1);
+    setHeaderHidden(true);
+    setContextMenuPolicy(Qt::CustomContextMenu);
+    initialActions();
+
+    connect(this, SIGNAL(itemExpanded(QTreeWidgetItem*)),
+            this, SLOT(updateItemSubtree(QTreeWidgetItem*)));
+    connect(this, SIGNAL(customContextMenuRequested(QPoint)),
+            this, SLOT(contextMenuRequested(QPoint)));
+}
+
+void VDirectoryTree::initialActions()
+{
+    newRootDirAct = new QAction(tr("New &root directory"), this);
+    newRootDirAct->setStatusTip(tr("Create a new root directory in current notebook"));
+    connect(newRootDirAct, &QAction::triggered,
+            this, &VDirectoryTree::newRootDirectory);
+
+    newSiblingDirAct = new QAction(tr("New &sibling directory"), this);
+    newSiblingDirAct->setStatusTip(tr("Create a new sibling directory at current level"));
+    connect(newSiblingDirAct, &QAction::triggered,
+            this, &VDirectoryTree::newSiblingDirectory);
+
+    newSubDirAct = new QAction(tr("&New sub-directory"), this);
+    newSubDirAct->setStatusTip(tr("Create a new sub-directory"));
+    connect(newSubDirAct, &QAction::triggered,
+            this, &VDirectoryTree::newSubDirectory);
 }
 
 void VDirectoryTree::setTreePath(const QString& path)
 {
+    if (path == treePath) {
+        return;
+    }
+
     treePath = path;
     qDebug() << "set directory tree path:" << path;
+
+    updateDirectoryTree();
+}
+
+bool VDirectoryTree::validatePath(const QString &path)
+{
+    QDir dir(path);
+    if (!dir.exists()) {
+        return false;
+    }
+
+    QString configFile = dir.filePath(dirConfigFileName);
+    QFileInfo fileInfo(configFile);
+    return fileInfo.exists() && fileInfo.isFile();
+}
+
+void VDirectoryTree::updateDirectoryTree()
+{
+    updateDirectoryTreeTopLevel();
+
+    int nrTopLevelItems = topLevelItemCount();
+    for (int i = 0; i < nrTopLevelItems; ++i) {
+        QTreeWidgetItem *item = topLevelItem(i);
+        Q_ASSERT(item);
+        updateDirectoryTreeOne(*item, 1);
+    }
+}
+
+// QJsonObject stored in each item's data[UserRole]:
+// 1. @item's related item in its parent's [sub_directories] section;
+// 2. "relative_path": the path where this item exists, relative to the treePath.
+void VDirectoryTree::fillDirectoryTreeItem(QTreeWidgetItem &item, QJsonObject itemJson, const QString &relativePath)
+{
+    item.setText(0, itemJson["name"].toString());
+    QString description = itemJson["description"].toString();
+    if (!description.isEmpty()) {
+        item.setToolTip(0, description);
+    }
+    itemJson["relative_path"] = relativePath;
+    item.setData(0, Qt::UserRole, itemJson);
+}
+
+QTreeWidgetItem* VDirectoryTree::insertDirectoryTreeItem(QTreeWidgetItem *parent, QTreeWidgetItem *preceding,
+                                                         const QJsonObject &newItem)
+{
+    QTreeWidgetItem *item;
+    QString relativePath;
+    if (parent) {
+        if (preceding) {
+            item = new QTreeWidgetItem(parent, preceding);
+        } else {
+            item = new QTreeWidgetItem(parent);
+        }
+        QJsonObject parentJson = parent->data(0, Qt::UserRole).toJsonObject();
+        Q_ASSERT(!parentJson.isEmpty());
+        QString parentRelativePath = parentJson["relative_path"].toString();
+        QString parentName = parentJson["name"].toString();
+        relativePath = QDir(parentRelativePath).filePath(parentName);
+    } else {
+        if (preceding) {
+            item = new QTreeWidgetItem(this, preceding);
+        } else {
+            item = new QTreeWidgetItem(this);
+        }
+        relativePath = "";
+    }
+
+    fillDirectoryTreeItem(*item, newItem, relativePath);
+    qDebug() << "insert new Item name:" << newItem["name"].toString()
+             << "relative_path:" << relativePath;
+    return item;
+}
+
+void VDirectoryTree::updateDirectoryTreeTopLevel()
+{
+    const QString &path = treePath;
+
+    clear();
+
+    if (!validatePath(path)) {
+        qDebug() << "invalid notebook path:" << path;
+        QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), tr("Invalid notebook path."));
+        msgBox.setInformativeText(QString("Notebook path \"%1\" either does not exist or is not valid.")
+                                  .arg(path));
+        msgBox.exec();
+        return;
+    }
+
+    QJsonObject configJson = readDirectoryConfig(path);
+    if (!validateDirConfigFile(configJson)) {
+        qDebug() << "invalid notebook configuration for path:" << path;
+        QMessageBox msgBox(QMessageBox::Warning, tr("Warning"), tr("Invalid notebook configuration."));
+        msgBox.setInformativeText(QString("Notebook path \"%1\" does not contain a valid configuration file.")
+                                  .arg(path));
+        msgBox.exec();
+        return;
+    }
+
+    // Handle sub_directories section
+    QJsonArray dirJson = configJson["sub_directories"].toArray();
+    QTreeWidgetItem *preItem = NULL;
+    for (int i = 0; i < dirJson.size(); ++i) {
+        QJsonObject dirItem = dirJson[i].toObject();
+        QTreeWidgetItem *treeItem = insertDirectoryTreeItem(NULL, preItem, dirItem);
+        preItem = treeItem;
+    }
+
+    qDebug() << "updated" << dirJson.size() << "top-level items";
+}
+
+void VDirectoryTree::updateDirectoryTreeOne(QTreeWidgetItem &parent, int depth)
+{
+    Q_ASSERT(parent.childCount() == 0);
+    // Going deep enough
+    if (depth <= 0) {
+        return;
+    }
+    QJsonObject parentJson = parent.data(0, Qt::UserRole).toJsonObject();
+    QString relativePath = QDir(parentJson["relative_path"].toString()).filePath(parentJson["name"].toString());
+    QString path(QDir::cleanPath(treePath + QDir::separator() + relativePath));
+    if (!validatePath(path)) {
+        qDebug() << "invalide 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 a valid notebook directory.")
+                                  .arg(path));
+        msgBox.exec();
+        return;
+    }
+
+    QJsonObject configJson = readDirectoryConfig(path);
+    if (!validateDirConfigFile(configJson)) {
+        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.")
+                                  .arg(path));
+        msgBox.exec();
+        return;
+    }
+
+    // Handle sub_directories section
+    QJsonArray dirJson = configJson["sub_directories"].toArray();
+    QTreeWidgetItem *preItem = NULL;
+    for (int i = 0; i < dirJson.size(); ++i) {
+        QJsonObject dirItem = dirJson[i].toObject();
+        QTreeWidgetItem *treeItem = insertDirectoryTreeItem(&parent, preItem, dirItem);
+        preItem = treeItem;
+
+        // Update its sub-directory recursively
+        updateDirectoryTreeOne(*treeItem, depth - 1);
+    }
+}
+
+QJsonObject VDirectoryTree::readDirectoryConfig(const QString &path)
+{
+    QString configFile = QDir(path).filePath(dirConfigFileName);
+
+    qDebug() << "read config file:" << configFile;
+    QFile config(configFile);
+    if (!config.open(QIODevice::ReadOnly)) {
+        qWarning() << "error: fail to read directory configuration file:"
+                   << configFile;
+        QMessageBox msgBox(QMessageBox::Warning, tr("Warning"),
+                           QString("Could not read directory configuration file \"%1\"")
+                           .arg(dirConfigFileName));
+        msgBox.setInformativeText(QString("Notebook directory \"%1\" may be corrupted").arg(path));
+        msgBox.exec();
+        return QJsonObject();
+    }
+
+    QByteArray configData = config.readAll();
+    return QJsonDocument::fromJson(configData).object();
+}
+
+bool VDirectoryTree::writeDirectoryConfig(const QString &path, const QJsonObject &configJson)
+{
+    QString configFile = QDir(path).filePath(dirConfigFileName);
+
+    qDebug() << "write config file:" << configFile;
+    QFile config(configFile);
+    if (!config.open(QIODevice::WriteOnly)) {
+        qWarning() << "error: fail to open directory configuration file for write:"
+                   << configFile;
+        QMessageBox msgBox(QMessageBox::Warning, tr("Warning"),
+                           QString("Could not write directory configuration file \"%1\"")
+                           .arg(dirConfigFileName));
+        msgBox.exec();
+        return false;
+    }
+
+    QJsonDocument configDoc(configJson);
+    config.write(configDoc.toJson());
+    return true;
+}
+
+bool VDirectoryTree::deleteDirectoryConfig(const QString &path)
+{
+    QString configFile = QDir(path).filePath(dirConfigFileName);
+
+    QFile config(configFile);
+    if (!config.remove()) {
+        qWarning() << "error: fail to delete directory configuration file:"
+                   << configFile;
+        return false;
+    }
+    qDebug() << "delete config file:" << configFile;
+    return true;
+}
+
+bool VDirectoryTree::validateDirConfigFile(const QJsonObject &configJson)
+{
+    if (configJson.isEmpty()) {
+        return false;
+    }
+    if (!configJson.contains("version") || !configJson.contains("name")) {
+        return false;
+    }
+    return true;
+}
+
+void VDirectoryTree::updateItemSubtree(QTreeWidgetItem *item)
+{
+    QJsonObject itemJson = item->data(0, Qt::UserRole).toJsonObject();
+    Q_ASSERT(!itemJson.isEmpty());
+    int nrChild = item->childCount();
+    if (nrChild == 0) {
+        updateDirectoryTreeOne(*item, 2);
+    } else {
+        for (int i = 0; i < nrChild; ++i) {
+            QTreeWidgetItem *childItem = item->child(i);
+            if (childItem->childCount() > 0) {
+                continue;
+            }
+            updateDirectoryTreeOne(*childItem, 1);
+        }
+    }
+}
+
+void VDirectoryTree::contextMenuRequested(QPoint pos)
+{
+    QTreeWidgetItem *item = itemAt(pos);
+    QMenu menu(this);
+
+    if (!item) {
+        // Context menu on the free space of the QTreeWidget
+        menu.addAction(newRootDirAct);
+    } else {
+        // Context menu on a QTreeWidgetItem
+        if (item->parent()) {
+            // Low-level item
+            menu.addAction(newSubDirAct);
+            menu.addAction(newSiblingDirAct);
+        } else {
+            // Top-level item
+            menu.addAction(newRootDirAct);
+            menu.addAction(newSubDirAct);
+        }
+    }
+    menu.exec(mapToGlobal(pos));
+}
+
+void VDirectoryTree::newSiblingDirectory()
+{
+    QTreeWidgetItem *parentItem = currentItem()->parent();
+    Q_ASSERT(parentItem);
+    QJsonObject parentItemJson = parentItem->data(0, Qt::UserRole).toJsonObject();
+    QString parentItemName = parentItemJson["name"].toString();
+
+    QString text("&Directory name:");
+    QString defaultText("new_directory");
+    QString defaultDescription("");
+    do {
+        VNewDirDialog dialog(QString("Create a new directory under %1").arg(parentItemName), text,
+                             defaultText, tr("&Description:"), defaultDescription, this);
+        if (dialog.exec() == QDialog::Accepted) {
+            QString name = dialog.getNameInput();
+            QString description = dialog.getDescriptionInput();
+            if (isConflictNameWithChildren(parentItem, name)) {
+                text = "Name already exists.\nPlease choose another name:";
+                defaultText = name;
+                defaultDescription = description;
+                continue;
+            }
+            QTreeWidgetItem *newItem = createDirectoryAndUpdateTree(parentItem, name, description);
+            if (newItem) {
+                this->setCurrentItem(newItem);
+            }
+        }
+        break;
+    } while (true);
+}
+
+void VDirectoryTree::newSubDirectory()
+{
+    QTreeWidgetItem *curItem = currentItem();
+    QJsonObject curItemJson = curItem->data(0, Qt::UserRole).toJsonObject();
+    QString curItemName = curItemJson["name"].toString();
+
+    QString text("&Directory name:");
+    QString defaultText("new_directory");
+    QString defaultDescription("");
+    do {
+        VNewDirDialog dialog(QString("Create a new directory under %1").arg(curItemName), text,
+                             defaultText, tr("&Description:"), defaultDescription, this);
+        if (dialog.exec() == QDialog::Accepted) {
+            QString name = dialog.getNameInput();
+            QString description = dialog.getDescriptionInput();
+            if (isConflictNameWithChildren(curItem, name)) {
+                text = "Name already exists.\nPlease choose another name:";
+                defaultText = name;
+                defaultDescription = description;
+                continue;
+            }
+            QTreeWidgetItem *newItem = createDirectoryAndUpdateTree(curItem, name, description);
+            if (newItem) {
+                this->setCurrentItem(newItem);
+            }
+        }
+        break;
+    } while (true);
+}
+
+void VDirectoryTree::newRootDirectory()
+{
+    QString text("&Directory name:");
+    QString defaultText("new_directory");
+    QString defaultDescription("");
+    do {
+        VNewDirDialog dialog(tr("Create a new root directory"), text,
+                             defaultText, tr("&Description:"), defaultDescription, this);
+        if (dialog.exec() == QDialog::Accepted) {
+            QString name = dialog.getNameInput();
+            QString description = dialog.getDescriptionInput();
+            if (isConflictNameWithChildren(NULL, name)) {
+                text = "Name already exists.\nPlease choose another name:";
+                defaultText = name;
+                defaultDescription = description;
+                continue;
+            }
+            QTreeWidgetItem *newItem = createDirectoryAndUpdateTree(NULL, name, description);
+            if (newItem) {
+                this->setCurrentItem(newItem);
+            }
+        }
+        break;
+    } while (true);
+}
+
+QTreeWidgetItem* VDirectoryTree::createDirectoryAndUpdateTree(QTreeWidgetItem *parent,
+                                                              const QString &name, const QString &description)
+{
+    QString relativePath("");
+    QJsonObject parentJson;
+    if (parent) {
+        parentJson = parent->data(0, Qt::UserRole).toJsonObject();
+        relativePath = QDir(parentJson["relative_path"].toString()).filePath(parentJson["name"].toString());
+    }
+    QString path = QDir(treePath).filePath(relativePath);
+    QDir dir(path);
+    if (!dir.mkdir(name)) {
+        qDebug() << "warning: 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));
+        msgBox.exec();
+        return NULL;
+    }
+
+    QJsonObject configJson;
+    configJson["version"] = "1";
+    configJson["name"] = name;
+    configJson["sub_directories"] = QJsonArray();
+    configJson["files"] = QJsonArray();
+
+    if (!writeDirectoryConfig(QDir(path).filePath(name), configJson)) {
+        return NULL;
+    }
+
+    // Update parent's config file to include this new directory
+    configJson = readDirectoryConfig(path);
+    QJsonObject itemJson;
+    itemJson["name"] = name;
+    itemJson["description"] = description;
+    QJsonArray subDirArray = configJson["sub_directories"].toArray();
+    subDirArray.append(itemJson);
+    configJson["sub_directories"] = subDirArray;
+    if (!writeDirectoryConfig(path, configJson)) {
+        deleteDirectoryConfig(QDir(path).filePath(name));
+        dir.rmdir(name);
+        return NULL;
+    }
+
+    return insertDirectoryTreeItem(parent, NULL, itemJson);
+}
+
+bool VDirectoryTree::isConflictNameWithChildren(const QTreeWidgetItem *parent, const QString &name)
+{
+    if (parent) {
+        int nrChild = parent->childCount();
+        for (int i = 0; i < nrChild; ++i) {
+            QJsonObject childItemJson = parent->child(i)->data(0, Qt::UserRole).toJsonObject();
+            Q_ASSERT(!childItemJson.isEmpty());
+            if (childItemJson["name"].toString() == name) {
+                return true;
+            }
+        }
+    } else {
+        int nrTopLevelItems = topLevelItemCount();
+        for (int i = 0; i < nrTopLevelItems; ++i) {
+            QJsonObject itemJson = topLevelItem(i)->data(0, Qt::UserRole).toJsonObject();
+            Q_ASSERT(!itemJson.isEmpty());
+            if (itemJson["name"].toString() == name) {
+                return true;
+            }
+        }
+    }
+    return false;
 }

+ 47 - 1
vdirectorytree.h

@@ -3,20 +3,66 @@
 
 #include <QTreeWidget>
 
+class QJsonObject;
+
 class VDirectoryTree : public QTreeWidget
 {
     Q_OBJECT
 public:
-    explicit VDirectoryTree(QWidget *parent = 0);
+    VDirectoryTree(const QString &dirConfigFileName, QWidget *parent = 0);
 
 signals:
 
 public slots:
     void setTreePath(const QString& path);
 
+private slots:
+    // Read config file and pdate the subtree of @item in the directory tree.
+    // If @item has no child, we will call updateDirectoryTreeOne() to update it.
+    // Otherwise, we will loop all its direct-children and try to populate it if
+    // it has not been populated yet.
+    void updateItemSubtree(QTreeWidgetItem *item);
+    void contextMenuRequested(QPoint pos);
+    void newSiblingDirectory();
+    void newSubDirectory();
+    void newRootDirectory();
+
 private:
+    // Clean and pdate the TreeWidget according to treePath
+    void updateDirectoryTree();
+    // Update the top-level items of the directory tree. Will not clean the tree at first.
+    void updateDirectoryTreeTopLevel();
+    // Update one directory, going into @depth levels. Not cleaning the tree item at first,
+    // so you must ensure @parent has no child before calling this function.
+    void updateDirectoryTreeOne(QTreeWidgetItem &parent, int depth);
+    // Validate if a directory is valid
+    bool validatePath(const QString &path);
+    // Validate if a directory config file is valid
+    bool validateDirConfigFile(const QJsonObject &configJson);
+    // 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();
+    QTreeWidgetItem* createDirectoryAndUpdateTree(QTreeWidgetItem *parent, const QString &name,
+                                                  const QString &description);
+    // If @name conflict with the children's names of @parent.
+    bool isConflictNameWithChildren(const QTreeWidgetItem *parent, const QString &name);
+    // Read config from the directory config json file into a QJsonObject
+    QJsonObject readDirectoryConfig(const QString &path);
+    bool writeDirectoryConfig(const QString &path, const QJsonObject &configJson);
+    bool deleteDirectoryConfig(const QString &path);
+    QTreeWidgetItem* insertDirectoryTreeItem(QTreeWidgetItem *parent, QTreeWidgetItem *preceding,
+                                             const QJsonObject &newItem);
+
     // The path of the directory tree root
     QString treePath;
+    // The name of the config file in each subdirectory
+    QString dirConfigFileName;
+
+    // Actions
+    QAction *newRootDirAct;
+    QAction *newSiblingDirAct;
+    QAction *newSubDirAct;
 };
 
 #endif // VDIRECTORYTREE_H

+ 6 - 4
vmainwindow.cpp

@@ -1,4 +1,4 @@
-#include <QtGui>
+#include <QtWidgets>
 #include "vmainwindow.h"
 #include "vdirectorytree.h"
 #include "vnote.h"
@@ -23,7 +23,7 @@ void VMainWindow::setupUI()
     // Notebook directory browser tree
     notebookLabel = new QLabel(tr("Notebook"));
     notebookComboBox = new QComboBox();
-    directoryTree = new VDirectoryTree();
+    directoryTree = new VDirectoryTree(VNote::dirConfigFileName);
 
     QHBoxLayout *nbTopLayout = new QHBoxLayout;
     notebookComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
@@ -46,6 +46,7 @@ void VMainWindow::setupUI()
     // Editor tab widget
     editorTabWidget = new QTabWidget();
     editorTabWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+    editorTabWidget->setTabBarAutoHide(true);
     QFile welcomeFile(":/resources/welcome.html");
     QString welcomeText("Welcome to VNote!");
     if (welcomeFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
@@ -72,6 +73,8 @@ void VMainWindow::setupUI()
             SLOT(setTreePath(const QString&)));
 
     setCentralWidget(mainSplitter);
+    // Create and show the status bar
+    statusBar();
 }
 
 void VMainWindow::updateNotebookComboBox()
@@ -83,10 +86,9 @@ void VMainWindow::updateNotebookComboBox()
         notebookComboBox->addItem(notebooks[i].getName());
     }
 
-    notebookComboBox->setCurrentIndex(vnote->getCurNotebookIndex());
-
     qDebug() << "update notebook combobox with" << notebookComboBox->count()
              << "items";
+    notebookComboBox->setCurrentIndex(vnote->getCurNotebookIndex());
 }
 
 void VMainWindow::setCurNotebookIndex(int index)

+ 64 - 0
vnewdirdialog.cpp

@@ -0,0 +1,64 @@
+#include <QtWidgets>
+#include "vnewdirdialog.h"
+
+VNewDirDialog::VNewDirDialog(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, &VNewDirDialog::enableOkButton);
+    connect(okBtn, &QPushButton::clicked, this, &VNewDirDialog::accept);
+    connect(cancelBtn, &QPushButton::clicked, this, &VNewDirDialog::reject);
+}
+
+void VNewDirDialog::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 VNewDirDialog::enableOkButton(const QString &editText)
+{
+    okBtn->setEnabled(!editText.isEmpty());
+}
+
+QString VNewDirDialog::getNameInput() const
+{
+    return nameEdit->text();
+}
+
+QString VNewDirDialog::getDescriptionInput() const
+{
+    return descriptionEdit->text();
+}

+ 40 - 0
vnewdirdialog.h

@@ -0,0 +1,40 @@
+#ifndef VNEWDIRDIALOG_H
+#define VNEWDIRDIALOG_H
+
+#include <QDialog>
+
+class QLabel;
+class QLineEdit;
+class QPushButton;
+class QString;
+
+class VNewDirDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    VNewDirDialog(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 // VNEWDIRDIALOG_H

+ 1 - 0
vnote.cpp

@@ -4,6 +4,7 @@
 
 const QString VNote::orgName = QString("tamlok");
 const QString VNote::appName = QString("VNote");
+const QString VNote::dirConfigFileName = QString(".vnote.json");
 
 VNote::VNote()
     : curNotebookIndex(0)

+ 6 - 2
vnote.h

@@ -16,6 +16,12 @@ public:
     const QVector<VNotebook>& getNotebooks();
     int getCurNotebookIndex() const;
     void setCurNotebookIndex(int index);
+
+    // The name of the config file in each subdirectory
+    static const QString dirConfigFileName;
+    static const QString orgName;
+    static const QString appName;
+
 private:
     // Write notebooks section of global config
     void writeGlobalConfigNotebooks(QSettings &settings);
@@ -24,8 +30,6 @@ private:
 
     QVector<VNotebook> notebooks;
     int curNotebookIndex;
-    static const QString orgName;
-    static const QString appName;
 };
 
 #endif // VNOTE_H