Files
RedBear-OS/local/recipes/kde/kirigami/source/src/mnemonicattached.cpp
T
2026-04-14 10:51:06 +01:00

497 lines
13 KiB
C++

/*
* SPDX-FileCopyrightText: 2017 Marco Martin <mart@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "mnemonicattached.h"
#include <QDebug>
#include <QGuiApplication>
#include <QQuickItem>
#include <QQuickRenderControl>
#include <QQuickWindow>
#include <QRegularExpression>
#include <QWindow>
QHash<QKeySequence, MnemonicAttached *> MnemonicAttached::s_sequenceToObject = QHash<QKeySequence, MnemonicAttached *>();
// If pos points to alphanumeric X in "...(X)...", which is preceded or
// followed only by non-alphanumerics, then "(X)" gets removed.
static QString removeReducedCJKAccMark(const QString &label, int pos)
{
/* clang-format off */
if (pos > 0 && pos + 1 < label.length()
&& label[pos - 1] == QLatin1Char('(')
&& label[pos + 1] == QLatin1Char(')')
&& label[pos].isLetterOrNumber()) { /* clang-format on */
// Check if at start or end, ignoring non-alphanumerics.
int len = label.length();
int p1 = pos - 2;
while (p1 >= 0 && !label[p1].isLetterOrNumber()) {
--p1;
}
++p1;
int p2 = pos + 2;
while (p2 < len && !label[p2].isLetterOrNumber()) {
++p2;
}
--p2;
const QStringView strView(label);
if (p1 == 0) {
return strView.left(pos - 1) + strView.mid(p2 + 1);
} else if (p2 + 1 == len) {
return strView.left(p1) + strView.mid(pos + 2);
}
}
return label;
}
static QString removeAcceleratorMarker(const QString &label_)
{
QString label = label_;
int p = 0;
bool accmarkRemoved = false;
while (true) {
p = label.indexOf(QLatin1Char('&'), p);
if (p < 0 || p + 1 == label.length()) {
break;
}
if (label.at(p + 1).isLetterOrNumber()) {
// Valid accelerator.
const QStringView sv(label);
label = sv.left(p) + sv.mid(p + 1);
// May have been an accelerator in CJK-style "(&X)"
// at the start or end of text.
label = removeReducedCJKAccMark(label, p);
accmarkRemoved = true;
} else if (label.at(p + 1) == QLatin1Char('&')) {
// Escaped accelerator marker.
const QStringView sv(label);
label = sv.left(p) + sv.mid(p + 1);
}
++p;
}
// If no marker was removed, and there are CJK characters in the label,
// also try to remove reduced CJK marker -- something may have removed
// ampersand beforehand.
if (!accmarkRemoved) {
bool hasCJK = false;
for (const QChar c : std::as_const(label)) {
if (c.unicode() >= 0x2e00) { // rough, but should be sufficient
hasCJK = true;
break;
}
}
if (hasCJK) {
p = 0;
while (true) {
p = label.indexOf(QLatin1Char('('), p);
if (p < 0) {
break;
}
label = removeReducedCJKAccMark(label, p + 1);
++p;
}
}
}
return label;
}
class MnemonicEventFilter : public QObject
{
Q_OBJECT
public:
static MnemonicEventFilter &instance()
{
static MnemonicEventFilter s_instance;
return s_instance;
}
bool eventFilter(QObject *watched, QEvent *event) override
{
Q_UNUSED(watched);
if (event->type() == QEvent::KeyPress) {
QKeyEvent *ke = static_cast<QKeyEvent *>(event);
if (ke->key() == Qt::Key_Alt) {
m_altPressed = true;
Q_EMIT altPressed();
}
} else if (event->type() == QEvent::KeyRelease) {
QKeyEvent *ke = static_cast<QKeyEvent *>(event);
if (ke->key() == Qt::Key_Alt) {
m_altPressed = false;
Q_EMIT altReleased();
}
} else if (event->type() == QEvent::ApplicationStateChange) {
if (m_altPressed) {
m_altPressed = false;
Q_EMIT altReleased();
}
}
return false;
}
Q_SIGNALS:
void altPressed();
void altReleased();
private:
MnemonicEventFilter()
: QObject(nullptr)
{
qGuiApp->installEventFilter(this);
}
bool m_altPressed = false;
};
MnemonicAttached::MnemonicAttached(QObject *parent)
: QObject(parent)
{
connect(&MnemonicEventFilter::instance(), &MnemonicEventFilter::altPressed, this, &MnemonicAttached::onAltPressed);
connect(&MnemonicEventFilter::instance(), &MnemonicEventFilter::altReleased, this, &MnemonicAttached::onAltReleased);
}
MnemonicAttached::~MnemonicAttached()
{
s_sequenceToObject.remove(m_sequence);
}
QWindow *MnemonicAttached::window() const
{
if (auto *parentItem = qobject_cast<QQuickItem *>(parent())) {
if (auto *window = parentItem->window()) {
if (auto *renderWindow = QQuickRenderControl::renderWindowFor(window)) {
return renderWindow;
}
return window;
}
}
return nullptr;
}
void MnemonicAttached::onAltPressed()
{
if (m_active || m_richTextLabel.isEmpty()) {
return;
}
auto *win = window();
if (!win || !win->isActive()) {
return;
}
m_actualRichTextLabel = m_richTextLabel;
Q_EMIT richTextLabelChanged();
m_active = true;
Q_EMIT activeChanged();
}
void MnemonicAttached::onAltReleased()
{
if (!m_active || m_richTextLabel.isEmpty()) {
return;
}
// Disabling menmonics again is always fine, e.g. on window deactivation,
// don't check for window is active here.
m_actualRichTextLabel = removeAcceleratorMarker(m_label);
Q_EMIT richTextLabelChanged();
m_active = false;
Q_EMIT activeChanged();
}
// Algorithm adapted from KAccelString
void MnemonicAttached::calculateWeights()
{
m_weights.clear();
int pos = 0;
bool start_character = true;
bool wanted_character = false;
while (pos < m_label.length()) {
QChar c = m_label[pos];
// skip non typeable characters
if (!c.isLetterOrNumber() && c != QLatin1Char('&')) {
start_character = true;
++pos;
continue;
}
int weight = 1;
// add special weight to first character
if (pos == 0) {
weight += FIRST_CHARACTER_EXTRA_WEIGHT;
// add weight to word beginnings
} else if (start_character) {
weight += WORD_BEGINNING_EXTRA_WEIGHT;
start_character = false;
}
// add weight to characters that have an & beforehand
if (wanted_character) {
weight += WANTED_ACCEL_EXTRA_WEIGHT;
wanted_character = false;
}
// add decreasing weight to left characters
if (pos < 50) {
weight += (50 - pos);
}
// try to preserve the wanted accelerators
/* clang-format off */
if (c == QLatin1Char('&')
&& (pos != m_label.length() - 1
&& m_label[pos + 1] != QLatin1Char('&')
&& m_label[pos + 1].isLetterOrNumber())) { /* clang-format on */
wanted_character = true;
++pos;
continue;
}
while (m_weights.contains(weight)) {
++weight;
}
if (c != QLatin1Char('&')) {
m_weights[weight] = c;
}
++pos;
}
// update our maximum weight
if (m_weights.isEmpty()) {
m_weight = m_baseWeight;
} else {
m_weight = m_baseWeight + (std::prev(m_weights.cend())).key();
}
}
void MnemonicAttached::updateSequence()
{
const QKeySequence oldSequence = m_sequence;
if (!m_sequence.isEmpty()) {
s_sequenceToObject.remove(m_sequence);
m_sequence = {};
}
calculateWeights();
// Preserve strings like "One & Two" where & is not an accelerator escape
const QString text = label().replace(QStringLiteral("& "), QStringLiteral("&& "));
m_actualRichTextLabel = removeAcceleratorMarker(text);
if (!m_enabled) {
// was the label already completely plain text? try to limit signal emission
if (m_mnemonicLabel != m_actualRichTextLabel) {
m_mnemonicLabel = m_actualRichTextLabel;
Q_EMIT mnemonicLabelChanged();
Q_EMIT richTextLabelChanged();
}
if (m_sequence != oldSequence) {
Q_EMIT sequenceChanged();
}
return;
}
m_mnemonicLabel = text;
m_mnemonicLabel.replace(QRegularExpression(QLatin1String("\\&([^\\&])")), QStringLiteral("\\1"));
if (!m_weights.isEmpty()) {
QMap<int, QChar>::const_iterator i = m_weights.constEnd();
do {
--i;
QChar c = i.value();
QKeySequence ks(QStringLiteral("Alt+") % c);
MnemonicAttached *otherMa = s_sequenceToObject.value(ks);
Q_ASSERT(otherMa != this);
if (!otherMa || otherMa->m_weight < m_weight) {
// the old shortcut is less valuable than the current: remove it
if (otherMa) {
s_sequenceToObject.remove(otherMa->sequence());
otherMa->m_sequence = {};
}
s_sequenceToObject[ks] = this;
m_sequence = ks;
m_richTextLabel = text;
m_richTextLabel.replace(QRegularExpression(QLatin1String("\\&([^\\&])")), QStringLiteral("\\1"));
m_mnemonicLabel = text;
const int mnemonicPos = m_mnemonicLabel.indexOf(c);
if (mnemonicPos > -1 && (mnemonicPos == 0 || m_mnemonicLabel[mnemonicPos - 1] != QLatin1Char('&'))) {
m_mnemonicLabel.replace(mnemonicPos, 1, QStringLiteral("&") % c);
}
const int richTextPos = m_richTextLabel.indexOf(c);
if (richTextPos > -1) {
m_richTextLabel.replace(richTextPos, 1, QLatin1String("<u>") % c % QLatin1String("</u>"));
}
// remap the sequence of the previous shortcut
if (otherMa) {
otherMa->updateSequence();
}
break;
}
} while (i != m_weights.constBegin());
}
if (m_sequence != oldSequence) {
Q_EMIT sequenceChanged();
}
Q_EMIT richTextLabelChanged();
Q_EMIT mnemonicLabelChanged();
}
void MnemonicAttached::setLabel(const QString &text)
{
if (m_label == text) {
return;
}
m_label = text;
updateSequence();
Q_EMIT labelChanged();
}
QString MnemonicAttached::richTextLabel() const
{
if (!m_actualRichTextLabel.isEmpty()) {
return m_actualRichTextLabel;
} else {
return removeAcceleratorMarker(m_label);
}
}
QString MnemonicAttached::mnemonicLabel() const
{
return m_mnemonicLabel;
}
QString MnemonicAttached::label() const
{
return m_label;
}
void MnemonicAttached::setEnabled(bool enabled)
{
if (m_enabled == enabled) {
return;
}
m_enabled = enabled;
updateSequence();
Q_EMIT enabledChanged();
}
bool MnemonicAttached::enabled() const
{
return m_enabled;
}
void MnemonicAttached::setControlType(MnemonicAttached::ControlType controlType)
{
if (m_controlType == controlType) {
return;
}
m_controlType = controlType;
switch (controlType) {
case ActionElement:
m_baseWeight = ACTION_ELEMENT_WEIGHT;
break;
case DialogButton:
m_baseWeight = DIALOG_BUTTON_EXTRA_WEIGHT;
break;
case MenuItem:
m_baseWeight = MENU_ITEM_WEIGHT;
break;
case FormLabel:
m_baseWeight = FORM_LABEL_WEIGHT;
break;
default:
m_baseWeight = SECONDARY_CONTROL_WEIGHT;
break;
}
// update our maximum weight
if (m_weights.isEmpty()) {
m_weight = m_baseWeight;
} else {
m_weight = m_baseWeight + (std::prev(m_weights.constEnd())).key();
}
Q_EMIT controlTypeChanged();
}
MnemonicAttached::ControlType MnemonicAttached::controlType() const
{
return m_controlType;
}
QKeySequence MnemonicAttached::sequence()
{
return m_sequence;
}
bool MnemonicAttached::active() const
{
return m_active;
}
MnemonicAttached *MnemonicAttached::qmlAttachedProperties(QObject *object)
{
return new MnemonicAttached(object);
}
void MnemonicAttached::setActive(bool active)
{
// We can't rely on previous value when it's true since it can be
// caused by Alt key press and we need to remove the event filter
// additionally. False should be ok as it's a default state.
if (!m_active && m_active == active) {
return;
}
m_active = active;
if (m_active) {
if (m_actualRichTextLabel != m_richTextLabel) {
m_actualRichTextLabel = m_richTextLabel;
Q_EMIT richTextLabelChanged();
}
} else {
m_actualRichTextLabel = removeAcceleratorMarker(m_label);
Q_EMIT richTextLabelChanged();
}
Q_EMIT activeChanged();
}
#include "mnemonicattached.moc"