cf12defd28
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
578 lines
19 KiB
C++
578 lines
19 KiB
C++
/*
|
|
This file is part of the KDE libraries
|
|
SPDX-FileCopyrightText: 1998 Mark Donohoe <donohoe@kde.org>
|
|
SPDX-FileCopyrightText: 2001 Ellis Whitehead <ellis@kde.org>
|
|
SPDX-FileCopyrightText: 2007 Andreas Hartmetz <ahartmetz@gmail.com>
|
|
SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
|
|
|
|
SPDX-License-Identifier: LGPL-2.0-or-later
|
|
*/
|
|
|
|
#include "config-xmlgui.h"
|
|
|
|
#include "kkeysequencewidget.h"
|
|
|
|
#include "debug.h"
|
|
#include "kactioncollection.h"
|
|
|
|
#include <QAction>
|
|
#include <QApplication>
|
|
#include <QHBoxLayout>
|
|
#include <QHash>
|
|
#include <QToolButton>
|
|
|
|
#include <KKeySequenceRecorder>
|
|
#include <KLocalizedString>
|
|
#include <KMessageBox>
|
|
#if HAVE_GLOBALACCEL
|
|
#include <KGlobalAccel>
|
|
#endif
|
|
|
|
static constexpr QStringView inputRecordingMarkupSuffix(u" …");
|
|
|
|
static bool shortcutsConflictWith(const QList<QKeySequence> &shortcuts, const QKeySequence &needle)
|
|
{
|
|
if (needle.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
for (const QKeySequence &sequence : shortcuts) {
|
|
if (sequence.isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
if (sequence.matches(needle) != QKeySequence::NoMatch //
|
|
|| needle.matches(sequence) != QKeySequence::NoMatch) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
class KKeySequenceWidgetPrivate
|
|
{
|
|
public:
|
|
KKeySequenceWidgetPrivate(KKeySequenceWidget *qq);
|
|
|
|
void init();
|
|
|
|
void updateShortcutDisplay();
|
|
void startRecording();
|
|
|
|
// Conflicts the key sequence @p seq with a current standard shortcut?
|
|
bool conflictWithStandardShortcuts(const QKeySequence &seq);
|
|
// Conflicts the key sequence @p seq with a current local shortcut?
|
|
bool conflictWithLocalShortcuts(const QKeySequence &seq);
|
|
// Conflicts the key sequence @p seq with a current global shortcut?
|
|
bool conflictWithGlobalShortcuts(const QKeySequence &seq);
|
|
|
|
bool promptStealLocalShortcut(const QList<QAction *> &actions, const QKeySequence &seq);
|
|
bool promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq);
|
|
|
|
#if HAVE_GLOBALACCEL
|
|
struct KeyConflictInfo {
|
|
QKeySequence key;
|
|
QList<KGlobalShortcutInfo> shortcutInfo;
|
|
};
|
|
bool promptStealGlobalShortcut(const std::vector<KeyConflictInfo> &shortcuts, const QKeySequence &sequence);
|
|
#endif
|
|
void wontStealShortcut(QAction *item, const QKeySequence &seq);
|
|
|
|
bool checkAgainstStandardShortcuts() const
|
|
{
|
|
return checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts;
|
|
}
|
|
|
|
bool checkAgainstGlobalShortcuts() const
|
|
{
|
|
return checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts;
|
|
}
|
|
|
|
bool checkAgainstLocalShortcuts() const
|
|
{
|
|
return checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts;
|
|
}
|
|
|
|
// private slot
|
|
void doneRecording();
|
|
|
|
// members
|
|
KKeySequenceWidget *const q;
|
|
KKeySequenceRecorder *recorder;
|
|
QHBoxLayout *layout;
|
|
QPushButton *keyButton;
|
|
QToolButton *clearButton;
|
|
|
|
QKeySequence keySequence;
|
|
QKeySequence oldKeySequence;
|
|
QString componentName;
|
|
|
|
//! Check the key sequence against KStandardShortcut::find()
|
|
KKeySequenceWidget::ShortcutTypes checkAgainstShortcutTypes;
|
|
|
|
/**
|
|
* The list of action collections to check against for conflict shortcut
|
|
*/
|
|
QList<KActionCollection *> checkActionCollections;
|
|
|
|
/**
|
|
* The action to steal the shortcut from.
|
|
*/
|
|
QList<QAction *> stealActions;
|
|
};
|
|
|
|
KKeySequenceWidgetPrivate::KKeySequenceWidgetPrivate(KKeySequenceWidget *qq)
|
|
: q(qq)
|
|
, layout(nullptr)
|
|
, keyButton(nullptr)
|
|
, clearButton(nullptr)
|
|
, componentName()
|
|
, checkAgainstShortcutTypes(KKeySequenceWidget::LocalShortcuts | KKeySequenceWidget::GlobalShortcuts)
|
|
, stealActions()
|
|
{
|
|
}
|
|
|
|
void KKeySequenceWidgetPrivate::init()
|
|
{
|
|
layout = new QHBoxLayout(q);
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
keyButton = new QPushButton(q);
|
|
keyButton->setFocusPolicy(Qt::StrongFocus);
|
|
keyButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
|
|
keyButton->setToolTip(
|
|
i18nc("@info:tooltip",
|
|
"Click on the button, then enter the shortcut like you would in the program.\nExample for Ctrl+A: hold the Ctrl key and press A."));
|
|
layout->addWidget(keyButton);
|
|
|
|
clearButton = new QToolButton(q);
|
|
layout->addWidget(clearButton);
|
|
|
|
if (qApp->isLeftToRight()) {
|
|
clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl")));
|
|
} else {
|
|
clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-ltr")));
|
|
}
|
|
|
|
recorder = new KKeySequenceRecorder(q->window()->windowHandle(), q);
|
|
recorder->setModifierlessAllowed(false);
|
|
recorder->setMultiKeyShortcutsAllowed(true);
|
|
|
|
updateShortcutDisplay();
|
|
}
|
|
|
|
bool KKeySequenceWidgetPrivate::promptStealLocalShortcut(const QList<QAction *> &actions, const QKeySequence &seq)
|
|
{
|
|
const int listSize = actions.size();
|
|
|
|
QString title = i18ncp("%1 is the number of conflicts", "Shortcut Conflict", "Shortcut Conflicts", listSize);
|
|
|
|
QString conflictingShortcuts;
|
|
for (const QAction *action : actions) {
|
|
conflictingShortcuts += i18n("Shortcut '%1' for action '%2'\n",
|
|
action->shortcut().toString(QKeySequence::NativeText),
|
|
KLocalizedString::removeAcceleratorMarker(action->text()));
|
|
}
|
|
QString message = i18ncp("%1 is the number of ambiguous shortcut clashes (hidden)",
|
|
"The \"%2\" shortcut is ambiguous with the following shortcut.\n"
|
|
"Do you want to assign an empty shortcut to this action?\n"
|
|
"%3",
|
|
"The \"%2\" shortcut is ambiguous with the following shortcuts.\n"
|
|
"Do you want to assign an empty shortcut to these actions?\n"
|
|
"%3",
|
|
listSize,
|
|
seq.toString(QKeySequence::NativeText),
|
|
conflictingShortcuts);
|
|
|
|
return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
|
|
}
|
|
|
|
void KKeySequenceWidgetPrivate::wontStealShortcut(QAction *item, const QKeySequence &seq)
|
|
{
|
|
QString title(i18nc("@title:window", "Shortcut conflict"));
|
|
QString msg(
|
|
i18n("<qt>The '%1' key combination is already used by the <b>%2</b> action.<br>"
|
|
"Please select a different one.</qt>",
|
|
seq.toString(QKeySequence::NativeText),
|
|
KLocalizedString::removeAcceleratorMarker(item->text())));
|
|
KMessageBox::error(q, msg, title);
|
|
}
|
|
|
|
bool KKeySequenceWidgetPrivate::conflictWithLocalShortcuts(const QKeySequence &keySequence)
|
|
{
|
|
if (!(checkAgainstShortcutTypes & KKeySequenceWidget::LocalShortcuts)) {
|
|
return false;
|
|
}
|
|
|
|
// Add all the actions from the checkActionCollections list to a single list to
|
|
// be able to process them in a single loop below.
|
|
// Note that this can't be done in setCheckActionCollections(), because we
|
|
// keep pointers to the action collections, and between the call to
|
|
// setCheckActionCollections() and this function some actions might already be
|
|
// removed from the collection again.
|
|
QList<QAction *> allActions;
|
|
for (KActionCollection *collection : std::as_const(checkActionCollections)) {
|
|
allActions += collection->actions();
|
|
}
|
|
|
|
// Because of multikey shortcuts we can have clashes with many shortcuts.
|
|
//
|
|
// Example 1:
|
|
//
|
|
// Application currently uses 'CTRL-X,a', 'CTRL-X,f' and 'CTRL-X,CTRL-F'
|
|
// and the user wants to use 'CTRL-X'. 'CTRL-X' will only trigger as
|
|
// 'activatedAmbiguously()' for obvious reasons.
|
|
//
|
|
// Example 2:
|
|
//
|
|
// Application currently uses 'CTRL-X'. User wants to use 'CTRL-X,CTRL-F'.
|
|
// This will shadow 'CTRL-X' for the same reason as above.
|
|
//
|
|
// Example 3:
|
|
//
|
|
// Some weird combination of Example 1 and 2 with three shortcuts using
|
|
// 1/2/3 key shortcuts. I think you can imagine.
|
|
QList<QAction *> conflictingActions;
|
|
|
|
// find conflicting shortcuts with existing actions
|
|
for (QAction *qaction : std::as_const(allActions)) {
|
|
if (shortcutsConflictWith(qaction->shortcuts(), keySequence)) {
|
|
// A conflict with a KAction. If that action is configurable
|
|
// ask the user what to do. If not reject this keySequence.
|
|
if (KActionCollection::isShortcutsConfigurable(qaction)) {
|
|
conflictingActions.append(qaction);
|
|
} else {
|
|
wontStealShortcut(qaction, keySequence);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (conflictingActions.isEmpty()) {
|
|
// No conflicting shortcuts found.
|
|
return false;
|
|
}
|
|
|
|
if (promptStealLocalShortcut(conflictingActions, keySequence)) {
|
|
stealActions = conflictingActions;
|
|
// Announce that the user agreed
|
|
for (QAction *stealAction : std::as_const(stealActions)) {
|
|
Q_EMIT q->stealShortcut(keySequence, stealAction);
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#if HAVE_GLOBALACCEL
|
|
bool KKeySequenceWidgetPrivate::promptStealGlobalShortcut(const std::vector<KeyConflictInfo> &clashing, const QKeySequence &sequence)
|
|
{
|
|
QString clashingKeys;
|
|
for (const auto &[key, shortcutInfo] : clashing) {
|
|
const QString seqAsString = key.toString();
|
|
for (const KGlobalShortcutInfo &info : shortcutInfo) {
|
|
clashingKeys += i18n("Shortcut '%1' in Application '%2' for action '%3'\n", //
|
|
seqAsString,
|
|
info.componentFriendlyName(),
|
|
info.friendlyName());
|
|
}
|
|
}
|
|
const int hashSize = clashing.size();
|
|
|
|
QString message = i18ncp("%1 is the number of conflicts (hidden), %2 is the key sequence of the shortcut that is problematic",
|
|
"The shortcut '%2' conflicts with the following key combination:\n",
|
|
"The shortcut '%2' conflicts with the following key combinations:\n",
|
|
hashSize,
|
|
sequence.toString());
|
|
message += clashingKeys;
|
|
|
|
QString title = i18ncp("%1 is the number of shortcuts with which there is a conflict",
|
|
"Conflict with Registered Global Shortcut",
|
|
"Conflict with Registered Global Shortcuts",
|
|
hashSize);
|
|
|
|
return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
|
|
}
|
|
#endif
|
|
|
|
bool KKeySequenceWidgetPrivate::conflictWithGlobalShortcuts(const QKeySequence &keySequence)
|
|
{
|
|
#ifdef Q_OS_WIN
|
|
// on windows F12 is reserved by the debugger at all times, so we can't use it for a global shortcut
|
|
if (KKeySequenceWidget::GlobalShortcuts && keySequence.toString().contains(QLatin1String("F12"))) {
|
|
QString title = i18n("Reserved Shortcut");
|
|
QString message = i18n(
|
|
"The F12 key is reserved on Windows, so cannot be used for a global shortcut.\n"
|
|
"Please choose another one.");
|
|
|
|
KMessageBox::error(q, message, title);
|
|
return false;
|
|
}
|
|
#endif
|
|
#if HAVE_GLOBALACCEL
|
|
if (!(checkAgainstShortcutTypes & KKeySequenceWidget::GlobalShortcuts)) {
|
|
return false;
|
|
}
|
|
// Global shortcuts are on key+modifier shortcuts. They can clash with
|
|
// each of the keys of a multi key shortcut.
|
|
std::vector<KeyConflictInfo> clashing;
|
|
for (int i = 0; i < keySequence.count(); ++i) {
|
|
QKeySequence keys(keySequence[i]);
|
|
if (!KGlobalAccel::isGlobalShortcutAvailable(keySequence, componentName)) {
|
|
clashing.push_back({keySequence, KGlobalAccel::globalShortcutsByKey(keys)});
|
|
}
|
|
}
|
|
if (clashing.empty()) {
|
|
return false;
|
|
}
|
|
|
|
if (!promptStealGlobalShortcut(clashing, keySequence)) {
|
|
return true;
|
|
}
|
|
// The user approved stealing the shortcut. We have to steal
|
|
// it immediately because KAction::setGlobalShortcut() refuses
|
|
// to set a global shortcut that is already used. There is no
|
|
// error it just silently fails. So be nice because this is
|
|
// most likely the first action that is done in the slot
|
|
// listening to keySequenceChanged().
|
|
KGlobalAccel::stealShortcutSystemwide(keySequence);
|
|
return false;
|
|
#else
|
|
Q_UNUSED(keySequence);
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool KKeySequenceWidgetPrivate::promptstealStandardShortcut(KStandardShortcut::StandardShortcut std, const QKeySequence &seq)
|
|
{
|
|
QString title = i18nc("@title:window", "Conflict with Standard Application Shortcut");
|
|
QString message = i18n(
|
|
"The '%1' key combination is also used for the standard action "
|
|
"\"%2\" that some applications use.\n"
|
|
"Do you really want to use it as a global shortcut as well?",
|
|
seq.toString(QKeySequence::NativeText),
|
|
KStandardShortcut::label(std));
|
|
|
|
return KMessageBox::warningContinueCancel(q, message, title, KGuiItem(i18nc("@action:button", "Reassign"))) == KMessageBox::Continue;
|
|
}
|
|
|
|
bool KKeySequenceWidgetPrivate::conflictWithStandardShortcuts(const QKeySequence &seq)
|
|
{
|
|
if (!(checkAgainstShortcutTypes & KKeySequenceWidget::StandardShortcuts)) {
|
|
return false;
|
|
}
|
|
KStandardShortcut::StandardShortcut ssc = KStandardShortcut::find(seq);
|
|
if (ssc != KStandardShortcut::AccelNone && !promptstealStandardShortcut(ssc, seq)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void KKeySequenceWidgetPrivate::startRecording()
|
|
{
|
|
keyButton->setDown(true);
|
|
recorder->startRecording();
|
|
updateShortcutDisplay();
|
|
}
|
|
|
|
void KKeySequenceWidgetPrivate::doneRecording()
|
|
{
|
|
keyButton->setDown(false);
|
|
stealActions.clear();
|
|
keyButton->setText(keyButton->text().chopped(inputRecordingMarkupSuffix.size()));
|
|
q->setKeySequence(recorder->currentKeySequence(), KKeySequenceWidget::Validate);
|
|
updateShortcutDisplay();
|
|
}
|
|
|
|
void KKeySequenceWidgetPrivate::updateShortcutDisplay()
|
|
{
|
|
QString s;
|
|
QKeySequence sequence = recorder->isRecording() ? recorder->currentKeySequence() : keySequence;
|
|
if (!sequence.isEmpty()) {
|
|
s = sequence.toString(QKeySequence::NativeText);
|
|
} else if (recorder->isRecording()) {
|
|
s = i18nc("What the user inputs now will be taken as the new shortcut", "Input");
|
|
} else {
|
|
s = i18nc("No shortcut defined", "None");
|
|
}
|
|
|
|
if (recorder->isRecording()) {
|
|
// make it clear that input is still going on
|
|
s.append(inputRecordingMarkupSuffix);
|
|
}
|
|
|
|
s = QLatin1Char(' ') + s + QLatin1Char(' ');
|
|
keyButton->setText(s);
|
|
}
|
|
|
|
KKeySequenceWidget::KKeySequenceWidget(QWidget *parent)
|
|
: QWidget(parent)
|
|
, d(new KKeySequenceWidgetPrivate(this))
|
|
{
|
|
d->init();
|
|
setFocusProxy(d->keyButton);
|
|
connect(d->keyButton, &QPushButton::clicked, this, &KKeySequenceWidget::captureKeySequence);
|
|
connect(d->clearButton, &QToolButton::clicked, this, &KKeySequenceWidget::clearKeySequence);
|
|
|
|
connect(d->recorder, &KKeySequenceRecorder::currentKeySequenceChanged, this, [this] {
|
|
d->updateShortcutDisplay();
|
|
});
|
|
connect(d->recorder, &KKeySequenceRecorder::recordingChanged, this, [this] {
|
|
if (!d->recorder->isRecording()) {
|
|
d->doneRecording();
|
|
}
|
|
});
|
|
}
|
|
|
|
KKeySequenceWidget::~KKeySequenceWidget()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
KKeySequenceWidget::ShortcutTypes KKeySequenceWidget::checkForConflictsAgainst() const
|
|
{
|
|
return d->checkAgainstShortcutTypes;
|
|
}
|
|
|
|
void KKeySequenceWidget::setComponentName(const QString &componentName)
|
|
{
|
|
d->componentName = componentName;
|
|
}
|
|
|
|
bool KKeySequenceWidget::multiKeyShortcutsAllowed() const
|
|
{
|
|
return d->recorder->multiKeyShortcutsAllowed();
|
|
}
|
|
|
|
void KKeySequenceWidget::setMultiKeyShortcutsAllowed(bool allowed)
|
|
{
|
|
d->recorder->setMultiKeyShortcutsAllowed(allowed);
|
|
}
|
|
|
|
void KKeySequenceWidget::setCheckForConflictsAgainst(ShortcutTypes types)
|
|
{
|
|
d->checkAgainstShortcutTypes = types;
|
|
}
|
|
|
|
void KKeySequenceWidget::setModifierlessAllowed(bool allow)
|
|
{
|
|
d->recorder->setModifierlessAllowed(allow);
|
|
}
|
|
|
|
bool KKeySequenceWidget::isKeySequenceAvailable(const QKeySequence &keySequence) const
|
|
{
|
|
if (keySequence.isEmpty()) {
|
|
return true;
|
|
}
|
|
return !(d->conflictWithLocalShortcuts(keySequence) //
|
|
|| d->conflictWithGlobalShortcuts(keySequence) //
|
|
|| d->conflictWithStandardShortcuts(keySequence));
|
|
}
|
|
|
|
bool KKeySequenceWidget::isModifierlessAllowed()
|
|
{
|
|
return d->recorder->modifierlessAllowed();
|
|
}
|
|
|
|
bool KKeySequenceWidget::modifierOnlyAllowed() const
|
|
{
|
|
return d->recorder->modifierOnlyAllowed();
|
|
}
|
|
|
|
void KKeySequenceWidget::setModifierOnlyAllowed(bool allow)
|
|
{
|
|
d->recorder->setModifierOnlyAllowed(allow);
|
|
}
|
|
|
|
void KKeySequenceWidget::setClearButtonShown(bool show)
|
|
{
|
|
d->clearButton->setVisible(show);
|
|
}
|
|
|
|
void KKeySequenceWidget::setCheckActionCollections(const QList<KActionCollection *> &actionCollections)
|
|
{
|
|
d->checkActionCollections = actionCollections;
|
|
}
|
|
|
|
// slot
|
|
void KKeySequenceWidget::captureKeySequence()
|
|
{
|
|
d->recorder->setWindow(window()->windowHandle());
|
|
d->recorder->startRecording();
|
|
}
|
|
|
|
QKeySequence KKeySequenceWidget::keySequence() const
|
|
{
|
|
return d->keySequence;
|
|
}
|
|
|
|
// slot
|
|
void KKeySequenceWidget::setKeySequence(const QKeySequence &seq, Validation validate)
|
|
{
|
|
if (d->keySequence == seq) {
|
|
return;
|
|
}
|
|
if (validate == Validate && !isKeySequenceAvailable(seq)) {
|
|
return;
|
|
}
|
|
d->keySequence = seq;
|
|
d->updateShortcutDisplay();
|
|
Q_EMIT keySequenceChanged(seq);
|
|
}
|
|
|
|
// slot
|
|
void KKeySequenceWidget::clearKeySequence()
|
|
{
|
|
setKeySequence(QKeySequence());
|
|
}
|
|
|
|
// slot
|
|
void KKeySequenceWidget::applyStealShortcut()
|
|
{
|
|
QSet<KActionCollection *> changedCollections;
|
|
|
|
for (QAction *stealAction : std::as_const(d->stealActions)) {
|
|
// Stealing a shortcut means setting it to an empty one.
|
|
stealAction->setShortcuts(QList<QKeySequence>());
|
|
|
|
// The following code will find the action we are about to
|
|
// steal from and save it's actioncollection.
|
|
KActionCollection *parentCollection = nullptr;
|
|
for (KActionCollection *collection : std::as_const(d->checkActionCollections)) {
|
|
if (collection->actions().contains(stealAction)) {
|
|
parentCollection = collection;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Remember the changed collection
|
|
if (parentCollection) {
|
|
changedCollections.insert(parentCollection);
|
|
}
|
|
}
|
|
|
|
for (KActionCollection *col : std::as_const(changedCollections)) {
|
|
col->writeSettings();
|
|
}
|
|
|
|
d->stealActions.clear();
|
|
}
|
|
|
|
bool KKeySequenceWidget::event(QEvent *ev)
|
|
{
|
|
constexpr char _highlight[] = "_kde_highlight_neutral";
|
|
|
|
if (ev->type() == QEvent::DynamicPropertyChange) {
|
|
auto dpev = static_cast<QDynamicPropertyChangeEvent *>(ev);
|
|
if (dpev->propertyName() == _highlight) {
|
|
d->keyButton->setProperty(_highlight, property(_highlight));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return QWidget::event(ev);
|
|
}
|
|
|
|
#include "moc_kkeysequencewidget.cpp"
|