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 "qscriptdebuggerlocalswidget_p.h" |
41 | #include "qscriptdebuggerlocalswidgetinterface_p_p.h" |
42 | #include "qscriptdebuggerlocalsmodel_p.h" |
43 | #include "qscriptcompletionproviderinterface_p.h" |
44 | #include "qscriptcompletiontaskinterface_p.h" |
45 | |
46 | #include <QtCore/qdebug.h> |
47 | #include <QtWidgets/qheaderview.h> |
48 | #include <QtWidgets/qcompleter.h> |
49 | #include <QtCore/qstringlistmodel.h> |
50 | #include <QtWidgets/qtreeview.h> |
51 | #include <QtWidgets/qboxlayout.h> |
52 | #include <QtCore/qsortfilterproxymodel.h> |
53 | #include <QtWidgets/qlineedit.h> |
54 | #include <QtWidgets/qstyleditemdelegate.h> |
55 | #include <QtGui/qevent.h> |
56 | #include <QtWidgets/qmessagebox.h> |
57 | #include <QtScript/qscriptengine.h> |
58 | |
59 | QT_BEGIN_NAMESPACE |
60 | |
61 | namespace { |
62 | |
63 | class CustomProxyModel : public QSortFilterProxyModel |
64 | { |
65 | public: |
66 | CustomProxyModel(QObject *parent = 0) |
67 | : QSortFilterProxyModel(parent) {} |
68 | |
69 | bool hasChildren(const QModelIndex &parent) const |
70 | { |
71 | if (!sourceModel()) |
72 | return false; |
73 | QModelIndex sourceParent = mapToSource(proxyIndex: parent); |
74 | if (parent.isValid() && !sourceParent.isValid()) |
75 | return false; |
76 | return sourceModel()->hasChildren(parent: sourceParent); |
77 | } |
78 | }; |
79 | |
80 | } // namespace |
81 | |
82 | class QScriptDebuggerLocalsWidgetPrivate |
83 | : public QScriptDebuggerLocalsWidgetInterfacePrivate |
84 | { |
85 | Q_DECLARE_PUBLIC(QScriptDebuggerLocalsWidget) |
86 | public: |
87 | QScriptDebuggerLocalsWidgetPrivate(); |
88 | ~QScriptDebuggerLocalsWidgetPrivate(); |
89 | |
90 | void complete(QLineEdit *le); |
91 | |
92 | // private slots |
93 | void _q_onCompletionTaskFinished(); |
94 | void _q_insertCompletion(const QString &text); |
95 | void _q_expandIndex(const QModelIndex &index); |
96 | |
97 | QTreeView *view; |
98 | QPointer<QLineEdit> completingEditor; |
99 | QCompleter *completer; |
100 | CustomProxyModel *proxy; |
101 | }; |
102 | |
103 | QScriptDebuggerLocalsWidgetPrivate::QScriptDebuggerLocalsWidgetPrivate() |
104 | { |
105 | completingEditor = 0; |
106 | completer = 0; |
107 | proxy = 0; |
108 | } |
109 | |
110 | QScriptDebuggerLocalsWidgetPrivate::~QScriptDebuggerLocalsWidgetPrivate() |
111 | { |
112 | } |
113 | |
114 | void QScriptDebuggerLocalsWidgetPrivate::complete(QLineEdit *le) |
115 | { |
116 | Q_Q(QScriptDebuggerLocalsWidget); |
117 | QScriptCompletionTaskInterface *task = 0; |
118 | // ### need to pass the current frame # |
119 | task = completionProvider->createCompletionTask( |
120 | contents: le->text(), cursorPosition: le->cursorPosition(), |
121 | frameIndex: q->localsModel()->frameIndex(), /*options=*/0); |
122 | QObject::connect(sender: task, SIGNAL(finished()), |
123 | receiver: q, SLOT(_q_onCompletionTaskFinished())); |
124 | completingEditor = le; |
125 | task->start(); |
126 | } |
127 | |
128 | void QScriptDebuggerLocalsWidgetPrivate::_q_onCompletionTaskFinished() |
129 | { |
130 | Q_Q(QScriptDebuggerLocalsWidget); |
131 | QScriptCompletionTaskInterface *task = 0; |
132 | task = qobject_cast<QScriptCompletionTaskInterface*>(object: q_func()->sender()); |
133 | if (!completingEditor) { |
134 | task->deleteLater(); |
135 | return; |
136 | } |
137 | |
138 | if (task->resultCount() == 1) { |
139 | // do the completion right away |
140 | QString completion = task->resultAt(index: 0); |
141 | completion.append(s: task->appendix()); |
142 | QString tmp = completingEditor->text(); |
143 | tmp.remove(i: task->position(), len: task->length()); |
144 | tmp.insert(i: task->position(), s: completion); |
145 | completingEditor->setText(tmp); |
146 | completingEditor = 0; |
147 | } else if (task->resultCount() > 1) { |
148 | // popup completion |
149 | if (!completer) { |
150 | completer = new QCompleter(q); |
151 | completer->setCompletionMode(QCompleter::PopupCompletion); |
152 | completer->setCaseSensitivity(Qt::CaseSensitive); |
153 | completer->setWrapAround(false); |
154 | QObject::connect(sender: completer, SIGNAL(activated(QString)), |
155 | receiver: q, SLOT(_q_insertCompletion(QString))); |
156 | } |
157 | #ifndef QT_NO_STRINGLISTMODEL |
158 | QStringListModel *model = qobject_cast<QStringListModel*>(object: completer->model()); |
159 | if (!model) { |
160 | model = new QStringListModel(q); |
161 | completer->setModel(model); |
162 | } |
163 | QStringList strings; |
164 | for (int i = 0; i < task->resultCount(); ++i) |
165 | strings.append(t: task->resultAt(index: i)); |
166 | model->setStringList(strings); |
167 | #endif |
168 | QString prefix = completingEditor->text().mid(position: task->position(), n: task->length()); |
169 | completer->setCompletionPrefix(prefix); |
170 | completingEditor->setCompleter(completer); |
171 | // we want to handle the insertion ourselves |
172 | QObject::disconnect(sender: completer, signal: 0, receiver: completingEditor, member: 0); |
173 | completer->complete(); |
174 | } |
175 | task->deleteLater(); |
176 | } |
177 | |
178 | void QScriptDebuggerLocalsWidgetPrivate::_q_insertCompletion(const QString &text) |
179 | { |
180 | Q_ASSERT(completingEditor != 0); |
181 | QString tmp = completingEditor->text(); |
182 | tmp.insert(i: completingEditor->cursorPosition(), s: text.mid(position: completer->completionPrefix().length())); |
183 | completingEditor->setText(tmp); |
184 | completingEditor = 0; |
185 | } |
186 | |
187 | void QScriptDebuggerLocalsWidgetPrivate::_q_expandIndex(const QModelIndex &index) |
188 | { |
189 | if (view->model() == index.model()) |
190 | view->expand(index: proxy->mapFromSource(sourceIndex: index)); |
191 | } |
192 | |
193 | class QScriptDebuggerLocalsItemDelegate |
194 | : public QStyledItemDelegate |
195 | { |
196 | Q_OBJECT |
197 | public: |
198 | QScriptDebuggerLocalsItemDelegate(QObject *parent = 0); |
199 | |
200 | QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; |
201 | void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; |
202 | void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; |
203 | |
204 | bool eventFilter(QObject *watched, QEvent *event); |
205 | |
206 | private Q_SLOTS: |
207 | void validateInput(const QString &text) |
208 | { |
209 | QWidget *editor = qobject_cast<QWidget*>(o: sender()); |
210 | QPalette pal = editor->palette(); |
211 | QColor col; |
212 | bool ok = (QScriptEngine::checkSyntax(program: text).state() == QScriptSyntaxCheckResult::Valid); |
213 | if (ok) { |
214 | col = Qt::white; |
215 | } else { |
216 | QScriptSyntaxCheckResult result = QScriptEngine::checkSyntax( |
217 | program: text + QLatin1Char('\n')); |
218 | if (result.state() == QScriptSyntaxCheckResult::Intermediate) |
219 | col = QColor(255, 240, 192); |
220 | else |
221 | col = QColor(255, 102, 102); |
222 | } |
223 | pal.setColor(acg: QPalette::Active, acr: QPalette::Base, acolor: col); |
224 | editor->setPalette(pal); |
225 | } |
226 | }; |
227 | |
228 | QScriptDebuggerLocalsItemDelegate::QScriptDebuggerLocalsItemDelegate( |
229 | QObject *parent) |
230 | : QStyledItemDelegate(parent) |
231 | { |
232 | } |
233 | |
234 | QWidget *QScriptDebuggerLocalsItemDelegate::createEditor( |
235 | QWidget *parent, const QStyleOptionViewItem &option, |
236 | const QModelIndex &index) const |
237 | { |
238 | QWidget *editor = QStyledItemDelegate::createEditor(parent, option, index); |
239 | if (index.column() == 1) { |
240 | // value |
241 | QLineEdit *le = qobject_cast<QLineEdit*>(object: editor); |
242 | if (le) { |
243 | QObject::connect(sender: le, SIGNAL(textEdited(QString)), |
244 | receiver: this, SLOT(validateInput(QString))); |
245 | } |
246 | } |
247 | return editor; |
248 | } |
249 | |
250 | bool QScriptDebuggerLocalsItemDelegate::eventFilter(QObject *watched, QEvent *event) |
251 | { |
252 | QLineEdit *le = qobject_cast<QLineEdit*>(object: watched); |
253 | if (!le) |
254 | return QStyledItemDelegate::eventFilter(object: watched, event); |
255 | |
256 | QScriptDebuggerLocalsWidget *localsWidget = qobject_cast<QScriptDebuggerLocalsWidget*>(object: parent()); |
257 | Q_ASSERT(localsWidget != 0); |
258 | QScriptDebuggerLocalsWidgetPrivate *lvp = |
259 | reinterpret_cast<QScriptDebuggerLocalsWidgetPrivate*>( |
260 | QScriptDebuggerLocalsWidgetPrivate::get(w: localsWidget)); |
261 | |
262 | if ((event->type() == QEvent::FocusIn) && lvp->completingEditor) { |
263 | // because QLineEdit insists on being difficult... |
264 | return true; |
265 | } |
266 | |
267 | if (event->type() != QEvent::KeyPress) |
268 | return QStyledItemDelegate::eventFilter(object: watched, event); |
269 | QKeyEvent *ke = static_cast<QKeyEvent*>(event); |
270 | if ((ke->key() == Qt::Key_Enter) || (ke->key() == Qt::Key_Return)) { |
271 | if (QScriptEngine::checkSyntax(program: le->text()).state() != QScriptSyntaxCheckResult::Valid) { |
272 | // ignore when script contains syntax error |
273 | return true; |
274 | } |
275 | } |
276 | if (ke->key() != Qt::Key_Tab) |
277 | return QStyledItemDelegate::eventFilter(object: watched, event); |
278 | |
279 | // trigger completion |
280 | lvp->complete(le); |
281 | return true; |
282 | } |
283 | |
284 | void QScriptDebuggerLocalsItemDelegate::setModelData( |
285 | QWidget *editor, QAbstractItemModel *model, |
286 | const QModelIndex &index) const |
287 | { |
288 | if (index.column() == 1) { |
289 | // check that the syntax is OK |
290 | QString expression = qobject_cast<QLineEdit*>(object: editor)->text(); |
291 | if (QScriptEngine::checkSyntax(program: expression).state() != QScriptSyntaxCheckResult::Valid) |
292 | return; |
293 | } |
294 | QStyledItemDelegate::setModelData(editor, model, index); |
295 | } |
296 | |
297 | void QScriptDebuggerLocalsItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, |
298 | const QModelIndex &index) const |
299 | { |
300 | QStyledItemDelegate::paint(painter, option, index); |
301 | #if 0 |
302 | QModelIndex parent = index.parent(); |
303 | if (parent.isValid()) { |
304 | QStyledItemDelegate::paint(painter, option, index); |
305 | } else { |
306 | // this is a top-level item. |
307 | const QTreeView *view = qobject_cast<const QTreeView*>(option.widget); |
308 | Q_ASSERT(view != 0); |
309 | |
310 | QStyleOptionButton buttonOption; |
311 | |
312 | buttonOption.state = option.state; |
313 | #ifdef Q_WS_MAC |
314 | buttonOption.state |= QStyle::State_Raised; |
315 | #endif |
316 | buttonOption.state &= ~QStyle::State_HasFocus; |
317 | |
318 | buttonOption.rect = option.rect; |
319 | buttonOption.palette = option.palette; |
320 | buttonOption.features = QStyleOptionButton::None; |
321 | view->style()->drawControl(QStyle::CE_PushButton, &buttonOption, painter, view); |
322 | |
323 | QStyleOption branchOption; |
324 | static const int i = 9; // ### hardcoded in qcommonstyle.cpp |
325 | QRect r = option.rect; |
326 | branchOption.rect = QRect(r.left() + i/2, r.top() + (r.height() - i)/2, i, i); |
327 | branchOption.palette = option.palette; |
328 | branchOption.state = QStyle::State_Children; |
329 | |
330 | if (view->isExpanded(index)) |
331 | branchOption.state |= QStyle::State_Open; |
332 | |
333 | view->style()->drawPrimitive(QStyle::PE_IndicatorBranch, &branchOption, painter, view); |
334 | |
335 | // draw text |
336 | QRect textrect = QRect(r.left() + i*2, r.top(), r.width() - ((5*i)/2), r.height()); |
337 | QString text = elidedText(option.fontMetrics, textrect.width(), Qt::ElideMiddle, |
338 | index.data(Qt::DisplayRole).toString()); |
339 | view->style()->drawItemText(painter, textrect, Qt::AlignCenter, |
340 | option.palette, view->isEnabled(), text); |
341 | } |
342 | #endif |
343 | } |
344 | |
345 | QScriptDebuggerLocalsWidget::QScriptDebuggerLocalsWidget(QWidget *parent) |
346 | : QScriptDebuggerLocalsWidgetInterface(*new QScriptDebuggerLocalsWidgetPrivate, parent, {}) |
347 | { |
348 | Q_D(QScriptDebuggerLocalsWidget); |
349 | d->view = new QTreeView(); |
350 | d->view->setItemDelegate(new QScriptDebuggerLocalsItemDelegate(this)); |
351 | d->view->setEditTriggers(QAbstractItemView::DoubleClicked); |
352 | // d->view->setEditTriggers(QAbstractItemView::NoEditTriggers); |
353 | d->view->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::SelectedClicked); |
354 | d->view->setAlternatingRowColors(true); |
355 | d->view->setSelectionBehavior(QAbstractItemView::SelectRows); |
356 | d->view->setSortingEnabled(true); |
357 | d->view->header()->setDefaultAlignment(Qt::AlignLeft); |
358 | // d->view->header()->setSortIndicatorShown(true); |
359 | // d->view->header()->setResizeMode(QHeaderView::ResizeToContents); |
360 | |
361 | QVBoxLayout *vbox = new QVBoxLayout(this); |
362 | vbox->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0); |
363 | vbox->addWidget(d->view); |
364 | } |
365 | |
366 | QScriptDebuggerLocalsWidget::~QScriptDebuggerLocalsWidget() |
367 | { |
368 | } |
369 | |
370 | /*! |
371 | \reimp |
372 | */ |
373 | QScriptDebuggerLocalsModel *QScriptDebuggerLocalsWidget::localsModel() const |
374 | { |
375 | Q_D(const QScriptDebuggerLocalsWidget); |
376 | if (!d->proxy) |
377 | return 0; |
378 | return qobject_cast<QScriptDebuggerLocalsModel*>(object: d->proxy->sourceModel()); |
379 | } |
380 | |
381 | /*! |
382 | \reimp |
383 | */ |
384 | void QScriptDebuggerLocalsWidget::setLocalsModel(QScriptDebuggerLocalsModel *model) |
385 | { |
386 | Q_D(QScriptDebuggerLocalsWidget); |
387 | if (localsModel()) { |
388 | QObject::disconnect(sender: localsModel(), signal: 0, receiver: d->view, member: 0); |
389 | } |
390 | if (model) { |
391 | QObject::connect(sender: model, SIGNAL(scopeObjectAvailable(QModelIndex)), |
392 | receiver: this, SLOT(_q_expandIndex(QModelIndex))); |
393 | } |
394 | if (!d->proxy) { |
395 | d->proxy = new CustomProxyModel(this); |
396 | d->view->sortByColumn(column: 0, order: Qt::AscendingOrder); |
397 | } |
398 | d->proxy->setSourceModel(model); |
399 | d->view->setModel(d->proxy); |
400 | } |
401 | |
402 | /*! |
403 | \reimp |
404 | */ |
405 | void QScriptDebuggerLocalsWidget::expand(const QModelIndex &index) |
406 | { |
407 | Q_D(QScriptDebuggerLocalsWidget); |
408 | d->view->expand(index); |
409 | d->view->setFirstColumnSpanned(row: index.row(), parent: QModelIndex(), span: true); |
410 | } |
411 | |
412 | QT_END_NAMESPACE |
413 | |
414 | #include "qscriptdebuggerlocalswidget.moc" |
415 | |
416 | #include "moc_qscriptdebuggerlocalswidget_p.cpp" |
417 | |