File indexing completed on 2024-05-19 05:54:12
0001 // This file was part of the KDE libraries 0002 // SPDX-FileCopyrightText: 2022 Tao Guo <guotao945@gmail.com> 0003 // SPDX-License-Identifier: GPL-2.0-or-later 0004 0005 #include "quickcommandswidget.h" 0006 #include "filtermodel.h" 0007 #include "konsoledebug.h" 0008 #include "terminalDisplay/TerminalDisplay.h" 0009 0010 #include <QMenu> 0011 0012 #include "ui_qcwidget.h" 0013 #include <KLocalizedString> 0014 #include <KMessageBox> 0015 #include <kwidgetsaddons_version.h> 0016 0017 #include <QSettings> 0018 #include <QStandardPaths> 0019 #include <QTemporaryFile> 0020 #include <QTimer> 0021 0022 #include <QShowEvent> 0023 0024 #include <kmessagebox.h> 0025 #include <kstandardguiitem.h> 0026 0027 struct QuickCommandsWidget::Private { 0028 QuickCommandsModel *model = nullptr; 0029 FilterModel *filterModel = nullptr; 0030 Konsole::SessionController *controller = nullptr; 0031 bool hasShellCheck = false; 0032 bool isSetup = false; 0033 QTimer shellCheckTimer; 0034 }; 0035 0036 QuickCommandsWidget::QuickCommandsWidget(QWidget *parent) 0037 : QWidget(parent) 0038 , ui(std::make_unique<Ui::QuickCommandsWidget>()) 0039 , priv(std::make_unique<Private>()) 0040 { 0041 ui->setupUi(this); 0042 0043 priv->hasShellCheck = !QStandardPaths::findExecutable(QStringLiteral("shellcheck")).isEmpty(); 0044 if (!priv->hasShellCheck) { 0045 ui->warning->setPlainText(QStringLiteral("Missing executable 'shellcheck', please install")); 0046 } 0047 priv->shellCheckTimer.setSingleShot(true); 0048 0049 priv->filterModel = new FilterModel(this); 0050 connect(ui->btnAdd, &QPushButton::clicked, this, &QuickCommandsWidget::addMode); 0051 connect(ui->btnSave, &QPushButton::clicked, this, &QuickCommandsWidget::saveCommand); 0052 connect(ui->btnUpdate, &QPushButton::clicked, this, &QuickCommandsWidget::updateCommand); 0053 connect(ui->btnCancel, &QPushButton::clicked, this, &QuickCommandsWidget::viewMode); 0054 connect(ui->btnRun, &QPushButton::clicked, this, &QuickCommandsWidget::runCommand); 0055 0056 connect(ui->invertFilter, &QPushButton::clicked, priv->filterModel, &FilterModel::setInvertFilter); 0057 0058 connect(ui->filterLine, &QLineEdit::textChanged, this, [this] { 0059 priv->filterModel->setFilterRegularExpression(ui->filterLine->text()); 0060 priv->filterModel->invalidate(); 0061 }); 0062 0063 ui->commandsTreeView->setModel(priv->filterModel); 0064 ui->commandsTreeView->setContextMenuPolicy(Qt::CustomContextMenu); 0065 connect(ui->commandsTreeView, &QTreeView::doubleClicked, this, &QuickCommandsWidget::invokeCommand); 0066 connect(ui->commandsTreeView, &QTreeView::clicked, this, &QuickCommandsWidget::indexSelected); 0067 0068 connect(ui->commandsTreeView, &QTreeView::customContextMenuRequested, this, &QuickCommandsWidget::createMenu); 0069 0070 connect(&priv->shellCheckTimer, &QTimer::timeout, this, &QuickCommandsWidget::runShellCheck); 0071 connect(ui->command, &QPlainTextEdit::textChanged, this, [this] { 0072 priv->shellCheckTimer.start(250); 0073 }); 0074 0075 viewMode(); 0076 0077 QSettings settings; 0078 settings.beginGroup(QStringLiteral("plugins")); 0079 settings.beginGroup(QStringLiteral("quickcommands")); 0080 0081 const QKeySequence def(Qt::CTRL | Qt::ALT | Qt::Key_G); 0082 const QString defText = def.toString(); 0083 const QString entry = settings.value(QStringLiteral("shortcut"), defText).toString(); 0084 const QKeySequence shortcutEntry(entry); 0085 0086 connect(ui->keySequenceEdit, &QKeySequenceEdit::keySequenceChanged, this, [this] { 0087 auto shortcut = ui->keySequenceEdit->keySequence(); 0088 Q_EMIT quickAccessShortcutChanged(shortcut); 0089 }); 0090 ui->keySequenceEdit->setKeySequence(shortcutEntry); 0091 } 0092 0093 QuickCommandsWidget::~QuickCommandsWidget() = default; 0094 0095 void QuickCommandsWidget::prepareEdit() 0096 { 0097 QString groupName = ui->group->currentText(); 0098 0099 ui->group->clear(); 0100 ui->group->addItems(priv->model->groups()); 0101 ui->group->setCurrentText(groupName); 0102 ui->commandsTreeView->setDisabled(true); 0103 0104 ui->commandsWidget->show(); 0105 } 0106 void QuickCommandsWidget::viewMode() 0107 { 0108 ui->commandsTreeView->setDisabled(false); 0109 ui->commandsWidget->hide(); 0110 ui->btnAdd->show(); 0111 ui->btnSave->hide(); 0112 ui->btnUpdate->hide(); 0113 ui->btnCancel->hide(); 0114 } 0115 0116 void QuickCommandsWidget::addMode() 0117 { 0118 ui->btnAdd->hide(); 0119 ui->btnSave->show(); 0120 ui->btnUpdate->hide(); 0121 ui->btnCancel->show(); 0122 prepareEdit(); 0123 } 0124 0125 void QuickCommandsWidget::indexSelected(const QModelIndex &idx) 0126 { 0127 Q_UNUSED(idx) 0128 0129 const auto sourceIdx = priv->filterModel->mapToSource(ui->commandsTreeView->currentIndex()); 0130 if (priv->model->rowCount(sourceIdx) != 0) { 0131 ui->name->setText({}); 0132 ui->tooltip->setText({}); 0133 ui->command->setPlainText({}); 0134 ui->group->setCurrentText({}); 0135 return; 0136 } 0137 0138 const auto item = priv->model->itemFromIndex(sourceIdx); 0139 0140 if (item != nullptr && item->parent() != nullptr) { 0141 const auto data = item->data(QuickCommandsModel::QuickCommandRole).value<QuickCommandData>(); 0142 ui->name->setText(data.name); 0143 ui->tooltip->setText(data.tooltip); 0144 ui->command->setPlainText(data.command); 0145 ui->group->setCurrentText(item->parent()->text()); 0146 0147 runShellCheck(); 0148 } 0149 } 0150 0151 void QuickCommandsWidget::editMode() 0152 { 0153 ui->btnAdd->hide(); 0154 ui->btnSave->hide(); 0155 ui->btnUpdate->show(); 0156 ui->btnCancel->show(); 0157 prepareEdit(); 0158 } 0159 0160 void QuickCommandsWidget::saveCommand() 0161 { 0162 if (!valid()) 0163 return; 0164 if (priv->model->addChildItem(data(), ui->group->currentText())) 0165 viewMode(); 0166 else 0167 KMessageBox::error(this, i18n("A duplicate item exists")); 0168 } 0169 0170 void QuickCommandsWidget::updateCommand() 0171 { 0172 const auto sourceIdx = priv->filterModel->mapToSource(ui->commandsTreeView->currentIndex()); 0173 if (!valid()) 0174 return; 0175 if (priv->model->editChildItem(data(), sourceIdx, ui->group->currentText())) 0176 viewMode(); 0177 else 0178 KMessageBox::error(this, i18n("A duplicate item exists")); 0179 } 0180 0181 void QuickCommandsWidget::invokeCommand(const QModelIndex &idx) 0182 { 0183 if (!ui->warning->toPlainText().isEmpty()) { 0184 QMessageBox::warning(this, QStringLiteral("Shell Errors"), i18n("Please fix all the warnings before trying to run this script")); 0185 return; 0186 } 0187 0188 if (!priv->controller) { 0189 return; 0190 } 0191 const auto sourceIdx = priv->filterModel->mapToSource(idx); 0192 if (sourceIdx.parent() == priv->model->invisibleRootItem()->index()) { 0193 return; 0194 } 0195 const auto item = priv->model->itemFromIndex(sourceIdx); 0196 const auto data = item->data(QuickCommandsModel::QuickCommandRole).value<QuickCommandData>(); 0197 priv->controller->session()->sendTextToTerminal(data.command, QLatin1Char('\r')); 0198 0199 if (priv->controller->session()->views().count()) { 0200 priv->controller->session()->views().at(0)->setFocus(); 0201 } 0202 } 0203 0204 void QuickCommandsWidget::runCommand() 0205 { 0206 if (!priv->hasShellCheck) { 0207 // check again 0208 priv->hasShellCheck = !QStandardPaths::findExecutable(QStringLiteral("shellcheck")).isEmpty(); 0209 if (priv->hasShellCheck) { 0210 ui->warning->clear(); 0211 } 0212 } 0213 0214 if (!ui->warning->toPlainText().isEmpty()) { 0215 auto choice = KMessageBox::questionTwoActions(this, 0216 i18n("There are some errors on the script, do you really want to run it?"), 0217 i18n("Shell Errors"), 0218 KGuiItem(i18nc("@action:button", "Run"), QStringLiteral("system-run")), 0219 KStandardGuiItem::cancel(), 0220 QStringLiteral("quick-commands-question")); 0221 if (choice == KMessageBox::SecondaryAction) { 0222 return; 0223 } 0224 } 0225 0226 const QString command = ui->command->toPlainText(); 0227 priv->controller->session()->sendTextToTerminal(command, QLatin1Char('\r')); 0228 if (priv->controller->session()->views().count()) { 0229 priv->controller->session()->views().at(0)->setFocus(); 0230 } 0231 } 0232 0233 void QuickCommandsWidget::triggerRename() 0234 { 0235 ui->commandsTreeView->edit(ui->commandsTreeView->currentIndex()); 0236 } 0237 0238 void QuickCommandsWidget::triggerDelete() 0239 { 0240 const auto idx = ui->commandsTreeView->currentIndex(); 0241 const QString text = idx.data(Qt::DisplayRole).toString(); 0242 const QString dialogMessage = ui->commandsTreeView->model()->rowCount(idx) 0243 ? i18n("You are about to delete the group %1,\n with multiple configurations, are you sure?", text) 0244 : i18n("You are about to delete %1, are you sure?", text); 0245 0246 int result = 0247 KMessageBox::warningTwoActions(this, dialogMessage, i18n("Delete Quick Commands Configurations"), KStandardGuiItem::del(), KStandardGuiItem::cancel()); 0248 if (result != KMessageBox::PrimaryAction) 0249 return; 0250 0251 const auto sourceIdx = priv->filterModel->mapToSource(idx); 0252 priv->model->removeRow(sourceIdx.row(), sourceIdx.parent()); 0253 } 0254 0255 QuickCommandData QuickCommandsWidget::data() const 0256 { 0257 QuickCommandData data; 0258 data.name = ui->name->text().trimmed(); 0259 data.tooltip = ui->tooltip->text(); 0260 data.command = ui->command->toPlainText(); 0261 return data; 0262 } 0263 0264 void QuickCommandsWidget::setModel(QuickCommandsModel *model) 0265 { 0266 priv->model = model; 0267 priv->filterModel->setSourceModel(model); 0268 } 0269 void QuickCommandsWidget::setCurrentController(Konsole::SessionController *controller) 0270 { 0271 priv->controller = controller; 0272 } 0273 0274 bool QuickCommandsWidget::valid() 0275 { 0276 if (ui->name->text().isEmpty() || ui->name->text().trimmed().isEmpty()) { 0277 KMessageBox::error(this, i18n("Title can not be empty or blank")); 0278 return false; 0279 } 0280 if (ui->command->toPlainText().isEmpty()) { 0281 KMessageBox::error(this, i18n("Command can not be empty")); 0282 return false; 0283 } 0284 return true; 0285 } 0286 0287 void QuickCommandsWidget::createMenu(const QPoint &pos) 0288 { 0289 QModelIndex idx = ui->commandsTreeView->indexAt(pos); 0290 if (!idx.isValid()) 0291 return; 0292 auto sourceIdx = priv->filterModel->mapToSource(idx); 0293 const bool isParent = sourceIdx.parent() == priv->model->invisibleRootItem()->index(); 0294 QMenu *menu = new QMenu(this); 0295 0296 if (!isParent) { 0297 auto actionEdit = new QAction(i18n("Edit"), ui->commandsTreeView); 0298 menu->addAction(actionEdit); 0299 connect(actionEdit, &QAction::triggered, this, &QuickCommandsWidget::editMode); 0300 } else { 0301 auto actionRename = new QAction(i18n("Rename"), ui->commandsTreeView); 0302 menu->addAction(actionRename); 0303 connect(actionRename, &QAction::triggered, this, &QuickCommandsWidget::triggerRename); 0304 } 0305 auto actionDelete = new QAction(i18n("Delete"), ui->commandsTreeView); 0306 menu->addAction(actionDelete); 0307 connect(actionDelete, &QAction::triggered, this, &QuickCommandsWidget::triggerDelete); 0308 menu->popup(ui->commandsTreeView->viewport()->mapToGlobal(pos)); 0309 } 0310 0311 void QuickCommandsWidget::runShellCheck() 0312 { 0313 if (!priv->hasShellCheck) { 0314 return; 0315 } 0316 0317 QTemporaryFile file; 0318 file.open(); 0319 0320 QTextStream ts(&file); 0321 ts << "#!/bin/bash\n"; 0322 ts << ui->command->toPlainText(); 0323 file.close(); 0324 0325 QString fName = file.fileName(); 0326 QProcess process; 0327 process.start(QStringLiteral("shellcheck"), {fName}); 0328 process.waitForFinished(); 0329 0330 const QString errorString = QString::fromLocal8Bit(process.readAllStandardOutput()); 0331 ui->warning->setPlainText(errorString); 0332 0333 if (errorString.isEmpty()) { 0334 ui->tabWidget->setTabText(1, i18n("Warnings")); 0335 } else { 0336 ui->tabWidget->setTabText(1, i18n("Warnings (*)")); 0337 } 0338 } 0339 0340 void QuickCommandsWidget::showEvent(QShowEvent *) 0341 { 0342 if (!priv->isSetup) { 0343 ui->commandsTreeView->expandAll(); 0344 priv->isSetup = true; 0345 } 0346 } 0347 0348 #include "moc_quickcommandswidget.cpp"