Browse Source

support file associations and open with built-in editors

Le Tan 4 years ago
parent
commit
b3a385693c

+ 34 - 11
src/core/buffer/filetypehelper.cpp

@@ -5,6 +5,8 @@
 
 #include <utils/fileutils.h>
 #include "buffer.h"
+#include <core/configmgr.h>
+#include <core/coreconfig.h>
 
 using namespace vnotex;
 
@@ -20,24 +22,38 @@ bool FileType::isMarkdown() const
 
 FileTypeHelper::FileTypeHelper()
 {
-    setupBuiltInTypes();
+    reload();
+}
 
-    // TODO: read configuration file.
+void FileTypeHelper::reload()
+{
+    setupBuiltInTypes();
 
     setupSuffixTypeMap();
 }
 
 void FileTypeHelper::setupBuiltInTypes()
 {
+    m_fileTypes.clear();
+
+    const auto &coreConfig = ConfigMgr::getInst().getCoreConfig();
+
     {
         FileType type;
         type.m_type = FileType::Markdown;
-        type.m_displayName = Buffer::tr("Markdown");
         type.m_typeName = QStringLiteral("Markdown");
-        type.m_suffixes << QStringLiteral("md")
-                        << QStringLiteral("mkd")
-                        << QStringLiteral("rmd")
-                        << QStringLiteral("markdown");
+        type.m_displayName = Buffer::tr("Markdown");
+
+        auto suffixes = coreConfig.findFileTypeSuffix(type.m_typeName);
+        if (suffixes && !suffixes->isEmpty()) {
+            type.m_suffixes = *suffixes;
+        } else {
+            type.m_suffixes << QStringLiteral("md")
+                            << QStringLiteral("mkd")
+                            << QStringLiteral("rmd")
+                            << QStringLiteral("markdown");
+        }
+
         m_fileTypes.push_back(type);
     }
 
@@ -46,7 +62,14 @@ void FileTypeHelper::setupBuiltInTypes()
         type.m_type = FileType::Text;
         type.m_typeName = QStringLiteral("Text");
         type.m_displayName = Buffer::tr("Text");
-        type.m_suffixes << QStringLiteral("txt") << QStringLiteral("text") << QStringLiteral("log");
+
+        auto suffixes = coreConfig.findFileTypeSuffix(type.m_typeName);
+        if (suffixes && !suffixes->isEmpty()) {
+            type.m_suffixes = *suffixes;
+        } else {
+            type.m_suffixes << QStringLiteral("txt") << QStringLiteral("text") << QStringLiteral("log");
+        }
+
         m_fileTypes.push_back(type);
     }
 
@@ -88,10 +111,10 @@ const FileType &FileTypeHelper::getFileTypeBySuffix(const QString &p_suffix) con
     }
 }
 
-#define ADD(x, y) m_suffixTypeMap.insert((x), (y))
-
 void FileTypeHelper::setupSuffixTypeMap()
 {
+    m_suffixTypeMap.clear();
+
     for (int i = 0; i < m_fileTypes.size(); ++i) {
         for (const auto &suffix : m_fileTypes[i].m_suffixes) {
             if (m_suffixTypeMap.contains(suffix)) {
@@ -113,7 +136,7 @@ const FileType &FileTypeHelper::getFileType(int p_type) const
     return m_fileTypes[p_type];
 }
 
-const FileTypeHelper &FileTypeHelper::getInst()
+FileTypeHelper &FileTypeHelper::getInst()
 {
     static FileTypeHelper helper;
     return helper;

+ 4 - 1
src/core/buffer/filetypehelper.h

@@ -32,6 +32,7 @@ namespace vnotex
         bool isMarkdown() const;
     };
 
+    // Only handle built-in editors.
     class FileTypeHelper
     {
     public:
@@ -47,7 +48,9 @@ namespace vnotex
 
         bool checkFileType(const QString &p_filePath, int p_type) const;
 
-        static const FileTypeHelper &getInst();
+        void reload();
+
+        static FileTypeHelper &getInst();
 
     private:
         FileTypeHelper();

+ 2 - 0
src/core/buffer/ibufferfactory.h

@@ -18,6 +18,8 @@ namespace vnotex
 
         virtual Buffer *createBuffer(const BufferParameters &p_parameters,
                                      QObject *p_parent) = 0;
+
+        virtual bool isBufferCreatedByFactory(const Buffer *p_buffer) const = 0;
     };
 } // ns vnotex
 

+ 5 - 0
src/core/buffer/markdownbufferfactory.cpp

@@ -9,3 +9,8 @@ Buffer *MarkdownBufferFactory::createBuffer(const BufferParameters &p_parameters
 {
     return new MarkdownBuffer(p_parameters, p_parent);
 }
+
+bool MarkdownBufferFactory::isBufferCreatedByFactory(const Buffer *p_buffer) const
+{
+    return dynamic_cast<const MarkdownBuffer *>(p_buffer) != nullptr;
+}

+ 2 - 0
src/core/buffer/markdownbufferfactory.h

@@ -11,6 +11,8 @@ namespace vnotex
     public:
         Buffer *createBuffer(const BufferParameters &p_parameters,
                              QObject *p_parent) Q_DECL_OVERRIDE;
+
+        bool isBufferCreatedByFactory(const Buffer *p_buffer) const Q_DECL_OVERRIDE;
     };
 } // vnotex
 

+ 5 - 0
src/core/buffer/textbufferfactory.cpp

@@ -9,3 +9,8 @@ Buffer *TextBufferFactory::createBuffer(const BufferParameters &p_parameters,
 {
     return new TextBuffer(p_parameters, p_parent);
 }
+
+bool TextBufferFactory::isBufferCreatedByFactory(const Buffer *p_buffer) const
+{
+    return dynamic_cast<const TextBuffer *>(p_buffer) != nullptr;
+}

+ 2 - 0
src/core/buffer/textbufferfactory.h

@@ -11,6 +11,8 @@ namespace vnotex
     public:
         Buffer *createBuffer(const BufferParameters &p_parameters,
                              QObject *p_parent) Q_DECL_OVERRIDE;
+
+        bool isBufferCreatedByFactory(const Buffer *p_buffer) const Q_DECL_OVERRIDE;
     };
 }
 

