Messagelib

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

KDE's Doxygen guidelines are available online.