Files
konsole/src/plugins/QuickCommands/quickcommandswidget.cpp
2022-05-24 12:16:55 +05:00

322 lines
11 KiB
C++

// This file was part of the KDE libraries
// SPDX-FileCopyrightText: 2022 Tao Guo <guotao945@gmail.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "quickcommandswidget.h"
#include "filtermodel.h"
#include "konsoledebug.h"
#include "terminalDisplay/TerminalDisplay.h"
#include <QMenu>
#include "ui_qcwidget.h"
#include <KLocalizedString>
#include <KMessageBox>
#include <QStandardPaths>
#include <QTabWidget>
#include <QTemporaryFile>
#include <QTimer>
#include <kmessagebox.h>
#include <kstandardguiitem.h>
#include <KTextEditor/Document>
#include <KTextEditor/Editor>
#include <KTextEditor/View>
#include <KTextEditor/ConfigInterface>
struct QuickCommandsWidget::Private {
QuickCommandsModel *model = nullptr;
FilterModel *filterModel = nullptr;
Konsole::SessionController *controller = nullptr;
bool hasShellCheck = false;
QTimer shellCheckTimer;
};
QuickCommandsWidget::QuickCommandsWidget(QWidget *parent)
: QWidget(parent)
, ui(std::make_unique<Ui::QuickCommandsWidget>())
, priv(std::make_unique<Private>())
{
ui->setupUi(this);
priv->hasShellCheck = !QStandardPaths::findExecutable(QStringLiteral("shellcheck")).isEmpty();
if (!priv->hasShellCheck) {
ui->warning->setPlainText(QStringLiteral("Missing executable shellcheck"));
}
priv->shellCheckTimer.setSingleShot(true);
priv->filterModel = new FilterModel(this);
connect(ui->btnAdd, &QPushButton::clicked, this, &QuickCommandsWidget::addMode);
connect(ui->btnSave, &QPushButton::clicked, this, &QuickCommandsWidget::saveCommand);
connect(ui->btnUpdate, &QPushButton::clicked, this, &QuickCommandsWidget::updateCommand);
connect(ui->btnCancel, &QPushButton::clicked, this, &QuickCommandsWidget::viewMode);
connect(ui->btnRun, &QPushButton::clicked, this, &QuickCommandsWidget::runCommand);
connect(ui->invertFilter, &QPushButton::clicked, priv->filterModel, &FilterModel::setInvertFilter);
connect(ui->filterLine, &QLineEdit::textChanged, this, [this] {
priv->filterModel->setFilterRegularExpression(ui->filterLine->text());
priv->filterModel->invalidate();
});
auto doc = KTextEditor::Editor::instance()->createDocument(this);
auto view = doc->createView(this);
auto ciface = qobject_cast<KTextEditor::ConfigInterface*>(view);
ciface->setConfigValue(QStringLiteral("line-numbers"), true);
ciface->setConfigValue(QStringLiteral("modification-markers"), true);
ui->tabWidget->insertTab(0, view, i18n("Command"));
ui->commandsTreeView->setModel(priv->filterModel);
ui->commandsTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->commandsTreeView, &QTreeView::doubleClicked, this, &QuickCommandsWidget::invokeCommand);
connect(ui->commandsTreeView, &QTreeView::clicked, this, &QuickCommandsWidget::indexSelected);
connect(ui->commandsTreeView, &QTreeView::customContextMenuRequested, this, &QuickCommandsWidget::createMenu);
connect(&priv->shellCheckTimer, &QTimer::timeout, this, &QuickCommandsWidget::runShellCheck);
connect(ui->command, &QPlainTextEdit::textChanged, this, [this] {
priv->shellCheckTimer.start(250);
});
viewMode();
}
QuickCommandsWidget::~QuickCommandsWidget() = default;
void QuickCommandsWidget::prepareEdit()
{
QString groupName = ui->group->currentText();
ui->group->clear();
ui->group->addItems(priv->model->groups());
ui->group->setCurrentText(groupName);
ui->commandsTreeView->setDisabled(true);
ui->commandsWidget->show();
}
void QuickCommandsWidget::viewMode()
{
ui->commandsTreeView->setDisabled(false);
ui->commandsWidget->hide();
ui->btnAdd->show();
ui->btnSave->hide();
ui->btnUpdate->hide();
ui->btnCancel->hide();
}
void QuickCommandsWidget::addMode()
{
ui->btnAdd->hide();
ui->btnSave->show();
ui->btnUpdate->hide();
ui->btnCancel->show();
prepareEdit();
}
void QuickCommandsWidget::indexSelected(const QModelIndex &idx)
{
const auto sourceIdx = priv->filterModel->mapToSource(ui->commandsTreeView->currentIndex());
if (priv->model->rowCount(sourceIdx) != 0) {
ui->name->setText({});
ui->tooltip->setText({});
ui->command->setPlainText({});
ui->group->setCurrentText({});
return;
}
const auto item = priv->model->itemFromIndex(sourceIdx);
const auto data = item->data(QuickCommandsModel::QuickCommandRole).value<QuickCommandData>();
ui->name->setText(data.name);
ui->tooltip->setText(data.tooltip);
ui->command->setPlainText(data.command);
ui->group->setCurrentText(item->parent()->text());
runShellCheck();
}
void QuickCommandsWidget::editMode()
{
ui->btnAdd->hide();
ui->btnSave->hide();
ui->btnUpdate->show();
ui->btnCancel->show();
prepareEdit();
}
void QuickCommandsWidget::saveCommand()
{
if (!valid())
return;
if (priv->model->addChildItem(data(), ui->group->currentText()))
viewMode();
else
KMessageBox::messageBox(this, KMessageBox::DialogType::Error, i18n("A duplicate item exists"));
}
void QuickCommandsWidget::updateCommand()
{
const auto sourceIdx = priv->filterModel->mapToSource(ui->commandsTreeView->currentIndex());
if (!valid())
return;
if (priv->model->editChildItem(data(), sourceIdx, ui->group->currentText()))
viewMode();
else
KMessageBox::messageBox(this, KMessageBox::DialogType::Error, i18n("A duplicate item exists"));
}
void QuickCommandsWidget::invokeCommand(const QModelIndex &idx)
{
if (!ui->warning->toPlainText().isEmpty()) {
QMessageBox::warning(this, QStringLiteral("Shell Errors"), i18n("Please fix all the warnings before trying to execute this script"));
return;
}
if (!priv->controller) {
return;
}
const auto sourceIdx = priv->filterModel->mapToSource(idx);
if (sourceIdx.parent() == priv->model->invisibleRootItem()->index()) {
return;
}
const auto item = priv->model->itemFromIndex(sourceIdx);
const auto data = item->data(QuickCommandsModel::QuickCommandRole).value<QuickCommandData>();
priv->controller->session()->sendTextToTerminal(data.command, QLatin1Char('\r'));
if (priv->controller->session()->views().count()) {
priv->controller->session()->views().at(0)->setFocus();
}
}
void QuickCommandsWidget::runCommand()
{
if (!ui->warning->toPlainText().isEmpty()) {
auto choice = KMessageBox::questionYesNo(this,
i18n("There are some errors on the script, do you really want to execute it?"),
i18n("Shell Errors"),
KStandardGuiItem::yes(),
KStandardGuiItem::no(),
QStringLiteral("quick-commands-question"));
if (choice == KMessageBox::ButtonCode::No) {
return;
}
}
const QString command = ui->command->toPlainText();
priv->controller->session()->sendTextToTerminal(command, QLatin1Char('\r'));
if (priv->controller->session()->views().count()) {
priv->controller->session()->views().at(0)->setFocus();
}
}
void QuickCommandsWidget::triggerRename()
{
ui->commandsTreeView->edit(ui->commandsTreeView->currentIndex());
}
void QuickCommandsWidget::triggerDelete()
{
const auto idx = ui->commandsTreeView->currentIndex();
const QString text = idx.data(Qt::DisplayRole).toString();
const QString dialogMessage = ui->commandsTreeView->model()->rowCount(idx)
? i18n("You are about to remove the group %1,\n with multiple configurations, are you sure?", text)
: i18n("You are about to remove %1, are you sure?", text);
KMessageBox::ButtonCode result = KMessageBox::messageBox(this,
KMessageBox::DialogType::WarningYesNo,
dialogMessage,
i18n("Remove Quick Commands Configurations"),
KStandardGuiItem::yes(),
KStandardGuiItem::no(),
KStandardGuiItem::cancel());
if (result != KMessageBox::ButtonCode::Yes)
return;
const auto sourceIdx = priv->filterModel->mapToSource(idx);
priv->model->removeRow(sourceIdx.row(), sourceIdx.parent());
}
QuickCommandData QuickCommandsWidget::data() const
{
QuickCommandData data;
data.name = ui->name->text().trimmed();
data.tooltip = ui->tooltip->text();
data.command = ui->command->toPlainText();
return data;
}
void QuickCommandsWidget::setModel(QuickCommandsModel *model)
{
priv->model = model;
priv->filterModel->setSourceModel(model);
}
void QuickCommandsWidget::setCurrentController(Konsole::SessionController *controller)
{
priv->controller = controller;
}
bool QuickCommandsWidget::valid()
{
if (ui->name->text().isEmpty() || ui->name->text().trimmed().isEmpty()) {
KMessageBox::messageBox(this, KMessageBox::DialogType::Error, i18n("Title can not be empty or blank"));
return false;
}
if (ui->command->toPlainText().isEmpty()) {
KMessageBox::messageBox(this, KMessageBox::DialogType::Error, i18n("Command can not be empty"));
return false;
}
return true;
}
void QuickCommandsWidget::createMenu(const QPoint &pos)
{
QModelIndex idx = ui->commandsTreeView->indexAt(pos);
if (!idx.isValid())
return;
auto sourceIdx = priv->filterModel->mapToSource(idx);
const bool isParent = sourceIdx.parent() == priv->model->invisibleRootItem()->index();
QMenu *menu = new QMenu(this);
if (!isParent) {
auto actionEdit = new QAction(i18n("Edit"), ui->commandsTreeView);
menu->addAction(actionEdit);
connect(actionEdit, &QAction::triggered, this, &QuickCommandsWidget::editMode);
} else {
auto actionRename = new QAction(i18n("Rename"), ui->commandsTreeView);
menu->addAction(actionRename);
connect(actionRename, &QAction::triggered, this, &QuickCommandsWidget::triggerRename);
}
auto actionDelete = new QAction(i18n("Delete"), ui->commandsTreeView);
menu->addAction(actionDelete);
connect(actionDelete, &QAction::triggered, this, &QuickCommandsWidget::triggerDelete);
menu->popup(ui->commandsTreeView->viewport()->mapToGlobal(pos));
}
void QuickCommandsWidget::runShellCheck()
{
if (!priv->hasShellCheck) {
return;
}
QTemporaryFile file;
file.open();
QTextStream ts(&file);
ts << "#!/bin/bash\n";
ts << ui->command->toPlainText();
file.close();
QString fName = file.fileName();
QProcess process;
process.start(QStringLiteral("shellcheck"), {fName});
process.waitForFinished();
const QString errorString = QString::fromLocal8Bit(process.readAllStandardOutput());
ui->warning->setPlainText(errorString);
if (errorString.isEmpty()) {
ui->tabWidget->setTabText(1, i18n("Warnings"));
} else {
ui->tabWidget->setTabText(1, i18n("Warnings (*)"));
}
}