From 911392deabb92a7f407da7152f4e1898b2dc48e0 Mon Sep 17 00:00:00 2001 From: tootal Date: Thu, 9 Dec 2021 18:40:14 +0800 Subject: [PATCH] Feature Task System (#1947) * add task system code * enable ci build * Revert "enable ci build" This reverts commit 8c457a22e44e64c7d87804fc3c76ee778c1c3b6f. --- src/core/configmgr.cpp | 18 + src/core/configmgr.h | 4 + src/core/core.pri | 10 + src/core/shellexecution.cpp | 102 ++++ src/core/shellexecution.h | 42 ++ src/core/task.cpp | 557 ++++++++++++++++++++ src/core/task.h | 185 +++++++ src/core/taskhelper.cpp | 233 ++++++++ src/core/taskhelper.h | 50 ++ src/core/taskmgr.cpp | 192 +++++++ src/core/taskmgr.h | 79 +++ src/core/taskvariablemgr.cpp | 334 ++++++++++++ src/core/taskvariablemgr.h | 73 +++ src/core/vnotex.cpp | 14 + src/core/vnotex.h | 11 + src/data/core/core.qrc | 1 + src/data/core/icons/task_menu.svg | 1 + src/data/extra/extra.qrc | 8 + src/data/extra/tasks/git/commit.svg | 1 + src/data/extra/tasks/git/git.json | 80 +++ src/data/extra/tasks/git/git.svg | 1 + src/data/extra/tasks/git/history.svg | 1 + src/data/extra/tasks/git/initialization.svg | 1 + src/data/extra/tasks/git/pull.svg | 1 + src/data/extra/tasks/git/push.svg | 1 + src/data/extra/tasks/git/status.svg | 1 + src/utils/fileutils.cpp | 19 + src/utils/fileutils.h | 7 + src/utils/iconutils.cpp | 4 +- src/widgets/dialogs/selectdialog.cpp | 19 +- src/widgets/dialogs/selectdialog.h | 9 +- src/widgets/dockwidgethelper.cpp | 16 + src/widgets/dockwidgethelper.h | 3 + src/widgets/mainwindow.cpp | 27 + src/widgets/mainwindow.h | 7 + src/widgets/markdownviewwindow.h | 4 +- src/widgets/textviewwindow.h | 4 +- src/widgets/toolbarhelper.cpp | 76 +++ src/widgets/toolbarhelper.h | 7 + 39 files changed, 2195 insertions(+), 8 deletions(-) create mode 100644 src/core/shellexecution.cpp create mode 100644 src/core/shellexecution.h create mode 100644 src/core/task.cpp create mode 100644 src/core/task.h create mode 100644 src/core/taskhelper.cpp create mode 100644 src/core/taskhelper.h create mode 100644 src/core/taskmgr.cpp create mode 100644 src/core/taskmgr.h create mode 100644 src/core/taskvariablemgr.cpp create mode 100644 src/core/taskvariablemgr.h create mode 100644 src/data/core/icons/task_menu.svg create mode 100644 src/data/extra/tasks/git/commit.svg create mode 100644 src/data/extra/tasks/git/git.json create mode 100644 src/data/extra/tasks/git/git.svg create mode 100644 src/data/extra/tasks/git/history.svg create mode 100644 src/data/extra/tasks/git/initialization.svg create mode 100644 src/data/extra/tasks/git/pull.svg create mode 100644 src/data/extra/tasks/git/push.svg create mode 100644 src/data/extra/tasks/git/status.svg diff --git a/src/core/configmgr.cpp b/src/core/configmgr.cpp index 90daa31d..dce34710 100644 --- a/src/core/configmgr.cpp +++ b/src/core/configmgr.cpp @@ -227,6 +227,12 @@ bool ConfigMgr::checkAppConfig() FileUtils::copyDir(extraDataRoot + QStringLiteral("/themes"), appConfigDir.filePath(QStringLiteral("themes"))); + // Copy tasks. + qApp->processEvents(); + splash->showMessage("Copying tasks"); + FileUtils::copyDir(extraDataRoot + QStringLiteral("/tasks"), + appConfigDir.filePath(QStringLiteral("tasks"))); + // Copy docs. qApp->processEvents(); splash->showMessage("Copying docs"); @@ -377,6 +383,18 @@ QString ConfigMgr::getUserThemeFolder() const return folderPath; } +QString ConfigMgr::getAppTaskFolder() const +{ + return PathUtils::concatenateFilePath(m_appConfigFolderPath, QStringLiteral("tasks")); +} + +QString ConfigMgr::getUserTaskFolder() const +{ + auto folderPath = PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("tasks")); + QDir().mkpath(folderPath); + return folderPath; +} + QString ConfigMgr::getAppWebStylesFolder() const { return PathUtils::concatenateFilePath(m_appConfigFolderPath, QStringLiteral("web-styles")); diff --git a/src/core/configmgr.h b/src/core/configmgr.h index ad4595ba..baed8f27 100644 --- a/src/core/configmgr.h +++ b/src/core/configmgr.h @@ -74,6 +74,10 @@ namespace vnotex QString getUserThemeFolder() const; + QString getAppTaskFolder() const; + + QString getUserTaskFolder() const; + QString getAppWebStylesFolder() const; QString getUserWebStylesFolder() const; diff --git a/src/core/core.pri b/src/core/core.pri index 6c0eb8eb..2f9e2ac6 100644 --- a/src/core/core.pri +++ b/src/core/core.pri @@ -29,6 +29,11 @@ SOURCES += \ $$PWD/texteditorconfig.cpp \ $$PWD/vnotex.cpp \ $$PWD/thememgr.cpp \ + $$PWD/task.cpp \ + $$PWD/taskhelper.cpp \ + $$PWD/taskmgr.cpp \ + $$PWD/taskvariablemgr.cpp \ + $$PWD/shellexecution.cpp \ $$PWD/notebookmgr.cpp \ $$PWD/theme.cpp \ $$PWD/sessionconfig.cpp \ @@ -60,6 +65,11 @@ HEADERS += \ $$PWD/texteditorconfig.h \ $$PWD/vnotex.h \ $$PWD/thememgr.h \ + $$PWD/task.h \ + $$PWD/taskhelper.h \ + $$PWD/taskmgr.h \ + $$PWD/taskvariablemgr.h \ + $$PWD/shellexecution.h \ $$PWD/global.h \ $$PWD/namebasedserver.h \ $$PWD/exception.h \ diff --git a/src/core/shellexecution.cpp b/src/core/shellexecution.cpp new file mode 100644 index 00000000..9a734a13 --- /dev/null +++ b/src/core/shellexecution.cpp @@ -0,0 +1,102 @@ +#include "shellexecution.h" + +#include + +using namespace vnotex; + +ShellExecution::ShellExecution() +{ + auto shell = defaultShell(); + setShellExecutable(shell); + setShellArguments(defaultShellArguments(shell)); +} + +void ShellExecution::setProgram(const QString &p_prog) +{ + m_program = p_prog; +} + +void ShellExecution::setArguments(const QStringList &p_args) +{ + m_args = p_args; +} + +void ShellExecution::setShellExecutable(const QString &p_exec) +{ + m_shell_executable = p_exec; +} + +void ShellExecution::setShellArguments(const QStringList &p_args) +{ + m_shell_args = p_args; +} + +void ShellExecution::setupProcess(QProcess *p_process, + const QString &p_program, + const QStringList &p_args, + const QString &p_shell_exec, + const QStringList &p_shell_args) +{ + auto shell_exec = p_shell_exec.isNull() ? defaultShell() : p_shell_exec; + auto shell_args = p_shell_args.isEmpty() ? defaultShellArguments(shell_exec) + : p_shell_args; + p_process->setProgram(shell_exec); + auto args = p_args; + + auto shell = shellBasename(shell_exec); + if (!p_program.isEmpty() && !p_args.isEmpty()) { + args = shellQuote(args, shell_exec); + } + QStringList allArgs(shell_args); + if (shell == "bash") { + allArgs << (QStringList() << p_program << args).join(' '); + } else { + allArgs << p_program << args; + } + p_process->setArguments(allArgs); +} + +QString ShellExecution::shellBasename(const QString &p_shell) +{ + return QFileInfo(p_shell).baseName().toLower(); +} + +QString ShellExecution::defaultShell() +{ +#ifdef Q_OS_WIN + return "PowerShell.exe"; +#else + return "/bin/bash"; +#endif +} + +QStringList ShellExecution::defaultShellArguments(const QString &p_shell) +{ + auto shell = shellBasename(p_shell); + if (shell == "cmd") { + return {"/C"}; + } else if (shell == "powershell" || p_shell == "pwsh") { + return {"-Command"}; + } else if (shell == "bash") { + return {"-c"}; + } + return {}; +} + +QString ShellExecution::shellQuote(const QString &p_text, const QString &) +{ + if (p_text.contains(' ')) { + return QString("\"%1\"").arg(p_text); + } + return p_text; +} + +QStringList ShellExecution::shellQuote(const QStringList &p_list, const QString &p_shell) +{ + auto shell = shellBasename(p_shell); + QStringList list; + for (const auto &s : p_list) { + list << shellQuote(s, shell); + } + return list; +} diff --git a/src/core/shellexecution.h b/src/core/shellexecution.h new file mode 100644 index 00000000..3fdebf7e --- /dev/null +++ b/src/core/shellexecution.h @@ -0,0 +1,42 @@ +#ifndef SHELLEXECUTION_H +#define SHELLEXECUTION_H + +#include + +namespace vnotex { + + +class ShellExecution : public QProcess +{ + Q_OBJECT +public: + ShellExecution(); + void setProgram(const QString &p_prog); + void setArguments(const QStringList &p_args); + void setShellExecutable(const QString &p_exec); + void setShellArguments(const QStringList &p_args); + + static void setupProcess(QProcess *p_process, + const QString &p_program, + const QStringList &p_args = QStringList(), + const QString &p_shell_exec = QString(), + const QStringList &p_shell_args = QStringList()); + static QString defaultShell(); + static QStringList defaultShellArguments(const QString &p_shell); + static QString shellQuote(const QString &p_text, + const QString &p_shell); + static QStringList shellQuote(const QStringList &p_list, + const QString &p_shell); + +private: + QString m_shell_executable; + QStringList m_shell_args; + QString m_program; + QStringList m_args; + + static QString shellBasename(const QString &p_shell); +}; + +} + +#endif // SHELLEXECUTION_H diff --git a/src/core/task.cpp b/src/core/task.cpp new file mode 100644 index 00000000..118c0c7a --- /dev/null +++ b/src/core/task.cpp @@ -0,0 +1,557 @@ +#include "task.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils/fileutils.h" +#include "utils/pathutils.h" +#include "vnotex.h" +#include "exception.h" +#include "taskhelper.h" +#include "notebook/notebook.h" +#include "shellexecution.h" + + +using namespace vnotex; + +QString Task::s_latestVersion = "0.1.3"; +TaskVariableMgr Task::s_vars; + +Task *Task::fromFile(const QString &p_file, + const QJsonDocument &p_json, + const QString &p_locale, + QObject *p_parent) +{ + auto task = new Task(p_locale, p_file, p_parent); + return fromJson(task, p_json.object()); +} + +Task* Task::fromJson(Task *p_task, + const QJsonObject &p_obj) +{ + if (p_obj.contains("version")) { + p_task->dto.version = p_obj["version"].toString(); + } + + auto version = QVersionNumber::fromString(p_task->getVersion()); + if (version < QVersionNumber(1, 0, 0)) { + return fromJsonV0(p_task, p_obj); + } + qWarning() << "Unknow Task Version" << version << p_task->dto._source; + return p_task; +} + +Task *Task::fromJsonV0(Task *p_task, + const QJsonObject &p_obj, + bool mergeTasks) +{ + if (p_obj.contains("type")) { + p_task->dto.type = p_obj["type"].toString(); + } + + if (p_obj.contains("icon")) { + QString path = p_obj["icon"].toString(); + QDir iconPath(path); + if (iconPath.isRelative()) { + QDir taskDir(p_task->dto._source); + taskDir.cdUp(); + path = QDir(taskDir.filePath(path)).absolutePath(); + } + if (QFile::exists(path)) { + p_task->dto.icon = path; + } else { + qWarning() << "task icon not exists" << path; + } + } + + if (p_obj.contains("shortcut")) { + p_task->dto.shortcut = p_obj["shortcut"].toString(); + } + + if (p_obj.contains("type")) { + p_task->dto.type = p_obj["type"].toString(); + } + + if (p_obj.contains("command")) { + p_task->dto.command = getLocaleString(p_obj["command"], p_task->m_locale); + } + + if (p_obj.contains("args")) { + p_task->dto.args = getLocaleStringList(p_obj["args"], p_task->m_locale); + } + + if (p_obj.contains("label")) { + p_task->dto.label = getLocaleString(p_obj["label"], p_task->m_locale); + } else if (p_task->dto.label.isNull() && !p_task->dto.command.isNull()) { + p_task->dto.label = p_task->dto.command; + } + + if (p_obj.contains("options")) { + auto options = p_obj["options"].toObject(); + + if (options.contains("cwd")) { + p_task->dto.options.cwd = options["cwd"].toString(); + } + + if (options.contains("env")) { + p_task->dto.options.env.clear(); + auto env = options["env"].toObject(); + for (auto i = env.begin(); i != env.end(); i++) { + auto key = i.key(); + auto value = getLocaleString(i.value(), p_task->m_locale); + p_task->dto.options.env.insert(key, value); + } + } + + if (options.contains("shell") && p_task->getType() == "shell") { + auto shell = options["shell"].toObject(); + + if (shell.contains("executable")) { + p_task->dto.options.shell.executable = shell["executable"].toString(); + } + + if (shell.contains("args")) { + p_task->dto.options.shell.args.clear(); + + for (auto arg : shell["args"].toArray()) { + p_task->dto.options.shell.args << arg.toString(); + } + } + } + } + + if (p_obj.contains("tasks")) { + if (!mergeTasks) p_task->m_tasks.clear(); + auto tasks = p_obj["tasks"].toArray(); + + for (const auto &task : tasks) { + auto t = new Task(p_task->m_locale, + p_task->getFile(), + p_task); + p_task->m_tasks.append(fromJson(t, task.toObject())); + } + } + + if (p_obj.contains("inputs")) { + p_task->dto.inputs.clear(); + auto inputs = p_obj["inputs"].toArray(); + + for (const auto &input : inputs) { + auto in = input.toObject(); + InputDTO i; + if (in.contains("id")) { + i.id = in["id"].toString(); + } else { + qWarning() << "Input configuration not contain id"; + } + + if (in.contains("type")) { + i.type = in["type"].toString(); + } else { + i.type = "promptString"; + } + + if (in.contains("description")) { + i.description = getLocaleString(in["description"], p_task->m_locale); + } + + if (in.contains("default")) { + i.default_ = getLocaleString(in["default"], p_task->m_locale); + } + + if (i.type == "promptString" && in.contains("password")) { + i.password = in["password"].toBool(); + } else { + i.password = false; + } + + if (i.type == "pickString" && in.contains("options")) { + i.options = getLocaleStringList(in["options"], p_task->m_locale); + } + + if (i.type == "pickString" && !i.default_.isNull() && !i.options.contains(i.default_)) { + qWarning() << "default must be one of the option values"; + } + + p_task->dto.inputs << i; + } + } + + if (p_obj.contains("messages")) { + p_task->dto.messages.clear(); + auto messages = p_obj["messages"].toArray(); + + for (const auto &message : messages) { + auto msg = message.toObject(); + MessageDTO m; + if (msg.contains("id")) { + m.id = msg["id"].toString(); + } else { + qWarning() << "Message configuration not contain id"; + } + + if (msg.contains("type")) { + m.type = msg["type"].toString(); + } else { + m.type = "information"; + } + + if (msg.contains("title")) { + m.title = getLocaleString(msg["title"], p_task->m_locale); + } + + if (msg.contains("text")) { + m.text = getLocaleString(msg["text"], p_task->m_locale); + } + + if (msg.contains("detailedText")) { + m.detailedText = getLocaleString(msg["detailedText"], p_task->m_locale); + } + + if (msg.contains("buttons")) { + auto buttons = msg["buttons"].toArray(); + for (auto button : buttons) { + auto btn = button.toObject(); + ButtonDTO b; + b.text = getLocaleString(btn["text"], p_task->m_locale); + m.buttons << b; + } + } + p_task->dto.messages << m; + } + } + + // OS-specific task configuration +#if defined (Q_OS_WIN) +#define OS_SPEC "windows" +#endif +#if defined (Q_OS_MACOS) +#define OS_SPEC "osx" +#endif +#if defined (Q_OS_LINUX) +#define OS_SPEC "linux" +#endif + if (p_obj.contains(OS_SPEC)) { + auto os = p_obj[OS_SPEC].toObject(); + fromJsonV0(p_task, os, true); + } +#undef OS_SPEC + + return p_task; +} + +QString Task::getVersion() const +{ + return dto.version; +} + +QString Task::getType() const +{ + return dto.type; +} + +QString Task::getCommand() const +{ + return s_vars.evaluate(dto.command, this); +} + +QStringList Task::getArgs() const +{ + return s_vars.evaluate(dto.args, this); +} + +QString Task::getLabel() const +{ + return dto.label; +} + +QString Task::getIcon() const +{ + return dto.icon; +} + +QString Task::getShortcut() const +{ + return dto.shortcut; +} + +QString Task::getOptionsCwd() const +{ + auto cwd = dto.options.cwd; + if (!cwd.isNull()) { + return s_vars.evaluate(cwd, this); + } + auto notebook = TaskHelper::getCurrentNotebook(); + if (notebook) cwd = notebook->getRootFolderAbsolutePath(); + if (!cwd.isNull()) { + return cwd; + } + cwd = TaskHelper::getCurrentFile(); + if (!cwd.isNull()) { + return QFileInfo(cwd).dir().absolutePath(); + } + return QFileInfo(dto._source).dir().absolutePath(); +} + +const QMap &Task::getOptionsEnv() const +{ + return dto.options.env; +} + +QString Task::getOptionsShellExecutable() const +{ + return dto.options.shell.executable; +} + +QStringList Task::getOptionsShellArgs() const +{ + if (dto.options.shell.args.isEmpty()) { + return ShellExecution::defaultShellArguments(dto.options.shell.executable); + } else { + return s_vars.evaluate(dto.options.shell.args, this); + } +} + +const QVector &Task::getTasks() const +{ + return m_tasks; +} + +const QVector &Task::getInputs() const +{ + return dto.inputs; +} + +InputDTO Task::getInput(const QString &p_id) const +{ + for (auto i : dto.inputs) { + if (i.id == p_id) { + return i; + } + } + qDebug() << getLabel(); + qWarning() << "input" << p_id << "not found"; + throw "Input variable can not found"; +} + +MessageDTO Task::getMessage(const QString &p_id) const +{ + for (auto msg : dto.messages) { + if (msg.id == p_id) { + return msg; + } + } + qDebug() << getLabel(); + qWarning() << "message" << p_id << "not found"; + throw "Message can not found"; +} + +QString Task::getFile() const +{ + return dto._source; +} + +Task::Task(const QString &p_locale, + const QString &p_file, + QObject *p_parent) + : QObject(p_parent) +{ + dto._source = p_file; + dto.version = s_latestVersion; + dto.type = "shell"; + dto.options.shell.executable = ShellExecution::defaultShell(); + + // inherit configuration + m_parent = qobject_cast(p_parent); + if (m_parent) { + dto.version = m_parent->dto.version; + dto.type = m_parent->dto.type; + dto.command = m_parent->dto.command; + dto.args = m_parent->dto.args; + dto.options.cwd = m_parent->dto.options.cwd; + dto.options.env = m_parent->dto.options.env; + dto.options.shell.executable = m_parent->dto.options.shell.executable; + dto.options.shell.args = m_parent->dto.options.shell.args; + // not inherit label/inputs/tasks + } else { + dto.label = QFileInfo(p_file).baseName(); + } + + if (!p_locale.isNull()) { + m_locale = p_locale; + } +} + +QProcess *Task::setupProcess() const +{ + // Set process property + auto command = getCommand(); + if (command.isEmpty()) return nullptr; + auto process = new QProcess(this->parent()); + process->setWorkingDirectory(getOptionsCwd()); + + auto options_env = getOptionsEnv(); + if (!options_env.isEmpty()) { + auto env = QProcessEnvironment::systemEnvironment(); + for (auto i = options_env.begin(); i != options_env.end(); i++) { + env.insert(i.key(), i.value()); + } + process->setProcessEnvironment(env); + } + + auto args = getArgs(); + auto type = getType(); + + // set program and args + if (type == "shell") { + ShellExecution::setupProcess(process, + command, + args, + getOptionsShellExecutable(), + getOptionsShellArgs()); + } else if (getType() == "process") { + process->setProgram(command); + process->setArguments(args); + } + + // connect signal and slot + connect(process, &QProcess::started, + this, [this]() { + emit showOutput(tr("[Task %1 started]\n").arg(getLabel())); + }); + connect(process, &QProcess::readyReadStandardOutput, + this, [process, this]() { + auto text = textDecode(process->readAllStandardOutput()); + text = TaskHelper::handleCommand(text, process, this); + emit showOutput(text); + }); + connect(process, &QProcess::readyReadStandardError, + this, [process, this]() { + auto text = process->readAllStandardError(); + emit showOutput(textDecode(text)); + }); + connect(process, &QProcess::errorOccurred, + this, [this](QProcess::ProcessError error) { + emit showOutput(tr("[Task %1 error occurred with code %2]\n").arg(getLabel(), QString::number(error))); + }); + connect(process, QOverload::of(&QProcess::finished), + this, [this, process](int exitCode) { + emit showOutput(tr("\n[Task %1 finished with exit code %2]\n") + .arg(getLabel(), QString::number(exitCode))); + process->deleteLater(); + }); + return process; +} + +void Task::run() const +{ + QProcess *process; + try { + process = setupProcess(); + } catch (const char *msg) { + qDebug() << msg; + return ; + } + if (process) { + // start process + qInfo() << "run task" << process->program() << process->arguments(); + process->start(); + } +} + +TaskDTO Task::getDTO() const +{ + return dto; +} + +QString Task::textDecode(const QByteArray &p_text) +{ + static QByteArrayList codecNames = { + "UTF-8", + "System", + "UTF-16", + "GB18030" + }; + for (auto name : codecNames) { + auto text = textDecode(p_text, name); + if (!text.isNull()) return text; + } + return p_text; +} + +QString Task::textDecode(const QByteArray &p_text, const QByteArray &name) +{ + auto codec = QTextCodec::codecForName(name); + if (codec) { + QTextCodec::ConverterState state; + auto text = codec->toUnicode(p_text.data(), p_text.size(), &state); + if (state.invalidChars > 0) return QString(); + return text; + } + return QString(); +} + + +bool Task::isValidTaskFile(const QString &p_file, + QJsonDocument &p_json) +{ + QFile file(p_file); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return false; + } + QJsonParseError error; + p_json = QJsonDocument::fromJson(file.readAll(), &error); + file.close(); + if (p_json.isNull()) { + qDebug() << "load task" << p_file << "failed: " << error.errorString() + << "at offset" << error.offset; + return false; + } + + return true; +} + +QString Task::getLocaleString(const QJsonValue &p_value, const QString &p_locale) +{ + if (p_value.isObject()) { + auto obj = p_value.toObject(); + if (obj.contains(p_locale)) { + return obj.value(p_locale).toString(); + } else { + qWarning() << "current locale" << p_locale << "not found"; + if (!obj.isEmpty()){ + return obj.begin().value().toString(); + } else { + return QString(); + } + } + } else { + return p_value.toString(); + } +} + +QStringList Task::getLocaleStringList(const QJsonValue &p_value, const QString &p_locale) +{ + QStringList list; + for (auto value : p_value.toArray()) { + list << getLocaleString(value, p_locale); + } + return list; +} + +QStringList Task::getStringList(const QJsonValue &p_value) +{ + QStringList list; + for (auto value : p_value.toArray()) { + list << value.toString(); + } + return list; +} diff --git a/src/core/task.h b/src/core/task.h new file mode 100644 index 00000000..26f13fce --- /dev/null +++ b/src/core/task.h @@ -0,0 +1,185 @@ +#ifndef TASK_H +#define TASK_H + +#include +#include +#include +#include +#include + +#include "taskvariablemgr.h" + +class QAction; + +namespace vnotex { + + struct ButtonDTO { + QString text; + }; + + struct InputDTO { + QString id; + + QString type; + + QString description; + + QString default_; + + bool password; + + QStringList options; + }; + + struct MessageDTO { + QString id; + + QString type; + + QString title; + + QString text; + + QString detailedText; + + QVector buttons; + }; + + struct ShellOptionsDTO { + QString executable; + + QStringList args; + }; + + struct TaskOptionsDTO { + QString cwd; + + QMap env; + + ShellOptionsDTO shell; + }; + + struct TaskDTO { + QString version; + + QString type; + + QString command; + + QStringList args; + + QString label; + + QString icon; + + QString shortcut; + + QVector inputs; + + QVector messages; + + TaskOptionsDTO options; + + QString _scope; + + QString _source; + }; + + class Notebook; + + class Task : public QObject + { + Q_OBJECT + public: + + + static Task* fromFile(const QString &p_file, + const QJsonDocument &p_json, + const QString &p_locale, + QObject *p_parent = nullptr); + + void run() const; + + TaskDTO getDTO() const; + + QString getVersion() const; + + QString getType() const; + + QString getCommand() const; + + QStringList getArgs() const; + + QString getLabel() const; + + QString getIcon() const; + + QString getShortcut() const; + + QString getOptionsCwd() const; + + const QMap &getOptionsEnv() const; + + QString getOptionsShellExecutable() const; + + QStringList getOptionsShellArgs() const; + + const QVector &getTasks() const; + + const QVector &getInputs() const; + + InputDTO getInput(const QString &p_id) const; + + MessageDTO getMessage(const QString &p_id) const; + + QString getFile() const; + + static QString s_latestVersion; + + static bool isValidTaskFile(const QString &p_file, + QJsonDocument &p_json); + + static QString getLocaleString(const QJsonValue &p_value, + const QString &p_locale); + + static QStringList getLocaleStringList(const QJsonValue &p_value, + const QString &p_locale); + + static QStringList getStringList(const QJsonValue &p_value); + + signals: + void showOutput(const QString &p_text) const; + + private: + static Task* fromJson(Task *p_task, + const QJsonObject &p_obj); + + static Task* fromJsonV0(Task *p_task, + const QJsonObject &p_obj, + bool mergeTasks = false); + + explicit Task(const QString &p_locale, + const QString &p_file = QString(), + QObject *p_parent = nullptr); + + QProcess *setupProcess() const; + + static QString textDecode(const QByteArray &p_text); + + static QString textDecode(const QByteArray &p_text, const QByteArray &name); + + TaskDTO dto; + + Task *m_parent = nullptr; + + QVector m_tasks; + + QString m_locale; + + static TaskVariableMgr s_vars; + + }; + +} // ns vnotex + +#endif // TASK_H diff --git a/src/core/taskhelper.cpp b/src/core/taskhelper.cpp new file mode 100644 index 00000000..42811a95 --- /dev/null +++ b/src/core/taskhelper.cpp @@ -0,0 +1,233 @@ +#include "taskhelper.h" + +#include +#include +#include +#include +#include + +#include "vnotex.h" +#include "notebookmgr.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace vnotex; + +QString TaskHelper::normalPath(const QString &p_text) +{ +#if defined (Q_OS_WIN) + return QString(p_text).replace('/', '\\'); +#endif + return p_text; +} + +QString TaskHelper::getCurrentFile() +{ + auto win = VNoteX::getInst().getMainWindow()->getViewArea()->getCurrentViewWindow(); + QString file; + if (win && win->getBuffer()) { + file = win->getBuffer()->getPath(); + } + return file; +} + +QSharedPointer TaskHelper::getCurrentNotebook() +{ + const auto ¬ebookMgr = VNoteX::getInst().getNotebookMgr(); + auto id = notebookMgr.getCurrentNotebookId(); + if (id == Notebook::InvalidId) return nullptr; + return notebookMgr.findNotebookById(id); +} + +QString TaskHelper::getFileNotebookFolder(const QString p_currentFile) +{ + const auto ¬ebookMgr = VNoteX::getInst().getNotebookMgr(); + const auto ¬ebooks = notebookMgr.getNotebooks(); + for (auto notebook : notebooks) { + auto rootPath = notebook->getRootFolderAbsolutePath(); + if (PathUtils::pathContains(rootPath, p_currentFile)) { + return rootPath; + } + } + return QString(); +} + +QString TaskHelper::getSelectedText() +{ + auto window = VNoteX::getInst().getMainWindow()->getViewArea()->getCurrentViewWindow(); + { + auto win = dynamic_cast(window); + if (win) { + return win->selectedText(); + } + } + { + auto win = dynamic_cast(window); + if (win) { + return win->selectedText(); + } + } + return QString(); +} + +QStringList TaskHelper::getAllSpecialVariables(const QString &p_name, const QString &p_text) +{ + QStringList list; + QRegularExpression re(QString(R"(\$\{[\t ]*%1[\t ]*:[\t ]*(.*?)[\t ]*\})").arg(p_name)); + auto it = re.globalMatch(p_text); + while (it.hasNext()) { + list << it.next().captured(1); + } + return list; +} + +QString TaskHelper::replaceAllSepcialVariables(const QString &p_name, + const QString &p_text, + const QMap &p_map) +{ + auto text = p_text; + for (auto i = p_map.begin(); i != p_map.end(); i++) { + auto key = QString(i.key()).replace(".", "\\.").replace("[", "\\[").replace("]", "\\]"); + auto pattern = QRegularExpression(QString(R"(\$\{[\t ]*%1[\t ]*:[\t ]*%2[\t ]*\})").arg(p_name, key)); + text = text.replace(pattern, i.value()); + } + return text; +} + +QString TaskHelper::evaluateJsonExpr(const QJsonObject &p_obj, const QString &p_expr) +{ + QJsonValue value = p_obj; + for (auto token : p_expr.split('.')) { + auto pos = token.indexOf('['); + auto name = token.mid(0, pos); + value = value.toObject().value(name); + if (pos == -1) continue; + if (token.back() == ']') { + for (auto idx : token.mid(pos+1, token.length()-pos-2).split("][")) { + bool ok; + auto index = idx.toInt(&ok); + if (!ok) throw "Config variable syntax error!"; + value = value.toArray().at(index); + } + } else { + throw "Config variable syntax error!"; + } + } + if (value.isBool()) { + if (value.toBool()) return "true"; + else return "false"; + } else if (value.isDouble()) { + return QString::number(value.toDouble()); + } else if (value.isNull()) { + return "null"; + } else if (value.isUndefined()) { + return "undefined"; + } + return value.toString(); +} + +QString TaskHelper::getPathSeparator() +{ +#if defined (Q_OS_WIN) + return "\\"; +#else + return "/"; +#endif +} + +QString TaskHelper::handleCommand(const QString &p_text, + QProcess *p_process, + const Task *p_task) +{ + QRegularExpression re(R"(^::([a-zA-Z-]+)(.*?)?::(.*?)$)", + QRegularExpression::MultilineOption); + auto i = re.globalMatch(p_text); + while (i.hasNext()) { + auto match = i.next(); + auto cmd = match.captured(1).toLower(); + auto args = match.captured(2).trimmed().split(','); + auto value = match.captured(3); + + QMap arg; + for (const auto &i : args) { + auto s = i.trimmed(); + auto p = s.indexOf('='); + auto name = s.mid(0, p); + QString val; + if (p != -1) { + val = s.mid(p+1); + } + arg.insert(name, val); + } + if (cmd == "show-message") { + QMessageBox box; + // fill message dto + auto msgId = arg.value("id"); + QVector buttons; + if (!msgId.isEmpty()) { + MessageDTO msgd = p_task->getMessage(msgId); + box.setWindowTitle(msgd.title); + box.setText(msgd.text); + box.setDetailedText(msgd.detailedText); + for (auto button : msgd.buttons) { + buttons.append(box.addButton(button.text, QMessageBox::ActionRole)); + } + } + // fill args + if (arg.contains("title")) box.setWindowTitle(arg["title"]); + if (arg.contains("text")) box.setText(arg["text"]); + if (arg.contains("detailedText")) box.setWindowTitle(arg["detailedText"]); + if (arg.contains("buttons")) { + buttons.clear(); + for (auto button : arg["buttons"].split('|')) { + buttons.append(box.addButton(button, QMessageBox::ActionRole)); + } + } + box.exec(); + int clickedBtnId; + for (clickedBtnId = 0; clickedBtnId < buttons.size(); clickedBtnId++) { + if (box.clickedButton() == buttons.at(clickedBtnId)) { + break; + } + } + if (p_process) { + if (p_process->state() == QProcess::Running) { + p_process->write(QByteArray::number(clickedBtnId)+"\n"); + } + } else { + qWarning() << "process finished!"; + } + } else if (cmd == "show-inputdialog") { + QInputDialog dialog; + dialog.setWindowTitle(arg.value("title")); + + } else if (cmd == "show-info") { + QMessageBox::information(VNoteX::getInst().getMainWindow(), + arg.value("title"), + value); + } else if (cmd == "show-question") { + auto ret = QMessageBox::question(VNoteX::getInst().getMainWindow(), + arg.value("title"), + value); + if (p_process) { + if (p_process->state() == QProcess::Running) { + p_process->write(QByteArray::number(ret)+"\n"); + } + } else { + qWarning() << "process finished!"; + } + } + } + auto text = p_text; + return text.replace(re, ""); +} + +TaskHelper::TaskHelper() +{ + +} diff --git a/src/core/taskhelper.h b/src/core/taskhelper.h new file mode 100644 index 00000000..30ab83f6 --- /dev/null +++ b/src/core/taskhelper.h @@ -0,0 +1,50 @@ +#ifndef TASKHELPER_H +#define TASKHELPER_H + +#include +#include + +class QProcess; + +namespace vnotex { + +class Notebook; +class Task; + +class TaskHelper +{ +public: + // helper functions + + static QString normalPath(const QString &p_text); + + static QString getCurrentFile(); + + static QSharedPointer getCurrentNotebook(); + + static QString getFileNotebookFolder(const QString p_currentFile); + + static QString getSelectedText(); + + static QStringList getAllSpecialVariables(const QString &p_name, const QString &p_text); + + static QString replaceAllSepcialVariables(const QString &p_name, + const QString &p_text, + const QMap &p_map); + + static QString evaluateJsonExpr(const QJsonObject &p_obj, + const QString &p_expr); + + static QString getPathSeparator(); + + static QString handleCommand(const QString &p_text, + QProcess *p_process, + const Task *p_task); + +private: + TaskHelper(); +}; + +} // ns vnotex + +#endif // TASKHELPER_H diff --git a/src/core/taskmgr.cpp b/src/core/taskmgr.cpp new file mode 100644 index 00000000..0ea58baf --- /dev/null +++ b/src/core/taskmgr.cpp @@ -0,0 +1,192 @@ +#include "taskmgr.h" + +#include +#include +#include +#include + +#include "configmgr.h" +#include "coreconfig.h" +#include "utils/pathutils.h" +#include "utils/fileutils.h" +#include "vnotex.h" +#include "notebookmgr.h" +#include "notebookconfigmgr/bundlenotebookconfigmgr.h" + +using namespace vnotex; + +QStringList TaskMgr::s_searchPaths; + +TaskMgr::TaskMgr(QObject *parent) + : QObject(parent) +{ + m_watcher = new QFileSystemWatcher(this); +} + +void TaskMgr::init() +{ + // load all tasks and watch them + loadAllTask(); + + connect(&VNoteX::getInst().getNotebookMgr(), &NotebookMgr::currentNotebookChanged, + this, &TaskMgr::loadAllTask); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, + this, &TaskMgr::loadAllTask); + connect(m_watcher, &QFileSystemWatcher::fileChanged, + this, &TaskMgr::loadAllTask); +} + +void TaskMgr::refresh() +{ + loadAvailableTasks(); +} + +QVector TaskMgr::getAllTasks() const +{ + QVector tasks; + tasks.append(m_appTasks); + tasks.append(m_userTasks); + tasks.append(m_notebookTasks); + return tasks; +} + +const QVector &TaskMgr::getAppTasks() const +{ + return m_appTasks; +} + +const QVector &TaskMgr::getUserTasks() const +{ + return m_userTasks; +} + +const QVector &TaskMgr::getNotebookTasks() const +{ + return m_notebookTasks; +} + +void TaskMgr::addSearchPath(const QString &p_path) +{ + s_searchPaths << p_path; +} + +QString TaskMgr::getNotebookTaskFolder() +{ + const auto ¬ebookMgr = VNoteX::getInst().getNotebookMgr(); + auto id = notebookMgr.getCurrentNotebookId(); + if (id == Notebook::InvalidId) return QString(); + auto notebook = notebookMgr.findNotebookById(id); + if (!notebook) return QString(); + auto configMgr = notebook->getConfigMgr(); + if (!configMgr) return QString(); + auto configMgrName = configMgr->getName(); + if (configMgrName == "vx.vnotex") { + QDir dir(notebook->getRootFolderAbsolutePath()); + dir.cd(BundleNotebookConfigMgr::getConfigFolderName()); + if (!dir.cd("tasks")) return QString(); + return dir.absolutePath(); + } else { + qWarning() << "Unknow notebook config type"<< configMgrName <<"task will not be load."; + } + return QString(); +} + +void TaskMgr::addWatchPaths(const QStringList &list) +{ + if (list.isEmpty()) return ; + qDebug() << "addWatchPaths" << list; + m_watcher->addPaths(list); +} + +void TaskMgr::clearWatchPaths() +{ + auto entrys = m_watcher->files(); + if (!entrys.isEmpty()) { + m_watcher->removePaths(entrys); + } + entrys = m_watcher->directories(); + if (!entrys.isEmpty()) { + m_watcher->removePaths(entrys); + } +} + +void TaskMgr::clearTasks() +{ + m_appTasks.clear(); + m_userTasks.clear(); + m_notebookTasks.clear(); + m_files.clear(); +} + +void TaskMgr::addAllTaskFolder() +{ + s_searchPaths.clear(); + auto &configMgr = ConfigMgr::getInst(); + // App scope task folder + addSearchPath(configMgr.getAppTaskFolder()); + // User scope task folder + addSearchPath(configMgr.getUserTaskFolder()); + // Notebook scope task folder + auto path = getNotebookTaskFolder(); + if (!path.isNull()) addSearchPath(path); +} + +void TaskMgr::loadAllTask() +{ + addAllTaskFolder(); + loadAvailableTasks(); + watchTaskEntrys(); + emit taskChanged(); +} + +void TaskMgr::watchTaskEntrys() +{ + clearWatchPaths(); + addWatchPaths(s_searchPaths); + for (const auto &pa : s_searchPaths) { + addWatchPaths(FileUtils::entryListRecursively(pa, QStringList(), QDir::AllDirs)); + } + addWatchPaths(m_files); +} + +void TaskMgr::loadAvailableTasks() +{ + m_files.clear(); + auto &configMgr = ConfigMgr::getInst(); + loadAvailableTasks(m_appTasks, configMgr.getAppTaskFolder()); + loadAvailableTasks(m_userTasks, configMgr.getUserTaskFolder()); + loadAvailableTasks(m_notebookTasks, getNotebookTaskFolder()); +} + +void TaskMgr::loadAvailableTasks(QVector &p_tasks, const QString &p_searchPath) +{ + p_tasks.clear(); + if (p_searchPath.isEmpty()) return ; + const auto taskFiles = FileUtils::entryListRecursively(p_searchPath, {"*.json"}, QDir::Files); + for (auto &file : taskFiles) { + m_files << file; + checkAndAddTaskFile(p_tasks, file); + } + + { + QStringList list; + for (auto task : p_tasks) { + list << QFileInfo(task->getFile()).fileName(); + } + if (!p_tasks.isEmpty()) qDebug() << "load tasks" << list; + } +} + +void TaskMgr::checkAndAddTaskFile(QVector &p_tasks, const QString &p_file) +{ + QJsonDocument json; + if (Task::isValidTaskFile(p_file, json)) { + const auto localeStr = ConfigMgr::getInst().getCoreConfig().getLocaleToUse(); + p_tasks.push_back(Task::fromFile(p_file, json, localeStr, this)); + } +} + +void TaskMgr::deleteTask(Task *p_task) +{ + Q_UNUSED(p_task); +} diff --git a/src/core/taskmgr.h b/src/core/taskmgr.h new file mode 100644 index 00000000..b6560624 --- /dev/null +++ b/src/core/taskmgr.h @@ -0,0 +1,79 @@ +#ifndef TASKMGR_H +#define TASKMGR_H + +#include + +#include + +#include "task.h" + +class QFileSystemWatcher; + +namespace vnotex +{ + class TaskMgr : public QObject + { + Q_OBJECT + public: + + explicit TaskMgr(QObject *parent = nullptr); + + // It will be invoked after MainWindow show + void init(); + + void refresh(); + + QVector getAllTasks() const; + + const QVector &getAppTasks() const; + + const QVector &getUserTasks() const; + + const QVector &getNotebookTasks() const; + + void deleteTask(Task *p_task); + + static void addSearchPath(const QString &p_path); + + static QString getNotebookTaskFolder(); + + signals: + void taskChanged(); + + private: + void addWatchPaths(const QStringList &list); + + void clearWatchPaths(); + + void clearTasks(); + + void addAllTaskFolder(); + + void loadAllTask(); + + void watchTaskEntrys(); + + void loadAvailableTasks(); + + void loadAvailableTasks(QVector &p_tasks, const QString &p_searchPath); + + void loadTasks(const QString &p_path); + + void checkAndAddTaskFile(QVector &p_tasks, const QString &p_file); + + QVector m_appTasks; + QVector m_userTasks; + QVector m_notebookTasks; + + // all json files in task folder + // maybe invalid + QStringList m_files; + + QFileSystemWatcher *m_watcher; + + // List of path to search for tasks. + static QStringList s_searchPaths; + }; +} // ns vnotex + +#endif // TASKMGR_H diff --git a/src/core/taskvariablemgr.cpp b/src/core/taskvariablemgr.cpp new file mode 100644 index 00000000..c659b200 --- /dev/null +++ b/src/core/taskvariablemgr.cpp @@ -0,0 +1,334 @@ +#include "taskvariablemgr.h" + +#include +#include +#include +#include +#include + +#include "vnotex.h" +#include "task.h" +#include "taskhelper.h" +#include "shellexecution.h" +#include "configmgr.h" +#include "mainconfig.h" +#include "notebook/notebook.h" +#include +#include + +using namespace vnotex; + + +TaskVariable::TaskVariable(TaskVariable::Type p_type, + const QString &p_name, + TaskVariable::Func p_func) + : m_type(p_type), m_name(p_name), m_func(p_func) +{ + +} + + +TaskVariableMgr::TaskVariableMgr() + : m_initialized(false) +{ + +} + +void TaskVariableMgr::refresh() +{ + init(); +} + +QString TaskVariableMgr::evaluate(const QString &p_text, + const Task *p_task) const +{ + auto text = p_text; + auto eval = [&text](const QString &p_name, std::function p_func) { + auto reg = QRegularExpression(QString(R"(\$\{[\t ]*%1[\t ]*\})").arg(p_name)); + if (text.contains(reg)) { + text.replace(reg, p_func()); + } + }; + + // current notebook variables + { + eval("notebookFolder", []() { + auto notebook = TaskHelper::getCurrentNotebook(); + if (notebook) { + return TaskHelper::normalPath(notebook->getRootFolderAbsolutePath()); + } else return QString(); + }); + eval("notebookFolderBasename", []() { + auto notebook = TaskHelper::getCurrentNotebook(); + if (notebook) { + auto folder = notebook->getRootFolderAbsolutePath(); + return QDir(folder).dirName(); + } else return QString(); + }); + eval("notebookName", []() { + auto notebook = TaskHelper::getCurrentNotebook(); + if (notebook) { + return notebook->getName(); + } else return QString(); + }); + eval("notebookName", []() { + auto notebook = TaskHelper::getCurrentNotebook(); + if (notebook) { + return notebook->getDescription(); + } else return QString(); + }); + } + + // current file variables + { + eval("file", []() { + return TaskHelper::normalPath(TaskHelper::getCurrentFile()); + }); + eval("fileNotebookFolder", []() { + auto file = TaskHelper::getCurrentFile(); + return TaskHelper::normalPath(TaskHelper::getFileNotebookFolder(file)); + }); + eval("relativeFile", []() { + auto file = TaskHelper::getCurrentFile(); + auto folder = TaskHelper::getFileNotebookFolder(file); + return QDir(folder).relativeFilePath(file); + }); + eval("fileBasename", []() { + auto file = TaskHelper::getCurrentFile(); + return QFileInfo(file).fileName(); + }); + eval("fileBasename", []() { + auto file = TaskHelper::getCurrentFile(); + return QFileInfo(file).completeBaseName(); + }); + eval("fileDirname", []() { + auto file = TaskHelper::getCurrentFile(); + return TaskHelper::normalPath(QFileInfo(file).dir().absolutePath()); + }); + eval("fileExtname", []() { + auto file = TaskHelper::getCurrentFile(); + return QFileInfo(file).suffix(); + }); + eval("selectedText", []() { + return TaskHelper::getSelectedText(); + }); + eval("cwd", [p_task]() { + return TaskHelper::normalPath(p_task->getOptionsCwd()); + }); + eval("taskFile", [p_task]() { + return TaskHelper::normalPath(p_task->getFile()); + }); + eval("taskDirname", [p_task]() { + return TaskHelper::normalPath(QFileInfo(p_task->getFile()).dir().absolutePath()); + }); + eval("execPath", []() { + return TaskHelper::normalPath(qApp->applicationFilePath()); + }); + eval("pathSeparator", []() { + return TaskHelper::getPathSeparator(); + }); + eval("notebookTaskFolder", []() { + return TaskHelper::normalPath(TaskMgr::getNotebookTaskFolder()); + }); + eval("userTaskFolder", []() { + return TaskHelper::normalPath(ConfigMgr::getInst().getUserTaskFolder()); + }); + eval("appTaskFolder", []() { + return TaskHelper::normalPath(ConfigMgr::getInst().getAppTaskFolder()); + }); + eval("userThemeFolder", []() { + return TaskHelper::normalPath(ConfigMgr::getInst().getUserThemeFolder()); + }); + eval("appThemeFolder", []() { + return TaskHelper::normalPath(ConfigMgr::getInst().getAppThemeFolder()); + }); + eval("userDocsFolder", []() { + return TaskHelper::normalPath(ConfigMgr::getInst().getUserDocsFolder()); + }); + eval("appDocsFolder", []() { + return TaskHelper::normalPath(ConfigMgr::getInst().getAppDocsFolder()); + }); + } + + // Magic variables + { + auto cDT = QDateTime::currentDateTime(); + for(auto s : { + "d", "dd", "ddd", "dddd", "M", "MM", "MMM", "MMMM", + "yy", "yyyy", "h", "hh", "H", "HH", "m", "mm", + "s", "ss", "z", "zzz", "AP", "A", "ap", "a" + }) eval(QString("magic:%1").arg(s), [s]() { + return QDateTime::currentDateTime().toString(s); + }); + eval("magic:random", []() { + return QString::number(QRandomGenerator::global()->generate()); + }); + eval("magic:random_d", []() { + return QString::number(QRandomGenerator::global()->generate()); + }); + eval("magic:date", []() { + return QDate::currentDate().toString("yyyy-MM-dd"); + }); + eval("magic:da", []() { + return QDate::currentDate().toString("yyyyMMdd"); + }); + eval("magic:time", []() { + return QTime::currentTime().toString("hh:mm:ss"); + }); + eval("magic:datetime", []() { + return QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); + }); + eval("magic:dt", []() { + return QDateTime::currentDateTime().toString("yyyyMMdd hh:mm:ss"); + }); + eval("magic:note", []() { + auto file = TaskHelper::getCurrentFile(); + return QFileInfo(file).fileName(); + }); + eval("magic:no", []() { + auto file = TaskHelper::getCurrentFile(); + return QFileInfo(file).completeBaseName(); + }); + eval("magic:t", []() { + auto dt = QDateTime::currentDateTime(); + return dt.timeZone().displayName(dt, QTimeZone::ShortName); + }); + eval("magic:w", []() { + return QString::number(QDate::currentDate().weekNumber()); + }); + eval("magic:att", []() { + // TODO + return QString(); + }); + } + + // environment variables + do { + QMap map; + auto list = TaskHelper::getAllSpecialVariables("env", p_text); + list.erase(std::unique(list.begin(), list.end()), list.end()); + if (list.isEmpty()) break; + for (const auto &name : list) { + auto value = QProcessEnvironment::systemEnvironment().value(name); + map.insert(name, value); + } + text = TaskHelper::replaceAllSepcialVariables("env", text, map); + } while(0); + + // config variables + do { + const auto config_obj = ConfigMgr::getInst().getConfig().toJson(); + QMap map; + auto list = TaskHelper::getAllSpecialVariables("config", p_text); + if (list.isEmpty()) break; + list.erase(std::unique(list.begin(), list.end()), list.end()); + for (const auto &name : list) { + auto value = TaskHelper::evaluateJsonExpr(config_obj, name); + qDebug() << "insert" << name << value; + map.insert(name, value); + } + text = TaskHelper::replaceAllSepcialVariables("config", text, map); + } while(0); + + // input variables + text = evaluateInputVariables(text, p_task); + // shell variables + text = evaluateShellVariables(text, p_task); + return text; +} + +QStringList TaskVariableMgr::evaluate(const QStringList &p_list, + const Task *p_task) const +{ + QStringList list; + for (const auto &s : p_list) { + list << evaluate(s, p_task); + } + return list; +} + +void TaskVariableMgr::init() +{ + if (m_initialized) return ; + m_initialized = true; + + add(TaskVariable::FunctionBased, + "file", + [](const TaskVariable *, const Task *) { + return QString(); + }); +} + +void TaskVariableMgr::add(TaskVariable::Type p_type, + const QString &p_name, + TaskVariable::Func p_func) +{ + m_predefs.insert(p_name, TaskVariable(p_type, p_name, p_func)); +} + +QString TaskVariableMgr::evaluateInputVariables(const QString &p_text, + const Task *p_task) const +{ + QMap map; + auto list = TaskHelper::getAllSpecialVariables("input", p_text); + list.erase(std::unique(list.begin(), list.end()), list.end()); + if (list.isEmpty()) return p_text; + for (const auto &id : list) { + auto input = p_task->getInput(id); + QString text; + auto mainwin = VNoteX::getInst().getMainWindow(); + if (input.type == "promptString") { + auto desc = evaluate(input.description, p_task); + auto defaultText = evaluate(input.default_, p_task); + QInputDialog dialog(mainwin); + dialog.setInputMode(QInputDialog::TextInput); + if (input.password) dialog.setTextEchoMode(QLineEdit::Password); + else dialog.setTextEchoMode(QLineEdit::Normal); + dialog.setWindowTitle(p_task->getLabel()); + dialog.setLabelText(desc); + dialog.setTextValue(defaultText); + if (dialog.exec() == QDialog::Accepted) { + text = dialog.textValue(); + } else { + throw "TaskCancle"; + } + } else if (input.type == "pickString") { + // TODO: select description + SelectDialog dialog(p_task->getLabel(), input.description, mainwin); + for (int i = 0; i < input.options.size(); i++) { + dialog.addSelection(input.options.at(i), i); + } + + if (dialog.exec() == QDialog::Accepted) { + int selection = dialog.getSelection(); + text = input.options.at(selection); + } else { + throw "TaskCancle"; + } + } + map.insert(input.id, text); + } + return TaskHelper::replaceAllSepcialVariables("input", p_text, map); +} + +QString TaskVariableMgr::evaluateShellVariables(const QString &p_text, + const Task *p_task) const +{ + QMap map; + auto list = TaskHelper::getAllSpecialVariables("shell", p_text); + list.erase(std::unique(list.begin(), list.end()), list.end()); + if (list.isEmpty()) return p_text; + for (const auto &cmd : list) { + QProcess process; + process.setWorkingDirectory(p_task->getOptionsCwd()); + ShellExecution::setupProcess(&process, cmd); + process.start(); + if (!process.waitForStarted(1000) || !process.waitForFinished(1000)) { + throw "Shell variable execution timeout"; + } + auto res = process.readAllStandardOutput().trimmed(); + map.insert(cmd, res); + } + return TaskHelper::replaceAllSepcialVariables("shell", p_text, map); +} + diff --git a/src/core/taskvariablemgr.h b/src/core/taskvariablemgr.h new file mode 100644 index 00000000..5ab1c50f --- /dev/null +++ b/src/core/taskvariablemgr.h @@ -0,0 +1,73 @@ +#ifndef TASKVARIABLEMGR_H +#define TASKVARIABLEMGR_H + +#include + +#include +#include +#include + +namespace vnotex { + +class Task; +class TaskVariable; +class TaskVariableMgr; +class Notebook; + +class TaskVariable { +public: + enum Type + { + // Definition is plain text. + Simple = 0, + + // Definition is a function call to get the value. + FunctionBased, + + // Like FunctionBased, but should re-evaluate the value for each occurence. + Dynamic, + + Invalid + }; + typedef std::function Func; + TaskVariable(Type p_type, + const QString &p_name, + Func p_func = nullptr); +private: + Type m_type; + QString m_name; + Func m_func; +}; + +class TaskVariableMgr +{ +public: + TaskVariableMgr(); + void refresh(); + QString evaluate(const QString &p_text, + const Task *p_task) const; + + QStringList evaluate(const QStringList &p_list, + const Task *p_task) const; + +private: + void init(); + + void add(TaskVariable::Type p_type, + const QString &p_name, + TaskVariable::Func p_func = nullptr); + + QString evaluateInputVariables(const QString &p_text, + const Task *p_task) const; + + QString evaluateShellVariables(const QString &p_text, + const Task *p_task) const; + + QHash m_predefs; + bool m_initialized; +}; + +} // ns vnotex + +#endif // TASKVARIABLEMGR_H diff --git a/src/core/vnotex.cpp b/src/core/vnotex.cpp index 0259bdec..8edd9210 100644 --- a/src/core/vnotex.cpp +++ b/src/core/vnotex.cpp @@ -27,6 +27,8 @@ VNoteX::VNoteX(QObject *p_parent) initThemeMgr(); + initTaskMgr(); + initNotebookMgr(); initBufferMgr(); @@ -40,6 +42,7 @@ void VNoteX::initLoad() { qDebug() << "start init which may take a while"; m_notebookMgr->loadNotebooks(); + m_taskMgr->init(); } void VNoteX::initThemeMgr() @@ -56,11 +59,22 @@ void VNoteX::initThemeMgr() m_themeMgr = new ThemeMgr(configMgr.getCoreConfig().getTheme(), this); } +void VNoteX::initTaskMgr() +{ + Q_ASSERT(!m_taskMgr); + m_taskMgr = new TaskMgr(this); +} + ThemeMgr &VNoteX::getThemeMgr() const { return *m_themeMgr; } +TaskMgr &VNoteX::getTaskMgr() const +{ + return *m_taskMgr; +} + void VNoteX::setMainWindow(MainWindow *p_mainWindow) { Q_ASSERT(!m_mainWindow); diff --git a/src/core/vnotex.h b/src/core/vnotex.h index 87fe08ba..b805218d 100644 --- a/src/core/vnotex.h +++ b/src/core/vnotex.h @@ -6,6 +6,7 @@ #include "noncopyable.h" #include "thememgr.h" +#include "taskmgr.h" #include "global.h" namespace vnotex @@ -35,6 +36,8 @@ namespace vnotex ThemeMgr &getThemeMgr() const; + TaskMgr &getTaskMgr() const; + void setMainWindow(MainWindow *p_mainWindow); MainWindow *getMainWindow() const; @@ -79,6 +82,9 @@ namespace vnotex // Requested to new a folder in current notebook. void newFolderRequested(); + // Requested to show output message. + void showOutputRequested(const QString &p_text); + // Requested to show status message. void statusMessageRequested(const QString &p_message, int p_timeoutMilliseconds); @@ -116,6 +122,8 @@ namespace vnotex void initThemeMgr(); + void initTaskMgr(); + void initNotebookMgr(); void initBufferMgr(); @@ -129,6 +137,9 @@ namespace vnotex // QObject managed. ThemeMgr *m_themeMgr; + // QObject managed. + TaskMgr *m_taskMgr; + // QObject managed. NotebookMgr *m_notebookMgr; diff --git a/src/data/core/core.qrc b/src/data/core/core.qrc index ce546a87..766cfe9b 100644 --- a/src/data/core/core.qrc +++ b/src/data/core/core.qrc @@ -8,6 +8,7 @@ icons/import_notebook_of_vnote2.svg icons/new_notebook.svg icons/notebook_menu.svg + icons/task_menu.svg icons/advanced_settings.svg icons/new_notebook_from_folder.svg icons/discard_editor.svg diff --git a/src/data/core/icons/task_menu.svg b/src/data/core/icons/task_menu.svg new file mode 100644 index 00000000..8a3310fc --- /dev/null +++ b/src/data/core/icons/task_menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/extra/extra.qrc b/src/data/extra/extra.qrc index 2f09ac0d..a71d8793 100644 --- a/src/data/extra/extra.qrc +++ b/src/data/extra/extra.qrc @@ -150,6 +150,14 @@ themes/pure/up.svg themes/pure/up_disabled.svg themes/pure/web.css + tasks/git/git.json + tasks/git/git.svg + tasks/git/commit.svg + tasks/git/history.svg + tasks/git/initialization.svg + tasks/git/pull.svg + tasks/git/push.svg + tasks/git/status.svg syntax-highlighting/themes/markdown-default.theme syntax-highlighting/themes/markdown-breeze-dark.theme syntax-highlighting/themes/default.theme diff --git a/src/data/extra/tasks/git/commit.svg b/src/data/extra/tasks/git/commit.svg new file mode 100644 index 00000000..5052eee5 --- /dev/null +++ b/src/data/extra/tasks/git/commit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/extra/tasks/git/git.json b/src/data/extra/tasks/git/git.json new file mode 100644 index 00000000..4fd515c9 --- /dev/null +++ b/src/data/extra/tasks/git/git.json @@ -0,0 +1,80 @@ +{ + "version": "0.1.4", + "label": "Git", + "icon": "git.svg", + "tasks": [ + { + "label": { + "en_US": "Initialize", + "zh_CN": "初始化", + "ja_JP": "イニシャライズ" + }, + "icon": "initialization.svg", + "command": "git init -b main" + }, + { + "label": { + "en_US": "Status", + "zh_CN": "状态", + "ja_JP": "ステータス" + }, + "icon": "status.svg", + "command": "git status" + }, + { + "label": { + "en_US": "Commit", + "zh_CN": "提交", + "ja_JP": "全てコミット" + }, + "icon": "commit.svg", + "windows" :{ + "command": "git add -A -- . ; if ($?) { git commit --message=\"${input:msg}\" }" + }, + "command": "git add -A -- . && git commit --message=\"${input:msg}\"", + "inputs": [ + { + "id": "msg", + "type": "promptString", + "description": { + "en_US": "Please provide a commit message", + "zh_CN": "请输入提交信息", + "ja_JP": "コミットメッセージを提供してください" + }, + "default": { + "en_US": "Update note on ${magic:datetime}", + "zh_CN": "更新笔记于 ${magic:datetime}", + "ja_JP": "アップデート ${magic:datetime}" + } + } + ] + }, + { + "label": { + "en_US": "Push", + "zh_CN": "上传", + "ja_JP": "プッシュ" + }, + "icon": "push.svg", + "command": "git push" + }, + { + "label": { + "en_US": "Pull", + "zh_CN": "下载", + "ja_JP": "プル" + }, + "icon": "pull.svg", + "command": "git pull --no-rebase" + }, + { + "label": { + "en_US": "Log", + "zh_CN": "日志", + "ja_JP": "ログ" + }, + "icon": "history.svg", + "command": "git log -10 --graph --pretty=format:'%h -%d %s (%cr) <%an>' --abbrev-commit" + } + ] +} \ No newline at end of file diff --git a/src/data/extra/tasks/git/git.svg b/src/data/extra/tasks/git/git.svg new file mode 100644 index 00000000..68f6cc2a --- /dev/null +++ b/src/data/extra/tasks/git/git.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/extra/tasks/git/history.svg b/src/data/extra/tasks/git/history.svg new file mode 100644 index 00000000..254a703a --- /dev/null +++ b/src/data/extra/tasks/git/history.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/extra/tasks/git/initialization.svg b/src/data/extra/tasks/git/initialization.svg new file mode 100644 index 00000000..e64ba7ee --- /dev/null +++ b/src/data/extra/tasks/git/initialization.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/extra/tasks/git/pull.svg b/src/data/extra/tasks/git/pull.svg new file mode 100644 index 00000000..fa31a5ac --- /dev/null +++ b/src/data/extra/tasks/git/pull.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/extra/tasks/git/push.svg b/src/data/extra/tasks/git/push.svg new file mode 100644 index 00000000..ff17e9b8 --- /dev/null +++ b/src/data/extra/tasks/git/push.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/extra/tasks/git/status.svg b/src/data/extra/tasks/git/status.svg new file mode 100644 index 00000000..42778ef8 --- /dev/null +++ b/src/data/extra/tasks/git/status.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/utils/fileutils.cpp b/src/utils/fileutils.cpp index 02acfe78..65cec1e0 100644 --- a/src/utils/fileutils.cpp +++ b/src/utils/fileutils.cpp @@ -357,3 +357,22 @@ void FileUtils::removeEmptyDir(const QString &p_dirPath) removeDirIfEmpty(childPath); } } + +QStringList FileUtils::entryListRecursively(const QString &p_dirPath, + const QStringList &p_nameFilters, + QDir::Filters p_filters) +{ + QDir dir(p_dirPath); + if (dir.isEmpty()) return {}; + QStringList entrys; + const auto curEntrys = dir.entryList(p_nameFilters, p_filters | QDir::NoDotAndDotDot); + for (const auto &e : curEntrys) { + entrys.append(PathUtils::concatenateFilePath(p_dirPath, e)); + } + auto subdirs = dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); + for (const auto &subdir : subdirs) { + const auto dirPath = PathUtils::concatenateFilePath(p_dirPath, subdir); + entrys.append(entryListRecursively(dirPath, p_nameFilters, p_filters)); + } + return entrys; +} diff --git a/src/utils/fileutils.h b/src/utils/fileutils.h index edf8ae2b..ad1a6cb2 100644 --- a/src/utils/fileutils.h +++ b/src/utils/fileutils.h @@ -6,6 +6,7 @@ #include #include #include +#include class QTemporaryFile; @@ -77,6 +78,12 @@ namespace vnotex // Go through @p_dirPath recursively and delete all empty dirs. // @p_dirPath itself is not deleted. static void removeEmptyDir(const QString &p_dirPath); + + // Go through @p_dirPath recursively and get all entrys. + // @p_nameFilters is for each dir, not for all. + static QStringList entryListRecursively(const QString &p_dirPath, + const QStringList &p_nameFilters, + QDir::Filters p_filters=QDir::NoFilter); }; } // ns vnotex diff --git a/src/utils/iconutils.cpp b/src/utils/iconutils.cpp index cc871d7d..946a8d9c 100644 --- a/src/utils/iconutils.cpp +++ b/src/utils/iconutils.cpp @@ -59,8 +59,8 @@ QString IconUtils::replaceForegroundOfIcon(const QString &p_iconContent, const Q return p_iconContent; } - // Must have a # to avoid fill="none". - QRegExp styleRe("(\\s|\"|;)(fill|stroke)(:|(=\"))#[^#\"\\s]+"); + // Negative lookahead to avoid fill="none". + QRegExp styleRe(R"((\s|"|;)(fill|stroke)(:|(="))(?!none)[^;"]*)"); if (p_iconContent.indexOf(styleRe) > -1) { auto newContent(p_iconContent); newContent.replace(styleRe, QString("\\1\\2\\3%1").arg(p_foreground)); diff --git a/src/widgets/dialogs/selectdialog.cpp b/src/widgets/dialogs/selectdialog.cpp index 9bb3878f..878ff565 100644 --- a/src/widgets/dialogs/selectdialog.cpp +++ b/src/widgets/dialogs/selectdialog.cpp @@ -22,11 +22,28 @@ SelectDialog::SelectDialog(const QString &p_title, QWidget *p_parent) setupUI(p_title); } -void SelectDialog::setupUI(const QString &p_title) +SelectDialog::SelectDialog(const QString &p_title, + const QString &p_text, + QWidget *p_parent) + : QDialog(p_parent) +{ + const auto &themeMgr = VNoteX::getInst().getThemeMgr(); + m_shortcutIconForeground = themeMgr.paletteColor(QStringLiteral("widgets#quickselector#item_icon#fg")); + m_shortcutIconBorder = themeMgr.paletteColor(QStringLiteral("widgets#quickselector#item_icon#border")); + + setupUI(p_title, p_text); +} + +void SelectDialog::setupUI(const QString &p_title, const QString &p_text) { auto mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); + if (!p_text.isNull()) { + m_label = new QLabel(p_text, this); + mainLayout->addWidget(m_label); + } + m_list = new QListWidget(this); m_list->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_list->setSelectionMode(QAbstractItemView::SingleSelection); diff --git a/src/widgets/dialogs/selectdialog.h b/src/widgets/dialogs/selectdialog.h index a70899df..464c04c7 100644 --- a/src/widgets/dialogs/selectdialog.h +++ b/src/widgets/dialogs/selectdialog.h @@ -10,6 +10,7 @@ class QListWidget; class QListWidgetItem; class QShowEvent; class QKeyEvent; +class QLabel; namespace vnotex { @@ -19,6 +20,10 @@ namespace vnotex public: SelectDialog(const QString &p_title, QWidget *p_parent = nullptr); + SelectDialog(const QString &p_title, + const QString &p_text, + QWidget *p_parent = nullptr); + // @p_selectID should >= 0. void addSelection(const QString &p_selectStr, int p_selectID); @@ -37,12 +42,14 @@ namespace vnotex private: enum { CANCEL_ID = -1 }; - void setupUI(const QString &p_title); + void setupUI(const QString &p_title, const QString &p_text = QString()); void updateSize(); int m_choice = CANCEL_ID; + QLabel *m_label = nullptr; + QListWidget *m_list = nullptr; QMap m_shortcuts; diff --git a/src/widgets/dockwidgethelper.cpp b/src/widgets/dockwidgethelper.cpp index c7ddaabb..06139bb7 100644 --- a/src/widgets/dockwidgethelper.cpp +++ b/src/widgets/dockwidgethelper.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -112,6 +113,8 @@ void DockWidgetHelper::setupDocks() setupOutlineDock(); + setupOutputDock(); + setupLocationListDock(); setupShortcuts(); @@ -145,6 +148,19 @@ void DockWidgetHelper::setupOutlineDock() m_mainWindow->addDockWidget(Qt::RightDockWidgetArea, dock); } +void DockWidgetHelper::setupOutputDock() +{ + auto dock = createDockWidget(DockIndex::OutputDock, tr("Output"), m_mainWindow); + + dock->setObjectName(QStringLiteral("OutputDock.vnotex")); + dock->setAllowedAreas(Qt::BottomDockWidgetArea); + + dock->setWidget(m_mainWindow->m_outputViewer); + dock->setFocusProxy(m_mainWindow->m_outputViewer); + dock->hide(); + m_mainWindow->addDockWidget(Qt::BottomDockWidgetArea, dock); +} + void DockWidgetHelper::setupSearchDock() { auto dock = createDockWidget(DockIndex::SearchDock, tr("Search"), m_mainWindow); diff --git a/src/widgets/dockwidgethelper.h b/src/widgets/dockwidgethelper.h index e71e4001..8dbbb74a 100644 --- a/src/widgets/dockwidgethelper.h +++ b/src/widgets/dockwidgethelper.h @@ -29,6 +29,7 @@ namespace vnotex SearchDock, SnippetDock, OutlineDock, + OutputDock, LocationListDock, MaxDock }; @@ -97,6 +98,8 @@ namespace vnotex void setupOutlineDock(); + void setupOutputDock(); + void setupSearchDock(); void setupSnippetDock(); diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index f6f08381..049fe668 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -257,6 +257,8 @@ void MainWindow::setupDocks() setupOutlineViewer(); + setupOutputViewer(); + setupHistoryPanel(); setupSearchPanel(); @@ -512,11 +514,36 @@ void MainWindow::setupOutlineViewer() this, &MainWindow::focusViewArea); } +void MainWindow::setupOutputViewer() +{ + m_outputViewer = new QTextEdit(this); + m_outputViewer->setObjectName("OutputViewer.vnotex"); + m_outputViewer->setReadOnly(true); + + connect(&VNoteX::getInst(), &VNoteX::showOutputRequested, + m_outputViewer, [this](const QString &p_text) { + auto cursor = m_outputViewer->textCursor(); + cursor.movePosition(QTextCursor::End); + m_outputViewer->setTextCursor(cursor); + m_outputViewer->insertPlainText(p_text); + auto scrollBar = m_outputViewer->verticalScrollBar(); + if (scrollBar) { + scrollBar->setSliderPosition(scrollBar->maximum()); + } + m_dockWidgetHelper.getDock(DockWidgetHelper::OutputDock)->show(); + }); +} + const QVector &MainWindow::getDocks() const { return m_dockWidgetHelper.getDocks(); } +ViewArea *MainWindow::getViewArea() const +{ + return m_viewArea; +} + void MainWindow::focusViewArea() { m_viewArea->focus(); diff --git a/src/widgets/mainwindow.h b/src/widgets/mainwindow.h index c5d706a4..58428bec 100644 --- a/src/widgets/mainwindow.h +++ b/src/widgets/mainwindow.h @@ -13,6 +13,7 @@ class QDockWidget; class QSystemTrayIcon; class QTimer; class QLabel; +class QTextEdit; namespace vnotex { @@ -49,6 +50,8 @@ namespace vnotex const QVector &getDocks() const; + ViewArea *getViewArea() const; + void setContentAreaExpanded(bool p_expanded); // Should be called after MainWindow is shown. bool isContentAreaExpanded() const; @@ -103,6 +106,8 @@ namespace vnotex void setupOutlineViewer(); + void setupOutputViewer(); + void setupSearchPanel(); void setupLocationList(); @@ -162,6 +167,8 @@ namespace vnotex OutlineViewer *m_outlineViewer = nullptr; + QTextEdit *m_outputViewer = nullptr; + LocationList *m_locationList = nullptr; SearchPanel *m_searchPanel = nullptr; diff --git a/src/widgets/markdownviewwindow.h b/src/widgets/markdownviewwindow.h index a0e306dd..08a1d2ce 100644 --- a/src/widgets/markdownviewwindow.h +++ b/src/widgets/markdownviewwindow.h @@ -42,6 +42,8 @@ namespace vnotex QString getLatestContent() const Q_DECL_OVERRIDE; + QString selectedText() const Q_DECL_OVERRIDE; + void setMode(ViewWindowMode p_mode) Q_DECL_OVERRIDE; QSharedPointer getOutlineProvider() Q_DECL_OVERRIDE; @@ -101,8 +103,6 @@ namespace vnotex QPoint getFloatingWidgetPosition() Q_DECL_OVERRIDE; - QString selectedText() const Q_DECL_OVERRIDE; - void updateViewModeMenu(QMenu *p_menu) Q_DECL_OVERRIDE; private: diff --git a/src/widgets/textviewwindow.h b/src/widgets/textviewwindow.h index a8e90ceb..be409f1c 100644 --- a/src/widgets/textviewwindow.h +++ b/src/widgets/textviewwindow.h @@ -25,6 +25,8 @@ namespace vnotex QString getLatestContent() const Q_DECL_OVERRIDE; + QString selectedText() const Q_DECL_OVERRIDE; + void setMode(ViewWindowMode p_mode) Q_DECL_OVERRIDE; void openTwice(const QSharedPointer &p_paras) Q_DECL_OVERRIDE; @@ -68,8 +70,6 @@ namespace vnotex QPoint getFloatingWidgetPosition() Q_DECL_OVERRIDE; - QString selectedText() const Q_DECL_OVERRIDE; - private: void setupUI(); diff --git a/src/widgets/toolbarhelper.cpp b/src/widgets/toolbarhelper.cpp index df5bf56e..f6d7e7e6 100644 --- a/src/widgets/toolbarhelper.cpp +++ b/src/widgets/toolbarhelper.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "propertydefs.h" #include "dialogs/settings/settingsdialog.h" #include "dialogs/updater.h" @@ -282,6 +283,78 @@ QToolBar *ToolBarHelper::setupQuickAccessToolBar(MainWindow *p_win, QToolBar *p_ return tb; } +void ToolBarHelper::setupTaskMenu(QMenu *p_menu) +{ + p_menu->clear(); + const auto &taskMgr = VNoteX::getInst().getTaskMgr(); + for (auto task : taskMgr.getAppTasks()) { + addTaskMenu(p_menu, task); + } + p_menu->addSeparator(); + for (auto task : taskMgr.getUserTasks()) { + addTaskMenu(p_menu, task); + } + p_menu->addSeparator(); + for (auto task : taskMgr.getNotebookTasks()) { + addTaskMenu(p_menu, task); + } +} + +void ToolBarHelper::addTaskMenu(QMenu *p_menu, Task *p_task) +{ + MainWindow::connect(p_task, &Task::showOutput, + &VNoteX::getInst(), &VNoteX::showOutputRequested); + QAction *action = nullptr; + const auto &tasks = p_task->getTasks(); + auto label = p_task->getLabel(); + label = label.replace("&", "&&"); + QIcon icon; + try { + auto taskIcon = p_task->getIcon(); + if (!taskIcon.isEmpty()) { + icon = generateIcon(p_task->getIcon()); + } + } catch (Exception e) { + if (e.m_type != Exception::Type::FailToReadFile) { + throw; + } + } + if (tasks.isEmpty()) { + action = p_menu->addAction(label); + } else { + auto menu = p_menu->addMenu(label); + for (auto task : tasks) { + addTaskMenu(menu, task); + } + action = menu->menuAction(); + } + action->setIcon(icon); + WidgetUtils::addActionShortcut(action, p_task->getShortcut()); + MainWindow::connect(action, &QAction::triggered, + p_task, &Task::run); +} + +QToolBar *ToolBarHelper::setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar) +{ + auto tb = p_toolBar; + if (!tb) { + tb = createToolBar(p_win, MainWindow::tr("Task"), "TaskToolBar"); + } + + auto act = tb->addAction(generateIcon("task_menu.svg"), MainWindow::tr("Task")); + auto btn = dynamic_cast(tb->widgetForAction(act)); + btn->setPopupMode(QToolButton::InstantPopup); + btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); + + auto taskMenu = WidgetsFactory::createMenu(tb); + MainWindow::connect(&VNoteX::getInst().getTaskMgr(), &TaskMgr::taskChanged, + taskMenu, [taskMenu]() { + setupTaskMenu(taskMenu); + }); + btn->setMenu(taskMenu); + return tb; +} + QToolBar *ToolBarHelper::setupSettingsToolBar(MainWindow *p_win, QToolBar *p_toolBar) { auto tb = p_toolBar; @@ -344,6 +417,8 @@ void ToolBarHelper::setupToolBars(MainWindow *p_mainWindow) setupQuickAccessToolBar(p_mainWindow, nullptr); + setupTaskToolBar(p_mainWindow, nullptr); + setupSettingsToolBar(p_mainWindow, nullptr); } @@ -355,6 +430,7 @@ void ToolBarHelper::setupToolBars(MainWindow *p_mainWindow, QToolBar *p_toolBar) setupFileToolBar(p_mainWindow, p_toolBar); setupQuickAccessToolBar(p_mainWindow, p_toolBar); + setupTaskToolBar(p_mainWindow, p_toolBar); setupSettingsToolBar(p_mainWindow, p_toolBar); } diff --git a/src/widgets/toolbarhelper.h b/src/widgets/toolbarhelper.h index c31a7652..fadabae6 100644 --- a/src/widgets/toolbarhelper.h +++ b/src/widgets/toolbarhelper.h @@ -9,6 +9,7 @@ class QMenu; namespace vnotex { class MainWindow; + class Task; // Tool bar helper for MainWindow. class ToolBarHelper @@ -33,6 +34,12 @@ namespace vnotex static QToolBar *setupQuickAccessToolBar(MainWindow *p_win, QToolBar *p_toolBar); + static void setupTaskMenu(QMenu *p_menu); + + static void addTaskMenu(QMenu *p_menu, Task *p_task); + + static QToolBar *setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar); + static QToolBar *setupSettingsToolBar(MainWindow *p_win, QToolBar *p_toolBar); static void updateQuickAccessMenu(QMenu *p_menu);