support Image Host

This commit is contained in:
Le Tan 2021-07-29 20:29:55 +08:00
parent 30a9d0ecc7
commit f1d931c276
85 changed files with 1995 additions and 175 deletions

@ -1 +1 @@
Subproject commit 922084a388e1f135e25297ba84a9d0ca0078ed06
Subproject commit c53fc8dbf6df14b9e1327de0a4907700ffee6049

View File

@ -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+)<string>\\d\\.\\d\\.\\d</string>')
for line in fileinput.input(['src/data/core/Info.plist'], inplace = True):
print(regExp.sub('\\1<string>' + newVersion + '</string>', line), end='')
regExp = re.compile('(\\s+)<string>\\d\\.\\d\\.\\d\\.\\d</string>')
for line in fileinput.input(['src/data/core/Info.plist'], inplace = True):
print(regExp.sub('\\1<string>' + newVersion + '.1</string>', line), end='')

View File

@ -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;

View File

@ -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<QString> MarkdownBuffer::clearObsoleteImages()
QHash<QString, bool> MarkdownBuffer::clearObsoleteImages()
{
QSet<QString> obsoleteImages;
QHash<QString, bool> 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<QString> 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);
}
}

View File

@ -4,7 +4,7 @@
#include "buffer.h"
#include <QVector>
#include <QSet>
#include <QHash>
#include <vtextedit/markdownutils.h>
@ -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<QString> clearObsoleteImages();
// Return [ImagePath] -> IsRemote.
QHash<QString, bool> clearObsoleteImages();
protected:
ViewWindow *createViewWindowInternal(const QSharedPointer<FileOpenParameters> &p_paras, QWidget *p_parent) Q_DECL_OVERRIDE;

View File

@ -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::ImageHostItem> &EditorConfig::getImageHosts() const
{
return m_imageHosts;
}
void EditorConfig::setImageHosts(const QVector<ImageHostItem> &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);
}

View File

@ -6,6 +6,7 @@
#include <QScopedPointer>
#include <QSharedPointer>
#include <QObject>
#include <QVector>
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<ImageHostItem> &getImageHosts() const;
void setImageHosts(const QVector<ImageHostItem> &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<ImageHostItem> m_imageHosts;
QString m_defaultImageHost;
bool m_clearObsoleteImageAtImageHost = false;
};
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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<HistoryItem> m_history;
QJsonObject m_extraConfigs;
};
} // ns vnotex

View File

@ -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();
}

View File

@ -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;

View File

@ -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> NotebookConfig::fromNotebook(const QString &p_version,
@ -111,6 +115,7 @@ QSharedPointer<NotebookConfig> 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;
}

View File

@ -50,6 +50,10 @@ namespace vnotex
QVector<HistoryItem> 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;

View File

@ -105,13 +105,13 @@ namespace vnotex
void addNotebook(const QSharedPointer<Notebook> &p_notebook);
QSharedPointer<NameBasedServer<IVersionControllerFactory>> m_versionControllerServer;
QScopedPointer<NameBasedServer<IVersionControllerFactory>> m_versionControllerServer;
QSharedPointer<NameBasedServer<INotebookConfigMgrFactory>> m_configMgrServer;
QScopedPointer<NameBasedServer<INotebookConfigMgrFactory>> m_configMgrServer;
QSharedPointer<NameBasedServer<INotebookBackendFactory>> m_backendServer;
QScopedPointer<NameBasedServer<INotebookBackendFactory>> m_backendServer;
QSharedPointer<NameBasedServer<INotebookFactory>> m_notebookServer;
QScopedPointer<NameBasedServer<INotebookFactory>> m_notebookServer;
QVector<QSharedPointer<Notebook>> m_notebooks;

View File

