Commit bba9b4cd authored by Kåre Särs's avatar Kåre Särs Committed by Waqar Ahmed
Browse files

Build-plugin: Provide the run terminals in the plugin

- If konsolepart is present use a konsole terminal.
- if konsolepart is missing just print stdout/err in a line-edit
- Add multiple run tabs support
- Reuse tabs if there is no foreground process running
- Indicate on the tab if a process is running
- Fix work-dir/search-paths + tweak target column-widths

(cherry picked from commit 0240fd5d)
parent afdef5a4
// SPDX-FileCopyrightText: 2022 Kåre Särs <kare.sars@iki.fi>
//
// SPDX-License-Identifier: LGPL-2.0-only
#include "AppOutput.h"
#include "hostprocess.h"
#include <QDebug>
#include <QFontDatabase>
#include <QScrollBar>
#include <QTextCursor>
#include <QTextEdit>
#include <QTimer>
#include <QVBoxLayout>
#include <KColorScheme>
#include <KProcess>
#include <KService>
#include <KParts/ReadOnlyPart>
#include <kde_terminal_interface.h>
struct AppOutput::Private {
Private(AppOutput *pub)
: q(pub)
{
}
KParts::ReadOnlyPart *part = nullptr;
KProcess process;
QTextEdit *outputArea = nullptr;
QString terminalProcess;
AppOutput *q = nullptr;
void addOutputText(QString const &text)
{
qDebug() << text;
if (!outputArea) {
qWarning() << "Can't output text to nullptr";
return;
}
QScrollBar *scrollb = outputArea->verticalScrollBar();
if (!scrollb) {
return;
}
bool atEnd = (scrollb->value() == scrollb->maximum());
QTextCursor cursor = outputArea->textCursor();
if (!cursor.atEnd()) {
cursor.movePosition(QTextCursor::End);
}
cursor.insertText(text);
if (atEnd) {
scrollb->setValue(scrollb->maximum());
}
}
void updateTerminalProcessInfo()
{
TerminalInterface *t = qobject_cast<TerminalInterface *>(part);
if (!t) {
return;
}
if (terminalProcess != t->foregroundProcessName()) {
terminalProcess = t->foregroundProcessName();
Q_EMIT q->runningChanhged();
}
if (!terminalProcess.isEmpty()) {
AppOutput *qThis = q;
QTimer::singleShot(500, q, [qThis]() {
qThis->d->updateTerminalProcessInfo();
});
}
}
};
AppOutput::AppOutput(QWidget *parent)
: QWidget(parent)
, d(std::make_unique<AppOutput::Private>(this))
{
KPluginFactory *factory = KPluginFactory::loadFactory(QStringLiteral("konsolepart")).plugin;
if (!factory) {
qDebug() << "could not load the konsolepart factory";
} else {
d->part = factory->create<KParts::ReadOnlyPart>(this);
}
TerminalInterface *t = nullptr;
if (!d->part) {
qDebug() << "could not create a konsole part";
} else {
t = qobject_cast<TerminalInterface *>(d->part);
}
if (!t) {
qDebug() << "Failed to cast the TerminalInterface";
}
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
if (d->part) {
layout->addWidget(d->part->widget());
connect(d->part->widget(), &QObject::destroyed, this, &AppOutput::deleteLater);
setFocusProxy(d->part->widget());
} else {
d->outputArea = new QTextEdit(this);
layout->addWidget(d->outputArea);
d->outputArea->setAcceptRichText(false);
d->outputArea->setReadOnly(true);
d->outputArea->setUndoRedoEnabled(false);
// fixed wide font, like konsole
d->outputArea->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
// alternate color scheme, like konsole
KColorScheme schemeView(QPalette::Active, KColorScheme::View);
d->outputArea->setTextBackgroundColor(schemeView.foreground().color());
d->outputArea->setTextColor(schemeView.background().color());
QPalette p = d->outputArea->palette();
p.setColor(QPalette::Base, schemeView.foreground().color());
d->outputArea->setPalette(p);
d->process.setOutputChannelMode(KProcess::SeparateChannels);
connect(&d->process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &AppOutput::runningChanhged);
connect(&d->process, &KProcess::readyReadStandardError, this, [this]() {
d->addOutputText(QString::fromUtf8(d->process.readAllStandardError()));
});
connect(&d->process, &KProcess::readyReadStandardOutput, this, [this]() {
d->addOutputText(QString::fromUtf8(d->process.readAllStandardOutput()));
});
}
}
AppOutput::~AppOutput()
{
d->process.kill();
}
void AppOutput::setWorkingDir(const QString &path)
{
TerminalInterface *t = qobject_cast<TerminalInterface *>(d->part);
if (t) {
t->showShellInDir(path);
} else {
d->process.setWorkingDirectory(path);
}
}
void AppOutput::runCommand(const QString &cmd)
{
TerminalInterface *t = qobject_cast<TerminalInterface *>(d->part);
if (t) {
t->sendInput(cmd + QLatin1Char('\n'));
d->terminalProcess = cmd;
// Start updating d->terminalProcess since TerminalInterface does not provide the
// info of when the foregroundProcess changes.
QTimer::singleShot(500, this, [this]() {
d->updateTerminalProcessInfo();
});
} else {
d->outputArea->clear();
d->process.setShellCommand(cmd);
startHostProcess(d->process);
d->process.waitForStarted(300);
}
Q_EMIT runningChanhged();
}
QString AppOutput::runningProcess()
{
TerminalInterface *t = qobject_cast<TerminalInterface *>(d->part);
if (t) {
return d->terminalProcess;
}
QString program = d->process.program().isEmpty() ? QString() : d->process.program().first();
return d->process.state() == QProcess::NotRunning ? QString() : program;
}
// SPDX-FileCopyrightText: 2022 Kåre Särs <kare.sars@iki.fi>
//
// SPDX-License-Identifier: LGPL-2.0-only
#ifndef AppOutput_H
#define AppOutput_H
#include <QString>
#include <QWidget>
/** This widget provides terminal (where konsolepart is available and
* plain buffered stdout as a fallback.
*/
class AppOutput : public QWidget
{
Q_OBJECT
public:
AppOutput(QWidget *parent = nullptr);
~AppOutput(); // This one is needed for the std::unique_ptr
void setWorkingDir(const QString &path);
void runCommand(const QString &cmd);
QString runningProcess();
Q_SIGNALS:
void runningChanhged();
private:
struct Private;
friend struct Private;
std::unique_ptr<Private> d;
};
#endif
......@@ -13,5 +13,6 @@ target_sources(
TargetModel.cpp
UrlInserter.cpp
TargetFilterProxyModel.cpp
AppOutput.cpp
plugin.qrc
)
......@@ -295,9 +295,15 @@ const QString TargetModel::cmdName(const QModelIndex &itemIndex)
}
const QString TargetModel::workDir(const QModelIndex &itemIndex)
{
QStringList paths = searchPaths(itemIndex);
return paths.isEmpty() ? QString() : paths.first();
}
const QStringList TargetModel::searchPaths(const QModelIndex &itemIndex)
{
if (!itemIndex.isValid()) {
return QString();
return QStringList();
}
QModelIndex workDirIndex = itemIndex.sibling(itemIndex.row(), 1);
......@@ -305,7 +311,7 @@ const QString TargetModel::workDir(const QModelIndex &itemIndex)
if (itemIndex.parent().isValid()) {
workDirIndex = itemIndex.parent().siblingAtColumn(1);
}
return workDirIndex.data().toString();
return workDirIndex.data().toString().split(QLatin1Char(';'));
}
const QString TargetModel::targetName(const QModelIndex &itemIndex)
......
......@@ -68,6 +68,7 @@ public Q_SLOTS:
static const QString command(const QModelIndex &itemIndex);
static const QString cmdName(const QModelIndex &itemIndex);
static const QString workDir(const QModelIndex &itemIndex);
static const QStringList searchPaths(const QModelIndex &itemIndex);
static const QString targetName(const QModelIndex &itemIndex);
Q_SIGNALS:
......
......@@ -24,6 +24,8 @@
#include "plugin_katebuild.h"
#include "AppOutput.h"
#include "hostprocess.h"
#include <cassert>
......@@ -59,6 +61,9 @@
#include <kterminallauncherjob.h>
#include <ktexteditor_utils.h>
#include <kde_terminal_interface.h>
#include <kparts/part.h>
K_PLUGIN_FACTORY_WITH_JSON(KateBuildPluginFactory, "katebuildplugin.json", registerPlugin<KateBuildPlugin>();)
static const QString DefConfigCmd = QStringLiteral("cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_EXPORT_COMPILE_COMMANDS=1 ../");
......@@ -181,6 +186,13 @@ KateBuildView::KateBuildView(KTextEditor::Plugin *plugin, KTextEditor::MainWindo
m_targetsUi = new TargetsUi(this, m_buildUi.u_tabWidget);
m_buildUi.u_tabWidget->insertTab(0, m_targetsUi, i18nc("Tab label", "Target Settings"));
m_buildUi.u_tabWidget->setCurrentWidget(m_targetsUi);
m_buildUi.u_tabWidget->setTabsClosable(true);
m_buildUi.u_tabWidget->tabBar()->tabButton(0, QTabBar::RightSide)->hide();
m_buildUi.u_tabWidget->tabBar()->tabButton(1, QTabBar::RightSide)->hide();
connect(m_buildUi.u_tabWidget, &QTabWidget::tabCloseRequested, this, [this](int index) {
// FIXME check if the process is still running
m_buildUi.u_tabWidget->widget(index)->deleteLater();
});
m_buildWidget->installEventFilter(this);
......@@ -299,8 +311,6 @@ void KateBuildView::readSessionConfig(const KConfigGroup &cg)
addProjectTarget();
m_targetsUi->targetsView->expandAll();
m_targetsUi->targetsView->resizeColumnToContents(0);
m_targetsUi->targetsView->resizeColumnToContents(1);
// pre-select the last active target or the first target of the first set
int prevTargetSetRow = cg.readEntry(QStringLiteral("Active Target Index"), 0);
......@@ -923,11 +933,6 @@ void KateBuildView::slotSelectTarget()
/******************************************************************/
bool KateBuildView::buildCurrentTarget()
{
if (m_proc.state() != QProcess::NotRunning) {
displayBuildResult(i18n("Already building..."), KTextEditor::Message::Warning);
return false;
}
const QFileInfo docFInfo(docUrl().toLocalFile()); // docUrl() saves the current document
QModelIndex ind = m_targetsUi->targetsView->currentIndex();
......@@ -939,8 +944,8 @@ bool KateBuildView::buildCurrentTarget()
QString buildCmd = TargetModel::command(ind);
QString cmdName = TargetModel::cmdName(ind);
m_searchPaths = TargetModel::workDir(ind).split(QLatin1Char(';'));
QString workDir = m_searchPaths.isEmpty() ? QString() : m_searchPaths.first();
m_searchPaths = TargetModel::searchPaths(ind);
QString workDir = TargetModel::workDir(ind);
QString targetSet = TargetModel::targetName(ind);
QString dir = workDir;
......@@ -952,6 +957,15 @@ bool KateBuildView::buildCurrentTarget()
}
}
if (m_proc.state() != QProcess::NotRunning) {
displayBuildResult(i18n("Already building..."), KTextEditor::Message::Warning);
return false;
}
if (m_runAfterBuild && buildCmd.isEmpty()) {
slotRunAfterBuild();
return true;
}
// a single target can serve to build lots of projects with similar directory layout
if (m_projectPluginView) {
const QFileInfo baseDir(m_projectPluginView->property("projectBaseDir").toString());
......@@ -1076,9 +1090,10 @@ void KateBuildView::slotRunAfterBuild()
return;
}
QModelIndex idx = m_previousIndex;
idx = idx.siblingAtColumn(2);
const QString runCmd = idx.data().toString();
QModelIndex runIdx = idx.siblingAtColumn(2);
const QString runCmd = runIdx.data().toString();
if (runCmd.isEmpty()) {
// Nothing to run, and not a problem
return;
}
const QString workDir = TargetModel::workDir(idx);
......@@ -1086,15 +1101,51 @@ void KateBuildView::slotRunAfterBuild()
displayBuildResult(i18n("Cannot execute: %1 No working directory set.", runCmd), KTextEditor::Message::Warning);
return;
}
QObject *projectPluginView = m_win->pluginView(QStringLiteral("kateprojectplugin"));
if (projectPluginView) {
QMetaObject::invokeMethod(projectPluginView, "runCmdInTerminal", Q_ARG(QString, workDir), Q_ARG(QString, runCmd));
} else {
auto *job = new KTerminalLauncherJob(runCmd, this);
connect(job, &KJob::finished, job, &QObject::deleteLater);
job->setWorkingDirectory(workDir);
job->start();
QModelIndex nameIdx = idx.siblingAtColumn(0);
QString name = nameIdx.data().toString();
AppOutput *out = nullptr;
for (int i = 2; i < m_buildUi.u_tabWidget->count(); ++i) {
QString tabToolTip = m_buildUi.u_tabWidget->tabToolTip(i);
if (runCmd == tabToolTip) {
out = qobject_cast<AppOutput *>(m_buildUi.u_tabWidget->widget(i));
if (!out || !out->runningProcess().isEmpty()) {
out = nullptr;
continue;
}
// We have a winner, re-use this tab
m_buildUi.u_tabWidget->setCurrentIndex(i);
break;
}
}
if (!out) {
// This is means we create a new tab
out = new AppOutput();
int tabIndex = m_buildUi.u_tabWidget->addTab(out, name);
m_buildUi.u_tabWidget->setCurrentIndex(tabIndex);
m_buildUi.u_tabWidget->setTabToolTip(tabIndex, runCmd);
m_buildUi.u_tabWidget->setTabIcon(tabIndex, QIcon::fromTheme(QStringLiteral("media-playback-start")));
connect(out, &AppOutput::runningChanhged, this, [this]() {
// Update the tab icon when the run state changes
for (int i = 2; i < m_buildUi.u_tabWidget->count(); ++i) {
AppOutput *tabOut = qobject_cast<AppOutput *>(m_buildUi.u_tabWidget->widget(i));
if (!tabOut) {
continue;
}
if (!tabOut->runningProcess().isEmpty()) {
m_buildUi.u_tabWidget->setTabIcon(i, QIcon::fromTheme(QStringLiteral("media-playback-start")));
} else {
m_buildUi.u_tabWidget->setTabIcon(i, QIcon::fromTheme(QStringLiteral("media-playback-pause")));
}
}
});
}
out->setWorkingDir(workDir);
out->runCommand(runCmd);
m_win->activeView()->setFocus();
}
static void appendPlainTextTo(QPlainTextEdit *edit, const QString &text)
......
......@@ -67,6 +67,7 @@ TargetsUi::TargetsUi(QObject *view, QWidget *parent)
targetsView->header()->setStretchLastSection(false);
targetsView->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
targetsView->header()->setSectionResizeMode(1, QHeaderView::Stretch);
targetsView->header()->setSectionResizeMode(2, QHeaderView::Stretch);
QHBoxLayout *tLayout = new QHBoxLayout();
tLayout->addWidget(targetFilterEdit);
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment