support external programs

This commit is contained in:
Le Tan 2021-06-18 20:46:30 +08:00
parent b04b23d04b
commit ff79533fa0
21 changed files with 423 additions and 21 deletions

View File

@ -51,6 +51,7 @@ namespace vnotex
ActivateNextTab,
ActivatePreviousTab,
FocusContentArea,
OpenWithDefaultProgram,
MaxShortcut
};
Q_ENUM(Shortcut)

View File

@ -37,6 +37,24 @@ QJsonObject SessionConfig::NotebookItem::toJson() const
return jobj;
}
void SessionConfig::ExternalProgram::fromJson(const QJsonObject &p_jobj)
{
m_name = p_jobj[QStringLiteral("name")].toString();
m_command = p_jobj[QStringLiteral("command")].toString();
m_shortcut = p_jobj[QStringLiteral("shortcut")].toString();
}
QJsonObject SessionConfig::ExternalProgram::toJson() const
{
QJsonObject jobj;
jobj[QStringLiteral("name")] = m_name;
jobj[QStringLiteral("command")] = m_command;
jobj[QStringLiteral("shortcut")] = m_shortcut;
return jobj;
}
SessionConfig::SessionConfig(ConfigMgr *p_mgr)
: IConfig(p_mgr, nullptr)
{
@ -57,10 +75,6 @@ void SessionConfig::init()
loadStateAndGeometry(sessionJobj);
if (MainConfig::isVersionChanged()) {
doVersionSpecificOverride();
}
m_exportOption.fromJson(sessionJobj[QStringLiteral("export_option")].toObject());
m_searchOption.fromJson(sessionJobj[QStringLiteral("search_option")].toObject());
@ -68,6 +82,14 @@ void SessionConfig::init()
m_viewAreaSession = readByteArray(sessionJobj, QStringLiteral("viewarea_session"));
m_notebookExplorerSession = readByteArray(sessionJobj, QStringLiteral("notebook_explorer_session"));
loadExternalPrograms(sessionJobj);
loadNotebooks(sessionJobj);
if (MainConfig::isVersionChanged()) {
doVersionSpecificOverride();
}
}
void SessionConfig::loadCore(const QJsonObject &p_session)
@ -128,14 +150,8 @@ void SessionConfig::setNewNotebookDefaultRootFolderPath(const QString &p_path)
this);
}
const QVector<SessionConfig::NotebookItem> &SessionConfig::getNotebooks()
const QVector<SessionConfig::NotebookItem> &SessionConfig::getNotebooks() const
{
if (m_notebooks.isEmpty()) {
auto mgr = getMgr();
auto sessionSettings = mgr->getSettings(ConfigMgr::Source::Session);
const auto &sessionJobj = sessionSettings->getJson();
loadNotebooks(sessionJobj);
}
return m_notebooks;
}
@ -191,6 +207,7 @@ QJsonObject SessionConfig::toJson() const
obj[QStringLiteral("search_option")] = m_searchOption.toJson();
writeByteArray(obj, QStringLiteral("viewarea_session"), m_viewAreaSession);
writeByteArray(obj, QStringLiteral("notebook_explorer_session"), m_notebookExplorerSession);
obj[QStringLiteral("external_programs")] = saveExternalPrograms();
return obj;
}
@ -365,3 +382,26 @@ void SessionConfig::setQuickAccessFiles(const QStringList &p_files)
{
updateConfig(m_quickAccessFiles, p_files, this);
}
void SessionConfig::loadExternalPrograms(const QJsonObject &p_session)
{
const auto arr = p_session.value(QStringLiteral("external_programs")).toArray();
m_externalPrograms.resize(arr.size());
for (int i = 0; i < arr.size(); ++i) {
m_externalPrograms[i].fromJson(arr[i].toObject());
}
}
QJsonArray SessionConfig::saveExternalPrograms() const
{
QJsonArray arr;
for (const auto &pro : m_externalPrograms) {
arr.append(pro.toJson());
}
return arr;
}
const QVector<SessionConfig::ExternalProgram> &SessionConfig::getExternalPrograms() const
{
return m_externalPrograms;
}