+ 105 - 5
src/core/buffermgr.cpp

@@ -11,14 +11,19 @@
 #include <buffer/nodebufferprovider.h>
 #include <buffer/filebufferprovider.h>
 #include <utils/widgetutils.h>
+#include <utils/processutils.h>
 #include "notebookmgr.h"
 #include "vnotex.h"
 #include "externalfile.h"
+#include "sessionconfig.h"
+#include "configmgr.h"
 
 #include "fileopenparameters.h"
 
 using namespace vnotex;
 
+QMap<QString, QString> BufferMgr::s_suffixToFileType;
+
 BufferMgr::BufferMgr(QObject *p_parent)
     : QObject(p_parent)
 {
@@ -66,12 +71,27 @@ void BufferMgr::open(Node *p_node, const QSharedPointer<FileOpenParameters> &p_p
         return;
     }
 
+    const auto nodePath = p_node->fetchAbsolutePath();
+
+    auto fileType = p_paras->m_fileType;
+    if (fileType.isEmpty()) {
+        // Check if we need to open it with external program by default according to the suffix.
+        fileType = findFileTypeByFile(nodePath);
+        if (openWithExternalProgram(nodePath, fileType)) {
+            return;
+        }
+    }
+
     auto buffer = findBuffer(p_node);
-    if (!buffer) {
-        auto nodePath = p_node->fetchAbsolutePath();
+    if (!buffer || !isSameTypeBuffer(buffer, fileType)) {
         auto nodeFile = p_node->getContentFile();
         Q_ASSERT(nodeFile);
-        auto fileType = nodeFile->getContentType().m_typeName;
+        if (fileType.isEmpty()) {
+            fileType = nodeFile->getContentType().m_typeName;
+        } else if (fileType != nodeFile->getContentType().m_typeName) {
+            nodeFile->setContentType(FileTypeHelper::getInst().getFileTypeByName(fileType).m_type);
+        }
+
         auto factory = m_bufferServer->getItem(fileType);
         if (!factory) {
             // No factory to open this file type.
@@ -96,6 +116,11 @@ void BufferMgr::open(const QString &p_filePath, const QSharedPointer<FileOpenPar
         return;
     }
 
+    // Check if it is requested to open with external program.
+    if (openWithExternalProgram(p_filePath, p_paras->m_fileType)) {
+        return;
+    }
+
     QFileInfo finfo(p_filePath);
     if (!finfo.exists()) {
         auto msg = QString("Failed to open file that does not exist (%1)").arg(p_filePath);
@@ -123,11 +148,25 @@ void BufferMgr::open(const QString &p_filePath, const QSharedPointer<FileOpenPar
         return;
     }
 
+    auto fileType = p_paras->m_fileType;
+    if (fileType.isEmpty()) {
+        // Check if we need to open it with external program by default according to the suffix.
+        fileType = findFileTypeByFile(p_filePath);
+        if (openWithExternalProgram(p_filePath, fileType)) {
+            return;
+        }
+    }
+
     auto buffer = findBuffer(p_filePath);
-    if (!buffer) {
+    if (!buffer || !isSameTypeBuffer(buffer, fileType)) {
         // Open it as external file.
         auto externalFile = QSharedPointer<ExternalFile>::create(p_filePath);
-        auto fileType = externalFile->getContentType().m_typeName;
+        if (fileType.isEmpty()) {
+            fileType = externalFile->getContentType().m_typeName;
+        } else if (fileType != externalFile->getContentType().m_typeName) {
+            externalFile->setContentType(FileTypeHelper::getInst().getFileTypeByName(fileType).m_type);
+        }
+
         auto factory = m_bufferServer->getItem(fileType);
         if (!factory) {
             // No factory to open this file type.
@@ -188,3 +227,64 @@ void BufferMgr::addBuffer(Buffer *p_buffer)
                 p_buffer->deleteLater();
             });
 }
+
+bool BufferMgr::openWithExternalProgram(const QString &p_filePath, const QString &p_name) const
+{
+    if (p_name.isEmpty()) {
+        return false;
+    }
+
+    if (auto pro = ConfigMgr::getInst().getSessionConfig().findExternalProgram(p_name)) {
+        const auto command = pro->fetchCommand(p_filePath);
+        if (!command.isEmpty()) {
+            ProcessUtils::startDetached(command);
+        }
+        return true;
+    }
+
+    return false;
+}
+
+bool BufferMgr::isSameTypeBuffer(const Buffer *p_buffer, const QString &p_typeName) const
+{
+    if (p_typeName.isEmpty()) {
+        return true;
+    }
+
+    auto factory = m_bufferServer->getItem(p_typeName);
+    Q_ASSERT(factory);
+    if (factory) {
+        return factory->isBufferCreatedByFactory(p_buffer);
+    }
+
+    return true;
+}
+
+void BufferMgr::updateSuffixToFileType(const QVector<CoreConfig::FileTypeSuffix> &p_fileTypeSuffixes)
+{
+    s_suffixToFileType.clear();
+
+    for (const auto &fts : p_fileTypeSuffixes) {
+        for (const auto &suf : fts.m_suffixes) {
+            auto it = s_suffixToFileType.find(suf);
+            if (it != s_suffixToFileType.end()) {
+                qWarning() << "suffix conflicts for file types" << fts.m_name << it.value();
+                it.value() = fts.m_name;
+            } else {
+                s_suffixToFileType.insert(suf, fts.m_name);
+            }
+        }
+    }
+}
+
+QString BufferMgr::findFileTypeByFile(const QString &p_filePath)
+{
+    QFileInfo fi(p_filePath);
+    auto suffix = fi.suffix().toLower();
+    auto it = s_suffixToFileType.find(suffix);
+    if (it != s_suffixToFileType.end()) {
+        return it.value();
+    } else {
+        return QString();
+    }
+}

+ 13 - 0
src/core/buffermgr.h

@@ -5,8 +5,10 @@
 #include <QScopedPointer>
 #include <QSharedPointer>
 #include <QVector>
+#include <QMap>
 
 #include "namebasedserver.h"
+#include "coreconfig.h"
 
 namespace vnotex
 {
@@ -30,6 +32,8 @@ namespace vnotex
 
         void open(const QString &p_filePath, const QSharedPointer<FileOpenParameters> &p_paras);
 
+        static void updateSuffixToFileType(const QVector<CoreConfig::FileTypeSuffix> &p_fileTypeSuffixes);
+
     signals:
         void bufferRequested(Buffer *p_buffer, const QSharedPointer<FileOpenParameters> &p_paras);
 
@@ -42,10 +46,19 @@ namespace vnotex
 
         void addBuffer(Buffer *p_buffer);
 
+        bool openWithExternalProgram(const QString &p_filePath, const QString &p_name) const;
+
+        bool isSameTypeBuffer(const Buffer *p_buffer, const QString &p_typeName) const;
+
+        static QString findFileTypeByFile(const QString &p_filePath);
+
         QSharedPointer<NameBasedServer<IBufferFactory>> m_bufferServer;
 
         // Managed by QObject.
         QVector<Buffer *> m_buffers;
+
+        // Mapping from suffix to file type or external program name.
+        static QMap<QString, QString> s_suffixToFileType;
     };
 } // ns vnotex
 

+ 80 - 0
src/core/coreconfig.cpp

@@ -3,6 +3,8 @@
 #include <QMetaEnum>
 #include <QLocale>
 
+#include <utils/utils.h>
+
 using namespace vnotex;
 
 #define READSTR(key) readString(appObj, userObj, (key))
@@ -10,6 +12,17 @@ using namespace vnotex;
 #define READBOOL(key) readBool(appObj, userObj, (key))
 #define READSTRLIST(key) readStringList(appObj, userObj, (key))
 
+CoreConfig::FileTypeSuffix::FileTypeSuffix(const QString &p_name, const QStringList &p_suffixes)
+    : m_name(p_name),
+      m_suffixes(p_suffixes)
+{
+}
+
+bool CoreConfig::FileTypeSuffix::operator==(const FileTypeSuffix &p_other) const
+{
+    return m_name == p_other.m_name && m_suffixes == p_other.m_suffixes;
+}
+
 QStringList CoreConfig::s_availableLocales;
 
 CoreConfig::CoreConfig(ConfigMgr *p_mgr, IConfig *p_topConfig)
@@ -73,6 +86,8 @@ void CoreConfig::init(const QJsonObject &p_app,
         auto lineEnding = READSTR(QStringLiteral("line_ending"));
         m_lineEnding = stringToLineEndingPolicy(lineEnding);
     }
+
+    loadFileTypeSuffixes(appObj, userObj);
 }
 
 QJsonObject CoreConfig::toJson() const
@@ -89,6 +104,7 @@ QJsonObject CoreConfig::toJson() const
     obj[QStringLiteral("history_max_count")] = m_historyMaxCount;
     obj[QStringLiteral("per_notebook_history")] = m_perNotebookHistoryEnabled;
     obj[QStringLiteral("line_ending")] = lineEndingPolicyToString(m_lineEnding);
+    obj[QStringLiteral("file_type_suffixes")] = saveFileTypeSuffixes();
     return obj;
 }
 
@@ -241,3 +257,67 @@ void CoreConfig::setLineEndingPolicy(LineEndingPolicy p_ending)
 {
     updateConfig(m_lineEnding, p_ending, this);
 }
+
+void CoreConfig::loadFileTypeSuffixes(const QJsonObject &p_app, const QJsonObject &p_user)
+{
+    m_fileTypeSuffixes.clear();
+
+    QJsonArray arr;
+    if (p_user.contains(QStringLiteral("file_type_suffixes"))) {
+        arr = p_user[QStringLiteral("file_type_suffixes")].toArray();
+    } else {
+        arr = p_app[QStringLiteral("file_type_suffixes")].toArray();
+    }
+
+    m_fileTypeSuffixes.reserve(arr.size());
+
+    for (int i = 0; i < arr.size(); ++i) {
+        const auto obj = arr[i].toObject();
+        const auto name = obj[QStringLiteral("name")].toString();
+        if (name.isEmpty()) {
+            continue;
+        }
+        const auto suffixes = readStringList(obj, QStringLiteral("suffixes"));
+        if (suffixes.isEmpty()) {
+            continue;
+        }
+        m_fileTypeSuffixes.push_back(FileTypeSuffix(name, Utils::toLower(suffixes)));
+    }
+}
+
+QJsonArray CoreConfig::saveFileTypeSuffixes() const
+{
+    QJsonArray arr;
+    for (const auto &fts : m_fileTypeSuffixes) {
+        QJsonObject obj;
+        obj[QStringLiteral("name")] = fts.m_name;
+        writeStringList(obj, QStringLiteral("suffixes"), fts.m_suffixes);
+        arr.push_back(obj);
+    }
+    return arr;
+}
+
+const QVector<CoreConfig::FileTypeSuffix> &CoreConfig::getFileTypeSuffixes() const
+{
+    return m_fileTypeSuffixes;
+}
+
+void CoreConfig::setFileTypeSuffixes(const QVector<CoreConfig::FileTypeSuffix> &p_fileTypeSuffixes)
+{
+    updateConfig(m_fileTypeSuffixes, p_fileTypeSuffixes, this);
+}
+
+const QStringList *CoreConfig::findFileTypeSuffix(const QString &p_name) const
+{
+    if (p_name.isEmpty()) {
+        return nullptr;
+    }
+
+    for (const auto &fts : m_fileTypeSuffixes) {
+        if (fts.m_name == p_name) {
+            return &fts.m_suffixes;
+        }
+    }
+
+    return nullptr;
+}

+ 24 - 0
src/core/coreconfig.h

@@ -72,6 +72,19 @@ namespace vnotex
         };
         Q_ENUM(Shortcut)
 
+        struct FileTypeSuffix
+        {
+            FileTypeSuffix() = default;
+
+            FileTypeSuffix(const QString &p_name, const QStringList &p_suffixes);
+
+            bool operator==(const FileTypeSuffix &p_other) const;
+
+            QString m_name;
+
+            QStringList m_suffixes;
+        };
+
         CoreConfig(ConfigMgr *p_mgr, IConfig *p_topConfig);
 
         void init(const QJsonObject &p_app, const QJsonObject &p_user) Q_DECL_OVERRIDE;
@@ -115,6 +128,11 @@ namespace vnotex
         LineEndingPolicy getLineEndingPolicy() const;
         void setLineEndingPolicy(LineEndingPolicy p_ending);
 
+        const QVector<FileTypeSuffix> &getFileTypeSuffixes() const;
+        void setFileTypeSuffixes(const QVector<FileTypeSuffix> &p_fileTypeSuffixes);
+
+        const QStringList *findFileTypeSuffix(const QString &p_name) const;
+
     private:
         friend class MainConfig;
 
@@ -124,6 +142,10 @@ namespace vnotex
 
         QJsonObject saveShortcuts() const;
 
+        void loadFileTypeSuffixes(const QJsonObject &p_app, const QJsonObject &p_user);
+
+        QJsonArray saveFileTypeSuffixes() const;
+
         // Theme name.
         QString m_theme;
 
@@ -157,6 +179,8 @@ namespace vnotex
 
         LineEndingPolicy m_lineEnding = LineEndingPolicy::LF;
 
+        QVector<FileTypeSuffix> m_fileTypeSuffixes;
+
         static QStringList s_availableLocales;
     };
 } // ns vnotex

