Răsfoiți Sursa

Dev (#1644)

* add file type combobox in NewNoteDialog

* add SectionNumberStyle
Le Tan 4 ani în urmă
părinte
comite
6503b433e1

+ 1 - 1
libs/vtextedit

@@ -1 +1 @@
-Subproject commit 5fe5d4be7191de58fd1c2411b1ff97bcbecfaa3d
+Subproject commit 69bd57656ccac8cf75502506be0c87d80d86e577

+ 93 - 24
src/core/buffer/filetypehelper.cpp

@@ -1,54 +1,123 @@
 #include "filetypehelper.h"
 
 #include <QFileInfo>
+#include <QDebug>
 
 #include <utils/fileutils.h>
+#include "buffer.h"
 
 using namespace vnotex;
 
-const FileType FileTypeHelper::s_markdownFileType = "markdown";
-
-const FileType FileTypeHelper::s_textFileType = "text";
+FileTypeHelper::FileTypeHelper()
+{
+    setupBuiltInTypes();
 
-const FileType FileTypeHelper::s_unknownFileType = "unknown";
+    // TODO: read configuration file.
 
-QSharedPointer<QMap<QString, FileType>> FileTypeHelper::s_fileTypeMap;
+    setupSuffixTypeMap();
+}
 
-FileType FileTypeHelper::fileType(const QString &p_filePath)
+void FileTypeHelper::setupBuiltInTypes()
 {
-    Q_ASSERT(!p_filePath.isEmpty());
+    {
+        FileType type;
+        type.m_type = Type::Markdown;
+        type.m_displayName = Buffer::tr("Markdown");
+        type.m_typeName = QStringLiteral("Markdown");
+        type.m_suffixes << QStringLiteral("md") << QStringLiteral("mkd") << QStringLiteral("markdown");
+        m_fileTypes.push_back(type);
+    }
+
+    {
+        FileType type;
+        type.m_type = Type::Text;
+        type.m_typeName = QStringLiteral("Text");
+        type.m_displayName = Buffer::tr("Text");
+        type.m_suffixes << QStringLiteral("txt") << QStringLiteral("text") << QStringLiteral("log");
+        m_fileTypes.push_back(type);
+    }
 
-    if (!s_fileTypeMap) {
-        init();
+    {
+        FileType type;
+        type.m_type = Type::Others;
+        type.m_typeName = QStringLiteral("Others");
+        type.m_displayName = Buffer::tr("Others");
+        m_fileTypes.push_back(type);
     }
+}
+
+const FileType &FileTypeHelper::getFileType(const QString &p_filePath) const
+{
+    Q_ASSERT(!p_filePath.isEmpty());
 
     QFileInfo fi(p_filePath);
     auto suffix = fi.suffix().toLower();
-    auto it = s_fileTypeMap->find(suffix);
-    if (it != s_fileTypeMap->end()) {
-        return it.value();
+    auto it = m_suffixTypeMap.find(suffix);
+    if (it != m_suffixTypeMap.end()) {
+        return m_fileTypes.at(it.value());
     }
 
     // Treat all unknown text files as plain text files.
     if (FileUtils::isText(p_filePath)) {
-        return s_fileTypeMap->value(QStringLiteral("txt"));
+        return m_fileTypes[Type::Text];
     }
 
-    return s_unknownFileType;
+    return m_fileTypes[Type::Others];
+}
+
+const FileType &FileTypeHelper::getFileTypeBySuffix(const QString &p_suffix) const
+{
+    auto it = m_suffixTypeMap.find(p_suffix.toLower());
+    if (it != m_suffixTypeMap.end()) {
+        return m_fileTypes.at(it.value());
+    } else {
+        return m_fileTypes[Type::Others];
+    }
 }
 
-#define ADD(x, y) s_fileTypeMap->insert((x), (y))
+#define ADD(x, y) m_suffixTypeMap.insert((x), (y))
 
-void FileTypeHelper::init()
+void FileTypeHelper::setupSuffixTypeMap()
 {
-    // TODO: load mapping from configuration file.
-    s_fileTypeMap.reset(new QMap<QString, FileType>());
+    for (int i = 0; i < m_fileTypes.size(); ++i) {
+        for (const auto &suffix : m_fileTypes[i].m_suffixes) {
+            if (m_suffixTypeMap.contains(suffix)) {
+                qWarning() << "suffix conflicts detected" << suffix << m_fileTypes[i].m_type;
+            }
+            m_suffixTypeMap.insert(suffix, i);
+        }
+    }
+}
+
+const QVector<FileType> &FileTypeHelper::getAllFileTypes() const
+{
+    return m_fileTypes;
+}
 
-    ADD(QStringLiteral("md"), s_markdownFileType);
-    ADD(QStringLiteral("markdown"), s_markdownFileType);
-    ADD(QStringLiteral("mkd"), s_markdownFileType);
+const FileType &FileTypeHelper::getFileType(Type p_type) const
+{
+    return m_fileTypes[p_type];
+}
+
+const FileTypeHelper &FileTypeHelper::getInst()
+{
+    static FileTypeHelper helper;
+    return helper;
+}
+
+bool FileTypeHelper::checkFileType(const QString &p_filePath, Type p_type) const
+{
+    return getFileType(p_filePath).m_type == static_cast<int>(p_type);
+}
+
+const FileType &FileTypeHelper::getFileTypeByName(const QString &p_typeName) const
+{
+    for (const auto &ft : m_fileTypes) {
+        if (ft.m_typeName == p_typeName) {
+            return ft;
+        }
+    }
 
-    ADD(QStringLiteral("txt"), s_textFileType);
-    ADD(QStringLiteral("text"), s_textFileType);
-    ADD(QStringLiteral("log"), s_textFileType);
+    Q_ASSERT(false);
+    return m_fileTypes[Type::Others];
 }

+ 45 - 10
src/core/buffer/filetypehelper.h

@@ -3,30 +3,65 @@
 
 #include <QString>
 #include <QMap>
-#include <QSharedPointer>
+#include <QVector>
 
 namespace vnotex
 {
-    typedef QString FileType;
+    class FileType
+    {
+    public:
+        // FileTypeHelper::Type.
+        int m_type = -1;
+
+        QString m_typeName;
+
+        QString m_displayName;
+
+        QStringList m_suffixes;
+
+        QString preferredSuffix() const
+        {
+            return m_suffixes.isEmpty() ? QString() : m_suffixes.first();
+        }
+    };
 
-    // Map file suffix to file type.
     class FileTypeHelper
     {
     public:
-        FileTypeHelper() = delete;
+        enum Type
+        {
+            Markdown = 0,
+            Text,
+            Others
+        };
 
-        static FileType fileType(const QString &p_filePath);
+        const FileType &getFileType(const QString &p_filePath) const;
 
-        static const FileType s_markdownFileType;
+        const FileType &getFileType(Type p_type) const;
 
-        static const FileType s_textFileType;
+        const FileType &getFileTypeByName(const QString &p_typeName) const;
 
-        static const FileType s_unknownFileType;
+        const FileType &getFileTypeBySuffix(const QString &p_suffix) const;
+
+        const QVector<FileType> &getAllFileTypes() const;
+
+        bool checkFileType(const QString &p_filePath, Type p_type) const;
+
+        static const FileTypeHelper &getInst();
 
     private:
-        static void init();
+        FileTypeHelper();
+
+        void setupBuiltInTypes();
+
+        void setupSuffixTypeMap();
+
+        // Built-in Type could be accessed via enum Type.
+        QVector<FileType> m_fileTypes;
 
-        static QSharedPointer<QMap<QString, FileType>> s_fileTypeMap;
+        // suffix -> index of m_fileTypes.
+        // TODO: handle suffix conflicts.
+        QMap<QString, int> m_suffixTypeMap;
     };
 } // ns vnotex
 

+ 8 - 6
src/core/buffermgr.cpp

@@ -37,13 +37,15 @@ void BufferMgr::initBufferServer()
 {
     m_bufferServer.reset(new NameBasedServer<IBufferFactory>);
 
+    const auto &helper = FileTypeHelper::getInst();
+
     // Markdown.
     auto markdownFactory = QSharedPointer<MarkdownBufferFactory>::create();
-    m_bufferServer->registerItem(FileTypeHelper::s_markdownFileType, markdownFactory);
+    m_bufferServer->registerItem(helper.getFileType(FileTypeHelper::Markdown).m_typeName, markdownFactory);
 
     // Text.
     auto textFactory = QSharedPointer<TextBufferFactory>::create();
-    m_bufferServer->registerItem(FileTypeHelper::s_textFileType, textFactory);
+    m_bufferServer->registerItem(helper.getFileType(FileTypeHelper::Text).m_typeName, textFactory);
 }
 
 void BufferMgr::open(Node *p_node, const QSharedPointer<FileOpenParameters> &p_paras)
@@ -59,11 +61,11 @@ void BufferMgr::open(Node *p_node, const QSharedPointer<FileOpenParameters> &p_p
     auto buffer = findBuffer(p_node);
     if (!buffer) {
         auto nodePath = p_node->fetchAbsolutePath();
-        auto fileType = FileTypeHelper::fileType(nodePath);
+        auto fileType = FileTypeHelper::getInst().getFileType(nodePath).m_typeName;
         auto factory = m_bufferServer->getItem(fileType);
         if (!factory) {
             // No factory to open this file type.
-            qInfo() << "File will be opened by system:" << nodePath;
+            qInfo() << "file will be opened by default program" << nodePath;
             WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(nodePath));
             return;
         }
@@ -102,11 +104,11 @@ void BufferMgr::open(const QString &p_filePath, const QSharedPointer<FileOpenPar
     auto buffer = findBuffer(p_filePath);
     if (!buffer) {
         // Open it as external file.
-        auto fileType = FileTypeHelper::fileType(p_filePath);
+        auto fileType = FileTypeHelper::getInst().getFileType(p_filePath).m_typeName;
         auto factory = m_bufferServer->getItem(fileType);
         if (!factory) {
             // No factory to open this file type.
-            qInfo() << "File will be opened by system:" << p_filePath;
+            qInfo() << "file will be opened by default program" << p_filePath;
             WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(p_filePath));
             return;
         }

+ 33 - 0
src/core/markdowneditorconfig.cpp

@@ -37,6 +37,7 @@ void MarkdownEditorConfig::init(const QJsonObject &p_app, const QJsonObject &p_u
 
     m_sectionNumberMode = stringToSectionNumberMode(READSTR(QStringLiteral("section_number")));
     m_sectionNumberBaseLevel = READINT(QStringLiteral("section_number_base_level"));
+    m_sectionNumberStyle = stringToSectionNumberStyle(READSTR(QStringLiteral("section_number_style")));
 
     m_constrainImageWidthEnabled = READBOOL(QStringLiteral("constrain_image_width"));
     m_constrainInPlacePreviewWidthEnabled = READBOOL(QStringLiteral("constrain_inplace_preview_width"));
@@ -61,6 +62,7 @@ QJsonObject MarkdownEditorConfig::toJson() const
 
     obj[QStringLiteral("section_number")] = sectionNumberModeToString(m_sectionNumberMode);
     obj[QStringLiteral("section_number_base_level")] = m_sectionNumberBaseLevel;
+    obj[QStringLiteral("section_number_style")] = sectionNumberStyleToString(m_sectionNumberStyle);
 
     obj[QStringLiteral("constrain_image_width")] = m_constrainImageWidthEnabled;
     obj[QStringLiteral("constrain_inplace_preview_width")] = m_constrainInPlacePreviewWidthEnabled;
@@ -267,6 +269,27 @@ MarkdownEditorConfig::SectionNumberMode MarkdownEditorConfig::stringToSectionNum
     }
 }
 
+QString MarkdownEditorConfig::sectionNumberStyleToString(SectionNumberStyle p_style) const
+{
+    switch (p_style) {
+    case SectionNumberStyle::DigDotDig:
+        return QStringLiteral("digdotdig");
+
+    default:
+        return QStringLiteral("digdotdigdot");
+    }
+}
+
+MarkdownEditorConfig::SectionNumberStyle MarkdownEditorConfig::stringToSectionNumberStyle(const QString &p_str) const
+{
+    auto style = p_str.toLower();
+    if (style == QStringLiteral("digdotdig")) {
+        return SectionNumberStyle::DigDotDig;
+    } else {
+        return SectionNumberStyle::DigDotDigDot;
+    }
+}
+
 MarkdownEditorConfig::SectionNumberMode MarkdownEditorConfig::getSectionNumberMode() const
 {
     return m_sectionNumberMode;
@@ -286,3 +309,13 @@ void MarkdownEditorConfig::setSectionNumberBaseLevel(int p_level)
 {
     updateConfig(m_sectionNumberBaseLevel, p_level, this);
 }
+
+MarkdownEditorConfig::SectionNumberStyle MarkdownEditorConfig::getSectionNumberStyle() const
+{
+    return m_sectionNumberStyle;
+}
+
+void MarkdownEditorConfig::setSectionNumberStyle(SectionNumberStyle p_style)
+{
+    updateConfig(m_sectionNumberStyle, p_style, this);
+}

+ 17 - 0
src/core/markdowneditorconfig.h

@@ -22,6 +22,14 @@ namespace vnotex
             Edit
         };
 
+        enum SectionNumberStyle
+        {
+            // 1.1.
+            DigDotDigDot,
+            // 1.1
+            DigDotDig
+        };
+
         MarkdownEditorConfig(ConfigMgr *p_mgr,
                              IConfig *p_topConfig,
                              const QSharedPointer<TextEditorConfig> &p_textEditorConfig);
@@ -58,6 +66,9 @@ namespace vnotex
         int getSectionNumberBaseLevel() const;
         void setSectionNumberBaseLevel(int p_level);
 
+        SectionNumberStyle getSectionNumberStyle() const;
+        void setSectionNumberStyle(SectionNumberStyle p_style);
+
         bool getConstrainImageWidthEnabled() const;
         void setConstrainImageWidthEnabled(bool p_enabled);
 
@@ -88,6 +99,9 @@ namespace vnotex
         QString sectionNumberModeToString(SectionNumberMode p_mode) const;
         SectionNumberMode stringToSectionNumberMode(const QString &p_str) const;
 
+        QString sectionNumberStyleToString(SectionNumberStyle p_style) const;
+        SectionNumberStyle stringToSectionNumberStyle(const QString &p_str) const;
+
         QSharedPointer<TextEditorConfig> m_textEditorConfig;
 
         ViewerResource m_viewerResource;
@@ -112,6 +126,9 @@ namespace vnotex
         // 1 based.
         int m_sectionNumberBaseLevel = 2;
 
+        // Section number style.
+        SectionNumberStyle m_sectionNumberStyle = SectionNumberStyle::DigDotDigDot;
+
         // Whether enable image width constraint.
         bool m_constrainImageWidthEnabled = true;
 

+ 8 - 8
src/core/notebookconfigmgr/nodecontentmediautils.cpp

@@ -23,8 +23,8 @@ void NodeContentMediaUtils::copyMediaFiles(const Node *p_node,
                                            const QString &p_destFilePath)
 {
     Q_ASSERT(p_node->getType() == Node::Type::File);
-    auto fileType = FileTypeHelper::fileType(p_node->fetchAbsolutePath());
-    if (fileType == QStringLiteral("markdown")) {
+    const auto &fileType = FileTypeHelper::getInst().getFileType(p_node->fetchAbsolutePath());
+    if (fileType.m_type == FileTypeHelper::Markdown) {
         copyMarkdownMediaFiles(p_node->read(),
                                PathUtils::parentDirPath(p_node->fetchContentPath()),
                                p_backend,
@@ -36,8 +36,8 @@ void NodeContentMediaUtils::copyMediaFiles(const QString &p_filePath,
                                            INotebookBackend *p_backend,
                                            const QString &p_destFilePath)
 {
-    auto fileType = FileTypeHelper::fileType(p_filePath);
-    if (fileType == QStringLiteral("markdown")) {
+    const auto &fileType = FileTypeHelper::getInst().getFileType(p_filePath);
+    if (fileType.m_type == FileTypeHelper::Markdown) {
         copyMarkdownMediaFiles(FileUtils::readTextFile(p_filePath),
                                PathUtils::parentDirPath(p_filePath),
                                p_backend,
@@ -112,8 +112,8 @@ void NodeContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content,
 void NodeContentMediaUtils::removeMediaFiles(const Node *p_node)
 {
     Q_ASSERT(p_node->getType() == Node::Type::File);
-    auto fileType = FileTypeHelper::fileType(p_node->fetchAbsolutePath());
-    if (fileType == QStringLiteral("markdown")) {
+    const auto &fileType = FileTypeHelper::getInst().getFileType(p_node->fetchAbsolutePath());
+    if (fileType.m_type == FileTypeHelper::Markdown) {
         removeMarkdownMediaFiles(p_node);
     }
 }
@@ -162,8 +162,8 @@ void NodeContentMediaUtils::copyAttachment(Node *p_node,
         return;
     }
 
-    auto fileType = FileTypeHelper::fileType(p_node->fetchAbsolutePath());
-    if (fileType == QStringLiteral("markdown")) {
+    const auto &fileType = FileTypeHelper::getInst().getFileType(p_node->fetchAbsolutePath());
+    if (fileType.m_type == FileTypeHelper::Markdown) {
         fixMarkdownLinks(srcAttachmentFolderPath, p_backend, p_destFilePath, p_destAttachmentFolderPath);
     }
 }

+ 1 - 1
src/data/core/icons/type_code_block_editor.svg

@@ -1 +1 @@
-<svg t="1600000206395" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9662" width="512" height="512"><path d="M0 616.192l321.344 206.624 0-149.152L130.016 555.52l191.328-118.144L321.344 287.2 0 494.88 0 616.192zM389.312 896l110.592 0 162.048-768-111.328 0L389.312 896zM702.688 287.2l0 150.176 191.264 118.144-191.264 118.112 0 149.152L1024 616.192l0-121.312L702.688 287.2z" p-id="9663" fill="#000000"></path></svg>
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1609914714422" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7578" width="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M516.551 695.182c0 5.006 3.869 9.102 8.533 9.102h210.49c4.664 0 8.533-4.096 8.533-9.102V640.57c0-5.006-3.869-9.102-8.534-9.102H525.084c-4.664 0-8.533 4.096-8.533 9.102v54.613z m-221.753 6.94L513.252 518.94c4.323-3.64 4.323-10.353 0-13.994L294.798 321.877c-5.916-5.006-14.905-0.796-14.905 6.94v71.34c0 2.73 1.138 5.233 3.3 6.94L408.12 512 283.193 616.903c-2.048 1.707-3.3 4.324-3.3 6.94v71.34c0 7.736 8.989 11.946 14.905 6.94z" p-id="7579" fill="#000000"></path><path d="M930.702 56.889H93.298c-20.139 0-36.41 16.27-36.41 36.409v837.404c0 20.139 16.271 36.41 36.41 36.41h837.404c20.139 0 36.41-16.271 36.41-36.41V93.298c0-20.139-16.271-36.41-36.41-36.41z m-45.51 828.302H138.808V138.81H885.19V885.19z" p-id="7580" fill="#000000"></path></svg>

+ 4 - 1
src/data/core/vnotex.json

@@ -228,8 +228,11 @@
             "insert_file_name_as_title" : true,
             "//comment" : "none/read/edit",
             "section_number" : "read",
-            "//comment" : "base level to start section numbering, valid only in edit mode",
+            "//comment" : "Base level to start section numbering, valid only in edit mode",
             "section_number_base_level" : 2,
+            "//comment" : "Style of the section number in edit mode",
+            "//comment" : "digdotdigdot/digdotdig",
+            "section_number_style" : "digdotdigdot",
             "//comment" : "Whether enable image width constraint",
             "constrain_image_width" : true,
             "//comment" : "Whether enable in-place preview width constraint",

+ 15 - 0
src/data/extra/themes/moonlight/interface.qss

@@ -1079,3 +1079,18 @@ QSizeGrip {
     width: 16px;
     height: 16px;
 }
+
+/* ViewWindow */
+vnotex--ViewWindow QToolBar[ViewWindowToolBar="true"] {
+    background-color: @widgets#viewwindow#toolbar#bg;
+}
+
+/* ViewSplit */
+vnotex--ViewSplit QTabBar::tab:selected {
+    color: @widgets#viewsplit#tabbar#tab#selected#fg;
+    background-color: @widgets#viewsplit#tabbar#tab#selected#bg;
+}
+
+vte--VTextEdit {
+    border: none;
+}

+ 15 - 2
src/data/extra/themes/moonlight/palette.json

@@ -233,6 +233,14 @@
                 "active" : {
                     "fg" : "@base#icon#fg"
                 }
+            },
+            "tabbar" : {
+                "tab" : {
+                    "selected" : {
+                        "fg" : "@base#content#fg",
+                        "bg" : "@base#content#bg"
+                    }
+                }
             }
         },
         "qmainwindow" : {
@@ -403,8 +411,8 @@
                     "bg" : "@base#hover#bg"
                 },
                 "selected" : {
-                    "fg" : "@base#selected#fg",
-                    "bg" : "@base#selected#bg",
+                    "fg" : "@base#content#fg",
+                    "bg" : "@base#content#bg",
                     "border" : "@base#master#bg"
                 }
             }
@@ -584,6 +592,11 @@
                 "border" : "@widgets#qslider#handle#border",
                 "bg" : "@base#master#alt"
             }
+        },
+        "viewwindow" : {
+            "toolbar" : {
+                "bg" : "@base#content#bg"
+            }
         }
     }
 }

+ 1 - 1
src/data/extra/themes/moonlight/text-editor.theme

@@ -108,7 +108,7 @@
         },
         "HRULE" : {
             "text-color" : "#abb2bf",
-            "background-color" : "#493134"
+            "background-color" : "#864046"
         },
         "LIST_BULLET" : {
             "text-color" : "#e06c75",

+ 5 - 5
src/data/extra/web/css/globalstyles.css

@@ -22,27 +22,27 @@
 
 #vx-content.vx-section-number h2::before {
     counter-increment: section1;
-    content: counter(section1) " ";
+    content: counter(section1) ". ";
 }
 
 #vx-content.vx-section-number h3::before {
     counter-increment: section2;
-    content: counter(section1) "." counter(section2) " ";
+    content: counter(section1) "." counter(section2) ". ";
 }
 
 #vx-content.vx-section-number h4::before {
     counter-increment: section3;
-    content: counter(section1) "." counter(section2) "." counter(section3) " ";
+    content: counter(section1) "." counter(section2) "." counter(section3) ". ";
 }
 
 #vx-content.vx-section-number h5::before {
     counter-increment: section4;
-    content: counter(section1) "." counter(section2) "." counter(section3) "." counter(section4) " ";
+    content: counter(section1) "." counter(section2) "." counter(section3) "." counter(section4) ". ";
 }
 
 #vx-content.vx-section-number h6::before {
     counter-increment: section5;
-    content: counter(section1) "." counter(section2) "." counter(section3) "." counter(section4) "." counter(section5) " ";
+    content: counter(section1) "." counter(section2) "." counter(section3) "." counter(section4) "." counter(section5) ". ";
 }
 
 #vx-content.vx-constrain-image-width img {

+ 3 - 1
src/data/extra/web/js/markjs.js

@@ -61,7 +61,9 @@ class MarkJs {
             'className': this.className,
             'caseSensitive': p_options.caseSensitive,
             'accuracy': p_options.wholeWordOnly ? 'exactly' : 'partially',
-            'done': callbackFunc(this, p_text, p_options)
+            'done': callbackFunc(this, p_text, p_options),
+            // Ignore SVG, or SVG will be corrupted.
+            'exclude': ['svg *']
         }
 
         if (p_options.regularExpression) {

+ 1 - 1
src/data/extra/web/js/utils.js

@@ -111,6 +111,6 @@ class Utils {
     }
 
     static headingSequenceRegExp() {
-        return /^\d{1,3}(?:\.\d+)*\.? /;
+        return /^\d{1,3}(?:\.\d+)*\. /;
     }
 }

+ 8 - 0
src/utils/widgetutils.cpp

@@ -22,6 +22,7 @@
 #include <QMenu>
 #include <QDebug>
 #include <QFormLayout>
+#include <QLineEdit>
 
 using namespace vnotex;
 
@@ -366,3 +367,10 @@ QFormLayout *WidgetUtils::createFormLayout(QWidget *p_parent)
 
     return layout;
 }