View File

@ -53,6 +53,20 @@ namespace vnotex
Software
};
struct ExternalProgram
{
void fromJson(const QJsonObject &p_jobj);
QJsonObject toJson() const;
QString m_name;
// %1: the file paths to open.
QString m_command;
QString m_shortcut;
};
explicit SessionConfig(ConfigMgr *p_mgr);
~SessionConfig();
@ -65,7 +79,7 @@ namespace vnotex
const QString &getCurrentNotebookRootFolderPath() const;
void setCurrentNotebookRootFolderPath(const QString &p_path);
const QVector<SessionConfig::NotebookItem> &getNotebooks();
const QVector<SessionConfig::NotebookItem> &getNotebooks() const;
void setNotebooks(const QVector<SessionConfig::NotebookItem> &p_notebooks);
void writeToSettings() const Q_DECL_OVERRIDE;
@ -107,6 +121,8 @@ namespace vnotex
const QStringList &getQuickAccessFiles() const;
void setQuickAccessFiles(const QStringList &p_files);
const QVector<ExternalProgram> &getExternalPrograms() const;
private:
void loadCore(const QJsonObject &p_session);
@ -120,6 +136,10 @@ namespace vnotex
QJsonObject saveStateAndGeometry() const;
void loadExternalPrograms(const QJsonObject &p_session);
QJsonArray saveExternalPrograms() const;
void doVersionSpecificOverride();
QString m_newNotebookDefaultRootFolderPath;
@ -153,6 +173,8 @@ namespace vnotex
QString m_flashPage;
QStringList m_quickAccessFiles;
QVector<ExternalProgram> m_externalPrograms;
};
} // ns vnotex

View File

@ -44,7 +44,8 @@
"AlternateTab" : "Ctrl+G, 0",
"ActivateNextTab" : "Ctrl+G, N",
"ActivatePreviousTab" : "Ctrl+G, P",
"FocusContentArea" : "Ctrl+G, Y"
"FocusContentArea" : "Ctrl+G, Y",
"OpenWithDefaultProgram" : "F9"
},
"toolbar_icon_size" : 16,
"note_management" : {

View File

@ -0,0 +1,30 @@
# External Programs
VNote allows user to open notes with **external programs** via the `Open With` in the context menu of the node explorer.
To add custom external programs, user needs to edit the session configuration. A sample may look like this:
```json
{
"external_programs": [
{
"name" : "gvim",
"command" : "C:\\\"Program Files (x86)\"\\Vim\\vim80\\gvim.exe %1",
"shortcut" : "F4"
},
{
"name" : "notepad",
"command" : "notepad %1",
"shortcut" : ""
}
]
}
```
An external program could have 3 properties:
1. `name`: the name of the program in VNote;
2. `command`: the command to execute when opening notes with this external program;
1. Use `%1` as a placeholder which will be replaced by the real file paths (automatically wrapped by double quotes);
3. `shortcut`: the shortcut assigned to this external program;
Close VNote before editting the session configuration.

View File

@ -0,0 +1,30 @@
# 外部程序
VNote 支持通过在节点浏览器上下文菜单中的 `打开方式` 来调用 **外部程序** 打开笔记。
用户需要编辑会话配置来添加自定义外部程序。一个例子如下:
```json
{
"external_programs": [
{
"name" : "gvim",
"command" : "C:\\\"Program Files (x86)\"\\Vim\\vim80\\gvim.exe %1",
"shortcut" : "F4"
},
{
"name" : "notepad",
"command" : "notepad %1",
"shortcut" : ""
}
]
}
```
一个外部程序可以包含3个属性
1. `name`: 该程序在 VNote 中的名字;
2. `command`: 当使用该外部程序打开笔记时执行的命令;
1. 使用 `%1` 占位符,会被替换为真实的文件路径(自动加上双引号包裹)
3. `shortcut`: 分配给该外部程序的快捷键;
修改配置前请关闭 VNote。

View File

@ -4,10 +4,12 @@
<file>docs/en/about_vnotex.txt</file>
<file>docs/en/shortcuts.md</file>
<file>docs/en/markdown_guide.md</file>
<file>docs/en/external_programs.md</file>
<file>docs/zh_CN/get_started.txt</file>
<file>docs/zh_CN/about_vnotex.txt</file>
<file>docs/zh_CN/shortcuts.md</file>
<file>docs/zh_CN/markdown_guide.md</file>
<file>docs/zh_CN/external_programs.md</file>
<file>web/markdown-viewer-template.html</file>
<file>web/markdown-export-template.html</file>
<file>web/css/globalstyles.css</file>

View File

@ -333,7 +333,9 @@ void WebViewExporter::prepareWkhtmltopdfArguments(const ExportPdfOption &p_pdfOp
// Must be put after the global object options.
if (p_pdfOption.m_addTableOfContents) {
m_wkhtmltopdfArgs << "toc" << "--toc-text-size-shrink" << "1.0";
m_wkhtmltopdfArgs << "toc";
m_wkhtmltopdfArgs << "--toc-text-size-shrink" << "1.0";
m_wkhtmltopdfArgs << "--toc-header-text" << tr("Table of Contents");
}
}

View File

@ -156,3 +156,12 @@ ProcessUtils::State ProcessUtils::start(const QString &p_program,
return proc.exitStatus() == QProcess::NormalExit ? State::Succeeded : State::Crashed;
}
void ProcessUtils::startDetached(const QString &p_command)
{
Q_ASSERT(!p_command.isEmpty());
auto process = new QProcess();
QObject::connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
process, &QProcess::deleteLater);
process->start(p_command);
}