@ -21,9 +21,9 @@
<key>CFBundleExecutable</key>
<string>vnote</string>
<key>CFBundleShortVersionString</key>
<string>3.0.0</string>
<string>3.5.1</string>
<key>CFBundleVersion</key>
<string>3.0.0.3</string>
<string>3.5.1.1</string>
<key>NSHumanReadableCopyright</key>
<string>Created by VNoteX</string>
<key>CFBundleIconFile</key>

View File

@ -22,6 +22,7 @@
<file>icons/settings.svg</file>
<file>icons/view.svg</file>
<file>icons/inplace_preview_editor.svg</file>
<file>icons/image_host_editor.svg</file>
<file>icons/settings_menu.svg</file>
<file>icons/whatsthis.svg</file>
<file>icons/help_menu.svg</file>

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627979077351" class="icon" viewBox="0 0 1127 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8220" width="563.5" height="512" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M51.2 102.4l1024 0 0 51.2-1024 0 0-51.2Z" p-id="8221" fill="#000000"></path><path d="M102.4 0l921.6 0 0 51.2-921.6 0 0-51.2Z" p-id="8222" fill="#000000"></path><path d="M763.3408 602.368c-4.4032-6.912-13.2608-8.5504-19.8656-3.8912l-141.4656 99.4816c-3.2768 2.304-7.7312 1.3824-9.7792-2.1504L425.5232 407.8592C421.5296 400.896 415.1296 400.8448 411.392 408.0128L102.2976 901.632c-3.7376 7.2192-0.3072 13.0048 7.6288 13.0048l903.9872 0c7.68 0 10.8032-5.5296 6.4512-12.3392L763.3408 602.368z" p-id="8223" fill="#000000"></path><path d="M896 384m-76.8 0a1.5 1.5 0 1 0 153.6 0 1.5 1.5 0 1 0-153.6 0Z" p-id="8224" fill="#000000"></path><path d="M0 211.4048l0 805.9904C0 1021.0304 3.2256 1024 7.2192 1024L1119.232 1024C1123.1744 1024 1126.4 1021.0304 1126.4 1017.3952L1126.4 211.4048C1126.4 207.7696 1123.1744 204.8 1119.232 204.8L7.2192 204.8C3.2256 204.8 0 207.7696 0 211.4048zM51.2 256l1024 0 0 716.8L51.2 972.8 51.2 256z" p-id="8225" fill="#000000"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -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" : {

View File

@ -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`

View File

@ -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.

View File

@ -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`

View File

@ -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) 。

View File

@ -0,0 +1,204 @@
#include "githubimagehost.h"
#include <QDebug>
#include <QFileInfo>
#include <QByteArray>
#include <utils/utils.h>
#include <utils/webutils.h>
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<QByteArray, QByteArray> GitHubImageHost::authorizationHeader(const QString &p_token)
{
auto token = "token " + p_token;
return qMakePair(QByteArray("Authorization"), token.toUtf8());
}
QPair<QByteArray, QByteArray> 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;
}

View File

@ -0,0 +1,61 @@
#ifndef GITHUBIMAGEHOST_H
#define GITHUBIMAGEHOST_H
#include "imagehost.h"
#include <vtextedit/networkutils.h>
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<QByteArray, QByteArray> authorizationHeader(const QString &p_token);
static QPair<QByteArray, QByteArray> 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

View File

@ -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");
}
}

57
src/imagehost/imagehost.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef IMAGEHOST_H
#define IMAGEHOST_H
#include <QObject>
#include <QJsonObject>
#include <core/global.h>
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

View File

@ -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

View File

@ -0,0 +1,198 @@
#include "imagehostmgr.h"
#include <QDebug>
#include <core/configmgr.h>
#include <core/editorconfig.h>
#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<ImageHost::Type>(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<EditorConfig::ImageHostItem> items;
items.resize(m_hosts.size());
for (int i = 0; i < m_hosts.size(); ++i) {
items[i].m_type = static_cast<int>(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<ImageHost *> &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;
}

View File

@ -0,0 +1,61 @@
#ifndef IMAGEHOSTMGR_H
#define IMAGEHOSTMGR_H
#include <QObject>
#include <QScopedPointer>
#include <core/noncopyable.h>
#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<ImageHost *> &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<ImageHost *> m_hosts;
ImageHost *m_defaultHost = nullptr;
};
}
#endif // IMAGEHOSTMGR_H

