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

source code of qttools/src/qdbus/qdbusviewer/qdbusviewer.cpp