Browse Source

support meta word

- Add VLineEdit as a QLineEdit with meta data support;
- support custom magic words through [magic_words];
- add %help% for all magic words information;
Le Tan 8 years ago
parent
commit
787c61a5af

+ 31 - 11
src/dialog/vdirinfodialog.cpp

@@ -2,6 +2,7 @@
 #include "vdirinfodialog.h"
 #include "vdirectory.h"
 #include "vconfigmanager.h"
+#include "vlineedit.h"
 #include "utils/vutils.h"
 
 extern VConfigManager *g_config;
@@ -16,7 +17,7 @@ VDirInfoDialog::VDirInfoDialog(const QString &title,
 {
     setupUI();
 
-    connect(nameEdit, &QLineEdit::textChanged, this, &VDirInfoDialog::handleInputChanged);
+    connect(m_nameEdit, &QLineEdit::textChanged, this, &VDirInfoDialog::handleInputChanged);
 
     handleInputChanged();
 }
@@ -28,15 +29,18 @@ void VDirInfoDialog::setupUI()
         infoLabel = new QLabel(info);
     }
 
-    nameEdit = new QLineEdit(m_directory->getName());
-    nameEdit->selectAll();
+    m_nameEdit = new VLineEdit(m_directory->getName());
+    QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
+                                                 m_nameEdit);
+    m_nameEdit->setValidator(validator);
+    m_nameEdit->selectAll();
 
     // Created time.
     QString createdTimeStr = VUtils::displayDateTime(m_directory->getCreatedTimeUtc().toLocalTime());
     QLabel *createdTimeLabel = new QLabel(createdTimeStr);
 
     QFormLayout *topLayout = new QFormLayout();
-    topLayout->addRow(tr("Folder &name:"), nameEdit);
+    topLayout->addRow(tr("Folder &name:"), m_nameEdit);
     topLayout->addRow(tr("Created time:"), createdTimeLabel);
 
     m_warnLabel = new QLabel();
@@ -49,7 +53,7 @@ void VDirInfoDialog::setupUI()
     connect(m_btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
 
     QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok);
-    nameEdit->setMinimumWidth(okBtn->sizeHint().width() * 3);
+    m_nameEdit->setMinimumWidth(okBtn->sizeHint().width() * 3);
 
     QVBoxLayout *mainLayout = new QVBoxLayout();
     if (infoLabel) {
@@ -67,19 +71,35 @@ void VDirInfoDialog::setupUI()
 void VDirInfoDialog::handleInputChanged()
 {
     bool showWarnLabel = false;
-    QString name = nameEdit->text();
+    QString name = m_nameEdit->getEvaluatedText();
     bool nameOk = !name.isEmpty();
     if (nameOk && name != m_directory->getName()) {
         // Check if the name conflicts with existing directory name.
         // Case-insensitive when creating note.
         const VDirectory *directory = m_parentDirectory->findSubDirectory(name, false);
+        QString warnText;
         if (directory && directory != m_directory) {
             nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name (case-insensitive) <span style=\"%2\">%3</span> already exists. "
+                          "Please choose another name.")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        } else if (!VUtils::checkFileNameLegal(name)) {
+            // Check if evaluated name contains illegal characters.
+            nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name <span style=\"%2\">%3</span> contains illegal characters "
+                          "(after magic word evaluation).")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        }
+
+        if (!nameOk) {
             showWarnLabel = true;
-            QString nameConflictText = tr("<span style=\"%1\">WARNING</span>: Name (case-insensitive) already exists. "
-                                          "Please choose another name.")
-                                          .arg(g_config->c_warningTextStyle);
-            m_warnLabel->setText(nameConflictText);
+            m_warnLabel->setText(warnText);
         }
     }
 
@@ -91,5 +111,5 @@ void VDirInfoDialog::handleInputChanged()
 
 QString VDirInfoDialog::getNameInput() const
 {
-    return nameEdit->text();
+    return m_nameEdit->getEvaluatedText();
 }

+ 2 - 2
src/dialog/vdirinfodialog.h

@@ -4,7 +4,7 @@
 #include <QDialog>
 
 class QLabel;
-class QLineEdit;
+class VLineEdit;
 class QDialogButtonBox;
 class QString;
 class VDirectory;
@@ -27,7 +27,7 @@ private slots:
 private:
     void setupUI();
 
-    QLineEdit *nameEdit;
+    VLineEdit *m_nameEdit;
     QLabel *m_warnLabel;
     QDialogButtonBox *m_btnBox;
 

+ 35 - 16
src/dialog/vfileinfodialog.cpp

@@ -4,6 +4,7 @@
 #include "vnotefile.h"
 #include "vconfigmanager.h"
 #include "utils/vutils.h"
+#include "vlineedit.h"
 
 extern VConfigManager *g_config;
 
@@ -16,7 +17,7 @@ VFileInfoDialog::VFileInfoDialog(const QString &title,
 {
     setupUI(title, info);
 
-    connect(nameEdit, &QLineEdit::textChanged, this, &VFileInfoDialog::handleInputChanged);
+    connect(m_nameEdit, &QLineEdit::textChanged, this, &VFileInfoDialog::handleInputChanged);
 
     handleInputChanged();
 }
@@ -30,7 +31,10 @@ void VFileInfoDialog::setupUI(const QString &p_title, const QString &p_info)
 
     // File name.
     QString name = m_file->getName();
-    nameEdit = new QLineEdit(name);
+    m_nameEdit = new VLineEdit(name);
+    QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
+                                                 m_nameEdit);
+    m_nameEdit->setValidator(validator);
     int baseStart = 0, baseLength = name.size();
     int dotIdx = name.lastIndexOf('.');
     if (dotIdx != -1) {
@@ -38,7 +42,7 @@ void VFileInfoDialog::setupUI(const QString &p_title, const QString &p_info)
     }
 
     // Select without suffix.
-    nameEdit->setSelection(baseStart, baseLength);
+    m_nameEdit->setSelection(baseStart, baseLength);
 
     // Attachment folder.
     QLineEdit *attachmentFolderEdit = new QLineEdit(m_file->getAttachmentFolder());
@@ -56,7 +60,7 @@ void VFileInfoDialog::setupUI(const QString &p_title, const QString &p_info)
     modifiedTimeLabel->setToolTip(tr("Last modified time within VNote"));
 
     QFormLayout *topLayout = new QFormLayout();
-    topLayout->addRow(tr("Note &name:"), nameEdit);
+    topLayout->addRow(tr("Note &name:"), m_nameEdit);
     topLayout->addRow(tr("Attachment folder:"), attachmentFolderEdit);
     topLayout->addRow(tr("Created time:"), createdTimeLabel);
     topLayout->addRow(tr("Modified time:"), modifiedTimeLabel);
@@ -71,7 +75,7 @@ void VFileInfoDialog::setupUI(const QString &p_title, const QString &p_info)
     connect(m_btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
 
     QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok);
-    nameEdit->setMinimumWidth(okBtn->sizeHint().width() * 3);
+    m_nameEdit->setMinimumWidth(okBtn->sizeHint().width() * 3);
 
     QVBoxLayout *mainLayout = new QVBoxLayout();
     if (infoLabel) {
@@ -90,28 +94,43 @@ void VFileInfoDialog::setupUI(const QString &p_title, const QString &p_info)
 void VFileInfoDialog::handleInputChanged()
 {
     bool showWarnLabel = false;
-    QString name = nameEdit->text();
+    QString name = m_nameEdit->getEvaluatedText();
     bool nameOk = !name.isEmpty();
     if (nameOk && name != m_file->getName()) {
         // Check if the name conflicts with existing note name.
         // Case-insensitive when creating note.
         const VNoteFile *file = m_directory->findFile(name, false);
+        QString warnText;
         if (file && file != m_file) {
             nameOk = false;
-            showWarnLabel = true;
-            QString nameConflictText = tr("<span style=\"%1\">WARNING</span>: Name (case-insensitive) already exists. "
-                                          "Please choose another name.")
-                                          .arg(g_config->c_warningTextStyle);
-            m_warnLabel->setText(nameConflictText);
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name (case-insensitive) <span style=\"%2\">%3</span> already exists. "
+                          "Please choose another name.")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
         } else if (m_file->getDocType() != DocType::Unknown
                    && VUtils::docTypeFromName(name) != m_file->getDocType()) {
             // Check if the name change the doc type.
             nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Changing type of the note is not supported. "
+                          "Please use the same suffix as the old one.")
+                         .arg(g_config->c_warningTextStyle);
+        } else if (!VUtils::checkFileNameLegal(name)) {
+            // Check if evaluated name contains illegal characters.
+            nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name <span style=\"%2\">%3</span> contains illegal characters "
+                          "(after magic word evaluation).")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        }
+
+        if (!nameOk) {
             showWarnLabel = true;
-            QString nameConflictText = tr("<span style=\"%1\">WARNING</span>: Changing type of the note is not supported. "
-                                          "Please use the same suffix as the old one.")
-                                          .arg(g_config->c_warningTextStyle);
-            m_warnLabel->setText(nameConflictText);
+            m_warnLabel->setText(warnText);
         }
     }
 
@@ -123,5 +142,5 @@ void VFileInfoDialog::handleInputChanged()
 
 QString VFileInfoDialog::getNameInput() const
 {
-    return nameEdit->text();
+    return m_nameEdit->getEvaluatedText();
 }

+ 2 - 2
src/dialog/vfileinfodialog.h

