From f1d931c27696379b3ceab46a7d9b79c2209fea3b Mon Sep 17 00:00:00 2001 From: Le Tan Date: Thu, 29 Jul 2021 20:29:55 +0800 Subject: [PATCH] support Image Host --- libs/vtextedit | 2 +- scripts/update_version.py | 9 + src/core/buffer/buffer.h | 2 +- src/core/buffer/markdownbuffer.cpp | 35 ++- src/core/buffer/markdownbuffer.h | 5 +- src/core/editorconfig.cpp | 92 ++++++ src/core/editorconfig.h | 37 +++ src/core/logger.cpp | 2 + src/core/mainconfig.cpp | 4 +- src/core/markdowneditorconfig.h | 2 + src/core/notebook/bundlenotebook.cpp | 13 + src/core/notebook/bundlenotebook.h | 5 + src/core/notebook/notebook.cpp | 6 + src/core/notebook/notebook.h | 5 + src/core/notebookconfigmgr/notebookconfig.cpp | 5 + src/core/notebookconfigmgr/notebookconfig.h | 4 + src/core/notebookmgr.h | 8 +- src/data/core/Info.plist | 4 +- src/data/core/core.qrc | 1 + src/data/core/icons/image_host_editor.svg | 1 + src/data/core/vnotex.json | 8 +- src/data/extra/docs/en/shortcuts.md | 3 +- src/data/extra/docs/en/welcome.md | 2 + src/data/extra/docs/zh_CN/shortcuts.md | 5 +- src/data/extra/docs/zh_CN/welcome.md | 4 +- src/imagehost/githubimagehost.cpp | 204 ++++++++++++ src/imagehost/githubimagehost.h | 61 ++++ src/imagehost/imagehost.cpp | 30 ++ src/imagehost/imagehost.h | 57 ++++ src/imagehost/imagehost.pri | 14 + src/imagehost/imagehostmgr.cpp | 198 ++++++++++++ src/imagehost/imagehostmgr.h | 61 ++++ src/imagehost/imagehostutils.cpp | 29 ++ src/imagehost/imagehostutils.h | 24 ++ src/src.pro | 2 + src/utils/pathutils.cpp | 16 + src/utils/pathutils.h | 2 + src/utils/utils.cpp | 13 + src/utils/utils.h | 5 + src/utils/webutils.cpp | 4 +- src/widgets/dialogs/imageinsertdialog.cpp | 14 +- src/widgets/dialogs/imageinsertdialog.h | 7 +- src/widgets/dialogs/newnotedialog.cpp | 71 +++-- src/widgets/dialogs/newnotedialog.h | 2 + src/widgets/dialogs/nodeinfowidget.cpp | 3 - src/widgets/dialogs/scrolldialog.cpp | 2 +- .../dialogs/settings/appearancepage.cpp | 4 +- src/widgets/dialogs/settings/appearancepage.h | 2 +- src/widgets/dialogs/settings/editorpage.cpp | 4 +- src/widgets/dialogs/settings/editorpage.h | 2 +- src/widgets/dialogs/settings/generalpage.cpp | 4 +- src/widgets/dialogs/settings/generalpage.h | 2 +- .../dialogs/settings/imagehostpage.cpp | 295 ++++++++++++++++++ src/widgets/dialogs/settings/imagehostpage.h | 60 ++++ .../dialogs/settings/markdowneditorpage.cpp | 4 +- .../dialogs/settings/markdowneditorpage.h | 2 +- src/widgets/dialogs/settings/miscpage.cpp | 4 +- src/widgets/dialogs/settings/miscpage.h | 2 +- .../dialogs/settings/newimagehostdialog.cpp | 88 ++++++ .../dialogs/settings/newimagehostdialog.h | 39 +++ .../dialogs/settings/quickaccesspage.cpp | 4 +- .../dialogs/settings/quickaccesspage.h | 2 +- .../dialogs/settings/settingsdialog.cpp | 67 +++- src/widgets/dialogs/settings/settingsdialog.h | 12 +- src/widgets/dialogs/settings/settingspage.cpp | 22 +- src/widgets/dialogs/settings/settingspage.h | 10 +- .../dialogs/settings/texteditorpage.cpp | 4 +- src/widgets/dialogs/settings/texteditorpage.h | 2 +- src/widgets/dialogs/settings/themepage.cpp | 4 +- src/widgets/dialogs/settings/themepage.h | 2 +- src/widgets/dialogs/snippetinfowidget.cpp | 1 + src/widgets/editors/markdowneditor.cpp | 156 +++++++-- src/widgets/editors/markdowneditor.h | 12 + src/widgets/editors/plantumlhelper.cpp | 2 + src/widgets/filesystemviewer.cpp | 54 ++-- src/widgets/filesystemviewer.h | 2 + src/widgets/locationlist.cpp | 5 + src/widgets/markdownviewwindow.cpp | 111 ++++++- src/widgets/markdownviewwindow.h | 7 + src/widgets/viewwindow.cpp | 63 ++++ src/widgets/viewwindow.h | 10 + src/widgets/viewwindowtoolbarhelper.cpp | 15 + src/widgets/viewwindowtoolbarhelper.h | 3 +- src/widgets/widgets.pri | 4 + .../test_core/test_notebook/test_notebook.pro | 1 + 85 files changed, 1995 insertions(+), 175 deletions(-) create mode 100644 src/data/core/icons/image_host_editor.svg create mode 100644 src/imagehost/githubimagehost.cpp create mode 100644 src/imagehost/githubimagehost.h create mode 100644 src/imagehost/imagehost.cpp create mode 100644 src/imagehost/imagehost.h create mode 100644 src/imagehost/imagehost.pri create mode 100644 src/imagehost/imagehostmgr.cpp create mode 100644 src/imagehost/imagehostmgr.h create mode 100644 src/imagehost/imagehostutils.cpp create mode 100644 src/imagehost/imagehostutils.h create mode 100644 src/widgets/dialogs/settings/imagehostpage.cpp create mode 100644 src/widgets/dialogs/settings/imagehostpage.h create mode 100644 src/widgets/dialogs/settings/newimagehostdialog.cpp create mode 100644 src/widgets/dialogs/settings/newimagehostdialog.h diff --git a/libs/vtextedit b/libs/vtextedit index 922084a3..c53fc8db 160000 --- a/libs/vtextedit +++ b/libs/vtextedit @@ -1 +1 @@ -Subproject commit 922084a388e1f135e25297ba84a9d0ca0078ed06 +Subproject commit c53fc8dbf6df14b9e1327de0a4907700ffee6049 diff --git a/scripts/update_version.py b/scripts/update_version.py index 0b3be32c..da76368d 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -18,3 +18,12 @@ for line in fileinput.input(['src/data/core/vnotex.json'], inplace = True): regExp = re.compile('(\\s+)VNOTE_VER: \\S+') for line in fileinput.input(['.github/workflows/ci-win.yml', '.github/workflows/ci-linux.yml', '.github/workflows/ci-macos.yml'], inplace = True): print(regExp.sub('\\1VNOTE_VER: ' + newVersion, line), end='') + +# Info.plist +regExp = re.compile('(\\s+)\\d\\.\\d\\.\\d') +for line in fileinput.input(['src/data/core/Info.plist'], inplace = True): + print(regExp.sub('\\1' + newVersion + '', line), end='') + +regExp = re.compile('(\\s+)\\d\\.\\d\\.\\d\\.\\d') +for line in fileinput.input(['src/data/core/Info.plist'], inplace = True): + print(regExp.sub('\\1' + newVersion + '.1', line), end='') diff --git a/src/core/buffer/buffer.h b/src/core/buffer/buffer.h index 9c3a6441..6aa896cd 100644 --- a/src/core/buffer/buffer.h +++ b/src/core/buffer/buffer.h @@ -77,7 +77,7 @@ namespace vnotex QString getPath() const; - // In some cases, getPath() may point to a ocntainer containting all the stuffs. + // In some cases, getPath() may point to a container containting all the stuffs. // getContentPath() will return the real path to the file providing the content. QString getContentPath() const; diff --git a/src/core/buffer/markdownbuffer.cpp b/src/core/buffer/markdownbuffer.cpp index f2daa0ce..bd5b90b6 100644 --- a/src/core/buffer/markdownbuffer.cpp +++ b/src/core/buffer/markdownbuffer.cpp @@ -35,9 +35,10 @@ QString MarkdownBuffer::insertImage(const QImage &p_image, const QString &p_imag void MarkdownBuffer::fetchInitialImages() { Q_ASSERT(m_initialImages.isEmpty()); + vte::MarkdownLink::TypeFlags linkFlags = vte::MarkdownLink::TypeFlag::LocalRelativeInternal | vte::MarkdownLink::TypeFlag::Remote; m_initialImages = vte::MarkdownUtils::fetchImagesFromMarkdownText(getContent(), getResourcePath(), - vte::MarkdownLink::TypeFlag::LocalRelativeInternal); + linkFlags); } void MarkdownBuffer::addInsertedImage(const QString &p_imagePath, const QString &p_urlInLink) @@ -45,41 +46,51 @@ void MarkdownBuffer::addInsertedImage(const QString &p_imagePath, const QString vte::MarkdownLink link; link.m_path = p_imagePath; link.m_urlInLink = p_urlInLink; - link.m_type = vte::MarkdownLink::TypeFlag::LocalRelativeInternal; + // There are two types: local internal and remote for image host. + link.m_type = PathUtils::isLocalFile(p_imagePath) ? vte::MarkdownLink::TypeFlag::LocalRelativeInternal : vte::MarkdownLink::TypeFlag::Remote; m_insertedImages.append(link); } -QSet MarkdownBuffer::clearObsoleteImages() +QHash MarkdownBuffer::clearObsoleteImages() { - QSet obsoleteImages; + QHash obsoleteImages; Q_ASSERT(!isModified()); const bool discarded = state() & StateFlag::Discarded; + const vte::MarkdownLink::TypeFlags linkFlags = vte::MarkdownLink::TypeFlag::LocalRelativeInternal | vte::MarkdownLink::TypeFlag::Remote; const auto latestImages = vte::MarkdownUtils::fetchImagesFromMarkdownText(!discarded ? getContent() : m_provider->read(), getResourcePath(), - vte::MarkdownLink::TypeFlag::LocalRelativeInternal); + linkFlags); QSet latestImagesPath; for (const auto &link : latestImages) { - latestImagesPath.insert(PathUtils::normalizePath(link.m_path)); + if (link.m_type & vte::MarkdownLink::TypeFlag::Remote) { + latestImagesPath.insert(link.m_path); + } else { + latestImagesPath.insert(PathUtils::normalizePath(link.m_path)); + } } for (const auto &link : m_insertedImages) { - if (!(link.m_type & vte::MarkdownLink::TypeFlag::LocalRelativeInternal)) { + if (!(link.m_type & linkFlags)) { continue; } - if (!latestImagesPath.contains(PathUtils::normalizePath(link.m_path))) { - obsoleteImages.insert(link.m_path); + const bool isRemote = link.m_type & vte::MarkdownLink::TypeFlag::Remote; + const auto linkPath = isRemote ? link.m_path : PathUtils::normalizePath(link.m_path); + if (!latestImagesPath.contains(linkPath)) { + obsoleteImages.insert(link.m_path, isRemote); } } m_insertedImages.clear(); for (const auto &link : m_initialImages) { - Q_ASSERT(link.m_type & vte::MarkdownLink::TypeFlag::LocalRelativeInternal); - if (!latestImagesPath.contains(PathUtils::normalizePath(link.m_path))) { - obsoleteImages.insert(link.m_path); + Q_ASSERT(link.m_type & linkFlags); + const bool isRemote = link.m_type & vte::MarkdownLink::TypeFlag::Remote; + const auto linkPath = isRemote ? link.m_path : PathUtils::normalizePath(link.m_path); + if (!latestImagesPath.contains(linkPath)) { + obsoleteImages.insert(link.m_path, isRemote); } } diff --git a/src/core/buffer/markdownbuffer.h b/src/core/buffer/markdownbuffer.h index 51028bc0..7eda4dbb 100644 --- a/src/core/buffer/markdownbuffer.h +++ b/src/core/buffer/markdownbuffer.h @@ -4,7 +4,7 @@ #include "buffer.h" #include -#include +#include #include @@ -28,7 +28,8 @@ namespace vnotex // Clear obsolete images. // Won't delete images, just return a list of obsolete images path. // Will re-init m_initialImages and clear m_insertedImages. - QSet clearObsoleteImages(); + // Return [ImagePath] -> IsRemote. + QHash clearObsoleteImages(); protected: ViewWindow *createViewWindowInternal(const QSharedPointer &p_paras, QWidget *p_parent) Q_DECL_OVERRIDE; diff --git a/src/core/editorconfig.cpp b/src/core/editorconfig.cpp index 7dc87ae5..59df7b30 100644 --- a/src/core/editorconfig.cpp +++ b/src/core/editorconfig.cpp @@ -12,6 +12,30 @@ using namespace vnotex; #define READSTR(key) readString(appObj, userObj, (key)) #define READBOOL(key) readBool(appObj, userObj, (key)) +bool EditorConfig::ImageHostItem::operator==(const ImageHostItem &p_other) const +{ + return m_type == p_other.m_type + && m_name == p_other.m_name + && m_config == p_other.m_config; +} + +void EditorConfig::ImageHostItem::fromJson(const QJsonObject &p_jobj) +{ + m_type = p_jobj[QStringLiteral("type")].toInt(); + m_name = p_jobj[QStringLiteral("name")].toString(); + m_config = p_jobj[QStringLiteral("config")].toObject(); +} + +QJsonObject EditorConfig::ImageHostItem::toJson() const +{ + QJsonObject obj; + obj[QStringLiteral("type")] = m_type; + obj[QStringLiteral("name")] = m_name; + obj[QStringLiteral("config")] = m_config; + return obj; +} + + EditorConfig::EditorConfig(ConfigMgr *p_mgr, IConfig *p_topConfig) : IConfig(p_mgr, p_topConfig), m_textEditorConfig(new TextEditorConfig(p_mgr, p_topConfig)), @@ -32,6 +56,8 @@ void EditorConfig::init(const QJsonObject &p_app, loadCore(appObj, userObj); + loadImageHost(appObj, userObj); + m_textEditorConfig->init(appObj, userObj); m_markdownEditorConfig->init(appObj, userObj); } @@ -112,6 +138,7 @@ QJsonObject EditorConfig::toJson() const obj[m_textEditorConfig->getSessionName()] = m_textEditorConfig->toJson(); obj[m_markdownEditorConfig->getSessionName()] = m_markdownEditorConfig->toJson(); obj[QStringLiteral("core")] = saveCore(); + obj[QStringLiteral("image_host")] = saveImageHost(); return obj; } @@ -212,3 +239,68 @@ void EditorConfig::setSpellCheckDefaultDictionary(const QString &p_dict) { updateConfig(m_spellCheckDefaultDictionary, p_dict, this); } + +void EditorConfig::loadImageHost(const QJsonObject &p_app, const QJsonObject &p_user) +{ + const auto appObj = p_app.value(QStringLiteral("image_host")).toObject(); + const auto userObj = p_user.value(QStringLiteral("image_host")).toObject(); + + { + auto arr = read(appObj, userObj, QStringLiteral("hosts")).toArray(); + m_imageHosts.resize(arr.size()); + for (int i = 0; i < arr.size(); ++i) { + m_imageHosts[i].fromJson(arr[i].toObject()); + } + } + + m_defaultImageHost = READSTR(QStringLiteral("default_image_host")); + m_clearObsoleteImageAtImageHost = READBOOL(QStringLiteral("clear_obsolete_image")); +} + +QJsonObject EditorConfig::saveImageHost() const +{ + QJsonObject obj; + + { + QJsonArray arr; + for (const auto &item : m_imageHosts) { + arr.append(item.toJson()); + } + obj[QStringLiteral("hosts")] = arr; + } + + obj[QStringLiteral("default_image_host")] = m_defaultImageHost; + obj[QStringLiteral("clear_obsolete_image")] = m_clearObsoleteImageAtImageHost; + + return obj; +} + +const QVector &EditorConfig::getImageHosts() const +{ + return m_imageHosts; +} + +void EditorConfig::setImageHosts(const QVector &p_hosts) +{ + updateConfig(m_imageHosts, p_hosts, this); +} + +const QString &EditorConfig::getDefaultImageHost() const +{ + return m_defaultImageHost; +} + +void EditorConfig::setDefaultImageHost(const QString &p_host) +{ + updateConfig(m_defaultImageHost, p_host, this); +} + +bool EditorConfig::isClearObsoleteImageAtImageHostEnabled() const +{ + return m_clearObsoleteImageAtImageHost; +} + +void EditorConfig::setClearObsoleteImageAtImageHostEnabled(bool p_enabled) +{ + updateConfig(m_clearObsoleteImageAtImageHost, p_enabled, this); +} diff --git a/src/core/editorconfig.h b/src/core/editorconfig.h index 96c0578e..bce830c2 100644 --- a/src/core/editorconfig.h +++ b/src/core/editorconfig.h @@ -6,6 +6,7 @@ #include #include #include +#include namespace vnotex { @@ -62,6 +63,23 @@ namespace vnotex }; Q_ENUM(AutoSavePolicy) + struct ImageHostItem + { + ImageHostItem() = default; + + bool operator==(const ImageHostItem &p_other) const; + + void fromJson(const QJsonObject &p_jobj); + + QJsonObject toJson() const; + + int m_type = 0; + + QString m_name; + + QJsonObject m_config; + }; + EditorConfig(ConfigMgr *p_mgr, IConfig *p_topConfig); ~EditorConfig(); @@ -93,6 +111,15 @@ namespace vnotex const QString &getSpellCheckDefaultDictionary() const; void setSpellCheckDefaultDictionary(const QString &p_dict); + const QVector &getImageHosts() const; + void setImageHosts(const QVector &p_hosts); + + const QString &getDefaultImageHost() const; + void setDefaultImageHost(const QString &p_host); + + bool isClearObsoleteImageAtImageHostEnabled() const; + void setClearObsoleteImageAtImageHostEnabled(bool p_enabled); + private: friend class MainConfig; @@ -107,6 +134,10 @@ namespace vnotex QString autoSavePolicyToString(AutoSavePolicy p_policy) const; AutoSavePolicy stringToAutoSavePolicy(const QString &p_str) const; + void loadImageHost(const QJsonObject &p_app, const QJsonObject &p_user); + + QJsonObject saveImageHost() const; + // Icon size of editor tool bar. int m_toolBarIconSize = 16; @@ -128,6 +159,12 @@ namespace vnotex bool m_spellCheckAutoDetectLanguageEnabled = false; QString m_spellCheckDefaultDictionary; + + QVector m_imageHosts; + + QString m_defaultImageHost; + + bool m_clearObsoleteImageAtImageHost = false; }; } diff --git a/src/core/logger.cpp b/src/core/logger.cpp index cd90d79b..6742c788 100644 --- a/src/core/logger.cpp +++ b/src/core/logger.cpp @@ -71,6 +71,7 @@ void Logger::log(QtMsgType p_type, const QMessageLogContext &p_context, const QS case QtFatalMsg: header = QStringLiteral("Fatal:"); + break; } QString fileName = getFileName(p_context.file); @@ -109,6 +110,7 @@ void Logger::log(QtMsgType p_type, const QMessageLogContext &p_context, const QS fprintf(stderr, "%s(%s:%u) %s\n", header.toStdString().c_str(), file, p_context.line, localMsg.constData()); abort(); + break; } fflush(stderr); diff --git a/src/core/mainconfig.cpp b/src/core/mainconfig.cpp index b50b43b6..f21a7509 100644 --- a/src/core/mainconfig.cpp +++ b/src/core/mainconfig.cpp @@ -7,6 +7,7 @@ #include "coreconfig.h" #include "editorconfig.h" #include "widgetconfig.h" +#include "markdowneditorconfig.h" using namespace vnotex; @@ -117,6 +118,5 @@ QString MainConfig::getVersion(const QJsonObject &p_jobj) void MainConfig::doVersionSpecificOverride() { // In a new version, we may want to change one value by force. - m_coreConfig->m_shortcuts[CoreConfig::Shortcut::LocationListDock] = "Ctrl+G, C"; - m_coreConfig->m_shortcuts[CoreConfig::Shortcut::NewWorkspace] = ""; + m_editorConfig->getMarkdownEditorConfig().m_spellCheckEnabled = false; } diff --git a/src/core/markdowneditorconfig.h b/src/core/markdowneditorconfig.h index d9916264..47987b48 100644 --- a/src/core/markdowneditorconfig.h +++ b/src/core/markdowneditorconfig.h @@ -128,6 +128,8 @@ namespace vnotex void setInplacePreviewSources(InplacePreviewSources p_src); private: + friend class MainConfig; + QString sectionNumberModeToString(SectionNumberMode p_mode) const; SectionNumberMode stringToSectionNumberMode(const QString &p_str) const; diff --git a/src/core/notebook/bundlenotebook.cpp b/src/core/notebook/bundlenotebook.cpp index 83b447d3..71b15445 100644 --- a/src/core/notebook/bundlenotebook.cpp +++ b/src/core/notebook/bundlenotebook.cpp @@ -17,6 +17,7 @@ BundleNotebook::BundleNotebook(const NotebookParameters &p_paras, { m_nextNodeId = p_notebookConfig->m_nextNodeId; m_history = p_notebookConfig->m_history; + m_extraConfigs = p_notebookConfig->m_extraConfigs; } BundleNotebookConfigMgr *BundleNotebook::getBundleNotebookConfigMgr() const @@ -81,3 +82,15 @@ void BundleNotebook::clearHistory() updateNotebookConfig(); } + +const QJsonObject &BundleNotebook::getExtraConfigs() const +{ + return m_extraConfigs; +} + +void BundleNotebook::setExtraConfig(const QString &p_key, const QJsonObject &p_obj) +{ + m_extraConfigs[p_key] = p_obj; + + updateNotebookConfig(); +} diff --git a/src/core/notebook/bundlenotebook.h b/src/core/notebook/bundlenotebook.h index 6776b965..cd905b23 100644 --- a/src/core/notebook/bundlenotebook.h +++ b/src/core/notebook/bundlenotebook.h @@ -31,12 +31,17 @@ namespace vnotex void addHistory(const HistoryItem &p_item) Q_DECL_OVERRIDE; void clearHistory() Q_DECL_OVERRIDE; + const QJsonObject &getExtraConfigs() const Q_DECL_OVERRIDE; + void setExtraConfig(const QString &p_key, const QJsonObject &p_obj) Q_DECL_OVERRIDE; + private: BundleNotebookConfigMgr *getBundleNotebookConfigMgr() const; ID m_nextNodeId = 1; QVector m_history; + + QJsonObject m_extraConfigs; }; } // ns vnotex diff --git a/src/core/notebook/notebook.cpp b/src/core/notebook/notebook.cpp index f88436ac..5f27b074 100644 --- a/src/core/notebook/notebook.cpp +++ b/src/core/notebook/notebook.cpp @@ -355,3 +355,9 @@ void Notebook::reloadNodes() m_root.clear(); getRootNode(); } + +QJsonObject Notebook::getExtraConfig(const QString &p_key) const +{ + const auto &configs = getExtraConfigs(); + return configs.value(p_key).toObject(); +} diff --git a/src/core/notebook/notebook.h b/src/core/notebook/notebook.h index 704076c4..0c3c969c 100644 --- a/src/core/notebook/notebook.h +++ b/src/core/notebook/notebook.h @@ -135,6 +135,11 @@ namespace vnotex virtual void addHistory(const HistoryItem &p_item) = 0; virtual void clearHistory() = 0; + // Hold extra 3rd party configs. + virtual const QJsonObject &getExtraConfigs() const = 0; + QJsonObject getExtraConfig(const QString &p_key) const; + virtual void setExtraConfig(const QString &p_key, const QJsonObject &p_obj) = 0; + static const QString c_defaultAttachmentFolder; static const QString c_defaultImageFolder; diff --git a/src/core/notebookconfigmgr/notebookconfig.cpp b/src/core/notebookconfigmgr/notebookconfig.cpp index f80b4470..3466afd2 100644 --- a/src/core/notebookconfigmgr/notebookconfig.cpp +++ b/src/core/notebookconfigmgr/notebookconfig.cpp @@ -60,6 +60,8 @@ QJsonObject NotebookConfig::toJson() const jobj[QStringLiteral("history")] = saveHistory(); + jobj[QStringLiteral("extra_configs")] = m_extraConfigs; + return jobj; } @@ -94,6 +96,8 @@ void NotebookConfig::fromJson(const QJsonObject &p_jobj) } loadHistory(p_jobj); + + m_extraConfigs = p_jobj[QStringLiteral("extra_configs")].toObject(); } QSharedPointer NotebookConfig::fromNotebook(const QString &p_version, @@ -111,6 +115,7 @@ QSharedPointer NotebookConfig::fromNotebook(const QString &p_ver config->m_notebookConfigMgr = p_notebook->getConfigMgr()->getName(); config->m_nextNodeId = p_notebook->getNextNodeId(); config->m_history = p_notebook->getHistory(); + config->m_extraConfigs = p_notebook->getExtraConfigs(); return config; } diff --git a/src/core/notebookconfigmgr/notebookconfig.h b/src/core/notebookconfigmgr/notebookconfig.h index c54bcb6b..86ac65a0 100644 --- a/src/core/notebookconfigmgr/notebookconfig.h +++ b/src/core/notebookconfigmgr/notebookconfig.h @@ -50,6 +50,10 @@ namespace vnotex QVector m_history; + // Hold all the extra configs for other components or 3rd party plugins. + // Use a unique name as the key and the value is a QJsonObject. + QJsonObject m_extraConfigs; + private: QJsonArray saveHistory() const; diff --git a/src/core/notebookmgr.h b/src/core/notebookmgr.h index 46ecf7df..c7b68fb5 100644 --- a/src/core/notebookmgr.h +++ b/src/core/notebookmgr.h @@ -105,13 +105,13 @@ namespace vnotex void addNotebook(const QSharedPointer &p_notebook); - QSharedPointer> m_versionControllerServer; + QScopedPointer> m_versionControllerServer; - QSharedPointer> m_configMgrServer; + QScopedPointer> m_configMgrServer; - QSharedPointer> m_backendServer; + QScopedPointer> m_backendServer; - QSharedPointer> m_notebookServer; + QScopedPointer> m_notebookServer; QVector> m_notebooks; diff --git a/src/data/core/Info.plist b/src/data/core/Info.plist index 9eeb37d1..7410b053 100644 --- a/src/data/core/Info.plist +++ b/src/data/core/Info.plist @@ -21,9 +21,9 @@ CFBundleExecutable vnote CFBundleShortVersionString - 3.0.0 + 3.5.1 CFBundleVersion - 3.0.0.3 + 3.5.1.1 NSHumanReadableCopyright Created by VNoteX CFBundleIconFile diff --git a/src/data/core/core.qrc b/src/data/core/core.qrc index b4168dce..066a15af 100644 --- a/src/data/core/core.qrc +++ b/src/data/core/core.qrc @@ -22,6 +22,7 @@ icons/settings.svg icons/view.svg icons/inplace_preview_editor.svg + icons/image_host_editor.svg icons/settings_menu.svg icons/whatsthis.svg icons/help_menu.svg diff --git a/src/data/core/icons/image_host_editor.svg b/src/data/core/icons/image_host_editor.svg new file mode 100644 index 00000000..eb7f147c --- /dev/null +++ b/src/data/core/icons/image_host_editor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index 44b6aeeb..f18b578f 100644 --- a/src/data/core/vnotex.json +++ b/src/data/core/vnotex.json @@ -333,11 +333,17 @@ "smart_table" : true, "//comment" : "Time interval (milliseconds) to do smart table formatting", "smart_table_interval" : 1000, - "spell_check" : true, + "spell_check" : false, "editor_overridden_font_family" : "", "//comment" : "Sources to enable inplace preview, separated by ;", "//comment" : "imagelink/codeblock/math", "inplace_preview_sources" : "imagelink;codeblock;math" + }, + "image_host" : { + "hosts" : [ + ], + "default_image_host" : "", + "clear_obsolete_image" : false } }, "widget" : { diff --git a/src/data/extra/docs/en/shortcuts.md b/src/data/extra/docs/en/shortcuts.md index a9341201..9315a4d4 100644 --- a/src/data/extra/docs/en/shortcuts.md +++ b/src/data/extra/docs/en/shortcuts.md @@ -1,7 +1,8 @@ # Shortcuts 1. All the keys without special notice are **case insensitive**; 2. On macOS, `Ctrl` corresponds to `Command` except in Vi mode; -3. For a complete shortcuts list, please view the `vnotex.json` configuration file. +3. The key sequence `Ctrl+G, I` means that first press both `Ctrl` and `G` simultaneously, release them, then press `I` and release; +4. For a complete shortcuts list, please view the `vnotex.json` configuration file. ## General - `Ctrl+G E` diff --git a/src/data/extra/docs/en/welcome.md b/src/data/extra/docs/en/welcome.md index f412ccde..de337292 100644 --- a/src/data/extra/docs/en/welcome.md +++ b/src/data/extra/docs/en/welcome.md @@ -5,6 +5,8 @@ For more information, please visit [**VNote's Home Page**](https://vnotex.github ## FAQs * If VNote crashes after update, please delete the `vnotex.json` file under user configuration folder. +* For **Windows** users, if VNote hangs frequently or behaves unexpectedly in interface, please check the **OpenGL** option. [Details here](https://github.com/vnotex/vnote/issues/853). * VNote has a series of powerful shortcuts. Please view the user configuration file `vnotex.json` for a complete list of shortcuts. + * The key sequence `Ctrl+G, I` means that first press both `Ctrl` and `G` simultaneously, release them, then press `I` and release. * Feedbacks are appreciated! Please [post an issue](https://github.com/vnotex/vnote/issues) on GitHub if there is any. diff --git a/src/data/extra/docs/zh_CN/shortcuts.md b/src/data/extra/docs/zh_CN/shortcuts.md index 2c4c6689..d712ea49 100644 --- a/src/data/extra/docs/zh_CN/shortcuts.md +++ b/src/data/extra/docs/zh_CN/shortcuts.md @@ -1,7 +1,8 @@ # 快捷键 1. 以下按键除特别说明外,都不区分大小写; -2. 在 macOS 下,`Ctrl`对应于`Command`,在 Vi 模式下除外; -3. 可以通过查看配置文件 `vnotex.json` 来获取一个完整的快捷键列表。 +2. 在 macOS 下,`Ctrl` 对应于 `Command`,在 Vi 模式下除外; +3. 按键序列 `Ctrl+G, I` 表示先同时按下 `Ctrl` 和 `G`,释放,然后按下 `I` 并释放; +4. 可以通过查看配置文件 `vnotex.json` 来获取一个完整的快捷键列表。 ## 通用 - `Ctrl+G E` diff --git a/src/data/extra/docs/zh_CN/welcome.md b/src/data/extra/docs/zh_CN/welcome.md index dd14bda9..100d3478 100644 --- a/src/data/extra/docs/zh_CN/welcome.md +++ b/src/data/extra/docs/zh_CN/welcome.md @@ -1,10 +1,12 @@ # 欢迎使用 VNote 一个舒适的笔记平台。 -更多信息,请访问 [VNote 主页](https://tamlok.gitee.io/vnote) 或者[由 Gitee 托管的主页](https://tamlok.gitee.io/vnote) 。 +更多信息,请访问 [VNote 主页](https://vnotex.github.io/vnote) 或者[由 Gitee 托管的主页](https://tamlok.gitee.io/vnote) 。 ## 常见问题 * 如果更新后 VNote 崩溃,请删除用户配置文件夹中的 `vnotex.json` 文件。 +* 对于 **Windows** 用户,如果 VNote 经常卡顿或无响应,或者界面异常,请检查 **OpenGL** 选项。[详情](https://github.com/vnotex/vnote/issues/853) 。 * VNote 有着一系列强大的快捷键。请查看用户配置文件 `vnotex.json` 以获取一个完整的快捷键列表。 + * 按键序列 `Ctrl+G, I` 表示先同时按下 `Ctrl` 和 `G`,释放,然后按下 `I` 并释放。 * 使用中有任何问题,欢迎[反馈](https://github.com/vnotex/vnote/issues) 。 diff --git a/src/imagehost/githubimagehost.cpp b/src/imagehost/githubimagehost.cpp new file mode 100644 index 00000000..dc34fb50 --- /dev/null +++ b/src/imagehost/githubimagehost.cpp @@ -0,0 +1,204 @@ +#include "githubimagehost.h" + +#include +#include +#include + +#include +#include + +using namespace vnotex; + +const QString GitHubImageHost::c_apiUrl = "https://api.github.com"; + +GitHubImageHost::GitHubImageHost(QObject *p_parent) + : ImageHost(p_parent) +{ +} + +bool GitHubImageHost::ready() const +{ + return !m_personalAccessToken.isEmpty() && !m_userName.isEmpty() && !m_repoName.isEmpty(); +} + +ImageHost::Type GitHubImageHost::getType() const +{ + return Type::GitHub; +} + +QJsonObject GitHubImageHost::getConfig() const +{ + QJsonObject obj; + obj[QStringLiteral("personal_access_token")] = m_personalAccessToken; + obj[QStringLiteral("user_name")] = m_userName; + obj[QStringLiteral("repository_name")] = m_repoName; + return obj; +} + +void GitHubImageHost::setConfig(const QJsonObject &p_jobj) +{ + parseConfig(p_jobj, m_personalAccessToken, m_userName, m_repoName); + + m_imageUrlPrefix = QString("https://raw.githubusercontent.com/%1/%2/master/").arg(m_userName, m_repoName); +} + +bool GitHubImageHost::testConfig(const QJsonObject &p_jobj, QString &p_msg) +{ + p_msg.clear(); + + QString token, userName, repoName; + parseConfig(p_jobj, token, userName, repoName); + + if (token.isEmpty() || userName.isEmpty() || repoName.isEmpty()) { + p_msg = tr("PersonalAccessToken/UserName/RepositoryName should not be empty."); + return false; + } + + auto reply = getRepoInfo(token, userName, repoName); + p_msg = QString::fromUtf8(reply.m_data); + return reply.m_error == QNetworkReply::NoError; +} + +QPair GitHubImageHost::authorizationHeader(const QString &p_token) +{ + auto token = "token " + p_token; + return qMakePair(QByteArray("Authorization"), token.toUtf8()); +} + +QPair GitHubImageHost::acceptHeader() +{ + return qMakePair(QByteArray("Accept"), QByteArray("application/vnd.github.v3+json")); +} + +vte::NetworkAccess::RawHeaderPairs GitHubImageHost::prepareCommonHeaders(const QString &p_token) +{ + vte::NetworkAccess::RawHeaderPairs rawHeader; + rawHeader.push_back(authorizationHeader(p_token)); + rawHeader.push_back(acceptHeader()); + return rawHeader; +} + +vte::NetworkReply GitHubImageHost::getRepoInfo(const QString &p_token, const QString &p_userName, const QString &p_repoName) const +{ + auto rawHeader = prepareCommonHeaders(p_token); + const auto urlStr = QString("%1/repos/%2/%3").arg(c_apiUrl, p_userName, p_repoName); + auto reply = vte::NetworkAccess::request(QUrl(urlStr), rawHeader); + return reply; +} + +void GitHubImageHost::parseConfig(const QJsonObject &p_jobj, + QString &p_token, + QString &p_userName, + QString &p_repoName) +{ + p_token = p_jobj[QStringLiteral("personal_access_token")].toString(); + p_userName = p_jobj[QStringLiteral("user_name")].toString(); + p_repoName = p_jobj[QStringLiteral("repository_name")].toString(); +} + +QString GitHubImageHost::create(const QByteArray &p_data, const QString &p_path, QString &p_msg) +{ + QString destUrl; + + if (p_path.isEmpty()) { + p_msg = tr("Failed to create image with empty path."); + return destUrl; + } + + destUrl = createResource(p_data, p_path, p_msg); + return destUrl; +} + +QString GitHubImageHost::createResource(const QByteArray &p_content, const QString &p_path, QString &p_msg) const +{ + Q_ASSERT(!p_path.isEmpty()); + + if (!ready()) { + p_msg = tr("Invalid GitHub image host configuration."); + return QString(); + } + + auto rawHeader = prepareCommonHeaders(m_personalAccessToken); + const auto urlStr = QString("%1/repos/%2/%3/contents/%4").arg(c_apiUrl, m_userName, m_repoName, p_path); + + // Check if @p_path already exists. + auto reply = vte::NetworkAccess::request(QUrl(urlStr), rawHeader); + if (reply.m_error == QNetworkReply::NoError) { + p_msg = tr("The resource already exists at the image host (%1).").arg(p_path); + return QString(); + } else if (reply.m_error != QNetworkReply::ContentNotFoundError) { + p_msg = tr("Failed to query the resource at the image host (%1) (%2) (%3).").arg(urlStr, reply.errorStr(), reply.m_data); + return QString(); + } + + // Create the content. + QJsonObject requestDataObj; + requestDataObj[QStringLiteral("message")] = QString("VX_ADD: %1").arg(p_path); + requestDataObj[QStringLiteral("content")] = QString::fromUtf8(p_content.toBase64()); + auto requestData = Utils::toJsonString(requestDataObj); + reply = vte::NetworkAccess::put(QUrl(urlStr), rawHeader, requestData); + if (reply.m_error != QNetworkReply::NoError) { + p_msg = tr("Failed to create resource at the image host (%1) (%2) (%3).").arg(urlStr, reply.errorStr(), reply.m_data); + return QString(); + } else { + auto replyObj = Utils::fromJsonString(reply.m_data); + Q_ASSERT(!replyObj.isEmpty()); + auto targetUrl = replyObj[QStringLiteral("content")].toObject().value(QStringLiteral("download_url")).toString(); + if (targetUrl.isEmpty()) { + p_msg = tr("Failed to create resource at the image host (%1) (%2) (%3).").arg(urlStr, reply.errorStr(), reply.m_data); + } else { + qDebug() << "created resource" << targetUrl; + } + return targetUrl; + } +} + +bool GitHubImageHost::ownsUrl(const QString &p_url) const +{ + return p_url.startsWith(m_imageUrlPrefix); +} + +bool GitHubImageHost::remove(const QString &p_url, QString &p_msg) +{ + Q_ASSERT(ownsUrl(p_url)); + + if (!ready()) { + p_msg = tr("Invalid GitHub image host configuration."); + return false; + } + + const QString resourcePath = WebUtils::purifyUrl(p_url.mid(m_imageUrlPrefix.size())); + + auto rawHeader = prepareCommonHeaders(m_personalAccessToken); + const auto urlStr = QString("%1/repos/%2/%3/contents/%4").arg(c_apiUrl, m_userName, m_repoName, resourcePath); + + // Get the SHA of the resource. + auto reply = vte::NetworkAccess::request(QUrl(urlStr), rawHeader); + if (reply.m_error != QNetworkReply::NoError) { + p_msg = tr("Failed to fetch information about the resource (%1).").arg(resourcePath); + return false; + } + + auto replyObj = Utils::fromJsonString(reply.m_data); + Q_ASSERT(!replyObj.isEmpty()); + const auto sha = replyObj[QStringLiteral("sha")].toString(); + if (sha.isEmpty()) { + p_msg = tr("Failed to fetch SHA about the resource (%1) (%2).").arg(resourcePath, QString::fromUtf8(reply.m_data)); + return false; + } + + // Delete. + QJsonObject requestDataObj; + requestDataObj[QStringLiteral("message")] = QString("VX_DEL: %1").arg(resourcePath); + requestDataObj[QStringLiteral("sha")] = sha; + auto requestData = Utils::toJsonString(requestDataObj); + reply = vte::NetworkAccess::deleteResource(QUrl(urlStr), rawHeader, requestData); + if (reply.m_error != QNetworkReply::NoError) { + p_msg = tr("Failed to delete resource (%1) (%2).").arg(resourcePath, QString::fromUtf8(reply.m_data)); + return false; + } + + qDebug() << "deleted resource" << resourcePath; + + return true; +} diff --git a/src/imagehost/githubimagehost.h b/src/imagehost/githubimagehost.h new file mode 100644 index 00000000..ef082d5c --- /dev/null +++ b/src/imagehost/githubimagehost.h @@ -0,0 +1,61 @@ +#ifndef GITHUBIMAGEHOST_H +#define GITHUBIMAGEHOST_H + +#include "imagehost.h" + +#include + +namespace vnotex +{ + class GitHubImageHost : public ImageHost + { + Q_OBJECT + public: + explicit GitHubImageHost(QObject *p_parent); + + bool ready() const Q_DECL_OVERRIDE; + + Type getType() const Q_DECL_OVERRIDE; + + QJsonObject getConfig() const Q_DECL_OVERRIDE; + + void setConfig(const QJsonObject &p_jobj) Q_DECL_OVERRIDE; + + bool testConfig(const QJsonObject &p_jobj, QString &p_msg) Q_DECL_OVERRIDE; + + QString create(const QByteArray &p_data, const QString &p_path, QString &p_msg) Q_DECL_OVERRIDE; + + bool remove(const QString &p_url, QString &p_msg) Q_DECL_OVERRIDE; + + bool ownsUrl(const QString &p_url) const Q_DECL_OVERRIDE; + + private: + // Used to test. + vte::NetworkReply getRepoInfo(const QString &p_token, const QString &p_userName, const QString &p_repoName) const; + + QString createResource(const QByteArray &p_content, const QString &p_path, QString &p_msg) const; + + static void parseConfig(const QJsonObject &p_jobj, + QString &p_token, + QString &p_userName, + QString &p_repoName); + + static QPair authorizationHeader(const QString &p_token); + + static QPair acceptHeader(); + + static vte::NetworkAccess::RawHeaderPairs prepareCommonHeaders(const QString &p_token); + + QString m_personalAccessToken; + + QString m_userName; + + QString m_repoName; + + QString m_imageUrlPrefix; + + static const QString c_apiUrl; + }; +} + +#endif // GITHUBIMAGEHOST_H diff --git a/src/imagehost/imagehost.cpp b/src/imagehost/imagehost.cpp new file mode 100644 index 00000000..11bbdde0 --- /dev/null +++ b/src/imagehost/imagehost.cpp @@ -0,0 +1,30 @@ +#include "imagehost.h" + +using namespace vnotex; + +ImageHost::ImageHost(QObject *p_parent) + : QObject(p_parent) +{ +} + +const QString &ImageHost::getName() const +{ + return m_name; +} + +void ImageHost::setName(const QString &p_name) +{ + m_name = p_name; +} + +QString ImageHost::typeString(ImageHost::Type p_type) +{ + switch (p_type) { + case Type::GitHub: + return tr("GitHub"); + + default: + Q_ASSERT(false); + return QString("Unknown"); + } +} diff --git a/src/imagehost/imagehost.h b/src/imagehost/imagehost.h new file mode 100644 index 00000000..c5d6dc7b --- /dev/null +++ b/src/imagehost/imagehost.h @@ -0,0 +1,57 @@ +#ifndef IMAGEHOST_H +#define IMAGEHOST_H + +#include +#include + +#include + +class QByteArray; + +namespace vnotex +{ + // Abstract class for image host. + class ImageHost : public QObject + { + Q_OBJECT + public: + enum Type + { + GitHub = 0, + MaxHost + }; + + virtual ~ImageHost() = default; + + const QString &getName() const; + void setName(const QString &p_name); + + virtual Type getType() const = 0; + + // Whether it is ready to serve. + virtual bool ready() const = 0; + + virtual QJsonObject getConfig() const = 0; + virtual void setConfig(const QJsonObject &p_jobj) = 0; + + virtual bool testConfig(const QJsonObject &p_jobj, QString &p_msg) = 0; + + // Upload @p_data to the host at path @p_path. Return the target Url string on success. + virtual QString create(const QByteArray &p_data, const QString &p_path, QString &p_msg) = 0; + + virtual bool remove(const QString &p_url, QString &p_msg) = 0; + + // Test if @p_url is owned by this image host. + virtual bool ownsUrl(const QString &p_url) const = 0; + + static QString typeString(Type p_type); + + protected: + explicit ImageHost(QObject *p_parent = nullptr); + + // Name to identify one image host. One type of image host may have multiple instances. + QString m_name; + }; +} + +#endif // IMAGEHOST_H diff --git a/src/imagehost/imagehost.pri b/src/imagehost/imagehost.pri new file mode 100644 index 00000000..46dc2611 --- /dev/null +++ b/src/imagehost/imagehost.pri @@ -0,0 +1,14 @@ +QT += widgets + +HEADERS += \ + $$PWD/githubimagehost.h \ + $$PWD/imagehost.h \ + $$PWD/imagehostmgr.h \ + $$PWD/imagehostutils.h + +SOURCES += \ + $$PWD/githubimagehost.cpp \ + $$PWD/imagehost.cpp \ + $$PWD/imagehostmgr.cpp \ + $$PWD/imagehostutils.cpp + diff --git a/src/imagehost/imagehostmgr.cpp b/src/imagehost/imagehostmgr.cpp new file mode 100644 index 00000000..c6b09248 --- /dev/null +++ b/src/imagehost/imagehostmgr.cpp @@ -0,0 +1,198 @@ +#include "imagehostmgr.h" + +#include + +#include +#include + +#include "githubimagehost.h" + +using namespace vnotex; + +ImageHostMgr::ImageHostMgr() +{ + loadImageHosts(); +} + +void ImageHostMgr::loadImageHosts() +{ + const auto &editorConfig = ConfigMgr::getInst().getEditorConfig(); + for (const auto &host : editorConfig.getImageHosts()) { + if (host.m_type >= ImageHost::Type::MaxHost) { + qWarning() << "skipped unknown type image host" << host.m_type << host.m_name; + continue; + } + + if (find(host.m_name)) { + qWarning() << "sikpped image host with name conflict" << host.m_type << host.m_name; + continue; + } + + auto imageHost = createImageHost(static_cast(host.m_type), this); + if (!imageHost) { + qWarning() << "failed to create image host" << host.m_type << host.m_name; + continue; + } + + imageHost->setName(host.m_name); + imageHost->setConfig(host.m_config); + add(imageHost); + } + + m_defaultHost = find(editorConfig.getDefaultImageHost()); + + qDebug() << "loaded" << m_hosts.size() << "image hosts"; +} + +void ImageHostMgr::saveImageHosts() +{ + QVector items; + items.resize(m_hosts.size()); + for (int i = 0; i < m_hosts.size(); ++i) { + items[i].m_type = static_cast(m_hosts[i]->getType()); + items[i].m_name = m_hosts[i]->getName(); + items[i].m_config = m_hosts[i]->getConfig(); + } + + auto &editorConfig = ConfigMgr::getInst().getEditorConfig(); + editorConfig.setImageHosts(items); +} + +ImageHost *ImageHostMgr::createImageHost(ImageHost::Type p_type, QObject *p_parent) +{ + switch (p_type) { + case ImageHost::Type::GitHub: + return new GitHubImageHost(p_parent); + + default: + return nullptr; + } +} + +void ImageHostMgr::add(ImageHost *p_host) +{ + p_host->setParent(this); + m_hosts.append(p_host); +} + +ImageHost *ImageHostMgr::find(const QString &p_name) const +{ + if (p_name.isEmpty()) { + return nullptr; + } + + for (auto host : m_hosts) { + if (host->getName() == p_name) { + return host; + } + } + + return nullptr; +} + +ImageHost *ImageHostMgr::newImageHost(ImageHost::Type p_type, const QString &p_name) +{ + if (find(p_name)) { + qWarning() << "failed to new image host with existing name" << p_name; + return nullptr; + } + + auto host = createImageHost(p_type, this); + if (!host) { + return nullptr; + } + + host->setName(p_name); + add(host); + + saveImageHosts(); + + emit imageHostChanged(); + + return host; +} + +const QVector &ImageHostMgr::getImageHosts() const +{ + return m_hosts; +} + +void ImageHostMgr::removeImageHost(ImageHost *p_host) +{ + m_hosts.removeOne(p_host); + + saveImageHosts(); + + if (p_host == m_defaultHost) { + m_defaultHost = nullptr; + saveDefaultImageHost(); + } + + emit imageHostChanged(); +} + +bool ImageHostMgr::renameImageHost(ImageHost *p_host, const QString &p_newName) +{ + if (p_newName.isEmpty()) { + return false; + } + + if (p_newName == p_host->getName()) { + return true; + } + + if (find(p_newName)) { + return false; + } + + p_host->setName(p_newName); + + saveImageHosts(); + + if (m_defaultHost == p_host) { + saveDefaultImageHost(); + } + + emit imageHostChanged(); + return true; +} + +ImageHost *ImageHostMgr::getDefaultImageHost() const +{ + return m_defaultHost; +} + +void ImageHostMgr::setDefaultImageHost(const QString &p_name) +{ + auto host = find(p_name); + if (m_defaultHost == host) { + return; + } + + m_defaultHost = host; + + saveDefaultImageHost(); + + emit imageHostChanged(); +} + +void ImageHostMgr::saveDefaultImageHost() +{ + auto &editorConfig = ConfigMgr::getInst().getEditorConfig(); + editorConfig.setDefaultImageHost(m_defaultHost ? m_defaultHost->getName() : QString()); +} + +ImageHost *ImageHostMgr::findByImageUrl(const QString &p_url) const +{ + if (p_url.isEmpty()) { + return nullptr; + } + + for (auto host : m_hosts) { + if (host->ownsUrl(p_url)) { + return host; + } + } + + return nullptr; +} diff --git a/src/imagehost/imagehostmgr.h b/src/imagehost/imagehostmgr.h new file mode 100644 index 00000000..0a82c620 --- /dev/null +++ b/src/imagehost/imagehostmgr.h @@ -0,0 +1,61 @@ +#ifndef IMAGEHOSTMGR_H +#define IMAGEHOSTMGR_H + +#include +#include + +#include + +#include "imagehost.h" + +namespace vnotex +{ + class ImageHostMgr : public QObject, private Noncopyable + { + Q_OBJECT + public: + static ImageHostMgr &getInst() + { + static ImageHostMgr inst; + return inst; + } + + ImageHost *find(const QString &p_name) const; + + ImageHost *findByImageUrl(const QString &p_url) const; + + ImageHost *newImageHost(ImageHost::Type p_type, const QString &p_name); + + const QVector &getImageHosts() const; + + void removeImageHost(ImageHost *p_host); + + bool renameImageHost(ImageHost *p_host, const QString &p_newName); + + void saveImageHosts(); + + ImageHost *getDefaultImageHost() const; + + void setDefaultImageHost(const QString &p_name); + + signals: + void imageHostChanged(); + + private: + ImageHostMgr(); + + void loadImageHosts(); + + void add(ImageHost *p_host); + + void saveDefaultImageHost(); + + static ImageHost *createImageHost(ImageHost::Type p_type, QObject *p_parent); + + QVector m_hosts; + + ImageHost *m_defaultHost = nullptr; + }; +} + +#endif // IMAGEHOSTMGR_H diff --git a/src/imagehost/imagehostutils.cpp b/src/imagehost/imagehostutils.cpp new file mode 100644 index 00000000..93b04146 --- /dev/null +++ b/src/imagehost/imagehostutils.cpp @@ -0,0 +1,29 @@ +#include "imagehostutils.h" + +#include +#include +#include +#include +#include + +using namespace vnotex; + +QString ImageHostUtils::generateRelativePath(const Buffer *p_buffer) +{ + QString relativePath; + + // To avoid leaking any private information, for external files, we won't add path to it. + if (auto node = p_buffer->getNode()) { + auto notebook = node->getNotebook(); + auto name = notebook->getName(); + if (name.isEmpty() || !PathUtils::isLegalFileName(name)) { + name = QStringLiteral("vx_notebooks"); + } + + relativePath = name; + relativePath += "/" + notebook->getBackend()->getRelativePath(p_buffer->getPath()); + relativePath = relativePath.toLower(); + } + + return relativePath; +} diff --git a/src/imagehost/imagehostutils.h b/src/imagehost/imagehostutils.h new file mode 100644 index 00000000..23e7c3cf --- /dev/null +++ b/src/imagehost/imagehostutils.h @@ -0,0 +1,24 @@ +#ifndef IMAGEHOSTUTILS_H +#define IMAGEHOSTUTILS_H + +#include + +class QImage; +class QWidget; + +namespace vnotex +{ + class Buffer; + + class ImageHostUtils + { + public: + ImageHostUtils() = delete; + + // According to @p_buffer, generate the relative path on image host for images. + // Return the relative path folder. + static QString generateRelativePath(const Buffer *p_buffer); + }; +} + +#endif // IMAGEHOSTUTILS_H diff --git a/src/src.pro b/src/src.pro index c82f4ded..28b4ade5 100644 --- a/src/src.pro +++ b/src/src.pro @@ -51,6 +51,8 @@ include($$PWD/search/search.pri) include($$PWD/snippet/snippet.pri) +include($$PWD/imagehost/imagehost.pri) + include($$PWD/core/core.pri) include($$PWD/widgets/widgets.pri) diff --git a/src/utils/pathutils.cpp b/src/utils/pathutils.cpp index d163338f..d735e451 100644 --- a/src/utils/pathutils.cpp +++ b/src/utils/pathutils.cpp @@ -83,6 +83,8 @@ QString PathUtils::fileNameCheap(const QString &p_path) QString PathUtils::normalizePath(const QString &p_path) { + Q_ASSERT(isLocalFile(p_path)); + auto absPath = QDir::cleanPath(QDir(p_path).absolutePath()); #if defined(Q_OS_WIN) return absPath.toLower(); @@ -234,3 +236,17 @@ bool PathUtils::isDir(const QString &p_path) { return QFileInfo(p_path).isDir(); } + +bool PathUtils::isLocalFile(const QString &p_path) +{ + if (p_path.isEmpty()) { + return false; + } + + QRegularExpression regExp("^(?:ftp|http|https)://"); + if (regExp.match(p_path).hasMatch()) { + return false; + } + + return true; +} diff --git a/src/utils/pathutils.h b/src/utils/pathutils.h index 93a1794e..2817453e 100644 --- a/src/utils/pathutils.h +++ b/src/utils/pathutils.h @@ -73,6 +73,8 @@ namespace vnotex static bool isImageUrl(const QString &p_url); + static bool isLocalFile(const QString &p_path); + // Regular expression string for file/folder name. // Forbidden chars: \/:*?"<>| and whitespaces except spaces. static const QString c_fileNameRegularExpression; diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 61466ce7..ce7eadeb 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include @@ -126,3 +128,14 @@ QString Utils::intToString(int p_val, int p_width) } return str; } + +QByteArray Utils::toJsonString(const QJsonObject &p_obj) +{ + QJsonDocument doc(p_obj); + return doc.toJson(QJsonDocument::Compact); +} + +QJsonObject Utils::fromJsonString(const QByteArray &p_data) +{ + return QJsonDocument::fromJson(p_data).object(); +} diff --git a/src/utils/utils.h b/src/utils/utils.h index bf2f1822..76b301f8 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -20,6 +20,7 @@ #endif class QWidget; +class QJsonObject; namespace vnotex { @@ -52,6 +53,10 @@ namespace vnotex static QString boolToString(bool p_val); static QString intToString(int p_val, int p_width = 0); + + static QByteArray toJsonString(const QJsonObject &p_obj); + + static QJsonObject fromJsonString(const QByteArray &p_data); }; } // ns vnotex diff --git a/src/utils/webutils.cpp b/src/utils/webutils.cpp index 866b1812..7069936b 100644 --- a/src/utils/webutils.cpp +++ b/src/utils/webutils.cpp @@ -36,7 +36,7 @@ QString WebUtils::toDataUri(const QUrl &p_url, bool p_keepTitle) QByteArray data; if (p_url.scheme() == "https" || p_url.scheme() == "http") { // Download it. - data = vte::Downloader::download(p_url); + data = vte::NetworkAccess::request(p_url).m_data; } else if (finfo.exists()) { data = FileUtils::readFile(filePath); } @@ -86,7 +86,7 @@ QString WebUtils::copyResource(const QUrl &p_url, const QString &p_folder) try { if (p_url.scheme() == "https" || p_url.scheme() == "http") { // Download it. - auto data = vte::Downloader::download(p_url); + auto data = vte::NetworkAccess::request(p_url).m_data; if (!data.isEmpty()) { FileUtils::writeFile(targetFile, data); } diff --git a/src/widgets/dialogs/imageinsertdialog.cpp b/src/widgets/dialogs/imageinsertdialog.cpp index 2d24781c..a03ef4c7 100644 --- a/src/widgets/dialogs/imageinsertdialog.cpp +++ b/src/widgets/dialogs/imageinsertdialog.cpp @@ -187,12 +187,12 @@ void ImageInsertDialog::checkImagePathInput() m_source = Source::ImageData; if (!m_downloader) { - m_downloader = new vte::Downloader(this); - connect(m_downloader, &vte::Downloader::downloadFinished, + m_downloader = new vte::NetworkAccess(this); + connect(m_downloader, &vte::NetworkAccess::requestFinished, this, &ImageInsertDialog::handleImageDownloaded); } - m_downloader->downloadAsync(url); + m_downloader->requestAsync(url); } m_imageTitleEdit->setText(QFileInfo(text).baseName()); @@ -300,17 +300,17 @@ int ImageInsertDialog::getScaledWidth() const return val == m_image.width() ? 0 : val; } -void ImageInsertDialog::handleImageDownloaded(const QByteArray &p_data, const QString &p_url) +void ImageInsertDialog::handleImageDownloaded(const vte::NetworkReply &p_data, const QString &p_url) { - setImage(QImage::fromData(p_data)); + setImage(QImage::fromData(p_data.m_data)); // Save it to a temp file to avoid potential data loss via QImage. bool savedToFile = false; - if (!p_data.isEmpty()) { + if (!p_data.m_data.isEmpty()) { auto format = QFileInfo(PathUtils::removeUrlParameters(p_url)).suffix(); m_tempFile.reset(FileUtils::createTemporaryFile(format)); if (m_tempFile->open()) { - savedToFile = -1 != m_tempFile->write(p_data); + savedToFile = -1 != m_tempFile->write(p_data.m_data); m_tempFile->close(); } } diff --git a/src/widgets/dialogs/imageinsertdialog.h b/src/widgets/dialogs/imageinsertdialog.h index d1cda918..7fc57ae5 100644 --- a/src/widgets/dialogs/imageinsertdialog.h +++ b/src/widgets/dialogs/imageinsertdialog.h @@ -17,7 +17,8 @@ class QScrollArea; namespace vte { - class Downloader; + class NetworkAccess; + struct NetworkReply; } namespace vnotex @@ -65,7 +66,7 @@ namespace vnotex void browseFile(); - void handleImageDownloaded(const QByteArray &p_data, const QString &p_url); + void handleImageDownloaded(const vte::NetworkReply &p_data, const QString &p_url); void handleScaleSliderValueChanged(int p_val); @@ -102,7 +103,7 @@ namespace vnotex QImage m_image; // Managed by QObject. - vte::Downloader *m_downloader = nullptr; + vte::NetworkAccess *m_downloader = nullptr; // Managed by QObject. QTimer *m_imagePathCheckTimer = nullptr; diff --git a/src/widgets/dialogs/newnotedialog.cpp b/src/widgets/dialogs/newnotedialog.cpp index ad151c05..800e0737 100644 --- a/src/widgets/dialogs/newnotedialog.cpp +++ b/src/widgets/dialogs/newnotedialog.cpp @@ -150,6 +150,16 @@ void NewNoteDialog::initDefaultValues(const Node *p_node) lineEdit->setText(defaultName); WidgetUtils::selectBaseName(lineEdit); } + + if (!s_lastTemplate.isEmpty()) { + // Restore. + int idx = m_templateComboBox->findData(s_lastTemplate); + if (idx != -1) { + m_templateComboBox->setCurrentIndex(idx); + } else { + s_lastTemplate.clear(); + } + } } void NewNoteDialog::setupTemplateComboBox(QWidget *p_parent) @@ -166,41 +176,10 @@ void NewNoteDialog::setupTemplateComboBox(QWidget *p_parent) m_templateComboBox->setItemData(idx++, temp, Qt::ToolTipRole); } - if (!s_lastTemplate.isEmpty()) { - // Restore. - int idx = m_templateComboBox->findData(s_lastTemplate); - if (idx != -1) { - m_templateComboBox->setCurrentIndex(idx); - } else { - s_lastTemplate.clear(); - } - } + m_templateComboBox->setCurrentIndex(0); connect(m_templateComboBox, QOverload::of(&QComboBox::currentIndexChanged), - this, [this]() { - m_templateContent.clear(); - m_templateTextEdit->clear(); - - auto temp = m_templateComboBox->currentData().toString(); - if (temp.isEmpty()) { - m_templateTextEdit->hide(); - return; - } - - const auto filePath = TemplateMgr::getInst().getTemplateFilePath(temp); - try { - m_templateContent = FileUtils::readTextFile(filePath); - m_templateTextEdit->setPlainText(m_templateContent); - m_templateTextEdit->show(); - } catch (Exception &p_e) { - m_templateTextEdit->hide(); - - QString msg = tr("Failed to load template (%1) (%2).") - .arg(filePath, p_e.what()); - qCritical() << msg; - setInformationText(msg, ScrollDialog::InformationLevel::Error); - } - }); + this, &NewNoteDialog::updateCurrentTemplate); } QString NewNoteDialog::getTemplateContent() const @@ -211,3 +190,29 @@ QString NewNoteDialog::getTemplateContent() const cursorOffset, SnippetMgr::generateOverrides(m_infoWidget->getName())); } + +void NewNoteDialog::updateCurrentTemplate() +{ + m_templateContent.clear(); + m_templateTextEdit->clear(); + + auto temp = m_templateComboBox->currentData().toString(); + if (temp.isEmpty()) { + m_templateTextEdit->hide(); + return; + } + + const auto filePath = TemplateMgr::getInst().getTemplateFilePath(temp); + try { + m_templateContent = FileUtils::readTextFile(filePath); + m_templateTextEdit->setPlainText(m_templateContent); + m_templateTextEdit->show(); + } catch (Exception &p_e) { + m_templateTextEdit->hide(); + + QString msg = tr("Failed to load template (%1) (%2).") + .arg(filePath, p_e.what()); + qCritical() << msg; + setInformationText(msg, ScrollDialog::InformationLevel::Error); + } +} diff --git a/src/widgets/dialogs/newnotedialog.h b/src/widgets/dialogs/newnotedialog.h index b36d3770..8d6f7b69 100644 --- a/src/widgets/dialogs/newnotedialog.h +++ b/src/widgets/dialogs/newnotedialog.h @@ -41,6 +41,8 @@ namespace vnotex QString getTemplateContent() const; + void updateCurrentTemplate(); + NodeInfoWidget *m_infoWidget = nullptr; QComboBox *m_templateComboBox = nullptr; diff --git a/src/widgets/dialogs/nodeinfowidget.cpp b/src/widgets/dialogs/nodeinfowidget.cpp index 96ce462e..2dd74b18 100644 --- a/src/widgets/dialogs/nodeinfowidget.cpp +++ b/src/widgets/dialogs/nodeinfowidget.cpp @@ -71,9 +71,6 @@ void NodeInfoWidget::setupUI(const Node *p_parentNode, Node::Flags p_newNodeFlag void NodeInfoWidget::setupNameLineEdit(QWidget *p_parent) { m_nameLineEdit = WidgetsFactory::createLineEditWithSnippet(p_parent); - auto validator = new QRegularExpressionValidator(QRegularExpression(PathUtils::c_fileNameRegularExpression), - m_nameLineEdit); - m_nameLineEdit->setValidator(validator); connect(m_nameLineEdit, &QLineEdit::textEdited, this, [this]() { // Choose the correct file type. diff --git a/src/widgets/dialogs/scrolldialog.cpp b/src/widgets/dialogs/scrolldialog.cpp index c0b27622..f4921fd4 100644 --- a/src/widgets/dialogs/scrolldialog.cpp +++ b/src/widgets/dialogs/scrolldialog.cpp @@ -40,7 +40,7 @@ void ScrollDialog::addBottomWidget(QWidget *p_widget) void ScrollDialog::showEvent(QShowEvent *p_event) { - QDialog::showEvent(p_event); + Dialog::showEvent(p_event); resizeToHideScrollBarLater(false, true); } diff --git a/src/widgets/dialogs/settings/appearancepage.cpp b/src/widgets/dialogs/settings/appearancepage.cpp index 16662dc4..55c57878 100644 --- a/src/widgets/dialogs/settings/appearancepage.cpp +++ b/src/widgets/dialogs/settings/appearancepage.cpp @@ -94,7 +94,7 @@ void AppearancePage::loadInternal() } } -void AppearancePage::saveInternal() +bool AppearancePage::saveInternal() { auto &sessionConfig = ConfigMgr::getInst().getSessionConfig(); auto &coreConfig = ConfigMgr::getInst().getCoreConfig(); @@ -115,6 +115,8 @@ void AppearancePage::saveInternal() } widgetConfig.setMainWindowKeepDocksExpandingContentArea(docks); } + + return true; } QString AppearancePage::title() const diff --git a/src/widgets/dialogs/settings/appearancepage.h b/src/widgets/dialogs/settings/appearancepage.h index a0e61a75..2c04c923 100644 --- a/src/widgets/dialogs/settings/appearancepage.h +++ b/src/widgets/dialogs/settings/appearancepage.h @@ -22,7 +22,7 @@ namespace vnotex protected: void loadInternal() Q_DECL_OVERRIDE; - void saveInternal() Q_DECL_OVERRIDE; + bool saveInternal() Q_DECL_OVERRIDE; private: void setupUI(); diff --git a/src/widgets/dialogs/settings/editorpage.cpp b/src/widgets/dialogs/settings/editorpage.cpp index c49385ac..53e60563 100644 --- a/src/widgets/dialogs/settings/editorpage.cpp +++ b/src/widgets/dialogs/settings/editorpage.cpp @@ -113,7 +113,7 @@ void EditorPage::loadInternal() } } -void EditorPage::saveInternal() +bool EditorPage::saveInternal() { auto &editorConfig = ConfigMgr::getInst().getEditorConfig(); @@ -129,6 +129,8 @@ void EditorPage::saveInternal() } notifyEditorConfigChange(); + + return true; } QString EditorPage::title() const diff --git a/src/widgets/dialogs/settings/editorpage.h b/src/widgets/dialogs/settings/editorpage.h index a41c3bb9..b017a44f 100644 --- a/src/widgets/dialogs/settings/editorpage.h +++ b/src/widgets/dialogs/settings/editorpage.h @@ -22,7 +22,7 @@ namespace vnotex protected: void loadInternal() Q_DECL_OVERRIDE; - void saveInternal() Q_DECL_OVERRIDE; + bool saveInternal() Q_DECL_OVERRIDE; private: void setupUI(); diff --git a/src/widgets/dialogs/settings/generalpage.cpp b/src/widgets/dialogs/settings/generalpage.cpp index 6d91d140..88d7d696 100644 --- a/src/widgets/dialogs/settings/generalpage.cpp +++ b/src/widgets/dialogs/settings/generalpage.cpp @@ -106,7 +106,7 @@ void GeneralPage::loadInternal() m_recoverLastSessionCheckBox->setChecked(coreConfig.isRecoverLastSessionOnStartEnabled()); } -void GeneralPage::saveInternal() +bool GeneralPage::saveInternal() { auto &coreConfig = ConfigMgr::getInst().getCoreConfig(); auto &sessionConfig = ConfigMgr::getInst().getSessionConfig(); @@ -127,6 +127,8 @@ void GeneralPage::saveInternal() } coreConfig.setRecoverLastSessionOnStartEnabled(m_recoverLastSessionCheckBox->isChecked()); + + return true; } QString GeneralPage::title() const diff --git a/src/widgets/dialogs/settings/generalpage.h b/src/widgets/dialogs/settings/generalpage.h index 3a6264e7..9990b74c 100644 --- a/src/widgets/dialogs/settings/generalpage.h +++ b/src/widgets/dialogs/settings/generalpage.h @@ -19,7 +19,7 @@ namespace vnotex protected: void loadInternal() Q_DECL_OVERRIDE; - void saveInternal() Q_DECL_OVERRIDE; + bool saveInternal() Q_DECL_OVERRIDE; private: void setupUI(); diff --git a/src/widgets/dialogs/settings/imagehostpage.cpp b/src/widgets/dialogs/settings/imagehostpage.cpp new file mode 100644 index 00000000..f8ced34e --- /dev/null +++ b/src/widgets/dialogs/settings/imagehostpage.cpp @@ -0,0 +1,295 @@ +#include "imagehostpage.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "editorpage.h" +#include "newimagehostdialog.h" + +using namespace vnotex; + +ImageHostPage::ImageHostPage(QWidget *p_parent) + : SettingsPage(p_parent) +{ + setupUI(); +} + +void ImageHostPage::setupUI() +{ + m_mainLayout = new QVBoxLayout(this); + + // New Image Host. + { + auto layout = new QHBoxLayout(); + m_mainLayout->addLayout(layout); + + auto newBtn = new QPushButton(tr("New Image Host"), this); + connect(newBtn, &QPushButton::clicked, + this, &ImageHostPage::newImageHost); + layout->addWidget(newBtn); + layout->addStretch(); + } + + auto box = setupGeneralBox(this); + m_mainLayout->addWidget(box); +} + +QGroupBox *ImageHostPage::setupGeneralBox(QWidget *p_parent) +{ + auto box = new QGroupBox(tr("General"), p_parent); + auto layout = WidgetsFactory::createFormLayout(box); + + { + m_defaultImageHostComboBox = WidgetsFactory::createComboBox(box); + + // Add items in loadInternal(). + + const QString label(tr("Default image host:")); + layout->addRow(label, m_defaultImageHostComboBox); + addSearchItem(label, m_defaultImageHostComboBox); + connect(m_defaultImageHostComboBox, QOverload::of(&QComboBox::currentIndexChanged), + this, &ImageHostPage::pageIsChanged); + } + + { + const QString label(tr("Clear obsolete image")); + m_clearObsoleteImageCheckBox = WidgetsFactory::createCheckBox(label, box); + m_clearObsoleteImageCheckBox->setToolTip(tr("Clear unused images at image host (based on current file only)")); + layout->addRow(m_clearObsoleteImageCheckBox); + addSearchItem(label, m_clearObsoleteImageCheckBox->toolTip(), m_clearObsoleteImageCheckBox); + connect(m_clearObsoleteImageCheckBox, &QCheckBox::stateChanged, + this, &ImageHostPage::pageIsChanged); + } + + return box; +} + +void ImageHostPage::addWidgetToLayout(QWidget *p_widget) +{ + m_mainLayout->addWidget(p_widget); +} + +void ImageHostPage::loadInternal() +{ + const auto &hosts = ImageHostMgr::getInst().getImageHosts(); + const auto &editorConfig = ConfigMgr::getInst().getEditorConfig(); + + { + m_defaultImageHostComboBox->clear(); + + m_defaultImageHostComboBox->addItem(tr("Local")); + for (const auto &host : hosts) { + m_defaultImageHostComboBox->addItem(host->getName(), host->getName()); + } + + auto defaultHost = ImageHostMgr::getInst().getDefaultImageHost(); + if (defaultHost) { + int idx = m_defaultImageHostComboBox->findData(defaultHost->getName()); + Q_ASSERT(idx > 0); + m_defaultImageHostComboBox->setCurrentIndex(idx); + } else { + m_defaultImageHostComboBox->setCurrentIndex(0); + } + } + + m_clearObsoleteImageCheckBox->setChecked(editorConfig.isClearObsoleteImageAtImageHostEnabled()); + + // Clear all the boxes before. + { + auto boxes = findChildren(QString(), Qt::FindDirectChildrenOnly); + for (auto box : boxes) { + if (box->objectName().isEmpty()) { + continue; + } + + m_mainLayout->removeWidget(box); + box->deleteLater(); + } + } + + // Setup boxes. + for (const auto &host : hosts) { + auto box = setupGroupBoxForImageHost(host, this); + addWidgetToLayout(box); + } +} + +bool ImageHostPage::saveInternal() +{ + auto &hostMgr = ImageHostMgr::getInst(); + auto &editorConfig = ConfigMgr::getInst().getEditorConfig(); + + Q_ASSERT(m_hostToFields.size() == hostMgr.getImageHosts().size()); + + bool hasError = false; + + hostMgr.setDefaultImageHost(m_defaultImageHostComboBox->currentData().toString()); + + editorConfig.setClearObsoleteImageAtImageHostEnabled(m_clearObsoleteImageCheckBox->isChecked()); + + for (auto it = m_hostToFields.constBegin(); it != m_hostToFields.constEnd(); ++it) { + auto host = it.key(); + const auto &fields = it.value(); + Q_ASSERT(!fields.isEmpty()); + + // Name. + { + auto box = dynamic_cast(fields[0]->parent()); + Q_ASSERT(box); + auto nameLineEdit = box->findChild(QStringLiteral("_name"), Qt::FindDirectChildrenOnly); + Q_ASSERT(nameLineEdit); + const auto &newName = nameLineEdit->text(); + if (newName != host->getName()) { + if (!hostMgr.renameImageHost(host, newName)) { + setError(tr("Failed to rename image host (%1) to (%2).").arg(host->getName(), newName)); + hasError = true; + break; + } + + box->setObjectName(newName); + } + } + + // Configs. + const auto configObj = fieldsToConfig(fields); + host->setConfig(configObj); + } + + hostMgr.saveImageHosts(); + + // No need to notify editor since ImageHostMgr will signal out. + // EditorPage::notifyEditorConfigChange(); + return !hasError; +} + +QString ImageHostPage::title() const +{ + return tr("Image Host"); +} + +void ImageHostPage::newImageHost() +{ + NewImageHostDialog dialog(this); + if (dialog.exec()) { + auto box = setupGroupBoxForImageHost(dialog.getNewImageHost(), this); + addWidgetToLayout(box); + } +} + +QGroupBox *ImageHostPage::setupGroupBoxForImageHost(ImageHost *p_host, QWidget *p_parent) +{ + auto box = new QGroupBox(p_parent); + box->setObjectName(p_host->getName()); + auto layout = WidgetsFactory::createFormLayout(box); + + // Add Test and Delete button. + { + auto btnLayout = new QHBoxLayout(); + btnLayout->addStretch(); + + layout->addRow(btnLayout); + + auto testBtn = new QPushButton(tr("Test"), box); + btnLayout->addWidget(testBtn); + connect(testBtn, &QPushButton::clicked, + this, [this, box]() { + const auto name = box->objectName(); + testImageHost(name); + }); + + auto deleteBtn = new QPushButton(tr("Delete"), box); + btnLayout->addWidget(deleteBtn); + connect(deleteBtn, &QPushButton::clicked, + this, [this, box]() { + const auto name = box->objectName(); + removeImageHost(name); + }); + } + + layout->addRow(tr("Type:"), new QLabel(ImageHost::typeString(p_host->getType()), box)); + + auto nameLineEdit = WidgetsFactory::createLineEdit(p_host->getName(), box); + nameLineEdit->setObjectName(QStringLiteral("_name")); + layout->addRow(tr("Name:"), nameLineEdit); + m_hostToFields[p_host].append(nameLineEdit); + connect(nameLineEdit, &QLineEdit::textChanged, + this, &ImageHostPage::pageIsChanged); + + const auto configObj = p_host->getConfig(); + const auto keys = configObj.keys(); + for (const auto &key : keys) { + Q_ASSERT(key != "_name"); + auto configLineEdit = WidgetsFactory::createLineEdit(configObj[key].toString(), box); + configLineEdit->setObjectName(key); + layout->addRow(tr("%1:").arg(key), configLineEdit); + m_hostToFields[p_host].append(configLineEdit); + connect(configLineEdit, &QLineEdit::textChanged, + this, &ImageHostPage::pageIsChanged); + } + + return box; +} + +void ImageHostPage::removeImageHost(const QString &p_hostName) +{ + int ret = MessageBoxHelper::questionOkCancel(MessageBoxHelper::Type::Question, + tr("Delete image host (%1)?").arg(p_hostName)); + if (ret != QMessageBox::Ok) { + return; + } + + auto &hostMgr = ImageHostMgr::getInst(); + auto host = hostMgr.find(p_hostName); + Q_ASSERT(host); + hostMgr.removeImageHost(host); + + // Remove the group box and related fields. + m_hostToFields.remove(host); + + auto box = findChild(p_hostName, Qt::FindDirectChildrenOnly); + Q_ASSERT(box); + m_mainLayout->removeWidget(box); + box->deleteLater(); +} + +QJsonObject ImageHostPage::fieldsToConfig(const QVector &p_fields) const +{ + QJsonObject configObj; + for (auto field : p_fields) { + configObj[field->objectName()] = field->text(); + } + + return configObj; +} + +void ImageHostPage::testImageHost(const QString &p_hostName) +{ + auto &hostMgr = ImageHostMgr::getInst(); + auto host = hostMgr.find(p_hostName); + Q_ASSERT(host); + + auto it = m_hostToFields.find(host); + Q_ASSERT(it != m_hostToFields.end()); + + const auto configObj = fieldsToConfig(it.value()); + QString msg; + bool ret = host->testConfig(configObj, msg); + MessageBoxHelper::notify(ret ? MessageBoxHelper::Information : MessageBoxHelper::Warning, + tr("Test %1.").arg(ret ? tr("succeeded") : tr("failed")), + QString(), + msg); +} diff --git a/src/widgets/dialogs/settings/imagehostpage.h b/src/widgets/dialogs/settings/imagehostpage.h new file mode 100644 index 00000000..df9d9ce0 --- /dev/null +++ b/src/widgets/dialogs/settings/imagehostpage.h @@ -0,0 +1,60 @@ +#ifndef IMAGEHOSTPAGE_H +#define IMAGEHOSTPAGE_H + +#include "settingspage.h" + +#include +#include + +class QGroupBox; +class QLineEdit; +class QVBoxLayout; +class QComboBox; +class QCheckBox; + +namespace vnotex +{ + class ImageHost; + + class ImageHostPage : public SettingsPage + { + Q_OBJECT + public: + explicit ImageHostPage(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 newImageHost(); + + QGroupBox *setupGroupBoxForImageHost(ImageHost *p_host, QWidget *p_parent); + + void removeImageHost(const QString &p_hostName); + + void addWidgetToLayout(QWidget *p_widget); + + QJsonObject fieldsToConfig(const QVector &p_fields) const; + + void testImageHost(const QString &p_hostName); + + QGroupBox *setupGeneralBox(QWidget *p_parent); + + QVBoxLayout *m_mainLayout = nullptr; + + // [host] -> list of related fields. + QMap> m_hostToFields; + + QComboBox *m_defaultImageHostComboBox = nullptr; + + QCheckBox *m_clearObsoleteImageCheckBox = nullptr; + }; +} + +#endif // IMAGEHOSTPAGE_H diff --git a/src/widgets/dialogs/settings/markdowneditorpage.cpp b/src/widgets/dialogs/settings/markdowneditorpage.cpp index d6f773a0..a14f59e1 100644 --- a/src/widgets/dialogs/settings/markdowneditorpage.cpp +++ b/src/widgets/dialogs/settings/markdowneditorpage.cpp @@ -117,7 +117,7 @@ void MarkdownEditorPage::loadInternal() } } -void MarkdownEditorPage::saveInternal() +bool MarkdownEditorPage::saveInternal() { auto &markdownConfig = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig(); @@ -186,6 +186,8 @@ void MarkdownEditorPage::saveInternal() } EditorPage::notifyEditorConfigChange(); + + return true; } QString MarkdownEditorPage::title() const diff --git a/src/widgets/dialogs/settings/markdowneditorpage.h b/src/widgets/dialogs/settings/markdowneditorpage.h index 2f660ed3..2fdfa00c 100644 --- a/src/widgets/dialogs/settings/markdowneditorpage.h +++ b/src/widgets/dialogs/settings/markdowneditorpage.h @@ -25,7 +25,7 @@ namespace vnotex protected: void loadInternal() Q_DECL_OVERRIDE; - void saveInternal() Q_DECL_OVERRIDE; + bool saveInternal() Q_DECL_OVERRIDE; private: void setupUI(); diff --git a/src/widgets/dialogs/settings/miscpage.cpp b/src/widgets/dialogs/settings/miscpage.cpp index 3163583e..e8d2127c 100644 --- a/src/widgets/dialogs/settings/miscpage.cpp +++ b/src/widgets/dialogs/settings/miscpage.cpp @@ -25,9 +25,9 @@ void MiscPage::loadInternal() } -void MiscPage::saveInternal() +bool MiscPage::saveInternal() { - + return true; } QString MiscPage::title() const diff --git a/src/widgets/dialogs/settings/miscpage.h b/src/widgets/dialogs/settings/miscpage.h index fc39d3c0..c1039443 100644 --- a/src/widgets/dialogs/settings/miscpage.h +++ b/src/widgets/dialogs/settings/miscpage.h @@ -16,7 +16,7 @@ namespace vnotex protected: void loadInternal() Q_DECL_OVERRIDE; - void saveInternal() Q_DECL_OVERRIDE; + bool saveInternal() Q_DECL_OVERRIDE; private: void setupUI(); diff --git a/src/widgets/dialogs/settings/newimagehostdialog.cpp b/src/widgets/dialogs/settings/newimagehostdialog.cpp new file mode 100644 index 00000000..a27c7247 --- /dev/null +++ b/src/widgets/dialogs/settings/newimagehostdialog.cpp @@ -0,0 +1,88 @@ +#include "newimagehostdialog.h" + +#include +#include +#include +#include + +#include +#include + +using namespace vnotex; + +NewImageHostDialog::NewImageHostDialog(QWidget *p_parent) + : ScrollDialog(p_parent) +{ + setupUI(); +} + +void NewImageHostDialog::setupUI() +{ + auto widget = new QWidget(this); + setCentralWidget(widget); + + auto mainLayout = WidgetsFactory::createFormLayout(widget); + + { + m_typeComboBox = WidgetsFactory::createComboBox(widget); + mainLayout->addRow(tr("Type:"), m_typeComboBox); + + for (int type = static_cast(ImageHost::GitHub); type < static_cast(ImageHost::MaxHost); ++type) { + m_typeComboBox->addItem(ImageHost::typeString(static_cast(type)), type); + } + } + + m_nameLineEdit = WidgetsFactory::createLineEdit(widget); + mainLayout->addRow(tr("Name:"), m_nameLineEdit); + + setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + setWindowTitle(tr("New Image Host")); +} + +void NewImageHostDialog::acceptedButtonClicked() +{ + if (validateInputs() && newImageHost()) { + accept(); + } +} + +bool NewImageHostDialog::validateInputs() +{ + bool valid = true; + QString msg; + + auto name = m_nameLineEdit->text(); + if (name.isEmpty()) { + msg = tr("Please specify a valid name for the image host."); + valid = false; + } else if (ImageHostMgr::getInst().find(name)) { + msg = tr("Name conflicts with existing image host."); + valid = false; + } + + if (!valid) { + setInformationText(msg, ScrollDialog::InformationLevel::Error); + return false; + } + + return true; +} + +bool NewImageHostDialog::newImageHost() +{ + m_imageHost = ImageHostMgr::getInst().newImageHost(static_cast(m_typeComboBox->currentData().toInt()), + m_nameLineEdit->text()); + if (!m_imageHost) { + setInformationText(tr("Failed to create image host (%1).").arg(m_nameLineEdit->text()), + ScrollDialog::InformationLevel::Error); + return false; + } + + return true; +} + +ImageHost *NewImageHostDialog::getNewImageHost() const +{ + return m_imageHost; +} diff --git a/src/widgets/dialogs/settings/newimagehostdialog.h b/src/widgets/dialogs/settings/newimagehostdialog.h new file mode 100644 index 00000000..b670703b --- /dev/null +++ b/src/widgets/dialogs/settings/newimagehostdialog.h @@ -0,0 +1,39 @@ +#ifndef NEWIMAGEHOSTDIALOG_H +#define NEWIMAGEHOSTDIALOG_H + +#include "../scrolldialog.h" + +class QComboBox; +class QLineEdit; + +namespace vnotex +{ + class ImageHost; + + class NewImageHostDialog : public ScrollDialog + { + Q_OBJECT + public: + explicit NewImageHostDialog(QWidget *p_parent = nullptr); + + ImageHost *getNewImageHost() const; + + protected: + void acceptedButtonClicked() Q_DECL_OVERRIDE; + + private: + void setupUI(); + + bool validateInputs(); + + bool newImageHost(); + + QComboBox *m_typeComboBox = nullptr; + + QLineEdit *m_nameLineEdit = nullptr; + + ImageHost *m_imageHost = nullptr; + }; +} + +#endif // NEWIMAGEHOSTDIALOG_H diff --git a/src/widgets/dialogs/settings/quickaccesspage.cpp b/src/widgets/dialogs/settings/quickaccesspage.cpp index 7a58b962..883ee185 100644 --- a/src/widgets/dialogs/settings/quickaccesspage.cpp +++ b/src/widgets/dialogs/settings/quickaccesspage.cpp @@ -47,7 +47,7 @@ void QuickAccessPage::loadInternal() } } -void QuickAccessPage::saveInternal() +bool QuickAccessPage::saveInternal() { auto &sessionConfig = ConfigMgr::getInst().getSessionConfig(); @@ -59,6 +59,8 @@ void QuickAccessPage::saveInternal() sessionConfig.setQuickAccessFiles(text.split(QChar('\n'))); } } + + return true; } QString QuickAccessPage::title() const diff --git a/src/widgets/dialogs/settings/quickaccesspage.h b/src/widgets/dialogs/settings/quickaccesspage.h index 86e5e905..6d1e90bd 100644 --- a/src/widgets/dialogs/settings/quickaccesspage.h +++ b/src/widgets/dialogs/settings/quickaccesspage.h @@ -21,7 +21,7 @@ namespace vnotex protected: void loadInternal() Q_DECL_OVERRIDE; - void saveInternal() Q_DECL_OVERRIDE; + bool saveInternal() Q_DECL_OVERRIDE; private: void setupUI(); diff --git a/src/widgets/dialogs/settings/settingsdialog.cpp b/src/widgets/dialogs/settings/settingsdialog.cpp index e37bf713..a091dbe8 100644 --- a/src/widgets/dialogs/settings/settingsdialog.cpp +++ b/src/widgets/dialogs/settings/settingsdialog.cpp @@ -17,6 +17,7 @@ #include "appearancepage.h" #include "quickaccesspage.h" #include "themepage.h" +#include "imagehostpage.h" using namespace vnotex; @@ -38,12 +39,12 @@ void SettingsDialog::setupUI() setupPageExplorer(mainLayout, widget); { - auto scrollArea = new QScrollArea(widget); - scrollArea->setWidgetResizable(true); - mainLayout->addWidget(scrollArea, 5); + m_scrollArea = new QScrollArea(widget); + m_scrollArea->setWidgetResizable(true); + mainLayout->addWidget(m_scrollArea, 6); - auto scrollWidget = new QWidget(scrollArea); - scrollArea->setWidget(scrollWidget); + auto scrollWidget = new QWidget(m_scrollArea); + m_scrollArea->setWidget(scrollWidget); m_pageLayout = new QStackedLayout(scrollWidget); } @@ -111,6 +112,12 @@ void SettingsDialog::setupPages() auto page = new EditorPage(this); auto item = addPage(page); + // Image Host. + { + auto subPage = new ImageHostPage(this); + addSubPage(subPage, item); + } + // Text Editor. { auto subPage = new TextEditorPage(this); @@ -171,7 +178,10 @@ void SettingsDialog::setChangesUnsaved(bool p_unsaved) void SettingsDialog::acceptedButtonClicked() { if (m_changesUnsaved) { - savePages(); + if (savePages()) { + accept(); + } + return; } accept(); @@ -179,9 +189,12 @@ void SettingsDialog::acceptedButtonClicked() void SettingsDialog::resetButtonClicked() { + clearInformationText(); + m_ready = false; forEachPage([](SettingsPage *p_page) { p_page->reset(); + return true; }); m_ready = true; @@ -194,20 +207,39 @@ void SettingsDialog::appliedButtonClicked() savePages(); } -void SettingsDialog::savePages() +bool SettingsDialog::savePages() { - forEachPage([](SettingsPage *p_page) { - p_page->save(); + clearInformationText(); + + bool allSaved = true; + forEachPage([this, &allSaved](SettingsPage *p_page) { + if (!p_page->save()) { + allSaved = false; + m_pageLayout->setCurrentWidget(p_page); + if (!p_page->error().isEmpty()) { + setInformationText(p_page->error(), InformationLevel::Error); + } + return false; + } + + return true; }); - setChangesUnsaved(false); + if (allSaved) { + setChangesUnsaved(false); + return true; + } + + return false; } -void SettingsDialog::forEachPage(const std::function &p_func) +void SettingsDialog::forEachPage(const std::function &p_func) { for (int i = 0; i < m_pageLayout->count(); ++i) { auto page = dynamic_cast(m_pageLayout->widget(i)); - p_func(page); + if (!p_func(page)) { + break; + } } } @@ -228,3 +260,14 @@ QTreeWidgetItem *SettingsDialog::addSubPage(SettingsPage *p_page, QTreeWidgetIte setupPage(subItem, p_page); return subItem; } + +void SettingsDialog::showEvent(QShowEvent *p_event) +{ + Dialog::showEvent(p_event); + + if (m_firstShown) { + m_firstShown = false; + const auto sz = size(); + resize(sz * 1.2); + } +} diff --git a/src/widgets/dialogs/settings/settingsdialog.h b/src/widgets/dialogs/settings/settingsdialog.h index 4cb875a0..681f50f5 100644 --- a/src/widgets/dialogs/settings/settingsdialog.h +++ b/src/widgets/dialogs/settings/settingsdialog.h @@ -9,6 +9,7 @@ class QTreeWidget; class QStackedLayout; class QLineEdit; class QTreeWidgetItem; +class QScrollArea; namespace vnotex { @@ -27,6 +28,8 @@ namespace vnotex void appliedButtonClicked() Q_DECL_OVERRIDE; + void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE; + private: void setupUI(); @@ -40,9 +43,10 @@ namespace vnotex void setChangesUnsaved(bool p_unsaved); - void savePages(); + bool savePages(); - void forEachPage(const std::function &p_func); + // @p_func: return true to continue the iteration. + void forEachPage(const std::function &p_func); QTreeWidgetItem *addPage(SettingsPage *p_page); @@ -52,11 +56,15 @@ namespace vnotex QTreeWidget *m_pageExplorer = nullptr; + QScrollArea *m_scrollArea = nullptr; + QStackedLayout *m_pageLayout = nullptr; bool m_changesUnsaved = false; bool m_ready = false; + + bool m_firstShown = true; }; } diff --git a/src/widgets/dialogs/settings/settingspage.cpp b/src/widgets/dialogs/settings/settingspage.cpp index 5a27cda1..3ba2682c 100644 --- a/src/widgets/dialogs/settings/settingspage.cpp +++ b/src/widgets/dialogs/settings/settingspage.cpp @@ -53,14 +53,18 @@ void SettingsPage::load() m_changed = false; } -void SettingsPage::save() +bool SettingsPage::save() { if (!m_changed) { - return; + return true; } - saveInternal(); - m_changed = false; + if (saveInternal()) { + m_changed = false; + return true; + } + + return false; } void SettingsPage::reset() @@ -71,3 +75,13 @@ void SettingsPage::reset() load(); } + +const QString &SettingsPage::error() const +{ + return m_error; +} + +void SettingsPage::setError(const QString &p_err) +{ + m_error = p_err; +} diff --git a/src/widgets/dialogs/settings/settingspage.h b/src/widgets/dialogs/settings/settingspage.h index 22a2acfe..93c2578c 100644 --- a/src/widgets/dialogs/settings/settingspage.h +++ b/src/widgets/dialogs/settings/settingspage.h @@ -15,7 +15,7 @@ namespace vnotex void load(); - void save(); + bool save(); void reset(); @@ -23,13 +23,15 @@ namespace vnotex bool search(const QString &p_key); + const QString &error() const; + signals: void changed(); protected: virtual void loadInternal() = 0; - virtual void saveInternal() = 0; + virtual bool saveInternal() = 0; // Subclass could override this method to highlight matched target. virtual void searchHit(QWidget *p_target); @@ -38,6 +40,8 @@ namespace vnotex void addSearchItem(const QString &p_name, const QString &p_tooltip, QWidget *p_target); + void setError(const QString &p_err); + protected slots: void pageIsChanged(); @@ -59,6 +63,8 @@ namespace vnotex QVector m_searchItems; bool m_changed = false; + + QString m_error; }; } diff --git a/src/widgets/dialogs/settings/texteditorpage.cpp b/src/widgets/dialogs/settings/texteditorpage.cpp index 56aa2674..8a9cd819 100644 --- a/src/widgets/dialogs/settings/texteditorpage.cpp +++ b/src/widgets/dialogs/settings/texteditorpage.cpp @@ -183,7 +183,7 @@ void TextEditorPage::loadInternal() m_spellCheckCheckBox->setChecked(textConfig.isSpellCheckEnabled()); } -void TextEditorPage::saveInternal() +bool TextEditorPage::saveInternal() { auto &textConfig = ConfigMgr::getInst().getEditorConfig().getTextEditorConfig(); @@ -218,6 +218,8 @@ void TextEditorPage::saveInternal() textConfig.setSpellCheckEnabled(m_spellCheckCheckBox->isChecked()); EditorPage::notifyEditorConfigChange(); + + return true; } QString TextEditorPage::title() const diff --git a/src/widgets/dialogs/settings/texteditorpage.h b/src/widgets/dialogs/settings/texteditorpage.h index 3bf89740..51bd8067 100644 --- a/src/widgets/dialogs/settings/texteditorpage.h +++ b/src/widgets/dialogs/settings/texteditorpage.h @@ -20,7 +20,7 @@ namespace vnotex protected: void loadInternal() Q_DECL_OVERRIDE; - void saveInternal() Q_DECL_OVERRIDE; + bool saveInternal() Q_DECL_OVERRIDE; private: void setupUI(); diff --git a/src/widgets/dialogs/settings/themepage.cpp b/src/widgets/dialogs/settings/themepage.cpp index 19840673..ca167fc5 100644 --- a/src/widgets/dialogs/settings/themepage.cpp +++ b/src/widgets/dialogs/settings/themepage.cpp @@ -98,12 +98,14 @@ void ThemePage::loadInternal() loadThemes(); } -void ThemePage::saveInternal() +bool ThemePage::saveInternal() { auto theme = currentTheme(); if (!theme.isEmpty()) { ConfigMgr::getInst().getCoreConfig().setTheme(theme); } + + return true; } QString ThemePage::title() const diff --git a/src/widgets/dialogs/settings/themepage.h b/src/widgets/dialogs/settings/themepage.h index 4be41ce0..e288a66f 100644 --- a/src/widgets/dialogs/settings/themepage.h +++ b/src/widgets/dialogs/settings/themepage.h @@ -19,7 +19,7 @@ namespace vnotex protected: void loadInternal() Q_DECL_OVERRIDE; - void saveInternal() Q_DECL_OVERRIDE; + bool saveInternal() Q_DECL_OVERRIDE; private: void setupUI(); diff --git a/src/widgets/dialogs/snippetinfowidget.cpp b/src/widgets/dialogs/snippetinfowidget.cpp index 08df5f48..85cf6568 100644 --- a/src/widgets/dialogs/snippetinfowidget.cpp +++ b/src/widgets/dialogs/snippetinfowidget.cpp @@ -76,6 +76,7 @@ void SnippetInfoWidget::setupUI() mainLayout->addRow(m_indentAsFirstLineCheckBox); m_contentTextEdit = WidgetsFactory::createPlainTextEdit(this); + m_contentTextEdit->setPlaceholderText(tr("Nested snippet is supported, like `%time%` to embed the snippet `time`")); connect(m_contentTextEdit, &QPlainTextEdit::textChanged, this, &SnippetInfoWidget::inputEdited); mainLayout->addRow(tr("Content:"), m_contentTextEdit); diff --git a/src/widgets/editors/markdowneditor.cpp b/src/widgets/editors/markdowneditor.cpp index 3f751acd..c03a4553 100644 --- a/src/widgets/editors/markdowneditor.cpp +++ b/src/widgets/editors/markdowneditor.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -44,6 +45,9 @@ #include #include #include +#include +#include +#include #include "previewhelper.h" #include "../outlineprovider.h" @@ -358,22 +362,34 @@ bool MarkdownEditor::insertImageToBufferFromLocalFile(const QString &p_title, auto destFileName = generateImageFileNameToInsertAs(p_title, QFileInfo(p_srcImagePath).suffix()); QString destFilePath; - try { - destFilePath = m_buffer->insertImage(p_srcImagePath, destFileName); - } catch (Exception e) { - MessageBoxHelper::notify(MessageBoxHelper::Warning, - QString("Failed to insert image from local file %1 (%2)").arg(p_srcImagePath, e.what()), - this); - return false; + + if (m_imageHost) { + // Save to image host. + QByteArray ba; + try { + ba = FileUtils::readFile(p_srcImagePath); + } catch (Exception &e) { + MessageBoxHelper::notify(MessageBoxHelper::Warning, + QString("Failed to read local image file (%1) (%2).").arg(p_srcImagePath, e.what()), + this); + return false; + } + destFilePath = saveToImageHost(ba, destFileName); + if (destFilePath.isEmpty()) { + return false; + } + } else { + try { + destFilePath = m_buffer->insertImage(p_srcImagePath, destFileName); + } catch (Exception &e) { + MessageBoxHelper::notify(MessageBoxHelper::Warning, + QString("Failed to insert image from local file (%1) (%2).").arg(p_srcImagePath, e.what()), + this); + return false; + } } - insertImageLink(p_title, - p_altText, - destFilePath, - p_scaledWidth, - p_scaledHeight, - p_insertText, - p_urlInLink); + insertImageLink(p_title, p_altText, destFilePath, p_scaledWidth, p_scaledHeight, p_insertText, p_urlInLink); return true; } @@ -389,16 +405,31 @@ bool MarkdownEditor::insertImageToBufferFromData(const QString &p_title, int p_scaledHeight) { // Save as PNG by default. - auto destFileName = generateImageFileNameToInsertAs(p_title, QStringLiteral("png")); + const QString format("png"); + const auto destFileName = generateImageFileNameToInsertAs(p_title, format); QString destFilePath; - try { - destFilePath = m_buffer->insertImage(p_image, destFileName); - } catch (Exception e) { - MessageBoxHelper::notify(MessageBoxHelper::Warning, - QString("Failed to insert image from data (%1)").arg(e.what()), - this); - return false; + + if (m_imageHost) { + // Save to image host. + QByteArray ba; + QBuffer buffer(&ba); + buffer.open(QIODevice::WriteOnly); + p_image.save(&buffer, format.toStdString().c_str()); + + destFilePath = saveToImageHost(ba, destFileName); + if (destFilePath.isEmpty()) { + return false; + } + } else { + try { + destFilePath = m_buffer->insertImage(p_image, destFileName); + } catch (Exception &e) { + MessageBoxHelper::notify(MessageBoxHelper::Warning, + QString("Failed to insert image from data (%1).").arg(e.what()), + this); + return false; + } } insertImageLink(p_title, p_altText, destFilePath, p_scaledWidth, p_scaledHeight); @@ -764,13 +795,17 @@ void MarkdownEditor::insertImageFromUrl(const QString &p_url) QString MarkdownEditor::getRelativeLink(const QString &p_path) { - auto relativePath = PathUtils::relativePath(PathUtils::parentDirPath(m_buffer->getContentPath()), p_path); - auto link = PathUtils::encodeSpacesInPath(QDir::fromNativeSeparators(relativePath)); - if (m_config.getPrependDotInRelativeLink()) { - PathUtils::prependDotIfRelative(link); - } + if (PathUtils::isLocalFile(p_path)) { + auto relativePath = PathUtils::relativePath(PathUtils::parentDirPath(m_buffer->getContentPath()), p_path); + auto link = PathUtils::encodeSpacesInPath(QDir::fromNativeSeparators(relativePath)); + if (m_config.getPrependDotInRelativeLink()) { + PathUtils::prependDotIfRelative(link); + } - return link; + return link; + } else { + return p_path; + } } const QVector &MarkdownEditor::getHeadings() const @@ -957,6 +992,8 @@ void MarkdownEditor::handleContextMenuEvent(QContextMenuEvent *p_event, bool *p_ } } + appendImageHostMenu(menu); + appendSpellCheckMenu(p_event, menu); } @@ -1100,7 +1137,7 @@ void MarkdownEditor::fetchImagesToLocalAndReplace(QString &p_text) if (imageUrl.startsWith(QStringLiteral("//"))) { imageUrl.prepend(QStringLiteral("https:")); } - QByteArray data = vte::Downloader::download(QUrl(imageUrl)); + QByteArray data = vte::NetworkAccess::request(QUrl(imageUrl)).m_data; if (!data.isEmpty()) { // Prefer the suffix from the real data. auto suffix = ImageUtils::guessImageSuffix(data); @@ -1293,3 +1330,64 @@ QRgb MarkdownEditor::getPreviewBackground() const const auto &fmt = th->editorStyle(vte::Theme::EditorStyle::Preview); return fmt.m_backgroundColor; } + +void MarkdownEditor::setImageHost(ImageHost *p_host) +{ + // It may be different than the global default image host. + m_imageHost = p_host; +} + +QString MarkdownEditor::saveToImageHost(const QByteArray &p_imageData, const QString &p_destFileName) +{ + Q_ASSERT(m_imageHost); + + auto destPath = ImageHostUtils::generateRelativePath(m_buffer); + if (destPath.isEmpty()) { + destPath = p_destFileName; + } else { + destPath += "/" + p_destFileName; + } + + QString errMsg; + + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + auto targetUrl = m_imageHost->create(p_imageData, destPath, errMsg); + QApplication::restoreOverrideCursor(); + + if (targetUrl.isEmpty()) { + MessageBoxHelper::notify(MessageBoxHelper::Warning, + QString("Failed to upload image to image host (%1) as (%2).").arg(m_imageHost->getName(), destPath), + QString(), + errMsg, + this); + } + + return targetUrl; +} + +void MarkdownEditor::appendImageHostMenu(QMenu *p_menu) +{ + p_menu->addSeparator(); + auto subMenu = p_menu->addMenu(tr("Upload Images To Image Host")); + + const auto &hosts = ImageHostMgr::getInst().getImageHosts(); + if (hosts.isEmpty()) { + auto act = subMenu->addAction(tr("None")); + act->setEnabled(false); + return; + } + + for (const auto &host : hosts) { + auto act = subMenu->addAction(host->getName(), + this, + &MarkdownEditor::uploadImagesToImageHost); + act->setData(host->getName()); + } +} + +void MarkdownEditor::uploadImagesToImageHost() +{ + auto act = static_cast(sender()); + auto host = ImageHostMgr::getInst().find(act->data().toString()); + Q_ASSERT(host); +} diff --git a/src/widgets/editors/markdowneditor.h b/src/widgets/editors/markdowneditor.h index 65c94c48..dbc72fb2 100644 --- a/src/widgets/editors/markdowneditor.h +++ b/src/widgets/editors/markdowneditor.h @@ -23,6 +23,7 @@ namespace vnotex class Buffer; class MarkdownEditorConfig; class MarkdownTableHelper; + class ImageHost; class MarkdownEditor : public vte::VMarkdownEditor { @@ -102,6 +103,8 @@ namespace vnotex QRgb getPreviewBackground() const; + void setImageHost(ImageHost *p_host); + public slots: void handleHtmlToMarkdownData(quint64 p_id, TimeStamp p_timeStamp, const QString &p_text); @@ -181,6 +184,13 @@ namespace vnotex void setupTableHelper(); + // Return the dest file path of the image on success. + QString saveToImageHost(const QByteArray &p_imageData, const QString &p_destFileName); + + void appendImageHostMenu(QMenu *p_menu); + + void uploadImagesToImageHost(); + static QString generateImageFileNameToInsertAs(const QString &p_title, const QString &p_suffix); const MarkdownEditorConfig &m_config; @@ -203,6 +213,8 @@ namespace vnotex // Managed by QObject. MarkdownTableHelper *m_tableHelper = nullptr; + + ImageHost *m_imageHost = nullptr; }; } diff --git a/src/widgets/editors/plantumlhelper.cpp b/src/widgets/editors/plantumlhelper.cpp index 2fe147b4..bd8d0474 100644 --- a/src/widgets/editors/plantumlhelper.cpp +++ b/src/widgets/editors/plantumlhelper.cpp @@ -57,7 +57,9 @@ void PlantUmlHelper::prepareProgramAndArgs(const QString &p_plantUmlJarFile, p_args << "java"; #endif +#if defined(Q_OS_MACOS) p_args << "-Djava.awt.headless=true"; +#endif p_args << "-jar" << QDir::toNativeSeparators(p_plantUmlJarFile); diff --git a/src/widgets/filesystemviewer.cpp b/src/widgets/filesystemviewer.cpp index 46460430..c01085cf 100644 --- a/src/widgets/filesystemviewer.cpp +++ b/src/widgets/filesystemviewer.cpp @@ -42,23 +42,7 @@ void FileSystemViewer::setupUI() } connect(m_viewer, &QTreeView::customContextMenuRequested, - this, [this](const QPoint &p_pos) { - // @p_pos is the position in the coordinate of parent widget if parent is a popup. - auto pos = p_pos; - if (m_fixContextMenuPos) { - pos = mapFromParent(p_pos); - pos = m_viewer->mapFromParent(pos); - } - auto index = m_viewer->indexAt(pos); - QScopedPointer menu(WidgetsFactory::createMenu()); - if (index.isValid()) { - createContextMenuOnItem(menu.data()); - } - - if (!menu->isEmpty()) { - menu->exec(m_viewer->mapToGlobal(pos)); - } - }); + this, &FileSystemViewer::handleContextMenuRequested); connect(m_viewer, &QTreeView::activated, this, [this](const QModelIndex &p_index) { if (!this->fileModel()->isDir(p_index)) { @@ -142,8 +126,7 @@ void FileSystemViewer::createContextMenuOnItem(QMenu *p_menu) act = createAction(Action::Delete, p_menu); p_menu->addAction(act); - const auto modelIndexList = m_viewer->selectionModel()->selectedRows(); - if (modelIndexList.size() == 1) { + if (selectedCount() == 1) { act = createAction(Action::CopyPath, p_menu); p_menu->addAction(act); @@ -224,7 +207,38 @@ void FileSystemViewer::scrollToAndSelect(const QStringList &p_paths) m_viewer->scrollTo(index); isFirst = false; } - selectionModel->select(index, QItemSelectionModel::SelectCurrent); + selectionModel->select(index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); } } } + +void FileSystemViewer::handleContextMenuRequested(const QPoint &p_pos) +{ + // @p_pos is the position in the coordinate of parent widget if parent is a popup. + auto pos = p_pos; + if (m_fixContextMenuPos) { + pos = mapFromParent(p_pos); + pos = m_viewer->mapFromParent(pos); + } + + QScopedPointer menu(WidgetsFactory::createMenu()); + + auto index = m_viewer->indexAt(pos); + if (index.isValid()) { + auto selectionModel = m_viewer->selectionModel(); + if (!selectionModel->isSelected(index)) { + // Must select entire row since we use selectedRows() to count. + selectionModel->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + } + + m_viewer->update(); + + createContextMenuOnItem(menu.data()); + } + + m_viewer->update(); + + if (!menu->isEmpty()) { + menu->exec(m_viewer->mapToGlobal(pos)); + } +} diff --git a/src/widgets/filesystemviewer.h b/src/widgets/filesystemviewer.h index 1ad9106c..4ba80e2b 100644 --- a/src/widgets/filesystemviewer.h +++ b/src/widgets/filesystemviewer.h @@ -38,6 +38,8 @@ namespace vnotex // Resize the first column. void resizeTreeToContents(); + void handleContextMenuRequested(const QPoint &p_pos); + private: enum Action { Open, diff --git a/src/widgets/locationlist.cpp b/src/widgets/locationlist.cpp index 16a9f143..3ef2d46d 100644 --- a/src/widgets/locationlist.cpp +++ b/src/widgets/locationlist.cpp @@ -45,6 +45,7 @@ void LocationList::setupUI() // When updated, pay attention to the Columns enum. m_tree->setHeaderLabels(QStringList() << tr("Path") << tr("Line") << tr("Text")); TreeWidget::showHorizontalScrollbar(m_tree); + m_tree->header()->setStretchLastSection(true); connect(m_tree, &QTreeWidget::itemActivated, this, [this](QTreeWidgetItem *p_item, int p_col) { Q_UNUSED(p_col); @@ -156,6 +157,10 @@ void LocationList::addLocation(const ComplexLocation &p_location) item->setExpanded(true); } + if (m_tree->topLevelItemCount() == 1) { + m_tree->setCurrentItem(item); + } + updateItemsCountLabel(); } diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 6c022330..65e746f5 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include @@ -19,6 +21,8 @@ #include #include #include +#include +#include #include "editors/markdowneditor.h" #include "textviewwindowhelper.h" #include "editors/markdownviewer.h" @@ -31,6 +35,7 @@ #include "editors/statuswidget.h" #include "editors/plantumlhelper.h" #include "editors/graphvizhelper.h" +#include "messageboxhelper.h" using namespace vnotex; @@ -277,6 +282,10 @@ void MarkdownViewWindow::setupToolBar() }); } + addAction(toolBar, ViewWindowToolBarHelper::ImageHost); + + toolBar->addSeparator(); + addAction(toolBar, ViewWindowToolBarHelper::TypeHeading); addAction(toolBar, ViewWindowToolBarHelper::TypeBold); addAction(toolBar, ViewWindowToolBarHelper::TypeItalic); @@ -326,6 +335,8 @@ void MarkdownViewWindow::setupTextEditor() m_previewHelper->setMarkdownEditor(m_editor); m_editor->setPreviewHelper(m_previewHelper); + m_editor->setImageHost(m_imageHost); + // Connect viewer and editor. connect(adapter(), &MarkdownViewerAdapter::viewerReady, m_editor->getHighlighter(), &vte::PegMarkdownHighlighter::updateHighlight); @@ -727,16 +738,31 @@ void MarkdownViewWindow::clearObsoleteImages() auto buffer = getBuffer(); Q_ASSERT(buffer); - auto &markdownEditorConfig = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig(); + auto &editorConfig = ConfigMgr::getInst().getEditorConfig(); + auto &markdownEditorConfig = editorConfig.getMarkdownEditorConfig(); + const bool clearRemote = editorConfig.isClearObsoleteImageAtImageHostEnabled(); + const auto &hostMgr = ImageHostMgr::getInst(); + + QVector> imagesToDelete; + imagesToDelete.reserve(obsoleteImages.size()); + if (markdownEditorConfig.getConfirmBeforeClearObsoleteImages()) { QVector items; - for (auto const &imgPath : obsoleteImages) { - items.push_back(ConfirmItemInfo(imgPath, imgPath, imgPath, nullptr)); + for (auto it = obsoleteImages.constBegin(); it != obsoleteImages.constEnd(); ++it) { + if (!it.value() || (clearRemote && hostMgr.findByImageUrl(it.key()))) { + const auto imgPath = it.key(); + // Use the @m_data field to denote whether it is remote. + items.push_back(ConfirmItemInfo(imgPath, imgPath, imgPath, it.value() ? reinterpret_cast(1ULL) : nullptr)); + } + } + + if (items.isEmpty()) { + return; } DeleteConfirmDialog dialog(tr("Clear Obsolete Images"), - tr("These images seems not in use anymore. Please confirm the deletion of them."), - tr("Deleted images could be found in the recycle bin of notebook if it is from a bundle notebook."), + tr("These images seems to be not in use anymore. Please confirm the deletion of them."), + tr("Deleted local images could be found in the recycle bin of notebook if it is from a bundle notebook."), items, DeleteConfirmDialog::Flag::AskAgain | DeleteConfirmDialog::Flag::Preview, false, @@ -745,14 +771,49 @@ void MarkdownViewWindow::clearObsoleteImages() items = dialog.getConfirmedItems(); markdownEditorConfig.setConfirmBeforeClearObsoleteImages(!dialog.isNoAskChecked()); for (const auto &item : items) { - buffer->removeImage(item.m_path); + imagesToDelete.push_back(qMakePair(item.m_path, item.m_data != nullptr)); } } } else { - for (const auto &imgPath : obsoleteImages) { - buffer->removeImage(imgPath); + for (auto it = obsoleteImages.constBegin(); it != obsoleteImages.constEnd(); ++it) { + if (clearRemote || !it.value()) { + imagesToDelete.push_back(qMakePair(it.key(), it.value())); + } } } + + if (imagesToDelete.isEmpty()) { + return; + } + + QProgressDialog proDlg(tr("Clearing obsolete images..."), + tr("Abort"), + 0, + imagesToDelete.size(), + this); + proDlg.setWindowModality(Qt::WindowModal); + proDlg.setWindowTitle(tr("Clear Obsolete Images")); + + int cnt = 0; + for (int i = 0; i < imagesToDelete.size(); ++i) { + proDlg.setValue(i + 1); + if (proDlg.wasCanceled()) { + break; + } + + proDlg.setLabelText(tr("Clear image (%1)").arg(imagesToDelete[i].first)); + if (imagesToDelete[i].second) { + removeFromImageHost(imagesToDelete[i].first); + } else { + buffer->removeImage(imagesToDelete[i].first); + } + ++cnt; + } + + proDlg.setValue(imagesToDelete.size()); + + // It may be deleted so showMessage() is not available. + VNoteX::getInst().showStatusMessageShort(tr("Cleared %n obsolete images", "", cnt)); } QSharedPointer MarkdownViewWindow::getOutlineProvider() @@ -897,7 +958,7 @@ void MarkdownViewWindow::handleFindNext(const QString &p_text, FindOptions p_opt void MarkdownViewWindow::handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText) { if (isReadMode()) { - VNoteX::getInst().showStatusMessageShort(tr("Replace is not supported in read mode")); + showMessage(tr("Replace is not supported in read mode")); } else { TextViewWindowHelper::handleReplace(this, p_text, p_options, p_replaceText); } @@ -906,7 +967,7 @@ void MarkdownViewWindow::handleReplace(const QString &p_text, FindOptions p_opti void MarkdownViewWindow::handleReplaceAll(const QString &p_text, FindOptions p_options, const QString &p_replaceText) { if (isReadMode()) { - VNoteX::getInst().showStatusMessageShort(tr("Replace is not supported in read mode")); + showMessage(tr("Replace is not supported in read mode")); } else { TextViewWindowHelper::handleReplaceAll(this, p_text, p_options, p_replaceText); } @@ -1035,3 +1096,33 @@ QPoint MarkdownViewWindow::getFloatingWidgetPosition() { return TextViewWindowHelper::getFloatingWidgetPosition(this); } + +void MarkdownViewWindow::handleImageHostChanged(const QString &p_hostName) +{ + m_imageHost = ImageHostMgr::getInst().find(p_hostName); + + if (m_editor) { + m_editor->setImageHost(m_imageHost); + } +} + +void MarkdownViewWindow::removeFromImageHost(const QString &p_url) +{ + auto host = ImageHostMgr::getInst().findByImageUrl(p_url); + if (!host) { + return; + } + + QString errMsg; + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + auto ret = host->remove(p_url, errMsg); + QApplication::restoreOverrideCursor(); + + if (!ret) { + MessageBoxHelper::notify(MessageBoxHelper::Warning, + QString("Failed to delete image (%1) from image host (%2).").arg(p_url, host->getName()), + QString(), + errMsg, + this); + } +} diff --git a/src/widgets/markdownviewwindow.h b/src/widgets/markdownviewwindow.h index f9212bfa..0e3c5cd4 100644 --- a/src/widgets/markdownviewwindow.h +++ b/src/widgets/markdownviewwindow.h @@ -23,6 +23,7 @@ namespace vnotex struct Outline; class MarkdownEditorConfig; class EditorConfig; + class ImageHost; class MarkdownViewWindow : public ViewWindow { @@ -60,6 +61,8 @@ namespace vnotex void handleSectionNumberOverride(OverrideState p_state) Q_DECL_OVERRIDE; + void handleImageHostChanged(const QString &p_hostName) Q_DECL_OVERRIDE; + void handleFindTextChanged(const QString &p_text, FindOptions p_options) Q_DECL_OVERRIDE; void handleFindNext(const QString &p_text, FindOptions p_options) Q_DECL_OVERRIDE; @@ -147,6 +150,8 @@ namespace vnotex void updatePreviewHelperFromConfig(const MarkdownEditorConfig &p_config); + void removeFromImageHost(const QString &p_url); + template static QSharedPointer headingsToOutline(const QVector &p_headings); @@ -184,6 +189,8 @@ namespace vnotex ViewWindowMode m_previousMode = ViewWindowMode::Invalid; QSharedPointer m_outlineProvider; + + ImageHost *m_imageHost = nullptr; }; } diff --git a/src/widgets/viewwindow.cpp b/src/widgets/viewwindow.cpp index 90c0bb21..897e326a 100644 --- a/src/widgets/viewwindow.cpp +++ b/src/widgets/viewwindow.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "toolbarhelper.h" @@ -23,6 +24,7 @@ #include #include #include +#include #include "messageboxhelper.h" #include "editreaddiscardaction.h" #include "viewsplit.h" @@ -451,6 +453,28 @@ QAction *ViewWindow::addAction(QToolBar *p_toolBar, ViewWindowToolBarHelper::Act break; } + case ViewWindowToolBarHelper::ImageHost: + { + act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action); + connect(this, &ViewWindow::modeChanged, + this, [this, act]() { + act->setEnabled(inModeCanInsert() && getBuffer() && !getBuffer()->isReadOnly()); + }); + auto toolBtn = dynamic_cast(p_toolBar->widgetForAction(act)); + Q_ASSERT(toolBtn); + m_imageHostMenu = toolBtn->menu(); + Q_ASSERT(m_imageHostMenu); + updateImageHostMenu(); + connect(m_imageHostMenu, &QMenu::triggered, + this, [this](QAction *p_act) { + handleImageHostChanged(p_act->data().toString()); + }); + + connect(&ImageHostMgr::getInst(), &ImageHostMgr::imageHostChanged, + this, &ViewWindow::updateImageHostMenu); + break; + } + default: Q_ASSERT(false); break; @@ -625,6 +649,12 @@ void ViewWindow::handleSectionNumberOverride(OverrideState p_state) Q_ASSERT(false); } +void ViewWindow::handleImageHostChanged(const QString &p_hostName) +{ + Q_UNUSED(p_hostName); + Q_ASSERT(false); +} + ViewWindow::TypeAction ViewWindow::toolBarActionToTypeAction(ViewWindowToolBarHelper::Action p_action) { Q_ASSERT(p_action >= ViewWindowToolBarHelper::Action::TypeBold @@ -1152,3 +1182,36 @@ QPoint ViewWindow::getFloatingWidgetPosition() { return mapToGlobal(QPoint(5, 5)); } + +void ViewWindow::updateImageHostMenu() +{ + Q_ASSERT(m_imageHostMenu); + m_imageHostMenu->clear(); + + if (m_imageHostActionGroup) { + m_imageHostActionGroup->deleteLater(); + } + + m_imageHostActionGroup = new QActionGroup(m_imageHostMenu); + + auto act = m_imageHostActionGroup->addAction(tr("Local")); + act->setCheckable(true); + m_imageHostMenu->addAction(act); + act->setChecked(true); + + const auto &hosts = ImageHostMgr::getInst().getImageHosts(); + auto curHost = ImageHostMgr::getInst().getDefaultImageHost(); + + for (const auto &host : hosts) { + auto act = m_imageHostActionGroup->addAction(host->getName()); + act->setCheckable(true); + act->setData(host->getName()); + m_imageHostMenu->addAction(act); + + if (curHost == host) { + act->setChecked(true); + } + } + + handleImageHostChanged(curHost ? curHost->getName() : nullptr); +} diff --git a/src/widgets/viewwindow.h b/src/widgets/viewwindow.h index 9be9c64f..269698de 100644 --- a/src/widgets/viewwindow.h +++ b/src/widgets/viewwindow.h @@ -14,6 +14,8 @@ class QVBoxLayout; class QTimer; class QToolBar; +class QMenu; +class QActionGroup; namespace vnotex { @@ -158,6 +160,8 @@ namespace vnotex virtual void handleSectionNumberOverride(OverrideState p_state); + virtual void handleImageHostChanged(const QString &p_hostName); + virtual void handleFindTextChanged(const QString &p_text, FindOptions p_options); virtual void handleFindNext(const QString &p_text, FindOptions p_options); @@ -302,6 +306,8 @@ namespace vnotex void handleBufferChanged(const QSharedPointer &p_paras); + void updateImageHostMenu(); + static ViewWindow::TypeAction toolBarActionToTypeAction(ViewWindowToolBarHelper::Action p_action); Buffer *m_buffer = nullptr; @@ -344,6 +350,10 @@ namespace vnotex WindowFlags m_flags = WindowFlag::None; + QMenu *m_imageHostMenu = nullptr; + + QActionGroup *m_imageHostActionGroup = nullptr; + static QIcon s_savedIcon; static QIcon s_modifiedIcon; }; diff --git a/src/widgets/viewwindowtoolbarhelper.cpp b/src/widgets/viewwindowtoolbarhelper.cpp index ab437485..889a0d15 100644 --- a/src/widgets/viewwindowtoolbarhelper.cpp +++ b/src/widgets/viewwindowtoolbarhelper.cpp @@ -360,6 +360,21 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action) break; } + case Action::ImageHost: + { + act = p_tb->addAction(ToolBarHelper::generateIcon("image_host_editor.svg"), + ViewWindow::tr("Image Host")); + + auto toolBtn = dynamic_cast(p_tb->widgetForAction(act)); + Q_ASSERT(toolBtn); + toolBtn->setPopupMode(QToolButton::InstantPopup); + toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); + + auto menu = WidgetsFactory::createMenu(p_tb); + toolBtn->setMenu(menu); + break; + } + default: Q_ASSERT(false); break; diff --git a/src/widgets/viewwindowtoolbarhelper.h b/src/widgets/viewwindowtoolbarhelper.h index 385fdbf1..e2a93d62 100644 --- a/src/widgets/viewwindowtoolbarhelper.h +++ b/src/widgets/viewwindowtoolbarhelper.h @@ -44,7 +44,8 @@ namespace vnotex Outline, FindAndReplace, SectionNumber, - InplacePreview + InplacePreview, + ImageHost }; static QAction *addAction(QToolBar *p_tb, Action p_action); diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index 2d27c7d8..d9549624 100644 --- a/src/widgets/widgets.pri +++ b/src/widgets/widgets.pri @@ -19,8 +19,10 @@ SOURCES += \ $$PWD/dialogs/settings/appearancepage.cpp \ $$PWD/dialogs/settings/editorpage.cpp \ $$PWD/dialogs/settings/generalpage.cpp \ + $$PWD/dialogs/settings/imagehostpage.cpp \ $$PWD/dialogs/settings/markdowneditorpage.cpp \ $$PWD/dialogs/settings/miscpage.cpp \ + $$PWD/dialogs/settings/newimagehostdialog.cpp \ $$PWD/dialogs/settings/quickaccesspage.cpp \ $$PWD/dialogs/settings/settingspage.cpp \ $$PWD/dialogs/settings/settingsdialog.cpp \ @@ -129,8 +131,10 @@ HEADERS += \ $$PWD/dialogs/settings/appearancepage.h \ $$PWD/dialogs/settings/editorpage.h \ $$PWD/dialogs/settings/generalpage.h \ + $$PWD/dialogs/settings/imagehostpage.h \ $$PWD/dialogs/settings/markdowneditorpage.h \ $$PWD/dialogs/settings/miscpage.h \ + $$PWD/dialogs/settings/newimagehostdialog.h \ $$PWD/dialogs/settings/quickaccesspage.h \ $$PWD/dialogs/settings/settingspage.h \ $$PWD/dialogs/settings/settingsdialog.h \ diff --git a/tests/test_core/test_notebook/test_notebook.pro b/tests/test_core/test_notebook/test_notebook.pro index 22dd9b9d..bf529c00 100644 --- a/tests/test_core/test_notebook/test_notebook.pro +++ b/tests/test_core/test_notebook/test_notebook.pro @@ -20,6 +20,7 @@ include($$SRC_FOLDER/utils/utils.pri) include($$SRC_FOLDER/export/export.pri) include($$SRC_FOLDER/search/search.pri) include($$SRC_FOLDER/snippet/snippet.pri) +include($$SRC_FOLDER/imagehost/imagehost.pri) SOURCES += \ test_notebook.cpp