+ 0 - 1
src/core/file.h

@@ -59,7 +59,6 @@ namespace vnotex
 
         const FileType &getContentType() const;
 
-    protected:
         void setContentType(int p_type);
 
     private:

+ 3 - 0
src/core/fileopenparameters.h

@@ -50,6 +50,9 @@ namespace vnotex
         // Whether should save this file into session.
         bool m_sessionEnabled = true;
 
+        // Whether specify the built-in file type to open as or the external program to open with.
+        QString m_fileType;
+
         std::function<void()> m_hooks[Hook::MaxHook];
     };
 }

+ 17 - 0
src/core/sessionconfig.cpp

@@ -56,6 +56,13 @@ QJsonObject SessionConfig::ExternalProgram::toJson() const
     return jobj;
 }
 
+QString SessionConfig::ExternalProgram::fetchCommand(const QString &p_file) const
+{
+    auto command(m_command);
+    command.replace(QStringLiteral("%1"), QString("\"%1\"").arg(p_file));
+    return command;
+}
+
 SessionConfig::SessionConfig(ConfigMgr *p_mgr)
     : IConfig(p_mgr, nullptr)
 {
@@ -438,6 +445,16 @@ const QVector<SessionConfig::ExternalProgram> &SessionConfig::getExternalProgram
     return m_externalPrograms;
 }
 