View File

@ -35,6 +35,8 @@ namespace vnotex
const std::function<void(const QString &)> &p_logger,
const bool &p_askedToStop);
static void startDetached(const QString &p_command);
// Copied from QProcess code.
static QStringList parseCombinedArgString(const QString &p_args);

View File

@ -175,6 +175,10 @@ void WidgetUtils::addActionShortcut(QAction *p_action,
void WidgetUtils::addActionShortcutText(QAction *p_action,
const QString &p_shortcut)
{
if (p_shortcut.isEmpty()) {
return;
}
QKeySequence kseq(p_shortcut);
if (kseq.isEmpty()) {
return;

View File

@ -612,7 +612,7 @@ QWidget *ExportDialog::getPdfAdvancedSettings()
}
{
m_addTableOfContentsCheckBox = WidgetsFactory::createCheckBox(tr("Add Table-Of-Contents"), widget);
m_addTableOfContentsCheckBox = WidgetsFactory::createCheckBox(tr("Add Table-of-Contents"), widget);
layout->addRow(m_addTableOfContentsCheckBox);
}

View File

@ -0,0 +1,36 @@
#include "miscpage.h"
#include <QFormLayout>
#include <widgets/widgetsfactory.h>
#include <core/coreconfig.h>
#include <core/sessionconfig.h>
#include <core/configmgr.h>
#include <utils/widgetutils.h>
using namespace vnotex;
MiscPage::MiscPage(QWidget *p_parent)
: SettingsPage(p_parent)
{
setupUI();
}
void MiscPage::setupUI()
{
}
void MiscPage::loadInternal()
{
}
void MiscPage::saveInternal()
{
}
QString MiscPage::title() const
{
return tr("Misc");
}

View File

@ -0,0 +1,26 @@
#ifndef MISCPAGE_H
#define MISCPAGE_H
#include "settingspage.h"
namespace vnotex
{
class MiscPage : public SettingsPage
{
Q_OBJECT
public:
explicit MiscPage(QWidget *p_parent = nullptr);
QString title() const Q_DECL_OVERRIDE;
protected:
void loadInternal() Q_DECL_OVERRIDE;
void saveInternal() Q_DECL_OVERRIDE;
private:
void setupUI();
};
}
#endif // MISCPAGE_H

View File

@ -10,6 +10,7 @@
#include <widgets/widgetsfactory.h>
#include "generalpage.h"
#include "miscpage.h"
#include "editorpage.h"
#include "texteditorpage.h"
#include "markdowneditorpage.h"
@ -123,6 +124,14 @@ void SettingsDialog::setupPages()
}
}
// Misc.
{
/*
auto page = new MiscPage(this);
addPage(page);
*/
}
setChangesUnsaved(false);
m_pageExplorer->setCurrentItem(m_pageExplorer->topLevelItem(0), 0, QItemSelectionModel::ClearAndSelect);
m_pageExplorer->expandAll();