View File

@ -0,0 +1,29 @@
#include "imagehostutils.h"
#include <buffer/buffer.h>
#include <notebook/node.h>
#include <notebook/notebook.h>
#include <notebookbackend/inotebookbackend.h>
#include <utils/pathutils.h>
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;
}

View File

@ -0,0 +1,24 @@
#ifndef IMAGEHOSTUTILS_H
#define IMAGEHOSTUTILS_H
#include <QString>
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

View File

@ -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)

View File

@ -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;
}

View File

@ -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;

View File

@ -9,6 +9,8 @@
#include <QRegularExpression>
#include <QSvgRenderer>
#include <QPainter>
#include <QJsonObject>
#include <QJsonDocument>
#include <cmath>
@ -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();
}

View File

@ -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

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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<int>::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);
}
}

View File

@ -41,6 +41,8 @@ namespace vnotex
QString getTemplateContent() const;
void updateCurrentTemplate();
NodeInfoWidget *m_infoWidget = nullptr;
QComboBox *m_templateComboBox = nullptr;

View File

@ -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.

View File

@ -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);
}

View File

@ -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

View File

@ -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();

View File

@ -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

View File

@ -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();

View File

@ -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

View File

@ -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();

View File

@ -0,0 +1,295 @@
#include "imagehostpage.h"
#include <QFormLayout>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QComboBox>
#include <QCheckBox>
#include <widgets/widgetsfactory.h>
#include <core/editorconfig.h>
#include <core/configmgr.h>
#include <utils/widgetutils.h>
#include <imagehost/imagehostmgr.h>
#include <widgets/messageboxhelper.h>
#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<int>::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<QGroupBox *>(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<QGroupBox *>(fields[0]->parent());
Q_ASSERT(box);
auto nameLineEdit = box->findChild<QLineEdit *>(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<QGroupBox *>(p_hostName, Qt::FindDirectChildrenOnly);
Q_ASSERT(box);
m_mainLayout->removeWidget(box);
box->deleteLater();
}
QJsonObject ImageHostPage::fieldsToConfig(const QVector<QLineEdit *> &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);
}

View File

@ -0,0 +1,60 @@
#ifndef IMAGEHOSTPAGE_H
#define IMAGEHOSTPAGE_H
#include "settingspage.h"
#include <QVector>
#include <QMap>
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<QLineEdit *> &p_fields) const;
void testImageHost(const QString &p_hostName);
QGroupBox *setupGeneralBox(QWidget *p_parent);
QVBoxLayout *m_mainLayout = nullptr;
// [host] -> list of related fields.
QMap<ImageHost *, QVector<QLineEdit *>> m_hostToFields;
QComboBox *m_defaultImageHostComboBox = nullptr;
QCheckBox *m_clearObsoleteImageCheckBox = nullptr;
};
}
#endif // IMAGEHOSTPAGE_H

View File

@ -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

View File

@ -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();

View File

@ -25,9 +25,9 @@ void MiscPage::loadInternal()
}
void MiscPage::saveInternal()
bool MiscPage::saveInternal()
{
return true;
}
QString MiscPage::title() const

View File

@ -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();

View File