+
+void WidgetUtils::selectBaseName(QLineEdit *p_lineEdit)
+{
+    auto text = p_lineEdit->text();
+    int dotIndex = text.lastIndexOf(QLatin1Char('.'));
+    p_lineEdit->setSelection(0, (dotIndex == -1) ? text.size() : dotIndex);
+}

+ 4 - 0
src/utils/widgetutils.h

@@ -18,6 +18,7 @@ class QListView;
 class QMenu;
 class QShortcut;
 class QFormLayout;
+class QLineEdit;
 
 namespace vnotex
 {
@@ -80,6 +81,9 @@ namespace vnotex
 
         static QFormLayout *createFormLayout(QWidget *p_parent = nullptr);
 
+        // Select the base name part of the line edit content.
+        static void selectBaseName(QLineEdit *p_lineEdit);
+
     private:
         static void resizeToHideScrollBar(QScrollArea *p_scroll, bool p_vertical, bool p_horizontal);
     };

+ 2 - 2
src/widgets/dialogs/newnotedialog.cpp

@@ -8,6 +8,7 @@
 #include <utils/pathutils.h>
 #include "exception.h"
 #include "nodeinfowidget.h"
+#include <utils/widgetutils.h>
 
 using namespace vnotex;
 
@@ -110,8 +111,7 @@ void NewNoteDialog::initDefaultValues()
     {
         auto lineEdit = m_infoWidget->getNameLineEdit();
         lineEdit->setText(c_defaultNoteName);
-        int dotIndex = c_defaultNoteName.lastIndexOf('.');
-        lineEdit->setSelection(0, (dotIndex == -1) ? c_defaultNoteName.size() : dotIndex);
+        WidgetUtils::selectBaseName(lineEdit);
 
         validateInputs();
     }

