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 | |
57 | QT_BEGIN_NAMESPACE |
58 | |
59 | namespace { |
60 | |
61 | class PromptLabel : public QLabel |
62 | { |
63 | public: |
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 | |
83 | class InputEdit : public QLineEdit |
84 | { |
85 | public: |
86 | InputEdit(QWidget *parent = 0) |
87 | : QLineEdit(parent) |
88 | { |
89 | setFrame(false); |
90 | setSizePolicy(hor: QSizePolicy::MinimumExpanding, ver: sizePolicy().verticalPolicy()); |
91 | } |
92 | }; |
93 | |
94 | class CommandLine : public QWidget |
95 | { |
96 | Q_OBJECT |
97 | public: |
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 | |
149 | Q_SIGNALS: |
150 | void lineEntered(const QString &contents); |
151 | void lineEdited(const QString &contents); |
152 | |
153 | private Q_SLOTS: |
154 | void onReturnPressed() |
155 | { |
156 | QString text = inputEdit->text(); |
157 | inputEdit->clear(); |
158 | emit lineEntered(contents: text); |
159 | } |
160 | |
161 | private: |
162 | PromptLabel *promptLabel; |
163 | InputEdit *inputEdit; |
164 | }; |
165 | |
166 | class QScriptDebuggerConsoleWidgetOutputEdit : public QPlainTextEdit |
167 | { |
168 | public: |
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 | |
195 | class QScriptDebuggerConsoleWidgetPrivate |
196 | : public QScriptDebuggerConsoleWidgetInterfacePrivate |
197 | { |
198 | Q_DECLARE_PUBLIC(QScriptDebuggerConsoleWidget) |
199 | public: |
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 | |
214 | QScriptDebuggerConsoleWidgetPrivate::QScriptDebuggerConsoleWidgetPrivate() |
215 | { |
216 | historyIndex = -1; |
217 | } |
218 | |
219 | QScriptDebuggerConsoleWidgetPrivate::~QScriptDebuggerConsoleWidgetPrivate() |
220 | { |
221 | } |
222 | |
223 | void 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 | |
233 | void 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 | |
245 | static 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 |
251 | static 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 | |
264 | void 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 | |
317 | QScriptDebuggerConsoleWidget::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 | |
347 | QScriptDebuggerConsoleWidget::~QScriptDebuggerConsoleWidget() |
348 | { |
349 | } |
350 | |
351 | void 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 | |
384 | void 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 | |
393 | void QScriptDebuggerConsoleWidget::clear() |
394 | { |
395 | Q_D(QScriptDebuggerConsoleWidget); |
396 | d->outputEdit->clear(); |
397 | } |
398 | |
399 | void 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 | |
431 | bool 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 | |
440 | QT_END_NAMESPACE |
441 | |
442 | #include "qscriptdebuggerconsolewidget.moc" |
443 | |
444 | #include "moc_qscriptdebuggerconsolewidget_p.cpp" |
445 | |