View File

@ -7,6 +7,7 @@
#include <QMenu>
#include <QAction>
#include <QSet>
#include <QShortcut>
#include <notebook/notebook.h>
#include <notebook/node.h>
@ -16,6 +17,8 @@
#include "vnotex.h"
#include "mainwindow.h"
#include <utils/iconutils.h>
#include <utils/docsutils.h>
#include <utils/processutils.h>
#include "treewidget.h"
#include "dialogs/notepropertiesdialog.h"
#include "dialogs/folderpropertiesdialog.h"
@ -31,6 +34,8 @@
#include <core/fileopenparameters.h>
#include <core/events.h>
#include <core/configmgr.h>
#include <core/coreconfig.h>
#include <core/sessionconfig.h>
using namespace vnotex;
@ -177,6 +182,8 @@ NotebookNodeExplorer::NotebookNodeExplorer(QWidget *p_parent)
initNodeIcons();
setupUI();
setupShortcuts();
}
void NotebookNodeExplorer::initNodeIcons() const
@ -793,6 +800,10 @@ void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_
act = createAction(Action::Open, p_menu);
p_menu->addAction(act);
addOpenWithMenu(p_menu);
p_menu->addSeparator();
if (selectedSize == 1 && p_node->isContainer()) {
act = createAction(Action::ExpandAll, p_menu);
p_menu->addAction(act);
@ -827,6 +838,10 @@ void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_
act = createAction(Action::Open, p_menu);
p_menu->addAction(act);
addOpenWithMenu(p_menu);
p_menu->addSeparator();
if (selectedSize == 1 && p_node->isContainer()) {
act = createAction(Action::ExpandAll, p_menu);
p_menu->addAction(act);
@ -902,6 +917,10 @@ void NotebookNodeExplorer::createContextMenuOnExternalNode(QMenu *p_menu, const
act = createAction(Action::Open, p_menu);
p_menu->addAction(act);
addOpenWithMenu(p_menu);
p_menu->addSeparator();
act = createAction(Action::ImportToConfig, p_menu);
p_menu->addAction(act);
@ -1831,6 +1850,27 @@ void NotebookNodeExplorer::openSelectedNodes()
}
}
QStringList NotebookNodeExplorer::getSelectedNodesPath() const
{
QStringList files;
// Support nodes and external nodes.
auto selectedNodes = getSelectedNodes();
for (const auto &externalNode : selectedNodes.second) {
files << externalNode->fetchAbsolutePath();
}
for (const auto &node : selectedNodes.first) {
if (checkInvalidNode(node)) {
continue;
}
files << node->fetchAbsolutePath();
}
return files;
}
QSharedPointer<Node> NotebookNodeExplorer::importToIndex(const ExternalNode *p_node)
{
auto node = m_notebook->addAsNode(p_node->getNode(),
@ -1914,3 +1954,93 @@ void NotebookNodeExplorer::expandItemRecursively(QTreeWidgetItem *p_item)
expandItemRecursively(p_item->child(i));
}
}
void NotebookNodeExplorer::addOpenWithMenu(QMenu *p_menu)
{
auto subMenu = p_menu->addMenu(tr("Open &With"));
{
const auto &sessionConfig = ConfigMgr::getInst().getSessionConfig();
for (const auto &pro : sessionConfig.getExternalPrograms()) {
QAction *act = subMenu->addAction(pro.m_name);
connect(act, &QAction::triggered,
this, [this, act]() {
openSelectedNodesWithExternalProgram(act->data().toString());
});
act->setData(pro.m_command);
WidgetUtils::addActionShortcutText(act, pro.m_shortcut);
}
}
subMenu->addSeparator();
{
auto defaultAct = subMenu->addAction(tr("System Default Program"),
this,
&NotebookNodeExplorer::openSelectedNodesWithDefaultProgram);
const auto &coreConfig = ConfigMgr::getInst().getCoreConfig();
WidgetUtils::addActionShortcutText(defaultAct, coreConfig.getShortcut(CoreConfig::OpenWithDefaultProgram));
}
subMenu->addAction(tr("Add External Program"),
this,
[]() {
const auto file = DocsUtils::getDocFile(QStringLiteral("external_programs.md"));
if (!file.isEmpty()) {
auto paras = QSharedPointer<FileOpenParameters>::create();
paras->m_readOnly = true;
emit VNoteX::getInst().openFileRequested(file, paras);
}
});
}
void NotebookNodeExplorer::setupShortcuts()
{
const auto &coreConfig = ConfigMgr::getInst().getCoreConfig();
// OpenWithDefaultProgram.
{
auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::OpenWithDefaultProgram), this);
if (shortcut) {
connect(shortcut, &QShortcut::activated,
this, &NotebookNodeExplorer::openSelectedNodesWithDefaultProgram);
}
}
const auto &sessionConfig = ConfigMgr::getInst().getSessionConfig();
for (const auto &pro : sessionConfig.getExternalPrograms()) {
auto shortcut = WidgetUtils::createShortcut(pro.m_shortcut, this);
const auto &command = pro.m_command;
if (shortcut) {
connect(shortcut, &QShortcut::activated,
this, [this, command]() {
openSelectedNodesWithExternalProgram(command);
});
}
}
}
void NotebookNodeExplorer::openSelectedNodesWithDefaultProgram()
{
const auto files = getSelectedNodesPath();
for (const auto &file : files) {
if (file.isEmpty()) {
continue;
}
WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(file));
}
}
void NotebookNodeExplorer::openSelectedNodesWithExternalProgram(const QString &p_command)
{
const auto files = getSelectedNodesPath();
for (const auto &file : files) {
if (file.isEmpty()) {
continue;
}
auto command = p_command;
command.replace(QStringLiteral("%1"), QString("\"%1\"").arg(file));
ProcessUtils::startDetached(command);
}
}

