diff --git a/.travis_linux.sh b/.travis_linux.sh index c793b23c..f1acda01 100644 --- a/.travis_linux.sh +++ b/.travis_linux.sh @@ -1,11 +1,11 @@ #!/bin/bash project_dir=$(pwd) -# Install qt5.7 +# Install qt5.9 sudo add-apt-repository ppa:george-edison55/cmake-3.x -y -sudo add-apt-repository ppa:beineri/opt-qt571-trusty -y +sudo add-apt-repository ppa:beineri/opt-qt591-trusty -y sudo apt-get update -qq -sudo apt-get -y install qt57base qt57webengine qt57webchannel qt57svg qt57location qt57tools qt57translations +sudo apt-get -y install qt59base qt59webengine qt59webchannel qt59svg qt59location qt59tools qt59translations source /opt/qt*/bin/qt*-env.sh # Compile newer version fcitx-qt5 @@ -34,7 +34,7 @@ make -j$(nproc) && sudo make install sudo cp /usr/local/lib/libFcitxQt5DBusAddons.so* /opt/qt*/lib/ sudo cp /usr/local/lib/libFcitxQt5WidgetsAddons.so* /opt/qt*/lib/ -tree /opt/qt57/lib/ +tree /opt/qt59/lib/ cd ${project_dir} mkdir build @@ -53,9 +53,9 @@ INSTALL_ROOT=${project_dir}/build/dist make install ; tree dist/ mkdir -p ./dist/usr/plugins/iconengines mkdir -p ./dist/usr/plugins/imageformats mkdir -p ./dist/usr/plugins/platforminputcontexts -cp /opt/qt57/plugins/iconengines/* ./dist/usr/plugins/iconengines/ -cp /opt/qt57/plugins/imageformats/* ./dist/usr/plugins/imageformats/ -cp /opt/qt57/plugins/platforminputcontexts/* ./dist/usr/plugins/platforminputcontexts/ +cp /opt/qt59/plugins/iconengines/* ./dist/usr/plugins/iconengines/ +cp /opt/qt59/plugins/imageformats/* ./dist/usr/plugins/imageformats/ +cp /opt/qt59/plugins/platforminputcontexts/* ./dist/usr/plugins/platforminputcontexts/ # Copy other project files cp "${project_dir}/README.md" "dist/README.md" @@ -70,7 +70,7 @@ unset QTDIR; unset QT_PLUGIN_PATH ; unset LD_LIBRARY_PATH ./linuxdeployqt*.AppImage ./dist/usr/share/applications/*.desktop -bundle-non-qt-libs # Copy translations -cp /opt/qt57/translations/*_zh_CN.qm ./dist/usr/translations/ +cp /opt/qt59/translations/*_zh_CN.qm ./dist/usr/translations/ # Package it for the second time. ./linuxdeployqt*.AppImage ./dist/usr/share/applications/*.desktop -appimage diff --git a/.travis_macos.sh b/.travis_macos.sh index 4e76c9a4..39215947 100644 --- a/.travis_macos.sh +++ b/.travis_macos.sh @@ -2,8 +2,8 @@ project_dir=$(pwd) brew update > /dev/null -brew install qt@5.7 -QTDIR="/usr/local/opt/qt@5.7" +brew install qt@5.9 +QTDIR="/usr/local/opt/qt@5.9" PATH="$QTDIR/bin:$PATH" LDFLAGS=-L$QTDIR/lib CPPFLAGS=-I$QTDIR/include diff --git a/README.md b/README.md index 84de95a3..5242f992 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,8 @@ Utilizing Qt, VNote could run on **Linux**, **Windows**, and **macOS** (due to t - Supports infinite levels of folders; - Supports multiple tabs and splitting windows; - Supports [Mermaid](http://knsv.github.io/mermaid/), [Flowchart.js](http://flowchart.js.org/), and [MathJax](https://www.mathjax.org/); -- Supports HiDPI. +- Supports HiDPI; +- Supports attachments of notes. ![VNote Edit](screenshots/vnote_edit.gif) @@ -136,7 +137,7 @@ VNote also supports many other features, like: - Auto indent and auto list; # Build & Development -VNote needs Qt 5.7 or above to build. +VNote needs Qt 5.9.1 or above to build. 1. Clone & Init ``` @@ -145,15 +146,15 @@ VNote needs Qt 5.7 or above to build. git submodule update --init ``` 2. Download Qt & Have Fun -Download [Qt 5.7.0](http://info.qt.io/download-qt-for-application-development) and open `VNote.pro` as a project. +Download [Qt 5.9.1](http://info.qt.io/download-qt-for-application-development) and open `VNote.pro` as a project. ## Linux -If your distribution does not have Qt 5.7 or above, you need to add it from other sources. In Ubuntu, you could do this: +If your distribution does not have Qt 5.9.1 or above, you need to add it from other sources. In Ubuntu, you could do this: ``` -sudo add-apt-repository ppa:beineri/opt-qt571-trusty -y +sudo add-apt-repository ppa:beineri/opt-qt591-trusty -y sudo apt-get update -qq -sudo apt-get -y install qt57base qt57webengine qt57webchannel qt57svg qt57location qt57tools qt57translations +sudo apt-get -y install qt59base qt59webengine qt59webchannel qt59svg qt59location qt59tools qt59translations source /opt/qt*/bin/qt*-env.sh ``` @@ -174,13 +175,13 @@ For details, you could reference [.travis_linux.sh](.travis_linux.sh) in the sou If you prefer command line on macOS, you could follow these steps. 1. Install Xcode and Homebrew; -2. Install Qt5.7 via Homebrew: +2. Install Qt 5.9.1 via Homebrew: ``` - brew install qt@5.7 + brew install qt@5.9.1 ``` 3. In the project directory, create `build_macos.sh` like this: ```sh - QTDIR="/usr/local/opt/qt@5.7" + QTDIR="/usr/local/opt/qt@5.9.1" PATH="$QTDIR/bin:$PATH" LDFLAGS=-L$QTDIR/lib CPPFLAGS=-I$QTDIR/include @@ -199,7 +200,7 @@ If you prefer command line on macOS, you could follow these steps. 5. Now you got the bundle `path/to/project/build/src/VNote.app`. Enjoy yourself! # Dependencies -- [Qt 5.7](http://qt-project.org) (L-GPL v3) +- [Qt 5.9](http://qt-project.org) (L-GPL v3) - [PEG Markdown Highlight](http://hasseg.org/peg-markdown-highlight/) (MIT License) - [Hoedown 3.0.7](https://github.com/hoedown/hoedown/) (ISC License) - [Marked](https://github.com/chjj/marked) (MIT License) @@ -212,6 +213,7 @@ If you prefer command line on macOS, you could follow these steps. - [MathJax](https://www.mathjax.org/) (Apache-2.0) - [showdown](https://github.com/showdownjs/showdown) (Unknown) - [flowchart.js](https://github.com/adrai/flowchart.js) (MIT License) +- Icons made by a326703305@qq.com # License VNote is licensed under the [MIT license](http://opensource.org/licenses/MIT). diff --git a/README_zh.md b/README_zh.md index e7a149ef..33e46e56 100644 --- a/README_zh.md +++ b/README_zh.md @@ -54,8 +54,9 @@ VNote不是一个简单的Markdown编辑器。通过提供笔记管理功能,V - 支持Vim模式以及一系列强大的快捷键; - 支持无限层级的文件夹; - 支持多个标签页和窗口分割; -- 支持[Mermaid](http://knsv.github.io/mermaid/), [Flowchart.js](http://flowchart.js.org/) 和 [MathJax](https://www.mathjax.org/); +- 支持[Mermaid](http://knsv.github.io/mermaid/), [Flowchart.js](http://flowchart.js.org/) 和 [MathJax](https://www.mathjax.org/); - 支持高分辨率; +- 支持笔记附件。 ![VNote Edit](screenshots/vnote_edit.gif) @@ -137,7 +138,7 @@ VNote还支持其他很多的功能,比如: - 自动缩进和自动列表; # 构建与开发 -VNote需要5.7版本以上的Qt进行构建。 +VNote需要5.9.1版本以上的Qt进行构建。 1. 克隆代码仓库 ``` @@ -146,15 +147,15 @@ VNote需要5.7版本以上的Qt进行构建。 git submodule update --init ``` 2. 下载Qt -下载[Qt 5.7.0](http://info.qt.io/download-qt-for-application-development),导入`VNote.pro`创建一个工程。 +下载[Qt 5.9.1](http://info.qt.io/download-qt-for-application-development),导入`VNote.pro`创建一个工程。 ## Linux -如果您的Linux发行版不提供5.7以上版本的Qt,那么您需要从其他来源获取Qt。在Ubuntu中,您可以执行以下步骤: +如果您的Linux发行版不提供5.9.1以上版本的Qt,那么您需要从其他来源获取Qt。在Ubuntu中,您可以执行以下步骤: ``` -sudo add-apt-repository ppa:beineri/opt-qt571-trusty -y +sudo add-apt-repository ppa:beineri/opt-qt591-trusty -y sudo apt-get update -qq -sudo apt-get -y install qt57base qt57webengine qt57webchannel qt57svg qt57location qt57tools qt57translations +sudo apt-get -y install qt59base qt59webengine qt59webchannel qt59svg qt59location qt59tools qt59translations source /opt/qt*/bin/qt*-env.sh ``` @@ -175,15 +176,15 @@ sudo make install 在macOS下,您可以执行以下步骤来编译: 1. 安装Xcode和Homebrew: -2. 通过Homebrew安装Qt5.7: +2. 通过Homebrew安装Qt5.9.1: ``` - brew install qt@5.7 + brew install qt@5.9.1 ``` 3. 在VNote源码根目录下,新建一个文件`build_macos.sh`: ```sh - QTDIR="/usr/local/opt/qt@5.7" + QTDIR="/usr/local/opt/qt@5.9.1" PATH="$QTDIR/bin:$PATH" LDFLAGS=-L$QTDIR/lib CPPFLAGS=-I$QTDIR/include @@ -203,7 +204,7 @@ sudo make install 5. 此时得到VNote的Bundle `path/to/project/build/src/VNote.app`,打开即可。 # 依赖 -- [Qt 5.7](http://qt-project.org) (L-GPL v3) +- [Qt 5.9](http://qt-project.org) (L-GPL v3) - [PEG Markdown Highlight](http://hasseg.org/peg-markdown-highlight/) (MIT License) - [Hoedown 3.0.7](https://github.com/hoedown/hoedown/) (ISC License) - [Marked](https://github.com/chjj/marked) (MIT License) @@ -216,6 +217,7 @@ sudo make install - [MathJax](https://www.mathjax.org/) (Apache-2.0) - [showdown](https://github.com/showdownjs/showdown) (Unknown) - [flowchart.js](https://github.com/adrai/flowchart.js) (MIT License) +- 图标由九梦岛主(a326703305@qq.com)制作 # 代码许可 VNote使用[MIT许可](http://opensource.org/licenses/MIT)。 diff --git a/appveyor.yml b/appveyor.yml index 714a91e9..4c32fe15 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,9 +11,9 @@ environment: VSVER: 14 matrix: - - QT: C:\Qt\5.7\msvc2015_64 + - QT: C:\Qt\5.9.1\msvc2015_64 PLATFORM: amd64 - - QT: C:\Qt\5.7\msvc2015 + - QT: C:\Qt\5.9.1\msvc2015 PLATFORM: x86 clone_depth: 1 @@ -29,8 +29,8 @@ before_build: # After calling vcvarsall.bat, %PLATFORM% will be X64 or x86 - mkdir build - cd build - - if "%PLATFORM%" EQU "X64" (qmake -r -spec win32-msvc2015 CONFIG+=x86_64 CONFIG-=debug CONFIG+=release ../VNote.pro) - - if "%PLATFORM%" EQU "x86" (qmake -r -spec win32-msvc2015 CONFIG+=Win32 CONFIG-=debug CONFIG+=release ../VNote.pro) + - if "%PLATFORM%" EQU "X64" (qmake -r -spec win32-msvc CONFIG+=x86_64 CONFIG-=debug CONFIG+=release ../VNote.pro) + - if "%PLATFORM%" EQU "x86" (qmake -r -spec win32-msvc CONFIG+=Win32 CONFIG-=debug CONFIG+=release ../VNote.pro) # custom build scripts build_script: diff --git a/src/dialog/vdirinfodialog.cpp b/src/dialog/vdirinfodialog.cpp index 63a9d477..e67c98a3 100644 --- a/src/dialog/vdirinfodialog.cpp +++ b/src/dialog/vdirinfodialog.cpp @@ -2,6 +2,7 @@ #include "vdirinfodialog.h" #include "vdirectory.h" #include "vconfigmanager.h" +#include "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("WARNING: " + "Name (case-insensitive) %3 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("WARNING: " + "Name %3 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("WARNING: 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(); } diff --git a/src/dialog/vdirinfodialog.h b/src/dialog/vdirinfodialog.h index 558334df..d23a0211 100644 --- a/src/dialog/vdirinfodialog.h +++ b/src/dialog/vdirinfodialog.h @@ -4,7 +4,7 @@ #include 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; diff --git a/src/dialog/vfileinfodialog.cpp b/src/dialog/vfileinfodialog.cpp index 40373a87..4fd4a8a8 100644 --- a/src/dialog/vfileinfodialog.cpp +++ b/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("WARNING: Name (case-insensitive) already exists. " - "Please choose another name.") - .arg(g_config->c_warningTextStyle); - m_warnLabel->setText(nameConflictText); + warnText = tr("WARNING: " + "Name (case-insensitive) %3 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("WARNING: " + "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("WARNING: " + "Name %3 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("WARNING: 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(); } diff --git a/src/dialog/vfileinfodialog.h b/src/dialog/vfileinfodialog.h index 939cfcdf..015705f0 100644 --- a/src/dialog/vfileinfodialog.h +++ b/src/dialog/vfileinfodialog.h @@ -4,7 +4,7 @@ #include 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; diff --git a/src/dialog/vinsertimagedialog.cpp b/src/dialog/vinsertimagedialog.cpp index 9b0b090e..214d8258 100644 --- a/src/dialog/vinsertimagedialog.cpp +++ b/src/dialog/vinsertimagedialog.cpp @@ -3,6 +3,7 @@ #include #include "vinsertimagedialog.h" #include "utils/vutils.h" +#include "vlineedit.h" VInsertImageDialog::VInsertImageDialog(const QString &title, const QString &defaultImageTitle, const QString &defaultPath, QWidget *parent) @@ -11,11 +12,14 @@ 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::handleInputChanged); + connect(pathEdit, &QLineEdit::textChanged, + this, &VInsertImageDialog::handleInputChanged); + connect(browseBtn, &QPushButton::clicked, + this, &VInsertImageDialog::handleBrowseBtnClicked); - enableOkButton(); + handleInputChanged(); } VInsertImageDialog::~VInsertImageDialog() @@ -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,31 @@ void VInsertImageDialog::setupUI() mainLayout->setSizeConstraint(QLayout::SetFixedSize); setWindowTitle(title); - imageTitleEdit->setFocus(); + m_imageTitleEdit->setFocus(); } -void VInsertImageDialog::enableOkButton() +void VInsertImageDialog::handleInputChanged() { - bool enabled = true; - if (imageTitleEdit->text().isEmpty() || !image) { - enabled = false; + bool pathOk = true; + if (pathEdit->isVisible() && !pathEdit->isReadOnly()) { + QString path = pathEdit->text(); + if (path.isEmpty() + || !VUtils::checkPathLegal(path)) { + pathOk = false; + } } + + QString title = m_imageTitleEdit->getEvaluatedText(); + QRegExp reg(VUtils::c_imageTitleRegExp); + bool titleOk = reg.exactMatch(title); + QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok); - okBtn->setEnabled(enabled); + okBtn->setEnabled(pathOk && titleOk); } QString VInsertImageDialog::getImageTitleInput() const { - return imageTitleEdit->text(); + return m_imageTitleEdit->getEvaluatedText(); } QString VInsertImageDialog::getPathInput() const @@ -130,7 +143,8 @@ void VInsertImageDialog::setImage(const QImage &image) imagePreviewLabel->setPixmap(pixmap); imagePreviewLabel->setVisible(true); - enableOkButton(); + + handleInputChanged(); } void VInsertImageDialog::setBrowseable(bool browseable, bool visible) @@ -141,6 +155,8 @@ void VInsertImageDialog::setBrowseable(bool browseable, bool visible) pathLabel->setVisible(visible); pathEdit->setVisible(visible); browseBtn->setVisible(visible); + + handleInputChanged(); } void VInsertImageDialog::imageDownloaded(const QByteArray &data) diff --git a/src/dialog/vinsertimagedialog.h b/src/dialog/vinsertimagedialog.h index b16b7cb8..65855af0 100644 --- a/src/dialog/vinsertimagedialog.h +++ b/src/dialog/vinsertimagedialog.h @@ -8,6 +8,7 @@ class QLabel; class QLineEdit; +class VLineEdit; class QPushButton; class QDialogButtonBox; @@ -30,14 +31,14 @@ public slots: void imageDownloaded(const QByteArray &data); private slots: - void enableOkButton(); + void handleInputChanged(); void handleBrowseBtnClicked(); private: void setupUI(); QLabel *imageTitleLabel; - QLineEdit *imageTitleEdit; + VLineEdit *m_imageTitleEdit; QLabel *pathLabel; QLineEdit *pathEdit; QPushButton *browseBtn; diff --git a/src/dialog/vinsertlinkdialog.cpp b/src/dialog/vinsertlinkdialog.cpp new file mode 100644 index 00000000..866300d5 --- /dev/null +++ b/src/dialog/vinsertlinkdialog.cpp @@ -0,0 +1,132 @@ +#include "vinsertlinkdialog.h" + +#include + +#include "vlineedit.h" + +VInsertLinkDialog::VInsertLinkDialog(const QString &p_title, + const QString &p_text, + const QString &p_info, + const QString &p_linkText, + const QString &p_linkUrl, + QWidget *p_parent) + : QDialog(p_parent) +{ + setupUI(p_title, p_text, p_info, p_linkText, p_linkUrl); + + fetchLinkFromClipboard(); + + handleInputChanged(); +} + +void VInsertLinkDialog::setupUI(const QString &p_title, + const QString &p_text, + const QString &p_info, + const QString &p_linkText, + const QString &p_linkUrl) +{ + QLabel *textLabel = NULL; + if (!p_text.isEmpty()) { + textLabel = new QLabel(p_text); + textLabel->setWordWrap(true); + } + + QLabel *infoLabel = NULL; + if (!p_info.isEmpty()) { + infoLabel = new QLabel(p_info); + infoLabel->setWordWrap(true); + } + + m_linkTextEdit = new VLineEdit(p_linkText); + m_linkTextEdit->selectAll(); + + m_linkUrlEdit = new QLineEdit(p_linkUrl); + m_linkUrlEdit->setToolTip(tr("Absolute or relative path of the link")); + + QFormLayout *inputLayout = new QFormLayout(); + inputLayout->addRow(tr("&Text:"), m_linkTextEdit); + inputLayout->addRow(tr("&URL:"), m_linkUrlEdit); + + // Ok is the default button. + m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(m_btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok); + m_linkTextEdit->setMinimumWidth(okBtn->sizeHint().width() * 3); + + QVBoxLayout *mainLayout = new QVBoxLayout; + if (textLabel) { + mainLayout->addWidget(textLabel); + } + + if (infoLabel) { + mainLayout->addWidget(infoLabel); + } + + mainLayout->addLayout(inputLayout); + mainLayout->addWidget(m_btnBox); + + setLayout(mainLayout); + setWindowTitle(p_title); + + connect(m_linkTextEdit, &QLineEdit::textChanged, + this, &VInsertLinkDialog::handleInputChanged); + connect(m_linkUrlEdit, &QLineEdit::textChanged, + this, &VInsertLinkDialog::handleInputChanged); +} + +void VInsertLinkDialog::handleInputChanged() +{ + bool textOk = true; + if (m_linkTextEdit->getEvaluatedText().isEmpty()) { + textOk = false; + } + + bool urlOk = true; + if (m_linkUrlEdit->text().isEmpty()) { + urlOk = false; + } + + QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok); + okBtn->setEnabled(textOk && urlOk); +} + +void VInsertLinkDialog::fetchLinkFromClipboard() +{ + if (!m_linkUrlEdit->text().isEmpty() + || !m_linkTextEdit->text().isEmpty()) { + return; + } + + QClipboard *clipboard = QApplication::clipboard(); + const QMimeData *mimeData = clipboard->mimeData(); + + if (!mimeData->hasText()) { + return; + } + + QString text = mimeData->text(); + if (text.isEmpty()) { + return; + } + + QUrl url = QUrl::fromUserInput(text); + if (url.isValid()) { + if (m_linkUrlEdit->text().isEmpty()) { + m_linkUrlEdit->setText(text); + } + } else if (m_linkTextEdit->text().isEmpty()) { + m_linkTextEdit->setText(text); + } +} + +QString VInsertLinkDialog::getLinkText() const +{ + return m_linkTextEdit->getEvaluatedText(); +} + +QString VInsertLinkDialog::getLinkUrl() const +{ + return m_linkUrlEdit->text(); +} diff --git a/src/dialog/vinsertlinkdialog.h b/src/dialog/vinsertlinkdialog.h new file mode 100644 index 00000000..d31d8982 --- /dev/null +++ b/src/dialog/vinsertlinkdialog.h @@ -0,0 +1,46 @@ +#ifndef VINSERTLINKDIALOG_H +#define VINSERTLINKDIALOG_H + +#include +#include + +class VLineEdit; +class QLineEdit; +class QDialogButtonBox; + +class VInsertLinkDialog : public QDialog +{ + Q_OBJECT +public: + VInsertLinkDialog(const QString &p_title, + const QString &p_text, + const QString &p_info, + const QString &p_linkText, + const QString &p_linkUrl, + QWidget *p_parent = nullptr); + + QString getLinkText() const; + + QString getLinkUrl() const; + +private slots: + void handleInputChanged(); + +private: + void setupUI(const QString &p_title, + const QString &p_text, + const QString &p_info, + const QString &p_linkText, + const QString &p_linkUrl); + + // Infer link text/url from clipboard only when text and url are both empty. + void fetchLinkFromClipboard(); + + VLineEdit *m_linkTextEdit; + + QLineEdit *m_linkUrlEdit; + + QDialogButtonBox *m_btnBox; +}; + +#endif // VINSERTLINKDIALOG_H diff --git a/src/dialog/vnewdirdialog.cpp b/src/dialog/vnewdirdialog.cpp index 7a8bff59..24ad8ccc 100644 --- a/src/dialog/vnewdirdialog.cpp +++ b/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("WARNING: " + "Name (case-insensitive) %3 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("WARNING: " + "Name %3 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("WARNING: 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(); } diff --git a/src/dialog/vnewdirdialog.h b/src/dialog/vnewdirdialog.h index 30ca9727..8594c594 100644 --- a/src/dialog/vnewdirdialog.h +++ b/src/dialog/vnewdirdialog.h @@ -4,7 +4,7 @@ #include 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; diff --git a/src/dialog/vnewfiledialog.cpp b/src/dialog/vnewfiledialog.cpp index 6ef321ff..730728cc 100644 --- a/src/dialog/vnewfiledialog.cpp +++ b/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("WARNING: " + "Name (case-insensitive) %3 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("WARNING: " + "Name %3 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("WARNING: 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 diff --git a/src/dialog/vnewfiledialog.h b/src/dialog/vnewfiledialog.h index 2eeb8370..496e90df 100644 --- a/src/dialog/vnewfiledialog.h +++ b/src/dialog/vnewfiledialog.h @@ -4,7 +4,7 @@ #include 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; diff --git a/src/dialog/vnewnotebookdialog.cpp b/src/dialog/vnewnotebookdialog.cpp index f19d8ca8..f211bdc0 100644 --- a/src/dialog/vnewnotebookdialog.cpp +++ b/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 @@ -142,7 +146,8 @@ void VNewNotebookDialog::handleBrowseBtnClicked() } } - QString dirPath = QFileDialog::getExistingDirectory(this, tr("Select Root Folder Of The Notebook"), + QString dirPath = QFileDialog::getExistingDirectory(this, + tr("Select Root Folder Of The Notebook"), defaultPath, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); @@ -160,7 +165,7 @@ bool VNewNotebookDialog::isImportExistingNotebook() const void VNewNotebookDialog::showEvent(QShowEvent *event) { - nameEdit->setFocus(); + m_nameEdit->setFocus(); QDialog::showEvent(event); } @@ -182,7 +187,7 @@ void VNewNotebookDialog::handleInputChanged() m_manualPath = true; } - if (nameEdit->isModified()) { + if (m_nameEdit->isModified()) { m_manualName = true; } @@ -247,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. @@ -259,13 +264,29 @@ void VNewNotebookDialog::handleInputChanged() } } + QString warnText; if (idx < m_notebooks.size()) { nameOk = false; + warnText = tr("WARNING: " + "Name (case-insensitive) %3 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("WARNING: " + "Name %3 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("WARNING: Name (case-insensitive) already exists. " - "Please choose another name.") - .arg(g_config->c_warningTextStyle); - m_warnLabel->setText(nameConflictText); + m_warnLabel->setText(warnText); } } @@ -280,6 +301,8 @@ void VNewNotebookDialog::handleInputChanged() bool VNewNotebookDialog::autoComplete() { + QString nameText = m_nameEdit->getEvaluatedText(); + if (m_manualPath) { if (m_manualName) { return false; @@ -289,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; } } @@ -306,7 +329,6 @@ bool VNewNotebookDialog::autoComplete() } bool ret = false; - QString nameText = nameEdit->text(); if (nameText.isEmpty()) { if (m_manualName) { return false; @@ -314,8 +336,8 @@ bool VNewNotebookDialog::autoComplete() // Get a folder name under vnoteFolder and set it as the name of the notebook. QString name = "vnotebook"; - name = VUtils::getFileNameWithSequence(vnoteFolder, name); - nameEdit->setText(name); + name = VUtils::getDirNameWithSequence(vnoteFolder, name); + m_nameEdit->setText(name); ret = true; } else { // Use the name as the folder name under vnoteFolder. diff --git a/src/dialog/vnewnotebookdialog.h b/src/dialog/vnewnotebookdialog.h index d1f38105..87d0d9b8 100644 --- a/src/dialog/vnewnotebookdialog.h +++ b/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; diff --git a/src/dialog/vnotebookinfodialog.cpp b/src/dialog/vnotebookinfodialog.cpp index d52eec1d..f7e41eb2 100644 --- a/src/dialog/vnotebookinfodialog.cpp +++ b/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("WARNING: " + "Name (case-insensitive) %3 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("WARNING: " + "Name %3 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("WARNING: 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 diff --git a/src/dialog/vnotebookinfodialog.h b/src/dialog/vnotebookinfodialog.h index 68d02fbd..d1b4c234 100644 --- a/src/dialog/vnotebookinfodialog.h +++ b/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. diff --git a/src/dialog/vorphanfileinfodialog.cpp b/src/dialog/vorphanfileinfodialog.cpp index 579865d6..aa9a48af 100644 --- a/src/dialog/vorphanfileinfodialog.cpp +++ b/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); diff --git a/src/dialog/vsettingsdialog.cpp b/src/dialog/vsettingsdialog.cpp index e52f1ea5..5191f219 100644 --- a/src/dialog/vsettingsdialog.cpp +++ b/src/dialog/vsettingsdialog.cpp @@ -169,9 +169,6 @@ VGeneralTab::VGeneralTab(QWidget *p_parent) m_langCombo->addItem(lang.second, lang.first); } - QLabel *langLabel = new QLabel(tr("Language:"), this); - langLabel->setToolTip(m_langCombo->toolTip()); - // System tray checkbox. m_systemTray = new QCheckBox(tr("System tray"), this); m_systemTray->setToolTip(tr("Minimized to the system tray after closing VNote" @@ -181,9 +178,13 @@ VGeneralTab::VGeneralTab(QWidget *p_parent) m_systemTray->setEnabled(false); #endif + // Startup pages. + QLayout *startupLayout = setupStartupPagesLayout(); + QFormLayout *optionLayout = new QFormLayout(); - optionLayout->addRow(langLabel, m_langCombo); + optionLayout->addRow(tr("Language:"), m_langCombo); optionLayout->addRow(m_systemTray); + optionLayout->addRow(tr("Startup pages:"), startupLayout); QVBoxLayout *mainLayout = new QVBoxLayout(); mainLayout->addLayout(optionLayout); @@ -191,6 +192,60 @@ VGeneralTab::VGeneralTab(QWidget *p_parent) setLayout(mainLayout); } +QLayout *VGeneralTab::setupStartupPagesLayout() +{ + m_startupPageTypeCombo = new QComboBox(this); + m_startupPageTypeCombo->setToolTip(tr("Restore tabs or open specific notes on startup")); + m_startupPageTypeCombo->addItem(tr("None"), (int)StartupPageType::None); + m_startupPageTypeCombo->addItem(tr("Continue where you left off"), (int)StartupPageType::ContinueLeftOff); + m_startupPageTypeCombo->addItem(tr("Open specific pages"), (int)StartupPageType::SpecificPages); + connect(m_startupPageTypeCombo, static_cast(&QComboBox::activated), + this, [this](int p_index) { + int type = m_startupPageTypeCombo->itemData(p_index).toInt(); + bool pagesEditVisible = type == (int)StartupPageType::SpecificPages; + m_startupPagesEdit->setVisible(pagesEditVisible); + m_startupPagesAddBtn->setVisible(pagesEditVisible); + }); + + m_startupPagesEdit = new QPlainTextEdit(this); + m_startupPagesEdit->setToolTip(tr("Absolute path of the notes to open on startup (one note per line)")); + + m_startupPagesAddBtn = new QPushButton(tr("Browse"), this); + m_startupPagesAddBtn->setToolTip(tr("Select files to add as startup pages")); + connect(m_startupPagesAddBtn, &QPushButton::clicked, + this, [this]() { + static QString lastPath = QDir::homePath(); + QStringList files = QFileDialog::getOpenFileNames(this, + tr("Select Files As Startup Pages"), + lastPath); + if (files.isEmpty()) { + return; + } + + // Update lastPath + lastPath = QFileInfo(files[0]).path(); + + m_startupPagesEdit->appendPlainText(files.join("\n")); + }); + + QHBoxLayout *startupPagesBtnLayout = new QHBoxLayout(); + startupPagesBtnLayout->addStretch(); + startupPagesBtnLayout->addWidget(m_startupPagesAddBtn); + + QVBoxLayout *startupPagesLayout = new QVBoxLayout(); + startupPagesLayout->addWidget(m_startupPagesEdit); + startupPagesLayout->addLayout(startupPagesBtnLayout); + + QVBoxLayout *startupLayout = new QVBoxLayout(); + startupLayout->addWidget(m_startupPageTypeCombo); + startupLayout->addLayout(startupPagesLayout); + + m_startupPagesEdit->hide(); + m_startupPagesAddBtn->hide(); + + return startupLayout; +} + bool VGeneralTab::loadConfiguration() { if (!loadLanguage()) { @@ -201,6 +256,10 @@ bool VGeneralTab::loadConfiguration() return false; } + if (!loadStartupPageType()) { + return false; + } + return true; } @@ -214,6 +273,10 @@ bool VGeneralTab::saveConfiguration() return false; } + if (!saveStartupPageType()) { + return false; + } + return true; } @@ -264,6 +327,42 @@ bool VGeneralTab::saveSystemTray() return true; } +bool VGeneralTab::loadStartupPageType() +{ + StartupPageType type = g_config->getStartupPageType(); + bool found = false; + for (int i = 0; i < m_startupPageTypeCombo->count(); ++i) { + if (m_startupPageTypeCombo->itemData(i).toInt() == (int)type) { + found = true; + m_startupPageTypeCombo->setCurrentIndex(i); + } + } + + Q_ASSERT(found); + + const QStringList &pages = g_config->getStartupPages(); + m_startupPagesEdit->setPlainText(pages.join("\n")); + + bool pagesEditVisible = type == StartupPageType::SpecificPages; + m_startupPagesEdit->setVisible(pagesEditVisible); + m_startupPagesAddBtn->setVisible(pagesEditVisible); + + return true; +} + +bool VGeneralTab::saveStartupPageType() +{ + StartupPageType type = (StartupPageType)m_startupPageTypeCombo->currentData().toInt(); + g_config->setStartupPageType(type); + + if (type == StartupPageType::SpecificPages) { + QStringList pages = m_startupPagesEdit->toPlainText().split("\n"); + g_config->setStartupPages(pages); + } + + return true; +} + VReadEditTab::VReadEditTab(QWidget *p_parent) : QWidget(p_parent) { @@ -504,28 +603,34 @@ VMarkdownTab::VMarkdownTab(QWidget *p_parent) m_openModeCombo->addItem(tr("Read Mode"), (int)OpenFileMode::Read); m_openModeCombo->addItem(tr("Edit Mode"), (int)OpenFileMode::Edit); - QLabel *openModeLabel = new QLabel(tr("Note open mode:")); - openModeLabel->setToolTip(m_openModeCombo->toolTip()); - // Heading sequence. - m_headingSequence = new QCheckBox(tr("Heading sequence")); - m_headingSequence->setToolTip(tr("Enable auto sequence for all headings (in the form like 1.2.3.4.)")); - m_headingSequenceCombo = new QComboBox(); - m_headingSequenceCombo->setToolTip(tr("Base level to start heading sequence")); - m_headingSequenceCombo->addItem(tr("1"), 1); - m_headingSequenceCombo->addItem(tr("2"), 2); - m_headingSequenceCombo->addItem(tr("3"), 3); - m_headingSequenceCombo->addItem(tr("4"), 4); - m_headingSequenceCombo->addItem(tr("5"), 5); - m_headingSequenceCombo->addItem(tr("6"), 6); - m_headingSequenceCombo->setEnabled(false); - connect(m_headingSequence, &QCheckBox::stateChanged, - this, [this](int p_state){ - this->m_headingSequenceCombo->setEnabled(p_state == Qt::Checked); + m_headingSequenceTypeCombo = new QComboBox(); + m_headingSequenceTypeCombo->setToolTip(tr("Enable auto sequence for all headings (in the form like 1.2.3.4.)")); + m_headingSequenceTypeCombo->addItem(tr("Disabled"), (int)HeadingSequenceType::Disabled); + m_headingSequenceTypeCombo->addItem(tr("Enabled"), (int)HeadingSequenceType::Enabled); + m_headingSequenceTypeCombo->addItem(tr("Enabled for notes only"), (int)HeadingSequenceType::EnabledNoteOnly); + + m_headingSequenceLevelCombo = new QComboBox(); + m_headingSequenceLevelCombo->setToolTip(tr("Base level to start heading sequence")); + m_headingSequenceLevelCombo->addItem(tr("1"), 1); + m_headingSequenceLevelCombo->addItem(tr("2"), 2); + m_headingSequenceLevelCombo->addItem(tr("3"), 3); + m_headingSequenceLevelCombo->addItem(tr("4"), 4); + m_headingSequenceLevelCombo->addItem(tr("5"), 5); + m_headingSequenceLevelCombo->addItem(tr("6"), 6); + m_headingSequenceLevelCombo->setEnabled(false); + + connect(m_headingSequenceTypeCombo, static_cast(&QComboBox::activated), + this, [this](int p_index){ + if (p_index > -1) { + HeadingSequenceType type = (HeadingSequenceType)m_headingSequenceTypeCombo->itemData(p_index).toInt(); + m_headingSequenceLevelCombo->setEnabled(type != HeadingSequenceType::Disabled); + } }); + QHBoxLayout *headingSequenceLayout = new QHBoxLayout(); - headingSequenceLayout->addWidget(m_headingSequence); - headingSequenceLayout->addWidget(m_headingSequenceCombo); + headingSequenceLayout->addWidget(m_headingSequenceTypeCombo); + headingSequenceLayout->addWidget(m_headingSequenceLevelCombo); // Web Zoom Factor. m_customWebZoom = new QCheckBox(tr("Custom Web zoom factor"), this); @@ -553,8 +658,8 @@ VMarkdownTab::VMarkdownTab(QWidget *p_parent) colorColumnLabel->setToolTip(m_colorColumnEdit->toolTip()); QFormLayout *mainLayout = new QFormLayout(); - mainLayout->addRow(openModeLabel, m_openModeCombo); - mainLayout->addRow(headingSequenceLayout); + mainLayout->addRow(tr("Note open mode:"), m_openModeCombo); + mainLayout->addRow(tr("Heading sequence:"), headingSequenceLayout); mainLayout->addRow(zoomFactorLayout); mainLayout->addRow(colorColumnLabel, m_colorColumnEdit); @@ -628,21 +733,28 @@ bool VMarkdownTab::saveOpenMode() bool VMarkdownTab::loadHeadingSequence() { - bool enabled = g_config->getEnableHeadingSequence(); + HeadingSequenceType type = g_config->getHeadingSequenceType(); int level = g_config->getHeadingSequenceBaseLevel(); if (level < 1 || level > 6) { level = 1; } - m_headingSequence->setChecked(enabled); - m_headingSequenceCombo->setCurrentIndex(level - 1); + int idx = m_headingSequenceTypeCombo->findData((int)type); + Q_ASSERT(idx > -1); + m_headingSequenceTypeCombo->setCurrentIndex(idx); + m_headingSequenceLevelCombo->setCurrentIndex(level - 1); + m_headingSequenceLevelCombo->setEnabled(type != HeadingSequenceType::Disabled); + return true; } bool VMarkdownTab::saveHeadingSequence() { - g_config->setEnableHeadingSequence(m_headingSequence->isChecked()); - g_config->setHeadingSequenceBaseLevel(m_headingSequenceCombo->currentData().toInt()); + QVariant typeData = m_headingSequenceTypeCombo->currentData(); + Q_ASSERT(typeData.isValid()); + g_config->setHeadingSequenceType((HeadingSequenceType)typeData.toInt()); + g_config->setHeadingSequenceBaseLevel(m_headingSequenceLevelCombo->currentData().toInt()); + return true; } diff --git a/src/dialog/vsettingsdialog.h b/src/dialog/vsettingsdialog.h index d9b1eeeb..2406134f 100644 --- a/src/dialog/vsettingsdialog.h +++ b/src/dialog/vsettingsdialog.h @@ -13,6 +13,8 @@ class QCheckBox; class QLineEdit; class QStackedLayout; class QListWidget; +class QPlainTextEdit; +class QVBoxLayout; class VGeneralTab : public QWidget { @@ -23,18 +25,32 @@ public: bool saveConfiguration(); private: + QLayout *setupStartupPagesLayout(); + bool loadLanguage(); bool saveLanguage(); bool loadSystemTray(); bool saveSystemTray(); + bool loadStartupPageType(); + bool saveStartupPageType(); + // Language QComboBox *m_langCombo; // System tray QCheckBox *m_systemTray; + // Startup page type. + QComboBox *m_startupPageTypeCombo; + + // Startup pages. + QPlainTextEdit *m_startupPagesEdit; + + // Startup pages add files button. + QPushButton *m_startupPagesAddBtn; + static const QVector c_availableLangs; }; @@ -101,8 +117,8 @@ public: QComboBox *m_openModeCombo; // Whether enable heading sequence. - QCheckBox *m_headingSequence; - QComboBox *m_headingSequenceCombo; + QComboBox *m_headingSequenceTypeCombo; + QComboBox *m_headingSequenceLevelCombo; // Web zoom factor. QCheckBox *m_customWebZoom; diff --git a/src/main.cpp b/src/main.cpp index 98db9020..5bae6076 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,7 +13,6 @@ #include "vconfigmanager.h" VConfigManager *g_config; -VMainWindow *g_mainWin; #if defined(QT_NO_DEBUG) QFile g_logFile; @@ -116,22 +115,10 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); // The file path passed via command line arguments. - QStringList filePaths; - QStringList args = app.arguments(); - for (int i = 1; i < args.size(); ++i) { - if (QFileInfo::exists(args[i])) { - QString filePath = args[i]; - QFileInfo fi(filePath); - if (fi.isFile()) { - // Need to use absolute path here since VNote may be launched - // in different working directory. - filePath = QDir::cleanPath(fi.absoluteFilePath()); - filePaths.append(filePath); - } - } - } + QStringList filePaths = VUtils::filterFilePathsToOpen(app.arguments().mid(1)); - qDebug() << "command line arguments" << args; + qDebug() << "command line arguments" << app.arguments(); + qDebug() << "files to open from arguments" << filePaths; if (!canRun) { // Ask another instance to open files passed in. @@ -169,7 +156,6 @@ int main(int argc, char *argv[]) } VMainWindow w(&guard); - g_mainWin = &w; QString style = VUtils::readFileFromDisk(":/resources/vnote.qss"); if (!style.isEmpty()) { VUtils::processStyle(style, w.getPalette()); @@ -178,7 +164,11 @@ int main(int argc, char *argv[]) w.show(); - w.openExternalFiles(filePaths); + w.openStartupPages(); + + w.openFiles(filePaths); + + w.promptNewNotebookIfEmpty(); return app.exec(); } diff --git a/src/resources/docs/shortcuts_en.md b/src/resources/docs/shortcuts_en.md index 0942ac2f..5a0c2948 100644 --- a/src/resources/docs/shortcuts_en.md +++ b/src/resources/docs/shortcuts_en.md @@ -49,13 +49,17 @@ Save current changes and exit edit mode. #### Text Editing - `Ctrl+B` -Insert bold. Press `Ctrl+B` again to exit. Current selected text will be changed to bold if exist. +Insert bold. Press `Ctrl+B` again to exit. Current selected text will be changed to bold if exists. - `Ctrl+I` -Insert italic. Press `Ctrl+I` again to exit. Current selected text will be changed to italic if exist. +Insert italic. Press `Ctrl+I` again to exit. Current selected text will be changed to italic if exists. - `Ctrl+D` -Insert strikethrought. Press `Ctrl+D` again to exit. Current selected text will be changed to strikethrough if exist. +Insert strikethrought. Press `Ctrl+D` again to exit. Current selected text will be changed to strikethrough if exists. - `Ctrl+O` -Insert inline code. Press `Ctrl+O` again to exit. Current selected text will be changed to inline code if exist. +Insert inline code. Press `Ctrl+O` again to exit. Current selected text will be changed to inline code if exists. +- `Ctrl+M` +Insert fenced code block. Press `Ctrl+M` again to exit. Current selected text will be wrapped into a code block if exists. +- `Ctrl+L` +Insert link. - `Ctrl+H` Backspace. Delete a character backward. - `Ctrl+W` @@ -63,7 +67,7 @@ Delete all the characters from current cursor to the first space backward. - `Ctrl+U` Delete all the characters from current cursor to the beginning of current line. - `Ctrl+` -Insert title at level ``. `` should be 1 to 6. Current selected text will be changed to title if exist. +Insert title at level ``. `` should be 1 to 6. Current selected text will be changed to title if exists. - `Tab`/`Shift+Tab` Increase or decrease the indentation. If any text is selected, the indentation will operate on all these selected lines. - `Shift+Enter` @@ -80,42 +84,102 @@ Expand the selection to the beginning or end of current line. Expand the selection to the beginning or end of current note. ## Custom Shortcuts -VNote supports customing some standard shortcuts, though it is not recommended. VNote stores shortcuts' configuration information in the `[shortcuts]` section of user configuration file `vnote.ini`. +VNote supports customing some standard shortcuts, though it is not recommended. VNote stores shortcuts' configuration information in the `[shortcuts]` and `[captain_mode_shortcuts]` sections of user configuration file `vnote.ini`. For example, the default configruation may look like this: ```ini [shortcuts] -1\operation=NewNote -1\keysequence=Ctrl+N -2\operation=SaveNote -2\keysequence=Ctrl+S -3\operation=SaveAndRead -3\keysequence=Ctrl+T -4\operation=EditNote -4\keysequence=Ctrl+W -5\operation=CloseNote -5\keysequence= -6\operation=Find -6\keysequence=Ctrl+F -7\operation=FindNext -7\keysequence=F3 -8\operation=FindPrevious -8\keysequence=Shift+F3 -size=8 +; Define shortcuts here, with each item in the form "operation=keysequence". +; Leave keysequence empty to disable the shortcut of an operation. +; Custom shortcuts may conflict with some key bindings in edit mode or Vim mode. +; Ctrl+Q is reserved for quitting VNote. + +; Leader key of Captain mode +CaptainMode=Ctrl+E +; Create a note in current folder +NewNote=Ctrl+Alt+N +; Save current note +SaveNote=Ctrl+S +; Save changes and enter read mode +SaveAndRead=Ctrl+T +; Edit current note +EditNote=Ctrl+W +; Close current note +CloseNote= +; Open file/replace dialog +Find=Ctrl+F +; Find next occurence +FindNext=F3 +; Find previous occurence +FindPrevious=Shift+F3 + +[captain_mode_shortcuts] +; Define shortcuts in Captain mode here. +; There shortcuts are the sub-sequence after the CaptainMode key sequence +; in [shortcuts]. + +; Enter Navigation mode +NavigationMode=W +; Show attachment list of current note +AttachmentList=A +; Locate to the folder of current note +LocateCurrentFile=D +; Toggle Expand mode +ExpandMode=E +; Alternate one/two panels view +OnePanelView=P +; Discard changes and enter read mode +DiscardAndRead=Q +; Toggle Tools dock widget +ToolsDock=T +; Close current note +CloseNote=X +; Show shortcuts help document +ShortcutsHelp=? +; Flush the log file +FlushLogFile=";" +; Show opened files list +OpenedFileList=F +; Activate the ith tab +ActivateTab1=1 +ActivateTab2=2 +ActivateTab3=3 +ActivateTab4=4 +ActivateTab5=5 +ActivateTab6=6 +ActivateTab7=7 +ActivateTab8=8 +ActivateTab9=9 +; Alternate between current and last tab +AlternateTab=0 +; Activate next tab +ActivateNextTab=J +; Activate previous tab +ActivatePreviousTab=K +; Activate the window split on the left +ActivateSplitLeft=H +; Activate the window split on the right +ActivateSplitRight=L +; Move current tab one split left +MoveTabSplitLeft=Shift+H +; Move current tab one split right +MoveTabSplitRight=Shift+L +; Create a vertical split +VerticalSplit=V +; Remove current split +RemoveSplit=R ``` -`size=8` tells VNote that there are 8 shotcuts defined here, with each beginning with the number sequence. You could change the `keysequence` value to change the default key sequence of a specified operation. Leave the `keysequence` empty (`keysequence=`) to disable shortcut for that operation. +Each item is in the form `operation=keysequence`, with `keysequence` empty to disable shortcuts for that operation. -Pay attention that `Ctrl+E` is reserved for *Captain Mode* and `Ctrl+Q` is reserved for quitting VNote. +Pay attention that `Ctrl+Q` is reserved for quitting VNote. # Captain Mode To efficiently utilize the shortcuts, VNote supports the **Captain Mode**. Press the leader key `Ctrl+E`, then VNote will enter the Captain Mode, within which VNote supports more efficient shortcuts. -By the way, in this mode, `Ctrl+W` and `W` is equivalent, thus pressing `Ctrl+E+W` equals to `Ctrl+E W`. - - `E` Toggle expanding the edit area. - `P` diff --git a/src/resources/docs/shortcuts_zh.md b/src/resources/docs/shortcuts_zh.md index 926c0aba..adb72f1f 100644 --- a/src/resources/docs/shortcuts_zh.md +++ b/src/resources/docs/shortcuts_zh.md @@ -56,6 +56,10 @@ 插入删除线;再次按`Ctrl+D`退出。如果已经选择文本,则将当前选择文本改为删除线。 - `Ctrl+O` 插入行内代码;再次按`Ctrl+O`退出。如果已经选择文本,则将当前选择文本改为行内代码。 +- `Ctrl+M` +插入代码块;再次按`Ctrl+M`退出。如果已经选择文本,则将当前选择文本嵌入到代码块中。 +- `Ctrl+L` +插入链接。 - `Ctrl+H` 退格键,向前删除一个字符。 - `Ctrl+W` @@ -80,43 +84,103 @@ 扩展选定到笔记开始或结尾处。 ## 自定义快捷键 -VNote支持自定义部分标准快捷键(但并不建议这么做)。VNote将快捷键信息保存在用户配置文件`vnote.ini`中的`[shortcuts]`小节。 +VNote支持自定义部分标准快捷键(但并不建议这么做)。VNote将快捷键信息保存在用户配置文件`vnote.ini`中的`[shortcuts]`和`[captain_mode_shortcuts]`两个小节。 例如,默认的配置可能是这样子的: ```ini [shortcuts] -1\operation=NewNote -1\keysequence=Ctrl+N -2\operation=SaveNote -2\keysequence=Ctrl+S -3\operation=SaveAndRead -3\keysequence=Ctrl+T -4\operation=EditNote -4\keysequence=Ctrl+W -5\operation=CloseNote -5\keysequence= -6\operation=Find -6\keysequence=Ctrl+F -7\operation=FindNext -7\keysequence=F3 -8\operation=FindPrevious -8\keysequence=Shift+F3 -size=8 +; Define shortcuts here, with each item in the form "operation=keysequence". +; Leave keysequence empty to disable the shortcut of an operation. +; Custom shortcuts may conflict with some key bindings in edit mode or Vim mode. +; Ctrl+Q is reserved for quitting VNote. + +; Leader key of Captain mode +CaptainMode=Ctrl+E +; Create a note in current folder +NewNote=Ctrl+Alt+N +; Save current note +SaveNote=Ctrl+S +; Save changes and enter read mode +SaveAndRead=Ctrl+T +; Edit current note +EditNote=Ctrl+W +; Close current note +CloseNote= +; Open file/replace dialog +Find=Ctrl+F +; Find next occurence +FindNext=F3 +; Find previous occurence +FindPrevious=Shift+F3 + +[captain_mode_shortcuts] +; Define shortcuts in Captain mode here. +; There shortcuts are the sub-sequence after the CaptainMode key sequence +; in [shortcuts]. + +; Enter Navigation mode +NavigationMode=W +; Show attachment list of current note +AttachmentList=A +; Locate to the folder of current note +LocateCurrentFile=D +; Toggle Expand mode +ExpandMode=E +; Alternate one/two panels view +OnePanelView=P +; Discard changes and enter read mode +DiscardAndRead=Q +; Toggle Tools dock widget +ToolsDock=T +; Close current note +CloseNote=X +; Show shortcuts help document +ShortcutsHelp=? +; Flush the log file +FlushLogFile=";" +; Show opened files list +OpenedFileList=F +; Activate the ith tab +ActivateTab1=1 +ActivateTab2=2 +ActivateTab3=3 +ActivateTab4=4 +ActivateTab5=5 +ActivateTab6=6 +ActivateTab7=7 +ActivateTab8=8 +ActivateTab9=9 +; Alternate between current and last tab +AlternateTab=0 +; Activate next tab +ActivateNextTab=J +; Activate previous tab +ActivatePreviousTab=K +; Activate the window split on the left +ActivateSplitLeft=H +; Activate the window split on the right +ActivateSplitRight=L +; Move current tab one split left +MoveTabSplitLeft=Shift+H +; Move current tab one split right +MoveTabSplitRight=Shift+L +; Create a vertical split +VerticalSplit=V +; Remove current split +RemoveSplit=R ``` -`size=8` 告诉VNote这里定义了8组快捷键,每组快捷键都以一个数字序号开始。通过改变每组快捷键中`keysequence`的值来改变某个操作的默认快捷键。将`keysequence`设置为空(`keysequence=`)则会禁用该操作的任何快捷键。 +每一项配置的形式为`操作=按键序列`。如果`按键序列`为空,则表示禁用该操作的快捷键。 -注意,`Ctrl+E`保留作为*舰长模式*的前导键,`Ctrl+Q`保留为退出VNote。 +注意,`Ctrl+Q`保留为退出VNote。 # 舰长模式 为了更有效地利用快捷键,VNote支持 **舰长模式**。 按前导键`Ctrl+E`后,VNote会进入舰长模式。在舰长模式中,VNote会支持更多高效的快捷操作。 -另外,在该模式中,`Ctrl+W`和`W`是等效的,因此,可以`Ctrl+E+W`来实现`Ctrl+E W`的操作。 - - `E` 是否扩展编辑区域。 - `P` diff --git a/src/resources/icons/128x128/vnote.png b/src/resources/icons/128x128/vnote.png index 97d2d795..d325b223 100644 Binary files a/src/resources/icons/128x128/vnote.png and b/src/resources/icons/128x128/vnote.png differ diff --git a/src/resources/icons/16x16/vnote.png b/src/resources/icons/16x16/vnote.png index e3346ddb..184b5bf7 100644 Binary files a/src/resources/icons/16x16/vnote.png and b/src/resources/icons/16x16/vnote.png differ diff --git a/src/resources/icons/256x256/vnote.png b/src/resources/icons/256x256/vnote.png index a7bff7b1..69fee17d 100644 Binary files a/src/resources/icons/256x256/vnote.png and b/src/resources/icons/256x256/vnote.png differ diff --git a/src/resources/icons/32x32/vnote.png b/src/resources/icons/32x32/vnote.png index ed794c1e..155456b5 100644 Binary files a/src/resources/icons/32x32/vnote.png and b/src/resources/icons/32x32/vnote.png differ diff --git a/src/resources/icons/48x48/vnote.png b/src/resources/icons/48x48/vnote.png index 3716cb6c..d6d9a156 100644 Binary files a/src/resources/icons/48x48/vnote.png and b/src/resources/icons/48x48/vnote.png differ diff --git a/src/resources/icons/64x64/vnote.png b/src/resources/icons/64x64/vnote.png index a0c3eb01..e889ca0e 100644 Binary files a/src/resources/icons/64x64/vnote.png and b/src/resources/icons/64x64/vnote.png differ diff --git a/src/resources/icons/code_block.svg b/src/resources/icons/code_block.svg new file mode 100644 index 00000000..82e44a14 --- /dev/null +++ b/src/resources/icons/code_block.svg @@ -0,0 +1,6 @@ + + + Layer 1 + # + + diff --git a/src/resources/icons/compact_mode.svg b/src/resources/icons/compact_mode.svg new file mode 100644 index 00000000..6cc49aa2 --- /dev/null +++ b/src/resources/icons/compact_mode.svg @@ -0,0 +1,10 @@ + + + one_panel + + Layer 1 + + + + + diff --git a/src/resources/icons/heading_sequence.svg b/src/resources/icons/heading_sequence.svg new file mode 100644 index 00000000..b8014ac6 --- /dev/null +++ b/src/resources/icons/heading_sequence.svg @@ -0,0 +1,7 @@ + + + + Layer 2 + 1.2. + + diff --git a/src/resources/icons/link.svg b/src/resources/icons/link.svg new file mode 100644 index 00000000..97465186 --- /dev/null +++ b/src/resources/icons/link.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/src/resources/icons/vnote.icns b/src/resources/icons/vnote.icns index 7411d59f..5635a216 100644 Binary files a/src/resources/icons/vnote.icns and b/src/resources/icons/vnote.icns differ diff --git a/src/resources/icons/vnote.ico b/src/resources/icons/vnote.ico index f1b8f90b..a196a1a6 100644 Binary files a/src/resources/icons/vnote.ico and b/src/resources/icons/vnote.ico differ diff --git a/src/resources/icons/vnote.png b/src/resources/icons/vnote.png index 573d58fe..2c1ecb69 100644 Binary files a/src/resources/icons/vnote.png and b/src/resources/icons/vnote.png differ diff --git a/src/resources/icons/vnote.svg b/src/resources/icons/vnote.svg index 38ce5a55..8cd2fb03 100644 --- a/src/resources/icons/vnote.svg +++ b/src/resources/icons/vnote.svg @@ -1,17 +1 @@ - - - - vnote - - Layer 3 - - - - Layer 1 - - - - Layer 2 - - - \ No newline at end of file +vnote diff --git a/src/resources/icons/vnote_update.png b/src/resources/icons/vnote_update.png new file mode 100644 index 00000000..8e7b3f9c Binary files /dev/null and b/src/resources/icons/vnote_update.png differ diff --git a/src/resources/icons/vnote_update.svg b/src/resources/icons/vnote_update.svg index 34d5b17c..54bc77cf 100644 --- a/src/resources/icons/vnote_update.svg +++ b/src/resources/icons/vnote_update.svg @@ -1,17 +1 @@ - - - - vnote - - Layer 3 - - - - Layer 1 - - - - Layer 2 - - - \ No newline at end of file +vnote_cube diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index 564adf03..d6e42680 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -97,7 +97,8 @@ insert_title_from_note_name=true note_open_mode=0 ; Whether auto generate heading sequence -enable_heading_sequence=false +; 0 - Disabled, 1 - Enabled, 2 - Enabled only for notes +heading_sequence_type=0 ; Heading sequence base level heading_sequence_base_level=1 @@ -135,13 +136,26 @@ confirm_reload_folder=true ; Whether double click on a tab to close it double_click_close_tab=true +; Whether put folder and note panel in one vertical column +enable_compact_mode=false + +; Whether enable tools dock widget +tools_dock_checked=true + +; Pages to open on startup +; 0 - none; 1 - Continue where you left off; 2 - specific pages +startup_page_type=0 + +; Specific pages to open on startup when startup_page_type is 2 +; A list of file path separated by , +; Notice: should escape \ by \\ +; C:\users\vnote\vnote.md -> C:\\users\\vnote\\vnote.md +startup_pages= + [web] ; Location and configuration for Mathjax mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML -[session] -tools_dock_checked=true - [predefined_colors] 1\name=White 1\rgb=EEEEEE @@ -154,25 +168,84 @@ tools_dock_checked=true size=4 [shortcuts] -; Define shortcuts here, with each item in the form "operation->keysequence". +; Define shortcuts here, with each item in the form "operation=keysequence". ; Leave keysequence empty to disable the shortcut of an operation. ; Custom shortcuts may conflict with some key bindings in edit mode or Vim mode. -; Ctrl+E is reserved for Captain Mode. ; Ctrl+Q is reserved for quitting VNote. -1\operation=NewNote -1\keysequence=Ctrl+Alt+N -2\operation=SaveNote -2\keysequence=Ctrl+S -3\operation=SaveAndRead -3\keysequence=Ctrl+T -4\operation=EditNote -4\keysequence=Ctrl+W -5\operation=CloseNote -5\keysequence= -6\operation=Find -6\keysequence=Ctrl+F -7\operation=FindNext -7\keysequence=F3 -8\operation=FindPrevious -8\keysequence=Shift+F3 -size=8 + +; Leader key of Captain mode +CaptainMode=Ctrl+E +; Create a note in current folder +NewNote=Ctrl+Alt+N +; Save current note +SaveNote=Ctrl+S +; Save changes and enter read mode +SaveAndRead=Ctrl+T +; Edit current note +EditNote=Ctrl+W +; Close current note +CloseNote= +; Open file/replace dialog +Find=Ctrl+F +; Find next occurence +FindNext=F3 +; Find previous occurence +FindPrevious=Shift+F3 + +[captain_mode_shortcuts] +; Define shortcuts in Captain mode here. +; There shortcuts are the sub-sequence after the CaptainMode key sequence +; in [shortcuts]. + +; Enter Navigation mode +NavigationMode=W +; Show attachment list of current note +AttachmentList=A +; Locate to the folder of current note +LocateCurrentFile=D +; Toggle Expand mode +ExpandMode=E +; Alternate one/two panels view +OnePanelView=P +; Discard changes and enter read mode +DiscardAndRead=Q +; Toggle Tools dock widget +ToolsDock=T +; Close current note +CloseNote=X +; Show shortcuts help document +ShortcutsHelp=? +; Flush the log file +FlushLogFile=";" +; Show opened files list +OpenedFileList=F +; Activate the ith tab +ActivateTab1=1 +ActivateTab2=2 +ActivateTab3=3 +ActivateTab4=4 +ActivateTab5=5 +ActivateTab6=6 +ActivateTab7=7 +ActivateTab8=8 +ActivateTab9=9 +; Alternate between current and last tab +AlternateTab=0 +; Activate next tab +ActivateNextTab=J +; Activate previous tab +ActivatePreviousTab=K +; Activate the window split on the left +ActivateSplitLeft=H +; Activate the window split on the right +ActivateSplitRight=L +; Move current tab one split left +MoveTabSplitLeft=Shift+H +; Move current tab one split right +MoveTabSplitRight=Shift+L +; Create a vertical split +VerticalSplit=V +; Remove current split +RemoveSplit=R +; Evaluate selected text or cursor word as magic words +MagicWord=M diff --git a/src/resources/vnote.qss b/src/resources/vnote.qss index 059d3125..45ad190e 100644 --- a/src/resources/vnote.qss +++ b/src/resources/vnote.qss @@ -9,11 +9,11 @@ QPushButton[CornerBtn="true"]::menu-indicator { image: none; } -QPushButton[CornerBtn="true"]::hover { +QPushButton[CornerBtn="true"]:hover { background-color: @hover-color; } -QPushButton[CornerBtn="true"]::focus { +QPushButton[CornerBtn="true"]:focus { background-color: @focus-color; } @@ -25,11 +25,11 @@ QPushButton[StatusBtn="true"] { background-color: transparent; } -QPushButton[StatusBtn="true"]::hover { +QPushButton[StatusBtn="true"]:hover { background-color: @hover-color; } -QPushButton[StatusBtn="true"]::focus { +QPushButton[StatusBtn="true"]:focus { background-color: @focus-color; } @@ -40,11 +40,11 @@ QPushButton[FlatBtn="true"] { background-color: transparent; } -QPushButton[FlatBtn="true"]::hover { +QPushButton[FlatBtn="true"]:hover { background-color: @hover-color; } -QPushButton[FlatBtn="true"]::focus { +QPushButton[FlatBtn="true"]:focus { background-color: @focus-color; } @@ -56,11 +56,11 @@ QPushButton[SelectionBtn="true"] { text-align: left; } -QPushButton[SelectionBtn="true"]::hover { +QPushButton[SelectionBtn="true"]:hover { background-color: @hover-color; } -QPushButton[SelectionBtn="true"]::focus { +QPushButton[SelectionBtn="true"]:focus { background-color: @focus-color; } @@ -71,11 +71,11 @@ QPushButton[TitleBtn="true"] { background-color: @base-color; } -QPushButton[TitleBtn="true"]::hover { +QPushButton[TitleBtn="true"]:hover { background-color: @hover-color; } -QPushButton[TitleBtn="true"]::focus { +QPushButton[TitleBtn="true"]:focus { background-color: @focus-color; } @@ -85,7 +85,7 @@ QPushButton[DangerBtn="true"] { background-color: #d9534f; } -QPushButton[DangerBtn="true"]::hover { +QPushButton[DangerBtn="true"]:hover { color: #fff; border-color: #ac2925; background-color: #c9302c; @@ -95,10 +95,14 @@ QToolBar { border: none; } -QToolButton::hover { +QToolButton:hover { background-color: @hover-color; } +/* Override default shift behavior */ +QToolButton::menu-arrow:open { +} + QMenuBar { border: none; } @@ -141,7 +145,7 @@ QComboBox#NotebookSelector { icon-size: 30px; } -QComboBox#NotebookSelector::focus { +QComboBox#NotebookSelector:focus { background-color: @focus-color; } diff --git a/src/src.pro b/src/src.pro index 39730061..4dcc8d70 100644 --- a/src/src.pro +++ b/src/src.pro @@ -44,7 +44,6 @@ SOURCES += main.cpp\ veditwindow.cpp \ vedittab.cpp \ voutline.cpp \ - vtoc.cpp \ vsingleinstanceguard.cpp \ vdirectory.cpp \ vfile.cpp \ @@ -77,7 +76,12 @@ SOURCES += main.cpp\ dialog/vconfirmdeletiondialog.cpp \ vnotefile.cpp \ vattachmentlist.cpp \ - dialog/vsortdialog.cpp + dialog/vsortdialog.cpp \ + vfilesessioninfo.cpp \ + vtableofcontent.cpp \ + utils/vmetawordmanager.cpp \ + vlineedit.cpp \ + dialog/vinsertlinkdialog.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -107,7 +111,6 @@ HEADERS += vmainwindow.h \ veditwindow.h \ vedittab.h \ voutline.h \ - vtoc.h \ vsingleinstanceguard.h \ vdirectory.h \ vfile.h \ @@ -142,7 +145,12 @@ HEADERS += vmainwindow.h \ dialog/vconfirmdeletiondialog.h \ vnotefile.h \ vattachmentlist.h \ - dialog/vsortdialog.h + dialog/vsortdialog.h \ + vfilesessioninfo.h \ + vtableofcontent.h \ + utils/vmetawordmanager.h \ + vlineedit.h \ + dialog/vinsertlinkdialog.h RESOURCES += \ vnote.qrc \ diff --git a/src/utils/veditutils.cpp b/src/utils/veditutils.cpp index 5152e855..36d08495 100644 --- a/src/utils/veditutils.cpp +++ b/src/utils/veditutils.cpp @@ -48,7 +48,7 @@ bool VEditUtils::insertBlockWithIndent(QTextCursor &p_cursor) { V_ASSERT(!p_cursor.hasSelection()); p_cursor.insertBlock(); - return indentBlockAsPreviousBlock(p_cursor); + return indentBlockAsBlock(p_cursor, false); } bool VEditUtils::insertListMarkAsPreviousBlock(QTextCursor &p_cursor) @@ -85,21 +85,16 @@ bool VEditUtils::insertListMarkAsPreviousBlock(QTextCursor &p_cursor) } -bool VEditUtils::indentBlockAsPreviousBlock(QTextCursor &p_cursor) +bool VEditUtils::indentBlockAsBlock(QTextCursor &p_cursor, bool p_next) { bool changed = false; QTextBlock block = p_cursor.block(); - if (block.blockNumber() == 0) { - // The first block. + QTextBlock refBlock = p_next ? block.next() : block.previous(); + if (!refBlock.isValid()) { return false; } - QTextBlock preBlock = block.previous(); - QString text = preBlock.text(); - QRegExp regExp("(^\\s*)"); - regExp.indexIn(text); - V_ASSERT(regExp.captureCount() == 1); - QString leadingSpaces = regExp.capturedTexts()[1]; + QString leadingSpaces = fetchIndentSpaces(refBlock); moveCursorFirstNonSpaceCharacter(p_cursor, QTextCursor::MoveAnchor); if (!p_cursor.atBlockStart()) { @@ -689,3 +684,130 @@ bool VEditUtils::needToCancelAutoIndent(int p_autoIndentPos, const QTextCursor & return false; } + +void VEditUtils::insertTitleMark(QTextCursor &p_cursor, + const QTextBlock &p_block, + int p_level) +{ + if (!p_block.isValid()) { + return; + } + + Q_ASSERT(p_level >= 1 && p_level <= 6); + + bool needInsert = true; + + p_cursor.setPosition(p_block.position()); + + // Test if this block contains title marks. + QRegExp headerReg(VUtils::c_headerRegExp); + QString text = p_block.text(); + bool matched = headerReg.exactMatch(text); + if (matched) { + int level = headerReg.cap(1).length(); + if (level == p_level) { + needInsert = false; + } else { + // Remove the title mark. + p_cursor.movePosition(QTextCursor::NextCharacter, + QTextCursor::KeepAnchor, + level); + p_cursor.removeSelectedText(); + } + } + + // Insert titleMark + " " at the front of the block. + if (needInsert) { + // Remove the spaces at front. + // insertText() will remove the selection. + moveCursorFirstNonSpaceCharacter(p_cursor, QTextCursor::KeepAnchor); + + // Insert. + const QString titleMark(p_level, '#'); + p_cursor.insertText(titleMark + " "); + } + + // Go to the end of this block. + p_cursor.movePosition(QTextCursor::EndOfBlock); +} + +void VEditUtils::findCurrentWord(QTextCursor p_cursor, + int &p_start, + int &p_end) +{ + QString text = p_cursor.block().text(); + int pib = p_cursor.positionInBlock(); + + if (pib < text.size() && text[pib].isSpace()) { + p_start = p_end = p_cursor.position(); + return; + } + + p_cursor.movePosition(QTextCursor::StartOfWord); + p_start = p_cursor.position(); + p_cursor.movePosition(QTextCursor::EndOfWord); + p_end = p_cursor.position(); +} + +void VEditUtils::findCurrentWORD(const QTextCursor &p_cursor, + int &p_start, + int &p_end) +{ + QTextBlock block = p_cursor.block(); + QString text = block.text(); + int pib = p_cursor.positionInBlock(); + + if (pib < text.size() && text[pib].isSpace()) { + p_start = p_end = p_cursor.position(); + return; + } + + // Find the start. + p_start = 0; + for (int i = pib - 1; i >= 0; --i) { + if (text[i].isSpace()) { + p_start = i + 1; + break; + } + } + + // Find the end. + p_end = block.length() - 1; + for (int i = pib; i < text.size(); ++i) { + if (text[i].isSpace()) { + p_end = i; + break; + } + } + + p_start += block.position(); + p_end += block.position(); +} + +QString VEditUtils::fetchIndentSpaces(const QTextBlock &p_block) +{ + QString text = p_block.text(); + QRegExp regExp("(^\\s*)"); + regExp.indexIn(text); + Q_ASSERT(regExp.captureCount() == 1); + return regExp.capturedTexts()[1]; +} + +void VEditUtils::insertBlock(QTextCursor &p_cursor, + bool p_above) +{ + p_cursor.movePosition(p_above ? QTextCursor::StartOfBlock + : QTextCursor::EndOfBlock, + QTextCursor::MoveAnchor, + 1); + + p_cursor.insertBlock(); + + if (p_above) { + p_cursor.movePosition(QTextCursor::PreviousBlock, + QTextCursor::MoveAnchor, + 1); + } + + p_cursor.movePosition(QTextCursor::EndOfBlock); +} diff --git a/src/utils/veditutils.h b/src/utils/veditutils.h index bae56cca..b630180d 100644 --- a/src/utils/veditutils.h +++ b/src/utils/veditutils.h @@ -24,10 +24,11 @@ public: // Need to call setTextCursor() to make it take effect. static void moveCursorFirstNonSpaceCharacter(QTextCursor &p_cursor, QTextCursor::MoveMode p_mode); - // Indent current block as previous block. + // Indent current block as next/previous block. // Return true if some changes have been made. // @p_cursor will be placed at the position after inserting leading spaces. - static bool indentBlockAsPreviousBlock(QTextCursor &p_cursor); + // @p_next: indent as next block or previous block. + static bool indentBlockAsBlock(QTextCursor &p_cursor, bool p_next); // Returns true if two blocks has the same indent. static bool hasSameIndent(const QTextBlock &p_blocka, const QTextBlock &p_blockb); @@ -132,7 +133,38 @@ public: // Check if we need to cancel auto indent. // @p_autoIndentPos: the position of the cursor after auto indent. - static bool needToCancelAutoIndent(int p_autoIndentPos, const QTextCursor &p_cursor); + static bool needToCancelAutoIndent(int p_autoIndentPos, + const QTextCursor &p_cursor); + + // Insert title Mark at level @p_level in front of block @p_block + // If there already exists title marks, remove it first. + // Move cursor at the end of the block after insertion. + static void insertTitleMark(QTextCursor &p_cursor, + const QTextBlock &p_block, + int p_level); + + // Find the start and end of the word @p_cursor locates in (within a single block). + // @p_start and @p_end will be the global position of the start and end of the word. + // @p_start will equals to @p_end if @p_cursor is a space. + static void findCurrentWord(QTextCursor p_cursor, + int &p_start, + int &p_end); + + // Find the start and end of the WORD @p_cursor locates in (within a single block). + // @p_start and @p_end will be the global position of the start and end of the WORD. + // @p_start will equals to @p_end if @p_cursor is a space. + // Attention: www|sss will select www, which is different from findCurrentWord(). + static void findCurrentWORD(const QTextCursor &p_cursor, + int &p_start, + int &p_end); + + // Return the leading spaces of @p_block. + static QString fetchIndentSpaces(const QTextBlock &p_block); + + // Insert a block above/below current block. Move the cursor to the start of + // the new block after insertion. + static void insertBlock(QTextCursor &p_cursor, + bool p_above); private: VEditUtils() {} diff --git a/src/utils/vmetawordmanager.cpp b/src/utils/vmetawordmanager.cpp new file mode 100644 index 00000000..66cd4a67 --- /dev/null +++ b/src/utils/vmetawordmanager.cpp @@ -0,0 +1,596 @@ +#include "vmetawordmanager.h" + +#include +#include +#include +#include + +#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 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 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(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, + nullptr, + true); + 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, + bool p_allowAllSpaces) + : 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(p_allowAllSpaces); +} + +bool VMetaWord::checkType(MetaWordType p_type) +{ + return m_type == p_type; +} + +void VMetaWord::checkAndParseDefinition(bool p_allowAllSpaces) +{ + 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) && !p_allowAllSpaces) { + 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 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::parseToTokens(const QString &p_text) +{ + QVector 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 &p_tokens, + QHash &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; +} diff --git a/src/utils/vmetawordmanager.h b/src/utils/vmetawordmanager.h new file mode 100644 index 00000000..cfba310b --- /dev/null +++ b/src/utils/vmetawordmanager.h @@ -0,0 +1,205 @@ +#ifndef VMETAWORDMANAGER_H +#define VMETAWORDMANAGER_H + +#include + +#include +#include +#include +#include +#include + + +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 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 p_allowAllSpaces = false); + + 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. + // @p_allowAllSpaces: if true then we allow all spaces including \n and \t + // to appear in the definition. + void checkAndParseDefinition(bool p_allowAllSpaces); + + // Parse @p_text to a list of tokens. + static QVector 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 &p_tokens, + QHash &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 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 &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 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 &VMetaWordManager::getAllMetaWords() const +{ + return m_metaWords; +} + +#endif // VMETAWORDMANAGER_H diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 4a22144a..92dab9b6 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -33,6 +33,8 @@ QVector> 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*[^`]*$"); @@ -43,7 +45,7 @@ const QString VUtils::c_previewImageBlockRegExp = QString("[\\n|^][ |\\t]*\\xfff const QString VUtils::c_headerRegExp = QString("^(#{1,6})\\s+(((\\d+\\.)+(?=\\s))?\\s?\\S.*)\\s*$"); -const QString VUtils::c_headerPrefixRegExp = QString("^(#{1,6}\\s+((\\d+\\.)+(?=\\s))?\\s?)\\S.*\\s*$"); +const QString VUtils::c_headerPrefixRegExp = QString("^(#{1,6}\\s+((\\d+\\.)+(?=\\s))?\\s?)($|\\S.*\\s*$)"); void VUtils::initAvailableLanguage() { @@ -104,10 +106,10 @@ QRgb VUtils::QRgbFromString(const QString &str) return QRgb(); } -QString VUtils::generateImageFileName(const QString &path, const QString &title, +QString VUtils::generateImageFileName(const QString &path, + const QString &title, const QString &format) { - Q_ASSERT(!title.isEmpty()); QRegExp regExp("\\W"); QString baseName(title.toLower()); @@ -117,7 +119,9 @@ QString VUtils::generateImageFileName(const QString &path, const QString &title, // Constrain the length of the name. baseName.truncate(10); - baseName.prepend('_'); + if (!baseName.isEmpty()) { + baseName.prepend('_'); + } // Add current time and random number to make the name be most likely unique baseName = baseName + '_' + QString::number(QDateTime::currentDateTime().toTime_t()); @@ -377,31 +381,35 @@ int VUtils::showMessage(QMessageBox::Icon p_icon, const QString &p_title, const return msgBox.exec(); } -QString VUtils::generateCopiedFileName(const QString &p_dirPath, const QString &p_fileName) +QString VUtils::generateCopiedFileName(const QString &p_dirPath, + const QString &p_fileName, + bool p_completeBaseName) { - QString suffix; - QString base = p_fileName; - int dotIdx = p_fileName.lastIndexOf('.'); - if (dotIdx != -1) { - // .md - suffix = p_fileName.right(p_fileName.size() - dotIdx); - base = p_fileName.left(dotIdx); + QDir dir(p_dirPath); + if (!dir.exists() || !dir.exists(p_fileName)) { + return p_fileName; } - QDir dir(p_dirPath); - QString name = p_fileName; + QFileInfo fi(p_fileName); + QString baseName = p_completeBaseName ? fi.completeBaseName() : fi.baseName(); + QString suffix = p_completeBaseName ? fi.suffix() : fi.completeSuffix(); + int index = 0; - while (dir.exists(name)) { + QString fileName; + do { QString seq; if (index > 0) { - seq = QString::number(index); + seq = QString("%1").arg(QString::number(index), 3, '0'); } index++; - name = QString("%1_copy%2%3").arg(base).arg(seq).arg(suffix); - } + fileName = QString("%1_copy%2").arg(baseName).arg(seq); + if (!suffix.isEmpty()) { + fileName = fileName + "." + suffix; + } + } while (fileExists(dir, fileName, true)); - return name; + return fileName; } QString VUtils::generateCopiedDirName(const QString &p_parentDirPath, const QString &p_dirName) @@ -614,7 +622,8 @@ QString VUtils::generateHtmlTemplate(MarkdownConverterType p_conType, bool p_exp } QString VUtils::getFileNameWithSequence(const QString &p_directory, - const QString &p_baseFileName) + const QString &p_baseFileName, + bool p_completeBaseName) { QDir dir(p_directory); if (!dir.exists() || !dir.exists(p_baseFileName)) { @@ -623,8 +632,8 @@ QString VUtils::getFileNameWithSequence(const QString &p_directory, // Append a sequence. QFileInfo fi(p_baseFileName); - QString baseName = fi.baseName(); - QString suffix = fi.completeSuffix(); + QString baseName = p_completeBaseName ? fi.completeBaseName() : fi.baseName(); + QString suffix = p_completeBaseName ? fi.suffix() : fi.completeSuffix(); int seq = 1; QString fileName; do { @@ -637,6 +646,24 @@ QString VUtils::getFileNameWithSequence(const QString &p_directory, return fileName; } +QString VUtils::getDirNameWithSequence(const QString &p_directory, + const QString &p_baseDirName) +{ + QDir dir(p_directory); + if (!dir.exists() || !dir.exists(p_baseDirName)) { + return p_baseDirName; + } + + // Append a sequence. + int seq = 1; + QString fileName; + do { + fileName = QString("%1_%2").arg(p_baseDirName).arg(QString::number(seq++), 3, '0'); + } while (fileExists(dir, fileName, true)); + + return fileName; +} + QString VUtils::getRandomFileName(const QString &p_directory) { Q_ASSERT(!p_directory.isEmpty()); @@ -702,6 +729,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); @@ -850,7 +887,8 @@ bool VUtils::deleteFile(const QString &p_recycleBinFolderPath, } QString destName = getFileNameWithSequence(binPath, - fileNameFromPath(p_path)); + fileNameFromPath(p_path), + true); qDebug() << "try to move" << p_path << "to" << binPath << "as" << destName; if (!binDir.rename(p_path, binDir.filePath(destName))) { @@ -935,3 +973,30 @@ void VUtils::addErrMsg(QString *p_msg, const QString &p_str) *p_msg = *p_msg + '\n' + p_str; } } + +QStringList VUtils::filterFilePathsToOpen(const QStringList &p_files) +{ + QStringList paths; + for (int i = 0; i < p_files.size(); ++i) { + QString path = validFilePathToOpen(p_files[i]); + if (!path.isEmpty()) { + paths.append(path); + } + } + + return paths; +} + +QString VUtils::validFilePathToOpen(const QString &p_file) +{ + if (QFileInfo::exists(p_file)) { + QFileInfo fi(p_file); + if (fi.isFile()) { + // Need to use absolute path here since VNote may be launched + // in different working directory. + return QDir::cleanPath(fi.absoluteFilePath()); + } + } + + return QString(); +} diff --git a/src/utils/vutils.h b/src/utils/vutils.h index d0886dde..e21a580f 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -20,6 +20,32 @@ class VNotebook; #define V_ASSERT(cond) ((!(cond)) ? qt_assert(#cond, __FILE__, __LINE__) : qt_noop()) #endif +// Thanks to CGAL/cgal. +#ifndef __has_attribute + #define __has_attribute(x) 0 // Compatibility with non-clang compilers. +#endif + +#ifndef __has_cpp_attribute + #define __has_cpp_attribute(x) 0 // Compatibility with non-supporting compilers. +#endif + +// The fallthrough attribute. +// See for clang: +// http://clang.llvm.org/docs/AttributeReference.html#statement-attributes +// See for gcc: +// https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html +#if __has_cpp_attribute(fallthrough) +# define V_FALLTHROUGH [[fallthrough]] +#elif __has_cpp_attribute(gnu::fallthrough) +# define V_FALLTHROUGH [[gnu::fallthrough]] +#elif __has_cpp_attribute(clang::fallthrough) +# define V_FALLTHROUGH [[clang::fallthrough]] +#elif __has_attribute(fallthrough) && ! __clang__ +# define V_FALLTHROUGH __attribute__ ((fallthrough)) +#else +# define V_FALLTHROUGH while(false){} +#endif + enum class MessageBoxType { Normal = 0, @@ -54,9 +80,19 @@ public: // Given the file name @p_fileName and directory path @p_dirPath, generate // a file name based on @p_fileName which does not exist in @p_dirPath. - static QString generateCopiedFileName(const QString &p_dirPath, const QString &p_fileName); + // @p_completeBaseName: use complete base name or complete suffix. For example, + // "abc.tar.gz", if @p_completeBaseName is true, the base name is "abc.tar", + // otherwise, it is "abc". + static QString generateCopiedFileName(const QString &p_dirPath, + const QString &p_fileName, + bool p_completeBaseName = true); + + // Given the directory name @p_dirName and directory path @p_parentDirPath, + // generate a directory name based on @p_dirName which does not exist in + // @p_parentDirPath. + static QString generateCopiedDirName(const QString &p_parentDirPath, + const QString &p_dirName); - static QString generateCopiedDirName(const QString &p_parentDirPath, const QString &p_dirName); static void processStyle(QString &style, const QVector > &varMap); // Return the last directory name of @p_path. @@ -122,8 +158,18 @@ public: // Get an available file name in @p_directory with base @p_baseFileName. // If there already exists a file named @p_baseFileName, try to add sequence // suffix to the name, such as _001. + // @p_completeBaseName: use complete base name or complete suffix. For example, + // "abc.tar.gz", if @p_completeBaseName is true, the base name is "abc.tar", + // otherwise, it is "abc". static QString getFileNameWithSequence(const QString &p_directory, - const QString &p_baseFileName); + const QString &p_baseFileName, + bool p_completeBaseName = true); + + // Get an available directory name in @p_directory with base @p_baseDirName. + // If there already exists a file named @p_baseFileName, try to add sequence + // suffix to the name, such as _001. + static QString getDirNameWithSequence(const QString &p_directory, + const QString &p_baseDirName); // Get an available random file name in @p_directory. static QString getRandomFileName(const QString &p_directory); @@ -131,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); @@ -186,6 +235,13 @@ public: // Assign @p_str to @p_msg if it is not NULL. static void addErrMsg(QString *p_msg, const QString &p_str); + // Check each file of @p_files and return valid ones for VNote to open. + static QStringList filterFilePathsToOpen(const QStringList &p_files); + + // Return the normalized file path of @p_file if it is valid to open. + // Return empty if it is not valid. + static QString validFilePathToOpen(const QString &p_file); + // Regular expression for image link. // ![image title]( http://github.com/tamlok/vnote.jpg "alt \" text" ) // Captured texts (need to be trimmed): @@ -195,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; diff --git a/src/utils/vvim.cpp b/src/utils/vvim.cpp index 74bb2c1c..5d9e1c6a 100644 --- a/src/utils/vvim.cpp +++ b/src/utils/vvim.cpp @@ -18,6 +18,7 @@ extern VConfigManager *g_config; const QChar VVim::c_unnamedRegister = QChar('"'); const QChar VVim::c_blackHoleRegister = QChar('_'); const QChar VVim::c_selectionRegister = QChar('+'); +QMap VVim::s_registers; const int VVim::SearchHistory::c_capacity = 50; @@ -170,62 +171,6 @@ static void findCurrentSpace(const QTextCursor &p_cursor, int &p_start, int &p_e p_end += block.position(); } -// Find the start and end of the word @p_cursor locates in (within a single block). -// @p_start and @p_end will be the global position of the start and end of the word. -// @p_start will equals to @p_end if @p_cursor is a space. -static void findCurrentWord(QTextCursor p_cursor, int &p_start, int &p_end) -{ - QString text = p_cursor.block().text(); - int pib = p_cursor.positionInBlock(); - - if (pib < text.size() && text[pib].isSpace()) { - p_start = p_end = p_cursor.position(); - return; - } - - p_cursor.movePosition(QTextCursor::StartOfWord); - p_start = p_cursor.position(); - p_cursor.movePosition(QTextCursor::EndOfWord); - p_end = p_cursor.position(); -} - -// Find the start and end of the WORD @p_cursor locates in (within a single block). -// @p_start and @p_end will be the global position of the start and end of the WORD. -// @p_start will equals to @p_end if @p_cursor is a space. -// Attention: www|sss will select www, which is different from findCurrentWord(). -static void findCurrentWORD(const QTextCursor &p_cursor, int &p_start, int &p_end) -{ - QTextBlock block = p_cursor.block(); - QString text = block.text(); - int pib = p_cursor.positionInBlock(); - - if (pib < text.size() && text[pib].isSpace()) { - p_start = p_end = p_cursor.position(); - return; - } - - // Find the start. - p_start = 0; - for (int i = pib - 1; i >= 0; --i) { - if (text[i].isSpace()) { - p_start = i + 1; - break; - } - } - - // Find the end. - p_end = block.length() - 1; - for (int i = pib; i < text.size(); ++i) { - if (text[i].isSpace()) { - p_end = i; - break; - } - } - - p_start += block.position(); - p_end += block.position(); -} - // Move @p_cursor to skip spaces if current cursor is placed at a space // (may move across blocks). It will stop by the empty block on the way. // Forward: wwwwsssss|wwww @@ -351,7 +296,7 @@ static void insertChangeBlockAfterDeletion(QTextCursor &p_cursor, int p_deletion } if (g_config->getAutoIndent()) { - VEditUtils::indentBlockAsPreviousBlock(p_cursor); + VEditUtils::indentBlockAsBlock(p_cursor, false); } } @@ -556,7 +501,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) QChar reg = keyToRegisterName(keyInfo); if (!reg.isNull()) { // Insert register content. - m_editor->insertPlainText(m_registers[reg].read()); + m_editor->insertPlainText(getRegister(reg).read()); } goto clear_accept; @@ -602,11 +547,12 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) QChar reg = keyToRegisterName(keyInfo); if (!reg.isNull()) { m_keys.clear(); - setRegister(reg); - if (m_registers[reg].isNamedRegister()) { - m_registers[reg].m_append = (modifiers == Qt::ShiftModifier); + setCurrentRegisterName(reg); + Register &r = getRegister(reg); + if (r.isNamedRegister()) { + r.m_append = (modifiers == Qt::ShiftModifier); } else { - Q_ASSERT(!m_registers[reg].m_append); + Q_ASSERT(!r.m_append); } goto accept; @@ -965,7 +911,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) bool textInserted = false; if (g_config->getAutoIndent()) { - textInserted = VEditUtils::indentBlockAsPreviousBlock(cursor); + textInserted = VEditUtils::indentBlockAsBlock(cursor, false); if (g_config->getAutoList()) { textInserted = VEditUtils::insertListMarkAsPreviousBlock(cursor) || textInserted; @@ -1115,6 +1061,8 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) } else { break; } + + V_FALLTHROUGH; } case Qt::Key_PageUp: @@ -1381,6 +1329,8 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) } else { break; } + + V_FALLTHROUGH; } case Qt::Key_Escape: @@ -2219,7 +2169,7 @@ void VVim::resetState() m_keys.clear(); m_tokens.clear(); m_pendingKeys.clear(); - setRegister(c_unnamedRegister); + setCurrentRegisterName(c_unnamedRegister); m_resetPositionInBlock = true; m_registerPending = false; } @@ -2723,7 +2673,7 @@ bool VVim::processMovement(QTextCursor &p_cursor, for (int i = 0; i < p_repeat; ++i) { int start, end; // [start, end] is current WORD. - findCurrentWORD(p_cursor, start, end); + VEditUtils::findCurrentWORD(p_cursor, start, end); // Move cursor to end of current WORD. p_cursor.setPosition(end, p_moveMode); @@ -2773,7 +2723,7 @@ bool VVim::processMovement(QTextCursor &p_cursor, int start, end; // [start, end] is current WORD. - findCurrentWORD(p_cursor, start, end); + VEditUtils::findCurrentWORD(p_cursor, start, end); // Move cursor to the end of current WORD. p_cursor.setPosition(end, p_moveMode); @@ -2819,7 +2769,7 @@ bool VVim::processMovement(QTextCursor &p_cursor, int start, end; // [start, end] is current WORD. - findCurrentWORD(p_cursor, start, end); + VEditUtils::findCurrentWORD(p_cursor, start, end); // Move cursor to the start of current WORD. p_cursor.setPosition(start, p_moveMode); @@ -2856,7 +2806,7 @@ bool VVim::processMovement(QTextCursor &p_cursor, for (int i = 0; i < p_repeat; ++i) { int start, end; - findCurrentWORD(p_cursor, start, end); + VEditUtils::findCurrentWORD(p_cursor, start, end); p_cursor.setPosition(start, p_moveMode); @@ -3045,7 +2995,7 @@ handle_target: // Different from Vim: // We do not recognize a word as strict as Vim. int start, end; - findCurrentWord(p_cursor, start, end); + VEditUtils::findCurrentWord(p_cursor, start, end); if (start == end) { // Spaces, find next word. QTextCursor cursor = p_cursor; @@ -3056,7 +3006,7 @@ handle_target: } if (!doc->characterAt(cursor.position()).isSpace()) { - findCurrentWord(cursor, start, end); + VEditUtils::findCurrentWord(cursor, start, end); Q_ASSERT(start != end); break; } @@ -3181,7 +3131,7 @@ bool VVim::selectRange(QTextCursor &p_cursor, const QTextDocument *p_doc, Q_ASSERT(p_repeat == -1); bool spaces = false; int start, end; - findCurrentWord(p_cursor, start, end); + VEditUtils::findCurrentWord(p_cursor, start, end); if (start == end) { // Select the space between previous word and next word. @@ -3221,7 +3171,7 @@ bool VVim::selectRange(QTextCursor &p_cursor, const QTextDocument *p_doc, findCurrentSpace(p_cursor, start, end); if (start == end) { - findCurrentWORD(p_cursor, start, end); + VEditUtils::findCurrentWORD(p_cursor, start, end); } else { // Select the space between previous WORD and next WORD. spaces = true; @@ -3240,7 +3190,7 @@ bool VVim::selectRange(QTextCursor &p_cursor, const QTextDocument *p_doc, moveCursorAcrossSpaces(p_cursor, moveMode, true); // [start, end] is current WORD. - findCurrentWORD(p_cursor, start, end); + VEditUtils::findCurrentWORD(p_cursor, start, end); // Move cursor to the end of current WORD. p_cursor.setPosition(end, moveMode); @@ -3481,6 +3431,8 @@ void VVim::processDeleteAction(QList &p_tokens) // Fall through. mayCrossBlock = true; + V_FALLTHROUGH; + case Range::WordAround: // Fall through. case Range::WordInner: @@ -3712,6 +3664,8 @@ void VVim::processCopyAction(QList &p_tokens) // Fall through. mayCrossBlock = true; + V_FALLTHROUGH; + case Range::WordAround: // Fall through. case Range::WordInner: @@ -3873,7 +3827,7 @@ void VVim::processPasteAction(QList &p_tokens, bool p_pasteBefore) repeat = to.m_repeat; } - Register ® = m_registers[m_regName]; + Register ® = getRegister(m_regName); QString value = reg.read(); bool isBlock = reg.isBlock(); if (value.isEmpty()) { @@ -4847,14 +4801,17 @@ void VVim::expandSelectionToWholeLines(QTextCursor &p_cursor) void VVim::initRegisters() { - m_registers.clear(); - for (char ch = 'a'; ch <= 'z'; ++ch) { - m_registers[QChar(ch)] = Register(QChar(ch)); + if (!s_registers.isEmpty()) { + return; } - m_registers[c_unnamedRegister] = Register(c_unnamedRegister); - m_registers[c_blackHoleRegister] = Register(c_blackHoleRegister); - m_registers[c_selectionRegister] = Register(c_selectionRegister); + for (char ch = 'a'; ch <= 'z'; ++ch) { + s_registers[QChar(ch)] = Register(QChar(ch)); + } + + s_registers[c_unnamedRegister] = Register(c_unnamedRegister); + s_registers[c_blackHoleRegister] = Register(c_blackHoleRegister); + s_registers[c_selectionRegister] = Register(c_selectionRegister); } bool VVim::expectingRegisterName() const @@ -5137,12 +5094,12 @@ void VVim::saveToRegister(const QString &p_text) qDebug() << QString("save text(%1) to register(%2)").arg(text).arg(m_regName); - Register ® = m_registers[m_regName]; + Register ® = getRegister(m_regName); reg.update(text); if (!reg.isBlackHoleRegister() && !reg.isUnnamedRegister()) { // Save it to unnamed register. - m_registers[c_unnamedRegister].update(reg.m_value); + setRegister(c_unnamedRegister, reg.m_value); } } @@ -5249,7 +5206,7 @@ void VVim::message(const QString &p_msg) const QMap &VVim::getRegisters() const { - return m_registers; + return s_registers; } const VVim::Marks &VVim::getMarks() const @@ -5272,7 +5229,7 @@ QString VVim::getPendingKeys() const return str; } -void VVim::setRegister(QChar p_reg) +void VVim::setCurrentRegisterName(QChar p_reg) { m_regName = p_reg; } @@ -5785,7 +5742,7 @@ QString VVim::readRegister(int p_key, int p_modifiers) Key keyInfo(p_key, p_modifiers); QChar reg = keyToRegisterName(keyInfo); if (!reg.isNull()) { - return m_registers[reg].read(); + return getRegister(reg).read(); } return ""; diff --git a/src/utils/vvim.h b/src/utils/vvim.h index 69973e8f..7101d7c0 100644 --- a/src/utils/vvim.h +++ b/src/utils/vvim.h @@ -166,7 +166,7 @@ public: void setMode(VimMode p_mode, bool p_clearSelection = true); // Set current register. - void setRegister(QChar p_reg); + void setCurrentRegisterName(QChar p_reg); // Get m_registers. const QMap &getRegisters() const; @@ -797,6 +797,10 @@ private: // Clear search highlight. void clearSearchHighlight(); + // Function utils for register. + Register &getRegister(QChar p_regName) const; + void setRegister(QChar p_regName, const QString &p_val); + VEdit *m_editor; const VEditConfig *m_editConfig; VimMode m_mode; @@ -814,8 +818,6 @@ private: // Whether reset the position in block when moving cursor. bool m_resetPositionInBlock; - QMap m_registers; - // Currently used register. QChar m_regName; @@ -843,6 +845,18 @@ private: static const QChar c_unnamedRegister; static const QChar c_blackHoleRegister; static const QChar c_selectionRegister; + + static QMap s_registers; }; +inline VVim::Register &VVim::getRegister(QChar p_regName) const +{ + return s_registers[p_regName]; +} + +inline void VVim::setRegister(QChar p_regName, const QString &p_val) +{ + s_registers[p_regName].update(p_val); +} + #endif // VVIM_H diff --git a/src/vattachmentlist.cpp b/src/vattachmentlist.cpp index 240fa899..8c44044b 100644 --- a/src/vattachmentlist.cpp +++ b/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(); diff --git a/src/vcaptain.cpp b/src/vcaptain.cpp index 9fc49504..0bfc6d79 100644 --- a/src/vcaptain.cpp +++ b/src/vcaptain.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include "vcaptain.h" @@ -9,38 +8,39 @@ #include "vedittab.h" #include "vfilelist.h" #include "vnavigationmode.h" +#include "vconfigmanager.h" + +extern VMainWindow *g_mainWin; + +extern VConfigManager *g_config; // 3s pending time after the leader keys. const int c_pendingTime = 3 * 1000; -#if defined(QT_NO_DEBUG) -extern QFile g_logFile; -#endif - -VCaptain::VCaptain(VMainWindow *p_parent) - : QWidget(p_parent), m_mainWindow(p_parent), m_mode(VCaptain::Normal), - m_widgetBeforeCaptain(NULL), m_nextMajorKey('a'), m_ignoreFocusChange(false) +VCaptain::VCaptain(QWidget *p_parent) + : QWidget(p_parent), + m_mode(CaptainMode::Normal), + m_widgetBeforeNavigation(NULL), + m_nextMajorKey('a'), + m_ignoreFocusChange(false), + m_leaderKey(g_config->getShortcutKeySequence("CaptainMode")) { - m_pendingTimer = new QTimer(this); - m_pendingTimer->setSingleShot(true); - m_pendingTimer->setInterval(c_pendingTime); - connect(m_pendingTimer, &QTimer::timeout, - this, &VCaptain::pendingTimerTimeout); + Q_ASSERT(!m_leaderKey.isEmpty()); connect(qApp, &QApplication::focusChanged, this, &VCaptain::handleFocusChanged); - QShortcut *shortcut = new QShortcut(QKeySequence("Ctrl+E"), this, - Q_NULLPTR, Q_NULLPTR); - connect(shortcut, &QShortcut::activated, - this, &VCaptain::trigger); - - qApp->installEventFilter(this); - setWindowFlags(Qt::FramelessWindowHint); + // Make it as small as possible. This widget will stay at the top-left corner // of VMainWindow. resize(1, 1); + + // Register Navigation mode as Captain mode target. + registerCaptainTarget(tr("NavigationMode"), + g_config->getCaptainShortcutKeySequence("NavigationMode"), + this, + navigationModeByCaptain); } QChar VCaptain::getNextMajorKey() @@ -59,53 +59,26 @@ void VCaptain::registerNavigationTarget(VNavigationMode *p_target) QChar key = getNextMajorKey(); if (!key.isNull()) { p_target->registerNavigation(key); - m_targets.append(NaviModeTarget(p_target, true)); + m_naviTargets.push_back(NaviModeTarget(p_target, true)); } } -// In pending mode, if user click other widgets, we need to exit Captain mode. -void VCaptain::handleFocusChanged(QWidget *p_old, QWidget * /* p_now */) +void VCaptain::handleFocusChanged(QWidget *p_old, QWidget * p_now) { - if (!m_ignoreFocusChange && p_old == this) { - exitCaptainMode(); + Q_UNUSED(p_now); + + if (!m_ignoreFocusChange + && !checkMode(CaptainMode::Normal) + && p_old == this) { + exitNavigationMode(); } } -void VCaptain::pendingTimerTimeout() -{ - qDebug() << "Captain mode timeout"; - exitCaptainMode(); - restoreFocus(); -} - -void VCaptain::trigger() -{ - if (m_mode != VCaptain::Normal) { - return; - } - qDebug() << "trigger Captain mode"; - // Focus to listen pending key press. - m_widgetBeforeCaptain = QApplication::focusWidget(); - setFocus(); - m_mode = VCaptain::Pending; - m_pendingTimer->stop(); - m_pendingTimer->start(); - - emit captainModeChanged(true); -} - void VCaptain::keyPressEvent(QKeyEvent *p_event) { int key = p_event->key(); Qt::KeyboardModifiers modifiers = p_event->modifiers(); - if (m_mode == VCaptain::Normal) { - // Should not in focus while in Normal mode. - QWidget::keyPressEvent(p_event); - m_mainWindow->focusNextChild(); - return; - } - if (key == Qt::Key_Control || key == Qt::Key_Shift) { QWidget::keyPressEvent(p_event); return; @@ -120,230 +93,28 @@ void VCaptain::keyPressEvent(QKeyEvent *p_event) bool VCaptain::handleKeyPress(int p_key, Qt::KeyboardModifiers p_modifiers) { - bool ret = true; + if (!checkMode(CaptainMode::Navigation)) { + return false; + } if (p_key == Qt::Key_Escape || (p_key == Qt::Key_BracketLeft && p_modifiers == Qt::ControlModifier)) { - goto exit; + exitNavigationMode(); + return true; } - m_ignoreFocusChange = true; - - if (m_mode == VCaptainMode::Navigation) { - ret = handleKeyPressNavigationMode(p_key, p_modifiers); - m_ignoreFocusChange = false; - return ret; - } - - // In Captain mode, Ctrl key won't make a difference. - switch (p_key) { - case Qt::Key_1: - case Qt::Key_2: - case Qt::Key_3: - case Qt::Key_4: - case Qt::Key_5: - case Qt::Key_6: - case Qt::Key_7: - case Qt::Key_8: - case Qt::Key_9: - { - // Switch to tab . - VEditWindow *win = m_mainWindow->editArea->getCurrentWindow(); - if (win) { - int sequence = p_key - Qt::Key_0; - if (win->activateTab(sequence)) { - m_widgetBeforeCaptain = NULL; - } - } - break; - } - - case Qt::Key_0: - { - // Alternate the tab. - VEditWindow *win = m_mainWindow->editArea->getCurrentWindow(); - if (win) { - if (win->alternateTab()) { - m_widgetBeforeCaptain = NULL; - } - } - break; - } - - case Qt::Key_A: - { - // Show attachment list of current note. - m_mainWindow->showAttachmentList(); - break; - } - - case Qt::Key_D: - // Locate current tab. - if (m_mainWindow->locateCurrentFile()) { - m_widgetBeforeCaptain = NULL; - } - - break; - - case Qt::Key_E: - // Toggle expand view. - m_mainWindow->expandViewAct->trigger(); - break; - - case Qt::Key_F: - { - // Show current window's opened file list. - VEditWindow *win = m_mainWindow->editArea->getCurrentWindow(); - if (win) { - if (win->showOpenedFileList()) { - // showOpenedFileList() already focus the right widget. - m_widgetBeforeCaptain = NULL; - } - } - break; - } - - case Qt::Key_H: - { - if (p_modifiers & Qt::ShiftModifier) { - // Move current tab one split left. - m_mainWindow->editArea->moveCurrentTabOneSplit(false); - } else { - // Focus previous window split. - int idx = m_mainWindow->editArea->focusNextWindow(-1); - if (idx > -1) { - m_widgetBeforeCaptain = NULL; - } - } - break; - } - - case Qt::Key_J: - { - // Focus next tab. - VEditWindow *win = m_mainWindow->editArea->getCurrentWindow(); - if (win) { - win->focusNextTab(true); - // focusNextTab() will focus the right widget. - m_widgetBeforeCaptain = NULL; - } - break; - } - - case Qt::Key_K: - { - // Focus previous tab. - VEditWindow *win = m_mainWindow->editArea->getCurrentWindow(); - if (win) { - win->focusNextTab(false); - // focusNextTab() will focus the right widget. - m_widgetBeforeCaptain = NULL; - } - break; - } - - case Qt::Key_L: - { - if (p_modifiers & Qt::ShiftModifier) { - // Move current tab one split right. - m_mainWindow->editArea->moveCurrentTabOneSplit(true); - } else { - // Focus next window split. - int idx = m_mainWindow->editArea->focusNextWindow(1); - if (idx > -1) { - m_widgetBeforeCaptain = NULL; - } - } - break; - } - - case Qt::Key_P: - // Toggle one/two panel view. - m_mainWindow->toggleOnePanelView(); - break; - - case Qt::Key_Q: - // Discard changes and exit edit mode. - if (m_mainWindow->m_curFile) { - m_mainWindow->discardExitAct->trigger(); - } - break; - - case Qt::Key_R: - { - // Remove current window split. - m_mainWindow->editArea->removeCurrentWindow(); - - QWidget *nextFocus = m_mainWindow->editArea->currentEditTab(); - m_widgetBeforeCaptain = nextFocus ? nextFocus : m_mainWindow->fileList; - break; - } - - case Qt::Key_T: - // Toggle the Tools dock. - m_mainWindow->toolDock->setVisible(!m_mainWindow->toolDock->isVisible()); - break; - - case Qt::Key_V: - // Vertical split current window. - m_mainWindow->editArea->splitCurrentWindow(); - // Do not restore focus. - m_widgetBeforeCaptain = NULL; - break; - - case Qt::Key_W: - // Enter navigation mode. - triggerNavigationMode(); - m_ignoreFocusChange = false; - return ret; - - case Qt::Key_X: - { - // Close current tab. - m_mainWindow->closeCurrentFile(); - - // m_widgetBeforeCaptain may be the closed tab which will cause crash. - QWidget *nextFocus = m_mainWindow->editArea->currentEditTab(); - m_widgetBeforeCaptain = nextFocus ? nextFocus : m_mainWindow->fileList; - break; - } - - case Qt::Key_Question: - { - // Display shortcuts doc. - m_mainWindow->shortcutHelp(); - m_widgetBeforeCaptain = NULL; - break; - } - -#if defined(QT_NO_DEBUG) - case Qt::Key_Comma: - { - // Flush g_logFile. - g_logFile.flush(); - break; - } -#endif - - default: - // Not implemented yet. Just exit Captain mode. - break; - } - -exit: - exitCaptainMode(); - restoreFocus(); - return ret; + return handleKeyPressNavigationMode(p_key, p_modifiers); } bool VCaptain::handleKeyPressNavigationMode(int p_key, Qt::KeyboardModifiers /* p_modifiers */) { - Q_ASSERT(m_mode == VCaptainMode::Navigation); + Q_ASSERT(m_mode == CaptainMode::Navigation); bool hasConsumed = false; bool pending = false; - for (auto &target : m_targets) { + m_ignoreFocusChange = true; + for (auto &target : m_naviTargets) { if (hasConsumed) { target.m_available = false; target.m_target->hideNavigation(); @@ -351,12 +122,13 @@ bool VCaptain::handleKeyPressNavigationMode(int p_key, } if (target.m_available) { bool succeed = false; + // May change focus, so we need to ignore focus change here. bool consumed = target.m_target->handleKeyNavigation(p_key, succeed); if (consumed) { hasConsumed = true; if (succeed) { // Exit. - m_widgetBeforeCaptain = NULL; + m_widgetBeforeNavigation = NULL; } else { // Consumed but not succeed. Need more keys. pending = true; @@ -368,20 +140,23 @@ bool VCaptain::handleKeyPressNavigationMode(int p_key, } } } + + m_ignoreFocusChange = false; if (pending) { return true; } - exitCaptainMode(); - restoreFocus(); + + exitNavigationMode(); return true; } void VCaptain::triggerNavigationMode() { - m_pendingTimer->stop(); - m_mode = VCaptainMode::Navigation; - - for (auto &target : m_targets) { + setMode(CaptainMode::Navigation); + m_widgetBeforeNavigation = QApplication::focusWidget(); + // Focus to listen pending key press. + setFocus(); + for (auto &target : m_naviTargets) { target.m_available = true; target.m_target->showNavigation(); } @@ -389,51 +164,74 @@ void VCaptain::triggerNavigationMode() void VCaptain::exitNavigationMode() { - m_mode = VCaptainMode::Normal; + setMode(CaptainMode::Normal); - for (auto &target : m_targets) { + for (auto &target : m_naviTargets) { target.m_available = true; target.m_target->hideNavigation(); } -} -bool VCaptain::eventFilter(QObject *p_obj, QEvent *p_event) -{ - if (m_mode != VCaptain::Normal && p_event->type() == QEvent::Shortcut) { - qDebug() << "filter" << p_event; - QShortcutEvent *keyEve = dynamic_cast(p_event); - Q_ASSERT(keyEve); - const QKeySequence &keys = keyEve->key(); - if (keys.count() == 1) { - int key = keys[0]; - Qt::KeyboardModifiers modifiers = Qt::KeyboardModifiers(key & (~0x01FFFFFFU)); - key &= 0x01FFFFFFUL; - if (handleKeyPress(key, modifiers)) { - return true; - } - } - exitCaptainMode(); - restoreFocus(); - } - return QWidget::eventFilter(p_obj, p_event); + restoreFocus(); } void VCaptain::restoreFocus() { - if (m_widgetBeforeCaptain) { - m_widgetBeforeCaptain->setFocus(); + if (m_widgetBeforeNavigation) { + m_widgetBeforeNavigation->setFocus(); } } -void VCaptain::exitCaptainMode() +bool VCaptain::registerCaptainTarget(const QString &p_name, + const QString &p_key, + void *p_target, + CaptainFunc p_func) { - if (m_mode == VCaptainMode::Navigation) { - exitNavigationMode(); + if (p_key.isEmpty()) { + return false; } - m_mode = VCaptain::Normal; - m_pendingTimer->stop(); - m_ignoreFocusChange = false; - emit captainModeChanged(false); + QString lowerKey = p_key.toLower(); + + if (m_captainTargets.contains(lowerKey)) { + return false; + } + + // Register shortcuts. + QString sequence = QString("%1,%2").arg(m_leaderKey).arg(p_key); + QShortcut *shortcut = new QShortcut(QKeySequence(sequence), + this); + shortcut->setContext(Qt::ApplicationShortcut); + + connect(shortcut, &QShortcut::activated, + this, std::bind(&VCaptain::triggerCaptainTarget, this, p_key)); + + + CaptainModeTarget target(p_name, + p_key, + p_target, + p_func, + shortcut); + m_captainTargets.insert(lowerKey, target); + + qDebug() << "registered:" << target.toString() << sequence; + + return true; } +void VCaptain::triggerCaptainTarget(const QString &p_key) +{ + auto it = m_captainTargets.find(p_key.toLower()); + Q_ASSERT(it != m_captainTargets.end()); + const CaptainModeTarget &target = it.value(); + + qDebug() << "triggered:" << target.toString(); + + target.m_function(target.m_target, nullptr); +} + +void VCaptain::navigationModeByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VCaptain *obj = static_cast(p_target); + obj->triggerNavigationMode(); +} diff --git a/src/vcaptain.h b/src/vcaptain.h index dabb69f8..98bc19d8 100644 --- a/src/vcaptain.h +++ b/src/vcaptain.h @@ -1,77 +1,166 @@ #ifndef VCAPTAIN_H #define VCAPTAIN_H -#include -#include +#include + +#include +#include +#include -class QTimer; class QKeyEvent; -class VMainWindow; -class QEvent; class VNavigationMode; +class QShortcut; + +// void func(void *p_target, void *p_data); +typedef std::function CaptainFunc; class VCaptain : public QWidget { Q_OBJECT public: - explicit VCaptain(VMainWindow *p_parent); + explicit VCaptain(QWidget *p_parent); - // Trigger Captain mode. - void trigger(); - - // Register a target for Navigation Mode. + // Register a target for Navigation mode. void registerNavigationTarget(VNavigationMode *p_target); -signals: - void captainModeChanged(bool p_enabled); + // Register a target for Captain mode. + bool registerCaptainTarget(const QString &p_name, + const QString &p_key, + void *p_target, + CaptainFunc p_func); protected: void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; - bool eventFilter(QObject *p_obj, QEvent *p_event) Q_DECL_OVERRIDE; - -public slots: private slots: - void pendingTimerTimeout(); + // Exit Navigation mode if focus lost. void handleFocusChanged(QWidget *p_old, QWidget *p_new); private: - // Restore the focus to m_widgetBeforeCaptain. - void restoreFocus(); - void exitCaptainMode(); - // Return true if finish handling the event; otherwise, let the base widget - // to handle it. - bool handleKeyPress(int p_key, Qt::KeyboardModifiers p_modifiers); - bool handleKeyPressNavigationMode(int p_key, - Qt::KeyboardModifiers p_modifiers); - QChar getNextMajorKey(); - void triggerNavigationMode(); - void exitNavigationMode(); + // A widget target for Navigation mode. + struct NaviModeTarget { + NaviModeTarget() + : m_target(nullptr), m_available(false) + { + } - enum VCaptainMode { + NaviModeTarget(VNavigationMode *p_target, bool p_available) + : m_target(p_target), m_available(p_available) + { + } + + VNavigationMode *m_target; + bool m_available; + }; + + // Modes. + enum class CaptainMode { Normal = 0, Pending, Navigation }; - VMainWindow *m_mainWindow; - QTimer *m_pendingTimer; - int m_mode; - // The widget which has the focus before entering Captain mode. - QWidget* m_widgetBeforeCaptain; + struct CaptainModeTarget { + CaptainModeTarget() + : m_target(nullptr), m_function(nullptr), m_shortcut(nullptr) + { + } - struct NaviModeTarget { - VNavigationMode *m_target; - bool m_available; + CaptainModeTarget(const QString &p_name, + const QString &p_key, + void *p_target, + CaptainFunc p_func, + QShortcut *p_shortcut) + : m_name(p_name), + m_key(p_key), + m_target(p_target), + m_function(p_func), + m_shortcut(p_shortcut) + { + } - NaviModeTarget(VNavigationMode *p_target, bool p_available) - : m_target(p_target), m_available(p_available) {} + QString toString() const + { + return QString("Captain mode target %1 key[%2]").arg(m_name).arg(m_key); + } + + // Name to display. + QString m_name; + + // Key sequence to trigger this target. + // This is the sub-sequence after leader key. + QString m_key; + + // Target. + void *m_target; + + // Function to call when this target is trigger. + CaptainFunc m_function; + + // Shortcut for this target. + QShortcut *m_shortcut; }; - QList m_targets; + + // Restore the focus to m_widgetBeforeNavigation. + void restoreFocus(); + + // Return true if finish handling the event; otherwise, let the base widget + // to handle it. + bool handleKeyPress(int p_key, Qt::KeyboardModifiers p_modifiers); + + // Handle key press event in Navigation mode. + bool handleKeyPressNavigationMode(int p_key, + Qt::KeyboardModifiers p_modifiers); + + // Get next major key to use for Navigation mode. + QChar getNextMajorKey(); + + // Trigger navigation mode to ask all targets show themselves. + void triggerNavigationMode(); + + // Exit navigation mode to ask all targets hide themselves. + void exitNavigationMode(); + + // Called to trigger the action of a Captain target which has + // registered @p_key. + void triggerCaptainTarget(const QString &p_key); + + void setMode(CaptainMode p_mode); + + bool checkMode(CaptainMode p_mode) const; + + static void navigationModeByCaptain(void *p_target, void *p_data); + + // Used to indicate whether we are in Navigation mode. + CaptainMode m_mode; + + // The widget which has the focus before entering Navigation mode. + QWidget* m_widgetBeforeNavigation; + + // Targets for Navigation mode. + QVector m_naviTargets; + QChar m_nextMajorKey; - // Ignore focus change to avoid exiting Captain mode while handling key - // press. + + // Targets for Captain mode. + // Key(lower) -> CaptainModeTarget. + QHash m_captainTargets; + + // Ignore focus change during handling Navigation target actions. bool m_ignoreFocusChange; + + // Leader key sequence for Captain mode. + QString m_leaderKey; }; +inline void VCaptain::setMode(CaptainMode p_mode) +{ + m_mode = p_mode; +} + +inline bool VCaptain::checkMode(CaptainMode p_mode) const +{ + return m_mode == p_mode; +} + #endif // VCAPTAIN_H diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index 7a9de545..23c257ad 100644 --- a/src/vconfigmanager.cpp +++ b/src/vconfigmanager.cpp @@ -8,52 +8,60 @@ #include #include #include +#include #include "utils/vutils.h" #include "vstyleparser.h" const QString VConfigManager::orgName = QString("vnote"); + const QString VConfigManager::appName = QString("vnote"); + const QString VConfigManager::c_version = QString("1.9"); + const QString VConfigManager::c_obsoleteDirConfigFile = QString(".vnote.json"); + const QString VConfigManager::c_dirConfigFile = QString("_vnote.json"); -const QString VConfigManager::defaultConfigFilePath = QString(":/resources/vnote.ini"); + +const QString VConfigManager::c_defaultConfigFilePath = QString(":/resources/vnote.ini"); + +const QString VConfigManager::c_defaultConfigFile = QString("vnote.ini"); + +const QString VConfigManager::c_sessionConfigFile = QString("session.ini"); + const QString VConfigManager::c_styleConfigFolder = QString("styles"); + const QString VConfigManager::c_codeBlockStyleConfigFolder = QString("codeblock_styles"); + const QString VConfigManager::c_defaultCssFile = QString(":/resources/styles/default.css"); + const QString VConfigManager::c_defaultCodeBlockCssFile = QString(":/utils/highlightjs/styles/vnote.css"); + const QString VConfigManager::c_defaultMdhlFile = QString(":/resources/styles/default.mdhl"); + const QString VConfigManager::c_solarizedDarkMdhlFile = QString(":/resources/styles/solarized-dark.mdhl"); + const QString VConfigManager::c_solarizedLightMdhlFile = QString(":/resources/styles/solarized-light.mdhl"); + const QString VConfigManager::c_warningTextStyle = QString("color: red; font: bold"); + const QString VConfigManager::c_dataTextStyle = QString("font: bold"); + const QString VConfigManager::c_dangerBtnStyle = QString("QPushButton {color: #fff; border-color: #d43f3a; background-color: #d9534f;}" "QPushButton::hover {color: #fff; border-color: #ac2925; background-color: #c9302c;}"); + const QString VConfigManager::c_vnoteNotebookFolderName = QString("vnote_notebooks"); VConfigManager::VConfigManager(QObject *p_parent) - : QObject(p_parent), userSettings(NULL), defaultSettings(NULL) + : QObject(p_parent), + userSettings(NULL), + defaultSettings(NULL), + m_sessionSettings(NULL) { } -void VConfigManager::migrateIniFile() -{ - const QString originalFolder = "tamlok"; - const QString newFolder = orgName; - QString configFolder = getConfigFolder(); - QDir dir(configFolder); - dir.cdUp(); - dir.rename(originalFolder, newFolder); - userSettings->sync(); -} - void VConfigManager::initialize() { - Q_ASSERT(!userSettings && !defaultSettings); - userSettings = new QSettings(QSettings::IniFormat, QSettings::UserScope, - orgName, appName, this); - defaultSettings = new QSettings(defaultConfigFilePath, QSettings::IniFormat, this); - - migrateIniFile(); + initSettings(); // Override the default css styles on start up. outputDefaultCssStyle(); @@ -68,7 +76,6 @@ void VConfigManager::initialize() m_templateCss = getConfigFromSettings("global", "template_css").toString(); m_templateCodeBlockCss = getConfigFromSettings("global", "template_code_block_css").toString(); m_templateCodeBlockCssUrl = getConfigFromSettings("global", "template_code_block_css_url").toString(); - curNotebookIndex = getConfigFromSettings("global", "current_notebook").toInt(); markdownExtensions = hoedown_extensions(HOEDOWN_EXT_TABLES | HOEDOWN_EXT_FENCED_CODE | HOEDOWN_EXT_HIGHLIGHT | HOEDOWN_EXT_AUTOLINK | @@ -91,10 +98,7 @@ void VConfigManager::initialize() curRenderBackgroundColor = getConfigFromSettings("global", "current_render_background_color").toString(); - m_toolsDockChecked = getConfigFromSettings("session", "tools_dock_checked").toBool(); - m_mainWindowGeometry = getConfigFromSettings("session", "main_window_geometry").toByteArray(); - m_mainWindowState = getConfigFromSettings("session", "main_window_state").toByteArray(); - m_mainSplitterState = getConfigFromSettings("session", "main_splitter_state").toByteArray(); + m_toolsDockChecked = getConfigFromSettings("global", "tools_dock_checked").toBool(); m_findCaseSensitive = getConfigFromSettings("global", "find_case_sensitive").toBool(); @@ -168,6 +172,8 @@ void VConfigManager::initialize() readShortcutsFromSettings(); + readCaptainShortcutsFromSettings(); + initDocSuffixes(); m_markdownHighlightInterval = getConfigFromSettings("global", @@ -187,8 +193,14 @@ void VConfigManager::initialize() m_noteOpenMode = OpenFileMode::Read; } - m_enableHeadingSequence = getConfigFromSettings("global", - "enable_heading_sequence").toBool(); + int tmpHeadingSequenceType = getConfigFromSettings("global", + "heading_sequence_type").toInt(); + if (tmpHeadingSequenceType < (int)HeadingSequenceType::Invalid + && tmpHeadingSequenceType >= (int)HeadingSequenceType::Disabled) { + m_headingSequenceType = (HeadingSequenceType)tmpHeadingSequenceType; + } else { + m_headingSequenceType = HeadingSequenceType::Disabled; + } m_headingSequenceBaseLevel = getConfigFromSettings("global", "heading_sequence_base_level").toInt(); @@ -227,6 +239,76 @@ void VConfigManager::initialize() m_doubleClickCloseTab = getConfigFromSettings("global", "double_click_close_tab").toBool(); + + m_enableCompactMode = getConfigFromSettings("global", + "enable_compact_mode").toBool(); + + int tmpStartupPageMode = getConfigFromSettings("global", + "startup_page_type").toInt(); + if (tmpStartupPageMode < (int)StartupPageType::Invalid + && tmpStartupPageMode >= (int)StartupPageType::None) { + m_startupPageType = (StartupPageType)tmpStartupPageMode; + } else { + m_startupPageType = StartupPageType::None; + } + + m_startupPages = getConfigFromSettings("global", + "startup_pages").toStringList(); + + initFromSessionSettings(); +} + +void VConfigManager::initSettings() +{ + Q_ASSERT(!userSettings && !defaultSettings && !m_sessionSettings); + + const char *codecForIni = "UTF-8"; + + // vnote.ini. + // First try to read vnote.ini from the directory of the executable. + QString userIniPath = QDir(QCoreApplication::applicationDirPath()).filePath(c_defaultConfigFile); + if (QFileInfo::exists(userIniPath)) { + userSettings = new QSettings(userIniPath, + QSettings::IniFormat, + this); + } else { + userSettings = new QSettings(QSettings::IniFormat, + QSettings::UserScope, + orgName, + appName, + this); + } + + userSettings->setIniCodec(codecForIni); + + qDebug() << "use user config" << userSettings->fileName(); + + // Default vnote.ini from resource file. + defaultSettings = new QSettings(c_defaultConfigFilePath, QSettings::IniFormat, this); + defaultSettings->setIniCodec(codecForIni); + + // session.ini. + m_sessionSettings = new QSettings(QDir(getConfigFolder()).filePath(c_sessionConfigFile), + QSettings::IniFormat, + this); + m_sessionSettings->setIniCodec(codecForIni); +} + +void VConfigManager::initFromSessionSettings() +{ + curNotebookIndex = getConfigFromSessionSettings("global", "current_notebook").toInt(); + + m_mainWindowGeometry = getConfigFromSessionSettings("geometry", + "main_window_geometry").toByteArray(); + + m_mainWindowState = getConfigFromSessionSettings("geometry", + "main_window_state").toByteArray(); + + m_mainSplitterState = getConfigFromSessionSettings("geometry", + "main_splitter_state").toByteArray(); + + m_naviSplitterState = getConfigFromSessionSettings("geometry", + "navi_splitter_state").toByteArray(); } void VConfigManager::readPredefinedColorsFromSettings() @@ -245,49 +327,70 @@ void VConfigManager::readPredefinedColorsFromSettings() << "pre-defined colors from [predefined_colors] section"; } -void VConfigManager::readNotebookFromSettings(QVector &p_notebooks, QObject *parent) +void VConfigManager::readNotebookFromSettings(QSettings *p_settings, + QVector &p_notebooks, + QObject *parent) { Q_ASSERT(p_notebooks.isEmpty()); - int size = userSettings->beginReadArray("notebooks"); + int size = p_settings->beginReadArray("notebooks"); for (int i = 0; i < size; ++i) { - userSettings->setArrayIndex(i); - QString name = userSettings->value("name").toString(); - QString path = userSettings->value("path").toString(); + p_settings->setArrayIndex(i); + QString name = p_settings->value("name").toString(); + QString path = p_settings->value("path").toString(); VNotebook *notebook = new VNotebook(name, path, parent); notebook->readConfigNotebook(); p_notebooks.append(notebook); } - userSettings->endArray(); + + p_settings->endArray(); qDebug() << "read" << p_notebooks.size() << "notebook items from [notebooks] section"; } -void VConfigManager::writeNotebookToSettings(const QVector &p_notebooks) +void VConfigManager::writeNotebookToSettings(QSettings *p_settings, + const QVector &p_notebooks) { // Clear it first - userSettings->beginGroup("notebooks"); - userSettings->remove(""); - userSettings->endGroup(); + p_settings->beginGroup("notebooks"); + p_settings->remove(""); + p_settings->endGroup(); - userSettings->beginWriteArray("notebooks"); + p_settings->beginWriteArray("notebooks"); for (int i = 0; i < p_notebooks.size(); ++i) { - userSettings->setArrayIndex(i); + p_settings->setArrayIndex(i); const VNotebook ¬ebook = *p_notebooks[i]; - userSettings->setValue("name", notebook.getName()); - userSettings->setValue("path", notebook.getPath()); + p_settings->setValue("name", notebook.getName()); + p_settings->setValue("path", notebook.getPath()); } - userSettings->endArray(); + + p_settings->endArray(); qDebug() << "write" << p_notebooks.size() << "notebook items in [notebooks] section"; } +static QVariant getConfigFromSettingsBySectionKey(const QSettings *p_settings, + const QString &p_section, + const QString &p_key) +{ + QString fullKey = p_section + "/" + p_key; + return p_settings->value(fullKey); +} + +static void setConfigToSettingsBySectionKey(QSettings *p_settings, + const QString &p_section, + const QString &p_key, + const QVariant &p_value) +{ + QString fullKey = p_section + "/" + p_key; + return p_settings->setValue(fullKey, p_value); +} + QVariant VConfigManager::getConfigFromSettings(const QString §ion, const QString &key) const { - QString fullKey = section + "/" + key; // First, look up the user-scoped config file - QVariant value = userSettings->value(fullKey); + QVariant value = getConfigFromSettingsBySectionKey(userSettings, section, key); if (!value.isNull()) { - qDebug() << "user config:" << fullKey << value.toString(); + qDebug() << "user config:" << (section + "/" + key) << value; return value; } @@ -298,18 +401,14 @@ QVariant VConfigManager::getConfigFromSettings(const QString §ion, const QSt void VConfigManager::setConfigToSettings(const QString §ion, const QString &key, const QVariant &value) { // Set the user-scoped config file - QString fullKey = section + "/" + key; - userSettings->setValue(fullKey, value); - qDebug() << "set user config:" << fullKey << value.toString(); + setConfigToSettingsBySectionKey(userSettings, section, key, value); + qDebug() << "set user config:" << (section + "/" + key) << value; } QVariant VConfigManager::getDefaultConfig(const QString &p_section, const QString &p_key) const { - QString fullKey = p_section + "/" + p_key; - - QVariant value = defaultSettings->value(fullKey); - qDebug() << "default config:" << fullKey << value.toString(); - + QVariant value = getConfigFromSettingsBySectionKey(defaultSettings, p_section, p_key); + qDebug() << "default config:" << (p_section + "/" + p_key) << value; return value; } @@ -321,6 +420,24 @@ QVariant VConfigManager::resetDefaultConfig(const QString &p_section, const QStr return defaultValue; } +QVariant VConfigManager::getConfigFromSessionSettings(const QString &p_section, + const QString &p_key) const +{ + return getConfigFromSettingsBySectionKey(m_sessionSettings, + p_section, + p_key); +} + +void VConfigManager::setConfigToSessionSettings(const QString &p_section, + const QString &p_key, + const QVariant &p_value) +{ + setConfigToSettingsBySectionKey(m_sessionSettings, + p_section, + p_key, + p_value); +} + QString VConfigManager::fetchDirConfigFilePath(const QString &p_path) { QDir dir(p_path); @@ -887,76 +1004,120 @@ QString VConfigManager::getVnoteNotebookFolderPath() return QDir::home().filePath(c_vnoteNotebookFolderName); } +QHash VConfigManager::readShortcutsFromSettings(QSettings *p_settings, + const QString &p_group) +{ + QHash ret; + p_settings->beginGroup(p_group); + QStringList keys = p_settings->childKeys(); + for (auto const & key : keys) { + if (key.isEmpty()) { + continue; + } + + QVariant varVal = p_settings->value(key); + QString sequence = varVal.toString(); + if (varVal.type() == QVariant::StringList) { + sequence = varVal.toStringList().join(","); + } + + sequence = sequence.trimmed(); + if (isValidKeySequence(sequence)) { + ret.insert(key, sequence); + } + } + + p_settings->endGroup(); + + return ret; +} + bool VConfigManager::isValidKeySequence(const QString &p_seq) { - QString lower = p_seq.toLower(); - return lower != "ctrl+q" && lower != "ctrl+e"; + return p_seq.toLower() != "ctrl+q" + && !QKeySequence(p_seq).isEmpty(); } void VConfigManager::readShortcutsFromSettings() { + const QString group("shortcuts"); + m_shortcuts.clear(); - int size = defaultSettings->beginReadArray("shortcuts"); - for (int i = 0; i < size; ++i) { - defaultSettings->setArrayIndex(i); - QString op = defaultSettings->value("operation").toString(); - QString seq = defaultSettings->value("keysequence").toString().trimmed(); + m_shortcuts = readShortcutsFromSettings(defaultSettings, group); - if (isValidKeySequence(seq)) { - qDebug() << "read shortcut config" << op << seq; - m_shortcuts[op] = seq; - } - } - - defaultSettings->endArray(); - - // Whether we need to update user settings. - bool needUpdate = false; - size = userSettings->beginReadArray("shortcuts"); + // Update default settings according to user settings. + QHash userShortcuts = readShortcutsFromSettings(userSettings, + group); QSet matched; matched.reserve(m_shortcuts.size()); - for (int i = 0; i < size; ++i) { - userSettings->setArrayIndex(i); - QString op = userSettings->value("operation").toString(); - QString seq = userSettings->value("keysequence").toString().trimmed(); - - if (isValidKeySequence(seq)) { - qDebug() << "read user shortcut config" << op << seq; - auto it = m_shortcuts.find(op); - if (it == m_shortcuts.end()) { - // Could not find this in default settings. - needUpdate = true; + for (auto it = userShortcuts.begin(); it != userShortcuts.end(); ++it) { + auto defaultIt = m_shortcuts.find(it.key()); + if (defaultIt != m_shortcuts.end()) { + QString sequence = it.value().trimmed(); + if (sequence != defaultIt.value()) { + if (isValidKeySequence(sequence)) { + matched.insert(it.key()); + *defaultIt = sequence; + } } else { - matched.insert(op); - *it = seq; + matched.insert(it.key()); } } } - userSettings->endArray(); - - if (needUpdate || matched.size() < m_shortcuts.size()) { - // Write the combined config to user settings. - writeShortcutsToSettings(); + if (matched.size() < m_shortcuts.size()) { + writeShortcutsToSettings(userSettings, group, m_shortcuts); } + + qDebug() << "shortcuts:" << m_shortcuts; } -void VConfigManager::writeShortcutsToSettings() +void VConfigManager::readCaptainShortcutsFromSettings() { - // Clear it first - userSettings->beginGroup("shortcuts"); - userSettings->remove(""); - userSettings->endGroup(); + const QString group("captain_mode_shortcuts"); - userSettings->beginWriteArray("shortcuts"); - int idx = 0; - for (auto it = m_shortcuts.begin(); it != m_shortcuts.end(); ++it, ++idx) { - userSettings->setArrayIndex(idx); - userSettings->setValue("operation", it.key()); - userSettings->setValue("keysequence", it.value()); + m_captainShortcuts.clear(); + m_captainShortcuts = readShortcutsFromSettings(defaultSettings, group); + + // Update default settings according to user settings. + QHash userShortcuts = readShortcutsFromSettings(userSettings, + group); + QSet matched; + matched.reserve(m_captainShortcuts.size()); + for (auto it = userShortcuts.begin(); it != userShortcuts.end(); ++it) { + auto defaultIt = m_captainShortcuts.find(it.key()); + if (defaultIt != m_captainShortcuts.end()) { + QString sequence = it.value().trimmed(); + if (sequence != defaultIt.value()) { + if (isValidKeySequence(sequence)) { + matched.insert(it.key()); + *defaultIt = sequence; + } + } else { + matched.insert(it.key()); + } + } } - userSettings->endArray(); + if (matched.size() < m_captainShortcuts.size()) { + writeShortcutsToSettings(userSettings, group, m_captainShortcuts); + } + + qDebug() << "captain mode shortcuts:" << m_captainShortcuts; +} + +void VConfigManager::writeShortcutsToSettings(QSettings *p_settings, + const QString &p_group, + const QHash &p_shortcuts) +{ + p_settings->beginGroup(p_group); + p_settings->remove(""); + + for (auto it = p_shortcuts.begin(); it != p_shortcuts.end(); ++it) { + p_settings->setValue(it.key(), it.value()); + } + + p_settings->endGroup(); } QString VConfigManager::getShortcutKeySequence(const QString &p_operation) const @@ -969,6 +1130,16 @@ QString VConfigManager::getShortcutKeySequence(const QString &p_operation) const return *it; } +QString VConfigManager::getCaptainShortcutKeySequence(const QString &p_operation) const +{ + auto it = m_captainShortcuts.find(p_operation); + if (it == m_captainShortcuts.end()) { + return QString(); + } + + return *it; +} + void VConfigManager::initDocSuffixes() { m_docSuffixes.clear(); @@ -999,3 +1170,59 @@ void VConfigManager::initDocSuffixes() qDebug() << "doc suffixes" << m_docSuffixes; } + +QVector VConfigManager::getLastOpenedFiles() +{ + QVector files; + int size = m_sessionSettings->beginReadArray("last_opened_files"); + for (int i = 0; i < size; ++i) { + m_sessionSettings->setArrayIndex(i); + files.push_back(VFileSessionInfo::fromSettings(m_sessionSettings)); + } + + m_sessionSettings->endArray(); + qDebug() << "read" << files.size() + << "items from [last_opened_files] section"; + + return files; +} + +void VConfigManager::setLastOpenedFiles(const QVector &p_files) +{ + const QString section("last_opened_files"); + + // Clear it first + m_sessionSettings->beginGroup(section); + m_sessionSettings->remove(""); + m_sessionSettings->endGroup(); + + m_sessionSettings->beginWriteArray(section); + for (int i = 0; i < p_files.size(); ++i) { + m_sessionSettings->setArrayIndex(i); + const VFileSessionInfo &info = p_files[i]; + info.toSettings(m_sessionSettings); + } + + m_sessionSettings->endArray(); + qDebug() << "write" << p_files.size() + << "items in [last_opened_files] section"; + +} + +QVector VConfigManager::getCustomMagicWords() +{ + QVector 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; +} diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 5212c4a0..702f9271 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -11,6 +11,9 @@ #include "hgmarkdownhighlighter.h" #include "vmarkdownconverter.h" #include "vconstants.h" +#include "vfilesessioninfo.h" +#include "utils/vmetawordmanager.h" + class QJsonObject; class QString; @@ -41,6 +44,18 @@ struct MarkdownitOption bool m_linkify; }; +// Type of heading sequence. +enum class HeadingSequenceType +{ + Disabled = 0, + Enabled, + + // Enabled only for internal notes. + EnabledNoteOnly, + + Invalid +}; + class VConfigManager : public QObject { public: @@ -106,7 +121,10 @@ public: int getCurNotebookIndex() const; void setCurNotebookIndex(int index); - void getNotebooks(QVector &p_notebooks, QObject *parent); + // Read [notebooks] section from settings into @p_notebooks. + void getNotebooks(QVector &p_notebooks, QObject *p_parent); + + // Write @p_notebooks to [notebooks] section into settings. void setNotebooks(const QVector &p_notebooks); hoedown_extensions getMarkdownExtensions() const; @@ -153,6 +171,9 @@ public: const QByteArray &getMainSplitterState() const; void setMainSplitterState(const QByteArray &p_state); + const QByteArray &getNaviSplitterState() const; + void setNaviSplitterState(const QByteArray &p_state); + bool getFindCaseSensitive() const; void setFindCaseSensitive(bool p_enabled); @@ -254,8 +275,8 @@ public: OpenFileMode getNoteOpenMode() const; void setNoteOpenMode(OpenFileMode p_mode); - bool getEnableHeadingSequence() const; - void setEnableHeadingSequence(bool p_enabled); + HeadingSequenceType getHeadingSequenceType() const; + void setHeadingSequenceType(HeadingSequenceType p_type); int getHeadingSequenceBaseLevel() const; void setHeadingSequenceBaseLevel(int p_level); @@ -291,10 +312,32 @@ public: // Whether user specify template_code_block_css_url directly. bool getUserSpecifyTemplateCodeBlockCssUrl() const; + bool getEnableCompactMode() const; + void setEnableCompactMode(bool p_enabled); + + StartupPageType getStartupPageType() const; + void setStartupPageType(StartupPageType p_type); + + const QStringList &getStartupPages() const; + void setStartupPages(const QStringList &p_pages); + + // Read last opened files from [last_opened_files] of session.ini. + QVector getLastOpenedFiles(); + + // Write last opened files to [last_opened_files] of session.ini. + void setLastOpenedFiles(const QVector &p_files); + + // Read custom magic words from [magic_words] section. + QVector getCustomMagicWords(); + // Return the configured key sequence of @p_operation. // Return empty if there is no corresponding config. QString getShortcutKeySequence(const QString &p_operation) const; + // Return the configured key sequence in Captain mode. + // Return empty if there is no corresponding config. + QString getCaptainShortcutKeySequence(const QString &p_operation) const; + // Get the folder the ini file exists. QString getConfigFolder() const; @@ -317,7 +360,10 @@ public: QVector getEditorStyles() const; private: + // Look up a config from user and default settings. QVariant getConfigFromSettings(const QString §ion, const QString &key) const; + + // Set a config to user settings. void setConfigToSettings(const QString §ion, const QString &key, const QVariant &value); // Get default config from vnote.ini. @@ -326,8 +372,29 @@ private: // Reset user config to default config and return the default config value. QVariant resetDefaultConfig(const QString &p_section, const QString &p_key); - void readNotebookFromSettings(QVector &p_notebooks, QObject *parent); - void writeNotebookToSettings(const QVector &p_notebooks); + // Look up a config from session settings. + QVariant getConfigFromSessionSettings(const QString &p_section, const QString &p_key) const; + + // Set a config to session settings. + void setConfigToSessionSettings(const QString &p_section, + const QString &p_key, + const QVariant &p_value); + + // Init defaultSettings, userSettings, and m_sessionSettings. + void initSettings(); + + // Init from m_sessionSettings. + void initFromSessionSettings(); + + // Read [notebooks] section from @p_settings. + void readNotebookFromSettings(QSettings *p_settings, + QVector &p_notebooks, + QObject *parent); + + // Write to [notebooks] section to @p_settings. + void writeNotebookToSettings(QSettings *p_settings, + const QVector &p_notebooks); + void readPredefinedColorsFromSettings(); // 1. Update styles common in HTML and Markdown; @@ -336,10 +403,6 @@ private: void updateMarkdownEditStyle(); - // Migrate ini file from tamlok/vnote.ini to vnote/vnote.ini. - // This is for the change of org name. - void migrateIniFile(); - // Output pre-defined CSS styles to style folder. bool outputDefaultCssStyle() const; @@ -359,8 +422,16 @@ private: // write the combined configs to user settings. void readShortcutsFromSettings(); - // Write m_shortcuts to the [shortcuts] section in the user settings. - void writeShortcutsToSettings(); + // Read the [captain_mode_shortcuts] section in the settings to init + // m_captainShortcuts. + void readCaptainShortcutsFromSettings(); + + QHash readShortcutsFromSettings(QSettings *p_settings, + const QString &p_group); + + void writeShortcutsToSettings(QSettings *p_settings, + const QString &p_group, + const QHash &p_shortcuts); // Whether @p_seq is a valid key sequence for shortcuts. bool isValidKeySequence(const QString &p_seq); @@ -393,6 +464,8 @@ private: QString m_templateCodeBlockCssUrl; QString m_editorStyle; + + // Index of current notebook. int curNotebookIndex; // Markdown Converter @@ -429,6 +502,7 @@ private: QByteArray m_mainWindowGeometry; QByteArray m_mainWindowState; QByteArray m_mainSplitterState; + QByteArray m_naviSplitterState; // Find/Replace dialog options bool m_findCaseSensitive; @@ -530,6 +604,10 @@ private: // Operation -> KeySequence. QHash m_shortcuts; + // Shortcuts config in Captain mode. + // Operation -> KeySequence. + QHash m_captainShortcuts; + // Whether minimize to system tray icon when closing the app. // -1: uninitialized; // 0: do not minimize to the tay; @@ -553,7 +631,7 @@ private: OpenFileMode m_noteOpenMode; // Whether auto genearte heading sequence. - bool m_enableHeadingSequence; + HeadingSequenceType m_headingSequenceType; // Heading sequence base level. int m_headingSequenceBaseLevel; @@ -600,6 +678,15 @@ private: // Whether double click on a tab to close it. bool m_doubleClickCloseTab; + // Whether put folder and note panel in one single column. + bool m_enableCompactMode; + + // Type of the pages to open on startup. + StartupPageType m_startupPageType; + + // File paths to open on startup. + QStringList m_startupPages; + // The name of the config file in each directory, obsolete. // Use c_dirConfigFile instead. static const QString c_obsoleteDirConfigFile; @@ -607,13 +694,25 @@ private: // The name of the config file in each directory. static const QString c_dirConfigFile; - // The name of the default configuration file - static const QString defaultConfigFilePath; + // The path of the default configuration file + static const QString c_defaultConfigFilePath; + + // The name of the config file. + static const QString c_defaultConfigFile; + + // The name of the config file for session information. + static const QString c_sessionConfigFile; + // QSettings for the user configuration QSettings *userSettings; - // Qsettings for @defaultConfigFileName + + // Qsettings for @c_defaultConfigFilePath. QSettings *defaultSettings; + // QSettings for the session configuration, such as notebooks, + // geometry, last opened files. + QSettings *m_sessionSettings; + // The folder name of style files. static const QString c_styleConfigFolder; @@ -681,18 +780,35 @@ inline void VConfigManager::setCurNotebookIndex(int index) if (index == curNotebookIndex) { return; } + curNotebookIndex = index; - setConfigToSettings("global", "current_notebook", index); + setConfigToSessionSettings("global", "current_notebook", index); } -inline void VConfigManager::getNotebooks(QVector &p_notebooks, QObject *parent) +inline void VConfigManager::getNotebooks(QVector &p_notebooks, + QObject *p_parent) { - readNotebookFromSettings(p_notebooks, parent); + // We used to store it in vnote.ini. For now, we store it in session.ini. + readNotebookFromSettings(m_sessionSettings, p_notebooks, p_parent); + + // Migration. + if (p_notebooks.isEmpty()) { + readNotebookFromSettings(userSettings, p_notebooks, p_parent); + + if (!p_notebooks.isEmpty()) { + // Clear and save it in another place. + userSettings->beginGroup("notebooks"); + userSettings->remove(""); + userSettings->endGroup(); + + writeNotebookToSettings(m_sessionSettings, p_notebooks); + } + } } inline void VConfigManager::setNotebooks(const QVector &p_notebooks) { - writeNotebookToSettings(p_notebooks); + writeNotebookToSettings(m_sessionSettings, p_notebooks); } inline hoedown_extensions VConfigManager::getMarkdownExtensions() const @@ -860,7 +976,7 @@ inline bool VConfigManager::getToolsDockChecked() const inline void VConfigManager::setToolsDockChecked(bool p_checked) { m_toolsDockChecked = p_checked; - setConfigToSettings("session", "tools_dock_checked", + setConfigToSettings("global", "tools_dock_checked", m_toolsDockChecked); } @@ -872,8 +988,9 @@ inline const QByteArray& VConfigManager::getMainWindowGeometry() const inline void VConfigManager::setMainWindowGeometry(const QByteArray &p_geometry) { m_mainWindowGeometry = p_geometry; - setConfigToSettings("session", "main_window_geometry", - m_mainWindowGeometry); + setConfigToSessionSettings("geometry", + "main_window_geometry", + m_mainWindowGeometry); } inline const QByteArray& VConfigManager::getMainWindowState() const @@ -884,8 +1001,9 @@ inline const QByteArray& VConfigManager::getMainWindowState() const inline void VConfigManager::setMainWindowState(const QByteArray &p_state) { m_mainWindowState = p_state; - setConfigToSettings("session", "main_window_state", - m_mainWindowState); + setConfigToSessionSettings("geometry", + "main_window_state", + m_mainWindowState); } inline const QByteArray& VConfigManager::getMainSplitterState() const @@ -896,7 +1014,22 @@ inline const QByteArray& VConfigManager::getMainSplitterState() const inline void VConfigManager::setMainSplitterState(const QByteArray &p_state) { m_mainSplitterState = p_state; - setConfigToSettings("session", "main_splitter_state", m_mainSplitterState); + setConfigToSessionSettings("geometry", + "main_splitter_state", + m_mainSplitterState); +} + +inline const QByteArray& VConfigManager::getNaviSplitterState() const +{ + return m_naviSplitterState; +} + +inline void VConfigManager::setNaviSplitterState(const QByteArray &p_state) +{ + m_naviSplitterState = p_state; + setConfigToSessionSettings("geometry", + "navi_splitter_state", + m_naviSplitterState); } inline bool VConfigManager::getFindCaseSensitive() const @@ -1370,20 +1503,21 @@ inline void VConfigManager::setNoteOpenMode(OpenFileMode p_mode) m_noteOpenMode == OpenFileMode::Read ? 0 : 1); } -inline bool VConfigManager::getEnableHeadingSequence() const +inline HeadingSequenceType VConfigManager::getHeadingSequenceType() const { - return m_enableHeadingSequence; + return m_headingSequenceType; } -inline void VConfigManager::setEnableHeadingSequence(bool p_enabled) +inline void VConfigManager::setHeadingSequenceType(HeadingSequenceType p_type) { - if (m_enableHeadingSequence == p_enabled) { + if (m_headingSequenceType == p_type) { return; } - m_enableHeadingSequence = p_enabled; - setConfigToSettings("global", "enable_heading_sequence", - m_enableHeadingSequence); + m_headingSequenceType = p_type; + setConfigToSettings("global", + "heading_sequence_type", + (int)m_headingSequenceType); } inline int VConfigManager::getHeadingSequenceBaseLevel() const @@ -1570,4 +1704,49 @@ inline bool VConfigManager::getUserSpecifyTemplateCodeBlockCssUrl() const return !m_templateCodeBlockCssUrl.isEmpty(); } +inline bool VConfigManager::getEnableCompactMode() const +{ + return m_enableCompactMode; +} + +inline void VConfigManager::setEnableCompactMode(bool p_enabled) +{ + if (m_enableCompactMode == p_enabled) { + return; + } + + m_enableCompactMode = p_enabled; + setConfigToSettings("global", "enable_compact_mode", m_enableCompactMode); +} + +inline StartupPageType VConfigManager::getStartupPageType() const +{ + return m_startupPageType; +} + +inline void VConfigManager::setStartupPageType(StartupPageType p_type) +{ + if (m_startupPageType == p_type) { + return; + } + + m_startupPageType = p_type; + setConfigToSettings("global", "startup_page_type", (int)m_startupPageType); +} + +inline const QStringList &VConfigManager::getStartupPages() const +{ + return m_startupPages; +} + +inline void VConfigManager::setStartupPages(const QStringList &p_pages) +{ + if (m_startupPages == p_pages) { + return; + } + + m_startupPages = p_pages; + setConfigToSettings("global", "startup_pages", m_startupPages); +} + #endif // VCONFIGMANAGER_H diff --git a/src/vconstants.h b/src/vconstants.h index ae1a2205..217e7b4b 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -1,6 +1,8 @@ #ifndef VCONSTANTS_H #define VCONSTANTS_H +#include + // Html: rich text file; // Markdown: Markdown text file; // List: Infinite list file like WorkFlowy; @@ -22,7 +24,7 @@ namespace ClipboardConfig static const QString c_dirs = "dirs"; } -enum class OpenFileMode {Read = 0, Edit}; +enum class OpenFileMode {Read = 0, Edit, Invalid }; static const qreal c_webZoomFactorMax = 5; static const qreal c_webZoomFactorMin = 0.25; @@ -50,12 +52,16 @@ namespace DirConfig static const QString c_emptyHeaderName = "[EMPTY]"; -enum class TextDecoration { None, - Bold, - Italic, - Underline, - Strikethrough, - InlineCode }; +enum class TextDecoration +{ + None, + Bold, + Italic, + Underline, + Strikethrough, + InlineCode, + CodeBlock +}; enum FindOption { @@ -97,4 +103,13 @@ enum class LineNumberType CodeBlock }; +// Pages to open on start up. +enum class StartupPageType +{ + None = 0, + ContinueLeftOff = 1, + SpecificPages = 2, + Invalid +}; + #endif diff --git a/src/vdirectorytree.cpp b/src/vdirectorytree.cpp index 68ff4f0f..41c78cf1 100644 --- a/src/vdirectorytree.cpp +++ b/src/vdirectorytree.cpp @@ -430,7 +430,7 @@ void VDirectoryTree::newSubDirectory() .arg(g_config->c_dataTextStyle) .arg(curDir->getName()); QString defaultName("new_folder"); - defaultName = VUtils::getFileNameWithSequence(curDir->fetchPath(), defaultName); + defaultName = VUtils::getDirNameWithSequence(curDir->fetchPath(), defaultName); VNewDirDialog dialog(tr("Create Folder"), info, defaultName, curDir, this); if (dialog.exec() == QDialog::Accepted) { QString name = dialog.getNameInput(); @@ -467,7 +467,7 @@ void VDirectoryTree::newRootDirectory() .arg(g_config->c_dataTextStyle) .arg(m_notebook->getName()); QString defaultName("new_folder"); - defaultName = VUtils::getFileNameWithSequence(rootDir->fetchPath(), defaultName); + defaultName = VUtils::getDirNameWithSequence(rootDir->fetchPath(), defaultName); VNewDirDialog dialog(tr("Create Root Folder"), info, defaultName, rootDir, this); if (dialog.exec() == QDialog::Accepted) { QString name = dialog.getNameInput(); diff --git a/src/vdocument.h b/src/vdocument.h index 40b00bbb..08b51ed2 100644 --- a/src/vdocument.h +++ b/src/vdocument.h @@ -16,9 +16,15 @@ class VDocument : public QObject public: // @p_file could be NULL. VDocument(const VFile *p_file, QObject *p_parent = 0); + QString getToc(); + + // Scroll to @anchor in the web. + // @anchor is the id without '#', like "toc_1". If empty, will scroll to top. void scrollToAnchor(const QString &anchor); + void setHtml(const QString &html); + // Request to highlight a segment text. // Use p_id to identify the result. void highlightTextAsync(const QString &p_text, int p_id, int p_timeStamp); @@ -35,6 +41,7 @@ public slots: // When the Web view has been scrolled, it will signal current header anchor. // Empty @anchor to indicate an invalid header. + // The header does not begins with '#'. void setHeader(const QString &anchor); void setLog(const QString &p_log); @@ -49,8 +56,12 @@ public slots: signals: void textChanged(const QString &text); + void tocChanged(const QString &toc); + void requestScrollToAnchor(const QString &anchor); + + // @anchor is the id of that anchor, without '#'. void headerChanged(const QString &anchor); void htmlChanged(const QString &html); void logChanged(const QString &p_log); diff --git a/src/vedit.cpp b/src/vedit.cpp index 38cfb259..4d1669f0 100644 --- a/src/vedit.cpp +++ b/src/vedit.cpp @@ -4,19 +4,26 @@ #include "vedit.h" #include "vnote.h" #include "vconfigmanager.h" -#include "vtoc.h" +#include "vtableofcontent.h" #include "utils/vutils.h" #include "utils/veditutils.h" +#include "utils/vmetawordmanager.h" #include "veditoperations.h" #include "vedittab.h" +#include "dialog/vinsertlinkdialog.h" extern VConfigManager *g_config; + extern VNote *g_vnote; -void VEditConfig::init(const QFontMetrics &p_metric) +extern VMetaWordManager *g_mwMgr; + +void VEditConfig::init(const QFontMetrics &p_metric, + bool p_enableHeadingSequence) { update(p_metric); + // Init configs that do not support update later. m_enableVimMode = g_config->getEnableVimMode(); if (g_config->getLineDistanceHeight() <= 0) { @@ -26,6 +33,8 @@ void VEditConfig::init(const QFontMetrics &p_metric) } m_highlightWholeBlock = m_enableVimMode; + + m_enableHeadingSequence = p_enableHeadingSequence; } void VEditConfig::update(const QFontMetrics &p_metric) @@ -84,7 +93,7 @@ VEdit::VEdit(VFile *p_file, QWidget *p_parent) updateFontAndPalette(); - m_config.init(QFontMetrics(font())); + m_config.init(QFontMetrics(font()), false); updateConfig(); connect(this, &VEdit::cursorPositionChanged, @@ -157,15 +166,16 @@ void VEdit::reloadFile() setModified(false); } -void VEdit::scrollToLine(int p_lineNumber) +bool VEdit::scrollToBlock(int p_blockNumber) { - Q_ASSERT(p_lineNumber >= 0); - - QTextBlock block = document()->findBlockByLineNumber(p_lineNumber); + QTextBlock block = document()->findBlockByNumber(p_blockNumber); if (block.isValid()) { VEditUtils::scrollBlockInPage(this, block.blockNumber(), 0); moveCursor(QTextCursor::EndOfBlock); + return true; } + + return false; } bool VEdit::isModified() const @@ -189,6 +199,49 @@ void VEdit::insertImage() } } +void VEdit::insertLink() +{ + if (!m_editOps) { + return; + } + + QString text; + QString linkText, linkUrl; + QTextCursor cursor = textCursor(); + if (cursor.hasSelection()) { + text = VEditUtils::selectedText(cursor).trimmed(); + // Only pure space is accepted. + QRegExp reg("[\\S ]*"); + if (reg.exactMatch(text)) { + QUrl url = QUrl::fromUserInput(text, + m_file->fetchBasePath()); + QRegExp urlReg("[\\.\\\\/]"); + if (url.isValid() + && text.contains(urlReg)) { + // Url. + linkUrl = text; + } else { + // Text. + linkText = text; + } + } + } + + VInsertLinkDialog dialog(tr("Insert Link"), + "", + "", + linkText, + linkUrl, + this); + if (dialog.exec() == QDialog::Accepted) { + linkText = dialog.getLinkText(); + linkUrl = dialog.getLinkUrl(); + Q_ASSERT(!linkText.isEmpty() && !linkUrl.isEmpty()); + + m_editOps->insertLink(linkText, linkUrl); + } +} + bool VEdit::peekText(const QString &p_text, uint p_options, bool p_forward) { if (p_text.isEmpty()) { @@ -1383,3 +1436,36 @@ void VEdit::updateBlockLineDistanceHeight(int p_pos, cursor.endEditBlock(); } } + +void VEdit::evaluateMagicWords() +{ + QString text; + QTextCursor cursor = textCursor(); + if (!cursor.hasSelection()) { + // Get the WORD in current cursor. + int start, end; + VEditUtils::findCurrentWORD(cursor, start, end); + + if (start == end) { + return; + } else { + cursor.setPosition(start); + cursor.setPosition(end, QTextCursor::KeepAnchor); + } + } + + text = VEditUtils::selectedText(cursor); + Q_ASSERT(!text.isEmpty()); + QString evaText = g_mwMgr->evaluate(text); + if (text != evaText) { + qDebug() << "evaluateMagicWords" << text << evaText; + + cursor.insertText(evaText); + + if (m_editOps) { + m_editOps->setVimMode(VimMode::Insert); + } + + setTextCursor(cursor); + } +} diff --git a/src/vedit.h b/src/vedit.h index 7c8b4cce..f2d7e4b8 100644 --- a/src/vedit.h +++ b/src/vedit.h @@ -10,7 +10,6 @@ #include #include #include "vconstants.h" -#include "vtoc.h" #include "vnotefile.h" class VEditOperations; @@ -38,10 +37,12 @@ public: m_tabSpaces("\t"), m_enableVimMode(false), m_highlightWholeBlock(false), - m_lineDistanceHeight(0) + m_lineDistanceHeight(0), + m_enableHeadingSequence(false) {} - void init(const QFontMetrics &p_metric); + void init(const QFontMetrics &p_metric, + bool p_enableHeadingSequence); // Only update those configs which could be updated online. void update(const QFontMetrics &p_metric); @@ -64,6 +65,9 @@ public: // Line distance height in pixels. int m_lineDistanceHeight; + + // Whether enable auto heading sequence. + bool m_enableHeadingSequence; }; class LineNumberArea; @@ -81,10 +85,15 @@ public: virtual void setModified(bool p_modified); bool isModified() const; virtual void reloadFile(); - virtual void scrollToLine(int p_lineNumber); + + virtual bool scrollToBlock(int p_blockNumber); + // User requests to insert an image. virtual void insertImage(); + // User requests to insert a link. + virtual void insertLink(); + // Used for incremental search. // User has enter the content to search, but does not enter the "find" button yet. bool peekText(const QString &p_text, uint p_options, bool p_forward = true); @@ -135,6 +144,9 @@ public: bool isBlockVisible(const QTextBlock &p_block); + // Evaluate selected text or cursor word as magic words. + void evaluateMagicWords(); + signals: // Request VEditTab to save and exit edit mode. void saveAndRead(); @@ -163,6 +175,9 @@ signals: // Request the edit tab to close find and replace dialog. void requestCloseFindReplaceDialog(); + // Emit when all initialization is ready. + void ready(); + public slots: virtual void highlightCurrentLine(); diff --git a/src/veditarea.cpp b/src/veditarea.cpp index c92dcb32..2ebe8125 100644 --- a/src/veditarea.cpp +++ b/src/veditarea.cpp @@ -7,18 +7,27 @@ #include "vfile.h" #include "dialog/vfindreplacedialog.h" #include "utils/vutils.h" +#include "vfilesessioninfo.h" +#include "vmainwindow.h" +#include "vcaptain.h" extern VConfigManager *g_config; + extern VNote *g_vnote; -VEditArea::VEditArea(VNote *vnote, QWidget *parent) - : QWidget(parent), VNavigationMode(), - vnote(vnote), curWindowIndex(-1) +extern VMainWindow *g_mainWin; + +VEditArea::VEditArea(QWidget *parent) + : QWidget(parent), + VNavigationMode(), + curWindowIndex(-1) { setupUI(); insertSplitWindow(0); setCurrentWindow(0, false); + + registerCaptainTargets(); } void VEditArea::setupUI() @@ -65,7 +74,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); @@ -76,9 +85,9 @@ void VEditArea::insertSplitWindow(int idx) connect(win, &VEditWindow::getFocused, this, &VEditArea::handleWindowFocused); connect(win, &VEditWindow::outlineChanged, - this, &VEditArea::handleOutlineChanged); - connect(win, &VEditWindow::curHeaderChanged, - this, &VEditArea::handleCurHeaderChanged); + this, &VEditArea::handleWindowOutlineChanged); + connect(win, &VEditWindow::currentHeaderChanged, + this, &VEditArea::handleWindowCurrentHeaderChanged); connect(win, &VEditWindow::statusMessage, this, &VEditArea::handleWindowStatusMessage); connect(win, &VEditWindow::vimStatusUpdated, @@ -119,17 +128,17 @@ void VEditArea::removeSplitWindow(VEditWindow *win) win->deleteLater(); } -void VEditArea::openFile(VFile *p_file, OpenFileMode p_mode, bool p_forceMode) +VEditTab *VEditArea::openFile(VFile *p_file, OpenFileMode p_mode, bool p_forceMode) { if (!p_file) { - return; + return NULL; } // If it is DocType::Unknown, open it using system default method. if (p_file->getDocType() == DocType::Unknown) { QUrl url = QUrl::fromLocalFile(p_file->fetchPath()); QDesktopServices::openUrl(url); - return; + return NULL; } // Find if it has been opened already @@ -165,6 +174,8 @@ void VEditArea::openFile(VFile *p_file, OpenFileMode p_mode, bool p_forceMode) tabIdx = openFileInWindow(winIdx, p_file, p_mode); out: + VEditTab *tab = getTab(winIdx, tabIdx); + setCurrentTab(winIdx, tabIdx, setFocus); if (existFile && p_forceMode) { @@ -174,6 +185,8 @@ out: editFile(); } } + + return tab; } QVector > VEditArea::findTabsByFile(const VFile *p_file) @@ -232,15 +245,13 @@ void VEditArea::updateWindowStatus() Q_ASSERT(splitter->count() == 0); emit tabStatusUpdated(VEditTabInfo()); - emit outlineChanged(VToc()); - emit curHeaderChanged(VAnchor()); + emit outlineChanged(VTableOfContent()); + emit currentHeaderChanged(VHeaderPointer()); return; } VEditWindow *win = getWindow(curWindowIndex); win->updateTabStatus(); - win->requestUpdateOutline(); - win->requestUpdateCurHeader(); } bool VEditArea::closeFile(const VFile *p_file, bool p_forced) @@ -431,26 +442,28 @@ void VEditArea::handleWindowFocused() } } -void VEditArea::handleOutlineChanged(const VToc &toc) +void VEditArea::handleWindowOutlineChanged(const VTableOfContent &p_outline) { QObject *winObject = sender(); if (splitter->widget(curWindowIndex) == winObject) { - emit outlineChanged(toc); + emit outlineChanged(p_outline); } } -void VEditArea::handleCurHeaderChanged(const VAnchor &anchor) +void VEditArea::handleWindowCurrentHeaderChanged(const VHeaderPointer &p_header) { QObject *winObject = sender(); if (splitter->widget(curWindowIndex) == winObject) { - emit curHeaderChanged(anchor); + emit currentHeaderChanged(p_header); } } -void VEditArea::handleOutlineItemActivated(const VAnchor &anchor) +void VEditArea::scrollToHeader(const VHeaderPointer &p_header) { - // Notice current window - getWindow(curWindowIndex)->scrollCurTab(anchor); + VEditWindow *win = getCurrentWindow(); + if (win) { + win->scrollToHeader(p_header); + } } bool VEditArea::isFileOpened(const VFile *p_file) @@ -482,13 +495,35 @@ void VEditArea::handleNotebookUpdated(const VNotebook *p_notebook) } } -VEditTab *VEditArea::currentEditTab() +VEditTab *VEditArea::getCurrentTab() const { if (curWindowIndex == -1) { return NULL; } + VEditWindow *win = getWindow(curWindowIndex); - return win->currentEditTab(); + return win->getCurrentTab(); +} + +VEditTab *VEditArea::getTab(int p_winIdx, int p_tabIdx) const +{ + VEditWindow *win = getWindow(p_winIdx); + if (!win) { + return NULL; + } + + return win->getTab(p_tabIdx); +} + +QVector VEditArea::getAllTabsInfo() const +{ + QVector tabs; + int nrWin = splitter->count(); + for (int i = 0; i < nrWin; ++i) { + tabs.append(getWindow(i)->getAllTabsInfo()); + } + + return tabs; } int VEditArea::windowIndex(const VEditWindow *p_window) const @@ -518,7 +553,7 @@ void VEditArea::moveTab(QWidget *p_widget, int p_fromIdx, int p_toIdx) // Only propogate the search in the IncrementalSearch case. void VEditArea::handleFindTextChanged(const QString &p_text, uint p_options) { - VEditTab *tab = currentEditTab(); + VEditTab *tab = getCurrentTab(); if (tab) { if (p_options & FindOption::IncrementalSearch) { tab->findText(p_text, p_options, true); @@ -539,7 +574,7 @@ void VEditArea::handleFindNext(const QString &p_text, uint p_options, bool p_forward) { qDebug() << "find next" << p_text << p_options << p_forward; - VEditTab *tab = currentEditTab(); + VEditTab *tab = getCurrentTab(); if (tab) { tab->findText(p_text, p_options, false, p_forward); } @@ -550,7 +585,7 @@ void VEditArea::handleReplace(const QString &p_text, uint p_options, { qDebug() << "replace" << p_text << p_options << "with" << p_replaceText << p_findNext; - VEditTab *tab = currentEditTab(); + VEditTab *tab = getCurrentTab(); if (tab) { tab->replaceText(p_text, p_options, p_replaceText, p_findNext); } @@ -560,7 +595,7 @@ void VEditArea::handleReplaceAll(const QString &p_text, uint p_options, const QString &p_replaceText) { qDebug() << "replace all" << p_text << p_options << "with" << p_replaceText; - VEditTab *tab = currentEditTab(); + VEditTab *tab = getCurrentTab(); if (tab) { tab->replaceTextAll(p_text, p_options, p_replaceText); } @@ -584,7 +619,7 @@ void VEditArea::handleFindDialogClosed() QString VEditArea::getSelectedText() { - VEditTab *tab = currentEditTab(); + VEditTab *tab = getCurrentTab(); if (tab) { return tab->getSelectedText(); } else { @@ -621,6 +656,7 @@ VEditWindow *VEditArea::getCurrentWindow() const if (curWindowIndex < 0) { return NULL; } + return getWindow(curWindowIndex); } @@ -694,3 +730,220 @@ bool VEditArea::handleKeyNavigation(int p_key, bool &p_succeed) return ret; } +int VEditArea::openFiles(const QVector &p_files) +{ + int nrOpened = 0; + for (auto const & info : p_files) { + QString filePath = VUtils::validFilePathToOpen(info.m_file); + if (filePath.isEmpty()) { + continue; + } + + VFile *file = g_vnote->getFile(filePath); + if (!file) { + continue; + } + + VEditTab *tab = openFile(file, info.m_mode, true); + ++nrOpened; + + VEditTabInfo tabInfo; + tabInfo.m_editTab = tab; + info.toEditTabInfo(&tabInfo); + + tab->tryRestoreFromTabInfo(tabInfo); + } + + return nrOpened; +} + +void VEditArea::registerCaptainTargets() +{ + using namespace std::placeholders; + + VCaptain *captain = g_mainWin->getCaptain(); + + captain->registerCaptainTarget(tr("ActivateTab1"), + g_config->getCaptainShortcutKeySequence("ActivateTab1"), + this, + std::bind(activateTabByCaptain, _1, _2, 1)); + captain->registerCaptainTarget(tr("ActivateTab2"), + g_config->getCaptainShortcutKeySequence("ActivateTab2"), + this, + std::bind(activateTabByCaptain, _1, _2, 2)); + captain->registerCaptainTarget(tr("ActivateTab3"), + g_config->getCaptainShortcutKeySequence("ActivateTab3"), + this, + std::bind(activateTabByCaptain, _1, _2, 3)); + captain->registerCaptainTarget(tr("ActivateTab4"), + g_config->getCaptainShortcutKeySequence("ActivateTab4"), + this, + std::bind(activateTabByCaptain, _1, _2, 4)); + captain->registerCaptainTarget(tr("ActivateTab5"), + g_config->getCaptainShortcutKeySequence("ActivateTab5"), + this, + std::bind(activateTabByCaptain, _1, _2, 5)); + captain->registerCaptainTarget(tr("ActivateTab6"), + g_config->getCaptainShortcutKeySequence("ActivateTab6"), + this, + std::bind(activateTabByCaptain, _1, _2, 6)); + captain->registerCaptainTarget(tr("ActivateTab7"), + g_config->getCaptainShortcutKeySequence("ActivateTab7"), + this, + std::bind(activateTabByCaptain, _1, _2, 7)); + captain->registerCaptainTarget(tr("ActivateTab8"), + g_config->getCaptainShortcutKeySequence("ActivateTab8"), + this, + std::bind(activateTabByCaptain, _1, _2, 8)); + captain->registerCaptainTarget(tr("ActivateTab9"), + g_config->getCaptainShortcutKeySequence("ActivateTab9"), + this, + std::bind(activateTabByCaptain, _1, _2, 9)); + captain->registerCaptainTarget(tr("AlternateTab"), + g_config->getCaptainShortcutKeySequence("AlternateTab"), + this, + alternateTabByCaptain); + captain->registerCaptainTarget(tr("OpenedFileList"), + g_config->getCaptainShortcutKeySequence("OpenedFileList"), + this, + showOpenedFileListByCaptain); + captain->registerCaptainTarget(tr("ActivateSplitLeft"), + g_config->getCaptainShortcutKeySequence("ActivateSplitLeft"), + this, + activateSplitLeftByCaptain); + captain->registerCaptainTarget(tr("ActivateSplitRight"), + g_config->getCaptainShortcutKeySequence("ActivateSplitRight"), + this, + activateSplitRightByCaptain); + captain->registerCaptainTarget(tr("MoveTabSplitLeft"), + g_config->getCaptainShortcutKeySequence("MoveTabSplitLeft"), + this, + moveTabSplitLeftByCaptain); + captain->registerCaptainTarget(tr("MoveTabSplitRight"), + g_config->getCaptainShortcutKeySequence("MoveTabSplitRight"), + this, + moveTabSplitRightByCaptain); + captain->registerCaptainTarget(tr("ActivateNextTab"), + g_config->getCaptainShortcutKeySequence("ActivateNextTab"), + this, + activateNextTabByCaptain); + captain->registerCaptainTarget(tr("ActivatePreviousTab"), + g_config->getCaptainShortcutKeySequence("ActivatePreviousTab"), + this, + activatePreviousTabByCaptain); + captain->registerCaptainTarget(tr("VerticalSplit"), + g_config->getCaptainShortcutKeySequence("VerticalSplit"), + this, + verticalSplitByCaptain); + captain->registerCaptainTarget(tr("RemoveSplit"), + g_config->getCaptainShortcutKeySequence("RemoveSplit"), + this, + removeSplitByCaptain); + captain->registerCaptainTarget(tr("MagicWord"), + g_config->getCaptainShortcutKeySequence("MagicWord"), + this, + evaluateMagicWordsByCaptain); +} + +void VEditArea::activateTabByCaptain(void *p_target, void *p_data, int p_idx) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + VEditWindow *win = obj->getCurrentWindow(); + if (win) { + win->activateTab(p_idx); + } +} + +void VEditArea::alternateTabByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + VEditWindow *win = obj->getCurrentWindow(); + if (win) { + win->alternateTab(); + } +} + +void VEditArea::showOpenedFileListByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + VEditWindow *win = obj->getCurrentWindow(); + if (win) { + win->showOpenedFileList(); + } +} + +void VEditArea::activateSplitLeftByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + obj->focusNextWindow(-1); +} + +void VEditArea::activateSplitRightByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + obj->focusNextWindow(1); +} + +void VEditArea::moveTabSplitLeftByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + obj->moveCurrentTabOneSplit(false); +} + +void VEditArea::moveTabSplitRightByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + obj->moveCurrentTabOneSplit(true); +} + +void VEditArea::activateNextTabByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + VEditWindow *win = obj->getCurrentWindow(); + if (win) { + win->focusNextTab(true); + } +} + +void VEditArea::activatePreviousTabByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + VEditWindow *win = obj->getCurrentWindow(); + if (win) { + win->focusNextTab(false); + } +} + +void VEditArea::verticalSplitByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + obj->splitCurrentWindow(); +} + +void VEditArea::removeSplitByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + obj->removeCurrentWindow(); +} + +void VEditArea::evaluateMagicWordsByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + VEditTab *tab = obj->getCurrentTab(); + if (tab && tab->tabHasFocus()) { + tab->evaluateMagicWords(); + } +} + diff --git a/src/veditarea.h b/src/veditarea.h index b8ee6824..0172f8a0 100644 --- a/src/veditarea.h +++ b/src/veditarea.h @@ -12,10 +12,8 @@ #include #include "vnotebook.h" #include "veditwindow.h" -#include "vtoc.h" #include "vnavigationmode.h" -class VNote; class VFile; class VDirectory; class VFindReplaceDialog; @@ -26,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); @@ -35,17 +33,31 @@ public: bool closeFile(const VFile *p_file, bool p_forced); bool closeFile(const VDirectory *p_dir, bool p_forced); bool closeFile(const VNotebook *p_notebook, bool p_forced); - // Returns current edit tab. - VEditTab *currentEditTab(); - // Returns the count of VEditWindow. - inline int windowCount() const; + + // Return current edit window. + VEditWindow *getCurrentWindow() const; + + // Return current edit tab. + VEditTab *getCurrentTab() const; + + // Return the @p_tabIdx tab in the @p_winIdx window. + VEditTab *getTab(int p_winIdx, int p_tabIdx) const; + + // Return VEditTabInfo of all edit tabs. + QVector getAllTabsInfo() const; + + // Return the count of VEditWindow. + int windowCount() const; + // Returns the index of @p_window. int windowIndex(const VEditWindow *p_window) const; // Move tab widget @p_widget from window @p_fromIdx to @p_toIdx. // @p_widget has been removed from the original window. // If fail, just delete the p_widget. void moveTab(QWidget *p_widget, int p_fromIdx, int p_toIdx); - inline VFindReplaceDialog *getFindReplaceDialog() const; + + VFindReplaceDialog *getFindReplaceDialog() const; + // Return selected text of current edit tab. QString getSelectedText(); void splitCurrentWindow(); @@ -54,7 +66,6 @@ public: // Return the new current window index, otherwise, return -1. int focusNextWindow(int p_biaIdx); void moveCurrentTabOneSplit(bool p_right); - VEditWindow *getCurrentWindow() const; // Implementations for VNavigationMode. void registerNavigation(QChar p_majorKey) Q_DECL_OVERRIDE; @@ -62,12 +73,18 @@ public: void hideNavigation() Q_DECL_OVERRIDE; bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE; + // Open files @p_files. + int openFiles(const QVector &p_files); + signals: // Emit when current window's tab status updated. void tabStatusUpdated(const VEditTabInfo &p_info); - void outlineChanged(const VToc &toc); - void curHeaderChanged(const VAnchor &anchor); + // Emit when current window's tab's outline changed. + void outlineChanged(const VTableOfContent &p_outline); + + // Emit when current window's tab's current header changed. + void currentHeaderChanged(const VHeaderPointer &p_header); // Emit when want to show message in status bar. void statusMessage(const QString &p_msg); @@ -84,13 +101,16 @@ public slots: // @p_forceMode is true. // A given file can be opened in multiple split windows. A given file could be // opened at most in one tab inside a window. - void openFile(VFile *p_file, OpenFileMode p_mode, bool p_forceMode = false); + VEditTab *openFile(VFile *p_file, OpenFileMode p_mode, bool p_forceMode = false); void editFile(); void saveFile(); void readFile(); void saveAndReadFile(); - void handleOutlineItemActivated(const VAnchor &anchor); + + // Scroll current tab to @p_header. + void scrollToHeader(const VHeaderPointer &p_header); + void handleFileUpdated(const VFile *p_file); void handleDirectoryUpdated(const VDirectory *p_dir); void handleNotebookUpdated(const VNotebook *p_notebook); @@ -102,8 +122,11 @@ private slots: void handleRemoveSplitRequest(VEditWindow *curWindow); void handleWindowFocused(); - void handleOutlineChanged(const VToc &toc); - void handleCurHeaderChanged(const VAnchor &anchor); + + void handleWindowOutlineChanged(const VTableOfContent &p_outline); + + void handleWindowCurrentHeaderChanged(const VHeaderPointer &p_header); + void handleFindTextChanged(const QString &p_text, uint p_options); void handleFindOptionChanged(uint p_options); void handleFindNext(const QString &p_text, uint p_options, bool p_forward); @@ -128,14 +151,48 @@ private: int openFileInWindow(int windowIndex, VFile *p_file, OpenFileMode p_mode); void setCurrentTab(int windowIndex, int tabIndex, bool setFocus); void setCurrentWindow(int windowIndex, bool setFocus); - inline VEditWindow *getWindow(int windowIndex) const; + + VEditWindow *getWindow(int windowIndex) const; + void insertSplitWindow(int idx); void removeSplitWindow(VEditWindow *win); // Update status of current window. void updateWindowStatus(); - VNote *vnote; + // Init targets for Captain mode. + void registerCaptainTargets(); + + // Captain mode functions. + + // Activate tab @p_idx. + static void activateTabByCaptain(void *p_target, void *p_data, int p_idx); + + static void alternateTabByCaptain(void *p_target, void *p_data); + + static void showOpenedFileListByCaptain(void *p_target, void *p_data); + + static void activateSplitLeftByCaptain(void *p_target, void *p_data); + + static void activateSplitRightByCaptain(void *p_target, void *p_data); + + static void moveTabSplitLeftByCaptain(void *p_target, void *p_data); + + static void moveTabSplitRightByCaptain(void *p_target, void *p_data); + + static void activateNextTabByCaptain(void *p_target, void *p_data); + + static void activatePreviousTabByCaptain(void *p_target, void *p_data); + + static void verticalSplitByCaptain(void *p_target, void *p_data); + + static void removeSplitByCaptain(void *p_target, void *p_data); + + // Evaluate selected text or the word on cursor as magic words. + static void evaluateMagicWordsByCaptain(void *p_target, void *p_data); + + // End Captain mode functions. + int curWindowIndex; // Splitter holding multiple split windows diff --git a/src/veditoperations.cpp b/src/veditoperations.cpp index 18c87bb3..74e3183d 100644 --- a/src/veditoperations.cpp +++ b/src/veditoperations.cpp @@ -90,3 +90,10 @@ void VEditOperations::requestUpdateVimStatus() { emit vimStatusUpdated(m_vim); } + +void VEditOperations::setVimMode(VimMode p_mode) +{ + if (m_vim && m_editConfig->m_enableVimMode) { + m_vim->setMode(p_mode); + } +} diff --git a/src/veditoperations.h b/src/veditoperations.h index cc6b2371..8085e2f1 100644 --- a/src/veditoperations.h +++ b/src/veditoperations.h @@ -18,11 +18,18 @@ class VEditOperations: public QObject Q_OBJECT public: VEditOperations(VEdit *p_editor, VFile *p_file); + virtual ~VEditOperations(); + virtual bool insertImageFromMimeData(const QMimeData *source) = 0; + virtual bool insertImage() = 0; + virtual bool insertImageFromURL(const QUrl &p_imageUrl) = 0; + virtual bool insertLink(const QString &p_linkText, + const QString &p_linkUrl) = 0; + // Return true if @p_event has been handled and no need to be further // processed. virtual bool handleKeyPressEvent(QKeyEvent *p_event) = 0; @@ -33,6 +40,9 @@ public: // Insert decoration markers or decorate selected text. virtual void decorateText(TextDecoration p_decoration) {Q_UNUSED(p_decoration);}; + // Set Vim mode if not NULL. + void setVimMode(VimMode p_mode); + signals: // Want to display a template message in status bar. void statusMessage(const QString &p_msg); diff --git a/src/vedittab.cpp b/src/vedittab.cpp index 0f34297c..db8c4400 100644 --- a/src/vedittab.cpp +++ b/src/vedittab.cpp @@ -3,12 +3,13 @@ #include VEditTab::VEditTab(VFile *p_file, VEditArea *p_editArea, QWidget *p_parent) - : QWidget(p_parent), m_file(p_file), m_isEditMode(false), - m_modified(false), m_editArea(p_editArea) + : QWidget(p_parent), + m_file(p_file), + m_isEditMode(false), + m_outline(p_file), + m_currentHeader(p_file, -1), + m_editArea(p_editArea) { - m_toc.m_file = m_file; - m_curHeader.m_file = m_file; - connect(qApp, &QApplication::focusChanged, this, &VEditTab::handleFocusChanged); } @@ -24,6 +25,7 @@ void VEditTab::focusTab() { focusChild(); emit getFocused(); + updateStatus(); } bool VEditTab::isEditMode() const @@ -33,7 +35,7 @@ bool VEditTab::isEditMode() const bool VEditTab::isModified() const { - return m_modified; + return m_file->isModified(); } VFile *VEditTab::getFile() const @@ -48,21 +50,13 @@ void VEditTab::handleFocusChanged(QWidget * /* p_old */, QWidget *p_now) focusChild(); emit getFocused(); + updateStatus(); } else if (isAncestorOf(p_now)) { emit getFocused(); + updateStatus(); } } -void VEditTab::requestUpdateCurHeader() -{ - emit curHeaderChanged(m_curHeader); -} - -void VEditTab::requestUpdateOutline() -{ - emit outlineChanged(m_toc); -} - void VEditTab::wheelEvent(QWheelEvent *p_event) { QPoint angle = p_event->angleDelta(); @@ -78,17 +72,55 @@ void VEditTab::wheelEvent(QWheelEvent *p_event) p_event->ignore(); } -void VEditTab::updateStatus() -{ - m_modified = m_file->isModified(); - - emit statusUpdated(createEditTabInfo()); -} - -VEditTabInfo VEditTab::createEditTabInfo() +VEditTabInfo VEditTab::fetchTabInfo() const { VEditTabInfo info; - info.m_editTab = this; + info.m_editTab = const_cast(this); return info; } + +const VHeaderPointer &VEditTab::getCurrentHeader() const +{ + return m_currentHeader; +} + +const VTableOfContent &VEditTab::getOutline() const +{ + return m_outline; +} + +void VEditTab::tryRestoreFromTabInfo(const VEditTabInfo &p_info) +{ + if (p_info.m_editTab != this) { + m_infoToRestore.clear(); + return; + } + + if (restoreFromTabInfo(p_info)) { + m_infoToRestore.clear(); + return; + } + + // Save it and restore later. + m_infoToRestore = p_info; +} + +void VEditTab::updateStatus() +{ + emit statusUpdated(fetchTabInfo()); +} + +void VEditTab::evaluateMagicWords() +{ +} + +bool VEditTab::tabHasFocus() const +{ + QWidget *wid = QApplication::focusWidget(); + return wid == this || isAncestorOf(wid); +} + +void VEditTab::insertLink() +{ +} diff --git a/src/vedittab.h b/src/vedittab.h index 116c814b..23110fa2 100644 --- a/src/vedittab.h +++ b/src/vedittab.h @@ -4,7 +4,7 @@ #include #include #include -#include "vtoc.h" +#include "vtableofcontent.h" #include "vfile.h" #include "utils/vvim.h" #include "vedittabinfo.h" @@ -37,18 +37,21 @@ public: void focusTab(); - virtual void requestUpdateOutline(); + // Whether this tab has focus. + bool tabHasFocus() const; - virtual void requestUpdateCurHeader(); - - // Scroll to anchor @p_anchor. - virtual void scrollToAnchor(const VAnchor& p_anchor) = 0; + // Scroll to @p_header. + // Will emit currentHeaderChanged() if @p_header is valid. + virtual void scrollToHeader(const VHeaderPointer &p_header) { Q_UNUSED(p_header) } VFile *getFile() const; // User requests to insert image. virtual void insertImage() = 0; + // User requests to insert link. + virtual void insertLink(); + // Search @p_text in current note. virtual void findText(const QString &p_text, uint p_options, bool p_peek, bool p_forward = true) = 0; @@ -69,15 +72,29 @@ public: virtual void requestUpdateVimStatus() = 0; // Insert decoration markers or decorate selected text. - virtual void decorateText(TextDecoration p_decoration) {Q_UNUSED(p_decoration);}; + virtual void decorateText(TextDecoration p_decoration) {Q_UNUSED(p_decoration);} + + // Create a filled VEditTabInfo. + virtual VEditTabInfo fetchTabInfo() const; + + const VTableOfContent &getOutline() const; + + const VHeaderPointer &getCurrentHeader() const; + + // Restore status from @p_info. + // If this tab is not ready yet, it will restore once it is ready. + void tryRestoreFromTabInfo(const VEditTabInfo &p_info); + + // Emit signal to update current status. + virtual void updateStatus(); + + // Called by evaluateMagicWordsByCaptain() to evaluate the magic words. + virtual void evaluateMagicWords(); public slots: // Enter edit mode virtual void editFile() = 0; - // Update status of current tab. Emit statusUpdated(). - virtual void updateStatus(); - protected: void wheelEvent(QWheelEvent *p_event) Q_DECL_OVERRIDE; @@ -87,23 +104,32 @@ protected: // Called to zoom in/out content. virtual void zoom(bool p_zoomIn, qreal p_step = 0.25) = 0; - // Create a filled VEditTabInfo. - virtual VEditTabInfo createEditTabInfo(); + // Restore from @p_fino. + // Return true if succeed. + virtual bool restoreFromTabInfo(const VEditTabInfo &p_info) = 0; // File related to this tab. QPointer m_file; + bool m_isEditMode; - bool m_modified; - VToc m_toc; - VAnchor m_curHeader; + + // Table of content of this tab. + VTableOfContent m_outline; + + // Current header in m_outline of this tab. + VHeaderPointer m_currentHeader; + VEditArea *m_editArea; + // Tab info to restore from once ready. + VEditTabInfo m_infoToRestore; + signals: void getFocused(); - void outlineChanged(const VToc &p_toc); + void outlineChanged(const VTableOfContent &p_outline); - void curHeaderChanged(const VAnchor &p_anchor); + void currentHeaderChanged(const VHeaderPointer &p_header); // The status of current tab has updates. void statusUpdated(const VEditTabInfo &p_info); diff --git a/src/vedittabinfo.h b/src/vedittabinfo.h index 5fc4003b..f80fd623 100644 --- a/src/vedittabinfo.h +++ b/src/vedittabinfo.h @@ -6,8 +6,22 @@ class VEditTab; struct VEditTabInfo { VEditTabInfo() - : m_editTab(NULL), m_cursorBlockNumber(-1), m_cursorPositionInBlock(-1), - m_blockCount(-1) {} + : m_editTab(NULL), + m_cursorBlockNumber(-1), + m_cursorPositionInBlock(-1), + m_blockCount(-1), + m_headerIndex(-1) + { + } + + void clear() + { + m_editTab = NULL; + m_cursorBlockNumber = -1; + m_cursorPositionInBlock = -1; + m_blockCount = -1; + m_headerIndex = -1; + } VEditTab *m_editTab; @@ -15,6 +29,9 @@ struct VEditTabInfo int m_cursorBlockNumber; int m_cursorPositionInBlock; int m_blockCount; + + // Header index in outline. + int m_headerIndex; }; #endif // VEDITTABINFO_H diff --git a/src/veditwindow.cpp b/src/veditwindow.cpp index 857a2606..bc4a9e3a 100644 --- a/src/veditwindow.cpp +++ b/src/veditwindow.cpp @@ -2,7 +2,6 @@ #include #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((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); } }); @@ -262,8 +263,10 @@ void VEditWindow::removeEditTab(int p_index) int VEditWindow::insertEditTab(int p_index, VFile *p_file, QWidget *p_page) { - int idx = insertTab(p_index, p_page, p_file->getName()); - setTabToolTip(idx, generateTooltip(p_file)); + int idx = insertTab(p_index, + p_page, + p_file->getName()); + updateTabInfo(idx); return idx; } @@ -466,15 +469,17 @@ void VEditWindow::updateTabStatus(int p_index) if (p_index == -1) { emit tabStatusUpdated(VEditTabInfo()); - emit outlineChanged(VToc()); - emit curHeaderChanged(VAnchor()); + emit outlineChanged(VTableOfContent()); + emit currentHeaderChanged(VHeaderPointer()); return; } VEditTab *tab = getTab(p_index); - tab->updateStatus(); - tab->requestUpdateOutline(); - tab->requestUpdateCurHeader(); + emit tabStatusUpdated(tab->fetchTabInfo()); + emit outlineChanged(tab->getOutline()); + emit currentHeaderChanged(tab->getCurrentHeader()); + + updateTabInfo(p_index); } void VEditWindow::updateTabInfo(int p_index) @@ -483,8 +488,7 @@ void VEditWindow::updateTabInfo(int p_index) const VFile *file = editor->getFile(); bool editMode = editor->isEditMode(); - setTabText(p_index, generateTabText(p_index, file->getName(), - file->isModified(), file->isModifiable())); + setTabText(p_index, generateTabText(p_index, file)); setTabToolTip(p_index, generateTooltip(file)); QString iconUrl(":/resources/icons/reading.svg"); @@ -501,31 +505,28 @@ void VEditWindow::updateAllTabsSequence() for (int i = 0; i < count(); ++i) { VEditTab *editor = getTab(i); const VFile *file = editor->getFile(); - setTabText(i, generateTabText(i, file->getName(), - file->isModified(), file->isModifiable())); + setTabText(i, generateTabText(i, file)); } } -// Be requested to report current outline -void VEditWindow::requestUpdateOutline() +VTableOfContent VEditWindow::getOutline() const { int idx = currentIndex(); if (idx == -1) { - emit outlineChanged(VToc()); - return; + return VTableOfContent(); } - getTab(idx)->requestUpdateOutline(); + + return getTab(idx)->getOutline(); } -// Be requested to report current header -void VEditWindow::requestUpdateCurHeader() +VHeaderPointer VEditWindow::getCurrentHeader() const { int idx = currentIndex(); if (idx == -1) { - emit curHeaderChanged(VAnchor()); - return; + return VHeaderPointer(); } - getTab(idx)->requestUpdateCurHeader(); + + return getTab(idx)->getCurrentHeader(); } // Focus this windows. Try to focus current tab. @@ -681,44 +682,39 @@ bool VEditWindow::canRemoveSplit() return splitter->count() > 1; } -void VEditWindow::handleOutlineChanged(const VToc &p_toc) +void VEditWindow::handleTabOutlineChanged(const VTableOfContent &p_outline) { - // Only propagate it if it is current tab - int idx = currentIndex(); - if (idx == -1) { - emit outlineChanged(VToc()); + // Only propagate it if it is current tab. + VEditTab *tab = getCurrentTab(); + if (tab) { + if (tab->getFile() == p_outline.getFile()) { + emit outlineChanged(p_outline); + } + } else { + emit outlineChanged(VTableOfContent()); return; } - const VFile *file = getTab(idx)->getFile(); - if (p_toc.m_file == file) { - emit outlineChanged(p_toc); - } } -void VEditWindow::handleCurHeaderChanged(const VAnchor &p_anchor) +void VEditWindow::handleTabCurrentHeaderChanged(const VHeaderPointer &p_header) { - // Only propagate it if it is current tab - int idx = currentIndex(); - if (idx == -1) { - emit curHeaderChanged(VAnchor()); + // Only propagate it if it is current tab. + VEditTab *tab = getCurrentTab(); + if (tab) { + if (tab->getFile() == p_header.m_file) { + emit currentHeaderChanged(p_header); + } + } else { + emit currentHeaderChanged(VHeaderPointer()); return; } - const VFile *file = getTab(idx)->getFile(); - if (p_anchor.m_file == file) { - emit curHeaderChanged(p_anchor); - } } -void VEditWindow::scrollCurTab(const VAnchor &p_anchor) +void VEditWindow::scrollToHeader(const VHeaderPointer &p_header) { - int idx = currentIndex(); - if (idx == -1) { - emit curHeaderChanged(VAnchor()); - return; - } - const VFile *file = getTab(idx)->getFile(); - if (file == p_anchor.m_file) { - getTab(idx)->scrollToAnchor(p_anchor); + VEditTab *tab = getCurrentTab(); + if (tab) { + tab->scrollToHeader(p_header); } } @@ -794,22 +790,37 @@ void VEditWindow::updateNotebookInfo(const VNotebook *p_notebook) } } -VEditTab *VEditWindow::currentEditTab() +VEditTab *VEditWindow::getCurrentTab() const { int idx = currentIndex(); if (idx == -1) { return NULL; } + return getTab(idx); } +QVector VEditWindow::getAllTabsInfo() const +{ + int nrTab = count(); + + QVector tabs; + tabs.reserve(nrTab); + for (int i = 0; i < nrTab; ++i) { + VEditTab *editTab = getTab(i); + tabs.push_back(editTab->fetchTabInfo()); + } + + return tabs; +} + void VEditWindow::handleLocateAct() { int tab = m_locateAct->data().toInt(); VEditTab *editor = getTab(tab); QPointer file = editor->getFile(); if (file->getType() == FileType::Note) { - vnote->getMainWindow()->locateFile(file); + g_mainWin->locateFile(file); } } @@ -901,9 +912,9 @@ void VEditWindow::connectEditTab(const VEditTab *p_tab) connect(p_tab, &VEditTab::getFocused, this, &VEditWindow::getFocused); connect(p_tab, &VEditTab::outlineChanged, - this, &VEditWindow::handleOutlineChanged); - connect(p_tab, &VEditTab::curHeaderChanged, - this, &VEditWindow::handleCurHeaderChanged); + this, &VEditWindow::handleTabOutlineChanged); + connect(p_tab, &VEditTab::currentHeaderChanged, + this, &VEditWindow::handleTabCurrentHeaderChanged); connect(p_tab, &VEditTab::statusUpdated, this, &VEditWindow::handleTabStatusUpdated); connect(p_tab, &VEditTab::statusMessage, @@ -1014,7 +1025,7 @@ void VEditWindow::dropEvent(QDropEvent *p_event) if (!files.isEmpty()) { focusWindow(); - g_vnote->getMainWindow()->openExternalFiles(files); + g_mainWin->openFiles(files); } p_event->acceptProposedAction(); diff --git a/src/veditwindow.h b/src/veditwindow.h index 40e6b176..8e58ebb0 100644 --- a/src/veditwindow.h +++ b/src/veditwindow.h @@ -8,11 +8,9 @@ #include #include "vnotebook.h" #include "vedittab.h" -#include "vtoc.h" #include "vconstants.h" #include "vnotefile.h" -class VNote; class QPushButton; class QActionGroup; class VEditArea; @@ -21,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); @@ -32,15 +30,29 @@ public: void readFile(); void saveAndReadFile(); bool closeAllFiles(bool p_forced); - void requestUpdateOutline(); - void requestUpdateCurHeader(); + + // Return outline of current tab. + VTableOfContent getOutline() const; + + // Return current header of current tab. + VHeaderPointer getCurrentHeader() const; + // Focus to current tab's editor void focusWindow(); - void scrollCurTab(const VAnchor &p_anchor); + + // Scroll current tab to header @p_header. + void scrollToHeader(const VHeaderPointer &p_header); + void updateFileInfo(const VFile *p_file); void updateDirectoryInfo(const VDirectory *p_dir); void updateNotebookInfo(const VNotebook *p_notebook); - VEditTab *currentEditTab(); + + VEditTab *getCurrentTab() const; + + VEditTab *getTab(int tabIndex) const; + + QVector getAllTabsInfo() const; + // Insert a tab with @p_widget. @p_widget is a fully initialized VEditTab. bool addEditTab(QWidget *p_widget); // Set whether it is the current window. @@ -53,7 +65,6 @@ public: bool activateTab(int p_sequence); // Switch to previous activated tab. bool alternateTab(); - VEditTab *getTab(int tabIndex) const; // Ask tab @p_index to update its status and propogate. // The status here means tab status, outline, current header. @@ -79,8 +90,10 @@ signals: void requestRemoveSplit(VEditWindow *curWindow); // This widget or its children get the focus void getFocused(); - void outlineChanged(const VToc &toc); - void curHeaderChanged(const VAnchor &anchor); + + void outlineChanged(const VTableOfContent &p_outline); + + void currentHeaderChanged(const VHeaderPointer &p_header); // Emit when want to show message in status bar. void statusMessage(const QString &p_msg); @@ -100,8 +113,11 @@ private slots: void handleCurrentIndexChanged(int p_index); void contextMenuRequested(QPoint pos); void tabListJump(VFile *p_file); - void handleOutlineChanged(const VToc &p_toc); - void handleCurHeaderChanged(const VAnchor &p_anchor); + + void handleTabOutlineChanged(const VTableOfContent &p_outline); + + void handleTabCurrentHeaderChanged(const VHeaderPointer &p_header); + void updateSplitMenu(); void tabbarContextMenuRequested(QPoint p_pos); void handleLocateAct(); @@ -124,9 +140,11 @@ private: int insertEditTab(int p_index, VFile *p_file, QWidget *p_page); int appendEditTab(VFile *p_file, QWidget *p_page); int openFileInTab(VFile *p_file, OpenFileMode p_mode); - inline QString generateTooltip(const VFile *p_file) const; - inline QString generateTabText(int p_index, const QString &p_name, - bool p_modified, bool p_modifiable) const; + + QString generateTooltip(const VFile *p_file) const; + + QString generateTabText(int p_index, const VFile *p_file) const; + bool canRemoveSplit(); // Move tab at @p_tabIdx one split window. @@ -135,6 +153,7 @@ private: // and move the tab to the new split. void moveTabOneSplit(int p_tabIdx, bool p_right); + // Update info of tab @p_idx according to the state of the editor and file. void updateTabInfo(int p_idx); // Update the sequence number of all the tabs. @@ -143,7 +162,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(). @@ -197,11 +215,16 @@ inline QString VEditWindow::generateTooltip(const VFile *p_file) const } } -inline QString VEditWindow::generateTabText(int p_index, const QString &p_name, - bool p_modified, bool p_modifiable) const +inline QString VEditWindow::generateTabText(int p_index, const VFile *p_file) const { - QString seq = QString::number(p_index + c_tabSequenceBase, 10); - return seq + ". " + p_name + (p_modifiable ? (p_modified ? "*" : "") : "#"); + if (!p_file) { + return ""; + } + + return QString("%1.%2%3").arg(QString::number(p_index + c_tabSequenceBase, 10)) + .arg(p_file->getName()) + .arg(p_file->isModifiable() + ? (p_file->isModified() ? "*" : "") : "#"); } #endif // VEDITWINDOW_H diff --git a/src/vfilelist.cpp b/src/vfilelist.cpp index 5629ad37..54621ab1 100644 --- a/src/vfilelist.cpp +++ b/src/vfilelist.cpp @@ -263,7 +263,7 @@ void VFileList::fileInfo(VNoteFile *p_file) void VFileList::fillItem(QListWidgetItem *p_item, const VNoteFile *p_file) { - unsigned long long ptr = (long long)p_file; + qulonglong ptr = (qulonglong)p_file; p_item->setData(Qt::UserRole, ptr); p_item->setToolTip(p_file->getName()); p_item->setText(p_file->getName()); @@ -330,7 +330,9 @@ void VFileList::newFile() info = info + "
" + tr("Note with name ending with \"%1\" will be treated as Markdown type.") .arg(suffixStr); QString defaultName = QString("new_note.%1").arg(defaultSuf); - defaultName = VUtils::getFileNameWithSequence(m_directory->fetchPath(), defaultName); + defaultName = VUtils::getFileNameWithSequence(m_directory->fetchPath(), + defaultName, + true); VNewFileDialog dialog(tr("Create Note"), info, defaultName, m_directory, this); if (dialog.exec() == QDialog::Accepted) { VNoteFile *file = m_directory->createFile(dialog.getNameInput()); @@ -349,7 +351,7 @@ void VFileList::newFile() qWarning() << "fail to open newly-created note" << file->getName(); } else { Q_ASSERT(file->getContent().isEmpty()); - QString content = QString("# %1\n").arg(QFileInfo(file->getName()).baseName()); + QString content = QString("# %1\n").arg(QFileInfo(file->getName()).completeBaseName()); file->setContent(content); if (!file->save()) { qWarning() << "fail to write to newly-created note" << file->getName(); @@ -372,7 +374,7 @@ void VFileList::newFile() // Move cursor down if content has been inserted. if (contentInserted) { - const VMdTab *tab = dynamic_cast(editArea->currentEditTab()); + const VMdTab *tab = dynamic_cast(editArea->getCurrentTab()); if (tab) { VMdEdit *edit = dynamic_cast(tab->getEditor()); if (edit && edit->getFile() == file) { @@ -621,7 +623,7 @@ bool VFileList::importFiles(const QStringList &p_files, QString *p_errMsg) QString name = VUtils::fileNameFromPath(file); Q_ASSERT(!name.isEmpty()); - name = VUtils::getFileNameWithSequence(dirPath, name); + name = VUtils::getFileNameWithSequence(dirPath, name, true); QString targetFilePath = dir.filePath(name); bool ret = VUtils::copyFile(file, targetFilePath, false); if (!ret) { @@ -763,10 +765,14 @@ void VFileList::pasteFiles(VDirectory *p_destDir, } // Rename it to xxx_copy.md. - fileName = VUtils::generateCopiedFileName(file->fetchBasePath(), fileName); + fileName = VUtils::generateCopiedFileName(file->fetchBasePath(), + fileName, + true); } else { // Rename it to xxx_copy.md if needed. - fileName = VUtils::generateCopiedFileName(p_destDir->fetchPath(), fileName); + fileName = VUtils::generateCopiedFileName(p_destDir->fetchPath(), + fileName, + true); } QString msg; diff --git a/src/vfilelist.h b/src/vfilelist.h index ac24d9eb..6fa0258f 100644 --- a/src/vfilelist.h +++ b/src/vfilelist.h @@ -44,7 +44,9 @@ public: // Locate @p_file in the list widget. bool locateFile(const VNoteFile *p_file); - inline const VDirectory *currentDirectory() const; + const VDirectory *currentDirectory() const; + + QWidget *getContentWidget() const; // Implementations for VNavigationMode. void registerNavigation(QChar p_majorKey) Q_DECL_OVERRIDE; @@ -199,4 +201,9 @@ inline const VDirectory *VFileList::currentDirectory() const return m_directory; } +inline QWidget *VFileList::getContentWidget() const +{ + return fileList; +} + #endif // VFILELIST_H diff --git a/src/vfilesessioninfo.cpp b/src/vfilesessioninfo.cpp new file mode 100644 index 00000000..bdffad1f --- /dev/null +++ b/src/vfilesessioninfo.cpp @@ -0,0 +1,74 @@ +#include "vfilesessioninfo.h" + +#include + +#include "vedittabinfo.h" +#include "vtableofcontent.h" +#include "vedittab.h" + + +VFileSessionInfo::VFileSessionInfo() + : m_mode(OpenFileMode::Read), + m_headerIndex(-1), + m_cursorBlockNumber(-1), + m_cursorPositionInBlock(-1) +{ +} + +VFileSessionInfo::VFileSessionInfo(const QString &p_file, + OpenFileMode p_mode) + : m_file(p_file), + m_mode(p_mode), + m_headerIndex(-1), + m_cursorBlockNumber(-1), + m_cursorPositionInBlock(-1) +{ +} + +// Fetch VFileSessionInfo from @p_tabInfo. +VFileSessionInfo VFileSessionInfo::fromEditTabInfo(const VEditTabInfo *p_tabInfo) +{ + Q_ASSERT(p_tabInfo); + VEditTab *tab = p_tabInfo->m_editTab; + VFileSessionInfo info(tab->getFile()->fetchPath(), + tab->isEditMode() ? OpenFileMode::Edit : OpenFileMode::Read); + info.m_headerIndex = p_tabInfo->m_headerIndex; + info.m_cursorBlockNumber = p_tabInfo->m_cursorBlockNumber; + info.m_cursorPositionInBlock = p_tabInfo->m_cursorPositionInBlock; + + return info; +} + +void VFileSessionInfo::toEditTabInfo(VEditTabInfo *p_tabInfo) const +{ + p_tabInfo->m_headerIndex = m_headerIndex; + p_tabInfo->m_cursorBlockNumber = m_cursorBlockNumber; + p_tabInfo->m_cursorPositionInBlock = m_cursorPositionInBlock; +} + +VFileSessionInfo VFileSessionInfo::fromSettings(const QSettings *p_settings) +{ + VFileSessionInfo info; + info.m_file = p_settings->value(FileSessionConfig::c_file).toString(); + int tmpMode = p_settings->value(FileSessionConfig::c_mode).toInt(); + if (tmpMode >= (int)OpenFileMode::Read && tmpMode < (int)OpenFileMode::Invalid) { + info.m_mode = (OpenFileMode)tmpMode; + } else { + info.m_mode = OpenFileMode::Read; + } + + info.m_headerIndex = p_settings->value(FileSessionConfig::c_headerIndex).toInt(); + info.m_cursorBlockNumber = p_settings->value(FileSessionConfig::c_cursorBlockNumber).toInt(); + info.m_cursorPositionInBlock = p_settings->value(FileSessionConfig::c_cursorPositionInBlock).toInt(); + + return info; +} + +void VFileSessionInfo::toSettings(QSettings *p_settings) const +{ + p_settings->setValue(FileSessionConfig::c_file, m_file); + p_settings->setValue(FileSessionConfig::c_mode, (int)m_mode); + p_settings->setValue(FileSessionConfig::c_headerIndex, m_headerIndex); + p_settings->setValue(FileSessionConfig::c_cursorBlockNumber, m_cursorBlockNumber); + p_settings->setValue(FileSessionConfig::c_cursorPositionInBlock, m_cursorPositionInBlock); +} diff --git a/src/vfilesessioninfo.h b/src/vfilesessioninfo.h new file mode 100644 index 00000000..f0fd8673 --- /dev/null +++ b/src/vfilesessioninfo.h @@ -0,0 +1,57 @@ +#ifndef VFILESESSIONINFO_H +#define VFILESESSIONINFO_H + +#include "vconstants.h" + +struct VEditTabInfo; +class QSettings; + +namespace FileSessionConfig +{ + static const QString c_file = "file"; + static const QString c_mode = "mode"; + + // Index in outline of the anchor. + static const QString c_headerIndex = "header_index"; + + static const QString c_cursorBlockNumber = "cursor_block_number"; + static const QString c_cursorPositionInBlock = "cursor_position_in_block"; +} + +// Information about an opened file (session). +class VFileSessionInfo +{ +public: + VFileSessionInfo(); + + VFileSessionInfo(const QString &p_file, + OpenFileMode p_mode); + + // Fetch VFileSessionInfo from @p_tabInfo. + static VFileSessionInfo fromEditTabInfo(const VEditTabInfo *p_tabInfo); + + // Fill corresponding fields of @p_tabInfo. + void toEditTabInfo(VEditTabInfo *p_tabInfo) const; + + // Fetch VFileSessionInfo from @p_settings. + static VFileSessionInfo fromSettings(const QSettings *p_settings); + + void toSettings(QSettings *p_settings) const; + + // Absolute path of the file. + QString m_file; + + // Mode of this file in this session. + OpenFileMode m_mode; + + // Index in outline of the header. + int m_headerIndex; + + // Block number of cursor block. + int m_cursorBlockNumber; + + // Position in block of cursor. + int m_cursorPositionInBlock; +}; + +#endif // VFILESESSIONINFO_H diff --git a/src/vhtmltab.cpp b/src/vhtmltab.cpp index d41c32d3..6c8fb40c 100644 --- a/src/vhtmltab.cpp +++ b/src/vhtmltab.cpp @@ -32,7 +32,7 @@ void VHtmlTab::setupUI() { m_editor = new VEdit(m_file, this); connect(m_editor, &VEdit::textChanged, - this, &VHtmlTab::handleTextChanged); + this, &VHtmlTab::updateStatus); connect(m_editor, &VEdit::saveAndRead, this, &VHtmlTab::saveAndRead); connect(m_editor, &VEdit::discardAndRead, @@ -52,17 +52,6 @@ void VHtmlTab::setupUI() setLayout(mainLayout); } -void VHtmlTab::handleTextChanged() -{ - V_ASSERT(m_file->isModifiable()); - - if (m_modified) { - return; - } - - updateStatus(); -} - void VHtmlTab::showFileReadMode() { m_isEditMode = false; @@ -194,10 +183,6 @@ void VHtmlTab::discardAndRead() readFile(); } -void VHtmlTab::scrollToAnchor(const VAnchor & /* p_anchor */) -{ -} - void VHtmlTab::insertImage() { } @@ -252,3 +237,12 @@ void VHtmlTab::requestUpdateVimStatus() { m_editor->requestUpdateVimStatus(); } + +bool VHtmlTab::restoreFromTabInfo(const VEditTabInfo &p_info) +{ + if (p_info.m_editTab != this) { + return false; + } + + return true; +} diff --git a/src/vhtmltab.h b/src/vhtmltab.h index 9024dfd6..80471fa5 100644 --- a/src/vhtmltab.h +++ b/src/vhtmltab.h @@ -26,9 +26,6 @@ public: // Save file. bool saveFile() Q_DECL_OVERRIDE; - // Scroll to anchor @p_anchor. - void scrollToAnchor(const VAnchor& p_anchor) Q_DECL_OVERRIDE; - void insertImage() Q_DECL_OVERRIDE; // Search @p_text in current note. @@ -53,9 +50,6 @@ public slots: void editFile() Q_DECL_OVERRIDE; private slots: - // Handle text changed in m_editor. - void handleTextChanged(); - // m_editor requests to save changes and enter read mode. void saveAndRead(); @@ -78,6 +72,10 @@ private: // Focus the proper child widget. void focusChild() Q_DECL_OVERRIDE; + // Restore from @p_fino. + // Return true if succeed. + bool restoreFromTabInfo(const VEditTabInfo &p_info) Q_DECL_OVERRIDE; + VEdit *m_editor; }; #endif // VHTMLTAB_H diff --git a/src/vlineedit.cpp b/src/vlineedit.cpp new file mode 100644 index 00000000..ea716ce0 --- /dev/null +++ b/src/vlineedit.cpp @@ -0,0 +1,48 @@ +#include "vlineedit.h" + +#include +#include + +#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() +{ + m_evaluatedText = g_mwMgr->evaluate(text()); + + connect(this, &QLineEdit::textChanged, + this, &VLineEdit::handleTextChanged); +} + +const QString VLineEdit::getEvaluatedText() const +{ + return m_evaluatedText; +} diff --git a/src/vlineedit.h b/src/vlineedit.h new file mode 100644 index 00000000..81a8a86c --- /dev/null +++ b/src/vlineedit.h @@ -0,0 +1,29 @@ +#ifndef VLINEEDIT_H +#define VLINEEDIT_H + +#include + + +// 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 diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index fe1a0eec..068c2440 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -29,6 +29,9 @@ #include "vnotefile.h" #include "vbuttonwithwidget.h" #include "vattachmentlist.h" +#include "vfilesessioninfo.h" + +VMainWindow *g_mainWin; extern VConfigManager *g_config; @@ -40,16 +43,29 @@ const int VMainWindow::c_sharedMemTimerInterval = 1000; extern QFile g_logFile; #endif + VMainWindow::VMainWindow(VSingleInstanceGuard *p_guard, QWidget *p_parent) - : QMainWindow(p_parent), m_onePanel(false), m_guard(p_guard), + : QMainWindow(p_parent), m_guard(p_guard), m_windowOldState(Qt::WindowNoState), m_requestQuit(false) { + qsrand(QDateTime::currentDateTime().toTime_t()); + + g_mainWin = this; + setWindowIcon(QIcon(":/resources/icons/vnote.ico")); vnote = new VNote(this); g_vnote = vnote; vnote->initPalette(palette()); initPredefinedColorPixmaps(); + if (g_config->getEnableCompactMode()) { + m_panelViewState = PanelViewState::CompactMode; + } else { + m_panelViewState = PanelViewState::TwoPanels; + } + + initCaptain(); + setupUI(); initMenuBar(); @@ -58,13 +74,16 @@ VMainWindow::VMainWindow(VSingleInstanceGuard *p_guard, QWidget *p_parent) initAvatar(); restoreStateAndGeometry(); + + changePanelView(m_panelViewState); + setContextMenuPolicy(Qt::NoContextMenu); notebookSelector->update(); - initCaptain(); - initSharedMemoryWatcher(); + + registerCaptainAndNavigationTargets(); } void VMainWindow::initSharedMemoryWatcher() @@ -83,43 +102,82 @@ void VMainWindow::initCaptain() // VCaptain should be visible to accpet key focus. But VCaptain // may hide other widgets. m_captain = new VCaptain(this); - connect(m_captain, &VCaptain::captainModeChanged, - this, &VMainWindow::handleCaptainModeChanged); +} +void VMainWindow::registerCaptainAndNavigationTargets() +{ m_captain->registerNavigationTarget(notebookSelector); m_captain->registerNavigationTarget(directoryTree); - m_captain->registerNavigationTarget(fileList); + m_captain->registerNavigationTarget(m_fileList); m_captain->registerNavigationTarget(editArea); m_captain->registerNavigationTarget(outline); + + // Register Captain mode targets. + m_captain->registerCaptainTarget(tr("AttachmentList"), + g_config->getCaptainShortcutKeySequence("AttachmentList"), + this, + showAttachmentListByCaptain); + m_captain->registerCaptainTarget(tr("LocateCurrentFile"), + g_config->getCaptainShortcutKeySequence("LocateCurrentFile"), + this, + locateCurrentFileByCaptain); + m_captain->registerCaptainTarget(tr("ExpandMode"), + g_config->getCaptainShortcutKeySequence("ExpandMode"), + this, + toggleExpandModeByCaptain); + m_captain->registerCaptainTarget(tr("OnePanelView"), + g_config->getCaptainShortcutKeySequence("OnePanelView"), + this, + toggleOnePanelViewByCaptain); + m_captain->registerCaptainTarget(tr("DiscardAndRead"), + g_config->getCaptainShortcutKeySequence("DiscardAndRead"), + this, + discardAndReadByCaptain); + m_captain->registerCaptainTarget(tr("ToolsDock"), + g_config->getCaptainShortcutKeySequence("ToolsDock"), + this, + toggleToolsDockByCaptain); + m_captain->registerCaptainTarget(tr("CloseNote"), + g_config->getCaptainShortcutKeySequence("CloseNote"), + this, + closeFileByCaptain); + m_captain->registerCaptainTarget(tr("ShortcutsHelp"), + g_config->getCaptainShortcutKeySequence("ShortcutsHelp"), + this, + shortcutsHelpByCaptain); + m_captain->registerCaptainTarget(tr("FlushLogFile"), + g_config->getCaptainShortcutKeySequence("FlushLogFile"), + this, + flushLogFileByCaptain); } void VMainWindow::setupUI() { QWidget *directoryPanel = setupDirectoryPanel(); - fileList = new VFileList(); - fileList->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding); + 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(); - fileList->setEditArea(editArea); + m_fileList->setEditArea(editArea); directoryTree->setEditArea(editArea); - notebookSelector->setEditArea(editArea); // Main Splitter - mainSplitter = new QSplitter(); - mainSplitter->setObjectName("MainSplitter"); - mainSplitter->addWidget(directoryPanel); - mainSplitter->addWidget(fileList); - mainSplitter->addWidget(editArea); - mainSplitter->setStretchFactor(0, 0); - mainSplitter->setStretchFactor(1, 0); - mainSplitter->setStretchFactor(2, 1); + m_mainSplitter = new QSplitter(); + m_mainSplitter->setObjectName("MainSplitter"); + m_mainSplitter->addWidget(directoryPanel); + m_mainSplitter->addWidget(m_fileList); + setTabOrder(directoryTree, m_fileList->getContentWidget()); + m_mainSplitter->addWidget(editArea); + m_mainSplitter->setStretchFactor(0, 0); + m_mainSplitter->setStretchFactor(1, 0); + m_mainSplitter->setStretchFactor(2, 1); // Signals connect(directoryTree, &VDirectoryTree::currentDirectoryChanged, - fileList, &VFileList::setDirectory); + m_fileList, &VFileList::setDirectory); connect(directoryTree, &VDirectoryTree::directoryUpdated, editArea, &VEditArea::handleDirectoryUpdated); @@ -133,11 +191,11 @@ void VMainWindow::setupUI() } }); - connect(fileList, &VFileList::fileClicked, + connect(m_fileList, &VFileList::fileClicked, editArea, &VEditArea::openFile); - connect(fileList, &VFileList::fileCreated, + connect(m_fileList, &VFileList::fileCreated, editArea, &VEditArea::openFile); - connect(fileList, &VFileList::fileUpdated, + connect(m_fileList, &VFileList::fileUpdated, editArea, &VEditArea::handleFileUpdated); connect(editArea, &VEditArea::tabStatusUpdated, this, &VMainWindow::handleAreaTabStatusUpdated); @@ -148,7 +206,7 @@ void VMainWindow::setupUI() connect(m_findReplaceDialog, &VFindReplaceDialog::findTextChanged, this, &VMainWindow::handleFindDialogTextChanged); - setCentralWidget(mainSplitter); + setCentralWidget(m_mainSplitter); m_vimIndicator = new VVimIndicator(this); m_vimIndicator->hide(); @@ -165,26 +223,49 @@ void VMainWindow::setupUI() QWidget *VMainWindow::setupDirectoryPanel() { + // Notebook selector. notebookLabel = new QLabel(tr("Notebooks")); notebookLabel->setProperty("TitleLabel", true); notebookLabel->setProperty("NotebookPanel", true); - directoryLabel = new QLabel(tr("Folders")); - directoryLabel->setProperty("TitleLabel", true); - directoryLabel->setProperty("NotebookPanel", true); - notebookSelector = new VNotebookSelector(vnote); + notebookSelector = new VNotebookSelector(); notebookSelector->setObjectName("NotebookSelector"); notebookSelector->setProperty("NotebookPanel", true); notebookSelector->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); + // Navigation panel. + directoryLabel = new QLabel(tr("Folders")); + directoryLabel->setProperty("TitleLabel", true); + directoryLabel->setProperty("NotebookPanel", true); + directoryTree = new VDirectoryTree; directoryTree->setProperty("NotebookPanel", true); + QVBoxLayout *naviLayout = new QVBoxLayout; + naviLayout->addWidget(directoryLabel); + naviLayout->addWidget(directoryTree); + naviLayout->setContentsMargins(0, 0, 0, 0); + naviLayout->setSpacing(0); + QWidget *naviWidget = new QWidget(); + naviWidget->setLayout(naviLayout); + + QWidget *tmpWidget = new QWidget(); + + // Compact splitter. + m_naviSplitter = new QSplitter(); + m_naviSplitter->setOrientation(Qt::Vertical); + m_naviSplitter->setObjectName("NaviSplitter"); + m_naviSplitter->addWidget(naviWidget); + m_naviSplitter->addWidget(tmpWidget); + m_naviSplitter->setStretchFactor(0, 0); + m_naviSplitter->setStretchFactor(1, 1); + + tmpWidget->hide(); + QVBoxLayout *nbLayout = new QVBoxLayout; nbLayout->addWidget(notebookLabel); nbLayout->addWidget(notebookSelector); - nbLayout->addWidget(directoryLabel); - nbLayout->addWidget(directoryTree); + nbLayout->addWidget(m_naviSplitter); nbLayout->setContentsMargins(0, 0, 0, 0); nbLayout->setSpacing(0); QWidget *nbContainer = new QWidget(); @@ -203,6 +284,7 @@ QWidget *VMainWindow::setupDirectoryPanel() connect(directoryTree, &VDirectoryTree::currentDirectoryChanged, this, &VMainWindow::handleCurrentDirectoryChanged); + return nbContainer; } @@ -226,24 +308,60 @@ void VMainWindow::initViewToolBar(QSize p_iconSize) viewToolBar->setIconSize(p_iconSize); } + m_viewActGroup = new QActionGroup(this); QAction *onePanelViewAct = new QAction(QIcon(":/resources/icons/one_panel.svg"), - tr("&Single Panel"), this); + tr("&Single Panel"), + m_viewActGroup); onePanelViewAct->setStatusTip(tr("Display only the notes list panel")); onePanelViewAct->setToolTip(tr("Single Panel (Ctrl+E P)")); - connect(onePanelViewAct, &QAction::triggered, - this, &VMainWindow::onePanelView); + onePanelViewAct->setCheckable(true); + onePanelViewAct->setData((int)PanelViewState::SinglePanel); QAction *twoPanelViewAct = new QAction(QIcon(":/resources/icons/two_panels.svg"), - tr("&Two Panels"), this); + tr("&Two Panels"), + m_viewActGroup); twoPanelViewAct->setStatusTip(tr("Display both the folders and notes list panel")); twoPanelViewAct->setToolTip(tr("Two Panels (Ctrl+E P)")); - connect(twoPanelViewAct, &QAction::triggered, - this, &VMainWindow::twoPanelView); + twoPanelViewAct->setCheckable(true); + twoPanelViewAct->setData((int)PanelViewState::TwoPanels); + + QAction *compactViewAct = new QAction(QIcon(":/resources/icons/compact_mode.svg"), + tr("&Compact Mode"), + m_viewActGroup); + compactViewAct->setStatusTip(tr("Integrate the folders and notes list panel in one column")); + compactViewAct->setCheckable(true); + compactViewAct->setData((int)PanelViewState::CompactMode); + + connect(m_viewActGroup, &QActionGroup::triggered, + this, [this](QAction *p_action) { + if (!p_action) { + return; + } + + int act = p_action->data().toInt(); + switch (act) { + case (int)PanelViewState::SinglePanel: + onePanelView(); + break; + + case (int)PanelViewState::TwoPanels: + twoPanelView(); + break; + + case (int)PanelViewState::CompactMode: + compactModeView(); + break; + + default: + break; + } + }); QMenu *panelMenu = new QMenu(this); panelMenu->setToolTipsVisible(true); panelMenu->addAction(onePanelViewAct); panelMenu->addAction(twoPanelViewAct); + panelMenu->addAction(compactViewAct); expandViewAct = new QAction(QIcon(":/resources/icons/expand.svg"), tr("Expand (Ctrl+E E)"), this); @@ -251,11 +369,16 @@ void VMainWindow::initViewToolBar(QSize p_iconSize) expandViewAct->setCheckable(true); expandViewAct->setMenu(panelMenu); connect(expandViewAct, &QAction::triggered, - this, &VMainWindow::expandPanelView); + this, [this](bool p_checked) { + // Recover m_panelViewState or change to expand mode. + changePanelView(p_checked ? PanelViewState::ExpandMode + : m_panelViewState); + }); viewToolBar->addAction(expandViewAct); } +// Enable/disable all actions of @p_widget. static void setActionsEnabled(QWidget *p_widget, bool p_enabled) { Q_ASSERT(p_widget); @@ -276,6 +399,22 @@ void VMainWindow::initEditToolBar(QSize p_iconSize) m_editToolBar->addSeparator(); + m_headingSequenceAct = new QAction(QIcon(":/resources/icons/heading_sequence.svg"), + tr("Heading Sequence"), + this); + m_headingSequenceAct->setStatusTip(tr("Enable heading sequence in current note in edit mode")); + m_headingSequenceAct->setCheckable(true); + connect(m_headingSequenceAct, &QAction::triggered, + this, [this](bool p_checked){ + if (isHeadingSequenceApplicable()) { + VMdTab *tab = dynamic_cast(m_curTab.data()); + Q_ASSERT(tab); + tab->enableHeadingSequence(p_checked); + } + }); + + m_editToolBar->addAction(m_headingSequenceAct); + QAction *boldAct = new QAction(QIcon(":/resources/icons/bold.svg"), tr("Bold (Ctrl+B)"), this); boldAct->setStatusTip(tr("Insert bold text or change selected text to bold")); @@ -324,6 +463,48 @@ void VMainWindow::initEditToolBar(QSize p_iconSize) m_editToolBar->addAction(inlineCodeAct); + QAction *codeBlockAct = new QAction(QIcon(":/resources/icons/code_block.svg"), + tr("Code Block (Ctrl+M)"), + this); + codeBlockAct->setStatusTip(tr("Insert fenced code block text or wrap selected text into a fenced code block")); + connect(codeBlockAct, &QAction::triggered, + this, [this](){ + if (m_curTab) { + m_curTab->decorateText(TextDecoration::CodeBlock); + } + }); + + m_editToolBar->addAction(codeBlockAct); + + m_editToolBar->addSeparator(); + + // Insert link. + QAction *insetLinkAct = new QAction(QIcon(":/resources/icons/link.svg"), + tr("Insert Link (Ctrl+L)"), this); + insetLinkAct->setStatusTip(tr("Insert a link")); + connect(insetLinkAct, &QAction::triggered, + this, [this]() { + if (m_curTab) { + m_curTab->insertLink(); + } + }); + + m_editToolBar->addAction(insetLinkAct); + + // Insert image. + QAction *insertImageAct = new QAction(QIcon(":/resources/icons/insert_image.svg"), + tr("Insert Image"), + this); + insertImageAct->setStatusTip(tr("Insert an image from file or URL")); + connect(insertImageAct, &QAction::triggered, + this, [this]() { + if (m_curTab) { + m_curTab->insertImage(); + } + }); + + m_editToolBar->addAction(insertImageAct); + QAction *toggleAct = m_editToolBar->toggleViewAction(); toggleAct->setToolTip(tr("Toggle the edit toolbar")); viewMenu->addAction(toggleAct); @@ -378,7 +559,7 @@ void VMainWindow::initFileToolBar(QSize p_iconSize) qDebug() << "set NewNote shortcut to" << keySeq; newNoteAct->setShortcut(QKeySequence(keySeq)); connect(newNoteAct, &QAction::triggered, - fileList, &VFileList::newFile); + m_fileList, &VFileList::newFile); noteInfoAct = new QAction(QIcon(":/resources/icons/note_info_tb.svg"), tr("Note &Info"), this); @@ -397,11 +578,7 @@ void VMainWindow::initFileToolBar(QSize p_iconSize) m_closeNoteShortcut = new QShortcut(QKeySequence(keySeq), this); m_closeNoteShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(m_closeNoteShortcut, &QShortcut::activated, - this, [this](){ - if (m_curFile) { - editArea->closeFile(m_curFile, false); - } - }); + this, &VMainWindow::closeCurrentFile); editNoteAct = new QAction(QIcon(":/resources/icons/edit_note.svg"), tr("&Edit"), this); @@ -489,7 +666,7 @@ void VMainWindow::initHelpMenu() QAction *shortcutAct = new QAction(tr("&Shortcuts Help"), this); shortcutAct->setToolTip(tr("View information about shortcut keys")); connect(shortcutAct, &QAction::triggered, - this, &VMainWindow::shortcutHelp); + this, &VMainWindow::shortcutsHelp); QAction *mdGuideAct = new QAction(tr("&Markdown Guide"), this); mdGuideAct->setToolTip(tr("A quick guide of Markdown syntax")); @@ -692,7 +869,7 @@ void VMainWindow::initFileMenu() // Update lastPath lastPath = QFileInfo(files[0]).path(); - openExternalFiles(files); + openFiles(VUtils::filterFilePathsToOpen(files)); }); fileMenu->addAction(openAct); @@ -738,20 +915,15 @@ void VMainWindow::initFileMenu() fileMenu->addAction(settingsAct); - QAction *editConfigAct = new QAction(tr("Edit Configuration File"), this); - editConfigAct->setToolTip(tr("View and edit configuration file of VNote (vnote.ini)")); - connect(editConfigAct, &QAction::triggered, + QAction *openConfigAct = new QAction(tr("Open Configuration Folder"), this); + openConfigAct->setToolTip(tr("Open configuration folder of VNote")); + connect(openConfigAct, &QAction::triggered, this, [this](){ -#if defined(Q_OS_MACOS) || defined(Q_OS_MAC) - // On macOS, it seems that we could not open that ini file directly. QUrl url = QUrl::fromLocalFile(g_config->getConfigFolder()); -#else - QUrl url = QUrl::fromLocalFile(g_config->getConfigFilePath()); -#endif QDesktopServices::openUrl(url); }); - fileMenu->addAction(editConfigAct); + fileMenu->addAction(openConfigAct); QAction *customShortcutAct = new QAction(tr("Custom Shortcuts"), this); customShortcutAct->setToolTip(tr("Custom some standard shortcuts")); @@ -805,13 +977,6 @@ void VMainWindow::initEditMenu() QMenu *editMenu = menuBar()->addMenu(tr("&Edit")); editMenu->setToolTipsVisible(true); - // Insert image. - m_insertImageAct = newAction(QIcon(":/resources/icons/insert_image.svg"), - tr("Insert &Image"), this); - m_insertImageAct->setToolTip(tr("Insert an image from file into current note")); - connect(m_insertImageAct, &QAction::triggered, - this, &VMainWindow::insertImage); - // Find/Replace. m_findReplaceAct = newAction(QIcon(":/resources/icons/find_replace.svg"), tr("Find/Replace"), this); @@ -934,10 +1099,6 @@ void VMainWindow::initEditMenu() connect(trailingSapceAct, &QAction::triggered, this, &VMainWindow::changeHighlightTrailingSapce); - editMenu->addAction(m_insertImageAct); - editMenu->addSeparator(); - m_insertImageAct->setEnabled(false); - QMenu *findReplaceMenu = editMenu->addMenu(tr("Find/Replace")); findReplaceMenu->setToolTipsVisible(true); findReplaceMenu->addAction(m_findReplaceAct); @@ -1022,16 +1183,19 @@ void VMainWindow::initEditMenu() void VMainWindow::initDockWindows() { toolDock = new QDockWidget(tr("Tools"), this); - toolDock->setObjectName("tools_dock"); + toolDock->setObjectName("ToolsDock"); toolDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); toolBox = new QToolBox(this); + + // Outline tree. outline = new VOutline(this); connect(editArea, &VEditArea::outlineChanged, outline, &VOutline::updateOutline); + connect(editArea, &VEditArea::currentHeaderChanged, + outline, &VOutline::updateCurrentHeader); connect(outline, &VOutline::outlineItemActivated, - editArea, &VEditArea::handleOutlineItemActivated); - connect(editArea, &VEditArea::curHeaderChanged, - outline, &VOutline::updateCurHeader); + editArea, &VEditArea::scrollToHeader); + toolBox->addItem(outline, QIcon(":/resources/icons/outline.svg"), tr("Outline")); toolDock->setWidget(toolBox); addDockWidget(Qt::RightDockWidgetArea, toolDock); @@ -1064,7 +1228,7 @@ void VMainWindow::importNoteFromFile() lastPath = QFileInfo(files[0]).path(); QString msg; - if (!fileList->importFiles(files, &msg)) { + if (!m_fileList->importFiles(files, &msg)) { VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to create notes for all the files."), @@ -1616,61 +1780,64 @@ void VMainWindow::setCodeBlockStyle(QAction *p_action) } } -void VMainWindow::updateActionStateFromTabStatusChange(const VFile *p_file, - bool p_editMode) +void VMainWindow::updateActionsStateFromTab(const VEditTab *p_tab) { - bool systemFile = p_file - && p_file->getType() == FileType::Orphan - && dynamic_cast(p_file)->isSystemFile(); + const VFile *file = p_tab ? p_tab->getFile() : NULL; + bool editMode = p_tab ? p_tab->isEditMode() : false; + bool systemFile = file + && file->getType() == FileType::Orphan + && dynamic_cast(file)->isSystemFile(); - m_printAct->setEnabled(p_file && p_file->getDocType() == DocType::Markdown); - m_exportAsPDFAct->setEnabled(p_file && p_file->getDocType() == DocType::Markdown); + m_printAct->setEnabled(file && file->getDocType() == DocType::Markdown); + m_exportAsPDFAct->setEnabled(file && file->getDocType() == DocType::Markdown); - discardExitAct->setVisible(p_file && p_editMode); - saveExitAct->setVisible(p_file && p_editMode); - editNoteAct->setEnabled(p_file && p_file->isModifiable() && !p_editMode); + discardExitAct->setVisible(file && editMode); + saveExitAct->setVisible(file && editMode); + editNoteAct->setEnabled(file && file->isModifiable() && !editMode); editNoteAct->setVisible(!saveExitAct->isVisible()); - saveNoteAct->setEnabled(p_file && p_editMode); - deleteNoteAct->setEnabled(p_file && p_file->getType() == FileType::Note); - noteInfoAct->setEnabled(p_file && !systemFile); + saveNoteAct->setEnabled(file && editMode); + deleteNoteAct->setEnabled(file && file->getType() == FileType::Note); + noteInfoAct->setEnabled(file && !systemFile); - m_attachmentBtn->setEnabled(p_file && p_file->getType() == FileType::Note); + m_attachmentBtn->setEnabled(file && file->getType() == FileType::Note); - m_insertImageAct->setEnabled(p_file && p_editMode); + setActionsEnabled(m_editToolBar, file && editMode); - setActionsEnabled(m_editToolBar, p_file && p_editMode); + // Handle heading sequence act independently. + m_headingSequenceAct->setEnabled(isHeadingSequenceApplicable()); + const VMdTab *mdTab = dynamic_cast(p_tab); + m_headingSequenceAct->setChecked(mdTab && mdTab->isHeadingSequenceEnabled()); // Find/Replace - m_findReplaceAct->setEnabled(p_file); - m_findNextAct->setEnabled(p_file); - m_findPreviousAct->setEnabled(p_file); - m_replaceAct->setEnabled(p_file && p_editMode); - m_replaceFindAct->setEnabled(p_file && p_editMode); - m_replaceAllAct->setEnabled(p_file && p_editMode); + m_findReplaceAct->setEnabled(file); + m_findNextAct->setEnabled(file); + m_findPreviousAct->setEnabled(file); + m_replaceAct->setEnabled(file && editMode); + m_replaceFindAct->setEnabled(file && editMode); + m_replaceAllAct->setEnabled(file && editMode); - if (!p_file) { + if (!file) { m_findReplaceDialog->closeDialog(); } } void VMainWindow::handleAreaTabStatusUpdated(const VEditTabInfo &p_info) { - bool editMode = false; m_curTab = p_info.m_editTab; if (m_curTab) { m_curFile = m_curTab->getFile(); - editMode = m_curTab->isEditMode(); } else { m_curFile = NULL; } - updateActionStateFromTabStatusChange(m_curFile, editMode); + updateActionsStateFromTab(m_curTab); m_attachmentList->setFile(dynamic_cast(m_curFile.data())); QString title; if (m_curFile) { - m_findReplaceDialog->updateState(m_curFile->getDocType(), editMode); + m_findReplaceDialog->updateState(m_curFile->getDocType(), + m_curTab->isEditMode()); if (m_curFile->getType() == FileType::Note) { const VNoteFile *tmpFile = dynamic_cast((VFile *)m_curFile); @@ -1694,65 +1861,120 @@ void VMainWindow::handleAreaTabStatusUpdated(const VEditTabInfo &p_info) void VMainWindow::onePanelView() { - changeSplitterView(1); - expandViewAct->setChecked(false); - m_onePanel = true; + m_panelViewState = PanelViewState::SinglePanel; + g_config->setEnableCompactMode(false); + changePanelView(m_panelViewState); } void VMainWindow::twoPanelView() { - changeSplitterView(2); - expandViewAct->setChecked(false); - m_onePanel = false; + m_panelViewState = PanelViewState::TwoPanels; + g_config->setEnableCompactMode(false); + changePanelView(m_panelViewState); } -void VMainWindow::toggleOnePanelView() +void VMainWindow::compactModeView() { - if (m_onePanel) { - twoPanelView(); + m_panelViewState = PanelViewState::CompactMode; + g_config->setEnableCompactMode(true); + changePanelView(m_panelViewState); +} + +void VMainWindow::enableCompactMode(bool p_enabled) +{ + const int fileListIdx = 1; + bool isCompactMode = m_naviSplitter->indexOf(m_fileList) != -1; + if (p_enabled) { + // Change to compact mode. + if (isCompactMode) { + return; + } + + // Take m_fileList out of m_mainSplitter. + QWidget *tmpWidget = new QWidget(this); + Q_ASSERT(fileListIdx == m_mainSplitter->indexOf(m_fileList)); + m_fileList->hide(); + m_mainSplitter->replaceWidget(fileListIdx, tmpWidget); + tmpWidget->hide(); + + // Insert m_fileList into m_naviSplitter. + QWidget *wid = m_naviSplitter->replaceWidget(fileListIdx, m_fileList); + delete wid; + + m_fileList->show(); } else { - onePanelView(); + // Disable compact mode and go back to two panels view. + if (!isCompactMode) { + return; + } + + // Take m_fileList out of m_naviSplitter. + Q_ASSERT(fileListIdx == m_naviSplitter->indexOf(m_fileList)); + QWidget *tmpWidget = new QWidget(this); + m_fileList->hide(); + m_naviSplitter->replaceWidget(fileListIdx, tmpWidget); + tmpWidget->hide(); + + // Insert m_fileList into m_mainSplitter. + QWidget *wid = m_mainSplitter->replaceWidget(fileListIdx, m_fileList); + delete wid; + + m_fileList->show(); } + + // Set Tab order. + setTabOrder(directoryTree, m_fileList->getContentWidget()); } -void VMainWindow::expandPanelView(bool p_checked) +void VMainWindow::changePanelView(PanelViewState p_state) { - int nrSplits = 0; - if (p_checked) { - nrSplits = 0; - } else { - if (m_onePanel) { - nrSplits = 1; + switch (p_state) { + case PanelViewState::ExpandMode: + m_mainSplitter->widget(0)->hide(); + m_mainSplitter->widget(1)->hide(); + m_mainSplitter->widget(2)->show(); + break; + + case PanelViewState::SinglePanel: + enableCompactMode(false); + + m_mainSplitter->widget(0)->hide(); + m_mainSplitter->widget(1)->show(); + m_mainSplitter->widget(2)->show(); + break; + + case PanelViewState::TwoPanels: + enableCompactMode(false); + + m_mainSplitter->widget(0)->show(); + m_mainSplitter->widget(1)->show(); + m_mainSplitter->widget(2)->show(); + break; + + case PanelViewState::CompactMode: + m_mainSplitter->widget(0)->show(); + m_mainSplitter->widget(1)->hide(); + m_mainSplitter->widget(2)->show(); + + enableCompactMode(true); + break; + + default: + break; + } + + // Change the action state. + QList acts = m_viewActGroup->actions(); + for (auto & act : acts) { + if (act->data().toInt() == (int)p_state) { + act->setChecked(true); } else { - nrSplits = 2; + act->setChecked(false); } } - changeSplitterView(nrSplits); -} -void VMainWindow::changeSplitterView(int nrPanel) -{ - switch (nrPanel) { - case 0: - // Expand - mainSplitter->widget(0)->hide(); - mainSplitter->widget(1)->hide(); - mainSplitter->widget(2)->show(); - break; - case 1: - // Single panel - mainSplitter->widget(0)->hide(); - mainSplitter->widget(1)->show(); - mainSplitter->widget(2)->show(); - break; - case 2: - // Two panels - mainSplitter->widget(0)->show(); - mainSplitter->widget(1)->show(); - mainSplitter->widget(2)->show(); - break; - default: - qWarning() << "invalid panel number" << nrPanel; + if (p_state != PanelViewState::ExpandMode) { + expandViewAct->setChecked(false); } } @@ -1772,7 +1994,7 @@ void VMainWindow::curEditFileInfo() if (m_curFile->getType() == FileType::Note) { VNoteFile *file = dynamic_cast((VFile *)m_curFile); Q_ASSERT(file); - fileList->fileInfo(file); + m_fileList->fileInfo(file); } else if (m_curFile->getType() == FileType::Orphan) { VOrphanFile *file = dynamic_cast((VFile *)m_curFile); Q_ASSERT(file); @@ -1789,7 +2011,7 @@ void VMainWindow::deleteCurNote() } VNoteFile *file = dynamic_cast((VFile *)m_curFile); - fileList->deleteFile(file); + m_fileList->deleteFile(file); } void VMainWindow::closeEvent(QCloseEvent *event) @@ -1828,12 +2050,40 @@ void VMainWindow::closeEvent(QCloseEvent *event) } if (isExit || !m_trayIcon->isVisible()) { + // Get all the opened tabs. + bool saveOpenedNotes = g_config->getStartupPageType() == StartupPageType::ContinueLeftOff; + QVector fileInfos; + QVector tabs; + if (saveOpenedNotes) { + tabs = editArea->getAllTabsInfo(); + + fileInfos.reserve(tabs.size()); + + for (auto const & tab : tabs) { + // Skip system file. + VFile *file = tab.m_editTab->getFile(); + if (file->getType() == FileType::Orphan + && dynamic_cast(file)->isSystemFile()) { + continue; + } + + VFileSessionInfo info = VFileSessionInfo::fromEditTabInfo(&tab); + fileInfos.push_back(info); + + qDebug() << "file session:" << info.m_file << (info.m_mode == OpenFileMode::Edit); + } + } + if (!editArea->closeAllFiles(false)) { // Fail to close all the opened files, cancel closing app. event->ignore(); return; } + if (saveOpenedNotes) { + g_config->setLastOpenedFiles(fileInfos); + } + QMainWindow::closeEvent(event); } else { hide(); @@ -1843,14 +2093,17 @@ void VMainWindow::closeEvent(QCloseEvent *event) void VMainWindow::saveStateAndGeometry() { - // In one panel view, it will save the wrong state that the directory tree - // panel has a width of zero. - twoPanelView(); - g_config->setMainWindowGeometry(saveGeometry()); g_config->setMainWindowState(saveState()); g_config->setToolsDockChecked(toolDock->isVisible()); - g_config->setMainSplitterState(mainSplitter->saveState()); + + // In one panel view, it will save the wrong state that the directory tree + // panel has a width of zero. + changePanelView(PanelViewState::TwoPanels); + g_config->setMainSplitterState(m_mainSplitter->saveState()); + + changePanelView(PanelViewState::CompactMode); + g_config->setNaviSplitterState(m_naviSplitter->saveState()); } void VMainWindow::restoreStateAndGeometry() @@ -1864,9 +2117,15 @@ void VMainWindow::restoreStateAndGeometry() restoreState(state); } toolDock->setVisible(g_config->getToolsDockChecked()); + const QByteArray &splitterState = g_config->getMainSplitterState(); if (!splitterState.isEmpty()) { - mainSplitter->restoreState(splitterState); + m_mainSplitter->restoreState(splitterState); + } + + const QByteArray &naviSplitterState = g_config->getNaviSplitterState(); + if (!naviSplitterState.isEmpty()) { + m_naviSplitter->restoreState(naviSplitterState); } } @@ -1908,7 +2167,7 @@ void VMainWindow::keyPressEvent(QKeyEvent *event) void VMainWindow::repositionAvatar() { - int diameter = mainSplitter->pos().y(); + int diameter = m_mainSplitter->pos().y(); int x = width() - diameter - 5; int y = 0; qDebug() << "avatar:" << diameter << x << y; @@ -1917,15 +2176,6 @@ void VMainWindow::repositionAvatar() m_avatar->show(); } -void VMainWindow::insertImage() -{ - if (!m_curTab) { - return; - } - Q_ASSERT(m_curTab == editArea->currentEditTab()); - m_curTab->insertImage(); -} - bool VMainWindow::locateFile(VFile *p_file) { bool ret = false; @@ -1942,30 +2192,25 @@ bool VMainWindow::locateFile(VFile *p_file) VDirectory *dir = file->getDirectory(); if (directoryTree->locateDirectory(dir)) { - while (fileList->currentDirectory() != dir) { + while (m_fileList->currentDirectory() != dir) { QCoreApplication::sendPostedEvents(); } - if (fileList->locateFile(file)) { + if (m_fileList->locateFile(file)) { ret = true; - fileList->setFocus(); + m_fileList->setFocus(); } } } // Open the directory and file panels after location. - twoPanelView(); - - return ret; -} - -bool VMainWindow::locateCurrentFile() -{ - if (m_curFile) { - return locateFile(m_curFile); + if (m_panelViewState == PanelViewState::CompactMode) { + compactModeView(); + } else { + twoPanelView(); } - return false; + return ret; } void VMainWindow::handleFindDialogTextChanged(const QString &p_text, uint /* p_options */) @@ -1992,18 +2237,6 @@ void VMainWindow::viewSettings() settingsDialog.exec(); } -void VMainWindow::handleCaptainModeChanged(bool p_enabled) -{ - static QString normalBaseColor = m_avatar->getBaseColor(); - static QString captainModeColor = vnote->getColorFromPalette("Purple5"); - - if (p_enabled) { - m_avatar->updateBaseColor(captainModeColor); - } else { - m_avatar->updateBaseColor(normalBaseColor); - } -} - void VMainWindow::closeCurrentFile() { if (m_curFile) { @@ -2061,7 +2294,7 @@ void VMainWindow::enableImageCaption(bool p_checked) g_config->setEnableImageCaption(p_checked); } -void VMainWindow::shortcutHelp() +void VMainWindow::shortcutsHelp() { QString locale = VUtils::getLocale(); QString docName = VNote::c_shortcutsDocFile_en; @@ -2173,9 +2406,8 @@ bool VMainWindow::tryOpenInternalFile(const QString &p_filePath) return false; } -void VMainWindow::openExternalFiles(const QStringList &p_files, bool p_forceOrphan) +void VMainWindow::openFiles(const QStringList &p_files, bool p_forceOrphan) { - qDebug() << "open external files" << p_files; for (int i = 0; i < p_files.size(); ++i) { VFile *file = NULL; if (!p_forceOrphan) { @@ -2207,7 +2439,7 @@ void VMainWindow::checkSharedMemory() QStringList files = m_guard->fetchFilesToOpen(); if (!files.isEmpty()) { qDebug() << "shared memory fetch files" << files; - openExternalFiles(files); + openFiles(files); // Eliminate the signal. m_guard->fetchAskedToShow(); @@ -2273,9 +2505,128 @@ void VMainWindow::showMainWindow() this->activateWindow(); } -void VMainWindow::showAttachmentList() +void VMainWindow::openStartupPages() { - if (m_attachmentBtn->isEnabled()) { - m_attachmentBtn->showPopupWidget(); + StartupPageType type = g_config->getStartupPageType(); + switch (type) { + case StartupPageType::ContinueLeftOff: + { + QVector files = g_config->getLastOpenedFiles(); + qDebug() << "open" << files.size() << "last opened files"; + editArea->openFiles(files); + break; + } + + case StartupPageType::SpecificPages: + { + QStringList pagesToOpen = VUtils::filterFilePathsToOpen(g_config->getStartupPages()); + qDebug() << "open startup pages" << pagesToOpen; + openFiles(pagesToOpen); + break; + } + + default: + break; + } +} + +bool VMainWindow::isHeadingSequenceApplicable() const +{ + if (!m_curTab) { + return false; + } + + Q_ASSERT(m_curFile); + + if (!m_curFile->isModifiable() + || m_curFile->getDocType() != DocType::Markdown) { + return false; + } + + return true; +} + +// Popup the attachment list if it is enabled. +void VMainWindow::showAttachmentListByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VMainWindow *obj = static_cast(p_target); + if (obj->m_attachmentBtn->isEnabled()) { + obj->m_attachmentBtn->showPopupWidget(); + } +} + +void VMainWindow::locateCurrentFileByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VMainWindow *obj = static_cast(p_target); + if (obj->m_curFile) { + obj->locateFile(obj->m_curFile); + } +} + +void VMainWindow::toggleExpandModeByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VMainWindow *obj = static_cast(p_target); + obj->expandViewAct->trigger(); +} + +void VMainWindow::toggleOnePanelViewByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VMainWindow *obj = static_cast(p_target); + if (obj->m_panelViewState == PanelViewState::TwoPanels) { + obj->onePanelView(); + } else { + obj->twoPanelView(); + } +} + +void VMainWindow::discardAndReadByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VMainWindow *obj = static_cast(p_target); + if (obj->m_curFile) { + obj->discardExitAct->trigger(); + } +} + +void VMainWindow::toggleToolsDockByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VMainWindow *obj = static_cast(p_target); + obj->toolDock->setVisible(!obj->toolDock->isVisible()); +} + +void VMainWindow::closeFileByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VMainWindow *obj = static_cast(p_target); + obj->closeCurrentFile(); +} + +void VMainWindow::shortcutsHelpByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VMainWindow *obj = static_cast(p_target); + obj->shortcutsHelp(); +} + +void VMainWindow::flushLogFileByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_target); + Q_UNUSED(p_data); + +#if defined(QT_NO_DEBUG) + // Flush g_logFile. + g_logFile.flush(); +#endif +} + +void VMainWindow::promptNewNotebookIfEmpty() +{ + if (vnote->getNotebooks().isEmpty()) { + notebookSelector->newNotebook(); } } diff --git a/src/vmainwindow.h b/src/vmainwindow.h index a0e1bc61..799eb0a6 100644 --- a/src/vmainwindow.h +++ b/src/vmainwindow.h @@ -38,6 +38,15 @@ class QShortcut; class VButtonWithWidget; class VAttachmentList; +enum class PanelViewState +{ + ExpandMode, + SinglePanel, + TwoPanels, + CompactMode, + Invalid +}; + class VMainWindow : public QMainWindow { Q_OBJECT @@ -51,18 +60,17 @@ public: // Returns true if the location succeeds. bool locateFile(VFile *p_file); - // Returns true if the location succeeds. - bool locateCurrentFile(); - VFileList *getFileList() const; + VEditArea *getEditArea() const; + // View and edit the information of @p_file, which is an orphan file. void editOrphanFileInfo(VFile *p_file); - // Open external files @p_files as orphan files. + // Open files @p_files as orphan files or internal note files. // If @p_forceOrphan is false, for each file, VNote will try to find out if // it is a note inside VNote. If yes, VNote will open it as internal file. - void openExternalFiles(const QStringList &p_files, bool p_forceOrphan = false); + void openFiles(const QStringList &p_files, bool p_forceOrphan = false); // Try to open @p_filePath as internal note. bool tryOpenInternalFile(const QString &p_filePath); @@ -70,15 +78,23 @@ public: // Show a temporary message in status bar. void showStatusMessage(const QString &p_msg); - // Popup the attachment list if it is enabled. - void showAttachmentList(); + // Open startup pages according to configuration. + void openStartupPages(); + + VCaptain *getCaptain() const; + + // Prompt user for new notebook if there is no notebook. + void promptNewNotebookIfEmpty(); private slots: void importNoteFromFile(); void viewSettings(); void changeMarkdownConverter(QAction *action); void aboutMessage(); - void shortcutHelp(); + + // Display shortcuts help. + void shortcutsHelp(); + void changeExpandTab(bool checked); void setTabStopWidth(QAction *action); void setEditorBackgroundColor(QAction *action); @@ -105,17 +121,15 @@ private slots: void changeHighlightTrailingSapce(bool p_checked); void onePanelView(); void twoPanelView(); - void expandPanelView(bool p_checked); + void compactModeView(); void curEditFileInfo(); void deleteCurNote(); void handleCurrentDirectoryChanged(const VDirectory *p_dir); void handleCurrentNotebookChanged(const VNotebook *p_notebook); - void insertImage(); void handleFindDialogTextChanged(const QString &p_text, uint p_options); void openFindDialog(); void enableMermaid(bool p_checked); void enableMathjax(bool p_checked); - void handleCaptainModeChanged(bool p_enabled); void changeAutoIndent(bool p_checked); void changeAutoList(bool p_checked); void changeVimMode(bool p_checked); @@ -127,10 +141,14 @@ private slots: void printNote(); void exportAsPDF(); + // Set the panel view properly. + void enableCompactMode(bool p_enabled); + // Handle Vim status updated. void handleVimStatusUpdated(const VVim *p_vim); // Handle the status update of the current tab of VEditArea. + // Will be called frequently. void handleAreaTabStatusUpdated(const VEditTabInfo &p_info); // Check the shared memory between different instances to see if we have @@ -142,6 +160,9 @@ private slots: // Restore main window. void showMainWindow(); + // Close current note. + void closeCurrentFile(); + protected: void closeEvent(QCloseEvent *event) Q_DECL_OVERRIDE; void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE; @@ -186,17 +207,19 @@ private: void initEditorLineNumberMenu(QMenu *p_menu); void initEditorStyleMenu(QMenu *p_emnu); - void changeSplitterView(int nrPanel); void updateWindowTitle(const QString &str); - void updateActionStateFromTabStatusChange(const VFile *p_file, - bool p_editMode); + + // Update state of actions according to @p_tab. + void updateActionsStateFromTab(const VEditTab *p_tab); + void saveStateAndGeometry(); void restoreStateAndGeometry(); void repositionAvatar(); + // Should init VCaptain before setupUI(). void initCaptain(); - void toggleOnePanelView(); - void closeCurrentFile(); + + void registerCaptainAndNavigationTargets(); // Update status bar information. void updateStatusInfo(const VEditTabInfo &p_info); @@ -213,6 +236,37 @@ private: // Init system tray icon and correspondign context menu. void initTrayIcon(); + // Change the panel view according to @p_state. + // Will not change m_panelViewState. + void changePanelView(PanelViewState p_state); + + // Whether heading sequence is applicable to current tab. + // Only available for writable Markdown file. + bool isHeadingSequenceApplicable() const; + + // Captain mode functions. + + // Popup the attachment list if it is enabled. + static void showAttachmentListByCaptain(void *p_target, void *p_data); + + static void locateCurrentFileByCaptain(void *p_target, void *p_data); + + static void toggleExpandModeByCaptain(void *p_target, void *p_data); + + static void toggleOnePanelViewByCaptain(void *p_target, void *p_data); + + static void discardAndReadByCaptain(void *p_target, void *p_data); + + static void toggleToolsDockByCaptain(void *p_target, void *p_data); + + static void closeFileByCaptain(void *p_target, void *p_data); + + static void shortcutsHelpByCaptain(void *p_target, void *p_data); + + static void flushLogFileByCaptain(void *p_target, void *p_data); + + // End Captain mode functions. + VNote *vnote; QPointer m_curFile; QPointer m_curTab; @@ -222,10 +276,18 @@ private: QLabel *notebookLabel; QLabel *directoryLabel; VNotebookSelector *notebookSelector; - VFileList *fileList; + VFileList *m_fileList; VDirectoryTree *directoryTree; - QSplitter *mainSplitter; + + // Splitter for directory | files | edit. + QSplitter *m_mainSplitter; + + // Splitter for directory | files. + // Move directory and file panel in one compact vertical split. + QSplitter *m_naviSplitter; + VEditArea *editArea; + QDockWidget *toolDock; QToolBox *toolBox; VOutline *outline; @@ -234,8 +296,8 @@ private: VVimIndicator *m_vimIndicator; VTabIndicator *m_tabIndicator; - // Whether it is one panel or two panles. - bool m_onePanel; + // SinglePanel, TwoPanels, CompactMode. + PanelViewState m_panelViewState; // Actions QAction *newRootDirAct; @@ -251,7 +313,6 @@ private: QAction *m_printAct; QAction *m_exportAsPDFAct; - QAction *m_insertImageAct; QAction *m_findReplaceAct; QAction *m_findNextAct; QAction *m_findPreviousAct; @@ -261,6 +322,9 @@ private: QAction *m_autoIndentAct; + // Enable heading sequence for current note. + QAction *m_headingSequenceAct; + // Act group for render styles. QActionGroup *m_renderStyleActs; @@ -269,6 +333,9 @@ private: // Act group for code block render styles. QActionGroup *m_codeBlockStyleActs; + // Act group for panel view actions. + QActionGroup *m_viewActGroup; + QShortcut *m_closeNoteShortcut; // Menus @@ -306,7 +373,17 @@ private: inline VFileList *VMainWindow::getFileList() const { - return fileList; + return m_fileList; +} + +inline VEditArea *VMainWindow::getEditArea() const +{ + return editArea; +} + +inline VCaptain *VMainWindow::getCaptain() const +{ + return m_captain; } #endif // VMAINWINDOW_H diff --git a/src/vmdedit.cpp b/src/vmdedit.cpp index 0bb54218..2ca757a0 100644 --- a/src/vmdedit.cpp +++ b/src/vmdedit.cpp @@ -5,7 +5,7 @@ #include "vmdeditoperations.h" #include "vnote.h" #include "vconfigmanager.h" -#include "vtoc.h" +#include "vtableofcontent.h" #include "utils/vutils.h" #include "utils/veditutils.h" #include "utils/vpreviewutils.h" @@ -35,7 +35,7 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type, document()); connect(m_mdHighlighter, &HGMarkdownHighlighter::headersUpdated, - this, &VMdEdit::updateOutline); + this, &VMdEdit::updateHeaders); // After highlight, the cursor may trun into non-visible. We should make it visible // in this case. @@ -74,7 +74,7 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type, this, &VEdit::vimStatusUpdated); connect(this, &VMdEdit::cursorPositionChanged, - this, &VMdEdit::updateCurHeader); + this, &VMdEdit::updateCurrentHeader); connect(QApplication::clipboard(), &QClipboard::changed, this, &VMdEdit::handleClipboardChanged); @@ -111,7 +111,7 @@ void VMdEdit::beginEdit() setReadOnly(false); } - updateOutline(m_mdHighlighter->getHeaderRegions()); + updateHeaders(m_mdHighlighter->getHeaderRegions()); } void VMdEdit::endEdit() @@ -345,43 +345,9 @@ void VMdEdit::clearUnusedImages() m_initImages.clear(); } -int VMdEdit::currentCursorHeader() const +void VMdEdit::updateCurrentHeader() { - if (m_headers.isEmpty()) { - return -1; - } - - int curLine = textCursor().block().firstLineNumber(); - int i = 0; - for (i = m_headers.size() - 1; i >= 0; --i) { - if (!m_headers[i].isEmpty()) { - if (m_headers[i].lineNumber <= curLine) { - break; - } - } - } - - if (i == -1) { - return -1; - } else { - Q_ASSERT(m_headers[i].index == i); - return i; - } -} - -void VMdEdit::updateCurHeader() -{ - if (m_headers.isEmpty()) { - return; - } - - int idx = currentCursorHeader(); - if (idx == -1) { - emit curHeaderChanged(VAnchor(m_file, "", -1, -1)); - return; - } - - emit curHeaderChanged(VAnchor(m_file, "", m_headers[idx].lineNumber, m_headers[idx].index)); + emit currentHeaderChanged(textCursor().block().blockNumber()); } static void addHeaderSequence(QVector &p_sequence, int p_level, int p_baseLevel) @@ -448,11 +414,11 @@ static void insertSequenceToHeader(QTextBlock p_block, } } -void VMdEdit::updateOutline(const QVector &p_headerRegions) +void VMdEdit::updateHeaders(const QVector &p_headerRegions) { QTextDocument *doc = document(); - QVector headers; + QVector headers; QVector headerBlockNumbers; QVector headerSequences; if (!p_headerRegions.isEmpty()) { @@ -480,8 +446,10 @@ void VMdEdit::updateOutline(const QVector &p_headerRegions) if ((block.userState() == HighlightBlockState::Normal) && headerReg.exactMatch(block.text())) { int level = headerReg.cap(1).length(); - VHeader header(level, headerReg.cap(2).trimmed(), - "", block.firstLineNumber(), headers.size()); + VTableOfContentItem header(headerReg.cap(2).trimmed(), + level, + block.blockNumber(), + headers.size()); headers.append(header); headerBlockNumbers.append(block.blockNumber()); headerSequences.append(headerReg.cap(3)); @@ -496,7 +464,7 @@ void VMdEdit::updateOutline(const QVector &p_headerRegions) m_headers.clear(); - bool autoSequence = g_config->getEnableHeadingSequence() && !isReadOnly(); + bool autoSequence = m_config.m_enableHeadingSequence && !isReadOnly(); int headingSequenceBaseLevel = g_config->getHeadingSequenceBaseLevel(); if (headingSequenceBaseLevel < 1 || headingSequenceBaseLevel > 6) { headingSequenceBaseLevel = 1; @@ -506,22 +474,25 @@ void VMdEdit::updateOutline(const QVector &p_headerRegions) QRegExp preReg(VUtils::c_headerPrefixRegExp); int curLevel = baseLevel - 1; for (int i = 0; i < headers.size(); ++i) { - VHeader &item = headers[i]; - while (item.level > curLevel + 1) { + VTableOfContentItem &item = headers[i]; + while (item.m_level > curLevel + 1) { curLevel += 1; // Insert empty level which is an invalid header. - m_headers.append(VHeader(curLevel, c_emptyHeaderName, "", -1, m_headers.size())); + m_headers.append(VTableOfContentItem(c_emptyHeaderName, + curLevel, + -1, + m_headers.size())); if (autoSequence) { addHeaderSequence(seqs, curLevel, headingSequenceBaseLevel); } } - item.index = m_headers.size(); + item.m_index = m_headers.size(); m_headers.append(item); - curLevel = item.level; + curLevel = item.m_level; if (autoSequence) { - addHeaderSequence(seqs, item.level, headingSequenceBaseLevel); + addHeaderSequence(seqs, item.m_level, headingSequenceBaseLevel); QString seqStr = headerSequenceStr(seqs); if (headerSequences[i] != seqStr) { @@ -536,25 +507,16 @@ void VMdEdit::updateOutline(const QVector &p_headerRegions) emit headersChanged(m_headers); - updateCurHeader(); + updateCurrentHeader(); } -void VMdEdit::scrollToHeader(const VAnchor &p_anchor) +bool VMdEdit::scrollToHeader(int p_blockNumber) { - if (p_anchor.lineNumber == -1 - || p_anchor.m_outlineIndex < 0) { - // Move to the start of document if m_headers is not empty. - // Otherwise, there is no outline, so just let it be. - if (!m_headers.isEmpty()) { - moveCursor(QTextCursor::Start); - } - - return; - } else if (p_anchor.m_outlineIndex >= m_headers.size()) { - return; + if (p_blockNumber < 0) { + return false; } - scrollToLine(p_anchor.lineNumber); + return scrollToBlock(p_blockNumber); } QString VMdEdit::toPlainTextWithoutImg() @@ -728,9 +690,21 @@ void VMdEdit::resizeEvent(QResizeEvent *p_event) VEdit::resizeEvent(p_event); } -const QVector &VMdEdit::getHeaders() const +int VMdEdit::indexOfCurrentHeader() const { - return m_headers; + if (m_headers.isEmpty()) { + return -1; + } + + int blockNumber = textCursor().block().blockNumber(); + for (int i = m_headers.size() - 1; i >= 0; --i) { + if (!m_headers[i].isEmpty() + && m_headers[i].m_blockNumber <= blockNumber) { + return i; + } + } + + return -1; } bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) @@ -740,11 +714,11 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) } QTextCursor cursor = textCursor(); - int cursorLine = cursor.block().firstLineNumber(); + int cursorLine = cursor.block().blockNumber(); int targetIdx = -1; // -1: skip level check. int targetLevel = 0; - int idx = currentCursorHeader(); + int idx = indexOfCurrentHeader(); if (idx == -1) { // Cursor locates at the beginning, before any headers. if (p_relativeLevel < 0 || !p_forward) { @@ -761,7 +735,7 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) for (targetIdx = idx == -1 ? 0 : idx; targetIdx >= 0 && targetIdx < m_headers.size(); targetIdx += delta) { - const VHeader &header = m_headers[targetIdx]; + const VTableOfContentItem &header = m_headers[targetIdx]; if (header.isEmpty()) { continue; } @@ -769,7 +743,7 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) if (targetLevel == 0) { // The target level has not been init yet. Q_ASSERT(firstHeader); - targetLevel = header.level; + targetLevel = header.m_level; if (p_relativeLevel < 0) { targetLevel += p_relativeLevel; if (targetLevel < 1) { @@ -781,9 +755,9 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) } } - if (targetLevel == -1 || header.level == targetLevel) { + if (targetLevel == -1 || header.m_level == targetLevel) { if (firstHeader - && (cursorLine == header.lineNumber + && (cursorLine == header.m_blockNumber || p_forward) && idx != -1) { // This header is not counted for the repeat. @@ -795,7 +769,7 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) // Found. break; } - } else if (header.level < targetLevel) { + } else if (header.m_level < targetLevel) { // Stop by higher level. return false; } @@ -808,9 +782,9 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) } // Jump to target header. - int line = m_headers[targetIdx].lineNumber; + int line = m_headers[targetIdx].m_blockNumber; if (line > -1) { - QTextBlock block = document()->findBlockByLineNumber(line); + QTextBlock block = document()->findBlockByNumber(line); if (block.isValid()) { cursor.setPosition(block.position()); setTextCursor(cursor); @@ -837,6 +811,8 @@ void VMdEdit::finishOneAsyncJob(int p_idx) m_freshEdit = false; emit statusChanged(); - updateOutline(m_mdHighlighter->getHeaderRegions()); + updateHeaders(m_mdHighlighter->getHeaderRegions()); + + emit ready(); } } diff --git a/src/vmdedit.h b/src/vmdedit.h index 026bd449..231d58c0 100644 --- a/src/vmdedit.h +++ b/src/vmdedit.h @@ -7,7 +7,7 @@ #include #include #include -#include "vtoc.h" +#include "vtableofcontent.h" #include "veditoperations.h" #include "vconfigmanager.h" #include "utils/vutils.h" @@ -32,31 +32,34 @@ public: // @p_path is the absolute path of the inserted image. void imageInserted(const QString &p_path); - void scrollToHeader(const VAnchor &p_anchor); + // Scroll to header @p_blockNumber. + // Return true if @p_blockNumber is valid to scroll to. + bool scrollToHeader(int p_blockNumber); // Like toPlainText(), but remove image preview characters. QString toPlainTextWithoutImg(); - const QVector &getHeaders() const; - public slots: bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE; signals: - void headersChanged(const QVector &headers); + // Signal when headers change. + void headersChanged(const QVector &p_headers); // Signal when current header change. - void curHeaderChanged(VAnchor p_anchor); + void currentHeaderChanged(int p_blockNumber); // Signal when the status of VMdEdit changed. // Will be emitted by VImagePreviewer for now. void statusChanged(); private slots: - void updateOutline(const QVector &p_headerRegions); + // Update m_headers according to elements. + void updateHeaders(const QVector &p_headerRegions); + // Update current header according to cursor position. // When there is no header in current cursor, will signal an invalid header. - void updateCurHeader(); + void updateCurrentHeader(); void handleClipboardChanged(QClipboard::Mode p_mode); @@ -95,9 +98,6 @@ private: // in the selection. Get the QImage. QImage tryGetSelectedImage(); - // Return the header index in m_headers where current cursor locates. - int currentCursorHeader() const; - QString getPlainTextWithoutPreviewImage() const; // Try to get all the regions of preview image within @p_block. @@ -107,6 +107,9 @@ private: void finishOneAsyncJob(int p_idx); + // Index in m_headers of current header which contains the cursor. + int indexOfCurrentHeader() const; + HGMarkdownHighlighter *m_mdHighlighter; VCodeBlockHighlightHelper *m_cbHighlighter; VImagePreviewer *m_imagePreviewer; @@ -117,7 +120,8 @@ private: // Image links right at the beginning of the edit. QVector m_initImages; - QVector m_headers; + // Mainly used for title jump. + QVector m_headers; bool m_freshEdit; diff --git a/src/vmdeditoperations.cpp b/src/vmdeditoperations.cpp index bb80e883..0f73de01 100644 --- a/src/vmdeditoperations.cpp +++ b/src/vmdeditoperations.cpp @@ -26,7 +26,7 @@ extern VConfigManager *g_config; -const QString VMdEditOperations::c_defaultImageTitle = "image"; +const QString VMdEditOperations::c_defaultImageTitle = ""; VMdEditOperations::VMdEditOperations(VEdit *p_editor, VFile *p_file) : VEditOperations(p_editor, p_file), m_autoIndentPos(-1) @@ -278,6 +278,28 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event) break; } + case Qt::Key_L: + { + if (modifiers == Qt::ControlModifier) { + m_editor->insertLink(); + p_event->accept(); + ret = true; + } + + break; + } + + case Qt::Key_M: + { + if (modifiers == Qt::ControlModifier) { + decorateCodeBlock(); + p_event->accept(); + ret = true; + } + + break; + } + case Qt::Key_O: { if (modifiers == Qt::ControlModifier) { @@ -626,33 +648,25 @@ void VMdEditOperations::changeListBlockSeqNumber(QTextBlock &p_block, int p_seq) bool VMdEditOperations::insertTitle(int p_level) { - Q_ASSERT(p_level > 0 && p_level < 7); QTextDocument *doc = m_editor->document(); - QString titleMark(p_level, '#'); QTextCursor cursor = m_editor->textCursor(); + int firstBlock = cursor.block().blockNumber(); + int lastBlock = firstBlock; + if (cursor.hasSelection()) { - // Insert title # in front of the selected lines. + // Insert title # in front of the selected blocks. int start = cursor.selectionStart(); int end = cursor.selectionEnd(); - int startBlock = doc->findBlock(start).blockNumber(); - int endBlock = doc->findBlock(end).blockNumber(); - cursor.beginEditBlock(); - cursor.clearSelection(); - for (int i = startBlock; i <= endBlock; ++i) { - QTextBlock block = doc->findBlockByNumber(i); - cursor.setPosition(block.position(), QTextCursor::MoveAnchor); - cursor.insertText(titleMark + " "); - } - cursor.movePosition(QTextCursor::EndOfBlock); - cursor.endEditBlock(); - } else { - // Insert title # in front of current block. - cursor.beginEditBlock(); - cursor.movePosition(QTextCursor::StartOfBlock); - cursor.insertText(titleMark + " "); - cursor.movePosition(QTextCursor::EndOfBlock); - cursor.endEditBlock(); + firstBlock = doc->findBlock(start).blockNumber(); + lastBlock = doc->findBlock(end).blockNumber(); } + + cursor.beginEditBlock(); + for (int i = firstBlock; i <= lastBlock; ++i) { + VEditUtils::insertTitleMark(cursor, doc->findBlockByNumber(i), p_level); + } + + cursor.endEditBlock(); m_editor->setTextCursor(cursor); return true; } @@ -681,6 +695,10 @@ void VMdEditOperations::decorateText(TextDecoration p_decoration) decorateInlineCode(); break; + case TextDecoration::CodeBlock: + decorateCodeBlock(); + break; + default: validDecoration = false; qDebug() << "decoration" << (int)p_decoration << "is not implemented yet"; @@ -804,6 +822,86 @@ void VMdEditOperations::decorateInlineCode() m_editor->setTextCursor(cursor); } +void VMdEditOperations::decorateCodeBlock() +{ + const QString marker("```"); + + QTextCursor cursor = m_editor->textCursor(); + cursor.beginEditBlock(); + if (cursor.hasSelection()) { + // Insert ``` around the selected text. + int start = cursor.selectionStart(); + int end = cursor.selectionEnd(); + + QString indentation = VEditUtils::fetchIndentSpaces(cursor.block()); + + // Insert the end marker first. + cursor.setPosition(end, QTextCursor::MoveAnchor); + VEditUtils::insertBlock(cursor, false); + VEditUtils::indentBlock(cursor, indentation); + cursor.insertText(marker); + + // Insert the start marker. + cursor.setPosition(start, QTextCursor::MoveAnchor); + VEditUtils::insertBlock(cursor, true); + VEditUtils::indentBlock(cursor, indentation); + cursor.insertText(marker); + } else { + // Insert ``` ``` and place cursor after the first marker. + // Or if current block or next block is ```, we will skip it. + QTextBlock block = cursor.block(); + int state = block.userState(); + if (state == HighlightBlockState::CodeBlock + || state == HighlightBlockState::CodeBlockStart + || state == HighlightBlockState::CodeBlockEnd) { + // Find the block end. + while (block.isValid()) { + if (block.userState() == HighlightBlockState::CodeBlockEnd) { + break; + } + + block = block.next(); + } + + if (block.isValid()) { + // It is CodeBlockEnd. + cursor.setPosition(block.position()); + if (block.next().isValid()) { + cursor.movePosition(QTextCursor::NextBlock); + cursor.movePosition(QTextCursor::StartOfBlock); + } else { + cursor.movePosition(QTextCursor::EndOfBlock); + } + } else { + // Reach the end of the document. + cursor.movePosition(QTextCursor::End); + } + } else { + bool insertInline = false; + if (!cursor.atBlockEnd()) { + cursor.insertBlock(); + cursor.movePosition(QTextCursor::PreviousBlock); + } else if (cursor.atBlockStart()) { + insertInline = true; + } + + if (!insertInline) { + VEditUtils::insertBlock(cursor, false); + VEditUtils::indentBlockAsBlock(cursor, false); + } + + cursor.insertText(marker); + + VEditUtils::insertBlock(cursor, true); + VEditUtils::indentBlockAsBlock(cursor, true); + cursor.insertText(marker); + } + } + + cursor.endEditBlock(); + m_editor->setTextCursor(cursor); +} + void VMdEditOperations::decorateStrikethrough() { QTextCursor cursor = m_editor->textCursor(); @@ -840,3 +938,16 @@ void VMdEditOperations::decorateStrikethrough() cursor.endEditBlock(); m_editor->setTextCursor(cursor); } + +bool VMdEditOperations::insertLink(const QString &p_linkText, + const QString &p_linkUrl) +{ + QString link = QString("[%1](%2)").arg(p_linkText).arg(p_linkUrl); + QTextCursor cursor = m_editor->textCursor(); + cursor.insertText(link); + m_editor->setTextCursor(cursor); + + setVimMode(VimMode::Insert); + + return true; +} diff --git a/src/vmdeditoperations.h b/src/vmdeditoperations.h index faeb791f..d8609339 100644 --- a/src/vmdeditoperations.h +++ b/src/vmdeditoperations.h @@ -16,11 +16,18 @@ class VMdEditOperations : public VEditOperations Q_OBJECT public: VMdEditOperations(VEdit *p_editor, VFile *p_file); + bool insertImageFromMimeData(const QMimeData *source) Q_DECL_OVERRIDE; + bool insertImage() Q_DECL_OVERRIDE; + bool handleKeyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; + bool insertImageFromURL(const QUrl &p_imageUrl) Q_DECL_OVERRIDE; + bool insertLink(const QString &p_linkText, + const QString &p_linkUrl); + // Insert decoration markers or decorate selected text. // If it is Vim Normal mode, change to Insert mode first. void decorateText(TextDecoration p_decoration) Q_DECL_OVERRIDE; @@ -47,6 +54,10 @@ private: bool handleKeyEsc(QKeyEvent *p_event); bool handleKeyReturn(QKeyEvent *p_event); bool handleKeyBracketLeft(QKeyEvent *p_event); + + // Insert title of level @p_level. + // Will detect if current block already has some leading #s. If yes, + // will delete it and insert the correct #s. bool insertTitle(int p_level); // Change the sequence number of a list block. @@ -61,6 +72,9 @@ private: // Insert inline-code marker or set selected text inline-coded. void decorateInlineCode(); + // Insert inline-code marker or set selected text inline-coded. + void decorateCodeBlock(); + // Insert strikethrough marker or set selected text strikethrough. void decorateStrikethrough(); diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index 69d79642..658d5d07 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -11,7 +11,7 @@ #include "vconfigmanager.h" #include "vmarkdownconverter.h" #include "vnotebook.h" -#include "vtoc.h" +#include "vtableofcontent.h" #include "vmdedit.h" #include "dialog/vfindreplacedialog.h" #include "veditarea.h" @@ -22,13 +22,25 @@ extern VConfigManager *g_config; VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea, OpenFileMode p_mode, QWidget *p_parent) - : VEditTab(p_file, p_editArea, p_parent), m_editor(NULL), m_webViewer(NULL), - m_document(NULL), m_mdConType(g_config->getMdConverterType()) + : VEditTab(p_file, p_editArea, p_parent), + m_editor(NULL), + m_webViewer(NULL), + m_document(NULL), + m_mdConType(g_config->getMdConverterType()), + m_enableHeadingSequence(false) { V_ASSERT(m_file->getDocType() == DocType::Markdown); m_file->open(); + HeadingSequenceType headingSequenceType = g_config->getHeadingSequenceType(); + if (headingSequenceType == HeadingSequenceType::Enabled) { + m_enableHeadingSequence = true; + } else if (headingSequenceType == HeadingSequenceType::EnabledNoteOnly + && m_file->getType() == FileType::Note) { + m_enableHeadingSequence = true; + } + setupUI(); if (p_mode == OpenFileMode::Edit) { @@ -50,54 +62,110 @@ void VMdTab::setupUI() setLayout(m_stacks); } -void VMdTab::handleTextChanged() -{ - V_ASSERT(m_file->isModifiable()); - - if (m_modified) { - return; - } - - updateStatus(); -} - void VMdTab::showFileReadMode() { m_isEditMode = false; - int outlineIndex = m_curHeader.m_outlineIndex; + VHeaderPointer header(m_currentHeader); if (m_mdConType == MarkdownConverterType::Hoedown) { viewWebByConverter(); } else { m_document->updateText(); - updateTocFromHtml(m_document->getToc()); + updateOutlineFromHtml(m_document->getToc()); } m_stacks->setCurrentWidget(m_webViewer); clearSearchedWordHighlight(); - scrollWebViewToHeader(outlineIndex); + scrollWebViewToHeader(header); updateStatus(); } -void VMdTab::scrollWebViewToHeader(int p_outlineIndex) +bool VMdTab::scrollWebViewToHeader(const VHeaderPointer &p_header) { - QString anchor; - - m_curHeader = VAnchor(m_file, anchor, -1, p_outlineIndex); - - if (p_outlineIndex < m_toc.headers.size() && p_outlineIndex >= 0) { - QString tmp = m_toc.headers[p_outlineIndex].anchor; - V_ASSERT(!tmp.isEmpty()); - m_curHeader.anchor = tmp; - anchor = tmp.mid(1); + if (!m_outline.isMatched(p_header) + || m_outline.getType() != VTableOfContentType::Anchor) { + return false; } - m_document->scrollToAnchor(anchor); + if (p_header.isValid()) { + const VTableOfContentItem *item = m_outline.getItem(p_header); + if (item) { + if (item->m_anchor.isEmpty()) { + return false; + } - emit curHeaderChanged(m_curHeader); + m_currentHeader = p_header; + m_document->scrollToAnchor(item->m_anchor); + } else { + return false; + } + } else { + if (m_outline.isEmpty()) { + // Let it be. + m_currentHeader = p_header; + } else { + // Scroll to top. + m_currentHeader = p_header; + m_document->scrollToAnchor(""); + } + } + + emit currentHeaderChanged(m_currentHeader); + return true; +} + +bool VMdTab::scrollEditorToHeader(const VHeaderPointer &p_header) +{ + if (!m_outline.isMatched(p_header) + || m_outline.getType() != VTableOfContentType::BlockNumber) { + return false; + } + + VMdEdit *mdEdit = dynamic_cast(getEditor()); + + int blockNumber = -1; + if (p_header.isValid()) { + const VTableOfContentItem *item = m_outline.getItem(p_header); + if (item) { + blockNumber = item->m_blockNumber; + if (blockNumber == -1) { + // Empty item. + return false; + } + } else { + return false; + } + } else { + if (m_outline.isEmpty()) { + // No outline and scroll to -1 index. + // Just let it be. + m_currentHeader = p_header; + return true; + } else { + // Has outline and scroll to -1 index. + // Scroll to top. + blockNumber = 0; + } + } + + if (mdEdit->scrollToHeader(blockNumber)) { + m_currentHeader = p_header; + return true; + } else { + return false; + } +} + +bool VMdTab::scrollToHeaderInternal(const VHeaderPointer &p_header) +{ + if (m_isEditMode) { + return scrollEditorToHeader(p_header); + } else { + return scrollWebViewToHeader(p_header); + } } void VMdTab::viewWebByConverter() @@ -108,7 +176,7 @@ void VMdTab::viewWebByConverter() g_config->getMarkdownExtensions(), toc); m_document->setHtml(html); - updateTocFromHtml(toc); + updateOutlineFromHtml(toc); } void VMdTab::showFileEditMode() @@ -117,42 +185,28 @@ void VMdTab::showFileEditMode() return; } + VHeaderPointer header(m_currentHeader); + m_isEditMode = true; VMdEdit *mdEdit = dynamic_cast(getEditor()); V_ASSERT(mdEdit); - // beginEdit() may change m_curHeader. - int outlineIndex = m_curHeader.m_outlineIndex; - mdEdit->beginEdit(); m_stacks->setCurrentWidget(mdEdit); - int lineNumber = -1; - const QVector &headers = mdEdit->getHeaders(); // If editor is not init, we need to wait for it to init headers. // Generally, beginEdit() will generate the headers. Wait is needed when // highlight completion is going to re-generate the headers. int nrRetry = 5; - while (outlineIndex > -1 && headers.isEmpty() && nrRetry-- > 0) { + while (header.m_index > -1 && m_outline.isEmpty() && nrRetry-- > 0) { qDebug() << "wait another 100 ms for editor's headers ready"; VUtils::sleepWait(100); } - if (outlineIndex < 0 || outlineIndex >= headers.size()) { - lineNumber = -1; - outlineIndex = -1; - } else { - lineNumber = headers[outlineIndex].lineNumber; - } - - VAnchor anchor(m_file, "", lineNumber, outlineIndex); - - mdEdit->scrollToHeader(anchor); + scrollEditorToHeader(header); mdEdit->setFocus(); - - updateStatus(); } bool VMdTab::closeFile(bool p_forced) @@ -232,7 +286,7 @@ bool VMdTab::saveFile() return true; } - bool ret; + bool ret = true; // Make sure the file already exists. Temporary deal with cases when user delete or move // a file. QString filePath = m_file->fetchPath(); @@ -242,16 +296,16 @@ bool VMdTab::saveFile() tr("File %2 being written has been removed.") .arg(g_config->c_dataTextStyle).arg(filePath), QMessageBox::Ok, QMessageBox::Ok, this); - return false; - } - - m_editor->saveFile(); - ret = m_file->save(); - if (!ret) { - VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."), - tr("Fail to write to disk when saving a note. Please try it again."), - QMessageBox::Ok, QMessageBox::Ok, this); - m_editor->setModified(true); + ret = false; + } else { + m_editor->saveFile(); + ret = m_file->save(); + if (!ret) { + VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."), + tr("Fail to write to disk when saving a note. Please try it again."), + QMessageBox::Ok, QMessageBox::Ok, this); + m_editor->setModified(true); + } } updateStatus(); @@ -285,11 +339,13 @@ void VMdTab::setupMarkdownViewer() QWebChannel *channel = new QWebChannel(m_webViewer); channel->registerObject(QStringLiteral("content"), m_document); connect(m_document, &VDocument::tocChanged, - this, &VMdTab::updateTocFromHtml); - connect(m_document, SIGNAL(headerChanged(const QString&)), - this, SLOT(updateCurHeader(const QString &))); + this, &VMdTab::updateOutlineFromHtml); + connect(m_document, SIGNAL(headerChanged(const QString &)), + this, SLOT(updateCurrentHeader(const QString &))); connect(m_document, &VDocument::keyPressed, this, &VMdTab::handleWebKeyPressed); + connect(m_document, SIGNAL(logicsFinished(void)), + this, SLOT(restoreFromTabInfo(void))); page->setWebChannel(channel); m_webViewer->setHtml(VUtils::generateHtmlTemplate(m_mdConType, false), @@ -302,15 +358,16 @@ void VMdTab::setupMarkdownEditor() { Q_ASSERT(m_file->isModifiable() && !m_editor); qDebug() << "create Markdown editor"; + m_editor = new VMdEdit(m_file, m_document, m_mdConType, this); connect(dynamic_cast(m_editor), &VMdEdit::headersChanged, - this, &VMdTab::updateTocFromHeaders); + this, &VMdTab::updateOutlineFromHeaders); + connect(dynamic_cast(m_editor), SIGNAL(currentHeaderChanged(int)), + this, SLOT(updateCurrentHeader(int))); connect(dynamic_cast(m_editor), &VMdEdit::statusChanged, this, &VMdTab::updateStatus); - connect(m_editor, SIGNAL(curHeaderChanged(VAnchor)), - this, SLOT(updateCurHeader(VAnchor))); connect(m_editor, &VEdit::textChanged, - this, &VMdTab::handleTextChanged); + this, &VMdTab::updateStatus); connect(m_editor, &VEdit::cursorPositionChanged, this, &VMdTab::updateStatus); connect(m_editor, &VEdit::saveAndRead, @@ -324,193 +381,82 @@ void VMdTab::setupMarkdownEditor() connect(m_editor, &VEdit::vimStatusUpdated, this, &VEditTab::vimStatusUpdated); connect(m_editor, &VEdit::requestCloseFindReplaceDialog, - this, [this](){ + this, [this]() { this->m_editArea->getFindReplaceDialog()->closeDialog(); }); + connect(m_editor, SIGNAL(ready(void)), + this, SLOT(restoreFromTabInfo(void))); + enableHeadingSequence(m_enableHeadingSequence); m_editor->reloadFile(); m_stacks->addWidget(m_editor); } -static void parseTocUl(QXmlStreamReader &p_xml, QVector &p_headers, - int p_level); - -static void parseTocLi(QXmlStreamReader &p_xml, QVector &p_headers, int p_level) -{ - Q_ASSERT(p_xml.isStartElement() && p_xml.name() == "li"); - - if (p_xml.readNextStartElement()) { - if (p_xml.name() == "a") { - QString anchor = p_xml.attributes().value("href").toString(); - QString name; - if (p_xml.readNext()) { - if (p_xml.tokenString() == "Characters") { - name = p_xml.text().toString(); - } else if (!p_xml.isEndElement()) { - qWarning() << "TOC HTML should be ended by " << p_xml.name(); - return; - } - - VHeader header(p_level, name, anchor, -1, p_headers.size()); - p_headers.append(header); - } else { - // Error - return; - } - } else if (p_xml.name() == "ul") { - // Such as header 3 under header 1 directly - VHeader header(p_level, c_emptyHeaderName, "#", -1, p_headers.size()); - p_headers.append(header); - parseTocUl(p_xml, p_headers, p_level + 1); - } else { - qWarning() << "TOC HTML
  • should contain or