File indexing completed on 2025-01-26 03:28:31

0001 // SPDX-FileCopyrightText: 2012 Frederik Gladhorn <gladhorn@kde.org>
0002 // SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0003 
0004 #include "eventview.h"
0005 #include "accessibilityinspector_debug.h"
0006 
0007 #include <KLocalizedString>
0008 #include <QMetaEnum>
0009 #include <QScrollBar>
0010 #include <QSettings>
0011 #include <QSortFilterProxyModel>
0012 #include <QStandardItem>
0013 #include <QStandardItemModel>
0014 
0015 class EventsModel : public QStandardItemModel
0016 {
0017     Q_OBJECT
0018 public:
0019     enum Role {
0020         AccessibleRole = 0,
0021         RoleRole = 1,
0022         EventRole = 2,
0023         ActionRole = 3,
0024         EventTypeRole = Qt::UserRole,
0025         UrlRole,
0026         AppNameRole,
0027         AppUrlRole,
0028     };
0029 
0030     struct LogItem {
0031         QStandardItem *appItem = nullptr;
0032         bool isNewAppItem;
0033         LogItem(QStandardItem *appItem, bool isNewAppItem)
0034             : appItem(appItem)
0035             , isNewAppItem(isNewAppItem)
0036         {
0037         }
0038     };
0039 
0040     explicit EventsModel(EventsWidget *view)
0041         : QStandardItemModel(view)
0042     {
0043         clearLog();
0044     }
0045 
0046     QHash<int, QByteArray> roleNames() const override
0047     {
0048         return {
0049             {AccessibleRole, "accessible"},
0050             {RoleRole, "role"},
0051             {EventRole, "event"},
0052             {EventTypeRole, "eventType"},
0053             {UrlRole, "url"},
0054             {AppNameRole, "appName"},
0055             {AppUrlRole, "appUrl"},
0056         };
0057     }
0058 
0059     QString roleLabel(Role role) const
0060     {
0061         switch (role) {
0062         case AccessibleRole:
0063             return i18nc("@label", "Accessible");
0064         case RoleRole:
0065             return i18nc("@label", "Role");
0066         case EventRole:
0067             return i18nc("@label", "Event");
0068         case ActionRole:
0069             return i18nc("@label", "Action");
0070         case EventTypeRole:
0071         case UrlRole:
0072         case AppNameRole:
0073         case AppUrlRole:
0074             break;
0075         }
0076         return {};
0077     }
0078     void clearLog()
0079     {
0080         clear();
0081         m_apps.clear();
0082         setColumnCount(4);
0083         QStringList headerLabels;
0084         const QList<Role> roles{AccessibleRole, RoleRole, EventRole, ActionRole};
0085         for (Role r : roles) {
0086             headerLabels << roleLabel(r);
0087         }
0088         setHorizontalHeaderLabels(headerLabels);
0089     }
0090 
0091     LogItem addLog(const QList<QStandardItem *> &item)
0092     {
0093         const QString appUrl = item.first()->data(AppUrlRole).toString();
0094         QStandardItem *appItem = nullptr;
0095         const QMap<QString, QStandardItem *>::ConstIterator it = m_apps.constFind(appUrl);
0096         const bool isNewAppItem = it == m_apps.constEnd();
0097         if (isNewAppItem) {
0098             const QString appName = item.first()->data(AppNameRole).toString();
0099             m_apps[appUrl] = appItem = new QStandardItem(appName);
0100             appItem->setData(appUrl, EventsModel::AppUrlRole);
0101             invisibleRootItem()->appendRow(appItem);
0102         } else {
0103             appItem = it.value();
0104         }
0105         appItem->appendRow(item);
0106         return {appItem, isNewAppItem};
0107     }
0108 
0109 private:
0110     QMap<QString, QStandardItem *> m_apps;
0111 };
0112 
0113 class EventsProxyModel : public QSortFilterProxyModel
0114 {
0115     Q_OBJECT
0116 public:
0117     explicit EventsProxyModel(QWidget *parent = nullptr)
0118         : QSortFilterProxyModel(parent)
0119         , m_types(EventsWidget::AllEvents)
0120     {
0121     }
0122     EventsWidget::EventTypes filter() const
0123     {
0124         return m_types;
0125     }
0126     QString accessibleFilter() const
0127     {
0128         return m_accessibleFilter;
0129     }
0130     QString roleFilter() const
0131     {
0132         return m_roleFilter;
0133     }
0134     void setFilter(EventsWidget::EventTypes types)
0135     {
0136         m_types = types;
0137         invalidateFilter();
0138     }
0139     void setAccessibleFilter(const QString &filter)
0140     {
0141         m_accessibleFilter = filter;
0142         invalidateFilter();
0143     }
0144     void setRoleFilter(const QString &filter)
0145     {
0146         m_roleFilter = filter;
0147         invalidateFilter();
0148     }
0149 
0150 protected:
0151     bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
0152     {
0153         if (!source_parent.isValid())
0154             return true;
0155         if (!m_types.testFlag(EventsWidget::AllEvents)) {
0156             const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
0157             const auto type = index.data(EventsModel::EventTypeRole).value<EventsWidget::EventType>();
0158             if (!m_types.testFlag(type))
0159                 return false;
0160         }
0161         if (!m_accessibleFilter.isEmpty()) {
0162             const QModelIndex index = sourceModel()->index(source_row, EventsModel::AccessibleRole, source_parent);
0163             const QString accessibleName = index.data(Qt::DisplayRole).toString();
0164             if (!accessibleName.contains(m_accessibleFilter, Qt::CaseInsensitive))
0165                 return false;
0166         }
0167         if (!m_roleFilter.isEmpty()) {
0168             const QModelIndex index = sourceModel()->index(source_row, EventsModel::RoleRole, source_parent);
0169             const QString roleName = index.data(Qt::DisplayRole).toString();
0170             if (!roleName.contains(m_roleFilter, Qt::CaseInsensitive))
0171                 return false;
0172         }
0173         return true;
0174     }
0175 
0176 private:
0177     EventsWidget::EventTypes m_types;
0178     QString m_accessibleFilter;
0179     QString m_roleFilter;
0180 };
0181 
0182 using namespace QAccessibleClient;
0183 QAccessible::UpdateHandler EventsWidget::mOriginalAccessibilityUpdateHandler = nullptr;
0184 QObject *EventsWidget::mTextEditForAccessibilityUpdateHandler = nullptr;
0185 
0186 EventsWidget::EventsWidget(QWidget *parent)
0187     : QWidget(parent)
0188     , mModel(new EventsModel(this))
0189     , mProxyModel(new EventsProxyModel(this))
0190 {
0191     mUi.setupUi(this);
0192 
0193     mUi.eventListView->setAccessibleName(QLatin1String("Events View"));
0194     mUi.eventListView->setAccessibleDescription(i18n("Displays all received events"));
0195     mUi.eventListView->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge}));
0196 
0197     mUi.horizontalLayout->setSpacing(style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing));
0198     mUi.horizontalLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
0199                                              style()->pixelMetric(QStyle::PM_LayoutTopMargin),
0200                                              style()->pixelMetric(QStyle::PM_LayoutRightMargin),
0201                                              style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
0202 
0203     mProxyModel->setSourceModel(mModel);
0204     mUi.eventListView->setModel(mProxyModel);
0205 
0206     connect(mUi.accessibleFilterEdit, &QLineEdit::textChanged, this, &EventsWidget::accessibleFilterChanged);
0207     connect(mUi.roleFilterEdit, &QLineEdit::textChanged, this, &EventsWidget::roleFilterChanged);
0208 
0209     auto filerModel = new QStandardItemModel(this);
0210     auto firstFilterItem = new QStandardItem(QStringLiteral("Event Filter"));
0211     firstFilterItem->setFlags(Qt::ItemIsEnabled);
0212     filerModel->appendRow(firstFilterItem);
0213 
0214     const QVector<EventType> filterList = {StateChanged, NameChanged, DescriptionChanged, Window, Focus, Document, Object, Text, Table, Others};
0215     for (int i = 0; i < filterList.count(); ++i) {
0216         EventType t = filterList[i];
0217         auto item = new QStandardItem(eventName(t));
0218         item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
0219         item->setData(QVariant::fromValue<EventType>(t), EventsModel::EventTypeRole);
0220         item->setData(Qt::Checked, Qt::CheckStateRole);
0221         filerModel->appendRow(QList<QStandardItem *>() << item);
0222     }
0223     mUi.filterComboBox->setModel(filerModel);
0224 
0225     mUi.clearButton->setFixedWidth(QFontMetrics(mUi.clearButton->font()).boundingRect(mUi.clearButton->text()).width() + 4);
0226     mUi.clearButton->setFixedHeight(mUi.filterComboBox->sizeHint().height());
0227     connect(mUi.clearButton, &QPushButton::clicked, this, &EventsWidget::clearLog);
0228     connect(mUi.filterComboBox, &QComboBox::activated, this, &EventsWidget::checkStateChanged);
0229     connect(mUi.eventListView, &QTreeView::activated, this, &EventsWidget::eventActivated);
0230 
0231     // Collect multiple addLog calls and process them after 500 ms earliest. This
0232     // makes sure multiple calls to addLog will be compressed to one only one
0233     // view refresh what improves performance.
0234     mPendingTimer.setInterval(500);
0235     connect(&mPendingTimer, &QTimer::timeout, this, &EventsWidget::processPending);
0236     mTextEditForAccessibilityUpdateHandler = mUi.eventListView;
0237     checkStateChanged();
0238 
0239     // We need to wait for a11y to be active for this hack.
0240     QTimer::singleShot(500, this, &EventsWidget::installUpdateHandler);
0241 }
0242 
0243 void EventsWidget::installUpdateHandler()
0244 {
0245     mOriginalAccessibilityUpdateHandler = QAccessible::installUpdateHandler(customUpdateHandler);
0246     if (!mOriginalAccessibilityUpdateHandler)
0247         QTimer::singleShot(500, this, &EventsWidget::installUpdateHandler);
0248 }
0249 
0250 void EventsWidget::customUpdateHandler(QAccessibleEvent *event)
0251 {
0252     QObject *object = event->object();
0253     if (object == mTextEditForAccessibilityUpdateHandler)
0254         return;
0255     // if (m_originalAccessibilityUpdateHandler)
0256     //     m_originalAccessibilityUpdateHandler(object, who, reason);
0257 }
0258 
0259 QString EventsWidget::eventName(EventType eventType) const
0260 {
0261     QString s;
0262     switch (eventType) {
0263     case EventsWidget::Focus:
0264         s = i18nc("Accessibility event name", "Focus");
0265         break;
0266     case EventsWidget::StateChanged:
0267         s = i18nc("Accessibility event name: state changed", "State");
0268         break;
0269     case EventsWidget::NameChanged:
0270         s = i18nc("Accessibility event name: name changed", "Name");
0271         break;
0272     case EventsWidget::DescriptionChanged:
0273         s = i18nc("Accessibility event name: description changed", "Description");
0274         break;
0275     case EventsWidget::Window:
0276         s = i18nc("Accessibility event name", "Window");
0277         break;
0278     case EventsWidget::Document:
0279         s = i18nc("Accessibility event name", "Document");
0280         break;
0281     case EventsWidget::Object:
0282         s = i18nc("Accessibility event name", "Object");
0283         break;
0284     case EventsWidget::Text:
0285         s = i18nc("Accessibility event name", "Text");
0286         break;
0287     case EventsWidget::Table:
0288         s = i18nc("Accessibility event name", "Table");
0289         break;
0290     case EventsWidget::Others:
0291         s = i18nc("Accessibility event name", "Others");
0292         break;
0293     case EventsWidget::NoEvents:
0294         s = i18nc("Accessibility event name", "No Event");
0295         break;
0296     default:
0297         break;
0298     }
0299     return s;
0300 }
0301 
0302 void EventsWidget::loadSettings(QSettings &settings)
0303 {
0304     settings.beginGroup(QStringLiteral("events"));
0305 
0306     bool eventsFilterOk;
0307     EventTypes eventsFilter = EventTypes(settings.value(QStringLiteral("eventsFilter")).toInt(&eventsFilterOk));
0308     if (!eventsFilterOk)
0309         eventsFilter = AllEvents;
0310 
0311     QAbstractItemModel *model = mUi.filterComboBox->model();
0312     if (eventsFilter != mProxyModel->filter()) {
0313         for (int i = 1; i < model->rowCount(); ++i) {
0314             QModelIndex index = model->index(i, 0);
0315             auto type = model->data(index, EventsModel::EventTypeRole).value<EventType>();
0316             if (eventsFilter.testFlag(type))
0317                 model->setData(index, Qt::Checked, Qt::CheckStateRole);
0318             else
0319                 model->setData(index, Qt::Unchecked, Qt::CheckStateRole);
0320         }
0321         mProxyModel->setFilter(eventsFilter);
0322     }
0323 
0324     const QByteArray eventListViewState = settings.value(QStringLiteral("listViewHeader")).toByteArray();
0325     if (!eventListViewState.isEmpty())
0326         mUi.eventListView->header()->restoreState(eventListViewState);
0327 
0328     settings.endGroup();
0329 }
0330 
0331 void EventsWidget::saveSettings(QSettings &settings)
0332 {
0333     settings.beginGroup(QStringLiteral("events"));
0334     settings.setValue(QStringLiteral("eventsFilter"), int(mProxyModel->filter()));
0335 
0336     const QByteArray eventListViewState = mUi.eventListView->header()->saveState();
0337     settings.setValue(QStringLiteral("listViewHeader"), eventListViewState);
0338 
0339     settings.endGroup();
0340 }
0341 
0342 void EventsWidget::clearLog()
0343 {
0344     mModel->clearLog();
0345 }
0346 
0347 void EventsWidget::processPending()
0348 {
0349     mPendingTimer.stop();
0350     QVector<QList<QStandardItem *>> pendingLogs = m_pendingLogs;
0351     m_pendingLogs.clear();
0352     // bool wasMax = true;//m_ui.eventListView->verticalScrollBar()->value() - 10 >= m_ui.eventListView->verticalScrollBar()->maximum();
0353     QStandardItem *lastItem = nullptr;
0354     QStandardItem *lastAppItem = nullptr;
0355     for (int i = 0; i < pendingLogs.count(); ++i) {
0356         QList<QStandardItem *> item = pendingLogs[i];
0357         EventsModel::LogItem logItem = mModel->addLog(item);
0358 
0359         // Logic to scroll to the last added logItem of the last appItem that is expanded.
0360         // For appItem's not expanded the logItem is added but no scrolling will happen.
0361         if (lastItem && lastAppItem && lastAppItem != logItem.appItem)
0362             lastItem = nullptr;
0363         bool selected = lastItem;
0364         if (lastAppItem != logItem.appItem) {
0365             lastAppItem = logItem.appItem;
0366             QModelIndex index = mProxyModel->mapFromSource(mModel->indexFromItem(logItem.appItem));
0367             if (logItem.isNewAppItem) {
0368                 mUi.eventListView->setExpanded(index, true);
0369                 selected = true;
0370             } else {
0371                 selected = mUi.eventListView->isExpanded(index);
0372             }
0373         }
0374         if (selected)
0375             lastItem = item.first();
0376     }
0377     if (lastItem) { // scroll down to the lastItem.
0378         // m_ui.eventListView->verticalScrollBar()->setValue(m_ui.eventListView->verticalScrollBar()->maximum());
0379 
0380         QModelIndex index = mProxyModel->mapFromSource(mModel->indexFromItem(lastItem));
0381         mUi.eventListView->scrollTo(index, QAbstractItemView::PositionAtBottom);
0382         // m_ui.eventListView->scrollTo(index, QAbstractItemView::EnsureVisible);
0383     }
0384 }
0385 
0386 void EventsWidget::addLog(const QAccessibleClient::AccessibleObject &object, EventsWidget::EventType eventType, const QString &text)
0387 {
0388     if (!object.isValid())
0389         return;
0390 
0391     auto nameItem = new QStandardItem(object.name());
0392     nameItem->setData(QVariant::fromValue<EventType>(eventType), EventsModel::EventTypeRole);
0393     nameItem->setData(object.url().toString(), EventsModel::UrlRole);
0394 
0395     AccessibleObject app = object.application();
0396     if (app.isValid()) {
0397         nameItem->setData(app.name(), EventsModel::AppNameRole);
0398         nameItem->setData(app.url().toString(), EventsModel::AppUrlRole);
0399     }
0400 
0401     auto roleItem = new QStandardItem(object.roleName());
0402     auto typeItem = new QStandardItem(eventName(eventType));
0403     auto textItem = new QStandardItem(text);
0404     m_pendingLogs.append(QList<QStandardItem *>() << nameItem << roleItem << typeItem << textItem);
0405     if (!mPendingTimer.isActive()) {
0406         mPendingTimer.start();
0407     }
0408 }
0409 
0410 void EventsWidget::checkStateChanged()
0411 {
0412     EventTypes types;
0413     QStringList names;
0414     QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("EventType"));
0415     Q_ASSERT(e.isValid());
0416     QAbstractItemModel *model = mUi.filterComboBox->model();
0417     for (int i = 1; i < model->rowCount(); ++i) {
0418         QModelIndex index = model->index(i, 0);
0419         bool checked = model->data(index, Qt::CheckStateRole).toBool();
0420         if (checked) {
0421             auto type = model->data(index, EventsModel::EventTypeRole).value<EventType>();
0422             types |= type;
0423             names.append(QString::fromLatin1(e.valueToKey(type)));
0424         }
0425     }
0426     mProxyModel->setFilter(types);
0427 }
0428 
0429 void EventsWidget::eventActivated(const QModelIndex &index)
0430 {
0431     Q_ASSERT(index.isValid());
0432     const QModelIndex parent = index.parent();
0433     const QModelIndex firstIndex = mProxyModel->index(index.row(), 0, parent);
0434     const QString s = mProxyModel->data(firstIndex, parent.isValid() ? EventsModel::UrlRole : EventsModel::AppUrlRole).toString();
0435     const QUrl url(s);
0436     if (!url.isValid()) {
0437         qCWarning(ACCESSIBILITYINSPECTOR_LOG) << Q_FUNC_INFO << "Invalid url=" << s;
0438         return;
0439     }
0440     Q_EMIT anchorClicked(url);
0441 }
0442 
0443 void EventsWidget::accessibleFilterChanged()
0444 {
0445     mProxyModel->setAccessibleFilter(mUi.accessibleFilterEdit->text());
0446 }
0447 
0448 void EventsWidget::roleFilterChanged()
0449 {
0450     mProxyModel->setRoleFilter(mUi.roleFilterEdit->text());
0451 }
0452 
0453 #include "eventview.moc"
0454 #include "moc_eventview.cpp"