+const SessionConfig::ExternalProgram *SessionConfig::findExternalProgram(const QString &p_name) const
+{
+    for (const auto &pro : m_externalPrograms) {
+        if (pro.m_name == p_name) {
+            return &pro;
+        }
+    }
+    return nullptr;
+}
+
 const QVector<HistoryItem> &SessionConfig::getHistory() const
 {
     return m_history;

+ 3 - 0
src/core/sessionconfig.h

@@ -66,6 +66,8 @@ namespace vnotex
 
             QJsonObject toJson() const;
 
+            QString fetchCommand(const QString &p_file) const;
+
             QString m_name;
 
             // %1: the file paths to open.
@@ -134,6 +136,7 @@ namespace vnotex
         void removeQuickAccessFile(const QString &p_file);
 
         const QVector<ExternalProgram> &getExternalPrograms() const;
+        const ExternalProgram *findExternalProgram(const QString &p_name) const;
 
         const QVector<HistoryItem> &getHistory() const;
         void addHistory(const HistoryItem &p_item);

+ 2 - 0
src/core/vnotex.cpp

@@ -99,6 +99,8 @@ void VNoteX::initNotebookMgr()
 
 void VNoteX::initBufferMgr()
 {
+    BufferMgr::updateSuffixToFileType(ConfigMgr::getInst().getCoreConfig().getFileTypeSuffixes());
+
     Q_ASSERT(!m_bufferMgr);
     m_bufferMgr = new BufferMgr(this);
     m_bufferMgr->init();

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

@@ -6,7 +6,7 @@
         "version" : "3.12.0"
     },
     "core" : {
-        "theme" : "moonlight",
+        "theme" : "pure",
         "locale" : "",
         "shortcuts" : {
             "FullScreen" : "F11",
@@ -61,6 +61,25 @@
             "MoveOneSplitRight" : "Ctrl+G, Shift+L",
             "OpenLastClosedFile" : "Ctrl+Shift+T"
         },
+        "file_type_suffixes" : [
+            {
+                "name" : "Markdown",
+                "suffixes" : [
+                    "md",
+                    "mkd",
+                    "rmd",
+                    "markdown"
+                ]
+            },
+            {
+                "name" : "Text",
+                "suffixes" : [
+                    "txt",
+                    "text",
+                    "log"
+                ]
+            }
+        ],
         "shortcut_leader_key" : "Ctrl+G",
         "toolbar_icon_size" : 18,
         "docks_tabbar_icon_size" : 24,

+ 9 - 0
src/utils/utils.cpp

@@ -204,3 +204,12 @@ QColor Utils::toColor(const QString &p_color)
 
     return QColor(p_color);
 }
+
+QStringList Utils::toLower(const QStringList &p_list)
+{
+    QStringList lowerList;
+    for (const auto &ele : p_list) {
+        lowerList << ele.toLower();
+    }
+    return lowerList;
+}

+ 2 - 0
src/utils/utils.h

@@ -63,6 +63,8 @@ namespace vnotex
         static QJsonValue parseAndReadJson(const QJsonObject &p_obj, const QString &p_exp);
 
         static QColor toColor(const QString &p_color);
+
+        static QStringList toLower(const QStringList &p_list);
     };
 } // ns vnotex
 

