1 /*
2     SPDX-FileCopyrightText: 2003 John Birch <jbb@kdevelop.org>
3     SPDX-FileCopyrightText: 2006 Vladimir Prus <ghost@cs.msu.su>
4     SPDX-FileCopyrightText: 2007 Hamish Rodda <rodda@kde.org>
5     SPDX-FileCopyrightText: 2016 Aetf <aetf@unlimitedcodeworks.xyz>
6 
7     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
8 */
9 
10 #include "debuggerconsoleview.h"
11 
12 #include "debuglog.h"
13 #include "midebuggerplugin.h"
14 #include "midebugsession.h"
15 
16 #include <interfaces/icore.h>
17 #include <interfaces/idebugcontroller.h>
18 
19 #include <KColorScheme>
20 #include <KHistoryComboBox>
21 #include <KLocalizedString>
22 
23 #include <QAction>
24 #include <QEvent>
25 #include <QHBoxLayout>
26 #include <QIcon>
27 #include <QLabel>
28 #include <QMenu>
29 #include <QScopedPointer>
30 #include <QScrollBar>
31 #include <QStyle>
32 #include <QTextEdit>
33 #include <QToolBar>
34 #include <QVBoxLayout>
35 #include <QPoint>
36 
37 using namespace KDevMI;
38 
DebuggerConsoleView(MIDebuggerPlugin * plugin,QWidget * parent)39 DebuggerConsoleView::DebuggerConsoleView(MIDebuggerPlugin *plugin, QWidget *parent)
40     : QWidget(parent)
41     , m_repeatLastCommand(false)
42     , m_showInternalCommands(false)
43     , m_cmdEditorHadFocus(false)
44     , m_maxLines(5000)
45 {
46     setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-scripts")));
47     setWindowTitle(i18nc("@title:window", "Debugger Console"));
48     setWhatsThis(i18nc("@info:whatsthis",
49                       "<b>Debugger Console</b><p>"
50                       "Shows all debugger commands being executed. "
51                       "You can also issue any other debugger command while debugging.</p>"));
52 
53     setupUi();
54 
55     m_actRepeat = new QAction(QIcon::fromTheme(QStringLiteral("edit-redo")),
56                               QString(),
57                               this);
58     m_actRepeat->setToolTip(i18nc("@info:tooltip", "Repeat last command when hit Return"));
59     m_actRepeat->setCheckable(true);
60     m_actRepeat->setChecked(m_repeatLastCommand);
61     connect(m_actRepeat, &QAction::toggled, this, &DebuggerConsoleView::toggleRepeat);
62     m_toolBar->insertAction(m_actCmdEditor, m_actRepeat);
63 
64     m_actInterrupt = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-pause")),
65                                  QString(),
66                                  this);
67     m_actInterrupt->setToolTip(i18nc("@info:tooltip", "Pause execution of the app to enter debugger commands"));
68     connect(m_actInterrupt, &QAction::triggered, this, &DebuggerConsoleView::interruptDebugger);
69     m_toolBar->insertAction(m_actCmdEditor, m_actInterrupt);
70     setShowInterrupt(true);
71 
72     m_actShowInternal = new QAction(i18nc("@action", "Show Internal Commands"), this);
73     m_actShowInternal->setCheckable(true);
74     m_actShowInternal->setChecked(m_showInternalCommands);
75     m_actShowInternal->setWhatsThis(i18nc("@info:whatsthis",
76         "Controls if commands issued internally by KDevelop "
77         "will be shown or not.<br>"
78         "This option will affect only future commands, it will not "
79         "add or remove already issued commands from the view."));
80     connect(m_actShowInternal, &QAction::toggled,
81             this, &DebuggerConsoleView::toggleShowInternalCommands);
82 
83     handleDebuggerStateChange(s_none, s_dbgNotStarted);
84 
85     m_updateTimer.setSingleShot(true);
86     m_updateTimer.setInterval(100);
87     connect(&m_updateTimer, &QTimer::timeout, this, &DebuggerConsoleView::flushPending);
88 
89     connect(plugin->core()->debugController(), &KDevelop::IDebugController::currentSessionChanged,
90             this, &DebuggerConsoleView::handleSessionChanged);
91 
92     connect(plugin, &MIDebuggerPlugin::reset, this, &DebuggerConsoleView::clear);
93     connect(plugin, &MIDebuggerPlugin::raiseDebuggerConsoleViews,
94             this, &DebuggerConsoleView::requestRaise);
95 
96     handleSessionChanged(plugin->core()->debugController()->currentSession());
97 
98     updateColors();
99 }
100 
changeEvent(QEvent * event)101 void DebuggerConsoleView::changeEvent(QEvent *event)
102 {
103     if (event->type() == QEvent::PaletteChange) {
104         updateColors();
105     }
106 }
107 
updateColors()108 void DebuggerConsoleView::updateColors()
109 {
110     KColorScheme scheme(QPalette::Active);
111     m_stdColor = scheme.foreground(KColorScheme::LinkText).color();
112     m_errorColor = scheme.foreground(KColorScheme::NegativeText).color();
113 }
114 
setupUi()115 void DebuggerConsoleView::setupUi()
116 {
117     setupToolBar();
118 
119     m_textView = new QTextEdit;
120     m_textView->setReadOnly(true);
121     m_textView->setContextMenuPolicy(Qt::CustomContextMenu);
122     connect(m_textView, &QTextEdit::customContextMenuRequested,
123             this, &DebuggerConsoleView::showContextMenu);
124 
125     auto vbox = new QVBoxLayout;
126     vbox->setContentsMargins(0, 0, 0, 0);
127     vbox->addWidget(m_textView);
128     vbox->addWidget(m_toolBar);
129 
130     setLayout(vbox);
131 
132     m_cmdEditor = new KHistoryComboBox(this);
133     m_cmdEditor->setDuplicatesEnabled(false);
134     connect(m_cmdEditor, QOverload<const QString&>::of(&KHistoryComboBox::returnPressed),
135             this, &DebuggerConsoleView::trySendCommand);
136 
137     auto label = new QLabel(i18nc("@label:listbox", "&Command:"), this);
138     label->setBuddy(m_cmdEditor);
139 
140     auto hbox = new QHBoxLayout;
141     hbox->addWidget(label);
142     hbox->addWidget(m_cmdEditor);
143     hbox->setStretchFactor(m_cmdEditor, 1);
144     hbox->setContentsMargins(0, 0, 0, 0);
145 
146     auto cmdEditor = new QWidget(this);
147     cmdEditor->setLayout(hbox);
148     m_actCmdEditor = m_toolBar->addWidget(cmdEditor);
149 }
150 
setupToolBar()151 void DebuggerConsoleView::setupToolBar()
152 {
153     m_toolBar = new QToolBar(this);
154     int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize);
155     m_toolBar->setIconSize(QSize(iconSize, iconSize));
156     m_toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
157     m_toolBar->setFloatable(false);
158     m_toolBar->setMovable(false);
159     m_toolBar->setWindowTitle(i18nc("@title:window", "%1 Command Bar", windowTitle()));
160     m_toolBar->setContextMenuPolicy(Qt::PreventContextMenu);
161 
162     // remove margins, to make command editor nicely aligned with the output
163     m_toolBar->layout()->setContentsMargins(0, 0, 0, 0);
164 }
165 
focusInEvent(QFocusEvent *)166 void DebuggerConsoleView::focusInEvent(QFocusEvent*)
167 {
168     m_textView->verticalScrollBar()->setValue(m_textView->verticalScrollBar()->maximum());
169     m_cmdEditor->setFocus();
170 }
171 
~DebuggerConsoleView()172 DebuggerConsoleView::~DebuggerConsoleView()
173 {
174 }
175 
setShowInterrupt(bool enable)176 void DebuggerConsoleView::setShowInterrupt(bool enable)
177 {
178     m_actInterrupt->setVisible(enable);
179 }
180 
setReplacePrompt(const QString & prompt)181 void DebuggerConsoleView::setReplacePrompt(const QString& prompt)
182 {
183     m_alterPrompt = prompt;
184 }
185 
setShowInternalCommands(bool enable)186 void DebuggerConsoleView::setShowInternalCommands(bool enable)
187 {
188     if (enable != m_showInternalCommands)
189     {
190         m_showInternalCommands = enable;
191 
192         // Set of strings to show changes, text edit still has old
193         // set. Refresh.
194         m_textView->clear();
195         QStringList& newList = m_showInternalCommands ? m_allOutput : m_userOutput;
196 
197         for (const auto &line : newList) {
198             // Note that color formatting is already applied to 'line'.
199             appendLine(line);
200         }
201     }
202 }
203 
showContextMenu(const QPoint & pos)204 void DebuggerConsoleView::showContextMenu(const QPoint &pos)
205 {
206     // FIXME: QTextEdit::createStandardContextMenu takes position in document coordinates
207     // while pos is in QTextEdit::viewport coordinates.
208     // Seems not a big issue currently as menu content seems position independent, but still better fix
209     QScopedPointer<QMenu> popup(m_textView->createStandardContextMenu(pos));
210 
211     popup->addSeparator();
212     popup->addAction(m_actShowInternal);
213 
214     popup->exec(m_textView->viewport()->mapToGlobal(pos));
215 }
216 
toggleRepeat(bool checked)217 void DebuggerConsoleView::toggleRepeat(bool checked)
218 {
219     m_repeatLastCommand = checked;
220 }
221 
toggleShowInternalCommands(bool checked)222 void DebuggerConsoleView::toggleShowInternalCommands(bool checked)
223 {
224     setShowInternalCommands(checked);
225 }
226 
appendLine(const QString & line)227 void DebuggerConsoleView::appendLine(const QString& line)
228 {
229     m_pendingOutput += line;
230 
231     // To improve performance, we update the view after some delay.
232     if (!m_updateTimer.isActive())
233     {
234         m_updateTimer.start();
235     }
236 }
237 
flushPending()238 void DebuggerConsoleView::flushPending()
239 {
240     m_textView->setUpdatesEnabled(false);
241 
242     QTextDocument *document = m_textView->document();
243     QTextCursor cursor(document);
244     cursor.movePosition(QTextCursor::End);
245     cursor.insertHtml(m_pendingOutput);
246     m_pendingOutput.clear();
247 
248     m_textView->verticalScrollBar()->setValue(m_textView->verticalScrollBar()->maximum());
249     m_textView->setUpdatesEnabled(true);
250     m_textView->update();
251     if (m_cmdEditorHadFocus) {
252         m_cmdEditor->setFocus();
253     }
254 }
255 
clear()256 void DebuggerConsoleView::clear()
257 {
258     if (m_textView)
259         m_textView->clear();
260 
261     if (m_cmdEditor)
262         m_cmdEditor->clear();
263 
264     m_userOutput.clear();
265     m_allOutput.clear();
266 }
267 
handleDebuggerStateChange(DBGStateFlags,DBGStateFlags newStatus)268 void DebuggerConsoleView::handleDebuggerStateChange(DBGStateFlags, DBGStateFlags newStatus)
269 {
270     if (newStatus & s_dbgNotStarted) {
271         m_actInterrupt->setEnabled(false);
272         m_cmdEditor->setEnabled(false);
273         return;
274     } else {
275         m_actInterrupt->setEnabled(true);
276     }
277 
278     if (newStatus & s_dbgBusy) {
279         if (m_cmdEditor->isEnabled()) {
280             m_cmdEditorHadFocus = m_cmdEditor->hasFocus();
281         }
282         m_cmdEditor->setEnabled(false);
283     } else {
284         m_cmdEditor->setEnabled(true);
285     }
286 }
287 
toHtmlEscaped(QString text)288 QString DebuggerConsoleView::toHtmlEscaped(QString text)
289 {
290     text = text.toHtmlEscaped();
291 
292     text.replace(QLatin1Char('\n'), QLatin1String("<br>"));
293     return text;
294 }
295 
296 
colorify(QString text,const QColor & color)297 QString DebuggerConsoleView::colorify(QString text, const QColor& color)
298 {
299     text = QLatin1String("<font color=\"") + color.name() +  QLatin1String("\">") + text + QLatin1String("</font>");
300     return text;
301 }
302 
receivedInternalCommandStdout(const QString & line)303 void DebuggerConsoleView::receivedInternalCommandStdout(const QString& line)
304 {
305     receivedStdout(line, true);
306 }
307 
receivedUserCommandStdout(const QString & line)308 void DebuggerConsoleView::receivedUserCommandStdout(const QString& line)
309 {
310     receivedStdout(line, false);
311 }
312 
receivedStdout(const QString & line,bool internal)313 void DebuggerConsoleView::receivedStdout(const QString& line, bool internal)
314 {
315     QString colored = toHtmlEscaped(line);
316     if (colored.startsWith(QLatin1String("(gdb)"))) {
317         if (!m_alterPrompt.isEmpty()) {
318             colored.replace(0, 5, m_alterPrompt);
319         }
320         colored = colorify(colored, m_stdColor);
321     }
322 
323     m_allOutput.append(colored);
324     trimList(m_allOutput, m_maxLines);
325 
326     if (!internal) {
327         m_userOutput.append(colored);
328         trimList(m_userOutput, m_maxLines);
329     }
330 
331     if (!internal || m_showInternalCommands)
332         appendLine(colored);
333 }
334 
receivedStderr(const QString & line)335 void DebuggerConsoleView::receivedStderr(const QString& line)
336 {
337     QString colored = toHtmlEscaped(line);
338     colored = colorify(colored, m_errorColor);
339 
340     // Errors are shown inside user commands too.
341     m_allOutput.append(colored);
342     trimList(m_allOutput, m_maxLines);
343 
344     m_userOutput.append(colored);
345     trimList(m_userOutput, m_maxLines);
346 
347     appendLine(colored);
348 }
349 
trimList(QStringList & l,int max_size)350 void DebuggerConsoleView::trimList(QStringList& l, int max_size)
351 {
352     int length = l.count();
353     if (length > max_size)
354     {
355         for(int to_delete = length - max_size; to_delete; --to_delete)
356         {
357             l.erase(l.begin());
358         }
359     }
360 }
361 
trySendCommand(QString cmd)362 void DebuggerConsoleView::trySendCommand(QString cmd)
363 {
364     if (m_repeatLastCommand && cmd.isEmpty()) {
365         cmd = m_cmdEditor->historyItems().last();
366     }
367     if (!cmd.isEmpty())
368     {
369         m_cmdEditor->addToHistory(cmd);
370         m_cmdEditor->clearEditText();
371 
372         emit sendCommand(cmd);
373     }
374 }
375 
handleSessionChanged(KDevelop::IDebugSession * s)376 void DebuggerConsoleView::handleSessionChanged(KDevelop::IDebugSession* s)
377 {
378     auto *session = qobject_cast<MIDebugSession*>(s);
379     if (!session) return;
380 
381     connect(this, &DebuggerConsoleView::sendCommand,
382              session, &MIDebugSession::addUserCommand);
383     connect(this, &DebuggerConsoleView::interruptDebugger,
384              session, &MIDebugSession::interruptDebugger);
385 
386      connect(session, &MIDebugSession::debuggerInternalCommandOutput,
387              this, &DebuggerConsoleView::receivedInternalCommandStdout);
388      connect(session, &MIDebugSession::debuggerUserCommandOutput,
389              this, &DebuggerConsoleView::receivedUserCommandStdout);
390      connect(session, &MIDebugSession::debuggerInternalOutput,
391              this, &DebuggerConsoleView::receivedStderr);
392 
393      connect(session, &MIDebugSession::debuggerStateChanged,
394              this, &DebuggerConsoleView::handleDebuggerStateChange);
395 
396      handleDebuggerStateChange(s_none, session->debuggerState());
397 }
398