Add a manual test to show a QTreeView with QInputDevice::devices()

At least on X11 this is a real hierarchy: slave devices come and go
as children of the master devices. The goal is to verify that this tree
and the output of `xinput list` are in sync. The model calls
[begin|end]InsertRows() and [begin|end]RemoveRows() as necessary to
attempt to keep the view updated.

Task-number: QTBUG-46412
Task-number: QTBUG-98720
Task-number: QTBUG-104878
Task-number: QTBUG-112141
Change-Id: I8a2252f041cd1de777eef225d0e7f0db5c90a706
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
This commit is contained in:
Shawn Rutledge 2023-03-21 08:15:47 +01:00
parent 2a9d93efc6
commit dc7f4f7b4e
4 changed files with 360 additions and 0 deletions

View File

@ -0,0 +1,17 @@
project(inputdevices)
cmake_minimum_required(VERSION 3.19)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
qt_add_executable(inputdevices
main.cpp
inputdevicemodel.h inputdevicemodel.cpp
)
set_target_properties(inputdevices PROPERTIES
AUTOMOC TRUE
)
target_link_libraries(inputdevices PUBLIC
Qt::Widgets
)

View File

@ -0,0 +1,264 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "inputdevicemodel.h"
#include <QEvent>
#include <QInputDevice>
#include <QLoggingCategory>
#include <QMetaEnum>
#include <QPointingDevice>
Q_LOGGING_CATEGORY(lcIPDM, "qt.inputdevicemodel")
static QString enumToString(const QObject *obj, const char* enumName, int enumValue)
{
const auto *metaobj = obj->metaObject();
Q_ASSERT(metaobj);
const int enumIdx = metaobj->indexOfEnumerator(enumName);
if (enumIdx < 0)
return {};
Q_ASSERT(metaobj->enumerator(enumIdx).isValid());
const char *ret = metaobj->enumerator(enumIdx).valueToKey(enumValue);
if (!ret)
return {};
return QString::fromUtf8(ret);
}
static QString capabilitiesString(const QInputDevice *dev)
{
QStringList ret;
const auto caps = dev->capabilities();
if (caps.testFlag(QInputDevice::Capability::Position))
ret << InputDeviceModel::tr("pos");
if (caps.testFlag(QInputDevice::Capability::Area))
ret << InputDeviceModel::tr("area");
if (caps.testFlag(QInputDevice::Capability::Pressure))
ret << InputDeviceModel::tr("press");
if (caps.testFlag(QInputDevice::Capability::Velocity))
ret << InputDeviceModel::tr("vel");
if (caps.testFlag(QInputDevice::Capability::NormalizedPosition))
ret << InputDeviceModel::tr("norm");
if (caps.testFlag(QInputDevice::Capability::MouseEmulation))
ret << InputDeviceModel::tr("m-emu");
if (caps.testFlag(QInputDevice::Capability::Scroll))
ret << InputDeviceModel::tr("scroll");
if (caps.testFlag(QInputDevice::Capability::PixelScroll))
ret << InputDeviceModel::tr("pxscroll");
if (caps.testFlag(QInputDevice::Capability::Hover))
ret << InputDeviceModel::tr("hover");
if (caps.testFlag(QInputDevice::Capability::Rotation))
ret << InputDeviceModel::tr("rot");
if (caps.testFlag(QInputDevice::Capability::XTilt))
ret << InputDeviceModel::tr("xtilt");
if (caps.testFlag(QInputDevice::Capability::YTilt))
ret << InputDeviceModel::tr("ytilt");
if (caps.testFlag(QInputDevice::Capability::TangentialPressure))
ret << InputDeviceModel::tr("tan");
if (caps.testFlag(QInputDevice::Capability::ZPosition))
ret << InputDeviceModel::tr("z");
return ret.join(u'|');
}
/*!
Returns true only if the given \a device is a master:
that is, if its parent is \e not another QInputDevice.
*/
static const auto masterDevicePred = [](const QInputDevice *device)
{
return !qobject_cast<QInputDevice*>(device->parent());
};
/*!
Returns the master device at index \a i:
that is, the i'th of the devices that satisfy masterDevicePred().
*/
static const QInputDevice *masterDevice(int i)
{
const auto devices = QInputDevice::devices();
auto it = std::find_if(devices.constBegin(), devices.constEnd(), masterDevicePred);
it += i;
return (it == devices.constEnd() ? nullptr : *it);
}
/*!
Returns the index of the master \a device: that is, the index within the
subset of QInputDevice::devices() that satisfy masterDevicePred().
*/
static const int masterDeviceIndex(const QInputDevice *device)
{
Q_ASSERT(masterDevicePred(device)); // assume dev is not a slave
const auto devices = QInputDevice::devices();
auto it = std::find_if(devices.constBegin(), devices.constEnd(), masterDevicePred);
for (int i = 0; it != devices.constEnd(); ++i, ++it)
if (*it == device)
return i;
return -1;
}
InputDeviceModel::InputDeviceModel(QObject *parent)
: QAbstractItemModel{parent}
{
connect(this, &InputDeviceModel::deviceAdded, this, &InputDeviceModel::onDeviceAdded, Qt::QueuedConnection);
}
// invariant: always call createIndex(row, column, QInputDevice*) or else nullptr for the last argument
QModelIndex InputDeviceModel::index(int row, int column, const QModelIndex &parent) const
{
const QInputDevice *par = static_cast<QInputDevice *>(parent.internalPointer());
const QInputDevice *ret = par ? qobject_cast<const QInputDevice *>(par->children().at(row)) : masterDevice(row);
qCDebug(lcIPDM) << row << column << "under parent" << par << ":" << ret;
return createIndex(row, column, ret);
}
QModelIndex InputDeviceModel::parent(const QModelIndex &index) const
{
if (!index.internalPointer())
return {};
const QInputDevice *par = qobject_cast<const QInputDevice *>(
static_cast<QInputDevice *>(index.internalPointer())->parent());
if (par)
return createIndex(masterDeviceIndex(par), index.column(), par);
return {};
}
int InputDeviceModel::rowCount(const QModelIndex &parent) const
{
int ret = 0;
const QInputDevice *par = qobject_cast<const QInputDevice *>(static_cast<QObject *>(parent.internalPointer()));
if (par) {
ret = par->children().count();
} else {
const auto devices = QInputDevice::devices();
ret = std::count_if(devices.constBegin(), devices.constEnd(), masterDevicePred);
}
qCDebug(lcIPDM) << ret << "under parent" << parent << par;
return ret;
}
int InputDeviceModel::columnCount(const QModelIndex &) const
{
return NRoles;
}
QVariant InputDeviceModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
switch (section + Name) {
case Name:
return tr("Device Name");
case DeviceType:
return tr("Device Type");
case PointerType:
return tr("Pointer Type");
case Capabilities:
return tr("Capabilities");
case SystemId:
return tr("System ID");
case SeatName:
return tr("Seat Name");
case AvailableVirtualGeometry:
return tr("Available Virtual Geometry");
case MaximumPoints:
return tr("Maximum Points");
case ButtonCount:
return tr("Button Count");
case UniqueId:
return tr("Unique ID");
case NRoles:
break;
}
}
return {};
}
bool InputDeviceModel::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::ChildAdded)
// At this time, the child is not fully-constructed.
// Emit a signal which is connected to onDeviceAdded via queued connection, to delay row insertion.
emit deviceAdded(static_cast<QChildEvent *>(event)->child());
return false;
}
void InputDeviceModel::onDeviceAdded(const QObject *o)
{
const QInputDevice *child = qobject_cast<const QInputDevice *>(o);
const QInputDevice *parent = qobject_cast<const QInputDevice *>(child->parent());
const int idx = parent->children().indexOf(child);
qCDebug(lcIPDM) << parent << "has a baby!" << child << "@" << idx;
beginInsertRows(createIndex(masterDeviceIndex(parent), 0, parent), idx, idx);
endInsertRows();
}
void InputDeviceModel::watchDevice(const QInputDevice *dev) const
{
if (!m_known.contains(dev)) {
m_known << dev;
connect(dev, &QObject::destroyed, this, &InputDeviceModel::deviceDestroyed);
if (masterDevicePred(dev))
const_cast<QInputDevice *>(dev)->installEventFilter(const_cast<InputDeviceModel *>(this));
}
}
void InputDeviceModel::deviceDestroyed(QObject *o)
{
beginResetModel();
const QInputDevice *dev = static_cast<QInputDevice *>(o);
bool needsReset = true;
if (!masterDevicePred(dev)) {
const QInputDevice *parent = static_cast<const QInputDevice *>(dev->parent());
const int idx = parent->children().indexOf(dev);
Q_ASSERT(idx >= 0);
beginRemoveRows(createIndex(masterDeviceIndex(parent), 0, parent), idx, idx);
endRemoveRows();
needsReset = false;
}
m_known.removeOne(dev);
if (needsReset)
endResetModel();
}
QVariant InputDeviceModel::data(const QModelIndex &index, int role) const
{
if (role == Qt::DisplayRole)
role = index.column() + Role::Name;
if (role >= NRoles)
return {};
const QInputDevice *dev = static_cast<QInputDevice *>(index.internalPointer());
watchDevice(dev);
if (role < Name)
qCDebug(lcIPDM) << index << Qt::ItemDataRole(role) << dev;
else
qCDebug(lcIPDM) << index << Role(role) << dev;
const QPointingDevice *pdev = qobject_cast<const QPointingDevice *>(dev);
switch (role) {
case Name:
return dev->name();
case DeviceType:
return enumToString(dev, "DeviceType", int(dev->type()));
case PointerType:
return pdev ? enumToString(pdev, "PointerType", int(pdev->pointerType()))
: QString();
case Capabilities:
return capabilitiesString(dev);
case SystemId:
return dev->systemId();
case SeatName:
return dev->seatName();
case AvailableVirtualGeometry: {
const auto rect = dev->availableVirtualGeometry();
return tr("%1 x %2 %3 %4").arg(rect.width()).arg(rect.height()).arg(rect.x()).arg(rect.y());
}
case MaximumPoints:
return pdev ? pdev->maximumPoints() : 0;
case ButtonCount:
return pdev ? pdev->buttonCount() : 0;
case UniqueId:
return pdev ? pdev->uniqueId().numericId() : 0;
}
return {};
}
#include "moc_inputdevicemodel.cpp"