+ 8 - 0
src/utils/widgetutils.cpp

@@ -25,6 +25,7 @@
 #include <QLayout>
 #include <QPushButton>
 #include <QSplitter>
+#include <QFormLayout>
 
 #include <core/global.h>
 
@@ -460,3 +461,10 @@ bool WidgetUtils::distributeWidgetsOfSplitter(QSplitter *p_splitter)
 
     return false;
 }
+
+void WidgetUtils::clearLayout(QFormLayout *p_layout)
+{
+    for (int i = p_layout->rowCount() - 1; i >= 0; --i) {
+        p_layout->removeRow(i);
+    }
+}

+ 3 - 0
src/utils/widgetutils.h

@@ -22,6 +22,7 @@ class QLayout;
 class QPushButton;
 class QSplitter;
 class QScreen;
+class QFormLayout;
 
 namespace vnotex
 {
@@ -90,6 +91,8 @@ namespace vnotex
 
         static bool distributeWidgetsOfSplitter(QSplitter *p_splitter);
 
+        static void clearLayout(QFormLayout *p_layout);
+
     private:
         static void resizeToHideScrollBar(QScrollArea *p_scroll, bool p_vertical, bool p_horizontal);
     };

+ 132 - 0
src/widgets/dialogs/settings/fileassociationpage.cpp