+ 82 - 28
src/widgets/dialogs/nodeinfowidget.cpp

@@ -9,6 +9,7 @@
 #include "exception.h"
 #include "nodelabelwithupbutton.h"
 #include <utils/widgetutils.h>
+#include <buffer/filetypehelper.h>
 
 using namespace vnotex;
 
@@ -16,11 +17,9 @@ NodeInfoWidget::NodeInfoWidget(const Node *p_node, QWidget *p_parent)
     : QWidget(p_parent),
       m_mode(Mode::Edit)
 {
-    setupUI(p_node->getParent());
+    setupUI(p_node->getParent(), p_node->getType());
 
     setNode(p_node);
-
-    setStateAccordingToModeAndNodeType(p_node->getType());
 }
 
 NodeInfoWidget::NodeInfoWidget(const Node *p_parentNode,
@@ -29,31 +28,42 @@ NodeInfoWidget::NodeInfoWidget(const Node *p_parentNode,
     : QWidget(p_parent),
       m_mode(Mode::Create)
 {
-    setupUI(p_parentNode);
-
-    setStateAccordingToModeAndNodeType(p_typeToCreate);
+    setupUI(p_parentNode, p_typeToCreate);
 }
 
-void NodeInfoWidget::setupUI(const Node *p_parentNode)
+void NodeInfoWidget::setupUI(const Node *p_parentNode, Node::Type p_newNodeType)
 {
+    const bool createMode = m_mode == Mode::Create;
+    const bool isNote = p_newNodeType == Node::Type::File;
+
     m_mainLayout = WidgetUtils::createFormLayout(this);
 
     m_mainLayout->addRow(tr("Notebook:"),
                          new QLabel(p_parentNode->getNotebook()->getName(), this));
 
     m_parentNodeLabel = new NodeLabelWithUpButton(p_parentNode, this);
+    m_parentNodeLabel->setReadOnly(!createMode);
     connect(m_parentNodeLabel, &NodeLabelWithUpButton::nodeChanged,
             this, &NodeInfoWidget::inputEdited);
     m_mainLayout->addRow(tr("Location:"), m_parentNodeLabel);
 
+    if (createMode && isNote) {
+        setupFileTypeComboBox(this);
+        m_mainLayout->addRow(tr("File type:"), m_fileTypeComboBox);
+    }
+
     setupNameLineEdit(this);
     m_mainLayout->addRow(tr("Name:"), m_nameLineEdit);
 
-    m_createdDateTimeLabel = new QLabel(this);
-    m_mainLayout->addRow(tr("Created time:"), m_createdDateTimeLabel);
+    if (!createMode) {
+        m_createdDateTimeLabel = new QLabel(this);
+        m_mainLayout->addRow(tr("Created time:"), m_createdDateTimeLabel);
+    }
 
-    m_modifiedDateTimeLabel = new QLabel(this);
-    m_mainLayout->addRow(tr("Modified time:"), m_modifiedDateTimeLabel);
+    if (!createMode && isNote) {
+        m_modifiedDateTimeLabel = new QLabel(this);
+        m_mainLayout->addRow(tr("Modified time:"), m_modifiedDateTimeLabel);
+    }
 }
 
 void NodeInfoWidget::setupNameLineEdit(QWidget *p_parent)
@@ -63,23 +73,30 @@ void NodeInfoWidget::setupNameLineEdit(QWidget *p_parent)
                                                      m_nameLineEdit);
     m_nameLineEdit->setValidator(validator);
     connect(m_nameLineEdit, &QLineEdit::textEdited,
-            this, &NodeInfoWidget::inputEdited);
-}
-
-void NodeInfoWidget::setStateAccordingToModeAndNodeType(Node::Type p_type)
-{
-    bool createMode = m_mode == Mode::Create;
-    bool isNote = p_type == Node::Type::File;
-
-    m_parentNodeLabel->setReadOnly(!createMode);
-
-    bool visible = !createMode;
-    m_createdDateTimeLabel->setVisible(visible);
-    m_mainLayout->labelForField(m_createdDateTimeLabel)->setVisible(visible);
-
-    visible = !createMode && isNote;
-    m_modifiedDateTimeLabel->setVisible(visible);
-    m_mainLayout->labelForField(m_modifiedDateTimeLabel)->setVisible(visible);
+            this, [this]() {
+                // Choose the correct file type.
+                if (m_fileTypeComboBox) {
+                    auto inputName = m_nameLineEdit->text();
+                    QString typeName;
+                    int dotIdx = inputName.lastIndexOf(QLatin1Char('.'));
+                    if (dotIdx != -1) {
+                        auto suffix = inputName.mid(dotIdx + 1);
+                        const auto &fileType = FileTypeHelper::getInst().getFileTypeBySuffix(suffix);
+                        typeName = fileType.m_typeName;
+                    } else {
+                        typeName = FileTypeHelper::getInst().getFileType(FileTypeHelper::Others).m_typeName;
+                    }
+
+                    int idx = m_fileTypeComboBox->findData(typeName);
+                    if (idx != -1) {
+                        m_fileTypeComboBoxMuted = true;
+                        m_fileTypeComboBox->setCurrentIndex(idx);
+                        m_fileTypeComboBoxMuted = false;
+                    }
+                }
+
+                emit inputEdited();
+            });
 }
 
 QLineEdit *NodeInfoWidget::getNameLineEdit() const
