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 "kateexternaltoolsconfigwidget.h"
0008 #include "externaltoolsplugin.h"
0009 #include "kateexternaltool.h"
0010 
0011 #include <KLineEdit>
0012 #include <KTextEditor/Editor>
0013 #include <KTextEditor/View>
0014 #include <ktexteditor_version.h>
0015 
0016 #include <KConfig>
0017 #include <KConfigGroup>
0018 #include <KIconLoader>
0019 #include <KMimeTypeChooser>
0020 #include <KSharedConfig>
0021 #include <QBitmap>
0022 #include <QMenu>
0023 #include <QMessageBox>
0024 #include <QRegularExpression>
0025 #include <QRegularExpressionValidator>
0026 #include <QStandardItem>
0027 #include <QStandardPaths>
0028 #include <QTreeView>
0029 
0030 namespace
0031 {
0032 constexpr int ToolRole = Qt::UserRole + 1;
0033 
0034 /**
0035  * Helper function to create a QStandardItem that internally stores a pointer to a KateExternalTool.
0036  */
0037 QStandardItem *newToolItem(const QIcon &icon, KateExternalTool *tool)
0038 {
0039     auto item = new QStandardItem(icon, tool->translatedName());
0040     item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled);
0041     item->setData(QVariant::fromValue(reinterpret_cast<quintptr>(tool)), ToolRole);
0042     return item;
0043 }
0044 
0045 /**
0046  * Helper function to return an internally stored KateExternalTool for a QStandardItem.
0047  * If a nullptr is returned, it means the QStandardItem is a category.
0048  */
0049 KateExternalTool *toolForItem(QStandardItem *item)
0050 {
0051     return item ? reinterpret_cast<KateExternalTool *>(item->data(ToolRole).value<quintptr>()) : nullptr;
0052 }
0053 
0054 QIcon blankIcon()
0055 {
0056     QPixmap pm(KIconLoader::SizeSmall, KIconLoader::SizeSmall);
0057     pm.fill();
0058     pm.setMask(pm.createHeuristicMask());
0059     return QIcon(pm);
0060 }
0061 
0062 static void makeToolUnique(KateExternalTool *tool, const QList<KateExternalTool *> &tools)
0063 {
0064     // Ensure that tool->name is unique
0065     int i = 1;
0066     QString name = tool->name;
0067     while (true) {
0068         const bool isUnique = std::none_of(tools.cbegin(), tools.cend(), [tool, &name](const KateExternalTool *t) {
0069             return (t != tool) && (t->name == name);
0070         });
0071         if (isUnique) {
0072             break;
0073         }
0074         name = tool->name + QString::number(i++);
0075     }
0076     tool->name = name;
0077 
0078     // Ensure tool->actionName is unique
0079     i = 1;
0080     QString actName = tool->actionName;
0081     while (true) {
0082         const bool isUnique = std::none_of(tools.cbegin(), tools.cend(), [tool, &actName](const KateExternalTool *t) {
0083             return (t != tool) && (t->actionName == actName);
0084         });
0085         if (isUnique) {
0086             break;
0087         }
0088         actName = tool->actionName + QString::number(i++);
0089     }
0090     tool->actionName = actName;
0091 
0092     // Ensure the tool->cmdname is unique
0093     // empty command line name is OK
0094     if (tool->cmdname.isEmpty()) {
0095         return;
0096     }
0097 
0098     i = 1;
0099     QString cmdname = tool->cmdname;
0100     while (true) {
0101         const bool isUnique = std::none_of(tools.cbegin(), tools.cend(), [tool, &cmdname](const KateExternalTool *t) {
0102             return (t != tool) && (t->cmdname == cmdname);
0103         });
0104         if (isUnique) {
0105             break;
0106         }
0107         cmdname = tool->cmdname + QString::number(i++);
0108     }
0109     tool->cmdname = cmdname;
0110 }
0111 
0112 static KateExternalTool defaultTool(const QString &actionName, const QList<KateExternalTool> &defaultTools)
0113 {
0114     auto it = std::find_if(defaultTools.cbegin(), defaultTools.cend(), [actionName](const KateExternalTool &defaultTool) {
0115         return actionName == defaultTool.actionName;
0116     });
0117     return (it != defaultTools.cend()) ? *it : KateExternalTool();
0118 }
0119 
0120 static bool isDefaultTool(KateExternalTool *tool, const QList<KateExternalTool> &defaultTools)
0121 {
0122     return tool && !defaultTool(tool->actionName, defaultTools).actionName.isEmpty();
0123 }
0124 }
0125 
0126 // BEGIN KateExternalToolServiceEditor
0127 KateExternalToolServiceEditor::KateExternalToolServiceEditor(KateExternalTool *tool, KateExternalToolsPlugin *plugin, QWidget *parent)
0128     : QDialog(parent)
0129     , m_plugin(plugin)
0130     , m_tool(tool)
0131 {
0132     setWindowTitle(i18n("Edit External Tool"));
0133     setWindowIcon(QIcon::fromTheme(QStringLiteral("system-run")));
0134 
0135     ui.setupUi(this);
0136     ui.btnIcon->setIconSize(KIconLoader::SizeSmall);
0137 
0138     connect(ui.buttonBox, &QDialogButtonBox::accepted, this, &KateExternalToolServiceEditor::slotOKClicked);
0139     connect(ui.buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
0140     connect(ui.btnMimeType, &QToolButton::clicked, this, &KateExternalToolServiceEditor::showMTDlg);
0141 
0142     Q_ASSERT(m_tool != nullptr);
0143     ui.edtName->setText(m_tool->translatedName());
0144     if (!m_tool->icon.isEmpty()) {
0145         ui.btnIcon->setIcon(m_tool->icon);
0146     }
0147 
0148     ui.edtExecutable->setText(m_tool->executable);
0149     ui.edtArgs->setText(m_tool->arguments);
0150     ui.edtInput->setText(m_tool->input);
0151     ui.edtWorkingDir->setText(m_tool->workingDir);
0152     ui.edtWorkingDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly);
0153     ui.edtMimeType->setText(m_tool->mimetypes.join(QStringLiteral("; ")));
0154     ui.cmbSave->setCurrentIndex(static_cast<int>(m_tool->saveMode));
0155     ui.chkReload->setChecked(m_tool->reload);
0156     ui.cmbOutput->setCurrentIndex(static_cast<int>(m_tool->outputMode));
0157     ui.edtCommand->setText(m_tool->cmdname);
0158     ui.cmbTrigger->setCurrentIndex((int)m_tool->trigger);
0159 
0160     static const QRegularExpressionValidator cmdLineValidator(QRegularExpression(QStringLiteral("[\\w-]*")));
0161     ui.edtCommand->setValidator(&cmdLineValidator);
0162 
0163     if (isDefaultTool(tool, m_plugin->defaultTools())) {
0164         ui.buttonBox->setStandardButtons(ui.buttonBox->standardButtons() | QDialogButtonBox::RestoreDefaults);
0165         ui.buttonBox->setToolTip(i18n("Revert tool to default settings"));
0166         connect(ui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, [this, tool]() {
0167             const auto t = defaultTool(tool->actionName, m_plugin->defaultTools());
0168             ui.edtName->setText(t.translatedName());
0169             ui.btnIcon->setIcon(t.icon);
0170             ui.edtExecutable->setText(t.executable);
0171             ui.edtArgs->setText(t.arguments);
0172             ui.edtInput->setText(t.input);
0173             ui.edtWorkingDir->setText(t.workingDir);
0174             ui.edtMimeType->setText(t.mimetypes.join(QStringLiteral("; ")));
0175             ui.cmbSave->setCurrentIndex(static_cast<int>(t.saveMode));
0176             ui.chkReload->setChecked(t.reload);
0177             ui.cmbOutput->setCurrentIndex(static_cast<int>(t.outputMode));
0178             ui.edtCommand->setText(t.cmdname);
0179             ui.cmbTrigger->setCurrentIndex(static_cast<int>(t.trigger));
0180         });
0181     }
0182 
0183     // add support for variable expansion
0184     KTextEditor::Editor::instance()->addVariableExpansion({ui.edtExecutable->lineEdit(), ui.edtArgs, ui.edtInput, ui.edtWorkingDir->lineEdit()});
0185 }
0186 
0187 void KateExternalToolServiceEditor::slotOKClicked()
0188 {
0189     if (ui.edtName->text().isEmpty() || ui.edtExecutable->text().isEmpty()) {
0190         QMessageBox::information(this, i18n("External Tool"), i18n("You must specify at least a name and an executable"));
0191         return;
0192     }
0193 
0194     const bool hasTrigger = ui.cmbTrigger->currentIndex() != (int)KateExternalTool::Trigger::None;
0195     if (hasTrigger && ui.edtMimeType->text().isEmpty()) {
0196         QMessageBox::information(this, i18n("External Tool"), i18n("With 'Trigger' enabled, at least one mimetype needs to be specified."));
0197         return;
0198     }
0199 
0200     accept();
0201 }
0202 
0203 void KateExternalToolServiceEditor::showMTDlg()
0204 {
0205     QString text = i18n("Select the MimeTypes for which to enable this tool.");
0206     QStringList list = ui.edtMimeType->text().split(QRegularExpression(QStringLiteral("\\s*;\\s*")), Qt::SkipEmptyParts);
0207     KMimeTypeChooserDialog d(i18n("Select Mime Types"), text, list, QStringLiteral("text"), this);
0208     if (d.exec() == QDialog::Accepted) {
0209         ui.edtMimeType->setText(d.chooser()->mimeTypes().join(QStringLiteral(";")));
0210     }
0211 }
0212 
0213 // END KateExternalToolServiceEditor
0214 
0215 // BEGIN KateExternalToolsConfigWidget
0216 KateExternalToolsConfigWidget::KateExternalToolsConfigWidget(QWidget *parent, KateExternalToolsPlugin *plugin)
0217     : KTextEditor::ConfigPage(parent)
0218     , m_plugin(plugin)
0219 {
0220     setupUi(this);
0221     layout()->setContentsMargins(0, 0, 0, 0);
0222     layout()->setSpacing(0);
0223     lbTools->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::BottomEdge}));
0224     lbTools->setModel(&m_toolsModel);
0225     lbTools->setSelectionMode(QAbstractItemView::SingleSelection);
0226     lbTools->setDragEnabled(true);
0227     lbTools->setAcceptDrops(true);
0228     lbTools->setDefaultDropAction(Qt::MoveAction);
0229     lbTools->setDropIndicatorShown(true);
0230     lbTools->setDragDropOverwriteMode(false);
0231     lbTools->setDragDropMode(QAbstractItemView::InternalMove);
0232 
0233     horizontalLayout->setSpacing(style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing));
0234     horizontalLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
0235                                          style()->pixelMetric(QStyle::PM_LayoutTopMargin),
0236                                          style()->pixelMetric(QStyle::PM_LayoutRightMargin),
0237                                          style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
0238 
0239     // Add... button popup menu
0240     auto addMenu = new QMenu(btnAdd);
0241     auto addToolAction = addMenu->addAction(i18n("Add Tool..."));
0242     auto addDefaultsMenu = addMenu->addMenu(i18n("Add Tool from Defaults"));
0243     addMenu->addSeparator();
0244     auto addCategoryAction = addMenu->addAction(i18n("Add Category"));
0245     btnAdd->setMenu(addMenu);
0246     connect(addDefaultsMenu, &QMenu::aboutToShow, [this, addDefaultsMenu]() {
0247         lazyInitDefaultsMenu(addDefaultsMenu);
0248     });
0249 
0250     connect(addCategoryAction, &QAction::triggered, this, &KateExternalToolsConfigWidget::slotAddCategory);
0251     connect(addToolAction, &QAction::triggered, this, &KateExternalToolsConfigWidget::slotAddTool);
0252     connect(btnRemove, &QPushButton::clicked, this, &KateExternalToolsConfigWidget::slotRemove);
0253     connect(btnEdit, &QPushButton::clicked, this, &KateExternalToolsConfigWidget::slotEdit);
0254     connect(lbTools->selectionModel(), &QItemSelectionModel::currentChanged, [this]() {
0255         slotSelectionChanged();
0256     });
0257     connect(lbTools, &QTreeView::doubleClicked, this, &KateExternalToolsConfigWidget::slotEdit);
0258 
0259     // reset triggers a reload of the existing tools
0260     reset();
0261     slotSelectionChanged();
0262 
0263     connect(&m_toolsModel, &QStandardItemModel::itemChanged, this, &KateExternalToolsConfigWidget::slotItemChanged);
0264 }
0265 
0266 KateExternalToolsConfigWidget::~KateExternalToolsConfigWidget()
0267 {
0268 }
0269 
0270 QString KateExternalToolsConfigWidget::name() const
0271 {
0272     return i18n("External Tools");
0273 }
0274 
0275 QString KateExternalToolsConfigWidget::fullName() const
0276 {
0277     return i18n("External Tools");
0278 }
0279 
0280 QIcon KateExternalToolsConfigWidget::icon() const
0281 {
0282     return QIcon::fromTheme(QStringLiteral("system-run"));
0283 }
0284 
0285 void KateExternalToolsConfigWidget::reset()
0286 {
0287     m_toolsModel.clear();
0288     m_toolsModel.invisibleRootItem()->setFlags(Qt::NoItemFlags);
0289 
0290     // the "Uncategorized" category always exists
0291     m_noCategory = addCategory(i18n("Uncategorized"));
0292     m_noCategory->setFlags(Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled);
0293 
0294     // create other tools and categories
0295     const auto tools = m_plugin->tools();
0296     for (KateExternalTool *tool : tools) {
0297         auto item = newToolItem(tool->icon.isEmpty() ? blankIcon() : QIcon::fromTheme(tool->icon), tool);
0298         auto category = tool->category.isEmpty() ? m_noCategory : addCategory(tool->category);
0299         category->appendRow(item);
0300     }
0301     lbTools->expandAll();
0302     m_changed = false;
0303 }
0304 
0305 void KateExternalToolsConfigWidget::apply()
0306 {
0307     if (!m_changed) {
0308         return;
0309     }
0310     m_changed = false;
0311 
0312     KSharedConfigPtr config = m_plugin->config();
0313     config->group(QStringLiteral("Global")).writeEntry("firststart", false);
0314     config->sync();
0315 
0316     m_plugin->removeTools(m_toolsToRemove);
0317     m_changedTools.erase(std::remove_if(m_changedTools.begin(),
0318                                         m_changedTools.end(),
0319                                         [this](const ChangedToolInfo &cti) {
0320                                             return std::find(m_toolsToRemove.begin(), m_toolsToRemove.end(), cti.tool) != m_toolsToRemove.end();
0321                                         }),
0322                          m_changedTools.end());
0323     m_toolsToRemove.clear();
0324 
0325     for (auto &[tool, oldName] : m_changedTools) {
0326         m_plugin->save(tool, oldName);
0327     }
0328     m_changedTools.clear();
0329 
0330     // So that KateExternalToolsPluginView::rebuildMenu() is called,
0331     // needed to update the menu actions
0332     Q_EMIT m_plugin->externalToolsChanged();
0333 }
0334 
0335 void KateExternalToolsConfigWidget::slotSelectionChanged()
0336 {
0337     // update button state
0338     auto item = m_toolsModel.itemFromIndex(lbTools->currentIndex());
0339     const bool isToolItem = toolForItem(item) != nullptr;
0340     const bool isCategory = item && !isToolItem;
0341 
0342     btnEdit->setEnabled(isToolItem || isCategory);
0343     btnRemove->setEnabled(isToolItem);
0344 }
0345 
0346 bool KateExternalToolsConfigWidget::editTool(KateExternalTool *tool)
0347 {
0348     bool changed = false;
0349     KSharedConfigPtr config = m_plugin->config();
0350 
0351     KateExternalToolServiceEditor editor(tool, m_plugin, this);
0352     KConfigGroup editorGroup = config->group(QStringLiteral("Editor"));
0353     editor.resize(editorGroup.readEntry("Size", QSize()));
0354 
0355     if (editor.exec() == QDialog::Accepted) {
0356         const QString oldName = tool->name;
0357         tool->name = editor.ui.edtName->text().trimmed();
0358         tool->icon = editor.ui.btnIcon->icon();
0359         tool->arguments = editor.ui.edtArgs->text();
0360         tool->input = editor.ui.edtInput->toPlainText();
0361         tool->workingDir = editor.ui.edtWorkingDir->text();
0362         tool->mimetypes = editor.ui.edtMimeType->text().split(QRegularExpression(QStringLiteral("\\s*;\\s*")), Qt::SkipEmptyParts);
0363         tool->saveMode = static_cast<KateExternalTool::SaveMode>(editor.ui.cmbSave->currentIndex());
0364         tool->reload = editor.ui.chkReload->isChecked();
0365         tool->outputMode = static_cast<KateExternalTool::OutputMode>(editor.ui.cmbOutput->currentIndex());
0366         tool->cmdname = editor.ui.edtCommand->text().trimmed();
0367         tool->trigger = static_cast<KateExternalTool::Trigger>(editor.ui.cmbTrigger->currentIndex());
0368 
0369         tool->executable = editor.ui.edtExecutable->text().trimmed();
0370         tool->hasexec = tool->executable.contains(QLatin1Char('$')) ? std::nullopt : std::optional<bool>(tool->checkExec());
0371 
0372         // sticky action collection name, never changes again, so that shortcuts stay
0373         if (tool->actionName.isEmpty()) {
0374             tool->actionName = QStringLiteral("externaltool_") + QString(tool->name).remove(QRegularExpression(QStringLiteral("\\W+")));
0375         }
0376 
0377         makeToolUnique(tool, m_plugin->tools());
0378 
0379         const bool renamed = !oldName.isEmpty() && oldName != tool->name;
0380         // Delay saving to apply()
0381         m_changedTools.push_back({tool, renamed ? oldName : QString{}});
0382 
0383         changed = true;
0384     }
0385 
0386     editorGroup.writeEntry("Size", editor.size());
0387     config->sync();
0388 
0389     return changed;
0390 }
0391 
0392 void KateExternalToolsConfigWidget::lazyInitDefaultsMenu(QMenu *defaultsMenu)
0393 {
0394     if (!defaultsMenu->isEmpty()) {
0395         return;
0396     }
0397 
0398     // create tool actions
0399     std::map<QString, QMenu *> categories;
0400 
0401     // first add categorized actions, such that the submenus appear at the top
0402     int defaultToolsIndex = 0;
0403     for (const auto &tool : m_plugin->defaultTools()) {
0404         const QString category = tool.category.isEmpty() ? i18n("Uncategorized") : tool.translatedCategory();
0405         auto categoryMenu = categories[category];
0406         if (!categoryMenu) {
0407             categoryMenu = new QMenu(category, this);
0408             categories[category] = categoryMenu;
0409             defaultsMenu->addMenu(categoryMenu);
0410         }
0411 
0412         auto a = categoryMenu->addAction(QIcon::fromTheme(tool.icon), tool.translatedName());
0413         a->setData(defaultToolsIndex);
0414 
0415         connect(a, &QAction::triggered, [this, a]() {
0416             slotAddDefaultTool(a->data().toInt());
0417         });
0418         ++defaultToolsIndex;
0419     }
0420 }
0421 
0422 void KateExternalToolsConfigWidget::slotAddDefaultTool(int defaultToolsIndex)
0423 {
0424     const auto &defaultTools = m_plugin->defaultTools();
0425     if (defaultToolsIndex < 0 || defaultToolsIndex > defaultTools.size()) {
0426         return;
0427     }
0428 
0429     addNewTool(new KateExternalTool(defaultTools[defaultToolsIndex]));
0430 }
0431 
0432 void KateExternalToolsConfigWidget::addNewTool(KateExternalTool *tool)
0433 {
0434     makeToolUnique(tool, m_plugin->tools());
0435 
0436     auto item = newToolItem(tool->icon.isEmpty() ? blankIcon() : QIcon::fromTheme(tool->icon), tool);
0437     auto category = addCategory(tool->translatedCategory());
0438     category->appendRow(item);
0439     tool->category = category->text();
0440     lbTools->setCurrentIndex(item->index());
0441 
0442     m_plugin->addNewTool(tool);
0443     m_changedTools.push_back({tool, {}});
0444 
0445     Q_EMIT changed();
0446     m_changed = true;
0447 }
0448 
0449 QStandardItem *KateExternalToolsConfigWidget::addCategory(const QString &translatedCategory)
0450 {
0451     if (translatedCategory.isEmpty() || (m_noCategory && translatedCategory == i18n("Uncategorized"))) {
0452         return currentCategory();
0453     }
0454 
0455     // search for existing category
0456     auto items = m_toolsModel.findItems(translatedCategory);
0457     if (!items.empty()) {
0458         return items.front();
0459     }
0460 
0461     // ...otherwise, create it
0462     auto item = new QStandardItem(translatedCategory);
0463 
0464     // for now, categories are not movable, otherwise, the use can move a
0465     // category into another category, which is not supported right now
0466     item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled | Qt::ItemIsEditable);
0467 
0468     m_toolsModel.appendRow(item);
0469     return item;
0470 }
0471 
0472 QStandardItem *KateExternalToolsConfigWidget::currentCategory() const
0473 {
0474     auto index = lbTools->currentIndex();
0475     if (!index.isValid()) {
0476         return m_noCategory;
0477     }
0478 
0479     auto item = m_toolsModel.itemFromIndex(index);
0480     auto tool = toolForItem(item);
0481     if (tool) {
0482         // the parent of a ToolItem is always a category
0483         return item->parent();
0484     }
0485 
0486     // item is no ToolItem, so we must have a category at hand
0487     return item;
0488 }
0489 
0490 void KateExternalToolsConfigWidget::slotAddCategory()
0491 {
0492     // find unique name
0493     QString name = i18n("New Category");
0494     int i = 1;
0495     while (!m_toolsModel.findItems(name, Qt::MatchFixedString).isEmpty()) {
0496         name = (i18n("New Category %1", i++));
0497     }
0498 
0499     // add category and switch to edit mode
0500     auto item = addCategory(name);
0501     lbTools->edit(item->index());
0502 }
0503 
0504 void KateExternalToolsConfigWidget::slotAddTool()
0505 {
0506     std::unique_ptr tool = std::make_unique<KateExternalTool>();
0507     if (editTool(tool.get())) {
0508         addNewTool(tool.release());
0509     }
0510 }
0511 
0512 void KateExternalToolsConfigWidget::slotRemove()
0513 {
0514     auto item = m_toolsModel.itemFromIndex(lbTools->currentIndex());
0515     auto tool = toolForItem(item);
0516 
0517     if (tool) {
0518         item->parent()->removeRow(item->index().row());
0519         // Delay calling m_plugin->removeTools() to apply()
0520         m_toolsToRemove.push_back(tool);
0521         Q_EMIT changed();
0522         m_changed = true;
0523     }
0524 }
0525 
0526 void KateExternalToolsConfigWidget::slotEdit()
0527 {
0528     auto item = m_toolsModel.itemFromIndex(lbTools->currentIndex());
0529     auto tool = toolForItem(item);
0530     if (!tool) {
0531         if (item) {
0532             lbTools->edit(item->index());
0533         }
0534         return;
0535     }
0536     // show the item in an editor
0537     if (editTool(tool)) {
0538         // renew the icon and name
0539         item->setText(tool->name);
0540         item->setIcon(tool->icon.isEmpty() ? blankIcon() : QIcon::fromTheme(tool->icon));
0541 
0542         Q_EMIT changed();
0543         m_changed = true;
0544     }
0545 }
0546 
0547 void KateExternalToolsConfigWidget::slotItemChanged(QStandardItem *item)
0548 {
0549     // If a tool was drag and dropped to some other category, we need
0550     // to update the tool's category
0551     if (KateExternalTool *tool = toolForItem(item)) {
0552         if (QStandardItem *parentCategory = item->parent()) {
0553             tool->category = parentCategory != m_noCategory ? parentCategory->text() : QString{};
0554             // Changes will be saved in apply()
0555             m_changedTools.push_back({tool, {}});
0556         }
0557     }
0558 
0559     m_changed = true;
0560     Q_EMIT changed();
0561 }
0562 
0563 // END KateExternalToolsConfigWidget
0564 
0565 #include "moc_kateexternaltoolsconfigwidget.cpp"
0566 
0567 // kate: space-indent on; indent-width 4; replace-tabs on;