diff --git a/addons/CMakeLists.txt b/addons/CMakeLists.txt --- a/addons/CMakeLists.txt +++ b/addons/CMakeLists.txt @@ -49,6 +49,9 @@ # pipe text through some external command ecm_optional_add_subdirectory (textfilter) +# external tools +ecm_optional_add_subdirectory (externaltools) + # Rust complection plugin ecm_optional_add_subdirectory (rustcompletion) diff --git a/addons/externaltools/CMakeLists.txt b/addons/externaltools/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/addons/externaltools/CMakeLists.txt @@ -0,0 +1,42 @@ +project(externaltoolsplugin) +add_definitions(-DTRANSLATION_DOMAIN=\"kateexternaltoolsplugin\") + +include_directories( ${CMAKE_CURRENT_BINARY_DIR} ) + +set(externaltoolsplugin_PART_SRCS + externaltoolsplugin.cpp + kateexternaltoolsview.cpp + katetoolrunner.cpp + kateexternaltool.cpp + kateexternaltoolscommand.cpp + kateexternaltoolsconfigwidget.cpp +) + +# resource for ui file and stuff +qt5_add_resources(externaltoolsplugin_PART_SRCS plugin.qrc) + +set(externaltoolsplugin_PART_UI + configwidget.ui + tooldialog.ui + toolview.ui +) +ki18n_wrap_ui(externaltoolsplugin_PART_SRCS ${externaltoolsplugin_PART_UI} ) +add_library(externaltoolsplugin MODULE ${externaltoolsplugin_PART_SRCS}) + +# we compile in the .desktop file +kcoreaddons_desktop_to_json (externaltoolsplugin externaltoolsplugin.desktop) + +target_link_libraries(externaltoolsplugin + KF5::CoreAddons + KF5::IconThemes + KF5::TextEditor + KF5::I18n +) + +########### install files ############### +install(TARGETS externaltoolsplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) + +############# unit tests ################ +if (BUILD_TESTING) + add_subdirectory(autotests) +endif() diff --git a/addons/externaltools/Messages.sh b/addons/externaltools/Messages.sh new file mode 100644 --- /dev/null +++ b/addons/externaltools/Messages.sh @@ -0,0 +1,3 @@ +#! /bin/sh +$EXTRACTRC *.rc *.ui >> rc.cpp +$XGETTEXT *.cpp -o $podir/kateexternaltoolsplugin.pot diff --git a/addons/externaltools/autotests/CMakeLists.txt b/addons/externaltools/autotests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/addons/externaltools/autotests/CMakeLists.txt @@ -0,0 +1,11 @@ +include(ECMMarkAsTest) + +# Project Plugin +add_executable(externaltools_test + externaltooltest.cpp + ../kateexternaltool.cpp + ../katetoolrunner.cpp +) +add_test(plugin-externaltools_test externaltools_test) +target_link_libraries(externaltools_test kdeinit_kate Qt5::Test) +ecm_mark_as_test(externaltools_test) diff --git a/addons/externaltools/autotests/externaltooltest.h b/addons/externaltools/autotests/externaltooltest.h new file mode 100644 --- /dev/null +++ b/addons/externaltools/autotests/externaltooltest.h @@ -0,0 +1,42 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KATE_TOOLRUNNER_TEST_H +#define KATE_TOOLRUNNER_TEST_H + +#include + +class ExternalToolTest : public QObject +{ + Q_OBJECT + +public Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + +private Q_SLOTS: + void testLoadSave(); + void testRunListDirectory(); + void testRunTac(); +}; + +#endif + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/autotests/externaltooltest.cpp b/addons/externaltools/autotests/externaltooltest.cpp new file mode 100644 --- /dev/null +++ b/addons/externaltools/autotests/externaltooltest.cpp @@ -0,0 +1,116 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "externaltooltest.h" +#include "../kateexternaltool.h" +#include "../katetoolrunner.h" + +#include +#include + +#include +#include + +QTEST_MAIN(ExternalToolTest) + +void ExternalToolTest::initTestCase() +{ +} + +void ExternalToolTest::cleanupTestCase() +{ +} + +void ExternalToolTest::testLoadSave() +{ + KConfig config; + KConfigGroup cg(&config, "tool"); + + KateExternalTool tool; + tool.category = QStringLiteral("Git Tools"); + tool.name = QStringLiteral("git cola"); + tool.icon = QStringLiteral("git-cola"); + tool.executable = QStringLiteral("git-cola"); + tool.arguments = QStringLiteral("none"); + tool.input = QStringLiteral("in"); + tool.workingDir = QStringLiteral("/usr/bin"); + tool.mimetypes = QStringList{ QStringLiteral("everything") }; + tool.hasexec = true; + tool.actionName = QStringLiteral("asdf"); + tool.cmdname = QStringLiteral("git-cola"); + tool.saveMode = KateExternalTool::SaveMode::None; + + tool.save(cg); + + KateExternalTool clonedTool; + clonedTool.load(cg); + QCOMPARE(tool, clonedTool); +} + +void ExternalToolTest::testRunListDirectory() +{ + std::unique_ptr tool(new KateExternalTool()); + tool->category = QStringLiteral("Tools"); + tool->name = QStringLiteral("ls"); + tool->icon = QStringLiteral("none"); + tool->executable = QStringLiteral("ls"); + tool->arguments = QStringLiteral("/usr"); + tool->workingDir = QStringLiteral("/tmp"); + tool->mimetypes = QStringList{}; + tool->hasexec = true; + tool->actionName = QStringLiteral("ls"); + tool->cmdname = QStringLiteral("ls"); + tool->saveMode = KateExternalTool::SaveMode::None; + std::unique_ptr tool2(new KateExternalTool(*tool)); + + // 1. /tmp $ ls /usr + KateToolRunner runner1(std::move(tool), nullptr); + runner1.run(); + runner1.waitForFinished(); + QVERIFY(runner1.outputData().contains(QStringLiteral("bin"))); + + // 2. /usr $ ls + tool2->arguments.clear(); + tool2->workingDir = QStringLiteral("/usr"); + KateToolRunner runner2(std::move(tool2), nullptr); + runner2.run(); + runner2.waitForFinished(); + QVERIFY(runner2.outputData().contains(QStringLiteral("bin"))); + + // 1. and 2. must give the same result + QCOMPARE(runner1.outputData(), runner2.outputData()); +} + +void ExternalToolTest::testRunTac() +{ + std::unique_ptr tool(new KateExternalTool()); + tool->name = QStringLiteral("tac"); + tool->executable = QStringLiteral("tac"); + tool->input = QStringLiteral("a\nb\nc\n"); + tool->saveMode = KateExternalTool::SaveMode::None; + + // run tac to reverse order + KateToolRunner runner(std::move(tool), nullptr); + runner.run(); + runner.waitForFinished(); + QCOMPARE(runner.outputData(), QStringLiteral("c\nb\na\n")); +} + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/configwidget.ui b/addons/externaltools/configwidget.ui new file mode 100644 --- /dev/null +++ b/addons/externaltools/configwidget.ui @@ -0,0 +1,66 @@ + + + ExternalToolsConfigWidget + + + + 0 + 0 + 433 + 296 + + + + + + + This list shows all the configured tools, represented by their menu text. + + + true + + + + + + + + + &Add + + + + + + + &Edit... + + + + + + + &Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + diff --git a/addons/externaltools/externaltools b/addons/externaltools/externaltools new file mode 100644 --- /dev/null +++ b/addons/externaltools/externaltools @@ -0,0 +1,169 @@ +[Global] +tools=10 +version=1 + + +[Tool 0] +actionName=externaltool_RunShellScript +arguments=-e sh -c "cd %{CurrentDocument:Path} && pwd && chmod -vc a+x %{CurrentDocument:FileName} && ./%{CurrentDocument:FileName} ; echo Press any key to continue. && read -n 1" +category= +cmdname=run-script +executable=konsole +icon=system-run +input= +mimetypes= +name=Run Shell Script +output=Ignore +reload=false +save=CurrentDocument +workingDir=%{CurrentDocument:Path} + +[Tool 1] +actionName=externaltool_GoogleSelectedText +arguments="https://www.google.com/search?q=%{CurrentDocument:Selection:Text}" +category= +cmdname=google +executable=xdg-open +icon=globe +input= +mimetypes= +name=Google Selected Text +output=Ignore +reload=false +save=None +workingDir= + +[Tool 10] +actionName=externaltool_TestExternalTools +arguments= +category=Tools +cmdname=external-tool-test +executable=tac +icon= +input=CurrentDocument:FileBaseName: %{CurrentDocument:FileBaseName}\nCurrentDocument:FileName: %{CurrentDocument:FileName}\nCurrentDocument:FilePath: %{CurrentDocument:FilePath}\nCurrentDocument:FileExtension: %{CurrentDocument:FileExtension}\nCurrentDocument:Text: %{CurrentDocument:Text}\nCurrentDocument:Path: %{CurrentDocument:Path}\nCurrentDocument:NativeFilePath: %{CurrentDocument:NativeFilePath}\nCurrentDocument:NativePath: %{CurrentDocument:NativePath}\nCurrentDocument:Cursor:Line: %{CurrentDocument:Cursor:Line}\nCurrentDocument:Cursor:Column: %{CurrentDocument:Cursor:Column}\nCurrentDocument:Cursor:XPos: %{CurrentDocument:Cursor:XPos}\nCurrentDocument:Cursor:YPos: %{CurrentDocument:Cursor:YPos}\nCurrentDocument:Selection:Text: %{CurrentDocument:Selection:Text}\nCurrentDocument:Selection:StartLine: %{CurrentDocument:Selection:StartLine}\nCurrentDocument:Selection:StartColumn: %{CurrentDocument:Selection:StartColumn}\nCurrentDocument:Selection:EndLine: %{CurrentDocument:Selection:EndLine}\nCurrentDocument:Selection:EndColumn: %{CurrentDocument:Selection:EndColumn}\nCurrentDocument:RowCount: %{CurrentDocument:RowCount}\nDate:dd.MM.yyyy: %{Date:dd.MM.yyyy}\nDate:Locale: %{Date:Locale}\nDate:ISO: %{Date:ISO}\nTime:hh:mm:ss.zzz: %{Time:hh:mm:ss.zzz}\nTime:Locale: %{Time:Locale}\nTime:ISO: %{Time:ISO}\nENV:KATE_PID: %{ENV:KATE_PID}\nUUID: %{UUID}\n +mimetypes= +name=Test External Tools +output=DisplayInPane +reload=false +save=None +workingDir= + +[Tool 2] +actionName=externaltool_gitcola +arguments=-r %{CurrentDocument:Path} +category=Git +cmdname=git-cola +executable=git-cola +icon=git-cola +input= +mimetypes= +name=git-cola +output=Ignore +reload=false +save=None +workingDir= + +[Tool 3] +actionName=externaltool_gitk +arguments= +category=Git +cmdname=gitk +executable=gitk +icon=git-gui +input= +mimetypes= +name=gitk +output=Ignore +reload=false +save=None +workingDir=%{CurrentDocument:Path} + +[Tool 4] +actionName=externaltool_gitblame +arguments=gui blame %{CurrentDocument:FileName} +category=Git +cmdname=git-blame +executable=git +icon= +input= +mimetypes= +name=git blame +output=Ignore +reload=false +save=CurrentDocument +workingDir=%{CurrentDocument:Path} + +[Tool 5] +actionName=externaltool_QtQuick2Previewqmlscene +arguments=%{CurrentDocument:FileName} +category=Tools +cmdname=qml-preview +executable=qmlscene +icon= +input= +mimetypes=text/x-qml +name=Qt Quick 2 Preview (qmlscene) +output=Ignore +reload=false +save=CurrentDocument +workingDir=%{CurrentDocument:Path} + +[Tool 6] +actionName=externaltool_InsertUUID +arguments=%{UUID} +category=Tools +cmdname=uuid +executable=echo +icon= +input= +mimetypes= +name=Insert UUID +output=InsertAtCursor +reload=false +save=None +workingDir= + +[Tool 7] +actionName=externaltool_ClangFormatFullFile +arguments=-i %{CurrentDocument:FileName} +category=Tools +cmdname=clang-format-file +executable=clang-format +icon= +input= +mimetypes= +name=Clang Format Full File +output=Ignore +reload=true +save=CurrentDocument +workingDir=%{CurrentDocument:Path} + +[Tool 8] +actionName=externaltool_ClangFormatSelectedText +arguments=-assume-filename=%{CurrentDocument:FileName} +category=Tools +cmdname=clang-format-selection +executable=clang-format +icon= +input=\s%{CurrentDocument:Selection:Text} +mimetypes= +name=Clang Format Selected Text +output=ReplaceSelectedText +reload=false +save=None +workingDir=%{CurrentDocument:Path} + +[Tool 9] +actionName=externaltool_perl +arguments=%{ENV:KATE_PID +category=Tools +cmdname=perl +executable=echo +icon= +input= +mimetypes= +name=perl +output=DisplayInPane +reload=false +save=None +workingDir= diff --git a/addons/externaltools/externaltoolsplugin.h b/addons/externaltools/externaltoolsplugin.h new file mode 100644 --- /dev/null +++ b/addons/externaltools/externaltoolsplugin.h @@ -0,0 +1,123 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#ifndef KTEXTEDITOR_EXTERNALTOOLS_PLUGIN_H +#define KTEXTEDITOR_EXTERNALTOOLS_PLUGIN_H + +#include +#include + +namespace KTextEditor { class View; } + +class KateExternalToolsMenuAction; +class KateExternalToolsPluginView; +class KateExternalToolsCommand; +class KateExternalTool; +class KateToolRunner; + +class KateExternalToolsPlugin : public KTextEditor::Plugin +{ + Q_OBJECT + +public: + explicit KateExternalToolsPlugin(QObject* parent = nullptr, const QList& = QList()); + virtual ~KateExternalToolsPlugin(); + + /** + * Reimplemented to return the number of config pages, in this case 1. + */ + int configPages() const override; + + /** + * Reimplemented to return the KateExternalToolConfigWidget for number==0. + */ + KTextEditor::ConfigPage* configPage(int number = 0, QWidget* parent = nullptr) override; + + /** + * Reimplemented to instanciate a PluginView for each MainWindow. + */ + QObject* createView(KTextEditor::MainWindow* mainWindow) override; + + /** + * Reloads the external tools from disk. + */ + void reload(); + + /** + * Returns a list of KTextEDitor::Command strings. This is needed by + * the KateExternalToolsCommand constructor to pass the list of commands to + * the KTextEditor::Editor. + */ + QStringList commands() const; + + /** + * Returns the KateExternalTool for a specific command line command 'cmd. + */ + const KateExternalTool* toolForCommand(const QString& cmd) const; + + /** + * Returns a list of all existing external tools. + */ + const QVector & tools() const; + + /** + * Executes the tool based on the view as current document. + */ + void runTool(const KateExternalTool& tool, KTextEditor::View* view); + +Q_SIGNALS: + /** + * This signal is emitted whenever the external tools change. + * This is typically the case when external tools were modified, + * added, or removed via the config page. + */ + void externalToolsChanged(); + +public: + /** + * Called by the KateExternalToolsPluginView to register itself. + */ + void registerPluginView(KateExternalToolsPluginView * view); + + /** + * Called by the KateExternalToolsPluginView to unregister itself. + */ + void unregisterPluginView(KateExternalToolsPluginView * view); + + /** + * Returns the KateExternalToolsPluginView for the given mainWindow. + */ + KateExternalToolsPluginView* viewForMainWindow(KTextEditor::MainWindow* mainWindow) const; + +private: + QVector m_views; + QVector m_tools; + QStringList m_commands; + KateExternalToolsCommand* m_command = nullptr; + +private Q_SLOT: + /** + * Called whenever an external tool is done. + */ + void handleToolFinished(KateToolRunner* runner, int exitCode, bool crashed); +}; + +#endif + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/externaltoolsplugin.cpp b/addons/externaltools/externaltoolsplugin.cpp new file mode 100644 --- /dev/null +++ b/addons/externaltools/externaltoolsplugin.cpp @@ -0,0 +1,415 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#include "externaltoolsplugin.h" + +#include "kateexternaltoolsview.h" +#include "kateexternaltool.h" +#include "kateexternaltoolscommand.h" +#include "katetoolrunner.h" +#include "kateexternaltoolsconfigwidget.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +#include +#include +#include +#include +#include + + +K_PLUGIN_FACTORY_WITH_JSON(KateExternalToolsFactory, "externaltoolsplugin.json", + registerPlugin();) + +KateExternalToolsPlugin::KateExternalToolsPlugin(QObject* parent, const QList&) + : KTextEditor::Plugin(parent) +{ + reload(); + + auto editor = KTextEditor::Editor::instance(); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:FileBaseName"), i18n("Current document: File base name without path and suffix."), [](const QStringView&, KTextEditor::View* view) { + const auto url = view ? view->document()->url().toLocalFile() : QString(); + return QFileInfo(url).baseName(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:FileExtension"), i18n("Current document: File extension."), [](const QStringView&, KTextEditor::View* view) { + const auto url = view ? view->document()->url().toLocalFile() : QString(); + return QFileInfo(url).completeSuffix(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:FileName"), i18n("Current document: File name without path."), [](const QStringView&, KTextEditor::View* view) { + const auto url = view ? view->document()->url().toLocalFile() : QString(); + return QFileInfo(url).fileName(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:FilePath"), i18n("Current document: Full path including file name."), [](const QStringView&, KTextEditor::View* view) { + const auto url = view ? view->document()->url().toLocalFile() : QString(); + return QFileInfo(url).absoluteFilePath(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Text"), i18n("Current document: Contents of entire file."), [](const QStringView&, KTextEditor::View* view) { + return view ? view->document()->text() : QString(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Path"), i18n("Current document: Full path excluding file name."), [](const QStringView&, KTextEditor::View* view) { + const auto url = view ? view->document()->url().toLocalFile() : QString(); + return QFileInfo(url).absolutePath(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:NativeFilePath"), i18n("Current document: Full path including file name, with native path separator (backslash on Windows)."), [](const QStringView&, KTextEditor::View* view) { + const auto url = view ? view->document()->url().toLocalFile() : QString(); + return url.isEmpty() ? QString() : QDir::toNativeSeparators(QFileInfo(url).absoluteFilePath()); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:NativePath"), i18n("Current document: Full path excluding file name, with native path separator (backslash on Windows)."), [](const QStringView&, KTextEditor::View* view) { + const auto url = view ? view->document()->url().toLocalFile() : QString(); + return url.isEmpty() ? QString() : QDir::toNativeSeparators(QFileInfo(url).absolutePath()); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Cursor:Line"), i18n("Line number of the text cursor position in current document (starts with 0)."), [](const QStringView&, KTextEditor::View* view) { + return view ? QString::number(view->cursorPosition().line()) : QString(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Cursor:Column"), i18n("Column number of the text cursor position in current document (starts with 0)."), [](const QStringView&, KTextEditor::View* view) { + return view ? QString::number(view->cursorPosition().column()) : QString(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Cursor:XPos"), i18n("X component in global screen coordinates of the cursor position."), [](const QStringView&, KTextEditor::View* view) { + return view ? QString::number(view->mapToGlobal(view->cursorPositionCoordinates()).x()) : QString(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Cursor:YPos"), i18n("Y component in global screen coordinates of the cursor position."), [](const QStringView&, KTextEditor::View* view) { + return view ? QString::number(view->mapToGlobal(view->cursorPositionCoordinates()).y()) : QString(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Selection:Text"), i18n("Selection of current document."), [](const QStringView&, KTextEditor::View* view) { + return (view && view->selection()) ? view->selectionText() : QString(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Selection:StartLine"), i18n("Start line of selected text of current document."), [](const QStringView&, KTextEditor::View* view) { + return (view && view->selection()) ? QString::number(view->selectionRange().start().line()) : QString(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Selection:StartColumn"), i18n("Start column of selected text of current document."), [](const QStringView&, KTextEditor::View* view) { + return (view && view->selection()) ? QString::number(view->selectionRange().start().column()) : QString(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Selection:EndLine"), i18n("End line of selected text of current document."), [](const QStringView&, KTextEditor::View* view) { + return (view && view->selection()) ? QString::number(view->selectionRange().end().line()) : QString(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:Selection:EndColumn"), i18n("End column of selected text of current document."), [](const QStringView&, KTextEditor::View* view) { + return (view && view->selection()) ? QString::number(view->selectionRange().end().column()) : QString(); + }); + editor->registerVariableMatch(QStringLiteral("CurrentDocument:RowCount"), i18n("Number of rows of current document."), [](const QStringView&, KTextEditor::View* view) { + return view ? QString::number(view->document()->lines()) : QString(); + }); + + editor->registerVariableMatch(QStringLiteral("Date:Locale"), i18n("The current date in current locale format."), [](const QStringView&, KTextEditor::View*) { + return QDate::currentDate().toString(Qt::DefaultLocaleShortDate); + }); + editor->registerVariableMatch(QStringLiteral("Date:ISO"), i18n("The current date (ISO)."), [](const QStringView&, KTextEditor::View*) { + return QDate::currentDate().toString(Qt::ISODate); + }); + editor->registerVariablePrefix(QStringLiteral("Date:"), i18n("The current date (QDate formatstring)."), [](const QStringView& str, KTextEditor::View*) { + return QDate::currentDate().toString(str.right(str.length() - 5)); + }); + + editor->registerVariableMatch(QStringLiteral("Time:Locale"), i18n("The current time in current locale format."), [](const QStringView&, KTextEditor::View*) { + return QTime::currentTime().toString(Qt::DefaultLocaleShortDate); + }); + editor->registerVariableMatch(QStringLiteral("Time:ISO"), i18n("The current time (ISO)."), [](const QStringView&, KTextEditor::View*) { + return QTime::currentTime().toString(Qt::ISODate); + }); + editor->registerVariablePrefix(QStringLiteral("Time:"), i18n("The current time (QTime formatstring)."), [](const QStringView& str, KTextEditor::View*) { + return QTime::currentTime().toString(str.right(str.length() - 5)); + }); + + editor->registerVariablePrefix(QStringLiteral("ENV:"), i18n("Access environment variables."), [](const QStringView& str, KTextEditor::View*) { + return QString::fromLocal8Bit(qgetenv(str.right(str.size() - 4).toLocal8Bit().constData())); + }); + editor->registerVariablePrefix(QStringLiteral("JS:"), i18n("Evaluate simple JavaScript statements. The statements may not contain '{' nor '}' characters."), [](const QStringView& str, KTextEditor::View*) { + // FIXME + Q_UNUSED(str) + return QString(); + }); + + editor->registerVariableMatch(QStringLiteral("UUID"), i18n("Generate a new UUID."), [](const QStringView&, KTextEditor::View*) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) + return QUuid::createUuid().toString(QUuid::WithoutBraces); +#else + // LEGACY + QString uuid = QUuid::createUuid().toString(); + if (uuid.startsWith(QLatin1Char('{'))) + uuid.remove(0, 1); + if (uuid.endsWith(QLatin1Char('}'))) + uuid.chop(1); + return uuid; +#endif + }); +} + +KateExternalToolsPlugin::~KateExternalToolsPlugin() +{ + delete m_command; + m_command = nullptr; +} + +QObject* KateExternalToolsPlugin::createView(KTextEditor::MainWindow* mainWindow) +{ + KateExternalToolsPluginView* view = new KateExternalToolsPluginView(mainWindow, this); + connect(this, &KateExternalToolsPlugin::externalToolsChanged, view, &KateExternalToolsPluginView::rebuildMenu); + return view; +} + +void KateExternalToolsPlugin::reload() +{ + delete m_command; + m_command = nullptr; + m_commands.clear(); + qDeleteAll(m_tools); + m_tools.clear(); + + KConfig _config(QStringLiteral("externaltools"), KConfig::NoGlobals, QStandardPaths::ApplicationsLocation); + KConfigGroup config(&_config, "Global"); + const int toolCount = config.readEntry("tools", 0); + + for (int i = 0; i < toolCount; ++i) { + config = KConfigGroup(&_config, QStringLiteral("Tool %1").arg(i)); + + auto t = new KateExternalTool(); + t->load(config); + m_tools.push_back(t); + + // FIXME test for a command name first! + if (t->hasexec && (!t->cmdname.isEmpty())) { + m_commands.push_back(t->cmdname); + } + } + + if (KAuthorized::authorizeAction(QStringLiteral("shell_access"))) { + m_command = new KateExternalToolsCommand(this); + } + + Q_EMIT externalToolsChanged(); +} + +QStringList KateExternalToolsPlugin::commands() const +{ + return m_commands; +} + +const KateExternalTool* KateExternalToolsPlugin::toolForCommand(const QString& cmd) const +{ + for (auto tool : m_tools) { + if (tool->cmdname == cmd) { + return tool; + } + } + return nullptr; +} + +const QVector & KateExternalToolsPlugin::tools() const +{ + return m_tools; +} + +void KateExternalToolsPlugin::runTool(const KateExternalTool& tool, KTextEditor::View* view) +{ + // expand the macros in command if any, + // and construct a command with an absolute path + auto mw = view->mainWindow(); + + // save documents if requested + if (tool.saveMode == KateExternalTool::SaveMode::CurrentDocument) { + // only save if modified, to avoid unnecessary recompiles + if (view->document()->isModified()) { + view->document()->save(); + } + } else if (tool.saveMode == KateExternalTool::SaveMode::AllDocuments) { + foreach (KXMLGUIClient* client, mw->guiFactory()->clients()) { + if (QAction* a = client->actionCollection()->action(QStringLiteral("file_save_all"))) { + a->trigger(); + break; + } + } + } + + // copy tool + std::unique_ptr copy(new KateExternalTool(tool)); + + // clear previous toolview data + auto pluginView = viewForMainWindow(mw); + pluginView->clearToolView(); + pluginView->addToolStatus(i18n("Running external tool: %1", copy->name)); + pluginView->addToolStatus(i18n("- Executable: %1", copy->executable)); + pluginView->addToolStatus(i18n("- Arguments : %1", copy->arguments)); + pluginView->addToolStatus(i18n("- Input : %1", copy->input)); + pluginView->addToolStatus(QString()); + + // expand macros + auto editor = KTextEditor::Editor::instance(); + if (!editor->expandText(copy->executable, view, copy->executable)) { + pluginView->addToolStatus(i18n("Failed to expand executable: '%1'", copy->executable)); + pluginView->showToolView(ToolViewFocus::StatusTab); + return; + } + + if (!editor->expandText(copy->arguments, view, copy->arguments)) { + pluginView->addToolStatus(i18n("Failed to expand argument: %1", copy->arguments)); + pluginView->showToolView(ToolViewFocus::StatusTab); + return; + } + + if (!editor->expandText(copy->workingDir, view, copy->workingDir)) { + pluginView->addToolStatus(i18n("Failed to expand working directory: %1", copy->workingDir)); + pluginView->showToolView(ToolViewFocus::StatusTab); + return; + } + + if (!editor->expandText(copy->input, view, copy->input)) { + pluginView->addToolStatus(i18n("Failed to expand input: %1", copy->input)); + pluginView->showToolView(ToolViewFocus::StatusTab); + return; + } + + // Allocate runner on heap such that it lives as long as the child + // process is running and does not block the main thread. + auto runner = new KateToolRunner(std::move(copy), view, this); + + // use QueuedConnection, since handleToolFinished deletes the runner + connect(runner, &KateToolRunner::toolFinished, this, &KateExternalToolsPlugin::handleToolFinished, Qt::QueuedConnection); + runner->run(); +} + +void KateExternalToolsPlugin::handleToolFinished(KateToolRunner* runner, int exitCode, bool crashed) +{ + auto view = runner->view(); + if (view && !runner->outputData().isEmpty()) { + switch (runner->tool()->outputMode) { + case KateExternalTool::OutputMode::InsertAtCursor: { + KTextEditor::Document::EditingTransaction transaction(view->document()); + view->removeSelection(); + view->insertText(runner->outputData()); + break; + } + case KateExternalTool::OutputMode::ReplaceSelectedText: { + KTextEditor::Document::EditingTransaction transaction(view->document()); + view->removeSelectionText(); + view->insertText(runner->outputData()); + break; + } + case KateExternalTool::OutputMode::ReplaceCurrentDocument: { + KTextEditor::Document::EditingTransaction transaction(view->document()); + view->document()->clear(); + view->insertText(runner->outputData()); + break; + } + case KateExternalTool::OutputMode::AppendToCurrentDocument: { + view->document()->insertText(view->document()->documentEnd(), runner->outputData()); + break; + } + case KateExternalTool::OutputMode::InsertInNewDocument: { + auto mainWindow = view->mainWindow(); + auto newView = mainWindow->openUrl({}); + newView->insertText(runner->outputData()); + mainWindow->activateView(newView->document()); + break; + } + default: + break; + } + } + + if (view && runner->tool()->reload) { + // updates-enabled trick: avoid some flicker + const bool wereUpdatesEnabled = view->updatesEnabled(); + view->setUpdatesEnabled(false); + view->document()->documentReload(); + view->setUpdatesEnabled(wereUpdatesEnabled); + } + + KateExternalToolsPluginView* pluginView = runner->view() ? viewForMainWindow(runner->view()->mainWindow()) : nullptr; + if (pluginView) { + bool hasOutputInPane = false; + if (runner->tool()->outputMode == KateExternalTool::OutputMode::DisplayInPane) { + pluginView->setOutputData(runner->outputData()); + hasOutputInPane = !runner->outputData().isEmpty(); + } + + if (!runner->errorData().isEmpty()) { + pluginView->addToolStatus(i18n("Data written to stderr:")); + pluginView->addToolStatus(runner->errorData()); + } + + // empty line + pluginView->addToolStatus(QString()); + + // print crash & exit code + if (crashed) { + pluginView->addToolStatus(i18n("Warning: External tool crashed.")); + } + pluginView->addToolStatus(i18n("Finished with exit code: %1", exitCode)); + + if (crashed || exitCode != 0) { + pluginView->showToolView(ToolViewFocus::StatusTab); + } else if (hasOutputInPane) { + pluginView->showToolView(ToolViewFocus::OutputTab); + } + } + + delete runner; +} + +int KateExternalToolsPlugin::configPages() const +{ + return 1; +} + +KTextEditor::ConfigPage* KateExternalToolsPlugin::configPage(int number, QWidget* parent) +{ + if (number == 0) { + return new KateExternalToolsConfigWidget(parent, this); + } + return nullptr; +} + +void KateExternalToolsPlugin::registerPluginView(KateExternalToolsPluginView * view) +{ + Q_ASSERT(!m_views.contains(view)); + m_views.push_back(view); +} + +void KateExternalToolsPlugin::unregisterPluginView(KateExternalToolsPluginView * view) +{ + Q_ASSERT(m_views.contains(view)); + m_views.removeAll(view); +} + +KateExternalToolsPluginView* KateExternalToolsPlugin::viewForMainWindow(KTextEditor::MainWindow* mainWindow) const +{ + for (auto view : m_views) { + if (view->mainWindow() == mainWindow) { + return view; + } + } + return nullptr; +} + +#include "externaltoolsplugin.moc" + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/externaltoolsplugin.desktop b/addons/externaltools/externaltoolsplugin.desktop new file mode 100644 --- /dev/null +++ b/addons/externaltools/externaltoolsplugin.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Service +ServiceTypes=KTextEditor/Plugin +X-KDE-Library=externaltoolsplugin +Name=External Tools +Comment=External Tools diff --git a/addons/externaltools/kateexternaltool.h b/addons/externaltools/kateexternaltool.h new file mode 100644 --- /dev/null +++ b/addons/externaltools/kateexternaltool.h @@ -0,0 +1,126 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#ifndef KTEXTEDITOR_KATE_EXTERNALTOOL_H +#define KTEXTEDITOR_KATE_EXTERNALTOOL_H + +#include +#include +#include + +class KConfigGroup; + +/** + * This class defines a single external tool. + */ +class KateExternalTool +{ +public: + /** + * Defines whether any document should be saved before running the tool. + */ + enum class SaveMode { + //! Do not save any document. + None, + //! Save current document. + CurrentDocument, + //! Save all documents + AllDocuments + }; + + /** + * Defines where to redirect stdout from the tool. + */ + enum class OutputMode { + Ignore, + InsertAtCursor, + ReplaceSelectedText, + ReplaceCurrentDocument, + AppendToCurrentDocument, + InsertInNewDocument, + DisplayInPane + }; + +public: + /// The category used in the menu to categorize the tool. + QString category; + /// The name used in the menu. + QString name; + /// the icon to use in the menu. + QString icon; + /// The name or path of the executable. + QString executable; + /// The command line arguments. + QString arguments; + /// The stdin input. + QString input; + /// The working directory, if specified. + QString workingDir; + /// Optional list of mimetypes for which this action is valid. + QStringList mimetypes; + /// The name for the action. This is generated first time the + /// action is is created. + QString actionName; + /// The name for the commandline. + QString cmdname; + /// Possibly save documents prior to activating the tool command. + SaveMode saveMode = SaveMode::None; + /// Reload current document after execution + bool reload = false; + /// Defines where to redirect the tool's output + OutputMode outputMode = OutputMode::Ignore; + +public: + /// This is set when loading the Tool from disk. + bool hasexec = false; + + /** + * @return true if mimetypes is empty, or the @p mimetype matches. + */ + bool matchesMimetype(const QString& mimetype) const; + + /** + * @return true if "executable" exists and has the executable bit set, or is + * empty. + * This is run at least once, and the tool is disabled if it fails. + */ + bool checkExec() const; + + /** + * Load tool data from the config group @p cg. + */ + void load(const KConfigGroup& cg); + + /** + * Save tool data to the config group @p cg. + */ + void save(KConfigGroup& cg) const; +}; + +/** + * Compares for equality. All fields have to match. + */ +bool operator==(const KateExternalTool & lhs, const KateExternalTool & rhs); + +// for use in QVariant (QAction::setData() and QAction::data()) +Q_DECLARE_METATYPE(KateExternalTool*) + +#endif // KTEXTEDITOR_KATE_EXTERNALTOOL_H + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/kateexternaltool.cpp b/addons/externaltools/kateexternaltool.cpp new file mode 100644 --- /dev/null +++ b/addons/externaltools/kateexternaltool.cpp @@ -0,0 +1,136 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#include "kateexternaltool.h" + +#include +#include + +namespace { + QString toString(KateExternalTool::SaveMode saveMode) + { + switch (saveMode) { + case KateExternalTool::SaveMode::None: return QStringLiteral("None"); + case KateExternalTool::SaveMode::CurrentDocument: return QStringLiteral("CurrentDocument"); + case KateExternalTool::SaveMode::AllDocuments: return QStringLiteral("AllDocuments"); + } + Q_ASSERT(false); // yout forgot a case above + return QStringLiteral("None"); + } + + KateExternalTool::SaveMode toSaveMode(const QString & mode) + { + if (mode == QStringLiteral("None")) return KateExternalTool::SaveMode::None; + if (mode == QStringLiteral("CurrentDocument")) return KateExternalTool::SaveMode::CurrentDocument; + if (mode == QStringLiteral("AllDocuments")) return KateExternalTool::SaveMode::AllDocuments; + return KateExternalTool::SaveMode::None; + } + + QString toString(KateExternalTool::OutputMode outputMode) + { + switch (outputMode) { + case KateExternalTool::OutputMode::Ignore: return QStringLiteral("Ignore"); + case KateExternalTool::OutputMode::InsertAtCursor: return QStringLiteral("InsertAtCursor"); + case KateExternalTool::OutputMode::ReplaceSelectedText: return QStringLiteral("ReplaceSelectedText"); + case KateExternalTool::OutputMode::ReplaceCurrentDocument: return QStringLiteral("ReplaceCurrentDocument"); + case KateExternalTool::OutputMode::AppendToCurrentDocument: return QStringLiteral("AppendToCurrentDocument"); + case KateExternalTool::OutputMode::InsertInNewDocument: return QStringLiteral("InsertInNewDocument"); + case KateExternalTool::OutputMode::DisplayInPane: return QStringLiteral("DisplayInPane"); + } + Q_ASSERT(false); // yout forgot a case above + return QStringLiteral("Ignore"); + } + + KateExternalTool::OutputMode toOutputMode(const QString & mode) + { + if (mode == QStringLiteral("Ignore")) return KateExternalTool::OutputMode::Ignore; + if (mode == QStringLiteral("InsertAtCursor")) return KateExternalTool::OutputMode::InsertAtCursor; + if (mode == QStringLiteral("ReplaceSelectedText")) return KateExternalTool::OutputMode::ReplaceSelectedText; + if (mode == QStringLiteral("ReplaceCurrentDocument")) return KateExternalTool::OutputMode::ReplaceCurrentDocument; + if (mode == QStringLiteral("AppendToCurrentDocument")) return KateExternalTool::OutputMode::AppendToCurrentDocument; + if (mode == QStringLiteral("InsertInNewDocument")) return KateExternalTool::OutputMode::InsertInNewDocument; + if (mode == QStringLiteral("DisplayInPane")) return KateExternalTool::OutputMode::DisplayInPane; + return KateExternalTool::OutputMode::Ignore; + } +} + +bool KateExternalTool::checkExec() const +{ + return !QStandardPaths::findExecutable(executable).isEmpty(); +} + +bool KateExternalTool::matchesMimetype(const QString& mt) const +{ + return mimetypes.isEmpty() || mimetypes.contains(mt); +} + +void KateExternalTool::load(const KConfigGroup& cg) +{ + category = cg.readEntry("category", ""); + name = cg.readEntry("name", ""); + icon = cg.readEntry("icon", ""); + executable = cg.readEntry("executable", ""); + arguments = cg.readEntry("arguments", ""); + input = cg.readEntry("input", ""); + workingDir = cg.readEntry("workingDir", ""); + mimetypes = cg.readEntry("mimetypes", QStringList()); + actionName = cg.readEntry("actionName"); + cmdname = cg.readEntry("cmdname"); + saveMode = toSaveMode(cg.readEntry("save", "None")); + reload = cg.readEntry("reload", false); + outputMode = toOutputMode(cg.readEntry("output", "Ignore")); + + hasexec = checkExec(); +} + +void KateExternalTool::save(KConfigGroup& cg) const +{ + cg.writeEntry("category", category); + cg.writeEntry("name", name); + cg.writeEntry("icon", icon); + cg.writeEntry("executable", executable); + cg.writeEntry("arguments", arguments); + cg.writeEntry("input", input); + cg.writeEntry("workingDir", workingDir); + cg.writeEntry("mimetypes", mimetypes); + cg.writeEntry("actionName", actionName); + cg.writeEntry("cmdname", cmdname); + cg.writeEntry("save", toString(saveMode)); + cg.writeEntry("reload", reload); + cg.writeEntry("output", toString(outputMode)); +} + +bool operator==(const KateExternalTool & lhs, const KateExternalTool & rhs) +{ + return lhs.category == rhs.category + && lhs.name == rhs.name + && lhs.icon == rhs.icon + && lhs.executable == rhs.executable + && lhs.arguments == rhs.arguments + && lhs.input == rhs.input + && lhs.workingDir == rhs.workingDir + && lhs.mimetypes == rhs.mimetypes + && lhs.actionName == rhs.actionName + && lhs.cmdname == rhs.cmdname + && lhs.saveMode == rhs.saveMode + && lhs.reload == rhs.reload + && lhs.outputMode == rhs.outputMode; +} + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/kateexternaltoolscommand.h b/addons/externaltools/kateexternaltoolscommand.h new file mode 100644 --- /dev/null +++ b/addons/externaltools/kateexternaltoolscommand.h @@ -0,0 +1,51 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#ifndef KTEXTEDITOR_KATE_EXTERNALTOOLS_COMMAND_H +#define KTEXTEDITOR_KATE_EXTERNALTOOLS_COMMAND_H + +#include + +class KateExternalToolsPlugin; +class KateExternalTool; + +/** + * Helper class that registers and executes the respective external tool. + */ +class KateExternalToolsCommand : public KTextEditor::Command +{ +public: + KateExternalToolsCommand(KateExternalToolsPlugin* plugin); + virtual ~KateExternalToolsCommand() = default; + +public: + bool exec(KTextEditor::View* view, const QString& cmd, QString& msg, + const KTextEditor::Range& range = KTextEditor::Range::invalid()) override; + bool help(KTextEditor::View* view, const QString& cmd, QString& msg) override; + +private: + void runTool(KateExternalTool& tool, KTextEditor::View* view); + +private: + KateExternalToolsPlugin* m_plugin; +}; + +#endif // KTEXTEDITOR_KATE_EXTERNALTOOLS_COMMAND_H + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/kateexternaltoolscommand.cpp b/addons/externaltools/kateexternaltoolscommand.cpp new file mode 100644 --- /dev/null +++ b/addons/externaltools/kateexternaltoolscommand.cpp @@ -0,0 +1,59 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#include "kateexternaltoolscommand.h" +#include "externaltoolsplugin.h" +#include "kateexternaltool.h" + +#include + +KateExternalToolsCommand::KateExternalToolsCommand(KateExternalToolsPlugin* plugin) + : KTextEditor::Command(plugin->commands()) + , m_plugin(plugin) +{ +} + +bool KateExternalToolsCommand::exec(KTextEditor::View* view, const QString& cmd, QString& msg, + const KTextEditor::Range& range) +{ + Q_UNUSED(msg) + Q_UNUSED(range) + + const QString command = cmd.trimmed(); + const auto tool = m_plugin->toolForCommand(command); + if (tool) { + m_plugin->runTool(*tool, view); + return true; + } + return false; +} + +bool KateExternalToolsCommand::help(KTextEditor::View*, const QString& cmd, QString& msg) +{ + const QString command = cmd.trimmed(); + const auto tool = m_plugin->toolForCommand(command); + if (tool) { + msg = i18n("Starts the external tool '%1'", tool->name); + return true; + } + + return false; +} + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/kateexternaltoolsconfigwidget.h b/addons/externaltools/kateexternaltoolsconfigwidget.h new file mode 100644 --- /dev/null +++ b/addons/externaltools/kateexternaltoolsconfigwidget.h @@ -0,0 +1,168 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#ifndef KTEXTEDITOR_EXTERNALTOOLS_CONFIGWIDGET_H +#define KTEXTEDITOR_EXTERNALTOOLS_CONFIGWIDGET_H + +#include "ui_configwidget.h" +#include "ui_tooldialog.h" + +#include +#include +#include +#include + +#include +#include +#include + +class KConfig; +class KateExternalToolsPlugin; +class KateExternalTool; + +/** + * The config widget. + * The config widget allows the user to view a list of services of the type + * "Kate/ExternalTool" and add, remove or edit them. + */ +class KateExternalToolsConfigWidget : public KTextEditor::ConfigPage, public Ui::ExternalToolsConfigWidget +{ + Q_OBJECT +public: + KateExternalToolsConfigWidget(QWidget* parent, KateExternalToolsPlugin* plugin); + virtual ~KateExternalToolsConfigWidget(); + + QString name() const override; + QString fullName() const override; + QIcon icon() const override; + +public Q_SLOTS: + void apply() override; + void reset() override; + void defaults() override { reset(); } + +private Q_SLOTS: + void slotAddCategory(); + void slotAddTool(); + void slotEdit(); + void slotRemove(); + void slotSelectionChanged(); + + /** + * Helper to open the ToolDialog. + * Returns true, if the user clicked OK. + */ + bool editTool(KateExternalTool* tool); + + /** + * Creates a new category or returns existing one. + */ + QStandardItem * addCategory(const QString & category); + + /** + * Returns the currently active category. The returned pointer is always valid. + */ + QStandardItem * currentCategory() const; + + /** + * Clears the tools model. + */ + void clearTools(); + +private: + QPixmap blankIcon(); + +private: + KConfig* m_config = nullptr; + bool m_changed = false; + KateExternalToolsPlugin* m_plugin; + QStandardItemModel m_toolsModel; + QStandardItem * m_noCategory = nullptr; +}; + +/** + * A Dialog to edit a single KateExternalTool object + */ +class KateExternalToolServiceEditor : public QDialog +{ + Q_OBJECT + +public: + explicit KateExternalToolServiceEditor(KateExternalTool* tool = nullptr, QWidget* parent = nullptr); + +private Q_SLOTS: + /** + * Run when the OK button is clicked, to ensure critical values are provided. + */ + void slotOKClicked(); + + /** + * show a mimetype chooser dialog + */ + void showMTDlg(); + +public: + Ui::ToolDialog* ui; + +private: + KateExternalTool* m_tool; +}; + +/** + * Action that is only visible if QLineEdit has focus. + */ +class ContextAction : public QObject +{ + Q_OBJECT + +public: + /** + * Constructor that will add @p action to all attached QLineEdits. + */ + ContextAction(const QIcon & icon, const QString & text, QObject * parent = nullptr); + + /** + * Attaches the action provided in the constructor to @p lineEdit + * whenever the @p lineEdit has focus. + */ + void attachTo(QLineEdit * lineEdit); + +Q_SIGNALS: + /** + * This signal is emitted whenever the action was triggered for + * the given @p lineEdit. The @p lineEdit is always a valid pointer. + */ + void triggered(QLineEdit * lineEdit); + +protected: + /** + * Reimplemented to show/hide action for the currently focused + * line edit. + */ + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + QLineEdit * m_currentLineEdit = nullptr; + QAction * m_action; +}; + + +#endif // KTEXTEDITOR_EXTERNALTOOLS_CONFIGWIDGET_H + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/kateexternaltoolsconfigwidget.cpp b/addons/externaltools/kateexternaltoolsconfigwidget.cpp new file mode 100644 --- /dev/null +++ b/addons/externaltools/kateexternaltoolsconfigwidget.cpp @@ -0,0 +1,491 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +// TODO +// Icons +// Direct shortcut setting +#include "kateexternaltoolsconfigwidget.h" +#include "externaltoolsplugin.h" +#include "kateexternaltool.h" +#include "katetoolrunner.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + constexpr int ToolRole = Qt::UserRole + 1; + + /** + * Helper function to create a QStandardItem that internally stores a pointer to a KateExternalTool. + */ + QStandardItem * newToolItem(const QPixmap& icon, KateExternalTool* tool) + { + auto item = new QStandardItem(icon, tool->name); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled); + item->setData(QVariant::fromValue(reinterpret_cast(tool)), ToolRole); + return item; + } + + /** + * Helper function to return an internally stored KateExternalTool for a QStandardItem. + * If a nullptr is returned, it means the QStandardItem is a category. + */ + KateExternalTool* toolForItem(QStandardItem* item) + { + return item ? reinterpret_cast(item->data(ToolRole).value()) : nullptr; + } +} + +// BEGIN KateExternalToolServiceEditor +KateExternalToolServiceEditor::KateExternalToolServiceEditor(KateExternalTool* tool, QWidget* parent) + : QDialog(parent) + , m_tool(tool) +{ + setWindowTitle(i18n("Edit External Tool")); + setWindowIcon(QIcon::fromTheme(QStringLiteral("system-run"))); + + ui = new Ui::ToolDialog(); + ui->setupUi(this); + ui->btnIcon->setIconSize(KIconLoader::SizeSmall); + + auto contextAction = new ContextAction(QIcon::fromTheme(QStringLiteral("code-context"), QIcon::fromTheme(QStringLiteral("tools-wizard"))), + QStringLiteral("Insert variable"), this); + contextAction->attachTo(ui->edtExecutable); + contextAction->attachTo(ui->edtArgs); +// contextAction->attachTo(ui->edtInput); // not a QLineEdit + contextAction->attachTo(ui->edtWorkingDir); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &KateExternalToolServiceEditor::slotOKClicked); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(ui->btnMimeType, &QToolButton::clicked, this, &KateExternalToolServiceEditor::showMTDlg); + + Q_ASSERT(m_tool != nullptr); + ui->edtName->setText(m_tool->name); + if (!m_tool->icon.isEmpty()) + ui->btnIcon->setIcon(m_tool->icon); + + ui->edtExecutable->setText(m_tool->executable); + ui->edtArgs->setText(m_tool->arguments); + ui->edtInput->setText(m_tool->input); + ui->edtWorkingDir->setText(m_tool->workingDir); + ui->edtMimeType->setText(m_tool->mimetypes.join(QStringLiteral("; "))); + ui->cmbSave->setCurrentIndex(static_cast(m_tool->saveMode)); + ui->chkReload->setChecked(m_tool->reload); + ui->cmbOutput->setCurrentIndex(static_cast(m_tool->outputMode)); + ui->edtCommand->setText(m_tool->cmdname); +} + +void KateExternalToolServiceEditor::slotOKClicked() +{ + if (ui->edtName->text().isEmpty() || ui->edtExecutable->text().isEmpty()) { + QMessageBox::information(this, i18n("External Tool"), + i18n("You must specify at least a name and an executable")); + return; + } + accept(); +} + +void KateExternalToolServiceEditor::showMTDlg() +{ + QString text = i18n("Select the MimeTypes for which to enable this tool."); + QStringList list + = ui->edtMimeType->text().split(QRegularExpression(QStringLiteral("\\s*;\\s*")), QString::SkipEmptyParts); + KMimeTypeChooserDialog d(i18n("Select Mime Types"), text, list, QStringLiteral("text"), this); + if (d.exec() == QDialog::Accepted) { + ui->edtMimeType->setText(d.chooser()->mimeTypes().join(QStringLiteral(";"))); + } +} +// END KateExternalToolServiceEditor + +static std::vector childItems(const QStandardItem * item) +{ + // collect all KateExternalTool items + std::vector children; + for (int i = 0; i < item->rowCount(); ++i) { + children.push_back(item->child(i)); + } + return children; +} + +static std::vector collectTools(const QStandardItemModel & model) +{ + std::vector tools; + for (auto categoryItem : childItems(model.invisibleRootItem())) { + for (auto child : childItems(categoryItem)) { + auto tool = toolForItem(child); + Q_ASSERT(tool != nullptr); + tools.push_back(tool); + } + } + return tools; +} + +// BEGIN KateExternalToolsConfigWidget +KateExternalToolsConfigWidget::KateExternalToolsConfigWidget(QWidget* parent, KateExternalToolsPlugin* plugin) + : KTextEditor::ConfigPage(parent) + , m_plugin(plugin) +{ + setupUi(this); + layout()->setMargin(0); + lbTools->setModel(&m_toolsModel); + lbTools->setSelectionMode(QAbstractItemView::SingleSelection); + lbTools->setDragEnabled(true); + lbTools->setAcceptDrops(true); + lbTools->setDefaultDropAction(Qt::MoveAction); + lbTools->setDropIndicatorShown(true); + lbTools->setDragDropOverwriteMode(false); + lbTools->setDragDropMode(QAbstractItemView::InternalMove); + + // Add... button popup menu + auto addMenu = new QMenu(); + auto addToolAction = addMenu->addAction(i18n("Add Tool...")); + auto addCategoryAction = addMenu->addAction(i18n("Add Category")); + btnAdd->setMenu(addMenu); + + connect(addCategoryAction, &QAction::triggered, this, &KateExternalToolsConfigWidget::slotAddCategory); + connect(addToolAction, &QAction::triggered, this, &KateExternalToolsConfigWidget::slotAddTool); + connect(btnRemove, &QPushButton::clicked, this, &KateExternalToolsConfigWidget::slotRemove); + connect(btnEdit, &QPushButton::clicked, this, &KateExternalToolsConfigWidget::slotEdit); + connect(lbTools->selectionModel(), &QItemSelectionModel::currentChanged, [this](){ + slotSelectionChanged(); + }); + connect(lbTools, &QTreeView::doubleClicked, this, &KateExternalToolsConfigWidget::slotEdit); + + m_config = new KConfig(QStringLiteral("externaltools"), KConfig::NoGlobals, QStandardPaths::ApplicationsLocation); + + // reset triggers a reload of the existing tools + reset(); + slotSelectionChanged(); + + connect(&m_toolsModel, &QStandardItemModel::itemChanged, [this](){ + m_changed = true; + Q_EMIT changed(); + }); +} + +KateExternalToolsConfigWidget::~KateExternalToolsConfigWidget() +{ + clearTools(); + + delete m_config; +} + +QString KateExternalToolsConfigWidget::name() const +{ + return i18n("External Tools"); +} + +QString KateExternalToolsConfigWidget::fullName() const +{ + return i18n("External Tools"); +} + +QIcon KateExternalToolsConfigWidget::icon() const +{ + return QIcon::fromTheme(QStringLiteral("system-run")); +} + +void KateExternalToolsConfigWidget::reset() +{ + clearTools(); + m_toolsModel.invisibleRootItem()->setFlags(Qt::NoItemFlags); + + // the "Uncategorized" category always exists + m_noCategory = addCategory(i18n("Uncategorized")); + m_noCategory->setFlags(Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled); + + // create other tools and categories + const auto tools = m_plugin->tools(); + for (auto tool : tools) { + auto clone = new KateExternalTool(*tool); + auto item = newToolItem(clone->icon.isEmpty() ? blankIcon() : SmallIcon(clone->icon), clone); + auto category = clone->category.isEmpty() ? m_noCategory : addCategory(clone->category); + category->appendRow(item); + } + lbTools->expandAll(); + m_changed = false; +} + +QPixmap KateExternalToolsConfigWidget::blankIcon() +{ + QPixmap pm(KIconLoader::SizeSmall, KIconLoader::SizeSmall); + pm.fill(); + pm.setMask(pm.createHeuristicMask()); + return pm; +} + +void KateExternalToolsConfigWidget::apply() +{ + if (!m_changed) + return; + m_changed = false; + + // collect all KateExternalTool items + std::vector tools; + for (auto categoryItem : childItems(m_toolsModel.invisibleRootItem())) { + const QString category = (categoryItem == m_noCategory) ? QString() : categoryItem->text(); + for (auto child : childItems(categoryItem)) { + auto tool = toolForItem(child); + Q_ASSERT(tool != nullptr); + // at this point, we have to overwrite the category, since it may have changed (and we never tracked this) + tool->category = category; + tools.push_back(tool); + } + } + + // write tool configuration to disk + m_config->group("Global").writeEntry("tools", static_cast(tools.size())); + for (size_t i = 0; i < tools.size(); i++) { + const QString section = QStringLiteral("Tool ") + QString::number(i); + KConfigGroup cg(m_config, section); + tools[i]->save(cg); + } + + m_config->sync(); + m_plugin->reload(); +} + +void KateExternalToolsConfigWidget::slotSelectionChanged() +{ + // update button state + auto item = m_toolsModel.itemFromIndex(lbTools->currentIndex()); + const bool isToolItem = toolForItem(item) != nullptr; + const bool isCategory = item && !isToolItem; + + btnEdit->setEnabled(isToolItem || isCategory); + btnRemove->setEnabled(isToolItem); +} + +bool KateExternalToolsConfigWidget::editTool(KateExternalTool* tool) +{ + bool changed = false; + + KateExternalToolServiceEditor editor(tool, this); + editor.resize(m_config->group("Editor").readEntry("Size", QSize())); + if (editor.exec() == QDialog::Accepted) { + tool->name = editor.ui->edtName->text(); + tool->icon = editor.ui->btnIcon->icon(); + tool->executable = editor.ui->edtExecutable->text(); + tool->arguments = editor.ui->edtArgs->text(); + tool->input = editor.ui->edtInput->toPlainText(); + tool->workingDir = editor.ui->edtWorkingDir->text(); + tool->mimetypes = editor.ui->edtMimeType->text().split(QRegularExpression(QStringLiteral("\\s*;\\s*")), + QString::SkipEmptyParts); + tool->saveMode = static_cast(editor.ui->cmbSave->currentIndex()); + tool->reload = editor.ui->chkReload->isChecked(); + tool->outputMode = static_cast(editor.ui->cmbOutput->currentIndex()); + tool->cmdname = editor.ui->edtCommand->text(); + + // sticky action collection name, never changes again, so that shortcuts stay + tool->actionName = QStringLiteral("externaltool_") + QString(tool->name).remove(QRegularExpression(QStringLiteral("\\W+"))); + + changed = true; + } + + m_config->group("Editor").writeEntry("Size", editor.size()); + m_config->sync(); + + return changed; +} + +QStandardItem * KateExternalToolsConfigWidget::addCategory(const QString & category) +{ + // searach for existing category + auto items = m_toolsModel.findItems(category); + if (!items.empty()) { + return items.front(); + } + + // ...otherwise, create it + auto item = new QStandardItem(category); + + // for now, categories are not movable, otherwise, the use can move a + // category into another category, which is not supported right now + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled | Qt::ItemIsEditable); + + m_toolsModel.appendRow(item); + return item; +} + +QStandardItem * KateExternalToolsConfigWidget::currentCategory() const +{ + auto index = lbTools->currentIndex(); + if (!index.isValid()) { + return m_noCategory; + } + + auto item = m_toolsModel.itemFromIndex(index); + auto tool = toolForItem(item); + if (tool) { + // the parent of a ToolItem is always a category + return item->parent(); + } + + // item is no ToolItem, so we must have a category at hand + return item; +} + +void KateExternalToolsConfigWidget::clearTools() +{ + // collect all KateExternalTool items and delete them, since they are copies + std::vector tools = collectTools(m_toolsModel); + qDeleteAll(tools); + tools.clear(); + m_toolsModel.clear(); +} + +void KateExternalToolsConfigWidget::slotAddCategory() +{ + // find unique name + QString name = i18n("New Category"); + int i = 1; + while (!m_toolsModel.findItems(name, Qt::MatchFixedString).isEmpty()) { + name = (i18n("New Category %1", i++)); + } + + // add category and switch to edit mode + auto item = addCategory(name); + lbTools->edit(item->index()); +} + +//! Helper that ensures that tool->actionName is unique +static void makeActionNameUnique(KateExternalTool* tool, const std::vector & tools) +{ + QString name = tool->actionName; + int i = 1; + bool notUnique = true; + while (notUnique) { + auto it = std::find_if(tools.cbegin(), tools.cend(), [&name](const KateExternalTool* tool) { + return tool->actionName == name; + }); + if (it == tools.cend()) { + break; + } + name = tool->actionName + QString::number(i); + ++i; + } + tool->actionName = name; +} + +void KateExternalToolsConfigWidget::slotAddTool() +{ + auto t = new KateExternalTool(); + if (editTool(t)) { + makeActionNameUnique(t, collectTools(m_toolsModel)); + auto item = newToolItem(t->icon.isEmpty() ? blankIcon() : SmallIcon(t->icon), t); + auto category = currentCategory(); + category->appendRow(item); + lbTools->setCurrentIndex(item->index()); + + Q_EMIT changed(); + m_changed = true; + } else { + delete t; + } +} + +void KateExternalToolsConfigWidget::slotRemove() +{ + auto item = m_toolsModel.itemFromIndex(lbTools->currentIndex()); + auto tool = toolForItem(item); + + if (tool) { + item->parent()->removeRow(item->index().row()); + delete tool; + Q_EMIT changed(); + m_changed = true; + } +} + +void KateExternalToolsConfigWidget::slotEdit() +{ + auto item = m_toolsModel.itemFromIndex(lbTools->currentIndex()); + auto tool = toolForItem(item); + if (!tool) { + if (item) { + lbTools->edit(item->index()); + } + return; + } + // show the item in an editor + if (editTool(tool)) { + // renew the icon and name + item->setText(tool->name); + item->setIcon(tool->icon.isEmpty() ? blankIcon() : SmallIcon(tool->icon)); + + Q_EMIT changed(); + m_changed = true; + } +} +// END KateExternalToolsConfigWidget + + +ContextAction::ContextAction(const QIcon & icon, const QString & text, QObject * parent) + : QObject(parent) + , m_action(new QAction(icon, text, this)) +{ + connect(m_action, &QAction::triggered, [this]() { + if (m_currentLineEdit) { + Q_EMIT triggered(m_currentLineEdit); + } + }); +} + +void ContextAction::attachTo(QLineEdit * lineEdit) +{ + lineEdit->installEventFilter(this); +} + +bool ContextAction::eventFilter(QObject *watched, QEvent *event) +{ + auto lineEdit = qobject_cast(watched); + if (lineEdit) { + if (event->type() == QEvent::FocusOut) { + lineEdit->removeAction(m_action); + m_currentLineEdit = nullptr; + } + else if (event->type() == QEvent::FocusIn) { + m_currentLineEdit = lineEdit; + lineEdit->addAction(m_action, QLineEdit::TrailingPosition); + } + } + return false; +} + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/kateexternaltoolsview.h b/addons/externaltools/kateexternaltoolsview.h new file mode 100644 --- /dev/null +++ b/addons/externaltools/kateexternaltoolsview.h @@ -0,0 +1,144 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#ifndef KTEXTEDITOR_EXTERNALTOOLS_H +#define KTEXTEDITOR_EXTERNALTOOLS_H + +namespace KTextEditor { class MainWindow; } +namespace KTextEditor { class View; } + +#include +#include +#include + +class QTextDocument; + +class KActionCollection; +class KateExternalToolsPlugin; +class KateExternalTool; + +namespace Ui { class ToolView; } + +enum class ToolViewFocus { + OutputTab = 0, + StatusTab +}; + +/** + * Menu action that displays all KateExternalTool in a submenu. + * Enables/disables the tool actions whenever the view changes, depending on the mimetype. + */ +class KateExternalToolsMenuAction : public KActionMenu +{ + Q_OBJECT +public: + KateExternalToolsMenuAction(const QString& text, KActionCollection* collection, KateExternalToolsPlugin* plugin, + class KTextEditor::MainWindow* mw = nullptr); + virtual ~KateExternalToolsMenuAction(); + + /** + * This will load all the configured services. + */ + void reload(); + + KActionCollection* actionCollection() const { return m_actionCollection; } + +private Q_SLOTS: + /** + * Called whenever the current view changed. + * Required to enable/disable the tools that depend on specific mimetypes. + */ + void slotViewChanged(KTextEditor::View* view); + +private: + KateExternalToolsPlugin* m_plugin; + KTextEditor::MainWindow* m_mainwindow; // for the actions to access view/doc managers + KActionCollection* m_actionCollection; +}; + +class KateExternalToolsPluginView : public QObject, public KXMLGUIClient +{ + Q_OBJECT + +public: + /** + * Constructor. + */ + KateExternalToolsPluginView(KTextEditor::MainWindow* mainWindow, KateExternalToolsPlugin* plugin); + + /** + * Virtual destructor. + */ + ~KateExternalToolsPluginView(); + + /** + * Returns the associated mainWindow + */ + KTextEditor::MainWindow* mainWindow() const; + +public Q_SLOTS: + /** + * Called by the plugin view to reload the menu + */ + void rebuildMenu(); + + /** + * Creates the tool view. If already existing, does nothing. + */ + void createToolView(); + + /** + * Shows the tool view. The toolview will be created, if not yet existing. + */ + void showToolView(ToolViewFocus tab); + + /** + * Clears the toolview data. If no toolview is around, nothing happens. + */ + void clearToolView(); + + /** + * Shows the External Tools toolview and points the error message along with + * some more info about the tool. + */ + void addToolStatus(const QString& message); + + /** + * Sets the output data to data; + */ + void setOutputData(const QString& data); + + /** + * Deletes the tool view, if existing. + */ + void deleteToolView(); + +private: + KateExternalToolsPlugin* m_plugin; + KTextEditor::MainWindow* m_mainWindow; + KateExternalToolsMenuAction* m_externalToolsMenu = nullptr; + QWidget* m_toolView = nullptr; + Ui::ToolView* m_ui = nullptr; + QTextDocument* m_outputDoc = nullptr; + QTextDocument* m_statusDoc = nullptr; +}; + +#endif // KTEXTEDITOR_EXTERNALTOOLS_H + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/kateexternaltoolsview.cpp b/addons/externaltools/kateexternaltoolsview.cpp new file mode 100644 --- /dev/null +++ b/addons/externaltools/kateexternaltoolsview.cpp @@ -0,0 +1,271 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#include "kateexternaltoolsview.h" +#include "externaltoolsplugin.h" +#include "kateexternaltool.h" +#include "ui_toolview.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + + +// BEGIN KateExternalToolsMenuAction +KateExternalToolsMenuAction::KateExternalToolsMenuAction(const QString& text, KActionCollection* collection, + KateExternalToolsPlugin* plugin, KTextEditor::MainWindow* mw) + : KActionMenu(text, mw) + , m_plugin(plugin) + , m_mainwindow(mw) + , m_actionCollection(collection) +{ + reload(); + + // track active view to adapt enabled tool actions + connect(mw, &KTextEditor::MainWindow::viewChanged, this, &KateExternalToolsMenuAction::slotViewChanged); +} + +KateExternalToolsMenuAction::~KateExternalToolsMenuAction() = default; + +void KateExternalToolsMenuAction::reload() +{ + // clear action collection + bool needs_readd = (m_actionCollection->takeAction(this) != nullptr); + m_actionCollection->clear(); + if (needs_readd) + m_actionCollection->addAction(QStringLiteral("tools_external"), this); + menu()->clear(); + + // create tool actions + std::map categories; + std::vector uncategorizedActions; + + // first add categorized actions, such that the submenus appear at the top + for (auto tool : m_plugin->tools()) { + if (tool->hasexec) { + auto a = new QAction(tool->name, this); + a->setIcon(QIcon::fromTheme(tool->icon)); + a->setData(QVariant::fromValue(tool)); + + connect(a, &QAction::triggered, [this, a]() { + m_plugin->runTool(*a->data().value(), m_mainwindow->activeView()); + }); + + m_actionCollection->addAction(tool->actionName, a); + if (!tool->category.isEmpty()) { + auto categoryMenu = categories[tool->category]; + if (!categoryMenu) { + categoryMenu = new KActionMenu(tool->category, this); + categories[tool->category] = categoryMenu; + addAction(categoryMenu); + } + categoryMenu->addAction(a); + } else { + uncategorizedActions.push_back(a); + } + } + } + + // now add uncategorized actions below + for (auto uncategorizedAction : uncategorizedActions) { + addAction(uncategorizedAction); + } + + // load shortcuts + KSharedConfig::Ptr pConfig = KSharedConfig::openConfig(QStringLiteral("externaltools"), KConfig::NoGlobals, + QStandardPaths::ApplicationsLocation); + KConfigGroup config(pConfig, "Global"); + config = KConfigGroup(pConfig, "Shortcuts"); + m_actionCollection->readSettings(&config); + slotViewChanged(m_mainwindow->activeView()); +} + +void KateExternalToolsMenuAction::slotViewChanged(KTextEditor::View* view) +{ + // no active view, oh oh + if (!view) { + return; + } + + // try to enable/disable to match current mime type + const QString mimeType = view->document()->mimeType(); + foreach (QAction* action, m_actionCollection->actions()) { + if (action && action->data().value()) { + auto tool = action->data().value(); + action->setEnabled(tool->matchesMimetype(mimeType)); + } + } +} +// END KateExternalToolsMenuAction + + + + + +// BEGIN KateExternalToolsPluginView +KateExternalToolsPluginView::KateExternalToolsPluginView(KTextEditor::MainWindow* mainWindow, + KateExternalToolsPlugin* plugin) + : QObject(mainWindow) + , m_plugin(plugin) + , m_mainWindow(mainWindow) + , m_outputDoc(new QTextDocument(this)) + , m_statusDoc(new QTextDocument(this)) +{ + m_plugin->registerPluginView(this); + + KXMLGUIClient::setComponentName(QLatin1String("externaltools"), i18n("External Tools")); + setXMLFile(QLatin1String("ui.rc")); + + if (KAuthorized::authorizeAction(QStringLiteral("shell_access"))) { + m_externalToolsMenu = new KateExternalToolsMenuAction(i18n("External Tools"), actionCollection(), plugin, mainWindow); + actionCollection()->addAction(QStringLiteral("tools_external"), m_externalToolsMenu); + m_externalToolsMenu->setWhatsThis(i18n("Launch external helper applications")); + } + + mainWindow->guiFactory()->addClient(this); + + // ESC should close & hide ToolView + connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, [this](QEvent* event) { + auto keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape && keyEvent->modifiers() == Qt::NoModifier) { + deleteToolView(); + } + }); +} + +KateExternalToolsPluginView::~KateExternalToolsPluginView() +{ + m_plugin->unregisterPluginView(this); + + m_mainWindow->guiFactory()->removeClient(this); + + deleteToolView(); + + delete m_externalToolsMenu; + m_externalToolsMenu = nullptr; +} + +void KateExternalToolsPluginView::rebuildMenu() +{ + if (m_externalToolsMenu) { + KXMLGUIFactory* f = factory(); + f->removeClient(this); + reloadXML(); + m_externalToolsMenu->reload(); + f->addClient(this); + } +} + +KTextEditor::MainWindow* KateExternalToolsPluginView::mainWindow() const +{ + return m_mainWindow; +} + +void KateExternalToolsPluginView::createToolView() +{ + if (!m_toolView) { + m_toolView = mainWindow()->createToolView(m_plugin, QStringLiteral("ktexteditor_plugin_externaltools"), + KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("system-run")), i18n("External Tools")); + + m_ui = new Ui::ToolView(); + m_ui->setupUi(m_toolView); + + // set the documents + m_ui->teOutput->setDocument(m_outputDoc); + m_ui->teStatus->setDocument(m_statusDoc); + + // use fixed font for displaying status and output text + const auto fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); + m_ui->teOutput->setFont(fixedFont); + m_ui->teStatus->setFont(fixedFont); + + // close button to delete tool view + auto btnClose = new QToolButton(); + btnClose->setAutoRaise(true); + btnClose->setIcon(QIcon::fromTheme(QStringLiteral("tab-close"))); + connect(btnClose, &QToolButton::clicked, this, &KateExternalToolsPluginView::deleteToolView); + m_ui->tabWidget->setCornerWidget(btnClose); + } +} + +void KateExternalToolsPluginView::showToolView(ToolViewFocus tab) +{ + createToolView(); + + if (tab == ToolViewFocus::OutputTab) { + m_ui->tabWidget->setCurrentWidget(m_ui->tabOutput); + } else { + m_ui->tabWidget->setCurrentWidget(m_ui->tabStatus); + } + + mainWindow()->showToolView(m_toolView); +} + +void KateExternalToolsPluginView::clearToolView() +{ + m_outputDoc->clear(); + m_statusDoc->clear(); +} + +void KateExternalToolsPluginView::addToolStatus(const QString& message) +{ + QTextCursor cursor(m_statusDoc); + cursor.movePosition(QTextCursor::End); + cursor.insertText(message); + cursor.insertText(QStringLiteral("\n")); +} + +void KateExternalToolsPluginView::setOutputData(const QString& data) +{ + QTextCursor cursor(m_outputDoc); + cursor.movePosition(QTextCursor::End); + cursor.insertText(data); +} + +void KateExternalToolsPluginView::deleteToolView() +{ + if (m_toolView) { + delete m_ui; + m_ui = nullptr; + + delete m_toolView; + m_toolView = nullptr; + } +} +// END KateExternalToolsPluginView + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/katetoolrunner.h b/addons/externaltools/katetoolrunner.h new file mode 100644 --- /dev/null +++ b/addons/externaltools/katetoolrunner.h @@ -0,0 +1,114 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KTEXTEDITOR_EXTERNALTOOLRUNNER_H +#define KTEXTEDITOR_EXTERNALTOOLRUNNER_H + +#include +#include +#include +#include +#include + +#include + +class KateExternalTool; +class QProcess; +namespace KTextEditor { + class View; +} + +/** + * Helper class to run a KateExternalTool. + */ +class KateToolRunner : public QObject +{ + Q_OBJECT + +public: + /** + * Constructor that will run @p tool in the run() method. + * The @p view can later be retrieved again with view() to process the data when the tool is finished. + */ + KateToolRunner(std::unique_ptr tool, KTextEditor::View * view, QObject* parent = nullptr); + + KateToolRunner(const KateToolRunner&) = delete; + void operator=(const KateToolRunner&) = delete; + + ~KateToolRunner(); + + /** + * Returns the view that was active when running the tool. + * @warning May be a nullptr, since the view could have been closed in the meantime. + */ + KTextEditor::View* view() const; + + /** + * Returns the tool that was passed in the constructor. + */ + KateExternalTool* tool() const; + + /** + * Starts a child process that executes the tool. + */ + void run(); + + /** + * Blocking call that waits until the tool is finised. + * Used internally for unit testing. + */ + void waitForFinished(); + + /** + * Returns the data that was collected on stdout. + */ + QString outputData() const; + + /** + * Returns the data that was collected on stderr. + */ + QString errorData() const; + +Q_SIGNALS: + /** + * This signal is emitted when the tool is finished. + */ + void toolFinished(KateToolRunner* runner, int exitCode, bool crashed); + +private: + //! Use QPointer here, since the View may be closed in the meantime. + QPointer m_view; + + //! We are the owner of the tool (it was copied) + std::unique_ptr m_tool; + + //! Child process that runs the tool + std::unique_ptr m_process; + + //! Collect stdout + QByteArray m_stdout; + + //! Collect stderr + QByteArray m_stderr; +}; + +#endif // KTEXTEDITOR_EXTERNALTOOLRUNNER_H + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/katetoolrunner.cpp b/addons/externaltools/katetoolrunner.cpp new file mode 100644 --- /dev/null +++ b/addons/externaltools/katetoolrunner.cpp @@ -0,0 +1,103 @@ +/* This file is part of the KDE project + * + * Copyright 2019 Dominik Haumann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "katetoolrunner.h" + +#include "kateexternaltool.h" + +#include +#include +#include + +KateToolRunner::KateToolRunner(std::unique_ptr tool, KTextEditor::View * view, QObject* parent) + : QObject(parent) + , m_view(view) + , m_tool(std::move(tool)) + , m_process(new QProcess()) +{ + m_process->setProcessChannelMode(QProcess::SeparateChannels); +} + +KateToolRunner::~KateToolRunner() +{ +} + +KTextEditor::View* KateToolRunner::view() const +{ + return m_view; +} + +KateExternalTool* KateToolRunner::tool() const +{ + return m_tool.get(); +} + +void KateToolRunner::run() +{ + if (!m_tool->workingDir.isEmpty()) { + m_process->setWorkingDirectory(m_tool->workingDir); + } else if (m_view) { + // if nothing is set, use the current document's directory + const auto url = m_view->document()->url(); + if (url.isValid()) { + const QString path = m_view->document()->url().toString(QUrl::RemoveScheme | QUrl::RemoveFilename); + m_process->setWorkingDirectory(path); + } + } + + QObject::connect(m_process.get(), &QProcess::readyReadStandardOutput, [this]() { + m_stdout += m_process->readAllStandardOutput(); + }); + QObject::connect(m_process.get(), &QProcess::readyReadStandardError, [this]() { + m_stderr += m_process->readAllStandardError(); + }); + QObject::connect(m_process.get(), static_cast(&QProcess::finished), + [this](int exitCode, QProcess::ExitStatus exitStatus) { + Q_EMIT toolFinished(this, exitCode, exitStatus == QProcess::CrashExit); + }); + + // Write stdin to process, if applicable, then close write channel + QObject::connect(m_process.get(), &QProcess::started, [this]() { + if (!m_tool->input.isEmpty()) { + m_process->write(m_tool->input.toLocal8Bit()); + } + m_process->closeWriteChannel(); + }); + + const QStringList args = KShell::splitArgs(m_tool->arguments); + m_process->start(m_tool->executable, args); +} + +void KateToolRunner::waitForFinished() +{ + m_process->waitForFinished(); +} + +QString KateToolRunner::outputData() const +{ + return QString::fromLocal8Bit(m_stdout); +} + +QString KateToolRunner::errorData() const +{ + return QString::fromLocal8Bit(m_stderr); +} + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/plugin.qrc b/addons/externaltools/plugin.qrc new file mode 100644 --- /dev/null +++ b/addons/externaltools/plugin.qrc @@ -0,0 +1,6 @@ + + + + ui.rc + + diff --git a/addons/externaltools/tooldialog.ui b/addons/externaltools/tooldialog.ui new file mode 100644 --- /dev/null +++ b/addons/externaltools/tooldialog.ui @@ -0,0 +1,319 @@ + + + ToolDialog + + + + 0 + 0 + 470 + 534 + + + + Edit Tool + + + + + + Na&me: + + + edtName + + + + + + + + + The name will be displayed in the 'Tools->External Tools' menu. + + + Short name of the tool + + + + + + + + + + + + E&xecutable: + + + edtExecutable + + + + + + + + + Application or interpreter + + + + + + + + .. + + + QToolButton::InstantPopup + + + + + + + + + Ar&guments: + + + edtArgs + + + + + + + Command line arguments + + + + + + + &Input: + + + edtInput + + + + + + + Optional standard input + + + + + + + Working &directory: + + + edtWorkingDir + + + + + + + + + Uses current document path if empty + + + + + + + + .. + + + QToolButton::InstantPopup + + + + + + + + + Mime &types: + + + edtMimeType + + + + + + + + + A semicolon-separated list of mime types for which this tool should be available. If empty, the tool is always available. + + + Show tool only for given mime types + + + + + + + Opens a dialog that helps in creating a list of mimetypes. + + + + .. + + + + + + + + + Save: + + + cmbSave + + + + + + + Optionally save the current or all modified documents prior to running the command. This is helpful if you want to pass URLs to an application like, for example, an FTP client. + + + + None + + + + + Current Document + + + + + All Documents + + + + + + + + Reload current document after execution + + + + + + + O&utput: + + + cmbOutput + + + + + + + + Ignore + + + + + Insert at Cursor Position + + + + + Replace Selected Text + + + + + Replace Current Document + + + + + Append to Current Document + + + + + Insert in New Document + + + + + Display in Pane + + + + + + + + Editor command: + + + edtCommand + + + + + + + If you specify a name here, you can invoke the command from the view command line with exttool-the_name_you_specified_here. Please do not use spaces or tabs in the name. + + + Optional command bar name + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + KIconButton + QPushButton +
kiconbutton.h
+
+
+ + edtName + btnIcon + edtExecutable + btnExecutable + edtArgs + edtInput + edtWorkingDir + btnWorkingDir + edtMimeType + btnMimeType + cmbSave + chkReload + cmbOutput + edtCommand + + + +
diff --git a/addons/externaltools/toolview.ui b/addons/externaltools/toolview.ui new file mode 100644 --- /dev/null +++ b/addons/externaltools/toolview.ui @@ -0,0 +1,63 @@ + + + ToolView + + + + 0 + 0 + 678 + 295 + + + + + + 6 + 6 + 451 + 240 + + + + 0 + + + + Output + + + + + + true + + + Displays output from the external tool + + + + + + + + Status + + + + + + true + + + No errors detected + + + + + + + + + + diff --git a/addons/externaltools/ui.rc b/addons/externaltools/ui.rc new file mode 100644 --- /dev/null +++ b/addons/externaltools/ui.rc @@ -0,0 +1,11 @@ + + + + &File + + + &Tools + + + +