Advance Wayland and KDE package bring-up
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
/*
|
||||
* 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"
|
||||
Reference in New Issue
Block a user