@@ -108,6 +125,7 @@ void NodeInfoWidget::setNode(const Node *p_node)
         return;
     }
 
+    Q_ASSERT(m_mode != Mode::Create);
     m_node = p_node;
     if (m_node) {
         Q_ASSERT(getNotebook() == m_node->getNotebook());
@@ -120,3 +138,39 @@ void NodeInfoWidget::setNode(const Node *p_node)
         m_modifiedDateTimeLabel->setText(modifiedTime);
     }
 }
+
+void NodeInfoWidget::setupFileTypeComboBox(QWidget *p_parent)
+{
+    m_fileTypeComboBox = WidgetsFactory::createComboBox(p_parent);
+    const auto &fileTypes = FileTypeHelper::getInst().getAllFileTypes();
+    for (const auto &ft : fileTypes) {
+        m_fileTypeComboBox->addItem(ft.m_displayName, ft.m_typeName);
+    }
+    connect(m_fileTypeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
+            this, [this]() {
+                if (m_fileTypeComboBoxMuted) {
+                    return;
+                }
+                auto name = m_fileTypeComboBox->currentData().toString();
+                const auto &fileType = FileTypeHelper::getInst().getFileTypeByName(name);
+                const auto suffix = fileType.preferredSuffix();
+                if (!suffix.isEmpty()) {
+                    // Change the suffix.
+                    auto inputName = m_nameLineEdit->text();
+                    QString newName;
+                    int dotIdx = inputName.lastIndexOf(QLatin1Char('.'));
+                    if (dotIdx == -1) {
+                        newName = inputName + QLatin1Char('.') + suffix;
+                    } else if (inputName.mid(dotIdx + 1) != suffix) {
+                        newName = inputName.left(dotIdx + 1) + suffix;
+                    }
+
+                    if (!newName.isEmpty()) {
+                        m_nameLineEdit->setText(newName);
+                    }
+                }
+
+                WidgetUtils::selectBaseName(m_nameLineEdit);
+                m_nameLineEdit->setFocus();
+            });
+}

