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