@@ -0,0 +1,132 @@
+#include "fileassociationpage.h"
+
+#include <QFormLayout>
+#include <QGroupBox>
+#include <QVBoxLayout>
+#include <QMap>
+
+#include <widgets/widgetsfactory.h>
+#include <widgets/lineedit.h>
+#include <core/coreconfig.h>
+#include <core/sessionconfig.h>
+#include <core/configmgr.h>
+#include <core/buffermgr.h>
+#include <utils/widgetutils.h>
+#include <utils/utils.h>
+#include <core/vnotex.h>
+#include <buffer/filetypehelper.h>
+
+using namespace vnotex;
+
+const char *FileAssociationPage::c_nameProperty = "name";
+
+const QChar FileAssociationPage::c_suffixSeparator = QLatin1Char(';');
+
+FileAssociationPage::FileAssociationPage(QWidget *p_parent)
+    : SettingsPage(p_parent)
+{
+    setupUI();
+}
+
+void FileAssociationPage::setupUI()
+{
+    auto mainLayout = new QVBoxLayout(this);
+
+    m_builtInFileTypesBox = new QGroupBox(tr("Built-In File Types"), this);
+    WidgetsFactory::createFormLayout(m_builtInFileTypesBox);
+    mainLayout->addWidget(m_builtInFileTypesBox);
+
+    m_externalProgramsBox = new QGroupBox(tr("External Programs"), this);
+    WidgetsFactory::createFormLayout(m_externalProgramsBox);
+    mainLayout->addWidget(m_externalProgramsBox);
+}
+
+void FileAssociationPage::loadInternal()
+{
+    loadBuiltInTypesGroup(m_builtInFileTypesBox);
+
+    loadExternalProgramsGroup(m_externalProgramsBox);
+}
+
+bool FileAssociationPage::saveInternal()
+{
+    auto &coreConfig = ConfigMgr::getInst().getCoreConfig();
+
+    QVector<CoreConfig::FileTypeSuffix> fileTypeSuffixes;
+
+    auto lineEdits = m_builtInFileTypesBox->findChildren<QLineEdit *>(QString());
+    lineEdits << m_externalProgramsBox->findChildren<QLineEdit *>(QString());
+    fileTypeSuffixes.reserve(lineEdits.size());
+    for (const auto lineEdit : lineEdits) {
+        auto name = lineEdit->property(c_nameProperty).toString();
+        if (name.isEmpty()) {
+            continue;
+        }
+        auto suffixes = lineEdit->text().split(c_suffixSeparator, Qt::SkipEmptyParts);
+        if (suffixes.isEmpty()) {
+            continue;
+        }
+        fileTypeSuffixes.push_back(CoreConfig::FileTypeSuffix(name, Utils::toLower(suffixes)));
+    }
+
+    coreConfig.setFileTypeSuffixes(fileTypeSuffixes);
+
+    FileTypeHelper::getInst().reload();
+
+    BufferMgr::updateSuffixToFileType(coreConfig.getFileTypeSuffixes());
+
+    return true;
+}
+
+QString FileAssociationPage::title() const
+{
+    return tr("File Associations");
+}
+
+void FileAssociationPage::loadBuiltInTypesGroup(QGroupBox *p_box)
+{
+    auto layout = static_cast<QFormLayout *>(p_box->layout());
+    WidgetUtils::clearLayout(layout);
+
+    const auto &types = FileTypeHelper::getInst().getAllFileTypes();
+
+    for (const auto &ft : types) {
+        if (ft.m_type == FileType::Others) {
+            continue;
+        }
+
+        auto lineEdit = WidgetsFactory::createLineEdit(p_box);
+        layout->addRow(ft.m_displayName, lineEdit);
+        connect(lineEdit, &QLineEdit::textChanged,
+                this, &FileAssociationPage::pageIsChanged);
+
+        lineEdit->setPlaceholderText(tr("Suffixes separated by ;"));
+        lineEdit->setToolTip(tr("List of suffixes for this file type"));
+        lineEdit->setProperty(c_nameProperty, ft.m_typeName);
+        lineEdit->setText(ft.m_suffixes.join(c_suffixSeparator));
+    }
+}
+
+void FileAssociationPage::loadExternalProgramsGroup(QGroupBox *p_box)
+{
+    auto layout = static_cast<QFormLayout *>(p_box->layout());
+    WidgetUtils::clearLayout(layout);
+
+    const auto &coreConfig = ConfigMgr::getInst().getCoreConfig();
+    const auto &sessionConfig = ConfigMgr::getInst().getSessionConfig();
+    for (const auto &pro : sessionConfig.getExternalPrograms()) {
+        auto lineEdit = WidgetsFactory::createLineEdit(p_box);
+        layout->addRow(pro.m_name, lineEdit);
+        connect(lineEdit, &QLineEdit::textChanged,
+                this, &FileAssociationPage::pageIsChanged);
+
+        lineEdit->setPlaceholderText(tr("Suffixes separated by ;"));
+        lineEdit->setToolTip(tr("List of suffixes to open with external program"));
+        lineEdit->setProperty(c_nameProperty, pro.m_name);
+
+        auto suffixes = coreConfig.findFileTypeSuffix(pro.m_name);
+        if (suffixes) {
+            lineEdit->setText(suffixes->join(c_suffixSeparator));
+        }
+    }
+}

