Messagelib

webengineaccesskey.cpp
1/*
2 SPDX-FileCopyrightText: 2016-2024 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "webengineaccesskey.h"
8#include "webengineaccesskeyanchor.h"
9#include "webengineaccesskeyutils.h"
10#include "webenginemanagescript.h"
11
12#include <KActionCollection>
13#include <QAction>
14#include <QDebug>
15#include <QKeyEvent>
16#include <QLabel>
17#include <QList>
18#include <QToolTip>
19#include <QWebEngineView>
20using namespace WebEngineViewer;
21template<typename Arg, typename R, typename C>
22struct InvokeWrapperWebAccessKey {
23 R *receiver;
24 void (C::*memberFunction)(Arg);
25 void operator()(Arg result)
26 {
27 (receiver->*memberFunction)(result);
28 }
29};
30
31template<typename Arg, typename R, typename C>
32
33InvokeWrapperWebAccessKey<Arg, R, C> invokeWebAccessKey(R *receiver, void (C::*memberFunction)(Arg))
34{
35 InvokeWrapperWebAccessKey<Arg, R, C> wrapper = {receiver, memberFunction};
36 return wrapper;
37}
38
39class WebEngineViewer::WebEngineAccessKeyPrivate
40{
41public:
42 enum AccessKeyState {
43 NotActivated,
44 PreActivated,
45 Activated,
46 };
47
48 WebEngineAccessKeyPrivate(WebEngineAccessKey *qq, QWebEngineView *webEngine)
49 : mWebEngine(webEngine)
50 , q(qq)
51 {
52 }
53
54 void makeAccessKeyLabel(QChar accessKey, const WebEngineViewer::WebEngineAccessKeyAnchor &element);
55 bool checkForAccessKey(QKeyEvent *event);
56 QList<QLabel *> mAccessKeyLabels;
58 QHash<QString, QChar> mDuplicateLinkElements;
59 QWebEngineView *const mWebEngine;
60 AccessKeyState mAccessKeyActivated = NotActivated;
61 KActionCollection *mActionCollection = nullptr;
62 WebEngineAccessKey *const q = nullptr;
63};
64
65static QString linkElementKey(const WebEngineViewer::WebEngineAccessKeyAnchor &element, const QUrl &baseUrl)
66{
67 // qDebug()<<" element.href()"<<element.href();
68 if (!element.href().isEmpty()) {
69 const QUrl url = baseUrl.resolved(QUrl(element.href()));
70 // qDebug()<< "URL " << url;
71 QString linkKey(url.toString());
72 if (!element.target().isEmpty()) {
73 linkKey += QLatin1Char('+');
74 linkKey += element.target();
75 }
76 return linkKey;
77 }
78 return {};
79}
80
81static void
82handleDuplicateLinkElements(const WebEngineViewer::WebEngineAccessKeyAnchor &element, QHash<QString, QChar> *dupLinkList, QChar *accessKey, const QUrl &baseUrl)
83{
84 if (element.tagName().compare(QLatin1StringView("A"), Qt::CaseInsensitive) == 0) {
85 const QString linkKey(linkElementKey(element, baseUrl));
86 // qDebug() << "LINK KEY:" << linkKey;
87 if (dupLinkList->contains(linkKey)) {
88 // qDebug() << "***** Found duplicate link element:" << linkKey;
89 *accessKey = dupLinkList->value(linkKey);
90 } else if (!linkKey.isEmpty()) {
91 dupLinkList->insert(linkKey, *accessKey);
92 }
93 if (linkKey.isEmpty()) {
94 *accessKey = QChar();
95 }
96 }
97}
98
99static bool isHiddenElement(const WebEngineViewer::WebEngineAccessKeyAnchor &element)
100{
101 // width or height property set to less than zero
102 if (element.boundingRect().width() < 1 || element.boundingRect().height() < 1) {
103 return true;
104 }
105#if 0
106
107 // visibility set to 'hidden' in the element itself or its parent elements.
108 if (element.styleProperty(QStringLiteral("visibility"), QWebElement::ComputedStyle).compare(QLatin1StringView("hidden"), Qt::CaseInsensitive) == 0) {
109 return true;
110 }
111
112 // display set to 'none' in the element itself or its parent elements.
113 if (element.styleProperty(QStringLiteral("display"), QWebElement::ComputedStyle).compare(QLatin1StringView("none"), Qt::CaseInsensitive) == 0) {
114 return true;
115 }
116#endif
117 return false;
118}
119
120bool WebEngineAccessKeyPrivate::checkForAccessKey(QKeyEvent *event)
121{
122 if (mAccessKeyLabels.isEmpty()) {
123 return false;
124 }
125 QString text = event->text();
126 if (text.isEmpty()) {
127 return false;
128 }
129 QChar key = text.at(0).toUpper();
130 bool handled = false;
131 if (mAccessKeyNodes.contains(key)) {
132 WebEngineViewer::WebEngineAccessKeyAnchor element = mAccessKeyNodes.value(key);
133 if (element.tagName().compare(QLatin1StringView("A"), Qt::CaseInsensitive) == 0) {
134 const QString linkKey(linkElementKey(element, mWebEngine->url()));
135 if (!linkKey.isEmpty()) {
136 // qDebug()<<" WebEngineAccessKey::checkForAccessKey****"<<linkKey;
137 Q_EMIT q->openUrl(QUrl(linkKey));
138 handled = true;
139 }
140 }
141 }
142 return handled;
143}
144
145void WebEngineAccessKeyPrivate::makeAccessKeyLabel(QChar accessKey, const WebEngineViewer::WebEngineAccessKeyAnchor &element)
146{
147 // qDebug()<<" void WebEngineAccessKey::makeAccessKeyLabel(QChar accessKey, const WebEngineViewer::MailWebEngineAccessKeyAnchor &element)";
148 auto label = new QLabel(mWebEngine);
149 QFont font(label->font());
150 font.setBold(true);
151 label->setFont(font);
152 label->setText(accessKey);
153 QFontMetrics metric(label->font());
154 label->setFixedWidth(metric.boundingRect(QStringLiteral("WW")).width());
155 label->setPalette(QToolTip::palette());
156 label->setAutoFillBackground(true);
157 label->setFrameStyle(QFrame::Box | QFrame::Plain);
158 QPoint point = element.boundingRect().center();
159 label->move(point);
160 label->show();
161 point.setX(point.x() - label->width() / 2);
162 label->move(point);
163 mAccessKeyLabels.append(label);
164 mAccessKeyNodes.insert(accessKey, element);
165}
166
167WebEngineAccessKey::WebEngineAccessKey(QWebEngineView *webEngine, QObject *parent)
168 : QObject(parent)
169 , d(new WebEngineViewer::WebEngineAccessKeyPrivate(this, webEngine))
170{
171 // qDebug() << " WebEngineAccessKey::WebEngineAccessKey(QWebEngineView *webEngine, QObject *parent)";
172}
173
174WebEngineAccessKey::~WebEngineAccessKey() = default;
175
176void WebEngineAccessKey::setActionCollection(KActionCollection *ac)
177{
178 d->mActionCollection = ac;
179}
180
181void WebEngineAccessKey::wheelEvent(QWheelEvent *e)
182{
183 hideAccessKeys();
184 if (d->mAccessKeyActivated == WebEngineAccessKeyPrivate::PreActivated && (e->modifiers() & Qt::ControlModifier)) {
185 d->mAccessKeyActivated = WebEngineAccessKeyPrivate::NotActivated;
186 }
187}
188
189void WebEngineAccessKey::resizeEvent(QResizeEvent *)
190{
191 if (d->mAccessKeyActivated == WebEngineAccessKeyPrivate::Activated) {
192 hideAccessKeys();
193 }
194}
195
196void WebEngineAccessKey::keyPressEvent(QKeyEvent *e)
197{
198 if (e && d->mWebEngine->hasFocus()) {
199 if (d->mAccessKeyActivated == WebEngineAccessKeyPrivate::Activated) {
200 if (d->checkForAccessKey(e)) {
201 hideAccessKeys();
202 e->accept();
203 return;
204 }
205 hideAccessKeys();
206 } else if (e->key() == Qt::Key_Control && e->modifiers() == Qt::ControlModifier
207#if 0 // FIXME
208 && !isEditableElement(d->mWebView->page())
209#endif
210 ) {
211 d->mAccessKeyActivated = WebEngineAccessKeyPrivate::PreActivated; // Only preactive here, it will be actually activated in key release.
212 }
213 }
214}
215
216void WebEngineAccessKey::keyReleaseEvent(QKeyEvent *e)
217{
218 // qDebug() << " void WebEngineAccessKey::keyReleaseEvent(QKeyEvent *e)";
219 if (d->mAccessKeyActivated == WebEngineAccessKeyPrivate::PreActivated) {
220 // Activate only when the CTRL key is pressed and released by itself.
221 if (e->key() == Qt::Key_Control && e->modifiers() == Qt::NoModifier) {
222 showAccessKeys();
223 } else {
224 d->mAccessKeyActivated = WebEngineAccessKeyPrivate::NotActivated;
225 }
226 }
227}
228
229void WebEngineAccessKey::hideAccessKeys()
230{
231 if (!d->mAccessKeyLabels.isEmpty()) {
232 for (int i = 0, count = d->mAccessKeyLabels.count(); i < count; ++i) {
233 QLabel *label = d->mAccessKeyLabels[i];
234 label->hide();
235 label->deleteLater();
236 }
237 d->mAccessKeyLabels.clear();
238 d->mAccessKeyNodes.clear();
239 d->mDuplicateLinkElements.clear();
240 d->mAccessKeyActivated = WebEngineAccessKeyPrivate::NotActivated;
241 d->mWebEngine->update();
242 }
243}
244
245void WebEngineAccessKey::handleSearchAccessKey(const QVariant &res)
246{
247 // qDebug() << " void WebEngineAccessKey::handleSearchAccessKey(const QVariant &res)" << res;
248 const QVariantList lst = res.toList();
250 anchorList.reserve(lst.count());
251 for (const QVariant &var : lst) {
252 // qDebug()<<" var"<<var;
253 anchorList << WebEngineViewer::WebEngineAccessKeyAnchor(var);
254 }
255
257 unusedKeys.reserve(10 + ('Z' - 'A' + 1));
258 for (char c = 'A'; c <= 'Z'; ++c) {
260 }
261 for (char c = '0'; c <= '9'; ++c) {
263 }
264 if (d->mActionCollection) {
265 const auto actions = d->mActionCollection->actions();
266 for (QAction *act : actions) {
267 if (act) {
268 const QKeySequence shortCut = act->shortcut();
269 if (!shortCut.isEmpty()) {
271 for (QChar c : std::as_const(unusedKeys)) {
272 if (shortCut.matches(QKeySequence(c)) != QKeySequence::NoMatch) {
273 lstUnusedKeys.removeOne(c);
274 }
275 }
277 }
278 }
279 }
280 }
282 QRect viewport = d->mWebEngine->rect();
283 for (const WebEngineViewer::WebEngineAccessKeyAnchor &element : std::as_const(anchorList)) {
284 const QRect geometry = element.boundingRect();
285 if (geometry.size().isEmpty() || !viewport.contains(geometry.topLeft())) {
286 continue;
287 }
288 if (isHiddenElement(element)) {
289 continue; // Do not show access key for hidden elements...
290 }
291 const QString accessKeyAttribute(element.accessKey().toUpper());
292 if (accessKeyAttribute.isEmpty()) {
293 unLabeledElements.append(element);
294 continue;
295 }
296 QChar accessKey;
297 for (int i = 0; i < accessKeyAttribute.length(); i += 2) {
299 if (unusedKeys.contains(possibleAccessKey)) {
300 accessKey = possibleAccessKey;
301 break;
302 }
303 }
304 if (accessKey.isNull()) {
305 unLabeledElements.append(element);
306 continue;
307 }
308
309 handleDuplicateLinkElements(element, &d->mDuplicateLinkElements, &accessKey, d->mWebEngine->url());
310 if (!accessKey.isNull()) {
311 unusedKeys.removeOne(accessKey);
312 d->makeAccessKeyLabel(accessKey, element);
313 }
314 }
315
316 // Pick an access key first from the letters in the text and then from the
317 // list of unused access keys
318 for (const WebEngineViewer::WebEngineAccessKeyAnchor &element : std::as_const(unLabeledElements)) {
319 const QRect geometry = element.boundingRect();
320 if (unusedKeys.isEmpty() || geometry.size().isEmpty() || !viewport.contains(geometry.topLeft())) {
321 continue;
322 }
323 QChar accessKey;
324 const QString text = element.innerText().toUpper();
325 for (int i = 0, total = text.length(); i < total; ++i) {
326 const QChar &c = text.at(i);
327 if (unusedKeys.contains(c)) {
328 accessKey = c;
329 break;
330 }
331 }
332 if (accessKey.isNull()) {
333 accessKey = unusedKeys.takeFirst();
334 }
335
336 handleDuplicateLinkElements(element, &d->mDuplicateLinkElements, &accessKey, d->mWebEngine->url());
337 if (!accessKey.isNull()) {
338 unusedKeys.removeOne(accessKey);
339 d->makeAccessKeyLabel(accessKey, element);
340 }
341 }
342 d->mAccessKeyActivated = (!d->mAccessKeyLabels.isEmpty() ? WebEngineAccessKeyPrivate::Activated : WebEngineAccessKeyPrivate::NotActivated);
343}
344
345void WebEngineAccessKey::showAccessKeys()
346{
347 d->mAccessKeyActivated = WebEngineAccessKeyPrivate::Activated;
348 d->mWebEngine->page()->runJavaScript(WebEngineViewer::WebEngineAccessKeyUtils::accessKeyScript(),
349 WebEngineManageScript::scriptWordId(),
350 invokeWebAccessKey(this, &WebEngineAccessKey::handleSearchAccessKey));
351}
352
353#include "moc_webengineaccesskey.cpp"
constexpr bool isEmpty() const
The WebEngineAccessKey class.
QString label(StandardShortcut id)
bool isNull() const const
void accept()
bool contains(const Key &key) const const
iterator insert(const Key &key, const T &value)
T value(const Key &key) const const
Qt::KeyboardModifiers modifiers() const const
int key() const const
Qt::KeyboardModifiers modifiers() const const
void append(QList< T > &&value)
bool isEmpty() const const
bool contains(const Key &key, const T &value) const const
iterator insert(const Key &key, const T &value)
T value(const Key &key) const const
T qobject_cast(QObject *object)
void setX(int x)
int x() const const
QPoint center() const const
bool contains(const QPoint &point, bool proper) const const
int height() const const
QSize size() const const
QPoint topLeft() const const
int width() const const
bool isEmpty() const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
bool isEmpty() const const
QString toUpper() const const
CaseInsensitive
Key_Control
ControlModifier
QPalette palette()
QUrl resolved(const QUrl &relative) const const
QString toString(FormattingOptions options) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:44 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.