Messagelib

webengineaccesskey.cpp
1 /*
2  SPDX-FileCopyrightText: 2016-2023 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 {
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  QVector<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 
65 static 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 
81 static void
82 handleDuplicateLinkElements(const WebEngineViewer::WebEngineAccessKeyAnchor &element, QHash<QString, QChar> *dupLinkList, QChar *accessKey, const QUrl &baseUrl)
83 {
84  if (element.tagName().compare(QLatin1String("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 
99 static 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(QLatin1String("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(QLatin1String("none"), Qt::CaseInsensitive) == 0) {
114  return true;
115  }
116 #endif
117  return false;
118 }
119 
120 bool 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(QLatin1String("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 
145 void 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 
167 WebEngineAccessKey::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 
174 WebEngineAccessKey::~WebEngineAccessKey() = default;
175 
176 void WebEngineAccessKey::setActionCollection(KActionCollection *ac)
177 {
178  d->mActionCollection = ac;
179 }
180 
181 void 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 
189 void WebEngineAccessKey::resizeEvent(QResizeEvent *)
190 {
191  if (d->mAccessKeyActivated == WebEngineAccessKeyPrivate::Activated) {
192  hideAccessKeys();
193  }
194 }
195 
196 void 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 
216 void 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 
229 void 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 
245 void 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 
256  QVector<QChar> unusedKeys;
257  unusedKeys.reserve(10 + ('Z' - 'A' + 1));
258  for (char c = 'A'; c <= 'Z'; ++c) {
259  unusedKeys << QLatin1Char(c);
260  }
261  for (char c = '0'; c <= '9'; ++c) {
262  unusedKeys << QLatin1Char(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()) {
270  auto lstUnusedKeys = unusedKeys;
271  for (QChar c : std::as_const(unusedKeys)) {
272  if (shortCut.matches(QKeySequence(c)) != QKeySequence::NoMatch) {
273  lstUnusedKeys.removeOne(c);
274  }
275  }
276  unusedKeys = lstUnusedKeys;
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) {
298  const QChar &possibleAccessKey = accessKeyAttribute[i];
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 
345 void WebEngineAccessKey::showAccessKeys()
346 {
347  d->mAccessKeyActivated = WebEngineAccessKeyPrivate::Activated;
348  d->mWebEngine->page()->runJavaScript(WebEngineViewer::WebEngineAccessKeyUtils::script(),
349  WebEngineManageScript::scriptWordId(),
350  invokeWebAccessKey(this, &WebEngineAccessKey::handleSearchAccessKey));
351 }
const T value(const Key &key) const const
bool isEmpty() const const
QPoint topLeft() const const
CaseInsensitive
QSize size() const const
bool removeOne(const T &t)
bool isEmpty() const const
QPalette palette()
void clear()
void append(const T &value)
QKeySequence::SequenceMatch matches(const QKeySequence &seq) const const
int x() const const
bool contains(const QRect &rectangle, bool proper) const const
QHash::iterator insert(const Key &key, const T &value)
Qt::KeyboardModifiers modifiers() const const
QString toString(QUrl::FormattingOptions options) const const
constexpr bool isEmpty() const
void setX(int x)
bool contains(const T &value) const const
Key_Control
QUrl resolved(const QUrl &relative) const const
void reserve(int size)
T takeFirst()
Qt::KeyboardModifiers modifiers() const const
QString label(StandardShortcut id)
int key() const const
QString & insert(int position, QChar ch)
QList< QVariant > toList() const const
The WebEngineAccessKey class.
bool contains(const Key &key) const const
ControlModifier
bool isEmpty() const const
bool isNull() const const
QString & append(QChar ch)
void accept()
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Fri Mar 24 2023 04:08:32 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.