+ 8 - 3
src/widgets/dialogs/nodeinfowidget.h

@@ -8,6 +8,7 @@
 class QLineEdit;
 class QLabel;
 class QFormLayout;
+class QComboBox;
 
 namespace vnotex
 {
@@ -38,11 +39,11 @@ namespace vnotex
         void inputEdited();
 
     private:
-        void setupUI(const Node *p_parentNode);
+        void setupUI(const Node *p_parentNode, Node::Type p_newNodeType);
 
-        void setupNameLineEdit(QWidget *p_parent);
+        void setupFileTypeComboBox(QWidget *p_parent);
 
-        void setStateAccordingToModeAndNodeType(Node::Type p_type);
+        void setupNameLineEdit(QWidget *p_parent);
 
         void setNode(const Node *p_node);
 
@@ -50,6 +51,8 @@ namespace vnotex
 
         QFormLayout *m_mainLayout = nullptr;
 
+        QComboBox *m_fileTypeComboBox = nullptr;
+
         QLineEdit *m_nameLineEdit = nullptr;
 
         NodeLabelWithUpButton *m_parentNodeLabel = nullptr;
@@ -59,6 +62,8 @@ namespace vnotex
         QLabel *m_modifiedDateTimeLabel = nullptr;
 
         const Node *m_node = nullptr;
+
+        bool m_fileTypeComboBoxMuted = false;
     };
 } // ns vnotex
 

