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"