From 70c984353c36e355e2f0261534d6f0e05d95d029 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 10 Dec 2021 20:32:56 +0800 Subject: [PATCH] Task: refine task --- src/core/configmgr.cpp | 43 +- src/core/configmgr.h | 7 + src/core/core.pri | 10 - src/core/editorconfig.cpp | 6 +- src/core/notebookmgr.cpp | 5 + src/core/notebookmgr.h | 2 + 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 | 3 + src/core/vnotex.h | 2 +- src/data/core/icons/task_menu.svg | 2 +- src/data/extra/tasks/git/git.json | 6 +- src/export/webviewexporter.cpp | 4 +- src/snippet/snippetmgr.cpp | 31 +- src/snippet/snippetmgr.h | 9 +- src/src.pro | 2 + src/task/shellexecution.cpp | 72 +++ src/task/shellexecution.h | 34 + src/task/task.cpp | 588 ++++++++++++++++++ src/task/task.h | 197 ++++++ src/task/task.pri | 13 + src/task/taskmgr.cpp | 111 ++++ src/task/taskmgr.h | 63 ++ src/task/taskvariablemgr.cpp | 435 +++++++++++++ src/task/taskvariablemgr.h | 105 ++++ src/utils/fileutils.cpp | 22 +- src/utils/fileutils.h | 2 +- src/utils/pathutils.cpp | 1 - src/utils/utils.cpp | 55 +- src/utils/utils.h | 4 + src/widgets/dialogs/selectdialog.cpp | 7 +- src/widgets/dockwidgethelper.cpp | 17 +- src/widgets/dockwidgethelper.h | 4 +- .../framelessmainwindowlinux.cpp | 6 - .../framelessmainwindowlinux.h | 2 +- .../framelessmainwindowwin.cpp | 6 - .../framelessmainwindowwin.h | 2 +- src/widgets/mainwindow.cpp | 23 +- src/widgets/mainwindow.h | 5 +- src/widgets/notebooknodeexplorer.cpp | 2 +- src/widgets/terminalviewer.cpp | 62 ++ src/widgets/terminalviewer.h | 33 + src/widgets/toolbarhelper.cpp | 93 ++- src/widgets/toolbarhelper.h | 2 + src/widgets/viewwindow.h | 4 +- src/widgets/widgets.pri | 2 + tests/commonfull.pri | 29 + .../test_core/test_notebook/test_notebook.pro | 23 +- tests/test_task/test_task.cpp | 77 +++ tests/test_task/test_task.h | 31 + tests/test_task/test_task.pro | 10 + tests/test_utils/test_utils.cpp | 64 ++ tests/test_utils/test_utils.h | 12 + tests/test_utils/test_utils.pro | 2 + tests/tests.pro | 3 +- 63 files changed, 2207 insertions(+), 1995 deletions(-) delete mode 100644 src/core/shellexecution.cpp delete mode 100644 src/core/shellexecution.h delete mode 100644 src/core/task.cpp delete mode 100644 src/core/task.h delete mode 100644 src/core/taskhelper.cpp delete mode 100644 src/core/taskhelper.h delete mode 100644 src/core/taskmgr.cpp delete mode 100644 src/core/taskmgr.h delete mode 100644 src/core/taskvariablemgr.cpp delete mode 100644 src/core/taskvariablemgr.h create mode 100644 src/task/shellexecution.cpp create mode 100644 src/task/shellexecution.h create mode 100644 src/task/task.cpp create mode 100644 src/task/task.h create mode 100644 src/task/task.pri create mode 100644 src/task/taskmgr.cpp create mode 100644 src/task/taskmgr.h create mode 100644 src/task/taskvariablemgr.cpp create mode 100644 src/task/taskvariablemgr.h create mode 100644 src/widgets/terminalviewer.cpp create mode 100644 src/widgets/terminalviewer.h create mode 100644 tests/commonfull.pri create mode 100644 tests/test_task/test_task.cpp create mode 100644 tests/test_task/test_task.h create mode 100644 tests/test_task/test_task.pro diff --git a/src/core/configmgr.cpp b/src/core/configmgr.cpp index dce34710..10531a5f 100644 --- a/src/core/configmgr.cpp +++ b/src/core/configmgr.cpp @@ -36,6 +36,10 @@ const QString ConfigMgr::c_configFileName = "vnotex.json"; const QString ConfigMgr::c_sessionFileName = "session.json"; +const QString ConfigMgr::c_userFilesFolder = "user_files"; + +const QString ConfigMgr::c_appFilesFolder = "vnotex_files"; + const QJsonObject &ConfigMgr::Settings::getJson() const { return m_jobj; @@ -68,17 +72,21 @@ ConfigMgr::ConfigMgr(bool p_isUnitTest, QObject *p_parent) qWarning() << "failed to init ConfigMgr for UnitTest"; return; } - m_appConfigFolderPath = m_dirForUnitTest->filePath("vnotex_files"); - m_userConfigFolderPath = m_dirForUnitTest->filePath("user_files"); - FileUtils::copyFile(getConfigFilePath(Source::Default), PathUtils::concatenateFilePath(m_appConfigFolderPath, c_configFileName)); - } else { - locateConfigFolder(); + QDir dir(m_dirForUnitTest->path()); + dir.mkdir(c_appFilesFolder); + dir.mkdir(c_userFilesFolder); - bool needUpdate = checkAppConfig(); - if (needUpdate) { - checkUserConfig(); - } + m_appConfigFolderPath = m_dirForUnitTest->filePath(c_appFilesFolder); + m_userConfigFolderPath = m_dirForUnitTest->filePath(c_userFilesFolder); + return; + } + + locateConfigFolder(); + + bool needUpdate = checkAppConfig(); + if (needUpdate) { + checkUserConfig(); } m_config->init(); @@ -106,8 +114,7 @@ void ConfigMgr::locateConfigFolder() qInfo() << "app folder" << appDirPath; // Check app config. { - const QString configFolderName("vnotex_files"); - QString folderPath(appDirPath + '/' + configFolderName); + QString folderPath(appDirPath + '/' + c_appFilesFolder); if (QDir(folderPath).exists()) { // Config folder in app/. m_appConfigFolderPath = PathUtils::cleanPath(folderPath); @@ -118,8 +125,7 @@ void ConfigMgr::locateConfigFolder() // Check user config. { - const QString configFolderName("user_files"); - QString folderPath(appDirPath + '/' + configFolderName); + QString folderPath(appDirPath + '/' + c_userFilesFolder); if (QDir(folderPath).exists()) { // Config folder in app/. m_userConfigFolderPath = PathUtils::cleanPath(folderPath); @@ -564,3 +570,14 @@ QString ConfigMgr::getApplicationVersion() return appVersion; } + +QJsonValue ConfigMgr::parseAndReadConfig(const QString &p_exp) const +{ + if (p_exp.startsWith(QStringLiteral("main."))) { + return Utils::parseAndReadJson(m_config->toJson(), p_exp.mid(5)); + } else if (p_exp.startsWith(QStringLiteral("session."))) { + return Utils::parseAndReadJson(m_sessionConfig->toJson(), p_exp.mid(8)); + } else { + return QJsonValue(); + } +} diff --git a/src/core/configmgr.h b/src/core/configmgr.h index baed8f27..c397dd93 100644 --- a/src/core/configmgr.h +++ b/src/core/configmgr.h @@ -106,6 +106,9 @@ namespace vnotex QString getConfigFilePath(Source p_src) const; + // Parse exp like "[main|session].core.shortcuts.FullScreen" and return the config value. + QJsonValue parseAndReadConfig(const QString &p_exp) const; + // Called at boostrap without QApplication instance. static QString locateSessionConfigFilePathAtBootstrap(); @@ -168,6 +171,10 @@ namespace vnotex // Name of the session config file. static const QString c_sessionFileName; + + static const QString c_userFilesFolder; + + static const QString c_appFilesFolder; }; } // ns vnotex diff --git a/src/core/core.pri b/src/core/core.pri index 2f9e2ac6..6c0eb8eb 100644 --- a/src/core/core.pri +++ b/src/core/core.pri @@ -29,11 +29,6 @@ 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 \ @@ -65,11 +60,6 @@ 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/editorconfig.cpp b/src/core/editorconfig.cpp index bfaa0d51..4453ff46 100644 --- a/src/core/editorconfig.cpp +++ b/src/core/editorconfig.cpp @@ -150,7 +150,11 @@ QJsonObject EditorConfig::toJson() const obj[m_markdownEditorConfig->getSessionName()] = m_markdownEditorConfig->toJson(); obj[QStringLiteral("core")] = saveCore(); obj[QStringLiteral("image_host")] = saveImageHost(); - obj[QStringLiteral("vi")] = m_viConfig->toJson(); + + // In UT, it may be nullptr. + if (m_viConfig) { + obj[QStringLiteral("vi")] = m_viConfig->toJson(); + } return obj; } diff --git a/src/core/notebookmgr.cpp b/src/core/notebookmgr.cpp index 04408ddb..5a434ee9 100644 --- a/src/core/notebookmgr.cpp +++ b/src/core/notebookmgr.cpp @@ -265,6 +265,11 @@ ID NotebookMgr::getCurrentNotebookId() const return m_currentNotebookId; } +QSharedPointer NotebookMgr::getCurrentNotebook() const +{ + return findNotebookById(m_currentNotebookId); +} + void NotebookMgr::setCurrentNotebook(ID p_notebookId) { auto lastId = m_currentNotebookId; diff --git a/src/core/notebookmgr.h b/src/core/notebookmgr.h index 77186dc4..667d6018 100644 --- a/src/core/notebookmgr.h +++ b/src/core/notebookmgr.h @@ -62,6 +62,8 @@ namespace vnotex ID getCurrentNotebookId() const; + QSharedPointer getCurrentNotebook() const; + // Find the notebook with the same directory as root folder. QSharedPointer findNotebookByRootFolderPath(const QString &p_rootFolderPath) const; diff --git a/src/core/shellexecution.cpp b/src/core/shellexecution.cpp deleted file mode 100644 index 9a734a13..00000000 --- a/src/core/shellexecution.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#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 deleted file mode 100644 index 3fdebf7e..00000000 --- a/src/core/shellexecution.h +++ /dev/null @@ -1,42 +0,0 @@ -#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 deleted file mode 100644 index 118c0c7a..00000000 --- a/src/core/task.cpp +++ /dev/null @@ -1,557 +0,0 @@ -#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 deleted file mode 100644 index 26f13fce..00000000 --- a/src/core/task.h +++ /dev/null @@ -1,185 +0,0 @@ -#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 deleted file mode 100644 index 42811a95..00000000 --- a/src/core/taskhelper.cpp +++ /dev/null @@ -1,233 +0,0 @@ -#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 deleted file mode 100644 index 30ab83f6..00000000 --- a/src/core/taskhelper.h +++ /dev/null @@ -1,50 +0,0 @@ -#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 deleted file mode 100644 index 0ea58baf..00000000 --- a/src/core/taskmgr.cpp +++ /dev/null @@ -1,192 +0,0 @@ -#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 deleted file mode 100644 index b6560624..00000000 --- a/src/core/taskmgr.h +++ /dev/null @@ -1,79 +0,0 @@ -#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 deleted file mode 100644 index c659b200..00000000 --- a/src/core/taskvariablemgr.cpp +++ /dev/null @@ -1,334 +0,0 @@ -#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 deleted file mode 100644 index 5ab1c50f..00000000 --- a/src/core/taskvariablemgr.h +++ /dev/null @@ -1,73 +0,0 @@ -#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 8edd9210..99b95fab 100644 --- a/src/core/vnotex.cpp +++ b/src/core/vnotex.cpp @@ -14,6 +14,7 @@ #include "quickaccesshelper.h" #include +#include using namespace vnotex; @@ -63,6 +64,8 @@ void VNoteX::initTaskMgr() { Q_ASSERT(!m_taskMgr); m_taskMgr = new TaskMgr(this); + connect(m_taskMgr, &TaskMgr::taskOutputRequested, + this, &VNoteX::showOutputRequested); } ThemeMgr &VNoteX::getThemeMgr() const diff --git a/src/core/vnotex.h b/src/core/vnotex.h index b805218d..5f63ff8e 100644 --- a/src/core/vnotex.h +++ b/src/core/vnotex.h @@ -6,7 +6,6 @@ #include "noncopyable.h" #include "thememgr.h" -#include "taskmgr.h" #include "global.h" namespace vnotex @@ -19,6 +18,7 @@ namespace vnotex class Event; class Notebook; struct ComplexLocation; + class TaskMgr; class VNoteX : public QObject, private Noncopyable { diff --git a/src/data/core/icons/task_menu.svg b/src/data/core/icons/task_menu.svg index 8a3310fc..6416ad89 100644 --- a/src/data/core/icons/task_menu.svg +++ b/src/data/core/icons/task_menu.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/data/extra/tasks/git/git.json b/src/data/extra/tasks/git/git.json index 4fd515c9..d9aeac69 100644 --- a/src/data/extra/tasks/git/git.json +++ b/src/data/extra/tasks/git/git.json @@ -37,12 +37,12 @@ "id": "msg", "type": "promptString", "description": { - "en_US": "Please provide a commit message", + "en_US": "Please input the commit message", "zh_CN": "请输入提交信息", "ja_JP": "コミットメッセージを提供してください" }, "default": { - "en_US": "Update note on ${magic:datetime}", + "en_US": "Update note at ${magic:datetime}", "zh_CN": "更新笔记于 ${magic:datetime}", "ja_JP": "アップデート ${magic:datetime}" } @@ -77,4 +77,4 @@ "command": "git log -10 --graph --pretty=format:'%h -%d %s (%cr) <%an>' --abbrev-commit" } ] -} \ No newline at end of file +} diff --git a/src/export/webviewexporter.cpp b/src/export/webviewexporter.cpp index d5070e3b..ce547db1 100644 --- a/src/export/webviewexporter.cpp +++ b/src/export/webviewexporter.cpp @@ -192,13 +192,15 @@ bool WebViewExporter::writeHtmlFile(const QString &p_file, bool p_embedImages) { const auto baseName = QFileInfo(p_file).completeBaseName(); - auto title = QString("%1 - %2").arg(baseName, ConfigMgr::c_appName); + const QString resourceFolderName = baseName + "_files"; auto resourceFolder = PathUtils::concatenateFilePath(PathUtils::parentDirPath(p_file), resourceFolderName); qDebug() << "HTML files folder" << resourceFolder; auto htmlContent = m_exportHtmlTemplate; + + const auto title = QString("%1").arg(baseName); HtmlTemplateHelper::fillTitle(htmlContent, title); if (!p_styleContent.isEmpty() && p_embedStyles) { diff --git a/src/snippet/snippetmgr.cpp b/src/snippet/snippetmgr.cpp index 1421645a..b05e8a6b 100644 --- a/src/snippet/snippetmgr.cpp +++ b/src/snippet/snippetmgr.cpp @@ -20,7 +20,9 @@ using namespace vnotex; -const QString SnippetMgr::c_snippetSymbolRegExp = QString("%([^%]+)%"); +const QChar SnippetMgr::c_snippetSymbolGuard = QLatin1Char('%'); + +const QString SnippetMgr::c_snippetSymbolRegExp = QString("%1([^%]+)%1").arg(c_snippetSymbolGuard); SnippetMgr::SnippetMgr() { @@ -231,10 +233,11 @@ void SnippetMgr::applySnippet(const QString &p_name, p_textEdit->setTextCursor(cursor); } -QString SnippetMgr::applySnippetBySymbol(const QString &p_content) const +QString SnippetMgr::applySnippetBySymbol(const QString &p_content, + const OverrideMap &p_overrides) const { int offset = 0; - return applySnippetBySymbol(p_content, QString(), offset); + return applySnippetBySymbol(p_content, QString(), offset, p_overrides); } QString SnippetMgr::applySnippetBySymbol(const QString &p_content, @@ -244,11 +247,11 @@ QString SnippetMgr::applySnippetBySymbol(const QString &p_content, { QString content(p_content); - int maxTimes = 100; + int maxTimesAtSamePos = 100; QRegularExpression regExp(c_snippetSymbolRegExp); int pos = 0; - while (pos < content.size() && maxTimes-- > 0) { + while (pos < content.size()) { QRegularExpressionMatch match; int idx = content.indexOf(regExp, pos, &match); if (idx == -1) { @@ -288,6 +291,13 @@ QString SnippetMgr::applySnippetBySymbol(const QString &p_content, } // @afterText may still contains snippet symbol. + if (pos == idx) { + if (--maxTimesAtSamePos == 0) { + break; + } + } else { + maxTimesAtSamePos = 100; + } pos = idx; } @@ -426,8 +436,10 @@ void SnippetMgr::addDynamicSnippet(QVector> &p_snippets, SnippetMgr::OverrideMap SnippetMgr::generateOverrides(const Buffer *p_buffer) { OverrideMap overrides; - overrides.insert(QStringLiteral("note"), p_buffer->getName()); - overrides.insert(QStringLiteral("no"), QFileInfo(p_buffer->getName()).completeBaseName()); + if (p_buffer) { + overrides.insert(QStringLiteral("note"), p_buffer->getName()); + overrides.insert(QStringLiteral("no"), QFileInfo(p_buffer->getName()).completeBaseName()); + } return overrides; } @@ -438,3 +450,8 @@ SnippetMgr::OverrideMap SnippetMgr::generateOverrides(const QString &p_fileName) overrides.insert(QStringLiteral("no"), QFileInfo(p_fileName).completeBaseName()); return overrides; } + +QString SnippetMgr::generateSnippetSymbol(const QString &p_snippetName) +{ + return c_snippetSymbolGuard + p_snippetName + c_snippetSymbolGuard; +} diff --git a/src/snippet/snippetmgr.h b/src/snippet/snippetmgr.h index 0e34b730..d3ea94b0 100644 --- a/src/snippet/snippetmgr.h +++ b/src/snippet/snippetmgr.h @@ -60,14 +60,19 @@ namespace vnotex int &p_cursorOffset, const OverrideMap &p_overrides = OverrideMap()) const; - QString applySnippetBySymbol(const QString &p_content) const; + QString applySnippetBySymbol(const QString &p_content, + const OverrideMap &p_overrides = OverrideMap()) const; // Generate standard overrides for given buffer. static OverrideMap generateOverrides(const Buffer *p_buffer); - // Generate standard overrides. + // Generate standard overrides for given file name. static OverrideMap generateOverrides(const QString &p_fileName); + static QString generateSnippetSymbol(const QString &p_snippetName); + + static const QChar c_snippetSymbolGuard; + // %name%. // Captured texts: // 1 - The name of the snippet. diff --git a/src/src.pro b/src/src.pro index 7f169cc0..d2557a92 100644 --- a/src/src.pro +++ b/src/src.pro @@ -56,6 +56,8 @@ include($$PWD/snippet/snippet.pri) include($$PWD/imagehost/imagehost.pri) +include($$PWD/task/task.pri) + include($$PWD/core/core.pri) include($$PWD/widgets/widgets.pri) diff --git a/src/task/shellexecution.cpp b/src/task/shellexecution.cpp new file mode 100644 index 00000000..1b421b7f --- /dev/null +++ b/src/task/shellexecution.cpp @@ -0,0 +1,72 @@ +#include "shellexecution.h" + +#include +#include + +using namespace vnotex; + +void ShellExecution::setupProcess(QProcess *p_process, + const QString &p_program, + const QStringList &p_args, + const QString &p_shellExec, + const QStringList &p_shellArgs) +{ + auto shellExec = p_shellExec.isNull() ? defaultShell() : p_shellExec; + auto shellArgs = p_shellArgs.isEmpty() ? defaultShellArguments(shellExec) : p_shellArgs; + + p_process->setProgram(shellExec); + + const auto shell = shellBasename(p_shellExec); + QStringList allArgs(shellArgs); + if (shell == "bash") { + allArgs << (QStringList() << p_program << quoteSpaces(p_args)).join(' '); + } else { + allArgs << p_program << p_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 QStringLiteral("PowerShell.exe"); +#else + return QStringLiteral("/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::quoteSpace(const QString &p_arg) +{ + if (p_arg.contains(QLatin1Char(' '))) { + return QLatin1Char('"') + p_arg + QLatin1Char('"'); + } else { + return p_arg; + } +} + +QStringList ShellExecution::quoteSpaces(const QStringList &p_args) +{ + QStringList args; + for (const auto &arg : p_args) { + args << quoteSpace(arg); + } + return args; +} diff --git a/src/task/shellexecution.h b/src/task/shellexecution.h new file mode 100644 index 00000000..188e5d17 --- /dev/null +++ b/src/task/shellexecution.h @@ -0,0 +1,34 @@ +#ifndef SHELLEXECUTION_H +#define SHELLEXECUTION_H + +#include + +class QProcess; + +namespace vnotex +{ + class ShellExecution + { + public: + ShellExecution() = delete; + + static void setupProcess(QProcess *p_process, + const QString &p_program, + const QStringList &p_args = QStringList(), + const QString &p_shellExec = QString(), + const QStringList &p_shellArgs = QStringList()); + + static QString defaultShell(); + + static QStringList defaultShellArguments(const QString &p_shell); + + private: + static QString shellBasename(const QString &p_shell); + + static QString quoteSpace(const QString &p_arg); + + static QStringList quoteSpaces(const QStringList &p_args); + }; +} + +#endif // SHELLEXECUTION_H diff --git a/src/task/task.cpp b/src/task/task.cpp new file mode 100644 index 00000000..5d973b35 --- /dev/null +++ b/src/task/task.cpp @@ -0,0 +1,588 @@ +#include "task.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "shellexecution.h" +#include "taskmgr.h" + +using namespace vnotex; + +QString Task::s_latestVersion = "0.1.3"; + +QSharedPointer Task::fromFile(const QString &p_file, const QString &p_locale, TaskMgr *p_taskMgr) +{ + QSharedPointer task(new Task(p_locale, p_file, p_taskMgr, nullptr)); + const auto obj = FileUtils::readJsonFile(p_file); + if (fromJson(task.data(), obj)) { + return task; + } + return nullptr; +} + +bool Task::fromJson(Task *p_task, const QJsonObject &p_obj) +{ + // For child task, it will inherit the version from parent. + if (p_obj.contains("version")) { + p_task->m_dto.version = p_obj["version"].toString(); + } + + const auto version = QVersionNumber::fromString(p_task->getVersion()); + if (version.isNull()) { + qWarning() << "invalid task" << p_task->m_dto._source; + return false; + } + + if (version < QVersionNumber(1, 0, 0)) { + return fromJsonV0(p_task, p_obj); + } else { + qWarning() << "unknown task version" << version << p_task->m_dto._source; + return false; + } +} + +bool Task::fromJsonV0(Task *p_task, const QJsonObject &p_obj, bool p_mergeTasks) +{ + if (p_obj.contains("type")) { + p_task->m_dto.type = p_obj["type"].toString(); + } + + if (p_obj.contains("icon")) { + QString iconPath = p_obj["icon"].toString(); + if (!iconPath.isEmpty()) { + if (QDir::isRelativePath(iconPath)) { + iconPath = QFileInfo(p_task->m_dto._source).dir().absoluteFilePath(iconPath); + } + + if (QFileInfo::exists(iconPath)) { + p_task->m_dto.icon = iconPath; + } else { + qWarning() << "task icon does not exist" << p_task->getLabel() << iconPath; + } + } + } + + if (p_obj.contains("shortcut")) { + p_task->m_dto.shortcut = p_obj["shortcut"].toString(); + } + + if (p_obj.contains("type")) { + p_task->m_dto.type = p_obj["type"].toString(); + } + + if (p_obj.contains("command")) { + p_task->m_dto.command = getLocaleString(p_obj["command"], p_task->m_locale); + } + + if (p_obj.contains("args")) { + p_task->m_dto.args = getLocaleStringList(p_obj["args"], p_task->m_locale); + } + + if (p_obj.contains("label")) { + p_task->m_dto.label = getLocaleString(p_obj["label"], p_task->m_locale); + } else if (p_task->m_dto.label.isNull() && !p_task->m_dto.command.isNull()) { + p_task->m_dto.label = p_task->m_dto.command; + } + + if (p_obj.contains("options")) { + auto options = p_obj["options"].toObject(); + + if (options.contains("cwd")) { + p_task->m_dto.options.cwd = options["cwd"].toString(); + } + + if (options.contains("env")) { + p_task->m_dto.options.env.clear(); + auto env = options["env"].toObject(); + for (auto it = env.begin(); it != env.end(); it++) { + auto value = getLocaleString(it.value(), p_task->m_locale); + p_task->m_dto.options.env.insert(it.key(), value); + } + } + + if (options.contains("shell") && p_task->getType() == "shell") { + auto shell = options["shell"].toObject(); + + if (shell.contains("executable")) { + p_task->m_dto.options.shell.executable = shell["executable"].toString(); + } + + if (shell.contains("args")) { + p_task->m_dto.options.shell.args.clear(); + + const auto arr = shell["args"].toArray(); + for (int i = 0; i < arr.size(); ++i) { + p_task->m_dto.options.shell.args << arr[i].toString(); + } + } + } + } + + if (p_obj.contains("tasks")) { + if (!p_mergeTasks) { + p_task->m_children.clear(); + } + + auto arr = p_obj["tasks"].toArray(); + for (int i = 0; i < arr.size(); ++i) { + QScopedPointer childTask(new Task(p_task->m_locale, p_task->getFile(), p_task->m_taskMgr, p_task)); + if (fromJson(childTask.data(), arr[i].toObject())) { + connect(childTask.data(), &Task::outputRequested, + p_task, &Task::outputRequested); + p_task->m_children.append(childTask.take()); + } + } + } + + if (p_obj.contains("inputs")) { + p_task->m_dto.inputs.clear(); + auto arr = p_obj["inputs"].toArray(); + for (int i = 0; i < arr.size(); ++i) { + const auto inputObj = arr[i].toObject(); + InputDTO input; + if (inputObj.contains("id")) { + input.id = inputObj["id"].toString(); + } else { + qWarning() << "Input configuration not contains id"; + } + + if (inputObj.contains("type")) { + input.type = inputObj["type"].toString(); + } else { + input.type = "promptString"; + } + + if (inputObj.contains("description")) { + input.description = getLocaleString(inputObj["description"], p_task->m_locale); + } + + if (inputObj.contains("default")) { + input.default_ = getLocaleString(inputObj["default"], p_task->m_locale); + } + + if (input.type == "promptString" && inputObj.contains("password")) { + input.password = inputObj["password"].toBool(); + } else { + input.password = false; + } + + if (input.type == "pickString") { + if (inputObj.contains("options")) { + input.options = getLocaleStringList(inputObj["options"], p_task->m_locale); + } + + if (!input.default_.isNull() && !input.options.contains(input.default_)) { + qWarning() << "default of input must be one of the option values"; + } + } + + p_task->m_dto.inputs << input; + } + } + + if (p_obj.contains("messages")) { + p_task->m_dto.messages.clear(); + auto arr = p_obj["messages"].toArray(); + for (int i = 0; i < arr.size(); ++i) { + const auto msgObj = arr[i].toObject(); + MessageDTO msg; + if (msgObj.contains("id")) { + msg.id = msgObj["id"].toString(); + } else { + qWarning() << "Message configuration not contain id"; + } + + if (msgObj.contains("type")) { + msg.type = msgObj["type"].toString(); + } else { + msg.type = "information"; + } + + if (msgObj.contains("title")) { + msg.title = getLocaleString(msgObj["title"], p_task->m_locale); + } + + if (msgObj.contains("text")) { + msg.text = getLocaleString(msgObj["text"], p_task->m_locale); + } + + if (msgObj.contains("detailedText")) { + msg.detailedText = getLocaleString(msgObj["detailedText"], p_task->m_locale); + } + + if (msgObj.contains("buttons")) { + auto buttonsArr = msgObj["buttons"].toArray(); + for (int j = 0; j < buttonsArr.size(); ++j) { + const auto btnObj = buttonsArr[j].toObject(); + ButtonDTO btn; + btn.text = getLocaleString(btnObj["text"], p_task->m_locale); + msg.buttons << btn; + } + } + + p_task->m_dto.messages << msg; + } + } + + // OS-specific task configuration +#if defined (Q_OS_WIN) + #define OS_SPEC "windows" +#elif defined (Q_OS_MACOS) + #define OS_SPEC "osx" +#else + #define OS_SPEC "linux" +#endif + + if (p_obj.contains(OS_SPEC)) { + const auto osObj = p_obj[OS_SPEC].toObject(); + fromJsonV0(p_task, osObj, true); + } + +#undef OS_SPEC + + return true; +} + +const QString &Task::getVersion() const +{ + return m_dto.version; +} + +const QString &Task::getType() const +{ + return m_dto.type; +} + +QString Task::getCommand() +{ + return variableMgr().evaluate(this, m_dto.command); +} + +QStringList Task::getArgs() +{ + return variableMgr().evaluate(this, m_dto.args); +} + +const QString &Task::getLabel() const +{ + return m_dto.label; +} + +const QString &Task::getIcon() const +{ + return m_dto.icon; +} + +const QString &Task::getShortcut() const +{ + return m_dto.shortcut; +} + +QString Task::getOptionsCwd() +{ + auto cwd = m_dto.options.cwd; + if (!cwd.isNull()) { + return variableMgr().evaluate(this, cwd); + } + + auto notebook = TaskVariableMgr::getCurrentNotebook(); + if (notebook) { + cwd = notebook->getRootFolderAbsolutePath(); + } + + if (!cwd.isNull()) { + return cwd; + } + + auto buffer = TaskVariableMgr::getCurrentBuffer(); + if (buffer) { + return QFileInfo(buffer->getPath()).dir().absolutePath(); + } + + return QFileInfo(m_dto._source).dir().absolutePath(); +} + +const QMap &Task::getOptionsEnv() const +{ + return m_dto.options.env; +} + +const QString &Task::getOptionsShellExecutable() const +{ + return m_dto.options.shell.executable; +} + +QStringList Task::getOptionsShellArgs() +{ + if (m_dto.options.shell.args.isEmpty()) { + return ShellExecution::defaultShellArguments(m_dto.options.shell.executable); + } else { + return variableMgr().evaluate(this, m_dto.options.shell.args); + } +} + +const QVector &Task::getChildren() const +{ + return m_children; +} + +const QVector &Task::getInputs() const +{ + return m_dto.inputs; +} + +const InputDTO *Task::findInput(const QString &p_id) const +{ + for (const auto &input : m_dto.inputs) { + if (input.id == p_id) { + return &input; + } + } + + qWarning() << "input" << p_id << "not found for task" << getLabel(); + return nullptr; +} + +const MessageDTO *Task::findMessage(const QString &p_id) const +{ + for (const auto &msg : m_dto.messages) { + if (msg.id == p_id) { + return &msg; + } + } + + qWarning() << "message" << p_id << "not found for task" << getLabel(); + return nullptr; +} + +const QString &Task::getFile() const +{ + return m_dto._source; +} + +Task::Task(const QString &p_locale, + const QString &p_file, + TaskMgr *p_taskMgr, + QObject *p_parent) + : QObject(p_parent), + m_taskMgr(p_taskMgr), + m_locale(p_locale) +{ + m_dto._source = p_file; + m_dto.version = s_latestVersion; + m_dto.type = "shell"; + m_dto.options.shell.executable = ShellExecution::defaultShell(); + + // Inherit configuration. + m_parent = qobject_cast(p_parent); + if (m_parent) { + m_dto.version = m_parent->m_dto.version; + m_dto.type = m_parent->m_dto.type; + m_dto.command = m_parent->m_dto.command; + m_dto.args = m_parent->m_dto.args; + m_dto.options.cwd = m_parent->m_dto.options.cwd; + m_dto.options.env = m_parent->m_dto.options.env; + m_dto.options.shell.executable = m_parent->m_dto.options.shell.executable; + m_dto.options.shell.args = m_parent->m_dto.options.shell.args; + // Do not inherit label/inputs/tasks. + } else { + m_dto.label = QFileInfo(p_file).baseName(); + } +} + +QProcess *Task::setupProcess() +{ + setCancelled(false); + + auto command = getCommand(); + if (command.isEmpty()) { + return nullptr; + } + + QScopedPointer scopedProcess(new QProcess(this)); + + auto process = scopedProcess.data(); + process->setWorkingDirectory(getOptionsCwd()); + + const auto &optionsEnv = getOptionsEnv(); + if (!optionsEnv.isEmpty()) { + auto env = QProcessEnvironment::systemEnvironment(); + for (auto it = optionsEnv.begin(); it != optionsEnv.end(); it++) { + env.insert(it.key(), it.value()); + } + process->setProcessEnvironment(env); + } + + const auto args = getArgs(); + const auto &type = getType(); + + if (type == "shell") { + ShellExecution::setupProcess(process, + command, + args, + getOptionsShellExecutable(), + getOptionsShellArgs()); + } else if (getType() == "process") { + process->setProgram(command); + process->setArguments(args); + } + + if (isCancelled()) { + return nullptr; + } + + scopedProcess.take(); + + connect(process, &QProcess::started, + this, [this]() { + emit outputRequested(tr("[Task (%1) started]\n").arg(getLabel())); + }); + connect(process, &QProcess::readyReadStandardOutput, + this, [this, process]() { + auto text = decodeText(process->readAllStandardOutput()); + // TODO: interaction with process. + emit outputRequested(text); + }); + connect(process, &QProcess::readyReadStandardError, + this, [this, process]() { + auto text = process->readAllStandardError(); + emit outputRequested(decodeText(text)); + }); + connect(process, &QProcess::errorOccurred, + this, [this](QProcess::ProcessError error) { + emit outputRequested(tr("[Task (%1) error occurred (%2)]\n").arg(getLabel()).arg(error)); + }); + connect(process, QOverload::of(&QProcess::finished), + this, [this, process](int exitCode) { + emit outputRequested(tr("\n[Task (%1) finished (%2)]\n").arg(getLabel()).arg(exitCode)); + process->deleteLater(); + }); + + return process; +} + +void Task::run() +{ + QProcess *process; + try { + process = setupProcess(); + } catch (const char *msg) { + qWarning() << "exception while setup process" << msg; + return ; + } + + if (process) { + qDebug() << "run task" << process->program() << process->arguments(); + process->start(); + } +} + +const TaskDTO &Task::getDTO() const +{ + return m_dto; +} + +QString Task::decodeText(const QByteArray &p_text) +{ + static QByteArrayList codecNames = { + "UTF-8", + "System", + "UTF-16", + "GB18030" + }; + + for (const auto &name : codecNames) { + auto text = decodeText(p_text, name); + if (!text.isNull()) { + return text; + } + } + + return QString::fromLocal8Bit(p_text); +} + +QString Task::decodeText(const QByteArray &p_text, const QByteArray &p_name) +{ + auto codec = QTextCodec::codecForName(p_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(); +} + +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() << "value of locale not found" << p_locale; + 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 strs; + const auto arr = p_value.toArray(); + for (int i = 0; i < arr.size(); ++i) { + strs << getLocaleString(arr[i], p_locale); + } + return strs; +} + +QStringList Task::getStringList(const QJsonValue &p_value) +{ + QStringList strs; + const auto arr = p_value.toArray(); + for (int i = 0; i < arr.size(); ++i) { + strs << arr[i].toString(); + } + return strs; +} + +const TaskVariableMgr &Task::variableMgr() const +{ + return m_taskMgr->getVariableMgr(); +} + +bool Task::isCancelled() const +{ + return m_cancelled; +} + +void Task::setCancelled(bool p_cancelled) +{ + m_cancelled = p_cancelled; +} diff --git a/src/task/task.h b/src/task/task.h new file mode 100644 index 00000000..75e3564d --- /dev/null +++ b/src/task/task.h @@ -0,0 +1,197 @@ +#ifndef TASK_H +#define TASK_H + +#include + +#include +#include +#include + +class QAction; +class QProcess; +class QJsonObject; + +namespace tests +{ + class TestTask; +} + +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 TaskMgr; + class TaskVariableMgr; + + class Task : public QObject + { + Q_OBJECT + public: + friend class tests::TestTask; + + // For top level Task, use QSharedPointer instead of QObject to manage ownership. + static QSharedPointer fromFile(const QString &p_file, const QString &p_locale, TaskMgr *p_taskMgr); + + void run(); + + const TaskDTO &getDTO() const; + + const QString &getVersion() const; + + const QString &getType() const; + + QString getCommand(); + + QStringList getArgs(); + + const QString &getLabel() const; + + const QString &getIcon() const; + + const QString &getShortcut() const; + + QString getOptionsCwd(); + + const QMap &getOptionsEnv() const; + + const QString &getOptionsShellExecutable() const; + + QStringList getOptionsShellArgs(); + + const QVector &getChildren() const; + + const QVector &getInputs() const; + + const InputDTO *findInput(const QString &p_id) const; + + const MessageDTO *findMessage(const QString &p_id) const; + + const QString &getFile() const; + + bool isCancelled() const; + + void setCancelled(bool p_cancelled); + + static QString s_latestVersion; + + 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); + + static QString decodeText(const QByteArray &p_text); + + signals: + void outputRequested(const QString &p_text) const; + + private: + Task(const QString &p_locale, + const QString &p_file, + TaskMgr *p_taskMgr, + QObject *p_parent = nullptr); + + // Must call start() or delete the returned QProcess. + QProcess *setupProcess(); + + const TaskVariableMgr &variableMgr() const; + + static bool fromJson(Task *p_task, const QJsonObject &p_obj); + + static bool fromJsonV0(Task *p_task, const QJsonObject &p_obj, bool p_mergeTasks = false); + + static QString decodeText(const QByteArray &p_text, const QByteArray &p_name); + + Task *m_parent = nullptr; + + QVector m_children; + + TaskMgr *m_taskMgr = nullptr; + + TaskDTO m_dto; + + QString m_locale; + + bool m_cancelled = false; + }; +} // ns vnotex + +#endif // TASK_H diff --git a/src/task/task.pri b/src/task/task.pri new file mode 100644 index 00000000..80ada9d0 --- /dev/null +++ b/src/task/task.pri @@ -0,0 +1,13 @@ +QT += widgets + +SOURCES += \ + $$PWD/task.cpp \ + $$PWD/taskmgr.cpp \ + $$PWD/taskvariablemgr.cpp \ + $$PWD/shellexecution.cpp + +HEADERS += \ + $$PWD/task.h \ + $$PWD/taskmgr.h \ + $$PWD/taskvariablemgr.h \ + $$PWD/shellexecution.h diff --git a/src/task/taskmgr.cpp b/src/task/taskmgr.cpp new file mode 100644 index 00000000..a0e3342c --- /dev/null +++ b/src/task/taskmgr.cpp @@ -0,0 +1,111 @@ +#include "taskmgr.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace vnotex; + +TaskMgr::TaskMgr(QObject *p_parent) + : QObject(p_parent), + m_variableMgr(this) +{ +} + +void TaskMgr::init() +{ + m_variableMgr.init(); + + // Load all tasks and watch the location. + loadAllTasks(); + + connect(&VNoteX::getInst().getNotebookMgr(), &NotebookMgr::currentNotebookChanged, + this, [this]() { + loadNotebookTasks(); + emit tasksUpdated(); + }); +} + +void TaskMgr::reload() +{ + loadAllTasks(); +} + +const QVector> &TaskMgr::getAppTasks() const +{ + return m_appTasks; +} + +const QVector> &TaskMgr::getUserTasks() const +{ + return m_userTasks; +} + +const QVector> &TaskMgr::getNotebookTasks() const +{ + return m_notebookTasks; +} + +QString TaskMgr::getNotebookTaskFolder() +{ + return QString(); +} + +void TaskMgr::loadAllTasks() +{ + loadGlobalTasks(); + + loadNotebookTasks(); + + emit tasksUpdated(); +} + +void TaskMgr::loadNotebookTasks() +{ + loadTasksFromFolder(m_notebookTasks, getNotebookTaskFolder()); +} + +void TaskMgr::loadGlobalTasks() +{ + loadTasksFromFolder(m_appTasks, ConfigMgr::getInst().getAppTaskFolder()); + loadTasksFromFolder(m_userTasks, ConfigMgr::getInst().getUserTaskFolder()); +} + +void TaskMgr::loadTasksFromFolder(QVector> &p_tasks, const QString &p_folder) +{ + p_tasks.clear(); + + if (p_folder.isEmpty()) { + return; + } + + const auto taskFiles = FileUtils::entryListRecursively(p_folder, {"*.json"}, QDir::Files); + for (const auto &file : taskFiles) { + auto task = loadTask(file); + if (task) { + qDebug() << "loaded task" << task->getLabel(); + connect(task.data(), &Task::outputRequested, + this, &TaskMgr::taskOutputRequested); + p_tasks.append(task); + } + } +} + +QSharedPointer TaskMgr::loadTask(const QString &p_taskFile) +{ + const auto localeStr = ConfigMgr::getInst().getCoreConfig().getLocaleToUse(); + auto task = Task::fromFile(p_taskFile, localeStr, this); + return task; +} + +const TaskVariableMgr &TaskMgr::getVariableMgr() const +{ + return m_variableMgr; +} diff --git a/src/task/taskmgr.h b/src/task/taskmgr.h new file mode 100644 index 00000000..22195166 --- /dev/null +++ b/src/task/taskmgr.h @@ -0,0 +1,63 @@ +#ifndef TASKMGR_H +#define TASKMGR_H + +#include +#include + +#include +#include + +#include "task.h" +#include "taskvariablemgr.h" + +namespace vnotex +{ + class TaskMgr : public QObject, private Noncopyable + { + Q_OBJECT + public: + explicit TaskMgr(QObject *p_parent = nullptr); + + // It will be invoked after MainWindow show. + void init(); + + void reload(); + + const QVector> &getAppTasks() const; + + const QVector> &getUserTasks() const; + + const QVector> &getNotebookTasks() const; + + static QString getNotebookTaskFolder(); + + const TaskVariableMgr &getVariableMgr() const; + + signals: + void tasksUpdated(); + + void taskOutputRequested(const QString &p_text) const; + + private: + void loadAllTasks(); + + void loadNotebookTasks(); + + void loadGlobalTasks(); + + void loadTasksFromFolder(QVector> &p_tasks, const QString &p_folder); + + // Return nullptr if not a valid task. + QSharedPointer loadTask(const QString &p_taskFile); + + QVector> m_appTasks; + + QVector> m_userTasks; + + QVector> m_notebookTasks; + + TaskVariableMgr m_variableMgr; + }; +} // ns vnotex + +#endif // TASKMGR_H diff --git a/src/task/taskvariablemgr.cpp b/src/task/taskvariablemgr.cpp new file mode 100644 index 00000000..0f0595d8 --- /dev/null +++ b/src/task/taskvariablemgr.cpp @@ -0,0 +1,435 @@ +#include "taskvariablemgr.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "task.h" +#include "taskmgr.h" +#include "shellexecution.h" + +using namespace vnotex; + + +TaskVariable::TaskVariable(const QString &p_name, const Func &p_func) + : m_name(p_name), + m_func(p_func) +{ +} + +QString TaskVariable::evaluate(Task *p_task, const QString &p_value) const +{ + return m_func(p_task, p_value); +} + + +const QString TaskVariableMgr::c_variableSymbolRegExp = QString(R"(\$\{([^${}:]+)(?::([^${}:]+))?\})"); + +TaskVariableMgr::TaskVariableMgr(TaskMgr *p_taskMgr) + : m_taskMgr(p_taskMgr) +{ +} + +void TaskVariableMgr::init() +{ + initVariables(); +} + +void TaskVariableMgr::initVariables() +{ + m_variables.clear(); + + m_needUpdateSystemEnvironment = true; + + initNotebookVariables(); + + initBufferVariables(); + + initTaskVariables(); + + initMagicVariables(); + + initEnvironmentVariables(); + + initConfigVariables(); + + initInputVariables(); + + initShellVariables(); +} + +void TaskVariableMgr::initNotebookVariables() +{ + addVariable("notebookFolder", [](Task *, const QString &) { + auto notebook = TaskVariableMgr::getCurrentNotebook(); + if (notebook) { + return PathUtils::cleanPath(notebook->getRootFolderAbsolutePath()); + } else { + return QString(); + } + }); + addVariable("notebookFolderName", [](Task *, const QString &) { + auto notebook = TaskVariableMgr::getCurrentNotebook(); + if (notebook) { + return PathUtils::dirName(notebook->getRootFolderPath()); + } else { + return QString(); + } + }); + addVariable("notebookName", [](Task *, const QString &) { + auto notebook = TaskVariableMgr::getCurrentNotebook(); + if (notebook) { + return notebook->getName(); + } else { + return QString(); + } + }); + addVariable("notebookDescription", [](Task *, const QString &) { + auto notebook = TaskVariableMgr::getCurrentNotebook(); + if (notebook) { + return notebook->getDescription(); + } else { + return QString(); + } + }); +} + +void TaskVariableMgr::initBufferVariables() +{ + addVariable("buffer", [](Task *, const QString &) { + auto buffer = getCurrentBuffer(); + if (buffer) { + return PathUtils::cleanPath(buffer->getPath()); + } + return QString(); + }); + addVariable("bufferNotebookFolder", [](Task *, const QString &) { + auto buffer = getCurrentBuffer(); + if (buffer) { + auto node = buffer->getNode(); + if (node) { + return PathUtils::cleanPath(node->getNotebook()->getRootFolderAbsolutePath()); + } + } + return QString(); + }); + addVariable("bufferRelativePath", [](Task *, const QString &) { + auto buffer = getCurrentBuffer(); + if (buffer) { + auto node = buffer->getNode(); + if (node) { + return PathUtils::cleanPath(node->fetchPath()); + } else { + return PathUtils::cleanPath(buffer->getPath()); + } + } + return QString(); + }); + addVariable("bufferName", [](Task *, const QString &) { + auto buffer = getCurrentBuffer(); + if (buffer) { + return PathUtils::fileName(buffer->getPath()); + } + return QString(); + }); + addVariable("bufferBaseName", [](Task *, const QString &) { + auto buffer = getCurrentBuffer(); + if (buffer) { + return QFileInfo(buffer->getPath()).completeBaseName(); + } + return QString(); + }); + addVariable("bufferDir", [](Task *, const QString &) { + auto buffer = getCurrentBuffer(); + if (buffer) { + return PathUtils::parentDirPath(buffer->getPath()); + } + return QString(); + }); + addVariable("bufferExt", [](Task *, const QString &) { + auto buffer = getCurrentBuffer(); + if (buffer) { + return QFileInfo(buffer->getPath()).suffix(); + } + return QString(); + }); + addVariable("selectedText", [](Task *, const QString &) { + auto win = getCurrentViewWindow(); + if (win) { + return win->selectedText(); + } + return QString(); + }); +} + +void TaskVariableMgr::initTaskVariables() +{ + addVariable("cwd", [](Task *task, const QString &) { + return PathUtils::cleanPath(task->getOptionsCwd()); + }); + addVariable("taskFile", [](Task *task, const QString &) { + return PathUtils::cleanPath(task->getFile()); + }); + addVariable("taskDir", [](Task *task, const QString &) { + return PathUtils::parentDirPath(task->getFile()); + }); + addVariable("exeFile", [](Task *, const QString &) { + return PathUtils::cleanPath(qApp->applicationFilePath()); + }); + addVariable("pathSeparator", [](Task *, const QString &) { + return QDir::separator(); + }); + addVariable("notebookTaskFolder", [this](Task *, const QString &) { + return PathUtils::cleanPath(m_taskMgr->getNotebookTaskFolder()); + }); + addVariable("userTaskFolder", [](Task *, const QString &) { + return PathUtils::cleanPath(ConfigMgr::getInst().getUserTaskFolder()); + }); + addVariable("appTaskFolder", [](Task *, const QString &) { + return PathUtils::cleanPath(ConfigMgr::getInst().getAppTaskFolder()); + }); + addVariable("userThemeFolder", [](Task *, const QString &) { + return PathUtils::cleanPath(ConfigMgr::getInst().getUserThemeFolder()); + }); + addVariable("appThemeFolder", [](Task *, const QString &) { + return PathUtils::cleanPath(ConfigMgr::getInst().getAppThemeFolder()); + }); + addVariable("userDocsFolder", [](Task *, const QString &) { + return PathUtils::cleanPath(ConfigMgr::getInst().getUserDocsFolder()); + }); + addVariable("appDocsFolder", [](Task *, const QString &) { + return PathUtils::cleanPath(ConfigMgr::getInst().getAppDocsFolder()); + }); +} + +void TaskVariableMgr::initMagicVariables() +{ + addVariable("magic", [](Task *, const QString &val) { + if (val.isEmpty()) { + return QString(); + } + + auto overrides = SnippetMgr::generateOverrides(getCurrentBuffer()); + return SnippetMgr::getInst().applySnippetBySymbol(SnippetMgr::generateSnippetSymbol(val), overrides); + }); +} + +void TaskVariableMgr::initEnvironmentVariables() +{ + addVariable("env", [this](Task *, const QString &val) { + if (val.isEmpty()) { + return QString(); + } + if (m_needUpdateSystemEnvironment) { + m_needUpdateSystemEnvironment = false; + m_systemEnv = QProcessEnvironment::systemEnvironment(); + } + return m_systemEnv.value(val); + }); +} + +void TaskVariableMgr::initConfigVariables() +{ + // ${config:main.core.shortcuts.FullScreen}. + addVariable("config", [](Task *, const QString &val) { + if (val.isEmpty()) { + return QString(); + } + auto jsonVal = ConfigMgr::getInst().parseAndReadConfig(val); + switch (jsonVal.type()) { + case QJsonValue::Bool: + return jsonVal.toBool() ? QStringLiteral("1") : QStringLiteral("0"); + break; + + case QJsonValue::Double: + return QString::number(jsonVal.toDouble()); + break; + + case QJsonValue::String: + return jsonVal.toString(); + break; + + default: + return QString(); + } + }); +} + +void TaskVariableMgr::initInputVariables() +{ + // ${input:inputId}. + addVariable("input", [this](Task *task, const QString &val) { + if (val.isEmpty()) { + Exception::throwOne(Exception::Type::InvalidArgument, + QString("task (%1) with empty input id").arg(task->getLabel())); + } + + auto input = task->findInput(val); + if (!input) { + Exception::throwOne(Exception::Type::InvalidArgument, + QString("task (%1) with invalid input id (%2)").arg(task->getLabel(), val)); + } + + if (input->type == "promptString") { + const auto desc = evaluate(task, input->description); + const auto defaultText = evaluate(task, input->default_); + QInputDialog dialog(VNoteX::getInst().getMainWindow()); + dialog.setInputMode(QInputDialog::TextInput); + dialog.setTextEchoMode(input->password ? QLineEdit::Password : QLineEdit::Normal); + dialog.setWindowTitle(task->getLabel()); + dialog.setLabelText(desc); + dialog.setTextValue(defaultText); + if (dialog.exec() == QDialog::Accepted) { + return dialog.textValue(); + } else { + task->setCancelled(true); + return QString(); + } + } else if (input->type == "pickString") { + const auto desc = evaluate(task, input->description); + SelectDialog dialog(task->getLabel(), desc, VNoteX::getInst().getMainWindow()); + 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(); + return input->options.at(selection); + } else { + task->setCancelled(true); + return QString(); + } + } else { + Exception::throwOne(Exception::Type::InvalidArgument, + QString("task (%1) with invalid input type (%2)(%3)").arg(task->getLabel(), input->id, input->type)); + } + + return QString(); + }); +} + +void TaskVariableMgr::initShellVariables() +{ + // ${shell:command}. + addVariable("shell", [this](Task *task, const QString &val) { + QProcess process; + process.setWorkingDirectory(task->getOptionsCwd()); + ShellExecution::setupProcess(&process, val); + process.start(); + const int timeout = 1000; + if (!process.waitForStarted(timeout) || !process.waitForFinished(timeout)) { + Exception::throwOne(Exception::Type::InvalidArgument, + QString("task (%1) failed to fetch shell variable (%2)").arg(task->getLabel(), val)); + } + return Task::decodeText(process.readAllStandardOutput()); + }); +} + +void TaskVariableMgr::addVariable(const QString &p_name, const TaskVariable::Func &p_func) +{ + Q_ASSERT(!m_variables.contains(p_name)); + + m_variables.insert(p_name, TaskVariable(p_name, p_func)); +} + +const ViewWindow *TaskVariableMgr::getCurrentViewWindow() +{ + return VNoteX::getInst().getMainWindow()->getViewArea()->getCurrentViewWindow(); +} + +Buffer *TaskVariableMgr::getCurrentBuffer() +{ + auto win = getCurrentViewWindow(); + if (win) { + return win->getBuffer(); + } + return nullptr; +} + +QSharedPointer TaskVariableMgr::getCurrentNotebook() +{ + return VNoteX::getInst().getNotebookMgr().getCurrentNotebook(); +} + +QString TaskVariableMgr::evaluate(Task *p_task, const QString &p_text) const +{ + QString content(p_text); + + int maxTimesAtSamePos = 100; + + QRegularExpression regExp(c_variableSymbolRegExp); + int pos = 0; + while (pos < content.size()) { + QRegularExpressionMatch match; + int idx = content.indexOf(regExp, pos, &match); + if (idx == -1) { + break; + } + + const auto varName = match.captured(1).trimmed(); + const auto varValue = match.captured(2).trimmed(); + auto var = findVariable(varName); + if (!var) { + // Skip it. + pos = idx + match.capturedLength(0); + continue; + } + + const auto afterText = var->evaluate(p_task, varValue); + content.replace(idx, match.capturedLength(0), afterText); + + // @afterText may still contains variable symbol. + if (pos == idx) { + if (--maxTimesAtSamePos == 0) { + break; + } + } else { + maxTimesAtSamePos = 100; + } + pos = idx; + } + + return content; +} + +QStringList TaskVariableMgr::evaluate(Task *p_task, const QStringList &p_texts) const +{ + QStringList strs; + for (const auto &str : p_texts) { + strs << evaluate(p_task, str); + } + return strs; +} + +const TaskVariable *TaskVariableMgr::findVariable(const QString &p_name) const +{ + auto it = m_variables.find(p_name); + if (it != m_variables.end()) { + return &(it.value()); + } + + return nullptr; +} + +void TaskVariableMgr::overrideVariable(const QString &p_name, const TaskVariable::Func &p_func) +{ + m_variables.insert(p_name, TaskVariable(p_name, p_func)); +} diff --git a/src/task/taskvariablemgr.h b/src/task/taskvariablemgr.h new file mode 100644 index 00000000..9b1c9f38 --- /dev/null +++ b/src/task/taskvariablemgr.h @@ -0,0 +1,105 @@ +#ifndef TASKVARIABLEMGR_H +#define TASKVARIABLEMGR_H + +#include + +#include + +#include +#include +#include +#include +#include + +namespace vnotex +{ + class Task; + class Notebook; + class Buffer; + class ViewWindow; + class TaskMgr; + + class TaskVariable + { + public: + typedef std::function Func; + + TaskVariable(const QString &p_name, const Func &p_func); + + QString evaluate(Task *p_task, const QString &p_value) const; + + private: + QString m_name; + + Func m_func; + }; + + + class TaskVariableMgr : private Noncopyable + { + public: + explicit TaskVariableMgr(TaskMgr *p_taskMgr); + + void init(); + + QString evaluate(Task *p_task, const QString &p_text) const; + + QStringList evaluate(Task *p_task, const QStringList &p_texts) const; + + // Used for UT. + void overrideVariable(const QString &p_name, const TaskVariable::Func &p_func); + + static Buffer *getCurrentBuffer(); + + static QSharedPointer getCurrentNotebook(); + + private: + void initVariables(); + + void initNotebookVariables(); + + void initBufferVariables(); + + void initTaskVariables(); + + void initMagicVariables(); + + void initEnvironmentVariables(); + + void initConfigVariables(); + + void initInputVariables(); + + void initShellVariables(); + + void addVariable(const QString &p_name, const TaskVariable::Func &p_func); + + const TaskVariable *findVariable(const QString &p_name) const; + + /* + QString evaluateInputVariables(const QString &p_text, + const Task *p_task) const; + + QString evaluateShellVariables(const QString &p_text, + const Task *p_task) const; + */ + + static const ViewWindow *getCurrentViewWindow(); + + TaskMgr *m_taskMgr = nullptr; + + QHash m_variables; + + bool m_needUpdateSystemEnvironment = true; + + QProcessEnvironment m_systemEnv; + + // %{name[:value]}%. + // Captured texts: + // 1 - The name of the variable (trim needed). + // 2 - The value option of the variable if available (trim needed). + static const QString c_variableSymbolRegExp; + }; +} // ns vnotex + +#endif // TASKVARIABLEMGR_H diff --git a/src/utils/fileutils.cpp b/src/utils/fileutils.cpp index 65cec1e0..cf62d385 100644 --- a/src/utils/fileutils.cpp +++ b/src/utils/fileutils.cpp @@ -362,17 +362,23 @@ QStringList FileUtils::entryListRecursively(const QString &p_dirPath, const QStringList &p_nameFilters, QDir::Filters p_filters) { + QStringList entries; + 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)); + if (!dir.exists()) { + return entries; } - auto subdirs = dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); + + const auto curEntries = dir.entryList(p_nameFilters, p_filters | QDir::NoDotAndDotDot); + for (const auto &e : curEntries) { + entries.append(PathUtils::concatenateFilePath(p_dirPath, e)); + } + + const 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)); + entries.append(entryListRecursively(dirPath, p_nameFilters, p_filters)); } - return entrys; + + return entries; } diff --git a/src/utils/fileutils.h b/src/utils/fileutils.h index ad1a6cb2..90a07b8a 100644 --- a/src/utils/fileutils.h +++ b/src/utils/fileutils.h @@ -83,7 +83,7 @@ namespace vnotex // @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); + QDir::Filters p_filters = QDir::NoFilter); }; } // ns vnotex diff --git a/src/utils/pathutils.cpp b/src/utils/pathutils.cpp index 1e21ce1c..54bc939f 100644 --- a/src/utils/pathutils.cpp +++ b/src/utils/pathutils.cpp @@ -16,7 +16,6 @@ QString PathUtils::parentDirPath(const QString &p_path) } QFileInfo info(p_path); - Q_ASSERT(info.isAbsolute()); return cleanPath(info.absolutePath()); } diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index ce7eadeb..82f0bf1e 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -4,13 +4,15 @@ #include #include #include -#include #include #include +#include #include #include #include +#include #include +#include #include @@ -139,3 +141,54 @@ QJsonObject Utils::fromJsonString(const QByteArray &p_data) { return QJsonDocument::fromJson(p_data).object(); } + +QJsonValue Utils::parseAndReadJson(const QJsonObject &p_obj, const QString &p_exp) +{ + // abc[0] or abc. + QRegularExpression regExp(R"(^([^\[\]\s]+)(?:\[(\d+)\])?$)"); + + QJsonValue val(p_obj); + + bool valid = true; + const auto tokens = p_exp.split(QLatin1Char('.')); + for (int i = 0; i < tokens.size(); ++i) { + const auto &token = tokens[i]; + if (token.isEmpty()) { + continue; + } + + auto match = regExp.match(token); + if (!match.hasMatch()) { + valid = false; + break; + } + + const auto key = match.captured(1); + const auto obj = val.toObject(); + if (obj.contains(key)) { + val = obj.value(key); + } else { + valid = false; + break; + } + + if (!match.captured(2).isEmpty()) { + // Array. + const auto arr = val.toArray(); + int idx = match.captured(2).toInt(); + if (idx < 0 || idx >= arr.size()) { + valid = false; + break; + } + + val = arr[idx]; + } + } + + if (!valid) { + qWarning() << "invalid expression to parse for JSON" << p_exp; + return QJsonValue(); + } + + return val; +} diff --git a/src/utils/utils.h b/src/utils/utils.h index 76b301f8..6ac1a7f3 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -57,6 +57,10 @@ namespace vnotex static QByteArray toJsonString(const QJsonObject &p_obj); static QJsonObject fromJsonString(const QByteArray &p_data); + + // Parse @p_exp into tokens and read the target value from @p_obj. + // Format: obj1.obj2.arr[2].obj3. + static QJsonValue parseAndReadJson(const QJsonObject &p_obj, const QString &p_exp); }; } // ns vnotex diff --git a/src/widgets/dialogs/selectdialog.cpp b/src/widgets/dialogs/selectdialog.cpp index 878ff565..7aa29210 100644 --- a/src/widgets/dialogs/selectdialog.cpp +++ b/src/widgets/dialogs/selectdialog.cpp @@ -13,13 +13,8 @@ using namespace vnotex; const QChar SelectDialog::c_cancelShortcut = QLatin1Char('z'); SelectDialog::SelectDialog(const QString &p_title, QWidget *p_parent) - : QDialog(p_parent) + : SelectDialog(p_title, QString(), 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); } SelectDialog::SelectDialog(const QString &p_title, diff --git a/src/widgets/dockwidgethelper.cpp b/src/widgets/dockwidgethelper.cpp index 06139bb7..c58192ac 100644 --- a/src/widgets/dockwidgethelper.cpp +++ b/src/widgets/dockwidgethelper.cpp @@ -25,6 +25,7 @@ #include "snippetpanel.h" #include "historypanel.h" #include "tagexplorer.h" +#include "terminalviewer.h" using namespace vnotex; @@ -113,7 +114,7 @@ void DockWidgetHelper::setupDocks() setupOutlineDock(); - setupOutputDock(); + setupTerminalDock(); setupLocationListDock(); @@ -148,17 +149,17 @@ void DockWidgetHelper::setupOutlineDock() m_mainWindow->addDockWidget(Qt::RightDockWidgetArea, dock); } -void DockWidgetHelper::setupOutputDock() +void DockWidgetHelper::setupTerminalDock() { - auto dock = createDockWidget(DockIndex::OutputDock, tr("Output"), m_mainWindow); + auto dock = createDockWidget(DockIndex::TerminalDock, tr("Terminal"), m_mainWindow); - dock->setObjectName(QStringLiteral("OutputDock.vnotex")); - dock->setAllowedAreas(Qt::BottomDockWidgetArea); + dock->setObjectName(QStringLiteral("TerminalDock.vnotex")); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); - dock->setWidget(m_mainWindow->m_outputViewer); - dock->setFocusProxy(m_mainWindow->m_outputViewer); - dock->hide(); + dock->setWidget(m_mainWindow->m_terminalViewer); + dock->setFocusProxy(m_mainWindow->m_terminalViewer); m_mainWindow->addDockWidget(Qt::BottomDockWidgetArea, dock); + dock->hide(); } void DockWidgetHelper::setupSearchDock() diff --git a/src/widgets/dockwidgethelper.h b/src/widgets/dockwidgethelper.h index 8dbbb74a..87a1f906 100644 --- a/src/widgets/dockwidgethelper.h +++ b/src/widgets/dockwidgethelper.h @@ -29,7 +29,7 @@ namespace vnotex SearchDock, SnippetDock, OutlineDock, - OutputDock, + TerminalDock, LocationListDock, MaxDock }; @@ -98,7 +98,7 @@ namespace vnotex void setupOutlineDock(); - void setupOutputDock(); + void setupTerminalDock(); void setupSearchDock(); diff --git a/src/widgets/framelessmainwindow/framelessmainwindowlinux.cpp b/src/widgets/framelessmainwindow/framelessmainwindowlinux.cpp index c23fed7f..17a06a6f 100644 --- a/src/widgets/framelessmainwindow/framelessmainwindowlinux.cpp +++ b/src/widgets/framelessmainwindow/framelessmainwindowlinux.cpp @@ -230,10 +230,4 @@ void FramelessMainWindowLinux::showEvent(QShowEvent *p_event) } } -#else -using namespace vnotex; - -FramelessMainWindowLinuxDummy::FramelessMainWindowLinuxDummy() -{ -} #endif diff --git a/src/widgets/framelessmainwindow/framelessmainwindowlinux.h b/src/widgets/framelessmainwindow/framelessmainwindowlinux.h index c466ed1a..0485b03b 100644 --- a/src/widgets/framelessmainwindow/framelessmainwindowlinux.h +++ b/src/widgets/framelessmainwindow/framelessmainwindowlinux.h @@ -65,7 +65,7 @@ namespace vnotex class FramelessMainWindowLinuxDummy { public: - FramelessMainWindowLinuxDummy(); + FramelessMainWindowLinuxDummy() = default; }; #endif } diff --git a/src/widgets/framelessmainwindow/framelessmainwindowwin.cpp b/src/widgets/framelessmainwindow/framelessmainwindowwin.cpp index 43b80469..1b2a8299 100644 --- a/src/widgets/framelessmainwindow/framelessmainwindowwin.cpp +++ b/src/widgets/framelessmainwindow/framelessmainwindowwin.cpp @@ -221,10 +221,4 @@ void FramelessMainWindowWin::forceRedraw() } } -#else -using namespace vnotex; - -FramelessMainWindowWinDummy::FramelessMainWindowWinDummy() -{ -} #endif diff --git a/src/widgets/framelessmainwindow/framelessmainwindowwin.h b/src/widgets/framelessmainwindow/framelessmainwindowwin.h index dddf06d6..093accb3 100644 --- a/src/widgets/framelessmainwindow/framelessmainwindowwin.h +++ b/src/widgets/framelessmainwindow/framelessmainwindowwin.h @@ -41,7 +41,7 @@ namespace vnotex class FramelessMainWindowWinDummy { public: - FramelessMainWindowWinDummy(); + FramelessMainWindowWinDummy() = default; }; #endif } diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index 049fe668..db3ac948 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -56,6 +56,7 @@ #include "tagexplorer.h" #include "toolbarhelper.h" #include "statusbarhelper.h" +#include "terminalviewer.h" using namespace vnotex; @@ -257,7 +258,7 @@ void MainWindow::setupDocks() setupOutlineViewer(); - setupOutputViewer(); + setupTerminalViewer(); setupHistoryPanel(); @@ -514,23 +515,15 @@ void MainWindow::setupOutlineViewer() this, &MainWindow::focusViewArea); } -void MainWindow::setupOutputViewer() +void MainWindow::setupTerminalViewer() { - m_outputViewer = new QTextEdit(this); - m_outputViewer->setObjectName("OutputViewer.vnotex"); - m_outputViewer->setReadOnly(true); + m_terminalViewer = new TerminalViewer(this); + m_terminalViewer->setObjectName("TerminalViewer.vnotex"); 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(); + this, [this](const QString &p_text) { + m_terminalViewer->append(p_text); + m_dockWidgetHelper.activateDock(DockWidgetHelper::TerminalDock); }); } diff --git a/src/widgets/mainwindow.h b/src/widgets/mainwindow.h index 58428bec..3aa4dd02 100644 --- a/src/widgets/mainwindow.h +++ b/src/widgets/mainwindow.h @@ -28,6 +28,7 @@ namespace vnotex class SnippetPanel; class HistoryPanel; class ExportDialog; + class TerminalViewer; enum { RESTART_EXIT_CODE = 1000 }; @@ -106,7 +107,7 @@ namespace vnotex void setupOutlineViewer(); - void setupOutputViewer(); + void setupTerminalViewer(); void setupSearchPanel(); @@ -167,7 +168,7 @@ namespace vnotex OutlineViewer *m_outlineViewer = nullptr; - QTextEdit *m_outputViewer = nullptr; + TerminalViewer *m_terminalViewer = nullptr; LocationList *m_locationList = nullptr; diff --git a/src/widgets/notebooknodeexplorer.cpp b/src/widgets/notebooknodeexplorer.cpp index fbc0e85b..b4b4a9d5 100644 --- a/src/widgets/notebooknodeexplorer.cpp +++ b/src/widgets/notebooknodeexplorer.cpp @@ -838,7 +838,7 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent) break; case Action::OpenLocation: - act = new QAction(tr("Open &Location"), p_parent); + act = new QAction(tr("Open Locat&ion"), p_parent); connect(act, &QAction::triggered, this, [this]() { auto item = m_masterExplorer->currentItem(); diff --git a/src/widgets/terminalviewer.cpp b/src/widgets/terminalviewer.cpp new file mode 100644 index 00000000..b25178f0 --- /dev/null +++ b/src/widgets/terminalviewer.cpp @@ -0,0 +1,62 @@ +#include "terminalviewer.h" + +#include +#include +#include +#include + +#include + +#include "widgetsfactory.h" +#include "titlebar.h" + +using namespace vnotex; + +TerminalViewer::TerminalViewer(QWidget *p_parent) + : QFrame(p_parent) +{ + setupUI(); +} + +void TerminalViewer::setupUI() +{ + auto mainLayout = new QVBoxLayout(this); + WidgetUtils::setContentsMargins(mainLayout); + + { + setupTitleBar(QString(), this); + mainLayout->addWidget(m_titleBar); + } + + m_consoleEdit = new QPlainTextEdit(this); + m_consoleEdit->setReadOnly(true); + mainLayout->addWidget(m_consoleEdit); + + setFocusProxy(m_consoleEdit); +} + +void TerminalViewer::setupTitleBar(const QString &p_title, QWidget *p_parent) +{ + m_titleBar = new TitleBar(p_title, true, TitleBar::Action::None, p_parent); + m_titleBar->setActionButtonsAlwaysShown(true); + + { + auto clearBtn = m_titleBar->addActionButton(QStringLiteral("clear.svg"), tr("Clear")); + connect(clearBtn, &QToolButton::triggered, + this, &TerminalViewer::clear); + } +} + +void TerminalViewer::append(const QString &p_text) +{ + m_consoleEdit->appendPlainText(p_text); + auto scrollBar = m_consoleEdit->verticalScrollBar(); + if (scrollBar) { + scrollBar->setValue(scrollBar->maximum()); + } +} + +void TerminalViewer::clear() +{ + m_consoleEdit->clear(); +} diff --git a/src/widgets/terminalviewer.h b/src/widgets/terminalviewer.h new file mode 100644 index 00000000..c88045d4 --- /dev/null +++ b/src/widgets/terminalviewer.h @@ -0,0 +1,33 @@ +#ifndef TERMINALVIEWER_H +#define TERMINALVIEWER_H + +#include + +class QPlainTextEdit; + +namespace vnotex +{ + class TitleBar; + + class TerminalViewer : public QFrame + { + Q_OBJECT + public: + explicit TerminalViewer(QWidget *p_parent = nullptr); + + void append(const QString &p_text); + + void clear(); + + private: + void setupUI(); + + void setupTitleBar(const QString &p_title, QWidget *p_parent = nullptr); + + TitleBar *m_titleBar = nullptr; + + QPlainTextEdit *m_consoleEdit = nullptr; + }; +} + +#endif // TERMINALVIEWER_H diff --git a/src/widgets/toolbarhelper.cpp b/src/widgets/toolbarhelper.cpp index f6d7e7e6..4178d53a 100644 --- a/src/widgets/toolbarhelper.cpp +++ b/src/widgets/toolbarhelper.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include "propertydefs.h" #include "dialogs/settings/settingsdialog.h" #include "dialogs/updater.h" @@ -286,52 +287,80 @@ QToolBar *ToolBarHelper::setupQuickAccessToolBar(MainWindow *p_win, QToolBar *p_ void ToolBarHelper::setupTaskMenu(QMenu *p_menu) { p_menu->clear(); + + setupTaskActionMenu(p_menu); + + p_menu->addSeparator(); + const auto &taskMgr = VNoteX::getInst().getTaskMgr(); - for (auto task : taskMgr.getAppTasks()) { - addTaskMenu(p_menu, task); + for (const auto &task : taskMgr.getAppTasks()) { + addTaskMenu(p_menu, task.data()); } + p_menu->addSeparator(); - for (auto task : taskMgr.getUserTasks()) { - addTaskMenu(p_menu, task); + + for (const auto &task : taskMgr.getUserTasks()) { + addTaskMenu(p_menu, task.data()); } + p_menu->addSeparator(); - for (auto task : taskMgr.getNotebookTasks()) { - addTaskMenu(p_menu, task); + + for (const auto &task : taskMgr.getNotebookTasks()) { + addTaskMenu(p_menu, task.data()); } } +void ToolBarHelper::setupTaskActionMenu(QMenu *p_menu) +{ + p_menu->addAction(MainWindow::tr("Add Task"), + p_menu, + []() { + WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(ConfigMgr::getInst().getUserTaskFolder())); + }); + + p_menu->addAction(MainWindow::tr("Reload"), + p_menu, + []() { + VNoteX::getInst().getTaskMgr().reload(); + }); +} + 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(); + + const auto &children = p_task->getChildren(); + auto label = p_task->getLabel(); - label = label.replace("&", "&&"); + // '&' will be considered shortuct symbol in QAction. + label.replace("&", "&&"); + + if (children.isEmpty()) { + action = p_menu->addAction(label); + } else { + auto subMenu = p_menu->addMenu(label); + for (auto task : children) { + addTaskMenu(subMenu, task); + } + action = subMenu->menuAction(); + } + QIcon icon; try { auto taskIcon = p_task->getIcon(); if (!taskIcon.isEmpty()) { icon = generateIcon(p_task->getIcon()); } - } catch (Exception e) { + } catch (Exception &e) { if (e.m_type != Exception::Type::FailToReadFile) { - throw; + throw e; } } - 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); + + action->setData(reinterpret_cast(p_task)); + WidgetUtils::addActionShortcut(action, p_task->getShortcut()); - MainWindow::connect(action, &QAction::triggered, - p_task, &Task::run); } QToolBar *ToolBarHelper::setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar) @@ -347,11 +376,19 @@ QToolBar *ToolBarHelper::setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar btn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); auto taskMenu = WidgetsFactory::createMenu(tb); - MainWindow::connect(&VNoteX::getInst().getTaskMgr(), &TaskMgr::taskChanged, + setupTaskActionMenu(taskMenu); + btn->setMenu(taskMenu); + MainWindow::connect(taskMenu, &QMenu::triggered, + taskMenu, [](QAction *act) { + auto task = reinterpret_cast(act->data().toULongLong()); + Q_ASSERT(task); + task->run(); + }); + MainWindow::connect(&VNoteX::getInst().getTaskMgr(), &TaskMgr::tasksUpdated, taskMenu, [taskMenu]() { setupTaskMenu(taskMenu); }); - btn->setMenu(taskMenu); + return tb; } @@ -685,6 +722,12 @@ void ToolBarHelper::setupMenuButton(MainWindow *p_win, QToolBar *p_toolBar) WidgetUtils::openUrlByDesktop(QUrl("https://vnotex.github.io/vnote")); }); + helpMenu->addAction(MainWindow::tr("Documentation"), + helpMenu, + []() { + WidgetUtils::openUrlByDesktop(QUrl("https://vnotex.github.io/vnote/en_us/#!docs/vx.json")); + }); + helpMenu->addAction(MainWindow::tr("Feedback and Discussions"), helpMenu, []() { diff --git a/src/widgets/toolbarhelper.h b/src/widgets/toolbarhelper.h index fadabae6..5f5e0a5b 100644 --- a/src/widgets/toolbarhelper.h +++ b/src/widgets/toolbarhelper.h @@ -36,6 +36,8 @@ namespace vnotex static void setupTaskMenu(QMenu *p_menu); + static void setupTaskActionMenu(QMenu *p_menu); + static void addTaskMenu(QMenu *p_menu, Task *p_task); static QToolBar *setupTaskToolBar(MainWindow *p_win, QToolBar *p_toolBar); diff --git a/src/widgets/viewwindow.h b/src/widgets/viewwindow.h index 60af1f3e..14157031 100644 --- a/src/widgets/viewwindow.h +++ b/src/widgets/viewwindow.h @@ -97,6 +97,8 @@ namespace vnotex bool isSessionEnabled() const; + virtual QString selectedText() const; + public slots: virtual void handleEditorConfigChange() = 0; @@ -226,8 +228,6 @@ namespace vnotex virtual void zoom(bool p_zoomIn) = 0; - virtual QString selectedText() const; - void showZoomFactor(qreal p_factor); void showZoomDelta(int p_delta); diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index 9cab2409..7a9797f5 100644 --- a/src/widgets/widgets.pri +++ b/src/widgets/widgets.pri @@ -89,6 +89,7 @@ SOURCES += \ $$PWD/tagexplorer.cpp \ $$PWD/tagpopup.cpp \ $$PWD/tagviewer.cpp \ + $$PWD/terminalviewer.cpp \ $$PWD/textviewwindow.cpp \ $$PWD/toolbarhelper.cpp \ $$PWD/treeview.cpp \ @@ -218,6 +219,7 @@ HEADERS += \ $$PWD/tagexplorer.h \ $$PWD/tagpopup.h \ $$PWD/tagviewer.h \ + $$PWD/terminalviewer.h \ $$PWD/textviewwindow.h \ $$PWD/textviewwindowhelper.h \ $$PWD/toolbarhelper.h \ diff --git a/tests/commonfull.pri b/tests/commonfull.pri new file mode 100644 index 00000000..367a945c --- /dev/null +++ b/tests/commonfull.pri @@ -0,0 +1,29 @@ +include($$PWD/common.pri) + +QT += sql + +SRC_FOLDER = $$PWD/../src + +LIBS_FOLDER = $$PWD/../libs + +INCLUDEPATH *= $$SRC_FOLDER + +include($$LIBS_FOLDER/vtextedit/src/editor/editor_export.pri) + +include($$LIBS_FOLDER/vtextedit/src/libs/syntax-highlighting/syntax-highlighting_export.pri) + +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) + +include($$SRC_FOLDER/task/task.pri) + +include($$SRC_FOLDER/core/core.pri) + +include($$SRC_FOLDER/widgets/widgets.pri) diff --git a/tests/test_core/test_notebook/test_notebook.pro b/tests/test_core/test_notebook/test_notebook.pro index 0f2dc15f..9e2e87a3 100644 --- a/tests/test_core/test_notebook/test_notebook.pro +++ b/tests/test_core/test_notebook/test_notebook.pro @@ -1,29 +1,8 @@ -include($$PWD/../../common.pri) - -QT += sql +include($$PWD/../../commonfull.pri) TARGET = test_notebook TEMPLATE = app -SRC_FOLDER = $$PWD/../../../src -CORE_FOLDER = $$SRC_FOLDER/core - -INCLUDEPATH *= $$SRC_FOLDER - -LIBS_FOLDER = $$PWD/../../../libs - -include($$LIBS_FOLDER/vtextedit/src/editor/editor_export.pri) - -include($$LIBS_FOLDER/vtextedit/src/libs/syntax-highlighting/syntax-highlighting_export.pri) - -include($$CORE_FOLDER/core.pri) -include($$SRC_FOLDER/widgets/widgets.pri) -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 += \ dummynode.cpp \ dummynotebook.cpp \ diff --git a/tests/test_task/test_task.cpp b/tests/test_task/test_task.cpp new file mode 100644 index 00000000..a5455214 --- /dev/null +++ b/tests/test_task/test_task.cpp @@ -0,0 +1,77 @@ +#include "test_task.h" + +#include +#include + +#include +#include +#include +#include +#include + +using namespace tests; + +using namespace vnotex; + +TestTask::TestTask(QObject *p_parent) + : QObject(p_parent) +{ +} + +void TestTask::initTestCase() +{ + ConfigMgr::initForUnitTest(); +} + +void TestTask::TestTaskVariableMgr() +{ + TaskVariableMgr mgr(nullptr); + mgr.init(); + + mgr.overrideVariable("notebookFolder", [](const Task *, const QString &val) { + Q_ASSERT(val.isEmpty()); + return "/home/vnotex/vnote"; + }); + + mgr.overrideVariable("notebookFolderName", [](const Task *, const QString &val) { + Q_ASSERT(val.isEmpty()); + return "vnote"; + }); + + mgr.overrideVariable("magic", [](const Task *, const QString &val) { + if (val.isEmpty()) { + return QString(); + } else { + return val; + } + }); + + auto task = createTask(); + + auto result = mgr.evaluate(task.data(), "start ${notebookFolder} end"); + QCOMPARE(result, "start /home/vnotex/vnote end"); + + result = mgr.evaluate(task.data(), "start ${notebookFolder} mid ${notebookFolderName} end"); + QCOMPARE(result, "start /home/vnotex/vnote mid vnote end"); + + result = mgr.evaluate(task.data(), "${magic:yyyy} ${magic:MM} ${magic:dd}"); + QCOMPARE("yyyy MM dd", result); + + { + const auto env = QProcessEnvironment::systemEnvironment(); + result = mgr.evaluate(task.data(), "${env:PATH} ${env:QT_PATH} ${env:nonexist}"); + QCOMPARE(result, QString("%1 %2 %3").arg(env.value("PATH"), env.value("QT_PATH"), env.value("nonexist"))); + } + + result = mgr.evaluate(task.data(), "${config:main.core.toolbar_icon_size} ${config:main.core.nonexists} ${config:session.core.system_title_bar}"); + QCOMPARE(result, QString("%1 %2").arg(ConfigMgr::getInst().getCoreConfig().getToolBarIconSize()) + .arg(ConfigMgr::getInst().getSessionConfig().getSystemTitleBarEnabled())); +} + +QSharedPointer TestTask::createTask() const +{ + return QSharedPointer(new Task("en_US", "dummy_file", nullptr, nullptr)); +} + +QTEST_MAIN(tests::TestTask) + diff --git a/tests/test_task/test_task.h b/tests/test_task/test_task.h new file mode 100644 index 00000000..2483de7c --- /dev/null +++ b/tests/test_task/test_task.h @@ -0,0 +1,31 @@ +#ifndef TESTS_TASK_TEST_TASK_H +#define TESTS_TASK_TEST_TASK_H + +#include +#include + +namespace vnotex +{ + class Task; +} + +namespace tests +{ + class TestTask : public QObject + { + Q_OBJECT + public: + explicit TestTask(QObject *p_parent = nullptr); + + private slots: + void initTestCase(); + + // Define test cases here per slot. + void TestTaskVariableMgr(); + + private: + QSharedPointer createTask() const; + }; +} // ns tests + +#endif // TESTS_UTILS_TEST_UTILS_H diff --git a/tests/test_task/test_task.pro b/tests/test_task/test_task.pro new file mode 100644 index 00000000..275e6ef5 --- /dev/null +++ b/tests/test_task/test_task.pro @@ -0,0 +1,10 @@ +include($$PWD/../commonfull.pri) + +TARGET = test_task +TEMPLATE = app + +SOURCES += \ + test_task.cpp + +HEADERS += \ + test_task.h diff --git a/tests/test_utils/test_utils.cpp b/tests/test_utils/test_utils.cpp index 6dc2fce7..60d83c59 100644 --- a/tests/test_utils/test_utils.cpp +++ b/tests/test_utils/test_utils.cpp @@ -2,7 +2,9 @@ #include #include +#include +#include #include #include @@ -10,6 +12,68 @@ using namespace tests; using namespace vnotex; +TestUtils::TestUtils(QObject *p_parent) + : QObject(p_parent) +{ +} + +void TestUtils::initTestCase() +{ + m_obj["a"] = "a"; + + { + QJsonObject objb; + objb["a"] = "ba"; + objb["b"] = 2; + + { + QJsonObject objbc; + objbc["a"] = "bca"; + + objb["c"] = objbc; + } + + m_obj["b"] = objb; + } + + { + QJsonObject objc; + objc["a"] = "ca"; + + QJsonArray arr; + arr.append("cb0"); + arr.append("cb1"); + arr.append("cb2"); + objc["b"] = arr; + + m_obj["c"] = objc; + } +} + +void TestUtils::testParseAndReadJson_data() +{ + QTest::addColumn("exp"); + QTest::addColumn("result"); + + QTest::newRow("empty") << "" << QJsonValue(m_obj); + QTest::newRow("a") << "a" << QJsonValue("a"); + QTest::newRow("ba") << "b.a" << QJsonValue("ba"); + QTest::newRow("bb") << "b.b" << QJsonValue(2); + QTest::newRow("bca") << "b.c.a" << QJsonValue("bca"); + QTest::newRow("ca") << "c.a" << QJsonValue("ca"); + QTest::newRow("cb0") << "c.b[0]" << QJsonValue("cb0"); + QTest::newRow("cb1") << "c.b[1]" << QJsonValue("cb1"); + QTest::newRow("cb2") << "c.b[2]" << QJsonValue("cb2"); +} + +void TestUtils::testParseAndReadJson() +{ + QFETCH(QString, exp); + QFETCH(QJsonValue, result); + + QCOMPARE(Utils::parseAndReadJson(m_obj, exp), result); +} + void TestUtils::testParentDirPath_data() { QTest::addColumn("path"); diff --git a/tests/test_utils/test_utils.h b/tests/test_utils/test_utils.h index 31243393..cc8192fd 100644 --- a/tests/test_utils/test_utils.h +++ b/tests/test_utils/test_utils.h @@ -2,16 +2,25 @@ #define TESTS_UTILS_TEST_UTILS_H #include +#include namespace tests { class TestUtils : public QObject { Q_OBJECT + public: + explicit TestUtils(QObject *p_parent = nullptr); private slots: + void initTestCase(); + // Define test cases here per slot. + // Utils tests. + void testParseAndReadJson_data(); + void testParseAndReadJson(); + // PathUtils Tests. void testParentDirPath_data(); void testParentDirPath(); @@ -32,6 +41,9 @@ namespace tests void testRenameFile(); void testIsText(); + + private: + QJsonObject m_obj; }; } // ns tests diff --git a/tests/test_utils/test_utils.pro b/tests/test_utils/test_utils.pro index be437e34..91e5681f 100644 --- a/tests/test_utils/test_utils.pro +++ b/tests/test_utils/test_utils.pro @@ -10,10 +10,12 @@ INCLUDEPATH *= $$SRC_FOLDER SOURCES += \ test_utils.cpp \ + $$UTILS_FOLDER/utils.cpp \ $$UTILS_FOLDER/pathutils.cpp \ $$UTILS_FOLDER/fileutils.cpp HEADERS += \ test_utils.h \ + $$UTILS_FOLDER/utils.h \ $$UTILS_FOLDER/pathutils.h \ $$UTILS_FOLDER/fileutils.h diff --git a/tests/tests.pro b/tests/tests.pro index 95e8f08d..ae57b17f 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -2,4 +2,5 @@ TEMPLATE = subdirs SUBDIRS = \ test_utils \ - test_core + test_core \ + test_task