+ 2 - 2
src/widgets/dialogs/nodelabelwithupbutton.cpp

@@ -27,9 +27,9 @@ void NodeLabelWithUpButton::setupUI()
 
     auto iconFile = VNoteX::getInst().getThemeMgr().getIconFile("up_parent_node.svg");
     m_upButton = new QPushButton(IconUtils::fetchIconWithDisabledState(iconFile),
-                                 "",
+                                 tr("Up"),
                                  this);
-    m_upButton->setToolTip(tr("Up"));
+    m_upButton->setToolTip(tr("Create note under an upper level node"));
     connect(m_upButton, &QPushButton::clicked,
             this, [this]() {
                 if (!m_node->isRoot()) {

+ 19 - 3
src/widgets/dialogs/settings/markdowneditorpage.cpp

@@ -51,6 +51,10 @@ void MarkdownEditorPage::loadInternal()
         m_sectionNumberComboBox->setCurrentIndex(idx);
 
         m_sectionNumberBaseLevelSpinBox->setValue(markdownConfig.getSectionNumberBaseLevel());
+
+        idx = m_sectionNumberStyleComboBox->findData(static_cast<int>(markdownConfig.getSectionNumberStyle()));
+        Q_ASSERT(idx != -1);
+        m_sectionNumberStyleComboBox->setCurrentIndex(idx);
     }
 
     m_constrainImageWidthCheckBox->setChecked(markdownConfig.getConstrainImageWidthEnabled());
@@ -83,6 +87,11 @@ void MarkdownEditorPage::saveInternal()
         if (m_sectionNumberBaseLevelSpinBox->isEnabled()) {
             markdownConfig.setSectionNumberBaseLevel(m_sectionNumberBaseLevelSpinBox->value());
         }
+
+        if (m_sectionNumberStyleComboBox->isEnabled()) {
+            auto style = m_sectionNumberStyleComboBox->currentData().toInt();
+            markdownConfig.setSectionNumberStyle(static_cast<MarkdownEditorConfig::SectionNumberStyle>(style));
+        }
     }
 
     markdownConfig.setConstrainImageWidthEnabled(m_constrainImageWidthCheckBox->isChecked());
@@ -229,11 +238,9 @@ QGroupBox *MarkdownEditorPage::setupGeneralGroup()
 
         m_sectionNumberComboBox = WidgetsFactory::createComboBox(this);
         m_sectionNumberComboBox->setToolTip(tr("Section number mode"));
-
         m_sectionNumberComboBox->addItem(tr("None"), (int)MarkdownEditorConfig::SectionNumberMode::None);
         m_sectionNumberComboBox->addItem(tr("Read"), (int)MarkdownEditorConfig::SectionNumberMode::Read);
         m_sectionNumberComboBox->addItem(tr("Edit"), (int)MarkdownEditorConfig::SectionNumberMode::Edit);
-
         sectionLayout->addWidget(m_sectionNumberComboBox);
         connect(m_sectionNumberComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
                 this, &MarkdownEditorPage::pageIsChanged);
@@ -242,13 +249,22 @@ QGroupBox *MarkdownEditorPage::setupGeneralGroup()
         m_sectionNumberBaseLevelSpinBox->setToolTip(tr("Base level to start section numbering in edit mode"));
         m_sectionNumberBaseLevelSpinBox->setRange(1, 6);
         m_sectionNumberBaseLevelSpinBox->setSingleStep(1);
-
         sectionLayout->addWidget(m_sectionNumberBaseLevelSpinBox);
         connect(m_sectionNumberBaseLevelSpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
                 this, &MarkdownEditorPage::pageIsChanged);
+
+        m_sectionNumberStyleComboBox = WidgetsFactory::createComboBox(this);
+        m_sectionNumberStyleComboBox->setToolTip(tr("Section number style"));
+        m_sectionNumberStyleComboBox->addItem(tr("1.1."), (int)MarkdownEditorConfig::SectionNumberStyle::DigDotDigDot);
+        m_sectionNumberStyleComboBox->addItem(tr("1.1"), (int)MarkdownEditorConfig::SectionNumberStyle::DigDotDig);
+        sectionLayout->addWidget(m_sectionNumberStyleComboBox);
+        connect(m_sectionNumberStyleComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
+                this, &MarkdownEditorPage::pageIsChanged);
+
         connect(m_sectionNumberComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
                 this, [this](int p_index) {
                     m_sectionNumberBaseLevelSpinBox->setEnabled(p_index == MarkdownEditorConfig::SectionNumberMode::Edit);
+                    m_sectionNumberStyleComboBox->setEnabled(p_index == MarkdownEditorConfig::SectionNumberMode::Edit);
                 });
 
         const QString label(tr("Section number:"));

+ 2 - 0
src/widgets/dialogs/settings/markdowneditorpage.h

@@ -54,6 +54,8 @@ namespace vnotex
         QComboBox *m_sectionNumberComboBox = nullptr;
 
         QSpinBox *m_sectionNumberBaseLevelSpinBox = nullptr;
+
+        QComboBox *m_sectionNumberStyleComboBox = nullptr;
     };
 }
 

