1/****************************************************************************
2**
3** Copyright (C) 2018 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtSCriptTools module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qscriptdebuggerconsolewidget_p.h"
41#include "qscriptdebuggerconsolewidgetinterface_p_p.h"
42#include "qscriptdebuggerconsolehistorianinterface_p.h"
43#include "qscriptcompletionproviderinterface_p.h"
44#include "qscriptcompletiontaskinterface_p.h"
45
46#include <QtCore/qdebug.h>
47#include <QtWidgets/qplaintextedit.h>
48#include <QtWidgets/qlabel.h>
49#include <QtWidgets/qlineedit.h>
50#include <QtWidgets/qlistview.h>
51#include <QtWidgets/qscrollbar.h>
52#include <QtWidgets/qboxlayout.h>
53#include <QtWidgets/qcompleter.h>
54
55#include <algorithm>
56
57QT_BEGIN_NAMESPACE
58
59namespace {
60
61class PromptLabel : public QLabel
62{
63public:
64 PromptLabel(QWidget *parent = 0)
65 : QLabel(parent)
66 {
67 setFrameShape(QFrame::NoFrame);
68 setIndent(2);
69 setMargin(2);
70 setSizePolicy(hor: QSizePolicy::Minimum, ver: sizePolicy().verticalPolicy());
71 setAlignment(Qt::AlignHCenter);
72#ifndef QT_NO_STYLE_STYLESHEET
73 setStyleSheet(QLatin1String("background: white;"));
74#endif
75 }
76
77 QSize sizeHint() const {
78 QFontMetrics fm(font());
79 return fm.size(flags: 0, str: text()) + QSize(8, 0);
80 }
81};
82
83class InputEdit : public QLineEdit
84{
85public:
86 InputEdit(QWidget *parent = 0)
87 : QLineEdit(parent)
88 {
89 setFrame(false);
90 setSizePolicy(hor: QSizePolicy::MinimumExpanding, ver: sizePolicy().verticalPolicy());
91 }
92};
93
94class CommandLine : public QWidget
95{
96 Q_OBJECT
97public:
98 CommandLine(QWidget *parent = 0)
99 : QWidget(parent)
100 {
101 promptLabel = new PromptLabel();
102 inputEdit = new InputEdit();
103 QHBoxLayout *hbox = new QHBoxLayout(this);
104 hbox->setSpacing(0);
105 hbox->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
106 hbox->addWidget(promptLabel);
107 hbox->addWidget(inputEdit);
108
109 QObject::connect(sender: inputEdit, SIGNAL(returnPressed()),
110 receiver: this, SLOT(onReturnPressed()));
111 QObject::connect(sender: inputEdit, SIGNAL(textEdited(QString)),
112 receiver: this, SIGNAL(lineEdited(QString)));
113
114 setFocusProxy(inputEdit);
115 }
116
117 QString prompt() const
118 {
119 return promptLabel->text();
120 }
121 void setPrompt(const QString &prompt)
122 {
123 promptLabel->setText(prompt);
124 }
125
126 QString input() const
127 {
128 return inputEdit->text();
129 }
130 void setInput(const QString &input)
131 {
132 inputEdit->setText(input);
133 }
134
135 int cursorPosition() const
136 {
137 return inputEdit->cursorPosition();
138 }
139 void setCursorPosition(int position)
140 {
141 inputEdit->setCursorPosition(position);
142 }
143
144 QWidget *editor() const
145 {
146 return inputEdit;
147 }
148
149Q_SIGNALS:
150 void lineEntered(const QString &contents);
151 void lineEdited(const QString &contents);
152
153private Q_SLOTS:
154 void onReturnPressed()
155 {
156 QString text = inputEdit->text();
157 inputEdit->clear();
158 emit lineEntered(contents: text);
159 }
160
161private:
162 PromptLabel *promptLabel;
163 InputEdit *inputEdit;
164};
165
166class QScriptDebuggerConsoleWidgetOutputEdit : public QPlainTextEdit
167{
168public:
169 QScriptDebuggerConsoleWidgetOutputEdit(QWidget *parent = 0)
170 : QPlainTextEdit(parent)
171 {
172 setFrameShape(QFrame::NoFrame);
173 setReadOnly(true);
174// ### there's no context menu when the edit can't have focus,
175// even though you can select text in it.
176// setFocusPolicy(Qt::NoFocus);
177 setMaximumBlockCount(2500);
178 }
179
180 void scrollToBottom()
181 {
182 QScrollBar *bar = verticalScrollBar();
183 bar->setValue(bar->maximum());
184 }
185
186 int charactersPerLine() const
187 {
188 QFontMetrics fm(font());
189 return width() / fm.maxWidth();
190 }
191};
192
193} // namespace
194
195class QScriptDebuggerConsoleWidgetPrivate
196 : public QScriptDebuggerConsoleWidgetInterfacePrivate
197{
198 Q_DECLARE_PUBLIC(QScriptDebuggerConsoleWidget)
199public:
200 QScriptDebuggerConsoleWidgetPrivate();
201 ~QScriptDebuggerConsoleWidgetPrivate();
202
203 // private slots
204 void _q_onLineEntered(const QString &contents);
205 void _q_onLineEdited(const QString &contents);
206 void _q_onCompletionTaskFinished();
207
208 CommandLine *commandLine;
209 QScriptDebuggerConsoleWidgetOutputEdit *outputEdit;
210 int historyIndex;
211 QString newInput;
212};
213
214QScriptDebuggerConsoleWidgetPrivate::QScriptDebuggerConsoleWidgetPrivate()
215{
216 historyIndex = -1;
217}
218
219QScriptDebuggerConsoleWidgetPrivate::~QScriptDebuggerConsoleWidgetPrivate()
220{
221}
222
223void QScriptDebuggerConsoleWidgetPrivate::_q_onLineEntered(const QString &contents)
224{
225 Q_Q(QScriptDebuggerConsoleWidget);
226 outputEdit->appendPlainText(text: QString::fromLatin1(str: "%0 %1").arg(a: commandLine->prompt()).arg(a: contents));
227 outputEdit->scrollToBottom();
228 historyIndex = -1;
229 newInput.clear();
230 emit q->lineEntered(contents);
231}
232
233void QScriptDebuggerConsoleWidgetPrivate::_q_onLineEdited(const QString &contents)
234{
235 if (historyIndex != -1) {
236 // ### try to get the bash behavior...
237#if 0
238 historian->changeHistoryAt(historyIndex, contents);
239#endif
240 } else {
241 newInput = contents;
242 }
243}
244
245static bool lengthLessThan(const QString &s1, const QString &s2)
246{
247 return s1.length() < s2.length();
248}
249
250// input must be sorted by length already
251static QString longestCommonPrefix(const QStringList &lst)
252{
253 QString result = lst.last();
254 for (int i = lst.size() - 2; (i >= 0) && !result.isEmpty(); --i) {
255 const QString &s = lst.at(i);
256 int j = 0;
257 for ( ; (j < qMin(a: s.length(), b: result.length())) && (s.at(i: j) == result.at(i: j)); ++j)
258 ;
259 result = result.left(n: j);
260 }
261 return result;
262}
263
264void QScriptDebuggerConsoleWidgetPrivate::_q_onCompletionTaskFinished()
265{
266 QScriptCompletionTaskInterface *task = 0;
267 task = qobject_cast<QScriptCompletionTaskInterface*>(object: q_func()->sender());
268 if (task->resultCount() == 1) {
269 QString completion = task->resultAt(index: 0);
270 completion.append(s: task->appendix());
271 QString tmp = commandLine->input();
272 tmp.remove(i: task->position(), len: task->length());
273 tmp.insert(i: task->position(), s: completion);
274 commandLine->setInput(tmp);
275 } else if (task->resultCount() > 1) {
276 {
277 QStringList lst;
278 for (int i = 0; i < task->resultCount(); ++i)
279 lst.append(t: task->resultAt(index: i).mid(position: task->length()));
280 std::sort(first: lst.begin(), last: lst.end(), comp: lengthLessThan);
281 QString lcp = longestCommonPrefix(lst);
282 if (!lcp.isEmpty()) {
283 QString tmp = commandLine->input();
284 tmp.insert(i: task->position() + task->length(), s: lcp);
285 commandLine->setInput(tmp);
286 }
287 }
288
289 outputEdit->appendPlainText(text: QString::fromLatin1(str: "%0 %1")
290 .arg(a: commandLine->prompt()).arg(a: commandLine->input()));
291 int maxLength = 0;
292 for (int i = 0; i < task->resultCount(); ++i)
293 maxLength = qMax(a: maxLength, b: task->resultAt(index: i).length());
294 Q_ASSERT(maxLength > 0);
295 int tab = 8;
296 int columns = qMax(a: 1, b: outputEdit->charactersPerLine() / (maxLength + tab));
297 QString msg;
298 for (int i = 0; i < task->resultCount(); ++i) {
299 if (i != 0) {
300 if ((i % columns) == 0) {
301 outputEdit->appendPlainText(text: msg);
302 msg.clear();
303 } else {
304 int pad = maxLength + tab - (msg.length() % (maxLength + tab));
305 msg.append(s: QString(pad, QLatin1Char(' ')));
306 }
307 }
308 msg.append(s: task->resultAt(index: i));
309 }
310 if (!msg.isEmpty())
311 outputEdit->appendPlainText(text: msg);
312 outputEdit->scrollToBottom();
313 }
314 task->deleteLater();
315}
316
317QScriptDebuggerConsoleWidget::QScriptDebuggerConsoleWidget(QWidget *parent)
318 : QScriptDebuggerConsoleWidgetInterface(*new QScriptDebuggerConsoleWidgetPrivate, parent, {})
319{
320 Q_D(QScriptDebuggerConsoleWidget);
321 d->commandLine = new CommandLine();
322 d->commandLine->setPrompt(QString::fromLatin1(str: "qsdb>"));
323 d->outputEdit = new QScriptDebuggerConsoleWidgetOutputEdit();
324 QVBoxLayout *vbox = new QVBoxLayout(this);
325 vbox->setSpacing(0);
326 vbox->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
327 vbox->addWidget(d->outputEdit);
328 vbox->addWidget(d->commandLine);
329
330#if 0
331 QString sheet = QString::fromLatin1("background-color: black;"
332 "color: aquamarine;"
333 "font-size: 14px;"
334 "font-family: \"Monospace\"");
335#endif
336#ifndef QT_NO_STYLE_STYLESHEET
337 QString sheet = QString::fromLatin1(str: "font-size: 14px; font-family: \"Monospace\";");
338 setStyleSheet(sheet);
339#endif
340
341 QObject::connect(sender: d->commandLine, SIGNAL(lineEntered(QString)),
342 receiver: this, SLOT(_q_onLineEntered(QString)));
343 QObject::connect(sender: d->commandLine, SIGNAL(lineEdited(QString)),
344 receiver: this, SLOT(_q_onLineEdited(QString)));
345}
346
347QScriptDebuggerConsoleWidget::~QScriptDebuggerConsoleWidget()
348{
349}
350
351void QScriptDebuggerConsoleWidget::message(
352 QtMsgType type, const QString &text, const QString &fileName,
353 int lineNumber, int columnNumber, const QVariant &/*data*/)
354{
355 Q_D(QScriptDebuggerConsoleWidget);
356 QString msg;
357 if (!fileName.isEmpty() || (lineNumber != -1)) {
358 if (!fileName.isEmpty())
359 msg.append(s: fileName);
360 else
361 msg.append(s: QLatin1String("<noname>"));
362 if (lineNumber != -1) {
363 msg.append(c: QLatin1Char(':'));
364 msg.append(s: QString::number(lineNumber));
365 if (columnNumber != -1) {
366 msg.append(c: QLatin1Char(':'));
367 msg.append(s: QString::number(columnNumber));
368 }
369 }
370 msg.append(s: QLatin1String(": "));
371 }
372 msg.append(s: text);
373 QTextCharFormat oldFmt = d->outputEdit->currentCharFormat();
374 QTextCharFormat fmt(oldFmt);
375 if (type == QtCriticalMsg) {
376 fmt.setForeground(Qt::red);
377 d->outputEdit->setCurrentCharFormat(fmt);
378 }
379 d->outputEdit->appendPlainText(text: msg);
380 d->outputEdit->setCurrentCharFormat(oldFmt);
381 d->outputEdit->scrollToBottom();
382}
383
384void QScriptDebuggerConsoleWidget::setLineContinuationMode(bool enabled)
385{
386 Q_D(QScriptDebuggerConsoleWidget);
387 QString prompt = enabled
388 ? QString::fromLatin1(str: "....")
389 : QString::fromLatin1(str: "qsdb>");
390 d->commandLine->setPrompt(prompt);
391}
392
393void QScriptDebuggerConsoleWidget::clear()
394{
395 Q_D(QScriptDebuggerConsoleWidget);
396 d->outputEdit->clear();
397}
398
399void QScriptDebuggerConsoleWidget::keyPressEvent(QKeyEvent *event)
400{
401 Q_D(QScriptDebuggerConsoleWidget);
402 if (event->key() == Qt::Key_Up) {
403 if (d->historyIndex+1 == d->historian->historyCount())
404 return;
405 QString cmd = d->historian->historyAt(index: ++d->historyIndex);
406 d->commandLine->setInput(cmd);
407 } else if (event->key() == Qt::Key_Down) {
408 if (d->historyIndex == -1) {
409 // nothing to do
410 } else if (d->historyIndex == 0) {
411 d->commandLine->setInput(d->newInput);
412 --d->historyIndex;
413 } else {
414 QString cmd = d->historian->historyAt(index: --d->historyIndex);
415 d->commandLine->setInput(cmd);
416 }
417 } else if (event->key() == Qt::Key_Tab) {
418 QScriptCompletionTaskInterface *task = 0;
419 task = d->completionProvider->createCompletionTask(
420 contents: d->commandLine->input(), cursorPosition: d->commandLine->cursorPosition(),
421 /*frameIndex=*/-1, // current frame
422 options: QScriptCompletionProviderInterface::ConsoleCommandCompletion);
423 QObject::connect(sender: task, SIGNAL(finished()),
424 receiver: this, SLOT(_q_onCompletionTaskFinished()));
425 task->start();
426 } else {
427 QScriptDebuggerConsoleWidgetInterface::keyPressEvent(event);
428 }
429}
430
431bool QScriptDebuggerConsoleWidget::focusNextPrevChild(bool b)
432{
433 Q_D(QScriptDebuggerConsoleWidget);
434 if (d->outputEdit->hasFocus())
435 return QScriptDebuggerConsoleWidgetInterface::focusNextPrevChild(next: b);
436 else
437 return false;
438}
439
440QT_END_NAMESPACE
441
442#include "qscriptdebuggerconsolewidget.moc"
443
444#include "moc_qscriptdebuggerconsolewidget_p.cpp"
445

source code of qtscript/src/scripttools/debugging/qscriptdebuggerconsolewidget.cpp