File indexing completed on 2024-12-22 05:17:14

0001 /*
0002  * This file is part of the KDE wacomtablet project. For copyright
0003  * information and license terms see the AUTHORS and COPYING files
0004  * in the top-level directory of this distribution.
0005  *
0006  * This program is free software; you can redistribute it and/or
0007  * modify it under the terms of the GNU General Public License as
0008  * published by the Free Software Foundation; either version 2 of
0009  * the License, or (at your option) any later version.
0010  *
0011  * This program is distributed in the hope that it will be useful,
0012  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0013  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014  * GNU General Public License for more details.
0015  *
0016  * You should have received a copy of the GNU General Public License
0017  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0018  */
0019 
0020 #include "buttonshortcut.h"
0021 
0022 #include <QKeySequence>
0023 #include <QRegularExpression>
0024 
0025 #include <KGlobalAccel>
0026 #include <KLocalizedString>
0027 
0028 using namespace Wacom;
0029 
0030 /*
0031  * When mapping multiple keys to the same name, the last entry
0032  * will be the default one. This is because QMap.find() returns
0033  * the most recently added value as default.
0034  *
0035  * Example:
0036  *
0037  * super_l = meta
0038  * super   = meta  <-- default entry when searching for "meta"
0039  *
0040  */
0041 const char *ButtonShortcut::CONVERT_KEY_MAP_DATA[][2] = {
0042     // clang-format off
0043     {"alt_l",        "alt"},
0044     {"alt_r",        "alt"},
0045     {"alt",          "alt"},  // "alt" default
0046     {"ampersand",    "&"},
0047     {"apostrophe",   "'"},
0048     {"asciicircum",  "^"},
0049     {"asciitilde",   "~"},
0050     {"asterisk",     "*"},
0051     {"at",           "@"},
0052     {"backslash",    "\\"},
0053     {"bar",          "|"},
0054     {"braceleft",    "{"},
0055     {"braceright",   "}"},
0056     {"bracketleft",  "["},
0057     {"bracketright", "]"},
0058     {"colon",        ":"},
0059     {"comma",        ","},
0060     {"ctrl_l",       "ctrl"},
0061     {"ctrl_r",       "ctrl"},
0062     {"ctrl",         "ctrl"}, // "ctrl" default
0063     {"dollar",       "$"},
0064     {"equal",        "="},
0065     {"exclam",       "!"},
0066     {"greater",      ">"},
0067     {"less",         "<"},
0068     {"pgdn",         "pgdown"},
0069     {"numbersign",   "#"},
0070     {"period",       "."},
0071     {"plus",         "+"},
0072     {"minus",        "-"},
0073     {"parenleft",    "("},
0074     {"parenright",   ")"},
0075     {"percent",      "%"},
0076     {"question",     "?"},
0077     {"quotedbl",     "\""},
0078     {"quoteleft",    "`"},
0079     {"semicolon",    ";"},
0080     {"slash",        "/"},
0081     {"super_l",      "meta"},
0082     {"super_r",      "meta"},
0083     {"super",        "meta"}, // "super" default
0084     {"underscore",   "_"},
0085     {nullptr, nullptr},
0086     // clang-format on
0087 };
0088 
0089 QString buttonNumberToDescription(int buttonNr)
0090 {
0091     switch (buttonNr) {
0092     case 1:
0093         return i18nc("Tablet button triggers a left mouse button click.", "Left Mouse Button Click");
0094     case 2:
0095         return i18nc("Tablet button triggers a middle mouse button click.", "Middle Mouse Button Click");
0096     case 3:
0097         return i18nc("Tablet button triggers a right mouse button click.", "Right Mouse Button Click");
0098     case 4:
0099         return i18nc("Tablet button triggers mouse wheel up.", "Mouse Wheel Up");
0100     case 5:
0101         return i18nc("Tablet button triggers mouse wheel down.", "Mouse Wheel Down");
0102     case 6:
0103         return i18nc("Tablet button triggers mouse wheel left.", "Mouse Wheel Left");
0104     case 7:
0105         return i18nc("Tablet button triggers mouse wheel right.", "Mouse Wheel Right");
0106     default:
0107         return i18nc("Tablet button triggers a click of mouse button with number #", "Mouse Button %1 Click", buttonNr);
0108     }
0109 }
0110 
0111 namespace Wacom
0112 {
0113 class ButtonShortcutPrivate
0114 {
0115 public:
0116     ButtonShortcutPrivate()
0117     {
0118         type = ButtonShortcut::ShortcutType::NONE;
0119         button = 0;
0120     }
0121 
0122     ButtonShortcut::ShortcutType type;
0123     QString sequence; //!< The key or modifier sequence.
0124     int button; //!< The button number.
0125 };
0126 }
0127 
0128 ButtonShortcut::ButtonShortcut()
0129     : d_ptr(new ButtonShortcutPrivate)
0130 {
0131 }
0132 
0133 ButtonShortcut::ButtonShortcut(const ButtonShortcut &that)
0134     : d_ptr(new ButtonShortcutPrivate)
0135 {
0136     operator=(that);
0137 }
0138 
0139 ButtonShortcut::ButtonShortcut(const QString &shortcut)
0140     : d_ptr(new ButtonShortcutPrivate)
0141 {
0142     set(shortcut);
0143 }
0144 
0145 ButtonShortcut::ButtonShortcut(int buttonNumber)
0146     : d_ptr(new ButtonShortcutPrivate)
0147 {
0148     setButton(buttonNumber);
0149 }
0150 
0151 ButtonShortcut::~ButtonShortcut()
0152 {
0153     delete this->d_ptr;
0154 }
0155 
0156 ButtonShortcut &ButtonShortcut::operator=(const ButtonShortcut &that)
0157 {
0158     Q_D(ButtonShortcut);
0159 
0160     *d = *(that.d_ptr);
0161     return *this;
0162 }
0163 
0164 ButtonShortcut &ButtonShortcut::operator=(const QString &shortcut)
0165 {
0166     set(shortcut);
0167     return *this;
0168 }
0169 
0170 bool ButtonShortcut::operator==(const ButtonShortcut &that) const
0171 {
0172     Q_D(const ButtonShortcut);
0173 
0174     if (d->type != that.d_ptr->type) {
0175         return false;
0176     }
0177 
0178     if (d->button != that.d_ptr->button) {
0179         return false;
0180     }
0181 
0182     if (d->sequence.compare(that.d_ptr->sequence, Qt::CaseInsensitive) != 0) {
0183         return false;
0184     }
0185 
0186     return true;
0187 }
0188 
0189 bool ButtonShortcut::operator!=(const ButtonShortcut &that) const
0190 {
0191     return (!operator==(that));
0192 }
0193 
0194 void ButtonShortcut::clear()
0195 {
0196     Q_D(ButtonShortcut);
0197 
0198     d->type = ShortcutType::NONE;
0199     d->button = 0;
0200     d->sequence.clear();
0201 }
0202 
0203 int ButtonShortcut::getButton() const
0204 {
0205     Q_D(const ButtonShortcut);
0206     return d->button;
0207 }
0208 
0209 ButtonShortcut::ShortcutType ButtonShortcut::getType() const
0210 {
0211     Q_D(const ButtonShortcut);
0212     return d->type;
0213 }
0214 
0215 bool ButtonShortcut::isButton() const
0216 {
0217     return (getType() == ShortcutType::BUTTON);
0218 }
0219 
0220 bool ButtonShortcut::isKeystroke() const
0221 {
0222     return (getType() == ShortcutType::KEYSTROKE);
0223 }
0224 
0225 bool ButtonShortcut::isModifier() const
0226 {
0227     return (getType() == ShortcutType::MODIFIER);
0228 }
0229 
0230 bool ButtonShortcut::isSet() const
0231 {
0232     return (getType() != ShortcutType::NONE);
0233 }
0234 
0235 bool ButtonShortcut::setButton(int buttonNumber)
0236 {
0237     Q_D(ButtonShortcut);
0238 
0239     clear();
0240 
0241     if (buttonNumber > 0 && buttonNumber <= 32) {
0242         d->type = ShortcutType::BUTTON;
0243         d->button = buttonNumber;
0244         return true;
0245     }
0246 
0247     return false;
0248 }
0249 
0250 bool ButtonShortcut::set(const QString &sequence)
0251 {
0252     clear();
0253 
0254     QString seq = sequence.trimmed();
0255 
0256     if (seq.isEmpty()) {
0257         return true;
0258     }
0259 
0260     static const QRegularExpression modifierRx(QLatin1String("^(?:key )?(?:\\s*\\+?(?:alt|ctrl|meta|shift|super))+$"),
0261                                                QRegularExpression::CaseInsensitiveOption);
0262     static const QRegularExpression buttonRx(QLatin1String("^(?:button\\s+)?\\+?\\d+$"), QRegularExpression::CaseInsensitiveOption);
0263 
0264     if (seq.contains(buttonRx)) {
0265         // this is a button
0266         return setButtonSequence(seq);
0267 
0268     } else if (seq.contains(modifierRx)) {
0269         // this is a modifier sequence
0270         return setModifierSequence(seq);
0271     }
0272 
0273     // this is probably a key sequence
0274     return setKeySequence(seq);
0275 }
0276 
0277 const QString ButtonShortcut::toDisplayString() const
0278 {
0279     Q_D(const ButtonShortcut);
0280 
0281     QList<KGlobalShortcutInfo> globalShortcutList;
0282     QString displayString;
0283     int buttonNr = getButton();
0284 
0285     switch (d->type) {
0286     case ShortcutType::BUTTON:
0287         displayString = buttonNumberToDescription(buttonNr);
0288         break;
0289 
0290     case ShortcutType::MODIFIER:
0291         displayString = d->sequence;
0292         convertKeySequenceToQKeySequenceFormat(displayString);
0293         break;
0294 
0295     case ShortcutType::KEYSTROKE:
0296         displayString = d->sequence;
0297         convertKeySequenceToQKeySequenceFormat(displayString);
0298 
0299         // check if a global shortcut is assigned to this sequence
0300         globalShortcutList = KGlobalAccel::globalShortcutsByKey(QKeySequence(displayString));
0301 
0302         if (!globalShortcutList.isEmpty()) {
0303             displayString = globalShortcutList.at(0).uniqueName();
0304         }
0305         break;
0306 
0307     case ShortcutType::NONE:
0308         break;
0309     }
0310 
0311     return displayString;
0312 }
0313 
0314 const QString ButtonShortcut::toQKeySequenceString() const
0315 {
0316     Q_D(const ButtonShortcut);
0317 
0318     QString keySequence;
0319 
0320     if (d->type == ShortcutType::KEYSTROKE) {
0321         keySequence = d->sequence;
0322         convertKeySequenceToQKeySequenceFormat(keySequence);
0323     }
0324 
0325     return keySequence;
0326 }
0327 
0328 const QString ButtonShortcut::toString() const
0329 {
0330     Q_D(const ButtonShortcut);
0331 
0332     QString shortcutString = QLatin1String("0");
0333 
0334     switch (d->type) {
0335     case ShortcutType::BUTTON:
0336         shortcutString = QString::number(d->button);
0337         break;
0338 
0339     case ShortcutType::MODIFIER:
0340     case ShortcutType::KEYSTROKE:
0341         shortcutString = QString::fromLatin1("key %2").arg(d->sequence);
0342         break;
0343 
0344     case ShortcutType::NONE:
0345         break;
0346     }
0347 
0348     return shortcutString.toLower();
0349 }
0350 
0351 bool ButtonShortcut::convertKey(QString &key, bool fromStorage) const
0352 {
0353     QMap<QString, QString>::ConstIterator iter;
0354     QMap<QString, QString>::ConstIterator iterEnd;
0355 
0356     if (fromStorage) {
0357         iter = getConvertFromStorageMap().find(key.toLower());
0358         iterEnd = getConvertFromStorageMap().constEnd();
0359     } else {
0360         iter = getConvertToStorageMap().find(key.toLower());
0361         iterEnd = getConvertToStorageMap().constEnd();
0362     }
0363 
0364     if (iter == iterEnd) {
0365         return false;
0366     }
0367 
0368     key = iter.value();
0369 
0370     return true;
0371 }
0372 
0373 void ButtonShortcut::convertToNormalizedKeySequence(QString &sequence, bool fromStorage) const
0374 {
0375     normalizeKeySequence(sequence);
0376 
0377     static const QRegularExpression rx(QStringLiteral("\\s+"));
0378     QStringList keyList = sequence.split(rx, Qt::SkipEmptyParts);
0379     bool isFirstKey = true;
0380 
0381     sequence.clear();
0382 
0383     for (QStringList::iterator iter = keyList.begin(); iter != keyList.end(); ++iter) {
0384         convertKey(*iter, fromStorage);
0385         prettifyKey(*iter);
0386 
0387         if (isFirstKey) {
0388             sequence.append(*iter);
0389             isFirstKey = false;
0390         } else {
0391             sequence.append(QString::fromLatin1(" %1").arg(*iter));
0392         }
0393     }
0394 }
0395 
0396 void ButtonShortcut::convertKeySequenceToStorageFormat(QString &sequence) const
0397 {
0398     convertToNormalizedKeySequence(sequence, false);
0399 }
0400 
0401 void ButtonShortcut::convertKeySequenceToQKeySequenceFormat(QString &sequence) const
0402 {
0403     convertToNormalizedKeySequence(sequence, true);
0404     sequence.replace(QLatin1String(" "), QLatin1String("+"));
0405 }
0406 
0407 const QMap<QString, QString> &ButtonShortcut::getConvertFromStorageMap()
0408 {
0409     static QMap<QString, QString> map = initConversionMap(true);
0410     return map;
0411 }
0412 
0413 const QMap<QString, QString> &ButtonShortcut::getConvertToStorageMap()
0414 {
0415     static QMap<QString, QString> map = initConversionMap(false);
0416     return map;
0417 }
0418 
0419 QMap<QString, QString> ButtonShortcut::initConversionMap(bool fromStorageMap)
0420 {
0421     QMap<QString, QString> map;
0422 
0423     for (int i = 0;; ++i) {
0424         if (CONVERT_KEY_MAP_DATA[i][0] == nullptr || CONVERT_KEY_MAP_DATA[i][1] == nullptr) {
0425             return map;
0426         }
0427 
0428         if (fromStorageMap) {
0429             map.insert(QLatin1String(CONVERT_KEY_MAP_DATA[i][0]), QLatin1String(CONVERT_KEY_MAP_DATA[i][1]));
0430         } else {
0431             map.insert(QLatin1String(CONVERT_KEY_MAP_DATA[i][1]), QLatin1String(CONVERT_KEY_MAP_DATA[i][0]));
0432         }
0433     }
0434 
0435     return map;
0436 }
0437 
0438 void ButtonShortcut::normalizeKeySequence(QString &sequence) const
0439 {
0440     // When setting a shortcut like "ctrl+x", xsetwacom will convert it to "key +ctrl +x -x"
0441     // therefore we just truncate the string on the first "-key" we find.
0442     static const QRegularExpression minusKeyRx(QLatin1String("(^|\\s)-\\S"));
0443 
0444     const QRegularExpressionMatch minusKeyRxMatch = minusKeyRx.match(sequence);
0445 
0446     if (minusKeyRxMatch.hasMatch()) {
0447         sequence = sequence.left(minusKeyRxMatch.capturedStart());
0448     }
0449 
0450     // cleanup leading "key " identifier from xsetwacom sequences
0451     static const QRegularExpression leadingKey(QStringLiteral("^\\s*key\\s+"), QRegularExpression::CaseInsensitiveOption);
0452     sequence.remove(leadingKey);
0453 
0454     // Remove all '+' prefixes from keys.
0455     // This will convert shortcuts like "+ctrl +alt" to "ctrl alt", but not
0456     // shortcuts like "ctrl +" which is required to keep compatibility to older
0457     // (buggy) configuration files.
0458     static const QRegularExpression plusPrefixes(QStringLiteral("(^|\\s)\\+(\\S)"), QRegularExpression::CaseInsensitiveOption);
0459     sequence.replace(plusPrefixes, QLatin1String("\\1\\2"));
0460 
0461     // Cleanup plus signs between keys.
0462     // This will convert shortcuts like "ctrl+alt+shift" or "Ctrl++" to "ctrl alt shift" or "Ctrl +".
0463     static const QRegularExpression cleanupPlus(QStringLiteral("(\\S)\\+(\\S)"), QRegularExpression::CaseInsensitiveOption);
0464     sequence.replace(cleanupPlus, QLatin1String("\\1 \\2"));
0465 
0466     // replace multiple whitespaces with one
0467     static const QRegularExpression whitespaces(QStringLiteral("\\s{2,}"), QRegularExpression::CaseInsensitiveOption);
0468     sequence.replace(whitespaces, QLatin1String(" "));
0469 
0470     // trim the string
0471     sequence = sequence.trimmed();
0472 }
0473 
0474 void ButtonShortcut::prettifyKey(QString &key) const
0475 {
0476     if (!key.isEmpty()) {
0477         key = key.toLower();
0478         key[0] = key.at(0).toUpper();
0479     }
0480 }
0481 
0482 bool ButtonShortcut::setButtonSequence(const QString &buttonSequence)
0483 {
0484     QString buttonNumber = buttonSequence;
0485     static const QRegularExpression rx(QStringLiteral("^\\s*button\\s+"), QRegularExpression::CaseInsensitiveOption);
0486     buttonNumber.remove(rx);
0487 
0488     bool ok = false;
0489     int button = buttonNumber.toInt(&ok);
0490 
0491     if (!ok) {
0492         return false;
0493     }
0494 
0495     return setButton(button);
0496 }
0497 
0498 bool ButtonShortcut::setKeySequence(QString sequence)
0499 {
0500     Q_D(ButtonShortcut);
0501 
0502     clear();
0503 
0504     // Check if this keysequence is valid by converting it to a QKeySequence and then back
0505     // again. If both sequences are equal the sequence should be valid. This is not very
0506     // effective but it leaves us with something KDE can handle and it makes sure a key
0507     // is always converted to its default key name if there are multiple mappings.
0508     // TODO improve this
0509     QString convertedSequence = sequence;
0510     convertKeySequenceToQKeySequenceFormat(convertedSequence);
0511 
0512     QKeySequence qkeySequence(convertedSequence);
0513     convertedSequence = qkeySequence.toString();
0514 
0515     convertKeySequenceToStorageFormat(convertedSequence);
0516     convertKeySequenceToStorageFormat(sequence);
0517 
0518     // we do not allow invalid sequences to be set
0519     // the sequences have to be equal and only one sequence is allowed
0520     if (sequence.compare(convertedSequence, Qt::CaseInsensitive) != 0 || qkeySequence.count() != 1) {
0521         return false;
0522     }
0523 
0524     d->type = ShortcutType::KEYSTROKE;
0525     d->sequence = sequence;
0526 
0527     return true;
0528 }
0529 
0530 bool ButtonShortcut::setModifierSequence(QString sequence)
0531 {
0532     Q_D(ButtonShortcut);
0533 
0534     clear();
0535     convertKeySequenceToStorageFormat(sequence);
0536 
0537     d->type = ShortcutType::MODIFIER;
0538     d->sequence = sequence;
0539 
0540     return true;
0541 }