@ -0,0 +1,88 @@
#include "newimagehostdialog.h"
#include <QFormLayout>
#include <QComboBox>
#include <QLineEdit>
#include <QLabel>
#include <widgets/widgetsfactory.h>
#include <imagehost/imagehostmgr.h>
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<int>(ImageHost::GitHub); type < static_cast<int>(ImageHost::MaxHost); ++type) {
m_typeComboBox->addItem(ImageHost::typeString(static_cast<ImageHost::Type>(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<ImageHost::Type>(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;
}

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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<void(SettingsPage *)> &p_func)
void SettingsDialog::forEachPage(const std::function<bool(SettingsPage *)> &p_func)
{
for (int i = 0; i < m_pageLayout->count(); ++i) {
auto page = dynamic_cast<SettingsPage *>(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);
}
}

View File

@ -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<void(SettingsPage *)> &p_func);
// @p_func: return true to continue the iteration.
void forEachPage(const std::function<bool(SettingsPage *)> &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;
};
}

View File

@ -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;
}

View File

@ -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<SearchItem> m_searchItems;
bool m_changed = false;
QString m_error;
};
}

View File

@ -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

View File

@ -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();

View File

@ -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

View File

@ -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();

View File

@ -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);

View File

@ -14,6 +14,7 @@
#include <QProgressDialog>
#include <QTemporaryFile>
#include <QTimer>
#include <QBuffer>
#include <vtextedit/markdowneditorconfig.h>
#include <vtextedit/previewmgr.h>
@ -44,6 +45,9 @@
#include <core/configmgr.h>
#include <core/editorconfig.h>
#include <core/vnotex.h>
#include <imagehost/imagehostutils.h>
#include <imagehost/imagehost.h>
#include <imagehost/imagehostmgr.h>
#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::Heading> &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<QAction *>(sender());
auto host = ImageHostMgr::getInst().find(act->data().toString());
Q_ASSERT(host);
}

View File

@ -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;
};
}

View File

@ -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);

View File

@ -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<QMenu> 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<QMenu> 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));
}
}

View File

@ -38,6 +38,8 @@ namespace vnotex
// Resize the first column.
void resizeTreeToContents();
void handleContextMenuRequested(const QPoint &p_pos);
private:
enum Action {
Open,

View File

@ -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();
}

View File

@ -7,6 +7,8 @@
#include <QCoreApplication>
#include <QScrollBar>
#include <QLabel>
#include <QApplication>
#include <QProgressDialog>
#include <core/fileopenparameters.h>
#include <core/editorconfig.h>
@ -19,6 +21,8 @@
#include <buffer/markdownbuffer.h>
#include <core/vnotex.h>
#include <core/thememgr.h>
#include <imagehost/imagehostmgr.h>
#include <imagehost/imagehost.h>
#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<QPair<QString, bool>> imagesToDelete;
imagesToDelete.reserve(obsoleteImages.size());
if (markdownEditorConfig.getConfirmBeforeClearObsoleteImages()) {
QVector<ConfirmItemInfo> 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<void *>(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<OutlineProvider> 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);
}
}

View File

@ -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 <class T>
static QSharedPointer<Outline> headingsToOutline(const QVector<T> &p_headings);
@ -184,6 +189,8 @@ namespace vnotex
ViewWindowMode m_previousMode = ViewWindowMode::Invalid;
QSharedPointer<OutlineProvider> m_outlineProvider;
ImageHost *m_imageHost = nullptr;
};
}

View File

@ -14,6 +14,7 @@
#include <QShortcut>
#include <QWheelEvent>
#include <QWidgetAction>
#include <QActionGroup>
#include <core/fileopenparameters.h>
#include "toolbarhelper.h"
@ -23,6 +24,7 @@
#include <utils/widgetutils.h>
#include <core/configmgr.h>
#include <core/editorconfig.h>
#include <imagehost/imagehostmgr.h>
#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<QToolButton *>(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);
}

View File

@ -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<FileOpenParameters> &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;
};

View File

@ -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<QToolButton *>(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;

View File

@ -44,7 +44,8 @@ namespace vnotex
Outline,
FindAndReplace,
SectionNumber,
InplacePreview
InplacePreview,
ImageHost
};
static QAction *addAction(QToolBar *p_tb, Action p_action);

View File

@ -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 \

View File

@ -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