@@ -4,7 +4,7 @@
 #include <QDialog>
 
 class QLabel;
-class QLineEdit;
+class VLineEdit;
 class QDialogButtonBox;
 class QString;
 class VDirectory;
@@ -29,7 +29,7 @@ private slots:
 private:
     void setupUI(const QString &p_title, const QString &p_info);
 
-    QLineEdit *nameEdit;
+    VLineEdit *m_nameEdit;
     QLabel *m_warnLabel;
     QDialogButtonBox *m_btnBox;
 

+ 23 - 16
src/dialog/vinsertimagedialog.cpp

@@ -3,6 +3,7 @@
 #include <QRegExp>
 #include "vinsertimagedialog.h"
 #include "utils/vutils.h"
+#include "vlineedit.h"
 
 VInsertImageDialog::VInsertImageDialog(const QString &title, const QString &defaultImageTitle,
                                        const QString &defaultPath, QWidget *parent)
@@ -11,9 +12,12 @@ VInsertImageDialog::VInsertImageDialog(const QString &title, const QString &defa
 {
     setupUI();
 
-    connect(imageTitleEdit, &QLineEdit::textChanged, this, &VInsertImageDialog::enableOkButton);
-    connect(pathEdit, &QLineEdit::textChanged, this, &VInsertImageDialog::enableOkButton);
-    connect(browseBtn, &QPushButton::clicked, this, &VInsertImageDialog::handleBrowseBtnClicked);
+    connect(m_imageTitleEdit, &QLineEdit::textChanged,
+            this, &VInsertImageDialog::enableOkButton);
+    connect(pathEdit, &QLineEdit::textChanged,
+            this, &VInsertImageDialog::enableOkButton);
+    connect(browseBtn, &QPushButton::clicked,
+            this, &VInsertImageDialog::handleBrowseBtnClicked);
 
     enableOkButton();
 }
@@ -34,19 +38,19 @@ void VInsertImageDialog::setupUI()
     browseBtn = new QPushButton(tr("&Browse"));
 
     imageTitleLabel = new QLabel(tr("&Image title:"));
-    imageTitleEdit = new QLineEdit(defaultImageTitle);
-    imageTitleEdit->selectAll();
-    imageTitleLabel->setBuddy(imageTitleEdit);
-    QRegExp regExp("[\\w\\(\\)@#%\\*\\-\\+=\\?<>\\,\\.\\s]+");
-    QValidator *validator = new QRegExpValidator(regExp, this);
-    imageTitleEdit->setValidator(validator);
+    m_imageTitleEdit = new VLineEdit(defaultImageTitle);
+    m_imageTitleEdit->selectAll();
+    imageTitleLabel->setBuddy(m_imageTitleEdit);
+    QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_imageTitleRegExp),
+                                                 m_imageTitleEdit);
+    m_imageTitleEdit->setValidator(validator);
 
     QGridLayout *topLayout = new QGridLayout();
     topLayout->addWidget(pathLabel, 0, 0);
     topLayout->addWidget(pathEdit, 0, 1);
     topLayout->addWidget(browseBtn, 0, 2);
     topLayout->addWidget(imageTitleLabel, 1, 0);
-    topLayout->addWidget(imageTitleEdit, 1, 1, 1, 2);
+    topLayout->addWidget(m_imageTitleEdit, 1, 1, 1, 2);
     topLayout->setColumnStretch(0, 0);
     topLayout->setColumnStretch(1, 1);
     topLayout->setColumnStretch(2, 0);
@@ -67,22 +71,25 @@ void VInsertImageDialog::setupUI()
     mainLayout->setSizeConstraint(QLayout::SetFixedSize);
     setWindowTitle(title);
 
-    imageTitleEdit->setFocus();
+    m_imageTitleEdit->setFocus();
 }
 
 void VInsertImageDialog::enableOkButton()
 {
-    bool enabled = true;
-    if (imageTitleEdit->text().isEmpty() || !image) {
-        enabled = false;
+    QString title = m_imageTitleEdit->getEvaluatedText();
+    bool titleOk = !title.isEmpty();
+    if (titleOk) {
+        QRegExp reg(VUtils::c_imageTitleRegExp);
+        titleOk = reg.exactMatch(title);
     }
+
     QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok);
-    okBtn->setEnabled(enabled);
+    okBtn->setEnabled(titleOk);
 }
 
 QString VInsertImageDialog::getImageTitleInput() const
 {
-    return imageTitleEdit->text();
+    return m_imageTitleEdit->getEvaluatedText();
 }
 
 QString VInsertImageDialog::getPathInput() const

+ 2 - 1
src/dialog/vinsertimagedialog.h

@@ -8,6 +8,7 @@
 
 class QLabel;
 class QLineEdit;
+class VLineEdit;
 class QPushButton;
 class QDialogButtonBox;
 
@@ -37,7 +38,7 @@ private:
     void setupUI();
 
     QLabel *imageTitleLabel;
-    QLineEdit *imageTitleEdit;
+    VLineEdit *m_imageTitleEdit;
     QLabel *pathLabel;
     QLineEdit *pathEdit;
     QPushButton *browseBtn;

+ 33 - 12
src/dialog/vnewdirdialog.cpp

@@ -2,6 +2,8 @@
 #include "vnewdirdialog.h"
 #include "vdirectory.h"
 #include "vconfigmanager.h"
+#include "vlineedit.h"
+#include "utils/vutils.h"
 
 extern VConfigManager *g_config;
 
@@ -15,7 +17,7 @@ VNewDirDialog::VNewDirDialog(const QString &title,
 {
     setupUI();
 
-    connect(nameEdit, &QLineEdit::textChanged, this, &VNewDirDialog::handleInputChanged);
+    connect(m_nameEdit, &QLineEdit::textChanged, this, &VNewDirDialog::handleInputChanged);
 
     handleInputChanged();
 }
@@ -29,9 +31,12 @@ void VNewDirDialog::setupUI()
     }
 
     QLabel *nameLabel = new QLabel(tr("Folder &name:"));
-    nameEdit = new QLineEdit(defaultName);
-    nameEdit->selectAll();
-    nameLabel->setBuddy(nameEdit);
+    m_nameEdit = new VLineEdit(defaultName);
+    QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
+                                                 m_nameEdit);
+    m_nameEdit->setValidator(validator);
+    m_nameEdit->selectAll();
+    nameLabel->setBuddy(m_nameEdit);
 
     m_warnLabel = new QLabel();
     m_warnLabel->setWordWrap(true);
@@ -44,10 +49,10 @@ void VNewDirDialog::setupUI()
 
     QHBoxLayout *topLayout = new QHBoxLayout();
     topLayout->addWidget(nameLabel);
-    topLayout->addWidget(nameEdit);
+    topLayout->addWidget(m_nameEdit);
 
     QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok);
-    nameEdit->setMinimumWidth(okBtn->sizeHint().width() * 3);
+    m_nameEdit->setMinimumWidth(okBtn->sizeHint().width() * 3);
 
     QVBoxLayout *mainLayout = new QVBoxLayout();
     if (infoLabel) {
@@ -64,18 +69,34 @@ void VNewDirDialog::setupUI()
 void VNewDirDialog::handleInputChanged()
 {
     bool showWarnLabel = false;
-    QString name = nameEdit->text();
+    QString name = m_nameEdit->getEvaluatedText();
     bool nameOk = !name.isEmpty();
     if (nameOk) {
         // Check if the name conflicts with existing directory name.
         // Case-insensitive when creating folder.
+        QString warnText;
         if (m_directory->findSubDirectory(name, false)) {
             nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name (case-insensitive) <span style=\"%2\">%3</span> already exists. "
+                          "Please choose another name.")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        } else if (!VUtils::checkFileNameLegal(name)) {
+            // Check if evaluated name contains illegal characters.
+            nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name <span style=\"%2\">%3</span> contains illegal characters "
+                          "(after magic word evaluation).")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        }
+
+        if (!nameOk) {
             showWarnLabel = true;
-            QString nameConflictText = tr("<span style=\"%1\">WARNING</span>: Name (case-insensitive) already exists. "
-                                          "Please choose another name.")
-                                          .arg(g_config->c_warningTextStyle);
-            m_warnLabel->setText(nameConflictText);
+            m_warnLabel->setText(warnText);
         }
     }
 
@@ -87,5 +108,5 @@ void VNewDirDialog::handleInputChanged()
 
 QString VNewDirDialog::getNameInput() const
 {
-    return nameEdit->text();
+    return m_nameEdit->getEvaluatedText();
 }

+ 2 - 2
src/dialog/vnewdirdialog.h

@@ -4,7 +4,7 @@
 #include <QDialog>
 
 class QLabel;
-class QLineEdit;
+class VLineEdit;
 class QDialogButtonBox;
 class QString;
 class VDirectory;
@@ -27,7 +27,7 @@ private slots:
 private:
     void setupUI();
 
-    QLineEdit *nameEdit;
+    VLineEdit *m_nameEdit;
     QDialogButtonBox *m_btnBox;
 
     QLabel *m_warnLabel;

+ 33 - 12
src/dialog/vnewfiledialog.cpp

@@ -2,6 +2,8 @@
 #include "vnewfiledialog.h"
 #include "vconfigmanager.h"
 #include "vdirectory.h"
+#include "vlineedit.h"
+#include "utils/vutils.h"
 
 extern VConfigManager *g_config;
 
