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 + katemacroexpander.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 +) +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,12 @@ +include(ECMMarkAsTest) + +# Project Plugin +add_executable(externaltools_test + externaltooltest.cpp + ../kateexternaltool.cpp + ../katetoolrunner.cpp + ../katemacroexpander.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() +{ + auto 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; + + // 1. /tmp $ ls /usr + KateToolRunner runner1(tool, nullptr); + runner1.run(); + runner1.waitForFinished(); + QVERIFY(runner1.outputData().contains(QStringLiteral("bin"))); + + // 2. /usr $ ls + auto tool2 = new KateExternalTool(*tool); + tool2->arguments.clear(); + tool2->workingDir = QStringLiteral("/usr"); + KateToolRunner runner2(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() +{ + auto 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(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,23 @@ +[Global] +tools=2 +version=1 + +[Tool 0] +actionName=externaltool_GoogleSelection +cmdname=google-selection +command=[ -n "%selection" ] && kfmclient openURL "gg:%selection" +executable=kfmclient +icon=globe +mimetypes= +name=Google Selection +save=0 + +[Tool 1] +actionName=externaltool_RunScript +cmdname=runscript +command=konsole -e sh -c "cd %directory && pwd && chmod -vc a+x %filename && ./%filename ; echo Press any key to continue. && read -n 1" +executable=konsole +icon=run +mimetypes= +name=Run Script +save=1 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,106 @@ +/* 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(); + +private: + QVector m_tools; + QStringList m_commands; + KateExternalToolsCommand* m_command = nullptr; + +private Q_SLOT: + /** + * Called whenever an external tool is done. + */ + void handleToolFinished(KateToolRunner* runner); +}; + +#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,230 @@ +/* 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 "katemacroexpander.h" +#include "katetoolrunner.h" +#include "kateexternaltoolsconfigwidget.h" + +#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(); +} + +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) { + 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 + auto copy = new KateExternalTool(tool); + + MacroExpander macroExpander(view); + + if (!macroExpander.expandMacrosShellQuote(copy->executable)) { + KMessageBox::sorry(view, i18n("Failed to expand the executable '%1'.", copy->executable), + i18n("Kate External Tools")); + return; + } + + if (!macroExpander.expandMacrosShellQuote(copy->arguments)) { + KMessageBox::sorry(view, i18n("Failed to expand the arguments '%1'.", copy->arguments), + i18n("Kate External Tools")); + return; + } + + if (!macroExpander.expandMacrosShellQuote(copy->workingDir)) { + KMessageBox::sorry(view, i18n("Failed to expand the working directory '%1'.", copy->workingDir), + i18n("Kate External Tools")); + return; + } + + if (!macroExpander.expandMacrosShellQuote(copy->input)) { + KMessageBox::sorry(view, i18n("Failed to expand the input '%1'.", copy->input), + i18n("Kate External Tools")); + 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(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) +{ + auto view = runner->view(); + if (view && !runner->outputData().isEmpty()) { + switch (runner->tool()->outputMode) { + case KateExternalTool::OutputMode::InsertAtCursor: { + view->removeSelection(); + view->insertText(runner->outputData()); + break; + } + case KateExternalTool::OutputMode::ReplaceSelectedText: { + view->removeSelectionText(); + view->insertText(runner->outputData()); + break; + } + case KateExternalTool::OutputMode::ReplaceCurrentDocument: { + 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 doc = KTextEditor::Editor::instance()->createDocument(nullptr); +// view->mainWindow()->activateView(doc); + break; + } + default: + break; + } + } + + // TODO: case KateExternalTool::OutputMode::DisplayInPane: break; + // create a toolview with the contents. QTextEdit with fixed font? Something else? + + 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; +} + +#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,128 @@ +/* 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; + /// Include stderr output when running the tool. + bool includeStderr = false; + +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,91 @@ +/* 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 + +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 = static_cast(cg.readEntry("save", 0)); + reload = cg.readEntry("reload", false); + outputMode = static_cast(cg.readEntry("output", 0)); + includeStderr = cg.readEntry("includeStderr", false); + + 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", static_cast(saveMode)); + cg.writeEntry("reload", reload); + cg.writeEntry("output", static_cast(outputMode)); + cg.writeEntry("includeStderr", includeStderr); +} + +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 + && lhs.includeStderr == rhs.includeStderr; +} + +// 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,52 @@ +/* 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: + // const QStringList& cmds() override; // FIXME + 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,49 @@ +/* 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" + +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&, QString&) +{ + 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,129 @@ +/* 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; +}; + +#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,455 @@ +/* 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 "katemacroexpander.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; +} + +// BEGIN ToolItem +/** + * This is a QStandardItem, that has a KateExternalTool. + * The text is the Name of the tool. + */ +class ToolItem : public QStandardItem +{ +public: + ToolItem(const QPixmap& icon, KateExternalTool* tool) + : QStandardItem(icon, tool->name) + { + setFlags(Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled); + setData(QVariant::fromValue(reinterpret_cast(tool)), ToolRole ); + } + + KateExternalTool * tool() { + return reinterpret_cast(data(ToolRole).value()); + } +}; +// END ToolItem + +// 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); + + 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->chkIncludeStderr->setChecked(m_tool->includeStderr); + 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 toolItem = static_cast(child); + tools.push_back(toolItem->tool()); + } + } + return tools; +} + +// BEGIN KateExternalToolsConfigWidget +KateExternalToolsConfigWidget::KateExternalToolsConfigWidget(QWidget* parent, KateExternalToolsPlugin* plugin) + : KTextEditor::ConfigPage(parent) + , m_plugin(plugin) +{ + setupUi(this); + 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 = new ToolItem(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 toolItem = static_cast(child); + auto tool = toolItem->tool(); + // 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 = dynamic_cast(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->includeStderr = editor.ui->chkIncludeStderr->isChecked(); + 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 toolItem = dynamic_cast(item); + if (toolItem) { + // the parent of a ToolItem is always a category + return toolItem->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 = new ToolItem(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 toolItem = dynamic_cast(item); + + if (toolItem) { + auto tool = toolItem->tool(); + toolItem->parent()->removeRow(toolItem->index().row()); + delete tool; + Q_EMIT changed(); + m_changed = true; + } +} + +void KateExternalToolsConfigWidget::slotEdit() +{ + auto item = m_toolsModel.itemFromIndex(lbTools->currentIndex()); + auto toolItem = dynamic_cast(item); + if (!toolItem) { + if (item) { + lbTools->edit(item->index()); + } + return; + } + // show the item in an editor + KateExternalTool* t = toolItem->tool(); + if (editTool(t)) { + // renew the icon and name + toolItem->setText(t->name); + toolItem->setIcon(t->icon.isEmpty() ? blankIcon() : SmallIcon(t->icon)); + + Q_EMIT changed(); + m_changed = true; + } +} +// END KateExternalToolsConfigWidget + +// 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,113 @@ +/* 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 KActionCollection; +class KateExternalToolsPlugin; +class KateExternalTool; + +/** + * The external tools action + * This action creates a menu, in which each item will launch a process + * with the provided arguments, which may include the following macros: + * - %URLS: the URLs of all open documents. + * - %URL: The URL of the active document. + * - %filedir: The directory of the current document, if that is a local file. + * - %selection: The selection of the active document. + * - %text: The text of the active document. + * - %line: The line number of the cursor in the active view. + * - %column: The column of the cursor in the active view. + * + * Each item has the following properties: + * - Name: The friendly text for the menu + * - Exec: The command to execute, including arguments. + * - TryExec: the name of the executable, if not available, the + * item will not be displayed. + * - MimeTypes: An optional list of mimetypes. The item will be disabled or + * hidden if the current file is not of one of the indicated types. + * + */ +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() { return m_actionCollection; } + +private Q_SLOTS: + 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(); + +private: + KateExternalToolsPlugin* m_plugin; + KTextEditor::MainWindow* m_mainWindow; + KateExternalToolsMenuAction* m_externalToolsMenu = 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,180 @@ +/* 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 "kateexternaltoolsview.h" +#include "externaltoolsplugin.h" +#include "kateexternaltool.h" + +#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) +{ + 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); +} + +void KateExternalToolsPluginView::rebuildMenu() +{ + if (m_externalToolsMenu) { + KXMLGUIFactory* f = factory(); + f->removeClient(this); + reloadXML(); + m_externalToolsMenu->reload(); + f->addClient(this); + } +} + +KateExternalToolsPluginView::~KateExternalToolsPluginView() +{ + m_mainWindow->guiFactory()->removeClient(this); + + delete m_externalToolsMenu; + m_externalToolsMenu = nullptr; +} + +KTextEditor::MainWindow* KateExternalToolsPluginView::mainWindow() const +{ + return m_mainWindow; +} +// END KateExternalToolsPluginView + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/katemacroexpander.h b/addons/externaltools/katemacroexpander.h new file mode 100644 --- /dev/null +++ b/addons/externaltools/katemacroexpander.h @@ -0,0 +1,47 @@ +/* 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_MACRO_EXPANDER_H +#define KTEXTEDITOR_MACRO_EXPANDER_H + +#include + +namespace KTextEditor +{ +class View; +} + +/** + * Helper class for macro expansion. + */ +class MacroExpander : public KWordMacroExpander +{ +public: + MacroExpander(KTextEditor::View* view); + +protected: + bool expandMacro(const QString& str, QStringList& ret) override; + +private: + KTextEditor::View* m_view; +}; + +#endif // KTEXTEDITOR_MACRO_EXPANDER_H + +// kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/externaltools/katemacroexpander.cpp b/addons/externaltools/katemacroexpander.cpp new file mode 100644 --- /dev/null +++ b/addons/externaltools/katemacroexpander.cpp @@ -0,0 +1,66 @@ +/* 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 "katemacroexpander.h" + +#include +#include +#include +#include + +MacroExpander::MacroExpander(KTextEditor::View* view) + : KWordMacroExpander() + , m_view(view) +{ +} + +bool MacroExpander::expandMacro(const QString& str, QStringList& ret) +{ + KTextEditor::View* view = m_view; + if (!view) + return false; + + KTextEditor::Document* doc = view->document(); + QUrl url = doc->url(); + + if (str == QStringLiteral("URL")) + ret += url.url(); + else if (str == QStringLiteral("directory")) // directory of current doc + ret += url.toString(QUrl::RemoveScheme | QUrl::RemoveFilename); + else if (str == QStringLiteral("filename")) + ret += url.fileName(); + else if (str == QStringLiteral("line")) // cursor line of current doc + ret += QString::number(view->cursorPosition().line()); + else if (str == QStringLiteral("col")) // cursor col of current doc + ret += QString::number(view->cursorPosition().column()); + else if (str == QStringLiteral("selection")) // selection of current doc if any + ret += view->selectionText(); + else if (str == QStringLiteral("text")) // text of current doc + ret += doc->text(); + else if (str == QStringLiteral("URLs")) { + foreach (KTextEditor::Document* it, KTextEditor::Editor::instance()->application()->documents()) + if (!it->url().isEmpty()) + ret += it->url().url(); + } else + return false; + + return true; +} + +// 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,118 @@ +/* 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 + +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(KateExternalTool* 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. + * stderr is also included if includeStderr was set. + */ + QString outputData() const; + +Q_SIGNALS: + /** + * This signal is emitted when the tool is finished. + */ + void toolFinished(KateToolRunner* runner); + +private Q_SLOTS: + /** + * More tool output is available + */ + void slotReadyRead(); + + /** + * Analysis finished + * @param exitCode analyzer process exit code + * @param exitStatus analyzer process exit status + */ + void handleToolFinished(int exitCode, QProcess::ExitStatus exitStatus); + +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) + KateExternalTool* m_tool; + + //! Child process that runs the tool + QProcess* m_process = nullptr; + + //! Collect stdout, and optionally also stderr + QByteArray m_output; +}; + +#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,113 @@ +/* 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 + +KateToolRunner::KateToolRunner(KateExternalTool* tool, KTextEditor::View * view, QObject* parent) + : QObject(parent) + , m_view(view) + , m_tool(tool) + , m_process(new QProcess()) +{ +} + +KateToolRunner::~KateToolRunner() +{ + delete m_tool; + m_tool = nullptr; + + delete m_process; + m_process = nullptr; +} + +KTextEditor::View* KateToolRunner::view() const +{ + return m_view; +} + +KateExternalTool* KateToolRunner::tool() const +{ + return m_tool; +} + +void KateToolRunner::run() +{ + if (m_tool->includeStderr) { + m_process->setProcessChannelMode(QProcess::MergedChannels); + } + + if (!m_tool->workingDir.isEmpty()) { + m_process->setWorkingDirectory(m_tool->workingDir); + } + + QObject::connect(m_process, &QProcess::readyRead, this, &KateToolRunner::slotReadyRead); + QObject::connect(m_process, static_cast(&QProcess::finished), this, + &KateToolRunner::handleToolFinished); + + // Write stdin to process, if applicable, then close write channel + QObject::connect(m_process, &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_output); +} + +void KateToolRunner::slotReadyRead() +{ + m_output += m_process->readAll(); +} + +void KateToolRunner::handleToolFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitCode != 0) { + // FIXME: somehow tell user + return; + } + + if (exitStatus != QProcess::NormalExit) { + // FIXME: somehow tell user + return; + } + + // FIXME: process m_output depending on the tool's outputMode + + Q_EMIT toolFinished(this); +} + +// 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,365 @@ + + + ToolDialog + + + + 0 + 0 + 470 + 510 + + + + Edit Tool + + + + + + Na&me: + + + edtName + + + + + + + + + The name will be displayed in the 'Tools->External Tools' menu. + + + Short name of the tool + + + + + + + + + + + + E&xecutable: + + + edtExecutable + + + + + + + + + The executable used by the command. This is used to check if a tool should be displayed; if not set, the first word of <em>command</em> will be used. + + + Application or interpreter + + + + + + + + ../../../../../../../../.designer/backup../../../../../../../../.designer/backup + + + QToolButton::InstantPopup + + + + + + + + + Ar&guments: + + + edtArgs + + + + + + + &Input: + + + edtInput + + + + + + + + + <p>The script to execute to invoke the tool. The script is passed to /bin/sh for execution. The following macros will be expanded:</p><ul><li><code>%URL</code> - the URL of the current document.</li><li><code>%URLs</code> - a list of the URLs of all open documents.</li><li><code>%directory</code> - the URL of the directory containing the current document.</li><li><code>%filename</code> - the filename of the current document.</li><li><code>%line</code> - the current line of the text cursor in the current view.</li><li><code>%column</code> - the column of the text cursor in the current view.</li><li><code>%selection</code> - the selected text in the current view.</li><li><code>%text</code> - the text of the current document.</li></ul> + + + Optional standard input + + + + + + + + ../../../../../../../../.designer/backup../../../../../../../../.designer/backup + + + QToolButton::InstantPopup + + + + + + + + + Mime &types: + + + edtMimeType + + + + + + + + + A semicolon-separated list of mime types for which this tool should be available; if this is left empty, the tool is always available. To choose from known mimetypes, press the button on the right. + + + Show tool only for given mime types + + + + + + + Click for a dialog that can help you create a list of mimetypes. + + + + ../../../../../../../../.designer/backup../../../../../../../../.designer/backup + + + + + + + + + Save: + + + cmbSave + + + + + + + You can choose to 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 + + + + + + + Working &directory: + + + edtWorkingDir + + + + + + + + + Uses current document path if empty + + + + + + + + ../../../../../../../../.designer/backup../../../../../../../../.designer/backup + + + QToolButton::InstantPopup + + + + + + + + + + + Command line arguments + + + + + + + + ../../../../../../../../.designer/backup../../../../../../../../.designer/backup + + + QToolButton::InstantPopup + + + + + + + + + Include output from stderr + + + + + + + + KIconButton + QPushButton +
kiconbutton.h
+
+
+ + edtName + btnIcon + edtExecutable + btnExecutable + edtArgs + btnArgs + edtInput + btnInput + edtWorkingDir + btnWorkingDir + edtMimeType + btnMimeType + cmbSave + chkReload + cmbOutput + chkIncludeStderr + edtCommand + + + +
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 + + + +