View File

@ -0,0 +1,55 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef INPUTDEVICEMODEL_H
#define INPUTDEVICEMODEL_H
#include <QAbstractItemModel>
class QInputDevice;
class InputDeviceModel : public QAbstractItemModel
{
Q_OBJECT
public:
enum Role {
Name = Qt::UserRole + 1,
DeviceType,
PointerType,
Capabilities,
SystemId,
SeatName,
AvailableVirtualGeometry,
MaximumPoints,
ButtonCount,
UniqueId,
NRoles
};
Q_ENUM(Role);
explicit InputDeviceModel(QObject *parent = nullptr);
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
signals:
void deviceAdded(const QObject *o);
private slots:
void onDeviceAdded(const QObject *o);
private:
void watchDevice(const QInputDevice *dev) const;
void deviceDestroyed(QObject *o);
mutable QList<const QInputDevice *> m_known;
};
#endif // INPUTDEVICEMODEL_H

View File

@ -0,0 +1,24 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include <QApplication>
#include <QLoggingCategory>
#include <QTreeView>
#include "inputdevicemodel.h"
int main(int argc, char **argv)
{
QLoggingCategory::setFilterRules(QStringLiteral("qt.qpa.input.devices=true"));
QApplication app(argc, argv);
QTreeView view;
view.setModel(new InputDeviceModel(&view));
view.resize(1280, 600);
view.show();
view.resizeColumnToContents(0);
app.exec();
}