File indexing completed on 2024-05-12 04:39:46

0001 /*
0002     SPDX-FileCopyrightText: 2003 John Birch <jbb@kdevelop.org>
0003     SPDX-FileCopyrightText: 2006 Vladimir Prus <ghost@cs.msu.su>
0004     SPDX-FileCopyrightText: 2007 Hamish Rodda <rodda@kde.org>
0005     SPDX-FileCopyrightText: 2016 Aetf <aetf@unlimitedcodeworks.xyz>
0006 
0007     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0008 */
0009 
0010 #include "debuggerconsoleview.h"
0011 
0012 #include "debuglog.h"
0013 #include "midebuggerplugin.h"
0014 #include "midebugsession.h"
0015 
0016 #include <interfaces/icore.h>
0017 #include <interfaces/idebugcontroller.h>
0018 
0019 #include <KColorScheme>
0020 #include <KHistoryComboBox>
0021 #include <KLocalizedString>
0022 
0023 #include <QAction>
0024 #include <QEvent>
0025 #include <QHBoxLayout>
0026 #include <QIcon>
0027 #include <QLabel>
0028 #include <QMenu>
0029 #include <QScopedPointer>
0030 #include <QScrollBar>
0031 #include <QStyle>
0032 #include <QTextEdit>
0033 #include <QToolBar>
0034 #include <QVBoxLayout>
0035 #include <QPoint>
0036 
0037 using namespace KDevMI;
0038 
0039 DebuggerConsoleView::DebuggerConsoleView(MIDebuggerPlugin *plugin, QWidget *parent)
0040     : QWidget(parent)
0041     , m_repeatLastCommand(false)
0042     , m_showInternalCommands(false)
0043     , m_cmdEditorHadFocus(false)
0044     , m_maxLines(5000)
0045 {
0046     setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-scripts")));
0047     setWindowTitle(i18nc("@title:window", "Debugger Console"));
0048     setWhatsThis(i18nc("@info:whatsthis",
0049                       "<b>Debugger Console</b><p>"
0050                       "Shows all debugger commands being executed. "
0051                       "You can also issue any other debugger command while debugging.</p>"));
0052 
0053     setupUi();
0054 
0055     m_actRepeat = new QAction(QIcon::fromTheme(QStringLiteral("edit-redo")),
0056                               QString(),
0057                               this);
0058     m_actRepeat->setToolTip(i18nc("@info:tooltip", "Repeat last command when hit Return"));
0059     m_actRepeat->setCheckable(true);
0060     m_actRepeat->setChecked(m_repeatLastCommand);
0061     connect(m_actRepeat, &QAction::toggled, this, &DebuggerConsoleView::toggleRepeat);
0062     m_toolBar->insertAction(m_actCmdEditor, m_actRepeat);
0063 
0064     m_actInterrupt = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-pause")),
0065                                  QString(),
0066                                  this);
0067     m_actInterrupt->setToolTip(i18nc("@info:tooltip", "Pause execution of the app to enter debugger commands"));
0068     connect(m_actInterrupt, &QAction::triggered, this, &DebuggerConsoleView::interruptDebugger);
0069     m_toolBar->insertAction(m_actCmdEditor, m_actInterrupt);
0070     setShowInterrupt(true);
0071 
0072     m_actShowInternal = new QAction(i18nc("@action", "Show Internal Commands"), this);
0073     m_actShowInternal->setCheckable(true);
0074     m_actShowInternal->setChecked(m_showInternalCommands);
0075     m_actShowInternal->setWhatsThis(i18nc("@info:whatsthis",
0076         "Controls if commands issued internally by KDevelop "
0077         "will be shown or not.<br>"
0078         "This option will affect only future commands, it will not "
0079         "add or remove already issued commands from the view."));
0080     connect(m_actShowInternal, &QAction::toggled,
0081             this, &DebuggerConsoleView::toggleShowInternalCommands);
0082 
0083     handleDebuggerStateChange(s_none, s_dbgNotStarted);
0084 
0085     m_updateTimer.setSingleShot(true);
0086     m_updateTimer.setInterval(100);
0087     connect(&m_updateTimer, &QTimer::timeout, this, &DebuggerConsoleView::flushPending);
0088 
0089     connect(plugin->core()->debugController(), &KDevelop::IDebugController::currentSessionChanged,
0090             this, &DebuggerConsoleView::handleSessionChanged);
0091 
0092     connect(plugin, &MIDebuggerPlugin::reset, this, &DebuggerConsoleView::clear);
0093     connect(plugin, &MIDebuggerPlugin::raiseDebuggerConsoleViews,
0094             this, &DebuggerConsoleView::requestRaise);
0095 
0096     handleSessionChanged(plugin->core()->debugController()->currentSession());
0097 
0098     updateColors();
0099 }
0100 
0101 void DebuggerConsoleView::changeEvent(QEvent *event)
0102 {
0103     if (event->type() == QEvent::PaletteChange) {
0104         updateColors();
0105     }
0106 }
0107 
0108 void DebuggerConsoleView::updateColors()
0109 {
0110     KColorScheme scheme(QPalette::Active);
0111     m_stdColor = scheme.foreground(KColorScheme::LinkText).color();
0112     m_errorColor = scheme.foreground(KColorScheme::NegativeText).color();
0113 }
0114 
0115 void DebuggerConsoleView::setupUi()
0116 {
0117     setupToolBar();
0118 
0119     m_textView = new QTextEdit;
0120     m_textView->setReadOnly(true);
0121     m_textView->setContextMenuPolicy(Qt::CustomContextMenu);
0122     connect(m_textView, &QTextEdit::customContextMenuRequested,
0123             this, &DebuggerConsoleView::showContextMenu);
0124 
0125     auto vbox = new QVBoxLayout;
0126     vbox->setContentsMargins(0, 0, 0, 0);
0127     vbox->addWidget(m_textView);
0128     vbox->addWidget(m_toolBar);
0129 
0130     setLayout(vbox);
0131 
0132     m_cmdEditor = new KHistoryComboBox(this);
0133     m_cmdEditor->setDuplicatesEnabled(false);
0134     connect(m_cmdEditor, QOverload<const QString&>::of(&KHistoryComboBox::returnPressed),
0135             this, &DebuggerConsoleView::trySendCommand);
0136 
0137     auto label = new QLabel(i18nc("@label:listbox", "&Command:"), this);
0138     label->setBuddy(m_cmdEditor);
0139 
0140     auto hbox = new QHBoxLayout;
0141     hbox->addWidget(label);
0142     hbox->addWidget(m_cmdEditor);
0143     hbox->setStretchFactor(m_cmdEditor, 1);
0144     hbox->setContentsMargins(0, 0, 0, 0);
0145 
0146     auto cmdEditor = new QWidget(this);
0147     cmdEditor->setLayout(hbox);
0148     m_actCmdEditor = m_toolBar->addWidget(cmdEditor);
0149 }
0150 
0151 void DebuggerConsoleView::setupToolBar()
0152 {
0153     m_toolBar = new QToolBar(this);
0154     int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize);
0155     m_toolBar->setIconSize(QSize(iconSize, iconSize));
0156     m_toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
0157     m_toolBar->setFloatable(false);
0158     m_toolBar->setMovable(false);
0159     m_toolBar->setWindowTitle(i18nc("@title:window", "%1 Command Bar", windowTitle()));
0160     m_toolBar->setContextMenuPolicy(Qt::PreventContextMenu);
0161 
0162     // remove margins, to make command editor nicely aligned with the output
0163     m_toolBar->layout()->setContentsMargins(0, 0, 0, 0);
0164 }
0165 
0166 void DebuggerConsoleView::focusInEvent(QFocusEvent*)
0167 {
0168     m_textView->verticalScrollBar()->setValue(m_textView->verticalScrollBar()->maximum());
0169     m_cmdEditor->setFocus();
0170 }
0171 
0172 DebuggerConsoleView::~DebuggerConsoleView()
0173 {
0174 }
0175 
0176 void DebuggerConsoleView::setShowInterrupt(bool enable)
0177 {
0178     m_actInterrupt->setVisible(enable);
0179 }
0180 
0181 void DebuggerConsoleView::setReplacePrompt(const QString& prompt)
0182 {
0183     m_alterPrompt = prompt;
0184 }
0185 
0186 void DebuggerConsoleView::setShowInternalCommands(bool enable)
0187 {
0188     if (enable != m_showInternalCommands)
0189     {
0190         m_showInternalCommands = enable;
0191 
0192         // Set of strings to show changes, text edit still has old
0193         // set. Refresh.
0194         m_textView->clear();
0195         QStringList& newList = m_showInternalCommands ? m_allOutput : m_userOutput;
0196 
0197         for (const auto &line : newList) {
0198             // Note that color formatting is already applied to 'line'.
0199             appendLine(line);
0200         }
0201     }
0202 }
0203 
0204 void DebuggerConsoleView::showContextMenu(const QPoint &pos)
0205 {
0206     // FIXME: QTextEdit::createStandardContextMenu takes position in document coordinates
0207     // while pos is in QTextEdit::viewport coordinates.
0208     // Seems not a big issue currently as menu content seems position independent, but still better fix
0209     QScopedPointer<QMenu> popup(m_textView->createStandardContextMenu(pos));
0210 
0211     popup->addSeparator();
0212     popup->addAction(m_actShowInternal);
0213 
0214     popup->exec(m_textView->viewport()->mapToGlobal(pos));
0215 }
0216 
0217 void DebuggerConsoleView::toggleRepeat(bool checked)
0218 {
0219     m_repeatLastCommand = checked;
0220 }
0221 
0222 void DebuggerConsoleView::toggleShowInternalCommands(bool checked)
0223 {
0224     setShowInternalCommands(checked);
0225 }
0226 
0227 void DebuggerConsoleView::appendLine(const QString& line)
0228 {
0229     m_pendingOutput += line;
0230 
0231     // To improve performance, we update the view after some delay.
0232     if (!m_updateTimer.isActive())
0233     {
0234         m_updateTimer.start();
0235     }
0236 }
0237 
0238 void DebuggerConsoleView::flushPending()
0239 {
0240     m_textView->setUpdatesEnabled(false);
0241 
0242     QTextDocument *document = m_textView->document();
0243     QTextCursor cursor(document);
0244     cursor.movePosition(QTextCursor::End);
0245     cursor.insertHtml(m_pendingOutput);
0246     m_pendingOutput.clear();
0247 
0248     m_textView->verticalScrollBar()->setValue(m_textView->verticalScrollBar()->maximum());
0249     m_textView->setUpdatesEnabled(true);
0250     m_textView->update();
0251     if (m_cmdEditorHadFocus) {
0252         m_cmdEditor->setFocus();
0253     }
0254 }
0255 
0256 void DebuggerConsoleView::clear()
0257 {
0258     if (m_textView)
0259         m_textView->clear();
0260 
0261     if (m_cmdEditor)
0262         m_cmdEditor->clear();
0263 
0264     m_userOutput.clear();
0265     m_allOutput.clear();
0266 }
0267 
0268 void DebuggerConsoleView::handleDebuggerStateChange(DBGStateFlags, DBGStateFlags newStatus)
0269 {
0270     if (newStatus & s_dbgNotStarted) {
0271         m_actInterrupt->setEnabled(false);
0272         m_cmdEditor->setEnabled(false);
0273         return;
0274     } else {
0275         m_actInterrupt->setEnabled(true);
0276     }
0277 
0278     if (newStatus & s_dbgBusy) {
0279         if (m_cmdEditor->isEnabled()) {
0280             m_cmdEditorHadFocus = m_cmdEditor->hasFocus();
0281         }
0282         m_cmdEditor->setEnabled(false);
0283     } else {
0284         m_cmdEditor->setEnabled(true);
0285     }
0286 }
0287 
0288 QString DebuggerConsoleView::toHtmlEscaped(QString text)
0289 {
0290     text = text.toHtmlEscaped();
0291 
0292     text.replace(QLatin1Char('\n'), QLatin1String("<br>"));
0293     return text;
0294 }
0295 
0296 
0297 QString DebuggerConsoleView::colorify(QString text, const QColor& color)
0298 {
0299     text = QLatin1String("<font color=\"") + color.name() +  QLatin1String("\">") + text + QLatin1String("</font>");
0300     return text;
0301 }
0302 
0303 void DebuggerConsoleView::receivedInternalCommandStdout(const QString& line)
0304 {
0305     receivedStdout(line, true);
0306 }
0307 
0308 void DebuggerConsoleView::receivedUserCommandStdout(const QString& line)
0309 {
0310     receivedStdout(line, false);
0311 }
0312 
0313 void DebuggerConsoleView::receivedStdout(const QString& line, bool internal)
0314 {
0315     QString colored = toHtmlEscaped(line);
0316     if (colored.startsWith(QLatin1String("(gdb)"))) {
0317         if (!m_alterPrompt.isEmpty()) {
0318             colored.replace(0, 5, m_alterPrompt);
0319         }
0320         colored = colorify(colored, m_stdColor);
0321     }
0322 
0323     m_allOutput.append(colored);
0324     trimList(m_allOutput, m_maxLines);
0325 
0326     if (!internal) {
0327         m_userOutput.append(colored);
0328         trimList(m_userOutput, m_maxLines);
0329     }
0330 
0331     if (!internal || m_showInternalCommands)
0332         appendLine(colored);
0333 }
0334 
0335 void DebuggerConsoleView::receivedStderr(const QString& line)
0336 {
0337     QString colored = toHtmlEscaped(line);
0338     colored = colorify(colored, m_errorColor);
0339 
0340     // Errors are shown inside user commands too.
0341     m_allOutput.append(colored);
0342     trimList(m_allOutput, m_maxLines);
0343 
0344     m_userOutput.append(colored);
0345     trimList(m_userOutput, m_maxLines);
0346 
0347     appendLine(colored);
0348 }
0349 
0350 void DebuggerConsoleView::trimList(QStringList& l, int max_size)
0351 {
0352     int length = l.count();
0353     if (length > max_size)
0354     {
0355         for(int to_delete = length - max_size; to_delete; --to_delete)
0356         {
0357             l.erase(l.begin());
0358         }
0359     }
0360 }
0361 
0362 void DebuggerConsoleView::trySendCommand(QString cmd)
0363 {
0364     if (m_repeatLastCommand && cmd.isEmpty()) {
0365         cmd = m_cmdEditor->historyItems().last();
0366     }
0367     if (!cmd.isEmpty())
0368     {
0369         m_cmdEditor->addToHistory(cmd);
0370         m_cmdEditor->clearEditText();
0371 
0372         emit sendCommand(cmd);
0373     }
0374 }
0375 
0376 void DebuggerConsoleView::handleSessionChanged(KDevelop::IDebugSession* s)
0377 {
0378     auto *session = qobject_cast<MIDebugSession*>(s);
0379     if (!session) return;
0380 
0381     connect(this, &DebuggerConsoleView::sendCommand,
0382              session, &MIDebugSession::addUserCommand);
0383     connect(this, &DebuggerConsoleView::interruptDebugger,
0384              session, &MIDebugSession::interruptDebugger);
0385 
0386      connect(session, &MIDebugSession::debuggerInternalCommandOutput,
0387              this, &DebuggerConsoleView::receivedInternalCommandStdout);
0388      connect(session, &MIDebugSession::debuggerUserCommandOutput,
0389              this, &DebuggerConsoleView::receivedUserCommandStdout);
0390      connect(session, &MIDebugSession::debuggerInternalOutput,
0391              this, &DebuggerConsoleView::receivedStderr);
0392 
0393      connect(session, &MIDebugSession::debuggerStateChanged,
0394              this, &DebuggerConsoleView::handleDebuggerStateChange);
0395 
0396      handleDebuggerStateChange(s_none, session->debuggerState());
0397 }
0398 
0399 #include "moc_debuggerconsoleview.cpp"