@@ -13,7 +15,7 @@ VNewFileDialog::VNewFileDialog(const QString &title, const QString &info,
 {
     setupUI();
 
-    connect(nameEdit, &QLineEdit::textChanged, this, &VNewFileDialog::handleInputChanged);
+    connect(m_nameEdit, &VLineEdit::textChanged, this, &VNewFileDialog::handleInputChanged);
 
     handleInputChanged();
 }
@@ -27,10 +29,13 @@ void VNewFileDialog::setupUI()
 
     // Name.
     QLabel *nameLabel = new QLabel(tr("Note &name:"));
-    nameEdit = new QLineEdit(defaultName);
+    m_nameEdit = new VLineEdit(defaultName);
+    QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
+                                                 m_nameEdit);
+    m_nameEdit->setValidator(validator);
     int dotIndex = defaultName.lastIndexOf('.');
-    nameEdit->setSelection(0, (dotIndex == -1) ? defaultName.size() : dotIndex);
-    nameLabel->setBuddy(nameEdit);
+    m_nameEdit->setSelection(0, (dotIndex == -1) ? defaultName.size() : dotIndex);
+    nameLabel->setBuddy(m_nameEdit);
 
     // InsertTitle.
     m_insertTitleCB = new QCheckBox(tr("Insert note name as title (for Markdown only)"));
@@ -42,10 +47,10 @@ void VNewFileDialog::setupUI()
             });
 
     QFormLayout *topLayout = new QFormLayout();
-    topLayout->addRow(nameLabel, nameEdit);
+    topLayout->addRow(nameLabel, m_nameEdit);
     topLayout->addWidget(m_insertTitleCB);
 
-    nameEdit->setMinimumWidth(m_insertTitleCB->sizeHint().width());
+    m_nameEdit->setMinimumWidth(m_insertTitleCB->sizeHint().width());
 
     m_warnLabel = new QLabel();
     m_warnLabel->setWordWrap(true);
@@ -73,18 +78,34 @@ void VNewFileDialog::setupUI()
 void VNewFileDialog::handleInputChanged()
 {
     bool showWarnLabel = false;
-    QString name = nameEdit->text();
+    QString name = m_nameEdit->getEvaluatedText();
     bool nameOk = !name.isEmpty();
     if (nameOk) {
         // Check if the name conflicts with existing note name.
         // Case-insensitive when creating note.
+        QString warnText;
         if (m_directory->findFile(name, false)) {
             nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name (case-insensitive) <span style=\"%2\">%3</span> already exists. "
+                          "Please choose another name.")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        } else if (!VUtils::checkFileNameLegal(name)) {
+            // Check if evaluated name contains illegal characters.
+            nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name <span style=\"%2\">%3</span> contains illegal characters "
+                          "(after magic word evaluation).")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        }
+
+        if (!nameOk) {
             showWarnLabel = true;
-            QString nameConflictText = tr("<span style=\"%1\">WARNING</span>: Name (case-insensitive) already exists. "
-                                          "Please choose another name.")
-                                          .arg(g_config->c_warningTextStyle);
-            m_warnLabel->setText(nameConflictText);
+            m_warnLabel->setText(warnText);
         }
     }
 
@@ -96,7 +117,7 @@ void VNewFileDialog::handleInputChanged()
 
 QString VNewFileDialog::getNameInput() const
 {
-    return nameEdit->text();
+    return m_nameEdit->getEvaluatedText();
 }
 
 bool VNewFileDialog::getInsertTitleInput() const

+ 2 - 2
src/dialog/vnewfiledialog.h

@@ -4,7 +4,7 @@
 #include <QDialog>
 
 class QLabel;
-class QLineEdit;
+class VLineEdit;
 class QDialogButtonBox;
 class QCheckBox;
 class VDirectory;
@@ -27,7 +27,7 @@ private slots:
 private:
     void setupUI();
 
-    QLineEdit *nameEdit;
+    VLineEdit *m_nameEdit;
     QCheckBox *m_insertTitleCB;
 
     QPushButton *okBtn;

+ 38 - 17
src/dialog/vnewnotebookdialog.cpp

@@ -4,6 +4,7 @@
 #include "vconfigmanager.h"
 #include "utils/vutils.h"
 #include "vnotebook.h"
+#include "vlineedit.h"
 
 extern VConfigManager *g_config;
 
@@ -18,7 +19,7 @@ VNewNotebookDialog::VNewNotebookDialog(const QString &title, const QString &info
 {
     setupUI(title, info);
 
-    connect(nameEdit, &QLineEdit::textChanged, this, &VNewNotebookDialog::handleInputChanged);
+    connect(m_nameEdit, &QLineEdit::textChanged, this, &VNewNotebookDialog::handleInputChanged);
     connect(pathEdit, &QLineEdit::textChanged, this, &VNewNotebookDialog::handleInputChanged);
     connect(browseBtn, &QPushButton::clicked, this, &VNewNotebookDialog::handleBrowseBtnClicked);
 
@@ -34,8 +35,11 @@ void VNewNotebookDialog::setupUI(const QString &p_title, const QString &p_info)
     }
 
     QLabel *nameLabel = new QLabel(tr("Notebook &name:"));
-    nameEdit = new QLineEdit(defaultName);
-    nameLabel->setBuddy(nameEdit);
+    m_nameEdit = new VLineEdit(defaultName);
+    QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
+                                                 m_nameEdit);
+    m_nameEdit->setValidator(validator);
+    nameLabel->setBuddy(m_nameEdit);
 
     QLabel *pathLabel = new QLabel(tr("Notebook &root folder:"));
     pathEdit = new QLineEdit(defaultPath);
@@ -50,7 +54,7 @@ void VNewNotebookDialog::setupUI(const QString &p_title, const QString &p_info)
     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);
+    validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp), m_imageFolderEdit);
     m_imageFolderEdit->setValidator(validator);
 
     QLabel *attachmentFolderLabel = new QLabel(tr("&Attachment folder:"));
@@ -66,7 +70,7 @@ void VNewNotebookDialog::setupUI(const QString &p_title, const QString &p_info)
 
     QGridLayout *topLayout = new QGridLayout();
     topLayout->addWidget(nameLabel, 0, 0);
-    topLayout->addWidget(nameEdit, 0, 1, 1, 2);
+    topLayout->addWidget(m_nameEdit, 0, 1, 1, 2);
     topLayout->addWidget(pathLabel, 1, 0);
     topLayout->addWidget(pathEdit, 1, 1);
     topLayout->addWidget(browseBtn, 1, 2);
@@ -104,7 +108,7 @@ void VNewNotebookDialog::setupUI(const QString &p_title, const QString &p_info)
 
 QString VNewNotebookDialog::getNameInput() const
 {
-    return nameEdit->text();
+    return m_nameEdit->getEvaluatedText();
 }
 
 QString VNewNotebookDialog::getPathInput() const
@@ -161,7 +165,7 @@ bool VNewNotebookDialog::isImportExistingNotebook() const
 
 void VNewNotebookDialog::showEvent(QShowEvent *event)
 {
-    nameEdit->setFocus();
+    m_nameEdit->setFocus();
     QDialog::showEvent(event);
 }
 
@@ -183,7 +187,7 @@ void VNewNotebookDialog::handleInputChanged()
         m_manualPath = true;
     }
 
-    if (nameEdit->isModified()) {
+    if (m_nameEdit->isModified()) {
         m_manualName = true;
     }
 
@@ -248,7 +252,7 @@ void VNewNotebookDialog::handleInputChanged()
         }
     }
 
-    QString name = nameEdit->text();
+    QString name = m_nameEdit->getEvaluatedText();
     bool nameOk = !name.isEmpty();
     if (pathOk && nameOk) {
         // Check if the name conflicts with existing notebook name.
@@ -260,13 +264,29 @@ void VNewNotebookDialog::handleInputChanged()
             }
         }
 
+        QString warnText;
         if (idx < m_notebooks.size()) {
             nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name (case-insensitive) <span style=\"%2\">%3</span> already exists. "
+                          "Please choose another name.")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        } else if (!VUtils::checkFileNameLegal(name)) {
+            // Check if evaluated name contains illegal characters.
+            nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name <span style=\"%2\">%3</span> contains illegal characters "
+                          "(after magic word evaluation).")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        }
+
+        if (!nameOk) {
             showWarnLabel = true;
-            QString nameConflictText = tr("<span style=\"%1\">WARNING</span>: Name (case-insensitive) already exists. "
-                                          "Please choose another name.")
-                                          .arg(g_config->c_warningTextStyle);
-            m_warnLabel->setText(nameConflictText);
+            m_warnLabel->setText(warnText);
         }
     }
 