View File

@ -155,6 +155,8 @@ namespace vnotex
void setupUI();
void setupShortcuts();
void setupMasterExplorer(QWidget *p_parent = nullptr);
void clearExplorer();
@ -263,6 +265,14 @@ namespace vnotex
void expandItemRecursively(QTreeWidgetItem *p_item);
void addOpenWithMenu(QMenu *p_menu);
QStringList getSelectedNodesPath() const;
void openSelectedNodesWithDefaultProgram();
void openSelectedNodesWithExternalProgram(const QString &p_command);
static NotebookNodeExplorer::NodeData getItemNodeData(const QTreeWidgetItem *p_item);
static void setItemNodeData(QTreeWidgetItem *p_item, const NodeData &p_data);

View File

@ -569,6 +569,25 @@ void ViewSplit::createContextMenuOnTabBar(QMenu *p_menu, int p_tabIdx) const
p_menu->addSeparator();
{
auto act = p_menu->addAction(tr("Auto Reload"));
act->setToolTip(tr("Reload file from disk automatically if it is changed outside"));
act->setCheckable(true);
auto win = getViewWindow(p_tabIdx);
Q_ASSERT(win);
act->setChecked(win->getWindowFlags() & ViewWindow::AutoReload);
connect(act, &QAction::triggered,
this, [win](bool p_checked) {
if (p_checked) {
win->setWindowFlags(win->getWindowFlags() | ViewWindow::AutoReload);
} else {
win->setWindowFlags(win->getWindowFlags() & ~ViewWindow::AutoReload);
}
});
}
p_menu->addSeparator();
// Copy Path.
p_menu->addAction(tr("Copy Path"),
[this, p_tabIdx]() {

View File

@ -755,12 +755,15 @@ int ViewWindow::checkFileMissingOrChangedOutside()
return Failed;
}
} else if (m_buffer->checkFileChangedOutside()) {
int ret = MessageBoxHelper::questionSaveDiscardCancel(MessageBoxHelper::Warning,
tr("File is changed from outside (%1).").arg(m_buffer->getPath()),
tr("Do you want to save the buffer to the file to override, or discard the buffer?"),
tr("The file is changed from outside. Please choose to save the buffer to the file or "
"just discard the buffer and reload the file."),
this);
int ret = QMessageBox::Discard;
if (!(getWindowFlags() & WindowFlag::AutoReload)) {
ret = MessageBoxHelper::questionSaveDiscardCancel(MessageBoxHelper::Warning,
tr("File is changed from outside (%1).").arg(m_buffer->getPath()),
tr("Do you want to save the buffer to the file to override, or discard the buffer?"),
tr("The file is changed from outside. Please choose to save the buffer to the file or "
"just discard the buffer and reload the file."),
this);
}
switch (ret) {
case QMessageBox::Save:
if (!save(true)) {
@ -1095,3 +1098,13 @@ ViewWindowSession ViewWindow::saveSession() const
session.m_viewWindowMode = getMode();
return session;
}
ViewWindow::WindowFlags ViewWindow::getWindowFlags() const
{
return m_flags;
}
void ViewWindow::setWindowFlags(WindowFlags p_flags)
{
m_flags = p_flags;
}

View File

@ -30,6 +30,13 @@ namespace vnotex
{
Q_OBJECT
public:
enum WindowFlag
{
None = 0,
AutoReload = 0x1
};
Q_DECLARE_FLAGS(WindowFlags, WindowFlag);
explicit ViewWindow(QWidget *p_parent = nullptr);
virtual ~ViewWindow();
@ -74,6 +81,9 @@ namespace vnotex
virtual ViewWindowSession saveSession() const;
WindowFlags getWindowFlags() const;
void setWindowFlags(WindowFlags p_flags);
public slots:
virtual void handleEditorConfigChange() = 0;
@ -320,9 +330,13 @@ namespace vnotex
EditReadDiscardAction *m_editReadDiscardAct = nullptr;
WindowFlags m_flags = WindowFlag::None;
static QIcon s_savedIcon;
static QIcon s_modifiedIcon;
};
} // ns vnotex
Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::ViewWindow::WindowFlags)
#endif // VIEWWINDOW_H

View File

@ -19,6 +19,7 @@ SOURCES += \
$$PWD/dialogs/settings/editorpage.cpp \
$$PWD/dialogs/settings/generalpage.cpp \
$$PWD/dialogs/settings/markdowneditorpage.cpp \
$$PWD/dialogs/settings/miscpage.cpp \
$$PWD/dialogs/settings/quickaccesspage.cpp \
$$PWD/dialogs/settings/settingspage.cpp \
$$PWD/dialogs/settings/settingsdialog.cpp \
@ -117,6 +118,7 @@ HEADERS += \
$$PWD/dialogs/settings/editorpage.h \
$$PWD/dialogs/settings/generalpage.h \
$$PWD/dialogs/settings/markdowneditorpage.h \
$$PWD/dialogs/settings/miscpage.h \
$$PWD/dialogs/settings/quickaccesspage.h \
$$PWD/dialogs/settings/settingspage.h \
$$PWD/dialogs/settings/settingsdialog.h \