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