@@ -281,6 +301,8 @@ void VNewNotebookDialog::handleInputChanged()
 
 bool VNewNotebookDialog::autoComplete()
 {
+    QString nameText = m_nameEdit->getEvaluatedText();
+
     if (m_manualPath) {
         if (m_manualName) {
             return false;
@@ -290,8 +312,8 @@ bool VNewNotebookDialog::autoComplete()
         QString pathText = pathEdit->text();
         if (!pathText.isEmpty()) {
             QString autoName = VUtils::directoryNameFromPath(pathText);
-            if (autoName != nameEdit->text()) {
-                nameEdit->setText(autoName);
+            if (autoName != nameText) {
+                m_nameEdit->setText(autoName);
                 return true;
             }
         }
@@ -307,7 +329,6 @@ bool VNewNotebookDialog::autoComplete()
     }
 
     bool ret = false;
-    QString nameText = nameEdit->text();
     if (nameText.isEmpty()) {
         if (m_manualName) {
             return false;
@@ -316,7 +337,7 @@ bool VNewNotebookDialog::autoComplete()
         // Get a folder name under vnoteFolder and set it as the name of the notebook.
         QString name = "vnotebook";
         name = VUtils::getDirNameWithSequence(vnoteFolder, name);
-        nameEdit->setText(name);
+        m_nameEdit->setText(name);
         ret = true;
     } else {
         // Use the name as the folder name under vnoteFolder.

+ 2 - 1
src/dialog/vnewnotebookdialog.h

@@ -6,6 +6,7 @@
 
 class QLabel;
 class QLineEdit;
+class VLineEdit;
 class QPushButton;
 class QDialogButtonBox;
 class VNotebook;
@@ -52,7 +53,7 @@ private:
     // Returns true if name or path is modified.
     bool autoComplete();
 
-    QLineEdit *nameEdit;
+    VLineEdit *m_nameEdit;
     QLineEdit *pathEdit;
     QPushButton *browseBtn;
     QLabel *m_warnLabel;

+ 28 - 8
src/dialog/vnotebookinfodialog.cpp

@@ -3,6 +3,7 @@
 #include "vnotebook.h"
 #include "utils/vutils.h"
 #include "vconfigmanager.h"
+#include "vlineedit.h"
 
 extern VConfigManager *g_config;
 
@@ -29,7 +30,10 @@ void VNotebookInfoDialog::setupUI(const QString &p_title, const QString &p_info)
         infoLabel = new QLabel(p_info);
     }
 
-    m_nameEdit = new QLineEdit(m_notebook->getName());
+    m_nameEdit = new VLineEdit(m_notebook->getName());
+    QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp),
+                                                 m_nameEdit);
+    m_nameEdit->setValidator(validator);
     m_nameEdit->selectAll();
 
     m_pathEdit = new QLineEdit(m_notebook->getPath());
@@ -41,7 +45,7 @@ void VNotebookInfoDialog::setupUI(const QString &p_title, const QString &p_info)
                                             .arg(g_config->getImageFolder()));
     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);
+    validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp), m_imageFolderEdit);
     m_imageFolderEdit->setValidator(validator);
 
     // Attachment folder.
@@ -98,7 +102,7 @@ void VNotebookInfoDialog::setupUI(const QString &p_title, const QString &p_info)
 
 void VNotebookInfoDialog::handleInputChanged()
 {
-    QString name = m_nameEdit->text();
+    QString name = m_nameEdit->getEvaluatedText();
     bool nameOk = !name.isEmpty();
     bool showWarnLabel = false;
 
@@ -112,13 +116,29 @@ void VNotebookInfoDialog::handleInputChanged()
             }
         }
 
+        QString warnText;
         if (idx < m_notebooks.size() && m_notebooks[idx] != m_notebook) {
             nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name (case-insensitive) <span style=\"%2\">%3</span> already exists. "
+                          "Please choose another name.")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        } else if (!VUtils::checkFileNameLegal(name)) {
+            // Check if evaluated name contains illegal characters.
+            nameOk = false;
+            warnText = tr("<span style=\"%1\">WARNING</span>: "
+                          "Name <span style=\"%2\">%3</span> contains illegal characters "
+                          "(after magic word evaluation).")
+                         .arg(g_config->c_warningTextStyle)
+                         .arg(g_config->c_dataTextStyle)
+                         .arg(name);
+        }
+
+        if (!nameOk) {
             showWarnLabel = true;
-            QString nameConflictText = tr("<span style=\"%1\">WARNING</span>: Name (case-insensitive) already exists. "
-                                          "Please choose another name.")
-                                          .arg(g_config->c_warningTextStyle);
-            m_warnLabel->setText(nameConflictText);
+            m_warnLabel->setText(warnText);
         }
     }
 
@@ -130,7 +150,7 @@ void VNotebookInfoDialog::handleInputChanged()
 
 QString VNotebookInfoDialog::getName() const
 {
-    return m_nameEdit->text();
+    return m_nameEdit->getEvaluatedText();
 }
 
 QString VNotebookInfoDialog::getImageFolder() const

+ 2 - 1
src/dialog/vnotebookinfodialog.h

@@ -6,6 +6,7 @@
 
 class QLabel;
 class QLineEdit;
+class VLineEdit;
 class QDialogButtonBox;
 class QString;
 class VNotebook;
@@ -38,7 +39,7 @@ private:
 
     const VNotebook *m_notebook;
 
-    QLineEdit *m_nameEdit;
+    VLineEdit *m_nameEdit;
     QLineEdit *m_pathEdit;
     QLineEdit *m_imageFolderEdit;
     // Read-only.

+ 1 - 3
src/dialog/vorphanfileinfodialog.cpp

@@ -25,7 +25,6 @@ void VOrphanFileInfoDialog::setupUI()
     QLabel *fileLabel = new QLabel(m_file->fetchPath());
     topLayout->addRow(tr("File:"), fileLabel);
 
-    QLabel *imageFolderLabel = new QLabel(tr("Image folder:"));
     m_imageFolderEdit = new QLineEdit(m_file->getImageFolder());
     m_imageFolderEdit->setPlaceholderText(tr("Use global configuration (%1)")
                                             .arg(g_config->getImageFolderExt()));
@@ -33,9 +32,8 @@ void VOrphanFileInfoDialog::setupUI()
                               "of this file.\nIf absolute path is used, "
                               "VNote will not manage those images."
                               "(empty to use global configuration)");
-    imageFolderLabel->setToolTip(imgFolderTip);
     m_imageFolderEdit->setToolTip(imgFolderTip);
-    topLayout->addRow(imageFolderLabel, m_imageFolderEdit);
+    topLayout->addRow(tr("&Image folder:"), m_imageFolderEdit);
 
     // Ok is the default button.
     m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);

+ 6 - 2
src/src.pro

@@ -78,7 +78,9 @@ SOURCES += main.cpp\
     vattachmentlist.cpp \
     dialog/vsortdialog.cpp \
     vfilesessioninfo.cpp \
-    vtableofcontent.cpp
+    vtableofcontent.cpp \
+    utils/vmetawordmanager.cpp \
+    vlineedit.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -144,7 +146,9 @@ HEADERS  += vmainwindow.h \
     vattachmentlist.h \
     dialog/vsortdialog.h \
     vfilesessioninfo.h \
-    vtableofcontent.h
+    vtableofcontent.h \
+    utils/vmetawordmanager.h \
+    vlineedit.h
 
 RESOURCES += \
     vnote.qrc \

+ 593 - 0
src/utils/vmetawordmanager.cpp

@@ -0,0 +1,593 @@
+#include "vmetawordmanager.h"
+
+#include <QDebug>
+#include <QWidget>
+#include <QApplication>
+#include <QToolTip>
+
+#include "vconfigmanager.h"
+
+extern VConfigManager *g_config;
+
+
+// Used as the function template for some date/time related meta words.
+static QString formattedDateTime(const VMetaWord *p_metaWord,
+                                 const QString &p_format)
+{
+    return p_metaWord->getManager()->getDateTime().toString(p_format);
+}
+
+static QString allMetaWordsInfo(const VMetaWord *p_metaWord)
+{
+    QString msg = QObject::tr("All magic words:");
+
+    const VMetaWordManager *mgr = p_metaWord->getManager();
+    QList<QString> keys = mgr->getAllMetaWords().keys();
+    keys.sort(Qt::CaseInsensitive);
+
+    for (auto const & key : keys) {
+        const VMetaWord *word = mgr->findMetaWord(key);
+        Q_ASSERT(word);
+        msg += QString("\n%1:\t%2").arg(word->getWord()).arg(word->getDefinition());
+    }
+
+    QWidget *focusWid = QApplication::focusWidget();
+    if (focusWid) {
+        QPoint pos = focusWid->mapToGlobal(QPoint(0, focusWid->height()));
+        QToolTip::showText(pos, msg, focusWid);
+    }
+
+    // Just return the same word.
+    return QString("%1help%1").arg(VMetaWordManager::c_delimiter);
+}
+
+const QChar VMetaWordManager::c_delimiter = '%';
+
+VMetaWordManager::VMetaWordManager(QObject *p_parent)
+    : QObject(p_parent)
+{
+}
+
+void VMetaWordManager::init()
+{
+    using namespace std::placeholders;
+
+    // %d%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "d",
+                tr("the day as number without a leading zero (`1` to `31`)"),
+                std::bind(formattedDateTime, _1, "d"));
+
+    // %dd%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "dd",
+                tr("the day as number with a leading zero (`01` to `31`)"),
+                std::bind(formattedDateTime, _1, "dd"));
+
+    // %ddd%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "ddd",
+                tr("the abbreviated localized day name (e.g. `Mon` to `Sun`)"),
+                std::bind(formattedDateTime, _1, "ddd"));
+
+    // %dddd%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "dddd",
+                tr("the long localized day name (e.g. `Monday` to `Sunday`)"),
+                std::bind(formattedDateTime, _1, "dddd"));
+
+    // %M%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "M",
+                tr("the month as number without a leading zero (`1` to `12`)"),
+                std::bind(formattedDateTime, _1, "M"));
+
+    // %MM%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "MM",
+                tr("the month as number with a leading zero (`01` to `12`)"),
+                std::bind(formattedDateTime, _1, "MM"));
+
+    // %MMM%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "MMM",
+                tr("the abbreviated localized month name (e.g. `Jan` to `Dec`)"),
+                std::bind(formattedDateTime, _1, "MMM"));
+
+    // %MMMM%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "MMMM",
+                tr("the long localized month name (e.g. `January` to `December`"),
+                std::bind(formattedDateTime, _1, "MMMM"));
+
+    // %yy%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "yy",
+                tr("the year as two digit number (`00` to `99`)"),
+                std::bind(formattedDateTime, _1, "yy"));
+
+    // %yyyy%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "yyyy",
+                tr("the year as four digit number"),
+                std::bind(formattedDateTime, _1, "yyyy"));
+
+    // %h%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "h",
+                tr("the hour without a leading zero (`0` to `23` or `1` to `12` if AM/PM display"),
+                std::bind(formattedDateTime, _1, "h"));
+
+    // %hh%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "hh",
+                tr("the hour with a leading zero (`00` to `23` or `01` to `12` if AM/PM display"),
+                std::bind(formattedDateTime, _1, "hh"));
+
+    // %H%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "H",
+                tr("the hour without a leading zero (`0` to `23` even with AM/PM display"),
+                std::bind(formattedDateTime, _1, "H"));
+
+    // %HH%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "HH",
+                tr("the hour with a leading zero (`00` to `23` even with AM/PM display"),
+                std::bind(formattedDateTime, _1, "HH"));
+
+    // %m%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "m",
+                tr("the minute without a leading zero (`0` to `59`)"),
+                std::bind(formattedDateTime, _1, "m"));
+
+    // %mm%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "mm",
+                tr("the minute with a leading zero (`00` to `59`)"),
+                std::bind(formattedDateTime, _1, "mm"));
+
+    // %s%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "s",
+                tr("the second without a leading zero (`0` to `59`)"),
+                std::bind(formattedDateTime, _1, "s"));
+
+    // %ss%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "ss",
+                tr("the second with a leading zero (`00` to `59`)"),
+                std::bind(formattedDateTime, _1, "ss"));
+
+    // %z%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "z",
+                tr("the milliseconds without leading zeroes (`0` to `999`)"),
+                std::bind(formattedDateTime, _1, "z"));
+
+    // %zzz%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "zzz",
+                tr("the milliseconds with leading zeroes (`000` to `999`)"),
+                std::bind(formattedDateTime, _1, "zzz"));
+
+    // %AP%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "AP",
+                tr("use AM/PM display (`AM` or `PM`)"),
+                std::bind(formattedDateTime, _1, "AP"));
+
+    // %A%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "A",
+                tr("use AM/PM display (`AM` or `PM`)"),
+                std::bind(formattedDateTime, _1, "A"));
+
+    // %ap%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "ap",
+                tr("use am/pm display (`am` or `pm`)"),
+                std::bind(formattedDateTime, _1, "ap"));
+
+    // %a%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "a",
+                tr("use am/pm display (`am` or `pm`)"),
+                std::bind(formattedDateTime, _1, "a"));
+
+    // %t%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "t",
+                tr("the timezone (e.g. `CEST`)"),
+                std::bind(formattedDateTime, _1, "t"));
+
+    // %random%.
+    addMetaWord(MetaWordType::FunctionBased,
+                "random",
+                tr("a random number"),
+                [](const VMetaWord *) {
+                    return QString::number(qrand());
+                });
+
+    // %random_d%.
+    addMetaWord(MetaWordType::Dynamic,
+                "random_d",
+                tr("dynamic version of `random`"),
+                [](const VMetaWord *) {
+                    return QString::number(qrand());
+                });
+
+    // %date%.
+    addMetaWord(MetaWordType::Compound,
+                "date",
+                QString("%1yyyy%1-%1MM%1-%1dd%1").arg(c_delimiter));
+
+    // %da%.
+    addMetaWord(MetaWordType::Compound,
+                "da",
+                QString("%1yyyy%1%1MM%1%1dd%1").arg(c_delimiter));
+
+    // %time%.
+    addMetaWord(MetaWordType::Compound,
+                "time",
+                QString("%1hh%1:%1mm%1:%1ss%1").arg(c_delimiter));
+
+    // %datetime%.
+    addMetaWord(MetaWordType::Compound,
+                "datetime",
+                QString("%1date%1 %1time%1").arg(c_delimiter));
+
+    // Custom meta words.
+    initCustomMetaWords();
+
+    // %help% to print all metaword info.
+    addMetaWord(MetaWordType::FunctionBased,
+                "help",
+                tr("information about all defined magic words"),
+                allMetaWordsInfo);
+}
+
+void VMetaWordManager::initCustomMetaWords()
+{
+    QVector<VMagicWord> words = g_config->getCustomMagicWords();
+    for (auto const & item : words) {
+        addMetaWord(MetaWordType::Compound,
+                    item.m_name,
+                    item.m_definition);
+    }
+}
+
+QString VMetaWordManager::evaluate(const QString &p_text) const
+{
+    if (p_text.isEmpty()) {
+        return p_text;
+    }
+
+    // Update datetime for later parse.
+    const_cast<VMetaWordManager *>(this)->m_dateTime = QDateTime::currentDateTime();
+
+    // Treat the text as a Compound meta word.
+    const QString tmpWord("vnote_tmp_metaword");
+    Q_ASSERT(!contains(tmpWord));
+    VMetaWord metaWord(this,
+                       MetaWordType::Compound,
+                       tmpWord,
+                       p_text);
+    if (metaWord.isValid()) {
+        return metaWord.evaluate();
+    } else {
+        return p_text;
+    }
+}
+
+bool VMetaWordManager::contains(const QString &p_word) const
+{
+    return m_metaWords.contains(p_word);
+}
+
+const VMetaWord *VMetaWordManager::findMetaWord(const QString &p_word) const
+{
+    auto it = m_metaWords.find(p_word);
+    if (it != m_metaWords.end()) {
+        return &it.value();
+    }
+
+    return NULL;
+}
+
+void VMetaWordManager::addMetaWord(MetaWordType p_type,
+                                   const QString &p_word,
+                                   const QString &p_definition,
+                                   MetaWordFunc p_function)
+{
+    if (p_word.isEmpty() || contains(p_word)) {
+        return;
+    }
+
+    VMetaWord metaWord(this,
+                       p_type,
+                       p_word,
+                       p_definition,
+                       p_function);
+
+    if (metaWord.isValid()) {
+        m_metaWords.insert(p_word, metaWord);
+        qDebug() << QString("MetaWord %1%2%1[%3] added")
+                           .arg(c_delimiter).arg(p_word).arg(p_definition);
+    }
+}
+
+VMetaWord::VMetaWord(const VMetaWordManager *p_manager,
+                     MetaWordType p_type,
+                     const QString &p_word,
+                     const QString &p_definition,
+                     MetaWordFunc p_function)
+    : m_manager(p_manager),
+      m_type(p_type),
+      m_word(p_word),
+      m_definition(p_definition),
+      m_valid(false)
+{
+    m_function = p_function;
+
+    if (checkType(MetaWordType::Simple)
+        || checkType(MetaWordType::Compound)) {
+        Q_ASSERT(!m_function);
+    } else {
+        Q_ASSERT(m_function);
+    }
+
+    checkAndParseDefinition();
+}
+
+bool VMetaWord::checkType(MetaWordType p_type)
+{
+    return m_type == p_type;
+}
+
+void VMetaWord::checkAndParseDefinition()
+{
+    if (m_word.contains(VMetaWordManager::c_delimiter)) {
+        m_valid = false;
+        return;
+    }
+
+    m_valid = true;
+    m_tokens.clear();
+
+    // We do not accept \n and \t in the definition.
+    QRegExp defReg("[\\S ]*");
+    if (!defReg.exactMatch(m_definition)) {
+        m_valid = false;
+        return;
+    }
+
+    if (checkType(MetaWordType::FunctionBased)
+        || checkType(MetaWordType::Dynamic)) {
+        if (!m_function) {
+            m_valid = false;
+        }
+    } else if (checkType(MetaWordType::Compound)) {
+        m_tokens = parseToTokens(m_definition);
+        if (m_tokens.isEmpty()) {
+            m_valid = false;
+            return;
+        }
+
+        for (auto const & to : m_tokens) {
+            if (to.isMetaWord()) {
+                if (!m_manager->contains(to.m_value)) {
+                    // Dependency not defined.
+                    m_valid = false;
+                    break;
+                }
+            }
+        }
+    }
+}
+
+bool VMetaWord::isValid() const
+{
+    return m_valid;
+}
+
+QString VMetaWord::evaluate() const
+{
+    Q_ASSERT(m_valid);
+    qDebug() << "evaluate meta word" << m_word;
+    switch (m_type) {
+    case MetaWordType::Simple:
+        return m_definition;
+
+    case MetaWordType::FunctionBased:
+    case MetaWordType::Dynamic:
+        return m_function(this);
+
+    case MetaWordType::Compound:
+    {
+        QHash<QString, QString> cache;
+        return evaluateTokens(m_tokens, cache);
+    }
+
+    default:
+        return "";
+    }
+}
+
+MetaWordType VMetaWord::getType() const
+{
+    return m_type;
+}
+
+const QString &VMetaWord::getWord() const
+{
+    return m_word;
+}
+
+const QString &VMetaWord::getDefinition() const
+{
+    return m_definition;
+}
+
+const VMetaWordManager *VMetaWord::getManager() const
+{
+    return m_manager;
+}
+
+
+QString VMetaWord::toString() const
+{
+    QChar typeChar('U');
+    switch (m_type) {
+    case MetaWordType::Simple:
+        typeChar = 'S';
+        break;
+
+    case MetaWordType::FunctionBased:
+        typeChar = 'F';
+        break;
+
+    case MetaWordType::Dynamic:
+        typeChar = 'D';
+        break;
+
+    case MetaWordType::Compound:
+        typeChar = 'C';
+        break;
+
+    default:
+        break;
+    }
+
+    return QString("%1%2%1[%3]: %4").arg(VMetaWordManager::c_delimiter)
+                                     .arg(m_word)
+                                     .arg(typeChar)
+                                     .arg(m_definition);
+}
+
+QVector<VMetaWord::Token> VMetaWord::parseToTokens(const QString &p_text)
+{
+    QVector<Token> tokens;
+
+    TokenType type = TokenType::Raw;
+    QString value;
+    value.reserve(p_text.size());
+    for (int idx = 0; idx < p_text.size(); ++idx) {
+        const QChar &ch = p_text[idx];
+        if (ch == VMetaWordManager::c_delimiter) {
+            // Check if it is single or double.
+            int next = idx + 1;
+            if (next == p_text.size()
+                || p_text[next] != VMetaWordManager::c_delimiter) {
+                // Single delimiter.
+                if (type == TokenType::Raw) {
+                    // End of a raw token, begin of a MetaWord token.
+                    if (!value.isEmpty()) {
+                        tokens.push_back(Token(type, value));
+                    }
+
+                    type = TokenType::MetaWord;
+                } else {
+                    // End of a MetaWord token, begin of a Raw token.
+                    Q_ASSERT(!value.isEmpty());
+
+                    tokens.push_back(Token(type, value));
+                    type = TokenType::Raw;
+                }
+
+                value.clear();
+            } else {
+                // Double delimiter.
+                // If now is parsing a MetaWord token, treat the first delimiter
+                // as the end of a token.
+                // Otherwise, store one single delimiter in value and skip next char.
+                if (type == TokenType::MetaWord) {
+                    Q_ASSERT(!value.isEmpty());
+                    tokens.push_back(Token(type, value));
+                    type = TokenType::Raw;
+                    value.clear();
+                } else {
+                    value.push_back(ch);
+                    ++idx;
+                }
+            }
+        } else {
+            // Push ch in value.
+            value.push_back(ch);
+        }
+    }
+
+    if (!value.isEmpty()) {
+        if (type == TokenType::Raw) {
+            tokens.push_back(Token(type, value));
+        } else {
+            // An imcomplete metaword token.
+            // Treat it as raw.
+            tokens.push_back(Token(TokenType::Raw, "%" + value));
+        }
+
+        value.clear();
+    }
+
+    return tokens;
+}
+
+QString VMetaWord::evaluateTokens(const QVector<VMetaWord::Token> &p_tokens,
+                                  QHash<QString, QString> &p_cache) const
+{
+    QString val;
+
+    for (auto const & to : p_tokens) {
+        switch (to.m_type) {
+        case TokenType::Raw:
+            val += to.m_value;
+            break;
+
+        case TokenType::MetaWord:
+        {
+            const VMetaWord *metaWord = m_manager->findMetaWord(to.m_value);
+            if (!metaWord) {
+                // Invalid meta word. Treat it as literal value.
+                val += VMetaWordManager::c_delimiter + to.m_value + VMetaWordManager::c_delimiter;
+                break;
+            }
+
+            QString wordVal;
+            switch (metaWord->getType()) {
+            case MetaWordType::FunctionBased:
+            {
+                auto it = p_cache.find(metaWord->getWord());
+                if (it != p_cache.end()) {
+                    // Find it in the cache.
+                    wordVal = it.value();
+                } else {
+                    // First evaluate this meta word.
+                    wordVal = metaWord->evaluate();
+                    p_cache.insert(metaWord->getWord(), wordVal);
+                }
+
+                break;
+            }
+
+            case MetaWordType::Compound:
+                wordVal = evaluateTokens(metaWord->m_tokens, p_cache);
+                break;
+
+            default:
+                wordVal = metaWord->evaluate();
+                break;
+            }
+
+            val += wordVal;
+            break;
+        }
+
+        default:
+            Q_ASSERT(false);
+            break;
+        }
+    }
+
+    return val;
+}

