mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-04 21:39:52 +08:00
support Image Host
This commit is contained in:
parent
30a9d0ecc7
commit
f1d931c276
@ -1 +1 @@
|
||||
Subproject commit 922084a388e1f135e25297ba84a9d0ca0078ed06
|
||||
Subproject commit c53fc8dbf6df14b9e1327de0a4907700ffee6049
|
@ -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='')
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
1
src/data/core/icons/image_host_editor.svg
Normal file
1
src/data/core/icons/image_host_editor.svg
Normal 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 |
@ -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" : {
|
||||
|
@ -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`
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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`
|
||||
|
@ -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) 。
|
||||
|
||||
|
204
src/imagehost/githubimagehost.cpp
Normal file
204
src/imagehost/githubimagehost.cpp
Normal 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;
|
||||
}
|
61
src/imagehost/githubimagehost.h
Normal file
61
src/imagehost/githubimagehost.h
Normal 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
|
30
src/imagehost/imagehost.cpp
Normal file
30
src/imagehost/imagehost.cpp
Normal 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
57
src/imagehost/imagehost.h
Normal 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
|
14
src/imagehost/imagehost.pri
Normal file
14
src/imagehost/imagehost.pri
Normal 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
|
||||
|
198
src/imagehost/imagehostmgr.cpp
Normal file
198
src/imagehost/imagehostmgr.cpp
Normal 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;
|
||||
}
|
61
src/imagehost/imagehostmgr.h
Normal file
61
src/imagehost/imagehostmgr.h
Normal 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
|
29
src/imagehost/imagehostutils.cpp
Normal file
29
src/imagehost/imagehostutils.cpp
Normal 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;
|
||||
}
|
24
src/imagehost/imagehostutils.h
Normal file
24
src/imagehost/imagehostutils.h
Normal 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
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,8 @@ namespace vnotex
|
||||
|
||||
QString getTemplateContent() const;
|
||||
|
||||
void updateCurrentTemplate();
|
||||
|
||||
NodeInfoWidget *m_infoWidget = nullptr;
|
||||
|
||||
QComboBox *m_templateComboBox = nullptr;
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
295
src/widgets/dialogs/settings/imagehostpage.cpp
Normal file
295
src/widgets/dialogs/settings/imagehostpage.cpp
Normal 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);
|
||||
}
|
60
src/widgets/dialogs/settings/imagehostpage.h
Normal file
60
src/widgets/dialogs/settings/imagehostpage.h
Normal 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
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -25,9 +25,9 @@ void MiscPage::loadInternal()
|
||||
|
||||
}
|
||||
|
||||
void MiscPage::saveInternal()
|
||||
bool MiscPage::saveInternal()
|
||||
{
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString MiscPage::title() const
|
||||
|
@ -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();
|
||||
|
88
src/widgets/dialogs/settings/newimagehostdialog.cpp
Normal file
88
src/widgets/dialogs/settings/newimagehostdialog.cpp
Normal 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;
|
||||
}
|
39
src/widgets/dialogs/settings/newimagehostdialog.h
Normal file
39
src/widgets/dialogs/settings/newimagehostdialog.h
Normal 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
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,8 @@ namespace vnotex
|
||||
// Resize the first column.
|
||||
void resizeTreeToContents();
|
||||
|
||||
void handleContextMenuRequested(const QPoint &p_pos);
|
||||
|
||||
private:
|
||||
enum Action {
|
||||
Open,
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -44,7 +44,8 @@ namespace vnotex
|
||||
Outline,
|
||||
FindAndReplace,
|
||||
SectionNumber,
|
||||
InplacePreview
|
||||
InplacePreview,
|
||||
ImageHost
|
||||
};
|
||||
|
||||
static QAction *addAction(QToolBar *p_tb, Action p_action);
|
||||
|
@ -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 \
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user