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:
parent
2a9d93efc6
commit
dc7f4f7b4e
17
tests/manual/inputdevices/CMakeLists.txt
Normal file
17
tests/manual/inputdevices/CMakeLists.txt
Normal 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
|
||||
)
|
264
tests/manual/inputdevices/inputdevicemodel.cpp
Normal file
264
tests/manual/inputdevices/inputdevicemodel.cpp
Normal 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"
|
55
tests/manual/inputdevices/inputdevicemodel.h
Normal file
55
tests/manual/inputdevices/inputdevicemodel.h
Normal 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
|
24
tests/manual/inputdevices/main.cpp
Normal file
24
tests/manual/inputdevices/main.cpp
Normal 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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user