+ 202 - 0
src/utils/vmetawordmanager.h

@@ -0,0 +1,202 @@
+#ifndef VMETAWORDMANAGER_H
+#define VMETAWORDMANAGER_H
+
+#include <functional>
+
+#include <QObject>
+#include <QString>
+#include <QVector>
+#include <QHash>
+#include <QDateTime>
+
+
+enum class MetaWordType
+{
+    // Definition is plain text.
+    Simple = 0,
+
+    // Definition is a function call to get the value.
+    FunctionBased,
+
+    // Like FunctionBased, but should re-evaluate the value for each occurence.
+    Dynamic,
+
+    // Consists of other meta words.
+    Compound,
+
+    Invalid
+};
+
+// We call meta word "magic word" in user interaction.
+struct VMagicWord
+{
+    QString m_name;
+    QString m_definition;
+};
+
+class VMetaWordManager;
+class VMetaWord;
+
+typedef std::function<QString(const VMetaWord *)> MetaWordFunc;
+
+// A Meta Word is surrounded by %.
+// - Use %% for an escaped %;
+// - Built-in or user-defined;
+// - A meta word could contain other meta words as definition.
+class VMetaWord
+{
+public:
+    VMetaWord(const VMetaWordManager *p_manager,
+              MetaWordType p_type,
+              const QString &p_word,
+              const QString &p_definition,
+              MetaWordFunc p_function = nullptr);
+
+    bool isValid() const;
+
+    QString evaluate() const;
+
+    MetaWordType getType() const;
+
+    const QString &getWord() const;
+
+    const QString &getDefinition() const;
+
+    const VMetaWordManager *getManager() const;
+
+    QString toString() const;
+
+    enum class TokenType
+    {
+        Raw = 0,
+        MetaWord
+    };
+
+    struct Token
+    {
+        Token()
+            : m_type(TokenType::Raw)
+        {
+        }
+
+        Token(VMetaWord::TokenType p_type, const QString &p_value)
+            : m_type(p_type),
+              m_value(p_value)
+        {
+        }
+
+        QString toString() const
+        {
+            return QString("token %1[%2]").arg(m_type == TokenType::Raw
+                                              ? "Raw" : "MetaWord")
+                                         .arg(m_value);
+        }
+
+        bool isRaw() const
+        {
+            return m_type == TokenType::Raw;
+        }
+
+        bool isMetaWord() const
+        {
+            return m_type == TokenType::MetaWord;
+        }
+
+        TokenType m_type;
+
+        // For Raw type, m_value is the raw string of this token;
+        // For MetaWord type, m_value is the word of the meta word pointed to by
+        // this token.
+        QString m_value;
+    };
+
+private:
+    // Check if m_type is @p_type.
+    bool checkType(MetaWordType p_type);
+
+    // Parse children word from definition.
+    // Children word MUST be defined in m_manager already.
+    void checkAndParseDefinition();
+
+    // Parse @p_text to a list of tokens.
+    static QVector<VMetaWord::Token> parseToTokens(const QString &p_text);
+
+    // Used for Compound meta word with cache @p_cache.
+    // @p_cache: value cache for FunctionBased Token.
+    // For a meta word occuring more than once, we should evaluate it once for
+    // FunctionBased meta word.
+    QString evaluateTokens(const QVector<VMetaWord::Token> &p_tokens,
+                           QHash<QString, QString> &p_cache) const;
+
+    const VMetaWordManager *m_manager;
+
+    MetaWordType m_type;
+
+    // Word could contains spaces but no %.
+    QString m_word;
+
+    // For Simple/Compound meta word, this contains the definition;
+    // For FunctionBased/Dynamic meta word, this makes no sense and is used
+    // for description.
+    QString m_definition;
+
+    // For FunctionBased and Dynamic meta word.
+    MetaWordFunc m_function;
+
+    bool m_valid;
+
+    // Tokens used for Compound meta word.
+    QVector<Token> m_tokens;
+};
+
+
+// Manager of meta word.
+class VMetaWordManager : public QObject
+{
+    Q_OBJECT
+public:
+    explicit VMetaWordManager(QObject *p_parent = nullptr);
+
+    void init();
+
+    // Expand meta words in @p_text and return the expanded text.
+    QString evaluate(const QString &p_text) const;
+
+    const VMetaWord *findMetaWord(const QString &p_word) const;
+
+    bool contains(const QString &p_word) const;
+
+    const QDateTime &getDateTime() const;
+
+    const QHash<QString, VMetaWord> &getAllMetaWords() const;
+
+    // % by default.
+    static const QChar c_delimiter;
+
+private:
+    void addMetaWord(MetaWordType p_type,
+                     const QString &p_word,
+                     const QString &p_definition,
+                     MetaWordFunc p_function = nullptr);
+
+    void initCustomMetaWords();
+
+    // Map using word as key.
+    QHash<QString, VMetaWord> m_metaWords;
+
+    // Used for data/time related evaluate.
+    // Will be updated before each evaluation.
+    QDateTime m_dateTime;
+};
+
+inline const QDateTime &VMetaWordManager::getDateTime() const
+{
+    return m_dateTime;
+}
+
+inline const QHash<QString, VMetaWord> &VMetaWordManager::getAllMetaWords() const
+{
+    return m_metaWords;
+}
+
+#endif // VMETAWORDMANAGER_H

