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 "qscriptdebuggerlocalsmodel_p.h" |
41 | #include "qscriptdebuggercommandschedulerjob_p.h" |
42 | #include "qscriptdebuggervalue_p.h" |
43 | #include "qscriptdebuggerresponse_p.h" |
44 | #include "qscriptdebuggerevent_p.h" |
45 | #include "qscriptdebuggervalueproperty_p.h" |
46 | #include "qscriptdebuggercommandschedulerinterface_p.h" |
47 | #include "qscriptdebuggercommandschedulerfrontend_p.h" |
48 | #include "qscriptdebuggerjobschedulerinterface_p.h" |
49 | #include "qscriptdebuggerobjectsnapshotdelta_p.h" |
50 | |
51 | #include "private/qabstractitemmodel_p.h" |
52 | |
53 | #include <QtCore/qdebug.h> |
54 | #include <QtCore/qcoreapplication.h> |
55 | #include <QtCore/qpointer.h> |
56 | #include <QtGui/qbrush.h> |
57 | #include <QtGui/qfont.h> |
58 | |
59 | Q_DECLARE_METATYPE(QScriptDebuggerObjectSnapshotDelta) |
60 | |
61 | QT_BEGIN_NAMESPACE |
62 | |
63 | struct QScriptDebuggerLocalsModelNode |
64 | { |
65 | enum PopulationState { |
66 | NotPopulated, |
67 | Populating, |
68 | Populated |
69 | }; |
70 | |
71 | QScriptDebuggerLocalsModelNode() |
72 | : parent(0), populationState(NotPopulated), snapshotId(-1), changed(false) {} |
73 | |
74 | QScriptDebuggerLocalsModelNode( |
75 | const QScriptDebuggerValueProperty &prop, |
76 | QScriptDebuggerLocalsModelNode *par) |
77 | : property(prop), parent(par), |
78 | populationState(NotPopulated), snapshotId(-1), changed(false) |
79 | { |
80 | parent->children.append(t: this); |
81 | } |
82 | |
83 | ~QScriptDebuggerLocalsModelNode() { qDeleteAll(c: children); } |
84 | |
85 | QScriptDebuggerLocalsModelNode *findChild(const QString &name) |
86 | { |
87 | for (int i = 0; i < children.size(); ++i) { |
88 | QScriptDebuggerLocalsModelNode *child = children.at(i); |
89 | if (child->property.name() == name) |
90 | return child; |
91 | } |
92 | return 0; |
93 | } |
94 | |
95 | QScriptDebuggerValueProperty property; |
96 | QScriptDebuggerLocalsModelNode *parent; |
97 | QList<QScriptDebuggerLocalsModelNode*> children; |
98 | PopulationState populationState; |
99 | int snapshotId; |
100 | bool changed; |
101 | }; |
102 | |
103 | class QScriptDebuggerLocalsModelPrivate |
104 | : public QAbstractItemModelPrivate |
105 | { |
106 | Q_DECLARE_PUBLIC(QScriptDebuggerLocalsModel) |
107 | public: |
108 | QScriptDebuggerLocalsModelPrivate(); |
109 | ~QScriptDebuggerLocalsModelPrivate(); |
110 | |
111 | static QScriptDebuggerLocalsModelPrivate *get(QScriptDebuggerLocalsModel *q); |
112 | |
113 | QModelIndex addTopLevelObject(const QString &name, const QScriptDebuggerValue &object); |
114 | |
115 | QScriptDebuggerLocalsModelNode *nodeFromIndex(const QModelIndex &index) const; |
116 | QModelIndex indexFromNode(QScriptDebuggerLocalsModelNode *node) const; |
117 | bool isTopLevelNode(QScriptDebuggerLocalsModelNode *node) const; |
118 | |
119 | void populateIndex(const QModelIndex &index); |
120 | void reallyPopulateIndex(const QModelIndex &index, |
121 | const QScriptDebuggerValuePropertyList &props); |
122 | void syncIndex(const QModelIndex &index); |
123 | void reallySyncIndex(const QModelIndex &index, |
124 | const QScriptDebuggerObjectSnapshotDelta &delta); |
125 | void syncTopLevelNodes(); |
126 | void removeTopLevelNodes(); |
127 | void emitScopeObjectAvailable(const QModelIndex &index); |
128 | |
129 | void emitDataChanged(const QModelIndex &tl, const QModelIndex &br); |
130 | void removeChild(const QModelIndex &parentIndex, |
131 | QScriptDebuggerLocalsModelNode *parentNode, int row); |
132 | void depopulate(QScriptDebuggerLocalsModelNode *node); |
133 | void repopulate(QScriptDebuggerLocalsModelNode *node); |
134 | void addChildren(const QModelIndex &parentIndex, |
135 | QScriptDebuggerLocalsModelNode *parentNode, |
136 | const QScriptDebuggerValuePropertyList &props); |
137 | |
138 | void deleteObjectSnapshots(const QList<qint64> &snapshotIds); |
139 | void deleteAllObjectSnapshots(); |
140 | |
141 | QScriptDebuggerJobSchedulerInterface *jobScheduler; |
142 | QScriptDebuggerCommandSchedulerInterface *commandScheduler; |
143 | QScriptDebuggerLocalsModelNode *invisibleRootNode; |
144 | int frameIndex; |
145 | }; |
146 | |
147 | QScriptDebuggerLocalsModelPrivate::QScriptDebuggerLocalsModelPrivate() |
148 | { |
149 | invisibleRootNode = new QScriptDebuggerLocalsModelNode(); |
150 | frameIndex = -1; |
151 | } |
152 | |
153 | QScriptDebuggerLocalsModelPrivate::~QScriptDebuggerLocalsModelPrivate() |
154 | { |
155 | delete invisibleRootNode; |
156 | } |
157 | |
158 | void QScriptDebuggerLocalsModelPrivate::emitDataChanged(const QModelIndex &tl, const QModelIndex &br) |
159 | { |
160 | q_func()->dataChanged(topLeft: tl, bottomRight: br); |
161 | } |
162 | |
163 | static QList<qint64> findSnapshotIdsRecursively(QScriptDebuggerLocalsModelNode *root) |
164 | { |
165 | QList<qint64> result; |
166 | if (root->snapshotId == -1) { |
167 | Q_ASSERT(root->children.isEmpty()); |
168 | return result; |
169 | } |
170 | QList<QScriptDebuggerLocalsModelNode*> nodeStack; |
171 | nodeStack.append(t: root); |
172 | while (!nodeStack.isEmpty()) { |
173 | QScriptDebuggerLocalsModelNode *node = nodeStack.takeFirst(); |
174 | result.append(t: node->snapshotId); |
175 | for (int i = 0; i < node->children.count(); ++i) { |
176 | QScriptDebuggerLocalsModelNode *child = node->children.at(i); |
177 | if (child->snapshotId != -1) |
178 | nodeStack.prepend(t: child); |
179 | } |
180 | } |
181 | return result; |
182 | } |
183 | |
184 | void QScriptDebuggerLocalsModelPrivate::removeChild(const QModelIndex &parentIndex, |
185 | QScriptDebuggerLocalsModelNode *parentNode, |
186 | int row) |
187 | { |
188 | Q_Q(QScriptDebuggerLocalsModel); |
189 | q->beginRemoveRows(parent: parentIndex, first: row, last: row); |
190 | QScriptDebuggerLocalsModelNode *child = parentNode->children.takeAt(i: row); |
191 | QList<qint64> snapshotIds = findSnapshotIdsRecursively(root: child); |
192 | delete child; |
193 | q->endRemoveRows(); |
194 | deleteObjectSnapshots(snapshotIds); |
195 | } |
196 | |
197 | void QScriptDebuggerLocalsModelPrivate::depopulate(QScriptDebuggerLocalsModelNode *node) |
198 | { |
199 | Q_Q(QScriptDebuggerLocalsModel); |
200 | bool hasChildren = !node->children.isEmpty(); |
201 | if (hasChildren) |
202 | q->beginRemoveRows(parent: indexFromNode(node), first: 0, last: node->children.count() - 1); |
203 | QList<qint64> snapshotIds = findSnapshotIdsRecursively(root: node); |
204 | qDeleteAll(c: node->children); |
205 | node->children.clear(); |
206 | node->snapshotId = -1; |
207 | node->populationState = QScriptDebuggerLocalsModelNode::NotPopulated; |
208 | if (hasChildren) |
209 | q->endRemoveRows(); |
210 | deleteObjectSnapshots(snapshotIds); |
211 | } |
212 | |
213 | void QScriptDebuggerLocalsModelPrivate::repopulate(QScriptDebuggerLocalsModelNode *node) |
214 | { |
215 | if (node->populationState != QScriptDebuggerLocalsModelNode::Populated) |
216 | return; |
217 | depopulate(node); |
218 | if (node->property.value().type() == QScriptDebuggerValue::ObjectValue) |
219 | populateIndex(index: indexFromNode(node)); |
220 | } |
221 | |
222 | void QScriptDebuggerLocalsModelPrivate::addChildren(const QModelIndex &parentIndex, |
223 | QScriptDebuggerLocalsModelNode *parentNode, |
224 | const QScriptDebuggerValuePropertyList &props) |
225 | { |
226 | Q_Q(QScriptDebuggerLocalsModel); |
227 | if (props.isEmpty()) |
228 | return; |
229 | int first = parentNode->children.size(); |
230 | int last = first + props.size() - 1; |
231 | q->beginInsertRows(parent: parentIndex, first, last); |
232 | for (int i = 0; i < props.size(); ++i) |
233 | new QScriptDebuggerLocalsModelNode(props.at(i), parentNode); |
234 | q->endInsertRows(); |
235 | } |
236 | |
237 | void QScriptDebuggerLocalsModelPrivate::deleteObjectSnapshots(const QList<qint64> &snapshotIds) |
238 | { |
239 | QScriptDebuggerCommandSchedulerFrontend frontend(commandScheduler, 0); |
240 | for (int i = 0; i < snapshotIds.size(); ++i) |
241 | frontend.scheduleDeleteScriptObjectSnapshot(id: snapshotIds.at(i)); |
242 | } |
243 | |
244 | void QScriptDebuggerLocalsModelPrivate::deleteAllObjectSnapshots() |
245 | { |
246 | QList<qint64> snapshotIds; |
247 | for (int i = 0; i < invisibleRootNode->children.count(); ++i) |
248 | snapshotIds += findSnapshotIdsRecursively(root: invisibleRootNode->children.at(i)); |
249 | deleteObjectSnapshots(snapshotIds); |
250 | } |
251 | |
252 | QScriptDebuggerLocalsModelPrivate *QScriptDebuggerLocalsModelPrivate::get(QScriptDebuggerLocalsModel *q) |
253 | { |
254 | return q->d_func(); |
255 | } |
256 | |
257 | namespace { |
258 | |
259 | class SetPropertyJob : public QScriptDebuggerCommandSchedulerJob |
260 | { |
261 | public: |
262 | SetPropertyJob(const QPersistentModelIndex &index, |
263 | const QString &expression, |
264 | QScriptDebuggerCommandSchedulerInterface *scheduler) |
265 | : QScriptDebuggerCommandSchedulerJob(scheduler), |
266 | m_index(index), m_expression(expression), m_state(0) {} |
267 | |
268 | QScriptDebuggerLocalsModelPrivate *model() const |
269 | { |
270 | if (!m_index.isValid()) |
271 | return 0; |
272 | QAbstractItemModel *m = const_cast<QAbstractItemModel*>(m_index.model()); |
273 | QScriptDebuggerLocalsModel *lm = qobject_cast<QScriptDebuggerLocalsModel*>(object: m); |
274 | return QScriptDebuggerLocalsModelPrivate::get(q: lm); |
275 | } |
276 | |
277 | void start() |
278 | { |
279 | if (!m_index.isValid()) { |
280 | // nothing to do, the node has been removed |
281 | return; |
282 | } |
283 | QScriptDebuggerLocalsModelNode *node = model()->nodeFromIndex(index: m_index); |
284 | QScriptDebuggerCommandSchedulerFrontend frontend(commandScheduler(), this); |
285 | frontend.scheduleEvaluate(contextIndex: model()->frameIndex, program: m_expression, |
286 | fileName: QString::fromLatin1(str: "set property '%0' (%1)" ) |
287 | .arg(a: node->property.name()) |
288 | .arg(a: QDateTime::currentDateTime().toString())); |
289 | } |
290 | |
291 | void handleResponse(const QScriptDebuggerResponse &, int) |
292 | { |
293 | switch (m_state) { |
294 | case 0: |
295 | hibernateUntilEvaluateFinished(); |
296 | ++m_state; |
297 | break; |
298 | case 1: |
299 | finish(); |
300 | break; |
301 | } |
302 | } |
303 | |
304 | void evaluateFinished(const QScriptDebuggerValue &result) |
305 | { |
306 | if (!m_index.isValid()) { |
307 | // nothing to do, the node has been removed |
308 | return; |
309 | } |
310 | QScriptDebuggerLocalsModelNode *node = model()->nodeFromIndex(index: m_index); |
311 | Q_ASSERT(node->parent != 0); |
312 | QScriptDebuggerValue object = node->parent->property.value(); |
313 | QScriptDebuggerCommandSchedulerFrontend frontend(commandScheduler(), this); |
314 | frontend.scheduleSetScriptValueProperty(object, name: node->property.name(), value: result); |
315 | } |
316 | |
317 | private: |
318 | QPersistentModelIndex m_index; |
319 | QString m_expression; |
320 | int m_state; |
321 | }; |
322 | |
323 | } // namespace |
324 | |
325 | QScriptDebuggerLocalsModelNode *QScriptDebuggerLocalsModelPrivate::nodeFromIndex( |
326 | const QModelIndex &index) const |
327 | { |
328 | if (!index.isValid()) |
329 | return invisibleRootNode; |
330 | return static_cast<QScriptDebuggerLocalsModelNode*>(index.internalPointer()); |
331 | } |
332 | |
333 | QModelIndex QScriptDebuggerLocalsModelPrivate::indexFromNode( |
334 | QScriptDebuggerLocalsModelNode *node) const |
335 | { |
336 | if (!node || (node == invisibleRootNode)) |
337 | return QModelIndex(); |
338 | QScriptDebuggerLocalsModelNode *par = node->parent; |
339 | int row = par ? par->children.indexOf(t: node) : 0; |
340 | return createIndex(row, column: 0, data: node); |
341 | } |
342 | |
343 | bool QScriptDebuggerLocalsModelPrivate::isTopLevelNode(QScriptDebuggerLocalsModelNode *node) const |
344 | { |
345 | return node && (node->parent == invisibleRootNode); |
346 | } |
347 | |
348 | namespace { |
349 | |
350 | class PopulateModelIndexJob : public QScriptDebuggerCommandSchedulerJob |
351 | { |
352 | public: |
353 | PopulateModelIndexJob(const QPersistentModelIndex &index, |
354 | QScriptDebuggerCommandSchedulerInterface *scheduler) |
355 | : QScriptDebuggerCommandSchedulerJob(scheduler), |
356 | m_index(index), m_state(0) |
357 | { } |
358 | |
359 | QScriptDebuggerLocalsModelPrivate *model() const |
360 | { |
361 | if (!m_index.isValid()) |
362 | return 0; |
363 | QAbstractItemModel *m = const_cast<QAbstractItemModel*>(m_index.model()); |
364 | QScriptDebuggerLocalsModel *lm = qobject_cast<QScriptDebuggerLocalsModel*>(object: m); |
365 | return QScriptDebuggerLocalsModelPrivate::get(q: lm); |
366 | } |
367 | |
368 | void start() |
369 | { |
370 | if (!m_index.isValid()) { |
371 | // nothing to do, the node has been removed |
372 | finish(); |
373 | return; |
374 | } |
375 | QScriptDebuggerCommandSchedulerFrontend frontend(commandScheduler(), this); |
376 | frontend.scheduleNewScriptObjectSnapshot(); |
377 | } |
378 | |
379 | void handleResponse(const QScriptDebuggerResponse &response, |
380 | int) |
381 | { |
382 | if (!m_index.isValid()) { |
383 | // the node has been removed |
384 | finish(); |
385 | return; |
386 | } |
387 | switch (m_state) { |
388 | case 0: { |
389 | QScriptDebuggerLocalsModelNode *node = model()->nodeFromIndex(index: m_index); |
390 | Q_ASSERT(node->populationState == QScriptDebuggerLocalsModelNode::Populating); |
391 | node->snapshotId = response.resultAsInt(); |
392 | QScriptDebuggerCommandSchedulerFrontend frontend(commandScheduler(), this); |
393 | frontend.scheduleScriptObjectSnapshotCapture(id: node->snapshotId, object: node->property.value()); |
394 | ++m_state; |
395 | } break; |
396 | case 1: { |
397 | QScriptDebuggerObjectSnapshotDelta delta; |
398 | delta = qvariant_cast<QScriptDebuggerObjectSnapshotDelta>(v: response.result()); |
399 | Q_ASSERT(delta.removedProperties.isEmpty()); |
400 | Q_ASSERT(delta.changedProperties.isEmpty()); |
401 | QScriptDebuggerValuePropertyList props = delta.addedProperties; |
402 | model()->reallyPopulateIndex(index: m_index, props); |
403 | finish(); |
404 | } break; |
405 | } |
406 | } |
407 | |
408 | private: |
409 | QPersistentModelIndex m_index; |
410 | int m_state; |
411 | }; |
412 | |
413 | } // namespace |
414 | |
415 | void QScriptDebuggerLocalsModelPrivate::populateIndex( |
416 | const QModelIndex &index) |
417 | { |
418 | if (!index.isValid()) |
419 | return; |
420 | QScriptDebuggerLocalsModelNode *node = nodeFromIndex(index); |
421 | if (node->populationState != QScriptDebuggerLocalsModelNode::NotPopulated) |
422 | return; |
423 | if (node->property.value().type() != QScriptDebuggerValue::ObjectValue) |
424 | return; |
425 | node->populationState = QScriptDebuggerLocalsModelNode::Populating; |
426 | QScriptDebuggerJob *job = new PopulateModelIndexJob(index, commandScheduler); |
427 | jobScheduler->scheduleJob(job); |
428 | } |
429 | |
430 | void QScriptDebuggerLocalsModelPrivate::reallyPopulateIndex( |
431 | const QModelIndex &index, |
432 | const QScriptDebuggerValuePropertyList &props) |
433 | { |
434 | if (!index.isValid()) |
435 | return; |
436 | QScriptDebuggerLocalsModelNode *node = nodeFromIndex(index); |
437 | Q_ASSERT(node->populationState == QScriptDebuggerLocalsModelNode::Populating); |
438 | node->populationState = QScriptDebuggerLocalsModelNode::Populated; |
439 | addChildren(parentIndex: index, parentNode: node, props); |
440 | } |
441 | |
442 | QScriptDebuggerLocalsModel::QScriptDebuggerLocalsModel( |
443 | QScriptDebuggerJobSchedulerInterface *jobScheduler, |
444 | QScriptDebuggerCommandSchedulerInterface *commandScheduler, |
445 | QObject *parent) |
446 | : QAbstractItemModel(*new QScriptDebuggerLocalsModelPrivate, parent) |
447 | { |
448 | Q_D(QScriptDebuggerLocalsModel); |
449 | d->jobScheduler = jobScheduler; |
450 | d->commandScheduler = commandScheduler; |
451 | } |
452 | |
453 | QScriptDebuggerLocalsModel::~QScriptDebuggerLocalsModel() |
454 | { |
455 | } |
456 | |
457 | QModelIndex QScriptDebuggerLocalsModelPrivate::addTopLevelObject(const QString &name, const QScriptDebuggerValue &object) |
458 | { |
459 | Q_Q(QScriptDebuggerLocalsModel); |
460 | QScriptDebuggerLocalsModelNode *node = invisibleRootNode->findChild(name); |
461 | if (node) |
462 | return indexFromNode(node); |
463 | QScriptDebuggerValueProperty prop(name, object, |
464 | QString::fromLatin1(str: "" ), // ### string representation of object |
465 | QScriptValue::PropertyFlags{}); |
466 | int rowIndex = invisibleRootNode->children.size(); |
467 | q->beginInsertRows(parent: QModelIndex(), first: rowIndex, last: rowIndex); |
468 | node = new QScriptDebuggerLocalsModelNode(prop, invisibleRootNode); |
469 | q->endInsertRows(); |
470 | return indexFromNode(node); |
471 | } |
472 | |
473 | namespace { |
474 | |
475 | class InitModelJob : public QScriptDebuggerCommandSchedulerJob |
476 | { |
477 | public: |
478 | InitModelJob(QScriptDebuggerLocalsModel *model, |
479 | int frameIndex, |
480 | QScriptDebuggerCommandSchedulerInterface *scheduler) |
481 | : QScriptDebuggerCommandSchedulerJob(scheduler), |
482 | m_model(model), m_frameIndex(frameIndex), m_state(0) |
483 | { } |
484 | |
485 | void start() |
486 | { |
487 | if (!m_model) { |
488 | // Model has been deleted. |
489 | finish(); |
490 | return; |
491 | } |
492 | QScriptDebuggerCommandSchedulerFrontend frontend(commandScheduler(), this); |
493 | frontend.scheduleGetScopeChain(contextIndex: m_frameIndex); |
494 | } |
495 | |
496 | void handleResponse(const QScriptDebuggerResponse &response, |
497 | int) |
498 | { |
499 | if (!m_model) { |
500 | // Model has been deleted. |
501 | finish(); |
502 | return; |
503 | } |
504 | QScriptDebuggerCommandSchedulerFrontend frontend(commandScheduler(), this); |
505 | QScriptDebuggerLocalsModelPrivate *model_d = QScriptDebuggerLocalsModelPrivate::get(q: m_model); |
506 | switch (m_state) { |
507 | case 0: { |
508 | QScriptDebuggerValueList scopeChain = response.resultAsScriptValueList(); |
509 | for (int i = 0; i < scopeChain.size(); ++i) { |
510 | const QScriptDebuggerValue &scopeObject = scopeChain.at(i); |
511 | QString name = QString::fromLatin1(str: "Scope" ); |
512 | if (i > 0) |
513 | name.append(s: QString::fromLatin1(str: " (%0)" ).arg(a: i)); |
514 | QModelIndex index = model_d->addTopLevelObject(name, object: scopeObject); |
515 | if (i == 0) |
516 | model_d->emitScopeObjectAvailable(index); |
517 | } |
518 | frontend.scheduleGetThisObject(contextIndex: m_frameIndex); |
519 | ++m_state; |
520 | } break; |
521 | case 1: { |
522 | QScriptDebuggerValue thisObject = response.resultAsScriptValue(); |
523 | model_d->addTopLevelObject(name: QLatin1String("this" ), object: thisObject); |
524 | finish(); |
525 | } break; |
526 | } |
527 | } |
528 | |
529 | private: |
530 | QPointer<QScriptDebuggerLocalsModel> m_model; |
531 | int m_frameIndex; |
532 | int m_state; |
533 | }; |
534 | |
535 | } // namespace |
536 | |
537 | void QScriptDebuggerLocalsModel::init(int frameIndex) |
538 | { |
539 | Q_D(QScriptDebuggerLocalsModel); |
540 | d->frameIndex = frameIndex; |
541 | QScriptDebuggerJob *job = new InitModelJob(this, frameIndex, d->commandScheduler); |
542 | d->jobScheduler->scheduleJob(job); |
543 | } |
544 | |
545 | namespace { |
546 | |
547 | class SyncModelJob : public QScriptDebuggerCommandSchedulerJob |
548 | { |
549 | public: |
550 | SyncModelJob(QScriptDebuggerLocalsModel *model, |
551 | int frameIndex, |
552 | QScriptDebuggerCommandSchedulerInterface *scheduler) |
553 | : QScriptDebuggerCommandSchedulerJob(scheduler), |
554 | m_model(model), m_frameIndex(frameIndex), m_state(0) |
555 | { } |
556 | |
557 | void start() |
558 | { |
559 | if (!m_model) { |
560 | // Model has been deleted. |
561 | finish(); |
562 | return; |
563 | } |
564 | QScriptDebuggerCommandSchedulerFrontend frontend(commandScheduler(), this); |
565 | frontend.scheduleGetScopeChain(contextIndex: m_frameIndex); |
566 | } |
567 | |
568 | void handleResponse(const QScriptDebuggerResponse &response, |
569 | int) |
570 | { |
571 | if (!m_model) { |
572 | // Model has been deleted. |
573 | finish(); |
574 | return; |
575 | } |
576 | QScriptDebuggerCommandSchedulerFrontend frontend(commandScheduler(), this); |
577 | switch (m_state) { |
578 | case 0: { |
579 | QScriptDebuggerValueList scopeChain = response.resultAsScriptValueList(); |
580 | m_topLevelObjects << scopeChain; |
581 | frontend.scheduleGetThisObject(contextIndex: m_frameIndex); |
582 | ++m_state; |
583 | } break; |
584 | case 1: { |
585 | QScriptDebuggerLocalsModelPrivate *model_d = QScriptDebuggerLocalsModelPrivate::get(q: m_model); |
586 | QScriptDebuggerValue thisObject = response.resultAsScriptValue(); |
587 | m_topLevelObjects.append(t: thisObject); |
588 | bool equal = (m_topLevelObjects.size() == model_d->invisibleRootNode->children.size()); |
589 | for (int i = 0; equal && (i < m_topLevelObjects.size()); ++i) { |
590 | const QScriptDebuggerValue &object = m_topLevelObjects.at(i); |
591 | equal = (object == model_d->invisibleRootNode->children.at(i)->property.value()); |
592 | } |
593 | if (!equal) { |
594 | // the scope chain and/or this-object changed, so invalidate the model. |
595 | // we could try to be more clever, i.e. figure out |
596 | // exactly which objects were popped/pushed |
597 | model_d->removeTopLevelNodes(); |
598 | for (int j = 0; j < m_topLevelObjects.size(); ++j) { |
599 | const QScriptDebuggerValue &object = m_topLevelObjects.at(i: j); |
600 | QString name; |
601 | if (j == m_topLevelObjects.size()-1) { |
602 | name = QString::fromLatin1(str: "this" ); |
603 | } else { |
604 | name = QString::fromLatin1(str: "Scope" ); |
605 | if (j > 0) |
606 | name.append(s: QString::fromLatin1(str: " (%0)" ).arg(a: j)); |
607 | } |
608 | QModelIndex index = model_d->addTopLevelObject(name, object); |
609 | if (j == 0) |
610 | model_d->emitScopeObjectAvailable(index); |
611 | } |
612 | } else { |
613 | model_d->syncTopLevelNodes(); |
614 | } |
615 | finish(); |
616 | } break; |
617 | } |
618 | } |
619 | |
620 | private: |
621 | QPointer<QScriptDebuggerLocalsModel> m_model; |
622 | int m_frameIndex; |
623 | int m_state; |
624 | QScriptDebuggerValueList m_topLevelObjects; |
625 | }; |
626 | |
627 | } // namespace |
628 | |
629 | void QScriptDebuggerLocalsModel::sync(int frameIndex) |
630 | { |
631 | Q_D(QScriptDebuggerLocalsModel); |
632 | d->frameIndex = frameIndex; |
633 | QScriptDebuggerJob *job = new SyncModelJob(this, frameIndex, d->commandScheduler); |
634 | d->jobScheduler->scheduleJob(job); |
635 | } |
636 | |
637 | namespace { |
638 | |
639 | class SyncModelIndexJob : public QScriptDebuggerCommandSchedulerJob |
640 | { |
641 | public: |
642 | SyncModelIndexJob(const QPersistentModelIndex &index, |
643 | QScriptDebuggerCommandSchedulerInterface *scheduler) |
644 | : QScriptDebuggerCommandSchedulerJob(scheduler), |
645 | m_index(index) |
646 | { } |
647 | |
648 | QScriptDebuggerLocalsModelPrivate *model() const |
649 | { |
650 | if (!m_index.isValid()) |
651 | return 0; |
652 | QAbstractItemModel *m = const_cast<QAbstractItemModel*>(m_index.model()); |
653 | QScriptDebuggerLocalsModel *lm = qobject_cast<QScriptDebuggerLocalsModel*>(object: m); |
654 | return QScriptDebuggerLocalsModelPrivate::get(q: lm); |
655 | } |
656 | |
657 | void start() |
658 | { |
659 | if (!m_index.isValid()) { |
660 | // nothing to do, the node has been removed |
661 | finish(); |
662 | return; |
663 | } |
664 | QScriptDebuggerCommandSchedulerFrontend frontend(commandScheduler(), this); |
665 | QScriptDebuggerLocalsModelNode *node = model()->nodeFromIndex(index: m_index); |
666 | frontend.scheduleScriptObjectSnapshotCapture(id: node->snapshotId, object: node->property.value()); |
667 | } |
668 | |
669 | void handleResponse(const QScriptDebuggerResponse &response, |
670 | int) |
671 | { |
672 | QScriptDebuggerObjectSnapshotDelta delta; |
673 | delta = qvariant_cast<QScriptDebuggerObjectSnapshotDelta>(v: response.result()); |
674 | model()->reallySyncIndex(index: m_index, delta); |
675 | finish(); |
676 | } |
677 | |
678 | private: |
679 | QPersistentModelIndex m_index; |
680 | }; |
681 | |
682 | } // namespace |
683 | |
684 | void QScriptDebuggerLocalsModelPrivate::syncIndex(const QModelIndex &index) |
685 | { |
686 | if (!index.isValid()) |
687 | return; |
688 | QScriptDebuggerLocalsModelNode *node = nodeFromIndex(index); |
689 | if (node->populationState != QScriptDebuggerLocalsModelNode::Populated) |
690 | return; |
691 | QScriptDebuggerJob *job = new SyncModelIndexJob(index, commandScheduler); |
692 | jobScheduler->scheduleJob(job); |
693 | } |
694 | |
695 | void QScriptDebuggerLocalsModelPrivate::reallySyncIndex(const QModelIndex &index, |
696 | const QScriptDebuggerObjectSnapshotDelta &delta) |
697 | { |
698 | if (!index.isValid()) |
699 | return; |
700 | QScriptDebuggerLocalsModelNode *node = nodeFromIndex(index); |
701 | // update or remove existing children |
702 | for (int i = 0; i < node->children.count(); ++i) { |
703 | QScriptDebuggerLocalsModelNode *child = node->children.at(i); |
704 | int j; |
705 | for (j = 0; j < delta.changedProperties.count(); ++j) { |
706 | if (child->property.name() == delta.changedProperties.at(i: j).name()) { |
707 | child->property = delta.changedProperties.at(i: j); |
708 | child->changed = true; |
709 | emitDataChanged(tl: index, br: index.sibling(arow: 0, acolumn: 1)); |
710 | repopulate(node: child); |
711 | break; |
712 | } |
713 | } |
714 | if (j != delta.changedProperties.count()) |
715 | continue; // was changed |
716 | for (j = 0; j < delta.removedProperties.count(); ++j) { |
717 | if (child->property.name() == delta.removedProperties.at(i: j)) { |
718 | removeChild(parentIndex: index, parentNode: node, row: i); |
719 | --i; |
720 | break; |
721 | } |
722 | } |
723 | if (j != delta.removedProperties.count()) |
724 | continue; // was removed |
725 | // neither changed nor removed, but its children might be |
726 | if (child->populationState == QScriptDebuggerLocalsModelNode::Populated) { |
727 | QScriptDebuggerJob *job = new SyncModelIndexJob(indexFromNode(node: child), commandScheduler); |
728 | jobScheduler->scheduleJob(job); |
729 | } |
730 | } |
731 | addChildren(parentIndex: index, parentNode: node, props: delta.addedProperties); |
732 | } |
733 | |
734 | void QScriptDebuggerLocalsModelPrivate::syncTopLevelNodes() |
735 | { |
736 | Q_Q(QScriptDebuggerLocalsModel); |
737 | for (int i = 0; i < invisibleRootNode->children.count(); ++i) { |
738 | QModelIndex index = q->index(row: i, column: 0, parent: QModelIndex()); |
739 | syncIndex(index); |
740 | if (i == 0) |
741 | emit q->scopeObjectAvailable(index); |
742 | } |
743 | } |
744 | |
745 | void QScriptDebuggerLocalsModelPrivate::removeTopLevelNodes() |
746 | { |
747 | while (!invisibleRootNode->children.isEmpty()) |
748 | removeChild(parentIndex: QModelIndex(), parentNode: invisibleRootNode, row: 0); |
749 | } |
750 | |
751 | void QScriptDebuggerLocalsModelPrivate::emitScopeObjectAvailable(const QModelIndex &index) |
752 | { |
753 | emit q_func()->scopeObjectAvailable(index); |
754 | } |
755 | |
756 | int QScriptDebuggerLocalsModel::frameIndex() const |
757 | { |
758 | Q_D(const QScriptDebuggerLocalsModel); |
759 | return d->frameIndex; |
760 | } |
761 | |
762 | /*! |
763 | \reimp |
764 | */ |
765 | QModelIndex QScriptDebuggerLocalsModel::index(int row, int column, const QModelIndex &parent) const |
766 | { |
767 | Q_D(const QScriptDebuggerLocalsModel); |
768 | QScriptDebuggerLocalsModelNode *node = d->nodeFromIndex(index: parent); |
769 | if ((row < 0) || (row >= node->children.count())) |
770 | return QModelIndex(); |
771 | return createIndex(arow: row, acolumn: column, adata: node->children.at(i: row)); |
772 | } |
773 | |
774 | /*! |
775 | \reimp |
776 | */ |
777 | QModelIndex QScriptDebuggerLocalsModel::parent(const QModelIndex &index) const |
778 | { |
779 | Q_D(const QScriptDebuggerLocalsModel); |
780 | if (!index.isValid()) |
781 | return QModelIndex(); |
782 | QScriptDebuggerLocalsModelNode *node = d->nodeFromIndex(index); |
783 | return d->indexFromNode(node: node->parent); |
784 | } |
785 | |
786 | /*! |
787 | \reimp |
788 | */ |
789 | int QScriptDebuggerLocalsModel::columnCount(const QModelIndex &) const |
790 | { |
791 | return 2; |
792 | } |
793 | |
794 | /*! |
795 | \reimp |
796 | */ |
797 | int QScriptDebuggerLocalsModel::rowCount(const QModelIndex &parent) const |
798 | { |
799 | Q_D(const QScriptDebuggerLocalsModel); |
800 | // ### need this to make it work with a sortfilterproxymodel (QSFPM is too eager) |
801 | const_cast<QScriptDebuggerLocalsModel*>(this)->fetchMore(parent); |
802 | QScriptDebuggerLocalsModelNode *node = d->nodeFromIndex(index: parent); |
803 | return node ? node->children.count() : 0; |
804 | } |
805 | |
806 | /*! |
807 | \reimp |
808 | */ |
809 | QVariant QScriptDebuggerLocalsModel::data(const QModelIndex &index, int role) const |
810 | { |
811 | Q_D(const QScriptDebuggerLocalsModel); |
812 | if (!index.isValid()) |
813 | return QVariant(); |
814 | QScriptDebuggerLocalsModelNode *node = d->nodeFromIndex(index); |
815 | if (role == Qt::DisplayRole) { |
816 | if (index.column() == 0) |
817 | return node->property.name(); |
818 | else if (index.column() == 1) { |
819 | QString str = node->property.valueAsString(); |
820 | if (str.indexOf(c: QLatin1Char('\n')) != -1) { |
821 | QStringList lines = str.split(sep: QLatin1Char('\n')); |
822 | int lineCount = lines.size(); |
823 | if (lineCount > 1) { |
824 | lines = lines.mid(pos: 0, alength: 1); |
825 | lines.append(t: QString::fromLatin1(str: "(... %0 more lines ...)" ).arg(a: lineCount - 1)); |
826 | } |
827 | str = lines.join(sep: QLatin1String("\n" )); |
828 | } |
829 | return str; |
830 | } |
831 | } else if (role == Qt::EditRole) { |
832 | if ((index.column() == 1) && !d->isTopLevelNode(node)) { |
833 | QString str = node->property.valueAsString(); |
834 | if (node->property.value().type() == QScriptDebuggerValue::StringValue) { |
835 | // escape |
836 | str.replace(c: QLatin1Char('\"'), after: QLatin1String("\\\"" )); |
837 | str.prepend(c: QLatin1Char('\"')); |
838 | str.append(c: QLatin1Char('\"')); |
839 | } |
840 | return str; |
841 | } |
842 | } else if (role == Qt::ToolTipRole) { |
843 | if (index.column() == 1) { |
844 | QString str = node->property.valueAsString(); |
845 | if (str.indexOf(c: QLatin1Char('\n')) != -1) |
846 | return str; |
847 | } |
848 | } |
849 | // ### do this in the delegate |
850 | else if (role == Qt::BackgroundRole) { |
851 | if (d->isTopLevelNode(node)) |
852 | return QBrush(Qt::darkGray); |
853 | } else if (role == Qt::ForegroundRole) { |
854 | if (d->isTopLevelNode(node)) |
855 | return QColor(Qt::white); |
856 | } else if (role == Qt::FontRole) { |
857 | if (d->isTopLevelNode(node) || node->changed) { |
858 | QFont fnt; |
859 | fnt.setBold(true); |
860 | return fnt; |
861 | } |
862 | } |
863 | return QVariant(); |
864 | } |
865 | |
866 | /*! |
867 | \reimp |
868 | */ |
869 | bool QScriptDebuggerLocalsModel::setData(const QModelIndex &index, const QVariant &value, int role) |
870 | { |
871 | Q_D(QScriptDebuggerLocalsModel); |
872 | if (!index.isValid()) |
873 | return false; |
874 | if (role != Qt::EditRole) |
875 | return false; |
876 | QScriptDebuggerLocalsModelNode *node = d->nodeFromIndex(index); |
877 | if (!node) |
878 | return false; |
879 | QString expr = value.toString().trimmed(); |
880 | if (expr.isEmpty()) |
881 | return false; |
882 | QScriptDebuggerJob *job = new SetPropertyJob(index, expr, d->commandScheduler); |
883 | d->jobScheduler->scheduleJob(job); |
884 | return true; |
885 | } |
886 | |
887 | /*! |
888 | \reimp |
889 | */ |
890 | QVariant QScriptDebuggerLocalsModel::(int section, Qt::Orientation orient, int role) const |
891 | { |
892 | if (orient == Qt::Horizontal) { |
893 | if (role == Qt::DisplayRole) { |
894 | if (section == 0) |
895 | return QCoreApplication::translate(context: "QScriptDebuggerLocalsModel" , key: "Name" ); |
896 | else if (section == 1) |
897 | return QCoreApplication::translate(context: "QScriptDebuggerLocalsModel" , key: "Value" ); |
898 | } |
899 | } |
900 | return QVariant(); |
901 | } |
902 | |
903 | /*! |
904 | \reimp |
905 | */ |
906 | Qt::ItemFlags QScriptDebuggerLocalsModel::flags(const QModelIndex &index) const |
907 | { |
908 | Q_D(const QScriptDebuggerLocalsModel); |
909 | if (!index.isValid()) |
910 | return {}; |
911 | Qt::ItemFlags ret = QAbstractItemModel::flags(index); |
912 | if ((index.column() == 1) && index.parent().isValid()) { |
913 | QScriptDebuggerLocalsModelNode *node = d->nodeFromIndex(index); |
914 | if (!(node->property.flags() & QScriptValue::ReadOnly)) |
915 | ret |= Qt::ItemIsEditable; |
916 | } |
917 | return ret; |
918 | } |
919 | |
920 | /*! |
921 | \reimp |
922 | */ |
923 | bool QScriptDebuggerLocalsModel::hasChildren(const QModelIndex &parent) const |
924 | { |
925 | Q_D(const QScriptDebuggerLocalsModel); |
926 | QScriptDebuggerLocalsModelNode *node = d->nodeFromIndex(index: parent); |
927 | if (!node) |
928 | return false; |
929 | return !node->children.isEmpty() |
930 | || ((node->property.value().type() == QScriptDebuggerValue::ObjectValue) |
931 | && (node->populationState == QScriptDebuggerLocalsModelNode::NotPopulated)); |
932 | } |
933 | |
934 | /*! |
935 | \reimp |
936 | */ |
937 | bool QScriptDebuggerLocalsModel::canFetchMore(const QModelIndex &parent) const |
938 | { |
939 | Q_D(const QScriptDebuggerLocalsModel); |
940 | if (!parent.isValid()) |
941 | return false; |
942 | QScriptDebuggerLocalsModelNode *node = d->nodeFromIndex(index: parent); |
943 | return node |
944 | && (node->property.value().type() == QScriptDebuggerValue::ObjectValue) |
945 | && (node->populationState == QScriptDebuggerLocalsModelNode::NotPopulated); |
946 | } |
947 | |
948 | /*! |
949 | \reimp |
950 | */ |
951 | void QScriptDebuggerLocalsModel::fetchMore(const QModelIndex &parent) |
952 | { |
953 | Q_D(QScriptDebuggerLocalsModel); |
954 | d->populateIndex(index: parent); |
955 | } |
956 | |
957 | QT_END_NAMESPACE |
958 | |