File indexing completed on 2024-05-05 05:51:23

0001 /* This file is part of the KDE project
0002  *
0003  *  SPDX-FileCopyrightText: 2019 Dominik Haumann <dhaumann@kde.org>
0004  *
0005  *  SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 #include "kateexternaltoolsview.h"
0008 #include "externaltoolsplugin.h"
0009 #include "kateexternaltool.h"
0010 #include "ktexteditor_utils.h"
0011 #include "ui_toolview.h"
0012 
0013 #include <KTextEditor/Application>
0014 #include <KTextEditor/Document>
0015 #include <KTextEditor/Editor>
0016 #include <KTextEditor/MainWindow>
0017 #include <KTextEditor/View>
0018 
0019 #include <KActionCollection>
0020 #include <KAuthorized>
0021 #include <KConfig>
0022 #include <KConfigGroup>
0023 #include <KLocalizedString>
0024 #include <KSharedConfig>
0025 #include <KXMLGUIFactory>
0026 #include <QMenu>
0027 #include <QStandardPaths>
0028 #include <ktexteditor_version.h>
0029 
0030 #include <QFontDatabase>
0031 #include <QKeyEvent>
0032 #include <QTextDocument>
0033 #include <QToolButton>
0034 
0035 #include <map>
0036 #include <vector>
0037 
0038 // BEGIN KateExternalToolsMenuAction
0039 KateExternalToolsMenuAction::KateExternalToolsMenuAction(const QString &text,
0040                                                          KActionCollection *collection,
0041                                                          KateExternalToolsPlugin *plugin,
0042                                                          KTextEditor::MainWindow *mw)
0043     : KActionMenu(text, mw)
0044     , m_plugin(plugin)
0045     , m_mainwindow(mw)
0046     , m_actionCollection(collection)
0047 {
0048     reload();
0049 
0050     // track active view to adapt enabled tool actions
0051     connect(mw, &KTextEditor::MainWindow::viewChanged, this, &KateExternalToolsMenuAction::slotViewChanged);
0052 }
0053 
0054 KateExternalToolsMenuAction::~KateExternalToolsMenuAction() = default;
0055 
0056 void KateExternalToolsMenuAction::reload()
0057 {
0058     // clear action collection
0059     bool needs_readd = (m_actionCollection->takeAction(this) != nullptr);
0060     m_actionCollection->clear();
0061     if (needs_readd) {
0062         m_actionCollection->addAction(QStringLiteral("tools_external"), this);
0063     }
0064     menu()->clear();
0065 
0066     // create tool actions
0067     std::map<QString, KActionMenu *> categories;
0068     std::vector<QAction *> uncategorizedActions;
0069 
0070     // first add categorized actions, such that the submenus appear at the top
0071     for (auto tool : m_plugin->tools()) {
0072         // !tool->hasexec => tool exe has an expandable variable and thus cannot be checked reliably, consider it true
0073         if (tool->canExecute()) {
0074             auto a = new QAction(tool->translatedName().replace(QLatin1Char('&'), QLatin1String("&&")), this);
0075             a->setIcon(QIcon::fromTheme(tool->icon));
0076             a->setData(QVariant::fromValue(tool));
0077 
0078             connect(a, &QAction::triggered, a, [this, a]() {
0079                 m_plugin->runTool(*a->data().value<KateExternalTool *>(), m_mainwindow->activeView());
0080             });
0081 
0082             m_actionCollection->addAction(tool->actionName, a);
0083             if (!tool->category.isEmpty()) {
0084                 auto categoryMenu = categories[tool->category];
0085                 if (!categoryMenu) {
0086                     categoryMenu = new KActionMenu(tool->translatedCategory(), this);
0087                     categories[tool->category] = categoryMenu;
0088                     addAction(categoryMenu);
0089                 }
0090                 categoryMenu->addAction(a);
0091             } else {
0092                 uncategorizedActions.push_back(a);
0093             }
0094         }
0095     }
0096 
0097     // now add uncategorized actions below
0098     for (auto uncategorizedAction : uncategorizedActions) {
0099         addAction(uncategorizedAction);
0100     }
0101 
0102     addSeparator();
0103     auto cfgAction = new QAction(i18n("Configure..."), this);
0104     addAction(cfgAction);
0105     connect(cfgAction, &QAction::triggered, this, &KateExternalToolsMenuAction::showConfigPage, Qt::QueuedConnection);
0106 
0107     // load shortcuts
0108     KSharedConfigPtr pConfig = m_plugin->config();
0109     KConfigGroup group(pConfig, QStringLiteral("Global"));
0110     group = KConfigGroup(pConfig, QStringLiteral("Shortcuts"));
0111     m_actionCollection->readSettings(&group);
0112     slotViewChanged(m_mainwindow->activeView());
0113 }
0114 
0115 void KateExternalToolsMenuAction::slotViewChanged(KTextEditor::View *view)
0116 {
0117     // no active view, oh oh
0118     disconnect(m_docUrlChangedConnection);
0119     if (!view) {
0120         updateActionState(nullptr);
0121         return;
0122     }
0123 
0124     m_docUrlChangedConnection = connect(view->document(), &KTextEditor::Document::documentUrlChanged, this, [this](KTextEditor::Document *doc) {
0125         updateActionState(doc);
0126     });
0127 
0128     updateActionState(view->document());
0129 }
0130 
0131 void KateExternalToolsMenuAction::updateActionState(KTextEditor::Document *activeDoc)
0132 {
0133     // try to enable/disable to match current mime type or if we have no doc
0134     const QString mimeType = activeDoc ? activeDoc->mimeType() : QString();
0135     const auto actions = m_actionCollection->actions();
0136     for (QAction *action : actions) {
0137         if (action && action->data().value<KateExternalTool *>()) {
0138             auto tool = action->data().value<KateExternalTool *>();
0139             action->setEnabled(activeDoc && (tool->matchesMimetype(mimeType) || tool->mimetypes.isEmpty()));
0140         }
0141     }
0142 }
0143 
0144 void KateExternalToolsMenuAction::showConfigPage()
0145 {
0146     m_mainwindow->showPluginConfigPage(m_plugin, 0);
0147 }
0148 // END KateExternalToolsMenuAction
0149 
0150 // BEGIN KateExternalToolsPluginView
0151 KateExternalToolsPluginView::KateExternalToolsPluginView(KTextEditor::MainWindow *mainWindow, KateExternalToolsPlugin *plugin)
0152     : QObject(mainWindow)
0153     , m_plugin(plugin)
0154     , m_mainWindow(mainWindow)
0155     , m_outputDoc(new QTextDocument(this))
0156 {
0157     m_plugin->registerPluginView(this);
0158 
0159     KXMLGUIClient::setComponentName(QLatin1String("externaltools"), i18n("External Tools"));
0160     setXMLFile(QLatin1String("ui.rc"));
0161 
0162     if (KAuthorized::authorizeAction(QStringLiteral("shell_access"))) {
0163         m_externalToolsMenu = new KateExternalToolsMenuAction(i18n("External Tools"), actionCollection(), plugin, mainWindow);
0164         actionCollection()->addAction(QStringLiteral("tools_external"), m_externalToolsMenu);
0165         m_externalToolsMenu->setWhatsThis(i18n("Launch external helper applications"));
0166     }
0167 
0168     mainWindow->guiFactory()->addClient(this);
0169 
0170     // ESC should close & hide ToolView
0171     connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KateExternalToolsPluginView::handleEsc);
0172     connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KateExternalToolsPluginView::slotViewChanged);
0173     slotViewChanged(m_mainWindow->activeView());
0174 }
0175 
0176 KateExternalToolsPluginView::~KateExternalToolsPluginView()
0177 {
0178     m_plugin->unregisterPluginView(this);
0179 
0180     m_mainWindow->guiFactory()->removeClient(this);
0181 
0182     deleteToolView();
0183 
0184     delete m_externalToolsMenu;
0185     m_externalToolsMenu = nullptr;
0186 }
0187 
0188 void KateExternalToolsPluginView::rebuildMenu()
0189 {
0190     if (m_externalToolsMenu) {
0191         KXMLGUIFactory *f = factory();
0192         f->removeClient(this);
0193         reloadXML();
0194         m_externalToolsMenu->reload();
0195         f->addClient(this);
0196     }
0197 }
0198 
0199 KTextEditor::MainWindow *KateExternalToolsPluginView::mainWindow() const
0200 {
0201     return m_mainWindow;
0202 }
0203 
0204 void KateExternalToolsPluginView::createToolView()
0205 {
0206     if (!m_toolView) {
0207         m_toolView = mainWindow()->createToolView(m_plugin,
0208                                                   QStringLiteral("ktexteditor_plugin_externaltools"),
0209                                                   KTextEditor::MainWindow::Bottom,
0210                                                   QIcon::fromTheme(QStringLiteral("system-run")),
0211                                                   i18n("External Tools"));
0212 
0213         m_ui = new Ui::ToolView();
0214         m_ui->setupUi(m_toolView);
0215 
0216         // set the documents
0217         m_ui->teOutput->setDocument(m_outputDoc);
0218 
0219         // use fixed font for displaying status and output text
0220         const auto fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
0221         m_ui->teOutput->setFont(fixedFont);
0222 
0223         // close button to delete tool view
0224         auto btnClose = new QToolButton();
0225         btnClose->setAutoRaise(true);
0226         btnClose->setIcon(QIcon::fromTheme(QStringLiteral("tab-close")));
0227         connect(btnClose, &QToolButton::clicked, this, &KateExternalToolsPluginView::deleteToolView);
0228         m_ui->tabWidget->setCornerWidget(btnClose);
0229     }
0230 }
0231 
0232 void KateExternalToolsPluginView::showToolView()
0233 {
0234     createToolView();
0235     m_ui->tabWidget->setCurrentWidget(m_ui->tabOutput);
0236     mainWindow()->showToolView(m_toolView);
0237 }
0238 
0239 void KateExternalToolsPluginView::clearToolView()
0240 {
0241     m_outputDoc->clear();
0242 }
0243 
0244 void KateExternalToolsPluginView::setOutputData(const QString &data)
0245 {
0246     QTextCursor cursor(m_outputDoc);
0247     cursor.movePosition(QTextCursor::End);
0248     cursor.insertText(data);
0249 }
0250 
0251 void KateExternalToolsPluginView::deleteToolView()
0252 {
0253     if (m_toolView) {
0254         delete m_ui;
0255         m_ui = nullptr;
0256 
0257         delete m_toolView;
0258         m_toolView = nullptr;
0259     }
0260 }
0261 
0262 void KateExternalToolsPluginView::handleEsc(QEvent *event)
0263 {
0264     if (event->type() != QEvent::ShortcutOverride)
0265         return;
0266 
0267     auto keyEvent = static_cast<QKeyEvent *>(event);
0268     if (keyEvent && keyEvent->key() == Qt::Key_Escape && keyEvent->modifiers() == Qt::NoModifier) {
0269         deleteToolView();
0270     }
0271 }
0272 
0273 void KateExternalToolsPluginView::slotViewChanged(KTextEditor::View *v)
0274 {
0275     if (m_currentView) {
0276         disconnect(m_currentView->document(), &KTextEditor::Document::documentSavedOrUploaded, this, &KateExternalToolsPluginView::onDocumentSaved);
0277         disconnect(m_currentView->document(), &KTextEditor::Document::aboutToSave, this, &KateExternalToolsPluginView::onDocumentAboutToSave);
0278     }
0279     m_currentView = v;
0280 
0281     if (!m_currentView) {
0282         return;
0283     }
0284 
0285     connect(v->document(), &KTextEditor::Document::documentSavedOrUploaded, this, &KateExternalToolsPluginView::onDocumentSaved, Qt::UniqueConnection);
0286     connect(v->document(), &KTextEditor::Document::aboutToSave, this, &KateExternalToolsPluginView::onDocumentAboutToSave, Qt::UniqueConnection);
0287 }
0288 
0289 void KateExternalToolsPluginView::onDocumentSaved(KTextEditor::Document *doc)
0290 {
0291     // We only want to run this in the current active mainwindow
0292     if (KTextEditor::Editor::instance()->application()->activeMainWindow() != m_mainWindow) {
0293         return;
0294     }
0295 
0296     const auto tools = m_plugin->tools();
0297     for (KateExternalTool *tool : tools) {
0298         const bool hasSaveTrigger = tool->trigger == KateExternalTool::Trigger::AfterSave;
0299         if (hasSaveTrigger && tool->matchesMimetype(doc->mimeType())) {
0300             m_plugin->runTool(*tool, m_currentView, /*exec save trigger=*/true);
0301         }
0302     }
0303 }
0304 
0305 void KateExternalToolsPluginView::onDocumentAboutToSave(KTextEditor::Document *doc)
0306 {
0307     // We only want to run this in the current active mainwindow
0308     if (KTextEditor::Editor::instance()->application()->activeMainWindow() != m_mainWindow) {
0309         return;
0310     }
0311 
0312     const auto tools = m_plugin->tools();
0313     for (KateExternalTool *tool : tools) {
0314         const bool hasSaveTrigger = tool->trigger == KateExternalTool::Trigger::BeforeSave;
0315         if (hasSaveTrigger && tool->matchesMimetype(doc->mimeType())) {
0316             m_plugin->blockingRunTool(*tool, m_currentView, /*exec save trigger=*/true);
0317         }
0318     }
0319 }
0320 
0321 // END KateExternalToolsPluginView
0322 
0323 #include "moc_kateexternaltoolsview.cpp"
0324 
0325 // kate: space-indent on; indent-width 4; replace-tabs on;