+ 12 - 0
src/utils/vutils.cpp

@@ -33,6 +33,8 @@ QVector<QPair<QString, QString>> VUtils::s_availableLanguages;
 
 const QString VUtils::c_imageLinkRegExp = QString("\\!\\[([^\\]]*)\\]\\(([^\\)\"]+)\\s*(\"(\\\\.|[^\"\\)])*\")?\\s*\\)");
 
+const QString VUtils::c_imageTitleRegExp = QString("[\\w\\(\\)@#%\\*\\-\\+=\\?<>\\,\\.\\s]+");
+
 const QString VUtils::c_fileNameRegExp = QString("[^\\\\/:\\*\\?\"<>\\|]*");
 
 const QString VUtils::c_fencedCodeBlockStartRegExp = QString("^(\\s*)```([^`\\s]*)\\s*[^`]*$");
@@ -725,6 +727,16 @@ bool VUtils::checkPathLegal(const QString &p_path)
     return ret;
 }
 
+bool VUtils::checkFileNameLegal(const QString &p_name)
+{
+    if (p_name.isEmpty()) {
+        return false;
+    }
+
+    QRegExp exp(c_fileNameRegExp);
+    return exp.exactMatch(p_name);
+}
+
 bool VUtils::equalPath(const QString &p_patha, const QString &p_pathb)
 {
     QString a = QDir::cleanPath(p_patha);

+ 6 - 0
src/utils/vutils.h

@@ -177,6 +177,9 @@ public:
     // Try to check if @p_path is legal.
     static bool checkPathLegal(const QString &p_path);
 
+    // Check if file/folder name is legal.
+    static bool checkFileNameLegal(const QString &p_name);
+
     // Returns true if @p_patha and @p_pathb points to the same file/directory.
     static bool equalPath(const QString &p_patha, const QString &p_pathb);
 
@@ -248,6 +251,9 @@ public:
     // 4. Unused;
     static const QString c_imageLinkRegExp;
 
+    // Regular expression for image title.
+    static const QString c_imageTitleRegExp;
+
     // Regular expression for file/directory name.
     // Forbidden char: \/:*?"<>|
     static const QString c_fileNameRegExp;

+ 13 - 14
src/vattachmentlist.cpp

@@ -5,13 +5,12 @@
 #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;
+extern VMainWindow *g_mainWin;
 
 VAttachmentList::VAttachmentList(QWidget *p_parent)
     : QWidget(p_parent), m_file(NULL)
@@ -53,7 +52,7 @@ void VAttachmentList::setupUI()
                                                     .arg(m_file->fetchAttachmentFolderPath()),
                                                   QMessageBox::Ok | QMessageBox::Cancel,
                                                   QMessageBox::Ok,
-                                                  g_vnote->getMainWindow(),
+                                                  g_mainWin,
                                                   MessageBoxType::Danger);
                     if (ret == QMessageBox::Ok) {
                         if (!m_file->deleteAttachments()) {
@@ -66,7 +65,7 @@ void VAttachmentList::setupUI()
                                                    "maintain the configuration file manually."),
                                                 QMessageBox::Ok,
                                                 QMessageBox::Ok,
-                                                g_vnote->getMainWindow());
+                                                g_mainWin);
                         }
 
                         m_attachmentList->clear();
@@ -204,7 +203,7 @@ void VAttachmentList::addAttachment()
     }
 
     static QString lastPath = QDir::homePath();
-    QStringList files = QFileDialog::getOpenFileNames(g_vnote->getMainWindow(),
+    QStringList files = QFileDialog::getOpenFileNames(g_mainWin,
                                                       tr("Select Files As Attachments"),
                                                       lastPath);
     if (files.isEmpty()) {
@@ -236,16 +235,16 @@ void VAttachmentList::addAttachments(const QStringList &p_files)
                                 "",
                                 QMessageBox::Ok,
                                 QMessageBox::Ok,
-                                g_vnote->getMainWindow());
+                                g_mainWin);
         } else {
             ++addedFiles;
         }
     }
 
     if (addedFiles > 0) {
-        g_vnote->getMainWindow()->showStatusMessage(tr("%1 %2 added as attachments")
-                                                      .arg(addedFiles)
-                                                      .arg(addedFiles > 1 ? tr("files") : tr("file")));
+        g_mainWin->showStatusMessage(tr("%1 %2 added as attachments")
+                                     .arg(addedFiles)
+                                     .arg(addedFiles > 1 ? tr("files") : tr("file")));
     }
 }
 
@@ -330,7 +329,7 @@ void VAttachmentList::deleteSelectedItems()
                                   false,
                                   false,
                                   false,
-                                  g_vnote->getMainWindow());
+                                  g_mainWin);
     if (dialog.exec()) {
         items = dialog.getConfirmedItems();
 
@@ -349,7 +348,7 @@ void VAttachmentList::deleteSelectedItems()
                                    "maintain the configuration file manually."),
                                 QMessageBox::Ok,
                                 QMessageBox::Ok,
-                                g_vnote->getMainWindow());
+                                g_mainWin);
         }
 
         updateButtonState();
@@ -370,7 +369,7 @@ void VAttachmentList::sortItems()
                           "in the configuration file.")
                          .arg(g_config->c_dataTextStyle)
                          .arg(m_file->getName()),
-                       g_vnote->getMainWindow());
+                       g_mainWin);
     QTreeWidget *tree = dialog.getTreeWidget();
     tree->clear();
     tree->setColumnCount(1);
@@ -624,7 +623,7 @@ void VAttachmentList::checkAttachments()
                                   false,
                                   false,
                                   false,
-                                  g_vnote->getMainWindow());
+                                  g_mainWin);
     if (dialog.exec()) {
         items = dialog.getConfirmedItems();
 
@@ -643,7 +642,7 @@ void VAttachmentList::checkAttachments()
                                    "maintain the configuration file manually."),
                                 QMessageBox::Ok,
                                 QMessageBox::Ok,
-                                g_vnote->getMainWindow());
+                                g_mainWin);
         }
 
         updateButtonState();

+ 18 - 0
src/vconfigmanager.cpp

@@ -1152,3 +1152,21 @@ void VConfigManager::setLastOpenedFiles(const QVector<VFileSessionInfo> &p_files
              << "items in [last_opened_files] section";
 
 }
+
+QVector<VMagicWord> VConfigManager::getCustomMagicWords()
+{
+    QVector<VMagicWord> words;
+    int size = userSettings->beginReadArray("magic_words");
+    for (int i = 0; i < size; ++i) {
+        userSettings->setArrayIndex(i);
+
+        VMagicWord word;
+        word.m_name = userSettings->value("name").toString();
+        word.m_definition = userSettings->value("definition").toString();
+        words.push_back(word);
+    }
+
+    userSettings->endArray();
+
+    return words;
+}

+ 4 - 0
src/vconfigmanager.h

@@ -12,6 +12,7 @@
 #include "vmarkdownconverter.h"
 #include "vconstants.h"
 #include "vfilesessioninfo.h"
+#include "utils/vmetawordmanager.h"
 
 
 class QJsonObject;
@@ -326,6 +327,9 @@ public:
     // Write last opened files to [last_opened_files] of session.ini.
     void setLastOpenedFiles(const QVector<VFileSessionInfo> &p_files);
 
+    // Read custom magic words from [magic_words] section.
+    QVector<VMagicWord> getCustomMagicWords();
+
     // Return the configured key sequence of @p_operation.
     // Return empty if there is no corresponding config.
     QString getShortcutKeySequence(const QString &p_operation) const;

+ 5 - 4
src/veditarea.cpp

@@ -12,9 +12,10 @@
 extern VConfigManager *g_config;
 extern VNote *g_vnote;
 