+ 40 - 0
src/widgets/dialogs/settings/fileassociationpage.h

@@ -0,0 +1,40 @@
+#ifndef FILEASSOCIATIONPAGE_H
+#define FILEASSOCIATIONPAGE_H
+
+#include "settingspage.h"
+
+class QGroupBox;
+
+namespace vnotex
+{
+    class FileAssociationPage : public SettingsPage
+    {
+        Q_OBJECT
+    public:
+        explicit FileAssociationPage(QWidget *p_parent = nullptr);
+
+        QString title() const Q_DECL_OVERRIDE;
+
+    protected:
+        void loadInternal() Q_DECL_OVERRIDE;
+
+        bool saveInternal() Q_DECL_OVERRIDE;
+
+    private:
+        void setupUI();
+
+        void loadBuiltInTypesGroup(QGroupBox *p_box);
+
+        void loadExternalProgramsGroup(QGroupBox *p_box);
+
+        QGroupBox *m_builtInFileTypesBox = nullptr;
+
+        QGroupBox *m_externalProgramsBox = nullptr;
+
+        static const char *c_nameProperty;
+
+        static const QChar c_suffixSeparator;
+    };
+}
+
+#endif // FILEASSOCIATIONPAGE_H

+ 7 - 0
src/widgets/dialogs/settings/settingsdialog.cpp

@@ -27,6 +27,7 @@
 #include "imagehostpage.h"
 #include "vipage.h"
 #include "notemanagementpage.h"
+#include "fileassociationpage.h"
 
 using namespace vnotex;
 
@@ -173,6 +174,12 @@ void SettingsDialog::setupPages()
         */
     }
 
+    // File Association.
+    {
+        auto page = new FileAssociationPage(this);
+        addPage(page);
+    }
+
     setChangesUnsaved(false);
     m_pageExplorer->setCurrentItem(m_pageExplorer->topLevelItem(0), 0, QItemSelectionModel::ClearAndSelect);
     m_pageExplorer->expandAll();

+ 30 - 13
src/widgets/notebooknodeexplorer.cpp

@@ -17,7 +17,6 @@
 #include "mainwindow.h"
 #include <utils/iconutils.h>
 #include <utils/docsutils.h>
-#include <utils/processutils.h>
 #include "treewidget.h"
 #include "listwidget.h"
 #include "dialogs/notepropertiesdialog.h"
@@ -38,6 +37,7 @@
 #include <core/coreconfig.h>
 #include <core/sessionconfig.h>
 #include <core/widgetconfig.h>
+#include <buffer/filetypehelper.h>
 
 using namespace vnotex;
 
@@ -2106,15 +2106,32 @@ void NotebookNodeExplorer::addOpenWithMenu(QMenu *p_menu, bool p_master)
 {
     auto subMenu = p_menu->addMenu(tr("Open &With"));
 
+    const auto &types = FileTypeHelper::getInst().getAllFileTypes();
+
+    for (const auto &ft : types) {
+        if (ft.m_type == FileType::Others) {
+            continue;
+        }
+
+        QAction *act = subMenu->addAction(ft.m_displayName);
+        connect(act, &QAction::triggered,
+                this, [this, act, p_master]() {
+                    openSelectedNodesWithProgram(act->data().toString(), p_master);
+                });
+        act->setData(ft.m_typeName);
+    }
+
+    subMenu->addSeparator();
+
     {
         const auto &sessionConfig = ConfigMgr::getInst().getSessionConfig();
         for (const auto &pro : sessionConfig.getExternalPrograms()) {
             QAction *act = subMenu->addAction(pro.m_name);
             connect(act, &QAction::triggered,
                     this, [this, act, p_master]() {
-                        openSelectedNodesWithCommand(act->data().toString(), p_master);
+                        openSelectedNodesWithProgram(act->data().toString(), p_master);
                     });
-            act->setData(pro.m_command);
+            act->setData(pro.m_name);
             WidgetUtils::addActionShortcutText(act, pro.m_shortcut);
         }
     }
@@ -2125,7 +2142,7 @@ void NotebookNodeExplorer::addOpenWithMenu(QMenu *p_menu, bool p_master)
         auto defaultAct = subMenu->addAction(tr("System Default Program"));
         connect(defaultAct, &QAction::triggered,
                 this, [this, defaultAct, p_master]() {
-                    openSelectedNodesWithCommand(QString(), p_master);
+                    openSelectedNodesWithProgram(QString(), p_master);
                 });
         const auto &coreConfig = ConfigMgr::getInst().getCoreConfig();
         WidgetUtils::addActionShortcutText(defaultAct, coreConfig.getShortcut(CoreConfig::OpenWithDefaultProgram));
@@ -2157,7 +2174,7 @@ void NotebookNodeExplorer::setupShortcuts()
                         if (!isCombinedExploreMode()) {
                             isMaster = m_masterExplorer->hasFocus();
                         }
-                        openSelectedNodesWithCommand(QString(), isMaster);
+                        openSelectedNodesWithProgram(QString(), isMaster);
                     });
         }
     }
