1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "qdbusviewer.h" |
5 | #include "qdbusmodel.h" |
6 | #include "servicesproxymodel.h" |
7 | #include "propertydialog.h" |
8 | #include "logviewer.h" |
9 | |
10 | #include <QtWidgets/QLineEdit> |
11 | #include <QtWidgets/QVBoxLayout> |
12 | #include <QtWidgets/QSplitter> |
13 | #include <QtWidgets/QInputDialog> |
14 | #include <QtWidgets/QMessageBox> |
15 | #include <QtWidgets/QMenu> |
16 | #include <QtWidgets/QTableWidget> |
17 | #include <QtWidgets/QTreeWidget> |
18 | #include <QtWidgets/QHeaderView> |
19 | |
20 | #include <QtDBus/QDBusConnectionInterface> |
21 | #include <QtDBus/QDBusInterface> |
22 | #include <QtDBus/QDBusMetaType> |
23 | #include <QtDBus/QDBusServiceWatcher> |
24 | |
25 | #include <QtGui/QAction> |
26 | #include <QtGui/QKeyEvent> |
27 | #include <QtGui/QShortcut> |
28 | |
29 | #include <QtCore/QStringListModel> |
30 | #include <QtCore/QMetaProperty> |
31 | #include <QtCore/QSettings> |
32 | |
33 | #include <private/qdbusutil_p.h> |
34 | |
35 | using namespace Qt::StringLiterals; |
36 | |
37 | class QDBusViewModel: public QDBusModel |
38 | { |
39 | public: |
40 | inline QDBusViewModel(const QString &service, const QDBusConnection &connection) |
41 | : QDBusModel(service, connection) |
42 | {} |
43 | |
44 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override |
45 | { |
46 | if (role == Qt::FontRole && itemType(index) == InterfaceItem) { |
47 | QFont f; |
48 | f.setItalic(true); |
49 | return f; |
50 | } |
51 | return QDBusModel::data(index, role); |
52 | } |
53 | }; |
54 | |
55 | class ServicesModel : public QStringListModel |
56 | { |
57 | public: |
58 | explicit ServicesModel(QObject *parent = nullptr) |
59 | : QStringListModel(parent) |
60 | {} |
61 | |
62 | Qt::ItemFlags flags(const QModelIndex &index) const override |
63 | { |
64 | return QStringListModel::flags(index) & ~Qt::ItemIsEditable; |
65 | } |
66 | }; |
67 | |
68 | QDBusViewer::QDBusViewer(const QDBusConnection &connection, QWidget *parent) |
69 | : QWidget(parent), c(connection), objectPathRegExp("\\[ObjectPath: (.*)\\]"_L1 ) |
70 | { |
71 | serviceFilterLine = new QLineEdit(this); |
72 | serviceFilterLine->setPlaceholderText(tr(s: "Search..." )); |
73 | |
74 | // Create model for services list |
75 | servicesModel = new ServicesModel(this); |
76 | // Wrap service list model in proxy for easy filtering and interactive sorting |
77 | servicesProxyModel = new ServicesProxyModel(this); |
78 | servicesProxyModel->setSourceModel(servicesModel); |
79 | servicesProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); |
80 | |
81 | servicesView = new QTableView(this); |
82 | servicesView->installEventFilter(filterObj: this); |
83 | servicesView->setModel(servicesProxyModel); |
84 | // Make services grid view behave like a list view with headers |
85 | servicesView->verticalHeader()->hide(); |
86 | servicesView->horizontalHeader()->setStretchLastSection(true); |
87 | servicesView->setShowGrid(false); |
88 | // Sort service list by default |
89 | servicesView->setSortingEnabled(true); |
90 | servicesView->sortByColumn(column: 0, order: Qt::AscendingOrder); |
91 | |
92 | connect(sender: serviceFilterLine, signal: &QLineEdit::textChanged, context: servicesProxyModel, slot: &QSortFilterProxyModel::setFilterFixedString); |
93 | connect(sender: serviceFilterLine, signal: &QLineEdit::returnPressed, context: this, slot: &QDBusViewer::serviceFilterReturnPressed); |
94 | |
95 | tree = new QTreeView; |
96 | tree->setContextMenuPolicy(Qt::CustomContextMenu); |
97 | |
98 | connect(sender: tree, signal: &QAbstractItemView::activated, context: this, slot: &QDBusViewer::activate); |
99 | |
100 | refreshAction = new QAction(tr(s: "&Refresh" ), tree); |
101 | refreshAction->setData(42); // increase the amount of 42 used as magic number by one |
102 | refreshAction->setShortcut(QKeySequence::Refresh); |
103 | connect(sender: refreshAction, signal: &QAction::triggered, context: this, slot: &QDBusViewer::refreshChildren); |
104 | |
105 | QShortcut *refreshShortcut = new QShortcut(QKeySequence::Refresh, tree); |
106 | connect(sender: refreshShortcut, signal: &QShortcut::activated, context: this, slot: &QDBusViewer::refreshChildren); |
107 | |
108 | QVBoxLayout *layout = new QVBoxLayout(this); |
109 | topSplitter = new QSplitter(Qt::Vertical, this); |
110 | layout->addWidget(topSplitter); |
111 | |
112 | log = new LogViewer; |
113 | connect(sender: log, signal: &QTextBrowser::anchorClicked, context: this, slot: &QDBusViewer::anchorClicked); |
114 | |
115 | splitter = new QSplitter(topSplitter); |
116 | splitter->addWidget(widget: servicesView); |
117 | |
118 | QWidget *servicesWidget = new QWidget; |
119 | QVBoxLayout *servicesLayout = new QVBoxLayout(servicesWidget); |
120 | servicesLayout->setContentsMargins(QMargins()); |
121 | servicesLayout->addWidget(serviceFilterLine); |
122 | servicesLayout->addWidget(servicesView); |
123 | splitter->addWidget(widget: servicesWidget); |
124 | splitter->addWidget(widget: tree); |
125 | |
126 | topSplitter->addWidget(widget: splitter); |
127 | topSplitter->addWidget(widget: log); |
128 | |
129 | connect(sender: servicesView->selectionModel(), signal: &QItemSelectionModel::currentChanged, context: this, slot: &QDBusViewer::serviceChanged); |
130 | connect(sender: tree, signal: &QWidget::customContextMenuRequested, context: this, slot: &QDBusViewer::showContextMenu); |
131 | |
132 | QMetaObject::invokeMethod(object: this, function: &QDBusViewer::refresh, type: Qt::QueuedConnection); |
133 | |
134 | if (c.isConnected()) { |
135 | QDBusServiceWatcher *watcher = |
136 | new QDBusServiceWatcher("*" , c, QDBusServiceWatcher::WatchForOwnerChange, this); |
137 | connect(sender: watcher, signal: &QDBusServiceWatcher::serviceOwnerChanged, context: this, |
138 | slot: &QDBusViewer::serviceOwnerChanged); |
139 | logMessage(msg: tr(s: "Connected to D-Bus." )); |
140 | } else { |
141 | logError(msg: tr(s: "Cannot connect to D-Bus: %1" ).arg(a: c.lastError().message())); |
142 | } |
143 | |
144 | objectPathRegExp.setPatternOptions(QRegularExpression::InvertedGreedinessOption); |
145 | } |
146 | |
147 | static inline QString topSplitterStateKey() |
148 | { |
149 | return u"topSplitterState"_s ; |
150 | } |
151 | |
152 | static inline QString splitterStateKey() |
153 | { |
154 | return u"splitterState"_s ; |
155 | } |
156 | |
157 | void QDBusViewer::saveState(QSettings *settings) const |
158 | { |
159 | settings->setValue(key: topSplitterStateKey(), value: topSplitter->saveState()); |
160 | settings->setValue(key: splitterStateKey(), value: splitter->saveState()); |
161 | } |
162 | |
163 | void QDBusViewer::restoreState(const QSettings *settings) |
164 | { |
165 | topSplitter->restoreState(state: settings->value(key: topSplitterStateKey()).toByteArray()); |
166 | splitter->restoreState(state: settings->value(key: splitterStateKey()).toByteArray()); |
167 | } |
168 | |
169 | void QDBusViewer::logMessage(const QString &msg) |
170 | { |
171 | log->append(text: msg + '\n'_L1); |
172 | } |
173 | |
174 | void QDBusViewer::showEvent(QShowEvent *) |
175 | { |
176 | serviceFilterLine->setFocus(); |
177 | } |
178 | |
179 | bool QDBusViewer::eventFilter(QObject *obj, QEvent *event) |
180 | { |
181 | if (obj == servicesView) { |
182 | if (event->type() == QEvent::KeyPress) { |
183 | QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event); |
184 | if (keyEvent->modifiers() == Qt::NoModifier) { |
185 | if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { |
186 | tree->setFocus(); |
187 | } |
188 | } |
189 | } |
190 | } |
191 | return false; |
192 | } |
193 | |
194 | void QDBusViewer::logError(const QString &msg) |
195 | { |
196 | log->append(text: tr(s: "<font color=\"red\">Error: </font>%1<br>" ).arg(a: msg.toHtmlEscaped())); |
197 | } |
198 | |
199 | void QDBusViewer::refresh() |
200 | { |
201 | servicesModel->removeRows(row: 0, count: servicesModel->rowCount()); |
202 | |
203 | if (c.isConnected()) { |
204 | const QStringList serviceNames = c.interface()->registeredServiceNames(); |
205 | servicesModel->setStringList(serviceNames); |
206 | } |
207 | } |
208 | |
209 | void QDBusViewer::activate(const QModelIndex &item) |
210 | { |
211 | if (!item.isValid()) |
212 | return; |
213 | |
214 | const QDBusModel *model = static_cast<const QDBusModel *>(item.model()); |
215 | |
216 | BusSignature sig; |
217 | sig.mService = currentService; |
218 | sig.mPath = model->dBusPath(index: item); |
219 | sig.mInterface = model->dBusInterface(index: item); |
220 | sig.mName = model->dBusMethodName(index: item); |
221 | sig.mTypeSig = model->dBusTypeSignature(index: item); |
222 | |
223 | switch (model->itemType(index: item)) { |
224 | case QDBusModel::SignalItem: |
225 | connectionRequested(sig); |
226 | break; |
227 | case QDBusModel::MethodItem: |
228 | callMethod(sig); |
229 | break; |
230 | case QDBusModel::PropertyItem: |
231 | getProperty(sig); |
232 | break; |
233 | default: |
234 | break; |
235 | } |
236 | } |
237 | |
238 | void QDBusViewer::getProperty(const BusSignature &sig) |
239 | { |
240 | QDBusMessage message = QDBusMessage::createMethodCall( |
241 | destination: sig.mService, path: sig.mPath, interface: "org.freedesktop.DBus.Properties"_L1 , method: "Get"_L1 ); |
242 | QList<QVariant> arguments; |
243 | arguments << sig.mInterface << sig.mName; |
244 | message.setArguments(arguments); |
245 | c.callWithCallback(message, receiver: this, SLOT(dumpMessage(QDBusMessage)), SLOT(dumpError(QDBusError))); |
246 | } |
247 | |
248 | void QDBusViewer::setProperty(const BusSignature &sig) |
249 | { |
250 | QDBusInterface iface(sig.mService, sig.mPath, sig.mInterface, c); |
251 | QMetaProperty prop = iface.metaObject()->property(index: iface.metaObject()->indexOfProperty(name: sig.mName.toLatin1())); |
252 | |
253 | bool ok; |
254 | QString input = QInputDialog::getText(parent: this, title: tr(s: "Arguments" ), |
255 | label: tr(s: "Please enter the value of the property %1 (type %2)" ).arg( |
256 | args: sig.mName, args: QString::fromLatin1(ba: prop.typeName())), |
257 | echo: QLineEdit::Normal, text: QString(), ok: &ok); |
258 | if (!ok) |
259 | return; |
260 | |
261 | QVariant value = input; |
262 | if (!value.convert(type: prop.metaType())) { |
263 | QMessageBox::warning(parent: this, title: tr(s: "Unable to marshall" ), |
264 | text: tr(s: "Value conversion failed, unable to set property" )); |
265 | return; |
266 | } |
267 | |
268 | QDBusMessage message = QDBusMessage::createMethodCall( |
269 | destination: sig.mService, path: sig.mPath, interface: "org.freedesktop.DBus.Properties"_L1 , method: "Set"_L1 ); |
270 | QList<QVariant> arguments; |
271 | arguments << sig.mInterface << sig.mName << QVariant::fromValue(value: QDBusVariant(value)); |
272 | message.setArguments(arguments); |
273 | c.callWithCallback(message, receiver: this, SLOT(dumpMessage(QDBusMessage)), SLOT(dumpError(QDBusError))); |
274 | } |
275 | |
276 | static QString getDbusSignature(const QMetaMethod& method) |
277 | { |
278 | // create a D-Bus type signature from QMetaMethod's parameters |
279 | QString sig; |
280 | for (const auto &type : method.parameterTypes()) |
281 | sig.append(s: QString::fromLatin1(ba: QDBusMetaType::typeToSignature(type: QMetaType::fromName(name: type)))); |
282 | return sig; |
283 | } |
284 | |
285 | void QDBusViewer::callMethod(const BusSignature &sig) |
286 | { |
287 | QDBusInterface iface(sig.mService, sig.mPath, sig.mInterface, c); |
288 | const QMetaObject *mo = iface.metaObject(); |
289 | |
290 | // find the method |
291 | QMetaMethod method; |
292 | for (int i = 0; i < mo->methodCount(); ++i) { |
293 | const QString signature = QString::fromLatin1(ba: mo->method(index: i).methodSignature()); |
294 | if (signature.startsWith(s: sig.mName) && signature.at(i: sig.mName.size()) == '('_L1) |
295 | if (getDbusSignature(method: mo->method(index: i)) == sig.mTypeSig) |
296 | method = mo->method(index: i); |
297 | } |
298 | if (!method.isValid()) { |
299 | QMessageBox::warning(parent: this, title: tr(s: "Unable to find method" ), |
300 | text: tr(s: "Unable to find method %1 on path %2 in interface %3" ).arg( |
301 | a: sig.mName).arg(a: sig.mPath).arg(a: sig.mInterface)); |
302 | return; |
303 | } |
304 | |
305 | PropertyDialog dialog; |
306 | QList<QVariant> args; |
307 | |
308 | const QList<QByteArray> paramTypes = method.parameterTypes(); |
309 | const QList<QByteArray> paramNames = method.parameterNames(); |
310 | QList<int> types; // remember the low-level D-Bus type |
311 | for (int i = 0; i < paramTypes.size(); ++i) { |
312 | const QByteArray paramType = paramTypes.at(i); |
313 | if (paramType.endsWith(c: '&')) |
314 | continue; // ignore OUT parameters |
315 | |
316 | const int type = QMetaType::fromName(name: paramType).id(); |
317 | dialog.addProperty(name: QString::fromLatin1(ba: paramNames.value(i)), type); |
318 | types.append(t: type); |
319 | } |
320 | |
321 | if (!types.isEmpty()) { |
322 | dialog.setInfo(tr(s: "Please enter parameters for the method \"%1\"" ).arg(a: sig.mName)); |
323 | |
324 | if (dialog.exec() != QDialog::Accepted) |
325 | return; |
326 | |
327 | args = dialog.values(); |
328 | } |
329 | |
330 | // Try to convert the values we got as closely as possible to the |
331 | // dbus signature. This is especially important for those input as strings |
332 | for (int i = 0; i < args.size(); ++i) { |
333 | QVariant a = args.at(i); |
334 | int desttype = types.at(i); |
335 | if (desttype < int(QMetaType::User) && desttype != qMetaTypeId<QVariantMap>()) { |
336 | const QMetaType metaType(desttype); |
337 | if (a.canConvert(targetType: metaType)) |
338 | args[i].convert(type: metaType); |
339 | } |
340 | // Special case - convert a value to a QDBusVariant if the |
341 | // interface wants a variant |
342 | if (types.at(i) == qMetaTypeId<QDBusVariant>()) |
343 | args[i] = QVariant::fromValue(value: QDBusVariant(args.at(i))); |
344 | } |
345 | |
346 | QDBusMessage message = QDBusMessage::createMethodCall(destination: sig.mService, path: sig.mPath, interface: sig.mInterface, |
347 | method: sig.mName); |
348 | message.setArguments(args); |
349 | c.callWithCallback(message, receiver: this, SLOT(dumpMessage(QDBusMessage)), SLOT(dumpError(QDBusError))); |
350 | } |
351 | |
352 | void QDBusViewer::(const QPoint &point) |
353 | { |
354 | QModelIndex item = tree->indexAt(p: point); |
355 | if (!item.isValid()) |
356 | return; |
357 | |
358 | const QDBusModel *model = static_cast<const QDBusModel *>(item.model()); |
359 | |
360 | BusSignature sig; |
361 | sig.mService = currentService; |
362 | sig.mPath = model->dBusPath(index: item); |
363 | sig.mInterface = model->dBusInterface(index: item); |
364 | sig.mName = model->dBusMethodName(index: item); |
365 | sig.mTypeSig = model->dBusTypeSignature(index: item); |
366 | |
367 | QMenu ; |
368 | menu.addAction(action: refreshAction); |
369 | |
370 | switch (model->itemType(index: item)) { |
371 | case QDBusModel::SignalItem: { |
372 | QAction *action = new QAction(tr(s: "&Connect" ), &menu); |
373 | action->setData(1); |
374 | menu.addAction(action); |
375 | break; } |
376 | case QDBusModel::MethodItem: { |
377 | QAction *action = new QAction(tr(s: "&Call" ), &menu); |
378 | action->setData(2); |
379 | menu.addAction(action); |
380 | break; } |
381 | case QDBusModel::PropertyItem: { |
382 | QDBusInterface iface(sig.mService, sig.mPath, sig.mInterface, c); |
383 | QMetaProperty prop = iface.metaObject()->property(index: iface.metaObject()->indexOfProperty(name: sig.mName.toLatin1())); |
384 | QAction *actionSet = new QAction(tr(s: "&Set value" ), &menu); |
385 | actionSet->setData(3); |
386 | actionSet->setEnabled(prop.isWritable()); |
387 | QAction *actionGet = new QAction(tr(s: "&Get value" ), &menu); |
388 | actionGet->setEnabled(prop.isReadable()); |
389 | actionGet->setData(4); |
390 | menu.addAction(action: actionSet); |
391 | menu.addAction(action: actionGet); |
392 | break; } |
393 | default: |
394 | break; |
395 | } |
396 | |
397 | QAction *selectedAction = menu.exec(pos: tree->viewport()->mapToGlobal(point)); |
398 | if (!selectedAction) |
399 | return; |
400 | |
401 | switch (selectedAction->data().toInt()) { |
402 | case 1: |
403 | connectionRequested(sig); |
404 | break; |
405 | case 2: |
406 | callMethod(sig); |
407 | break; |
408 | case 3: |
409 | setProperty(sig); |
410 | break; |
411 | case 4: |
412 | getProperty(sig); |
413 | break; |
414 | } |
415 | } |
416 | |
417 | void QDBusViewer::connectionRequested(const BusSignature &sig) |
418 | { |
419 | if (c.connect(service: sig.mService, path: QString(), interface: sig.mInterface, name: sig.mName, receiver: this, |
420 | SLOT(dumpMessage(QDBusMessage)))) { |
421 | logMessage(msg: tr(s: "Connected to service %1, path %2, interface %3, signal %4" ).arg( |
422 | args: sig.mService, args: sig.mPath, args: sig.mInterface, args: sig.mName)); |
423 | } else { |
424 | logError(msg: tr(s: "Unable to connect to service %1, path %2, interface %3, signal %4" ).arg( |
425 | args: sig.mService, args: sig.mPath, args: sig.mInterface, args: sig.mName)); |
426 | } |
427 | } |
428 | |
429 | void QDBusViewer::dumpMessage(const QDBusMessage &message) |
430 | { |
431 | QList<QVariant> args = message.arguments(); |
432 | |
433 | QString messageType; |
434 | switch (message.type()) { |
435 | case QDBusMessage::SignalMessage: |
436 | messageType = tr(s: "signal" ); |
437 | break; |
438 | case QDBusMessage::ErrorMessage: |
439 | messageType = tr(s: "error message" ); |
440 | break; |
441 | case QDBusMessage::ReplyMessage: |
442 | messageType = tr(s: "reply" ); |
443 | break; |
444 | default: |
445 | messageType = tr(s: "message" ); |
446 | break; |
447 | } |
448 | |
449 | QString out = tr(s: "Received %1 from %2" ).arg(a: messageType).arg(a: message.service()); |
450 | |
451 | if (!message.path().isEmpty()) |
452 | out += tr(s: ", path %1" ).arg(a: message.path()); |
453 | if (!message.interface().isEmpty()) |
454 | out += tr(s: ", interface <i>%1</i>" ).arg(a: message.interface()); |
455 | if (!message.member().isEmpty()) |
456 | out += tr(s: ", member %1" ).arg(a: message.member()); |
457 | out += "<br>"_L1 ; |
458 | if (args.isEmpty()) { |
459 | out += tr(s: " (no arguments)" ); |
460 | } else { |
461 | QStringList argStrings; |
462 | for (const QVariant &arg : std::as_const(t&: args)) { |
463 | QString str = QDBusUtil::argumentToString(variant: arg).toHtmlEscaped(); |
464 | // turn object paths into clickable links |
465 | str.replace(re: objectPathRegExp, after: tr(s: "[ObjectPath: <a href=\"qdbus://bus\\1\">\\1</a>]" )); |
466 | // convert new lines from command to proper HTML line breaks |
467 | str.replace(before: "\n"_L1 , after: "<br/>"_L1 ); |
468 | argStrings.append(t: str); |
469 | } |
470 | out += tr(s: " Arguments: %1" ).arg(a: argStrings.join(sep: tr(s: ", " ))); |
471 | } |
472 | |
473 | log->append(text: out); |
474 | } |
475 | |
476 | void QDBusViewer::dumpError(const QDBusError &error) |
477 | { |
478 | logError(msg: error.message()); |
479 | } |
480 | |
481 | void QDBusViewer::serviceChanged(const QModelIndex &index) |
482 | { |
483 | delete tree->model(); |
484 | |
485 | currentService.clear(); |
486 | if (!index.isValid()) |
487 | return; |
488 | currentService = index.data().toString(); |
489 | |
490 | QDBusViewModel *model = new QDBusViewModel(currentService, c); |
491 | tree->setModel(model); |
492 | connect(sender: model, signal: &QDBusModel::busError, context: this, slot: &QDBusViewer::logError); |
493 | } |
494 | |
495 | void QDBusViewer::serviceRegistered(const QString &service) |
496 | { |
497 | if (service == c.baseService()) |
498 | return; |
499 | |
500 | servicesModel->insertRows(row: 0, count: 1); |
501 | servicesModel->setData(index: servicesModel->index(row: 0, column: 0), value: service); |
502 | } |
503 | |
504 | static QModelIndex findItem(QStringListModel *servicesModel, const QString &name) |
505 | { |
506 | QModelIndexList hits = servicesModel->match(start: servicesModel->index(row: 0, column: 0), role: Qt::DisplayRole, value: name); |
507 | if (hits.isEmpty()) |
508 | return QModelIndex(); |
509 | |
510 | return hits.first(); |
511 | } |
512 | |
513 | void QDBusViewer::serviceOwnerChanged(const QString &name, const QString &oldOwner, |
514 | const QString &newOwner) |
515 | { |
516 | QModelIndex hit = findItem(servicesModel, name); |
517 | |
518 | if (!hit.isValid() && oldOwner.isEmpty() && !newOwner.isEmpty()) |
519 | serviceRegistered(service: name); |
520 | else if (hit.isValid() && !oldOwner.isEmpty() && newOwner.isEmpty()) |
521 | servicesModel->removeRows(row: hit.row(), count: 1); |
522 | else if (hit.isValid() && !oldOwner.isEmpty() && !newOwner.isEmpty()) { |
523 | servicesModel->removeRows(row: hit.row(), count: 1); |
524 | serviceRegistered(service: name); |
525 | } |
526 | } |
527 | |
528 | void QDBusViewer::serviceFilterReturnPressed() |
529 | { |
530 | if (servicesProxyModel->rowCount() <= 0) |
531 | return; |
532 | |
533 | servicesView->selectRow(row: 0); |
534 | servicesView->setFocus(); |
535 | } |
536 | |
537 | void QDBusViewer::refreshChildren() |
538 | { |
539 | QDBusModel *model = qobject_cast<QDBusModel *>(object: tree->model()); |
540 | if (!model) |
541 | return; |
542 | model->refresh(index: tree->currentIndex()); |
543 | } |
544 | |
545 | void QDBusViewer::anchorClicked(const QUrl &url) |
546 | { |
547 | if (url.scheme() != "qdbus"_L1 ) |
548 | // not ours |
549 | return; |
550 | |
551 | // swallow the click without setting a new document |
552 | log->setSource(name: QUrl()); |
553 | |
554 | QDBusModel *model = qobject_cast<QDBusModel *>(object: tree->model()); |
555 | if (!model) |
556 | return; |
557 | |
558 | QModelIndex idx = model->findObject(objectPath: QDBusObjectPath(url.path())); |
559 | if (!idx.isValid()) |
560 | return; |
561 | |
562 | tree->scrollTo(index: idx); |
563 | tree->setCurrentIndex(idx); |
564 | } |
565 | |