-VEditArea::VEditArea(VNote *vnote, QWidget *parent)
-    : QWidget(parent), VNavigationMode(),
-      vnote(vnote), curWindowIndex(-1)
+VEditArea::VEditArea(QWidget *parent)
+    : QWidget(parent),
+      VNavigationMode(),
+      curWindowIndex(-1)
 {
     setupUI();
 
@@ -66,7 +67,7 @@ void VEditArea::setupUI()
 
 void VEditArea::insertSplitWindow(int idx)
 {
-    VEditWindow *win = new VEditWindow(vnote, this);
+    VEditWindow *win = new VEditWindow(this);
     splitter->insertWidget(idx, win);
     connect(win, &VEditWindow::tabStatusUpdated,
             this, &VEditArea::handleWindowTabStatusUpdated);

+ 1 - 3
src/veditarea.h

@@ -14,7 +14,6 @@
 #include "veditwindow.h"
 #include "vnavigationmode.h"
 
-class VNote;
 class VFile;
 class VDirectory;
 class VFindReplaceDialog;
@@ -25,7 +24,7 @@ class VEditArea : public QWidget, public VNavigationMode
 {
     Q_OBJECT
 public:
-    explicit VEditArea(VNote *vnote, QWidget *parent = 0);
+    explicit VEditArea(QWidget *parent = 0);
 
     // Whether @p_file has been opened in edit area.
     bool isFileOpened(const VFile *p_file);
@@ -161,7 +160,6 @@ private:
     // Update status of current window.
     void updateWindowStatus();
 
-    VNote *vnote;
     int curWindowIndex;
 
     // Splitter holding multiple split windows

+ 10 - 9
src/veditwindow.cpp

@@ -2,7 +2,6 @@
 #include <QtDebug>
 #include "veditwindow.h"
 #include "vedittab.h"
-#include "vnote.h"
 #include "utils/vutils.h"
 #include "vorphanfile.h"
 #include "vmainwindow.h"
@@ -13,12 +12,14 @@
 #include "vfilelist.h"
 #include "vconfigmanager.h"
 
-extern VNote *g_vnote;
 extern VConfigManager *g_config;
+extern VMainWindow *g_mainWin;
 
-VEditWindow::VEditWindow(VNote *vnote, VEditArea *editArea, QWidget *parent)
-    : QTabWidget(parent), vnote(vnote), m_editArea(editArea),
-      m_curTabWidget(NULL), m_lastTabWidget(NULL)
+VEditWindow::VEditWindow(VEditArea *editArea, QWidget *parent)
+    : QTabWidget(parent),
+      m_editArea(editArea),
+      m_curTabWidget(NULL),
+      m_lastTabWidget(NULL)
 {
     setAcceptDrops(true);
     initTabActions();
@@ -137,9 +138,9 @@ void VEditWindow::initTabActions()
                 Q_ASSERT(file);
                 if (file->getType() == FileType::Note) {
                     VNoteFile *tmpFile = dynamic_cast<VNoteFile *>((VFile *)file);
-                    g_vnote->getMainWindow()->getFileList()->fileInfo(tmpFile);
+                    g_mainWin->getFileList()->fileInfo(tmpFile);
                 } else if (file->getType() == FileType::Orphan) {
-                    g_vnote->getMainWindow()->editOrphanFileInfo(file);
+                    g_mainWin->editOrphanFileInfo(file);
                 }
             });
 
@@ -817,7 +818,7 @@ void VEditWindow::handleLocateAct()
     VEditTab *editor = getTab(tab);
     QPointer<VFile> file = editor->getFile();
     if (file->getType() == FileType::Note) {
-        vnote->getMainWindow()->locateFile(file);
+        g_mainWin->locateFile(file);
     }
 }
 
@@ -1022,7 +1023,7 @@ void VEditWindow::dropEvent(QDropEvent *p_event)
 
         if (!files.isEmpty()) {
             focusWindow();
-            g_vnote->getMainWindow()->openFiles(files);
+            g_mainWin->openFiles(files);
         }
 
         p_event->acceptProposedAction();

+ 1 - 3
src/veditwindow.h

@@ -11,7 +11,6 @@
 #include "vconstants.h"
 #include "vnotefile.h"
 
-class VNote;
 class QPushButton;
 class QActionGroup;
 class VEditArea;
@@ -20,7 +19,7 @@ class VEditWindow : public QTabWidget
 {
     Q_OBJECT
 public:
-    explicit VEditWindow(VNote *vnote, VEditArea *editArea, QWidget *parent = 0);
+    explicit VEditWindow(VEditArea *editArea, QWidget *parent = 0);
     int findTabByFile(const VFile *p_file) const;
     int openFile(VFile *p_file, OpenFileMode p_mode);
     bool closeFile(const VFile *p_file, bool p_forced);
@@ -160,7 +159,6 @@ private:
     // Connect the signals of VEditTab to this VEditWindow.
     void connectEditTab(const VEditTab *p_tab);
 
-    VNote *vnote;
     VEditArea *m_editArea;
 
     // These two members are only used for alternateTab().

+ 46 - 0
src/vlineedit.cpp

@@ -0,0 +1,46 @@
+#include "vlineedit.h"
+
+#include <QDebug>
+#include <QToolTip>
+
+#include "utils/vmetawordmanager.h"
+
+
+extern VMetaWordManager *g_mwMgr;
+
+VLineEdit::VLineEdit(QWidget *p_parent)
+    : QLineEdit(p_parent)
+{
+    init();
+}
+
+VLineEdit::VLineEdit(const QString &p_contents, QWidget *p_parent)
+    : QLineEdit(p_contents, p_parent)
+{
+    init();
+}
+
+void VLineEdit::handleTextChanged(const QString &p_text)
+{
+    m_evaluatedText = g_mwMgr->evaluate(p_text);
+    qDebug() << "evaluate text:" << m_evaluatedText;
+
+    if (m_evaluatedText == p_text) {
+        return;
+    }
+
+    // Display tooltip at bottom-left.
+    QPoint pos = mapToGlobal(QPoint(0, height()));
+    QToolTip::showText(pos, m_evaluatedText, this);
+}
+
+void VLineEdit::init()
+{
+    connect(this, &QLineEdit::textChanged,
+            this, &VLineEdit::handleTextChanged);
+}
+
+const QString VLineEdit::getEvaluatedText() const
+{
+    return m_evaluatedText;
+}

+ 29 - 0
src/vlineedit.h

@@ -0,0 +1,29 @@
+#ifndef VLINEEDIT_H
+#define VLINEEDIT_H
+
+#include <QLineEdit>
+
+
+// QLineEdit with meta word support.
+class VLineEdit : public QLineEdit
+{
+    Q_OBJECT
+public:
+    explicit VLineEdit(QWidget *p_parent = nullptr);
+
+    VLineEdit(const QString &p_contents, QWidget *p_parent = Q_NULLPTR);
+
+    // Return the evaluated text.
+    const QString getEvaluatedText() const;
+
+private slots:
+    void handleTextChanged(const QString &p_text);
+
+private:
+    void init();
+
+    // We should keep the evaluated text identical with what's displayed.
+    QString m_evaluatedText;
+};
+
+#endif // VLINEEDIT_H

+ 3 - 1
src/vmainwindow.cpp

@@ -45,6 +45,8 @@ VMainWindow::VMainWindow(VSingleInstanceGuard *p_guard, QWidget *p_parent)
     : QMainWindow(p_parent), m_guard(p_guard),
       m_windowOldState(Qt::WindowNoState), m_requestQuit(false)
 {
+    qsrand(QDateTime::currentDateTime().toTime_t());
+
     setWindowIcon(QIcon(":/resources/icons/vnote.ico"));
     vnote = new VNote(this);
     g_vnote = vnote;
@@ -110,7 +112,7 @@ void VMainWindow::setupUI()
     m_fileList = new VFileList();
     m_fileList->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
 
-    editArea = new VEditArea(vnote);
+    editArea = new VEditArea();
     editArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
     m_findReplaceDialog = editArea->getFindReplaceDialog();
     m_fileList->setEditArea(editArea);

+ 10 - 1
src/vnote.cpp

@@ -16,6 +16,10 @@
 
 extern VConfigManager *g_config;
 
+// Meta word manager.
+VMetaWordManager *g_mwMgr;
+
+
 QString VNote::s_markdownTemplate;
 QString VNote::s_markdownTemplatePDF;
 
@@ -53,10 +57,15 @@ const QString VNote::c_markdownGuideDocFile_en = ":/resources/docs/markdown_guid
 const QString VNote::c_markdownGuideDocFile_zh = ":/resources/docs/markdown_guide_zh.md";
 
 VNote::VNote(QObject *parent)
-    : QObject(parent), m_mainWindow(dynamic_cast<VMainWindow *>(parent))
+    : QObject(parent)
 {
     initTemplate();
+
     g_config->getNotebooks(m_notebooks, this);
+
+    m_metaWordMgr.init();
+
+    g_mwMgr = &m_metaWordMgr;
 }
 
 void VNote::initPalette(QPalette palette)

+ 4 - 8
src/vnote.h

@@ -12,11 +12,12 @@
 #include <QPalette>
 #include "vnotebook.h"
 #include "vconstants.h"
+#include "utils/vmetawordmanager.h"
 
-class VMainWindow;
 class VOrphanFile;
 class VNoteFile;
 
+
 class VNote : public QObject
 {
     Q_OBJECT
@@ -74,7 +75,6 @@ public:
     const QVector<QPair<QString, QString> > &getPalette() const;
     void initPalette(QPalette palette);
     QString getColorFromPalette(const QString &p_name) const;
-    VMainWindow *getMainWindow() const;
 
     QString getNavigationLabelStyle(const QString &p_str) const;
 
@@ -106,7 +106,8 @@ private:
     // Maintain all the notebooks. Other holder should use QPointer.
     QVector<VNotebook *> m_notebooks;
     QVector<QPair<QString, QString> > m_palette;
-    VMainWindow *m_mainWindow;
+
+    VMetaWordManager m_metaWordMgr;
 
     // Hold all external file: Orphan File.
     // Need to clean up periodly.
@@ -118,9 +119,4 @@ inline const QVector<QPair<QString, QString> >& VNote::getPalette() const
     return m_palette;
 }
 
-inline VMainWindow *VNote::getMainWindow() const
-{
-    return m_mainWindow;
-}
-
 #endif // VNOTE_H

+ 2 - 3
src/vpreviewpage.cpp

@@ -2,10 +2,9 @@
 
 #include <QDesktopServices>
 
-#include "vnote.h"
 #include "vmainwindow.h"
 
-extern VNote *g_vnote;
+extern VMainWindow *g_mainWin;
 
 VPreviewPage::VPreviewPage(QWidget *parent) : QWebEnginePage(parent)
 {
@@ -21,7 +20,7 @@ bool VPreviewPage::acceptNavigationRequest(const QUrl &p_url,
 
     if (p_url.isLocalFile()) {
         QString filePath = p_url.toLocalFile();
-        if (g_vnote->getMainWindow()->tryOpenInternalFile(filePath)) {
+        if (g_mainWin->tryOpenInternalFile(filePath)) {
             qDebug() << "internal notes jump" << filePath;
             return false;
         }