@@ -2165,21 +2182,21 @@ void NotebookNodeExplorer::setupShortcuts()
     const auto &sessionConfig = ConfigMgr::getInst().getSessionConfig();
     for (const auto &pro : sessionConfig.getExternalPrograms()) {
         auto shortcut = WidgetUtils::createShortcut(pro.m_shortcut, this);
-        const auto &command = pro.m_command;
+        const auto &name = pro.m_name;
         if (shortcut) {
             connect(shortcut, &QShortcut::activated,
-                    this, [this, command]() {
+                    this, [this, name]() {
                         bool isMaster = true;
                         if (!isCombinedExploreMode()) {
                             isMaster = m_masterExplorer->hasFocus();
                         }
-                        openSelectedNodesWithCommand(command, isMaster);
+                        openSelectedNodesWithProgram(name, isMaster);
                     });
         }
     }
 }
 
-void NotebookNodeExplorer::openSelectedNodesWithCommand(const QString &p_command, bool p_master)
+void NotebookNodeExplorer::openSelectedNodesWithProgram(const QString &p_name, bool p_master)
 {
     const bool closeBefore = ConfigMgr::getInst().getWidgetConfig().getNodeExplorerCloseBeforeOpenWithEnabled();
     const auto files = getSelectedNodesPath(p_master);
@@ -2196,12 +2213,12 @@ void NotebookNodeExplorer::openSelectedNodesWithCommand(const QString &p_command
             }
         }
 
-        if (p_command.isEmpty()) {
+        if (p_name.isEmpty()) {
             WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(file));
         } else {
-            auto command = p_command;
-            command.replace(QStringLiteral("%1"), QString("\"%1\"").arg(file));
-            ProcessUtils::startDetached(command);
+            auto paras = QSharedPointer<FileOpenParameters>::create();
+            paras->m_fileType = p_name;
+            emit VNoteX::getInst().openFileRequested(file, paras);
         }
     }
 }

+ 1 - 1
src/widgets/notebooknodeexplorer.h

@@ -297,7 +297,7 @@ namespace vnotex
 
         QStringList getSelectedNodesPath(bool p_master) const;
 
-        void openSelectedNodesWithCommand(const QString &p_command, bool p_master);
+        void openSelectedNodesWithProgram(const QString &p_name, bool p_master);
 
         bool belongsToMasterExplorer(const Node *p_node) const;
 

+ 1 - 2
src/widgets/webpage.cpp

@@ -9,7 +9,6 @@ using namespace vnotex;
 WebPage::WebPage(QWidget *p_parent)
     : QWebEnginePage(p_parent)
 {
-
 }
 
 bool WebPage::acceptNavigationRequest(const QUrl &p_url,
@@ -29,7 +28,7 @@ bool WebPage::acceptNavigationRequest(const QUrl &p_url,
     if (scheme == QStringLiteral("data")) {
         // Qt 5.12 and above will trigger this when calling QWebEngineView::setHtml().
         return true;
-    } else if (scheme == QStringLiteral("chrome-devtools")) {
+    } else if (scheme == QStringLiteral("chrome-devtools") || scheme == QStringLiteral("devtools")) {
         return true;
     }
 

+ 2 - 0
src/widgets/widgets.pri

@@ -22,6 +22,7 @@ SOURCES += \
     $$PWD/dialogs/selectionitemwidget.cpp \
     $$PWD/dialogs/settings/appearancepage.cpp \
     $$PWD/dialogs/settings/editorpage.cpp \
+    $$PWD/dialogs/settings/fileassociationpage.cpp \
     $$PWD/dialogs/settings/generalpage.cpp \
     $$PWD/dialogs/settings/imagehostpage.cpp \
     $$PWD/dialogs/settings/markdowneditorpage.cpp \
@@ -150,6 +151,7 @@ HEADERS += \
     $$PWD/dialogs/selectionitemwidget.h \
     $$PWD/dialogs/settings/appearancepage.h \
     $$PWD/dialogs/settings/editorpage.h \
+    $$PWD/dialogs/settings/fileassociationpage.h \
     $$PWD/dialogs/settings/generalpage.h \
     $$PWD/dialogs/settings/imagehostpage.h \
     $$PWD/dialogs/settings/markdowneditorpage.h \