+ 34 - 25
src/widgets/editors/markdowneditor.cpp

@@ -755,7 +755,6 @@ void MarkdownEditor::updateHeadings(const QVector<vte::peg::ElementRegion> &p_he
     // Assume that each block contains only one line.
     // Only support # syntax for now.
     auto doc = document();
-    QRegExp headerReg(vte::MarkdownUtils::c_headerRegExp);
     for (auto const &reg : p_headerRegions) {
         auto block = doc->findBlock(reg.m_startPos);
         if (!block.isValid()) {
@@ -766,11 +765,11 @@ void MarkdownEditor::updateHeadings(const QVector<vte::peg::ElementRegion> &p_he
             qWarning() << "header accross multiple blocks, starting from block" << block.blockNumber() << block.text();
         }
 
-        if (headerReg.exactMatch(block.text())) {
-            const int level = headerReg.cap(1).length();
-            Heading heading(headerReg.cap(2).trimmed(),
-                            level,
-                            headerReg.cap(3),
+        auto match = vte::MarkdownUtils::matchHeader(block.text());
+        if (match.m_matched) {
+            Heading heading(match.m_header,
+                            match.m_level,
+                            match.m_sequence,
                             block.blockNumber());
             headings.append(heading);
         }
@@ -1097,10 +1096,9 @@ static void increaseSectionNumber(QVector<int> &p_sectionNumber, int p_level, in
     }
 }
 
-static QString joinSectionNumberStr(const QVector<int> &p_sectionNumber)
+static QString joinSectionNumberStr(const QVector<int> &p_sectionNumber, bool p_endingDot)
 {
     QString res;
-    // TODO: make it configurable? 1.1 or 1.1.?
     for (auto sec : p_sectionNumber) {
         if (sec != 0) {
             if (res.isEmpty()) {
@@ -1115,28 +1113,41 @@ static QString joinSectionNumberStr(const QVector<int> &p_sectionNumber)
         }
     }
 
-    return res;
+    if (p_endingDot && !res.isEmpty()) {
+        return res + '.';
+    } else {
+        return res;
+    }
 }
 
 static bool updateHeadingSectionNumber(QTextCursor &p_cursor,
                                        const QTextBlock &p_block,
-                                       QRegExp &p_headingReg,
-                                       QRegExp &p_prefixReg,
-                                       const QString &p_sectionNumber)
+                                       const QString &p_sectionNumber,
+                                       bool p_endingDot)
 {
     if (!p_block.isValid()) {
         return false;
     }
 
     QString text = p_block.text();
-    bool matched = p_headingReg.exactMatch(text);
-    Q_ASSERT(matched);
-
-    matched = p_prefixReg.exactMatch(text);
-    Q_ASSERT(matched);
+    auto match = vte::MarkdownUtils::matchHeader(text);
+    Q_ASSERT(match.m_matched);
+
+    bool isSequence = false;
+    if (!match.m_sequence.isEmpty()) {
+        // Check if this sequence is the real sequence matching current style.
+        if (match.m_sequence.endsWith('.')) {
+            isSequence = p_endingDot;
+        } else {
+            isSequence = !p_endingDot;
+        }
+    }
 
-    int start = p_headingReg.cap(1).length() + 1;
-    int end = p_prefixReg.cap(1).length();
+    int start = match.m_level + 1;
+    int end = match.m_level + match.m_spacesAfterMarker;
+    if (isSequence) {
+        end += match.m_sequence.size() + match.m_spacesAfterSequence;
+    }
 
     Q_ASSERT(start <= end);
 
@@ -1162,20 +1173,18 @@ bool MarkdownEditor::updateSectionNumber(const QVector<Heading> &p_headings)
     }
 
     bool changed = false;
+    bool endingDot = m_config.getSectionNumberStyle() == MarkdownEditorConfig::SectionNumberStyle::DigDotDigDot;
     auto doc = document();
-    QRegExp headerReg(vte::MarkdownUtils::c_headerRegExp);
-    QRegExp prefixReg(vte::MarkdownUtils::c_headerPrefixRegExp);
     QTextCursor cursor(doc);
     cursor.beginEditBlock();
     for (const auto &heading : p_headings) {
         increaseSectionNumber(sectionNumber, heading.m_level, baseLevel);
-        auto sectionStr = m_sectionNumberEnabled ? joinSectionNumberStr(sectionNumber) : QString();
+        auto sectionStr = m_sectionNumberEnabled ? joinSectionNumberStr(sectionNumber, endingDot) : QString();
         if (heading.m_blockNumber > -1 && sectionStr != heading.m_sectionNumber) {
             if (updateHeadingSectionNumber(cursor,
                                            doc->findBlockByNumber(heading.m_blockNumber),
-                                           headerReg,
-                                           prefixReg,
-                                           sectionStr)) {
+                                           sectionStr,
+                                           endingDot)) {
                 changed = true;
             }
         }

+ 2 - 1
src/widgets/markdownviewwindow.cpp

@@ -251,7 +251,8 @@ void MarkdownViewWindow::handleBufferChangedInternal()
 
 void MarkdownViewWindow::setupToolBar()
 {
-    auto toolBar = new QToolBar(this);
+    auto toolBar = createToolBar(this);
+
     const auto &editorConfig = ConfigMgr::getInst().getEditorConfig();
     const int iconSize = editorConfig.getToolBarIconSize();
     toolBar->setIconSize(QSize(iconSize, iconSize));

+ 1 - 0
src/widgets/outlineviewer.cpp

@@ -51,6 +51,7 @@ void OutlineViewer::setupUI(const QString &p_title)
 {
     auto mainLayout = new QVBoxLayout(this);
     mainLayout->setContentsMargins(0, 0, 0, 0);
+    mainLayout->setSpacing(0);
 
     {
         auto titleBar = setupTitleBar(p_title, this);

+ 2 - 0
src/widgets/propertydefs.cpp

@@ -13,3 +13,5 @@ const char *PropertyDefs::s_dialogCentralWidget = "DialogCentralWidget";
 const char *PropertyDefs::s_viewSplitCornerWidget = "ViewSplitCornerWidget";
 
 const char *PropertyDefs::s_state = "State";
+
+const char *PropertyDefs::s_viewWindowToolBar = "ViewWindowToolBar";

+ 2 - 0
src/widgets/propertydefs.h

@@ -19,6 +19,8 @@ namespace vnotex
 
         static const char *s_viewSplitCornerWidget;
 
+        static const char *s_viewWindowToolBar;
+
         // Values: info/warning/error.
         static const char *s_state;
     };

+ 1 - 1
src/widgets/textviewwindow.cpp

@@ -55,7 +55,7 @@ void TextViewWindow::setupUI()
 
 void TextViewWindow::setupToolBar()
 {
-    auto toolBar = new QToolBar(this);
+    auto toolBar = createToolBar(this);
     const auto &editorConfig = ConfigMgr::getInst().getEditorConfig();
     const int iconSize = editorConfig.getToolBarIconSize();
     toolBar->setIconSize(QSize(iconSize, iconSize));

+ 11 - 0
src/widgets/viewwindow.cpp

@@ -32,6 +32,7 @@
 #include "exception.h"
 #include "findandreplacewidget.h"
 #include "editors/statuswidget.h"
+#include "propertydefs.h"
 
 using namespace vnotex;
 
@@ -106,14 +107,17 @@ ViewWindow::~ViewWindow()
 void ViewWindow::setupUI()
 {
     m_mainLayout = new QVBoxLayout(this);
+    m_mainLayout->setSpacing(0);
     m_mainLayout->setContentsMargins(0, 0, 0, 0);
 
     m_topLayout = new QVBoxLayout();
     m_topLayout->setContentsMargins(0, 0, 0, 0);
+    m_topLayout->setSpacing(0);
     m_mainLayout->addLayout(m_topLayout, 0);
 
     m_bottomLayout = new QVBoxLayout();
     m_bottomLayout->setContentsMargins(0, 0, 0, 0);
+    m_bottomLayout->setSpacing(0);
     m_mainLayout->addLayout(m_bottomLayout, 0);
 }
 
@@ -1073,3 +1077,10 @@ void ViewWindow::read(bool p_save)
     }
     setFocus();
 }
+
+QToolBar *ViewWindow::createToolBar(QWidget *p_parent)
+{
+    auto toolBar = new QToolBar(p_parent);
+    toolBar->setProperty(PropertyDefs::s_viewWindowToolBar, true);
+    return toolBar;
+}

+ 3 - 0
src/widgets/viewwindow.h

@@ -11,6 +11,7 @@
 
 class QVBoxLayout;
 class QTimer;
+class QToolBar;
 
 namespace vnotex
 {
@@ -218,6 +219,8 @@ namespace vnotex
 
         static ViewWindow::Mode modeFromOpenParameters(const FileOpenParameters &p_paras);
 
+        static QToolBar *createToolBar(QWidget *p_parent = nullptr);
+
         // The revision of the buffer of the last sync content.
         int m_bufferRevision = 0;