File indexing completed on 2024-05-12 05:35:46

0001 /*
0002     SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "kcm_keys.h"
0008 
0009 #include <QDBusMetaType>
0010 #include <QFile>
0011 #include <QQuickItem>
0012 #include <QQuickRenderControl>
0013 #include <QWindow>
0014 
0015 #include <KConfig>
0016 #include <KConfigGroup>
0017 #include <KDesktopFile>
0018 #include <KGlobalShortcutInfo>
0019 #include <KIO/DesktopExecParser>
0020 #include <KLocalizedString>
0021 #include <KMessageBox>
0022 #include <KOpenWithDialog>
0023 #include <KPluginFactory>
0024 #include <KShell>
0025 #include <kglobalaccel_interface.h>
0026 
0027 #include "basemodel.h"
0028 #include "filteredmodel.h"
0029 #include "globalaccelmodel.h"
0030 #include "kcmkeys_debug.h"
0031 #include "keysdata.h"
0032 #include "shortcutsmodel.h"
0033 #include "standardshortcutsmodel.h"
0034 
0035 K_PLUGIN_FACTORY_WITH_JSON(KCMKeysFactory, "kcm_keys.json", registerPlugin<KCMKeys>(); registerPlugin<KeysData>();)
0036 
0037 KCMKeys::KCMKeys(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args)
0038     : KQuickConfigModule(parent, metaData)
0039 {
0040     constexpr char uri[] = "org.kde.private.kcms.keys";
0041     qmlRegisterUncreatableType<BaseModel>(uri, 2, 0, "BaseModel", "Can't create BaseModel");
0042     qmlRegisterUncreatableMetaObject(ComponentNS::staticMetaObject, uri, 2, 0, "ComponentType", "Can't create Component namespace");
0043     qmlRegisterAnonymousType<ShortcutsModel>(uri, 2);
0044     qmlRegisterAnonymousType<FilteredShortcutsModel>(uri, 2);
0045     qmlProtectModule(uri, 2);
0046     qDBusRegisterMetaType<KGlobalShortcutInfo>();
0047     qDBusRegisterMetaType<QList<KGlobalShortcutInfo>>();
0048     qDBusRegisterMetaType<QList<QStringList>>();
0049     qDBusRegisterMetaType<QKeySequence>();
0050     qDBusRegisterMetaType<QList<QKeySequence>>();
0051 
0052     m_globalAccelInterface = new KGlobalAccelInterface(QStringLiteral("org.kde.kglobalaccel"), //
0053                                                        QStringLiteral("/kglobalaccel"),
0054                                                        QDBusConnection::sessionBus(),
0055                                                        this);
0056     if (!m_globalAccelInterface->isValid()) {
0057         setError(i18n("Failed to communicate with global shortcuts daemon"));
0058         qCCritical(KCMKEYS) << "Interface is not valid";
0059         if (m_globalAccelInterface->lastError().isValid()) {
0060             qCCritical(KCMKEYS) << m_globalAccelInterface->lastError().name() << m_globalAccelInterface->lastError().message();
0061         }
0062     }
0063     m_globalAccelModel = new GlobalAccelModel(m_globalAccelInterface, this);
0064     m_standardShortcutsModel = new StandardShortcutsModel(this);
0065     m_shortcutsModel = new ShortcutsModel(this);
0066     m_shortcutsModel->addSourceModel(m_globalAccelModel);
0067     m_shortcutsModel->addSourceModel(m_standardShortcutsModel);
0068     m_filteredModel = new FilteredShortcutsModel(this);
0069     m_filteredModel->setSourceModel(m_shortcutsModel);
0070 
0071     m_argument = args.isEmpty() ? QString() : args.first().toString();
0072     connect(m_shortcutsModel, &QAbstractItemModel::dataChanged, this, [this] {
0073         setNeedsSave(m_globalAccelModel->needsSave() || m_standardShortcutsModel->needsSave());
0074         setRepresentsDefaults(m_globalAccelModel->isDefault() && m_standardShortcutsModel->isDefault());
0075     });
0076     connect(m_shortcutsModel, &QAbstractItemModel::modelReset, this, [this] {
0077         setNeedsSave(false);
0078         setRepresentsDefaults(m_globalAccelModel->isDefault() && m_standardShortcutsModel->isDefault());
0079     });
0080     connect(m_globalAccelModel, &QAbstractItemModel::modelReset, this, [this] {
0081         if (!m_argument.isEmpty()) {
0082             for (int i = 0, c = m_filteredModel->rowCount(); i < c; ++i) {
0083                 if (m_filteredModel->data(m_filteredModel->index(i, 0), BaseModel::ComponentRole) == m_argument) {
0084                     Q_EMIT showComponent(i);
0085                     break;
0086                 }
0087             }
0088             m_argument.clear();
0089         }
0090     });
0091 
0092     connect(m_globalAccelModel, &GlobalAccelModel::errorOccured, this, &KCMKeys::setError);
0093 }
0094 
0095 void KCMKeys::load()
0096 {
0097     m_globalAccelModel->load();
0098     m_standardShortcutsModel->load();
0099 }
0100 
0101 void KCMKeys::save()
0102 {
0103     m_globalAccelModel->save();
0104     m_standardShortcutsModel->save();
0105 }
0106 
0107 void KCMKeys::defaults()
0108 {
0109     m_globalAccelModel->defaults();
0110     m_standardShortcutsModel->defaults();
0111 }
0112 
0113 ShortcutsModel *KCMKeys::shortcutsModel() const
0114 {
0115     return m_shortcutsModel;
0116 }
0117 
0118 FilteredShortcutsModel *KCMKeys::filteredModel() const
0119 {
0120     return m_filteredModel;
0121 }
0122 
0123 void KCMKeys::setError(const QString &errorMessage)
0124 {
0125     m_lastError = errorMessage;
0126     Q_EMIT this->errorOccured();
0127 }
0128 
0129 QString KCMKeys::lastError() const
0130 {
0131     return m_lastError;
0132 }
0133 
0134 void KCMKeys::writeScheme(const QUrl &url)
0135 {
0136     qCDebug(KCMKEYS) << "Exporting to " << url.toLocalFile();
0137     KConfig file(url.toLocalFile(), KConfig::SimpleConfig);
0138     m_globalAccelModel->exportToConfig(file);
0139     m_standardShortcutsModel->exportToConfig(file);
0140     file.sync();
0141 }
0142 
0143 void KCMKeys::loadScheme(const QUrl &url)
0144 {
0145     qCDebug(KCMKEYS) << "Loading scheme" << url.toLocalFile();
0146     KConfig file(url.toLocalFile(), KConfig::SimpleConfig);
0147     m_globalAccelModel->importConfig(file);
0148     m_standardShortcutsModel->importConfig(file);
0149 }
0150 
0151 QVariantList KCMKeys::defaultSchemes() const
0152 {
0153     QVariantList schemes;
0154     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kcmkeys"), QStandardPaths::LocateDirectory);
0155     for (const QString &dir : dirs) {
0156         const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.kksrc"));
0157         for (const QString &file : fileNames) {
0158             const QString path = dir + QLatin1Char('/') + file;
0159             KConfig scheme(path, KConfig::SimpleConfig);
0160             const QString name = KConfigGroup(&scheme, QStringLiteral("Settings")).readEntry(QStringLiteral("Name"), file);
0161             schemes.append(QVariantMap({{"name", name}, {"url", QUrl::fromLocalFile(path)}}));
0162         }
0163     }
0164     return schemes;
0165 }
0166 
0167 void KCMKeys::addApplication(QQuickItem *ctx)
0168 {
0169     KOpenWithDialog *dialog = new KOpenWithDialog();
0170     if (ctx && ctx->window()) {
0171         dialog->winId(); // so it creates windowHandle
0172         dialog->windowHandle()->setTransientParent(QQuickRenderControl::renderWindowFor(ctx->window()));
0173         dialog->setWindowModality(Qt::WindowModal);
0174     }
0175     dialog->hideRunInTerminal();
0176     dialog->setSaveNewApplications(true);
0177     dialog->open();
0178     connect(dialog, &KOpenWithDialog::finished, this, [this, dialog](int result) {
0179         if (result == QDialog::Accepted && dialog->service()) {
0180             const KService::Ptr service = dialog->service();
0181             const QString desktopFileName = service->storageId();
0182             if (m_globalAccelModel->match(m_shortcutsModel->index(0, 0), BaseModel::ComponentRole, desktopFileName, 1, Qt::MatchExactly).isEmpty()) {
0183                 m_globalAccelModel->addApplication(desktopFileName, service->name());
0184             } else {
0185                 qCDebug(KCMKEYS) << "Already have component" << service->storageId();
0186             }
0187         }
0188         dialog->deleteLater();
0189     });
0190 }
0191 
0192 void KCMKeys::addCommand(const QString &exec)
0193 {
0194     // escape %'s in the exec with %%
0195     QString escapedExec = exec;
0196     escapedExec.replace("%%", "%");
0197     escapedExec.replace('%', "%%");
0198     QString serviceName = KIO::DesktopExecParser::executableName(escapedExec);
0199     if (serviceName.isEmpty()) {
0200         // DesktopExecParser fails if there are any %'s in the exec which aren't valid % placeholders per the desktop file standard
0201         // escaping with backslashes or doubling doesn't seem to work either, so we just do this
0202         serviceName = escapedExec.left(escapedExec.indexOf(" "));
0203     }
0204     QString menuId;
0205     QString newPath = KService::newServicePath(false /* ignored argument */, serviceName, &menuId);
0206 
0207     KDesktopFile desktopFile(newPath);
0208     KConfigGroup cg = desktopFile.desktopGroup();
0209     cg.writeEntry("Type", "Application");
0210 
0211     QString finalExec = escapedExec;
0212 
0213     // If it was added as a URL, convert it to a local path, because desktop
0214     // files can't take URLs in their exec keys
0215     const QUrl execAsURL = QUrl(escapedExec);
0216     if (!execAsURL.scheme().isEmpty()) {
0217         finalExec = execAsURL.toLocalFile();
0218     }
0219 
0220     // For the user visible name, use the executable name with any
0221     // arguments appended, but with desktop-file specific expansion
0222     // arguments removed. This is done to more clearly communicate the
0223     // actual command used to the user and makes it easier to
0224     // distinguish things like "qdbus".
0225     QString name = KIO::DesktopExecParser::executableName(finalExec);
0226     auto view = QStringView{finalExec}.trimmed();
0227     int index = view.indexOf(QLatin1Char(' '));
0228     if (index > 0) {
0229         name.append(view.mid(index));
0230     }
0231     cg.writeEntry("Name", finalExec);
0232     cg.writeEntry("Exec", finalExec);
0233     cg.writeEntry("NoDisplay", true);
0234     cg.writeEntry("StartupNotify", false);
0235     cg.writeEntry("X-KDE-GlobalAccel-CommandShortcut", true);
0236     cg.sync();
0237 
0238     m_globalAccelModel->addApplication(newPath, name);
0239 }
0240 
0241 QString KCMKeys::editCommand(const QString &componentName, const QString &newExec)
0242 {
0243     QString finalExec = newExec;
0244 
0245     finalExec.replace("%%", "%");
0246     finalExec.replace('%', "%%");
0247 
0248     // If it was added as a URL, convert it to a local path, because desktop
0249     // files can't take URLs in their exec keys
0250     const QUrl execAsURL = QUrl(newExec);
0251     if (!execAsURL.scheme().isEmpty()) {
0252         finalExec = execAsURL.toLocalFile();
0253     }
0254     KDesktopFile desktopFile(componentName);
0255     KConfigGroup cg = desktopFile.desktopGroup();
0256     cg.writeEntry("Name", finalExec);
0257     cg.writeEntry("Exec", finalExec);
0258     cg.sync();
0259     return finalExec;
0260 }
0261 
0262 QString KCMKeys::quoteUrl(const QUrl &url)
0263 {
0264     return KShell::quoteArg(url.toLocalFile());
0265 }
0266 
0267 QString KCMKeys::keySequenceToString(const QKeySequence &keySequence) const
0268 {
0269     return keySequence.toString(QKeySequence::NativeText);
0270 }
0271 
0272 QString KCMKeys::urlFilename(const QUrl &url)
0273 {
0274     return url.fileName();
0275 }
0276 
0277 QModelIndex KCMKeys::conflictingIndex(const QKeySequence &keySequence)
0278 {
0279     for (int i = 0; i < m_shortcutsModel->rowCount(); ++i) {
0280         const QModelIndex componentIndex = m_shortcutsModel->index(i, 0);
0281         for (int j = 0; j < m_shortcutsModel->rowCount(componentIndex); ++j) {
0282             const QModelIndex actionIndex = m_shortcutsModel->index(j, 0, componentIndex);
0283             if (m_shortcutsModel->data(actionIndex, BaseModel::ActiveShortcutsRole).value<QSet<QKeySequence>>().contains(keySequence)) {
0284                 return m_shortcutsModel->mapToSource(actionIndex);
0285             }
0286         }
0287     }
0288     return QModelIndex();
0289 }
0290 
0291 void KCMKeys::requestKeySequence(QQuickItem *context, const QModelIndex &index, const QKeySequence &newSequence, const QKeySequence &oldSequence)
0292 {
0293     qCDebug(KCMKEYS) << index << "wants" << newSequence << "instead of" << oldSequence;
0294     const QModelIndex conflict = conflictingIndex(newSequence);
0295     if (!conflict.isValid()) {
0296         auto model = const_cast<BaseModel *>(static_cast<const BaseModel *>(index.model()));
0297         if (!oldSequence.isEmpty()) {
0298             model->changeShortcut(index, oldSequence, newSequence);
0299         } else {
0300             model->addShortcut(index, newSequence);
0301         }
0302         return;
0303     }
0304 
0305     qCDebug(KCMKEYS) << "Found conflict for" << newSequence << conflict;
0306     const bool isStandardAction = conflict.parent().data(BaseModel::SectionRole) == ComponentType::CommonAction;
0307     const QString actionName = conflict.data().toString();
0308     const QString componentName = conflict.parent().data().toString();
0309     const QString keysString = newSequence.toString(QKeySequence::NativeText);
0310     const QString message = isStandardAction
0311         ? i18nc("%2 is the name of a category inside the 'Common Actions' section",
0312                 "Shortcut %1 is already assigned to the common %2 action '%3'.\nDo you want to reassign it?",
0313                 keysString,
0314                 componentName,
0315                 actionName)
0316         : i18n("Shortcut %1 is already assigned to action '%2' of %3.\nDo you want to reassign it?", keysString, actionName, componentName);
0317     const QString title = i18nc("@title:window", "Found conflict");
0318     auto dialog = new QDialog;
0319     dialog->setWindowTitle(title);
0320     if (context && context->window()) {
0321         dialog->winId(); // so it creates windowHandle
0322         dialog->windowHandle()->setTransientParent(QQuickRenderControl::renderWindowFor(context->window()));
0323     }
0324     dialog->setWindowModality(Qt::WindowModal);
0325     dialog->setAttribute(Qt::WA_DeleteOnClose);
0326     KMessageBox::createKMessageBox(dialog,
0327                                    new QDialogButtonBox(QDialogButtonBox::Yes | QDialogButtonBox::No, dialog),
0328                                    QMessageBox::Question,
0329                                    message,
0330                                    {},
0331                                    QString(),
0332                                    nullptr,
0333                                    KMessageBox::NoExec);
0334     dialog->show();
0335 
0336     connect(dialog, &QDialog::finished, this, [index, conflict, newSequence, oldSequence](int result) {
0337         auto model = const_cast<BaseModel *>(static_cast<const BaseModel *>(index.model()));
0338         if (result != QDialogButtonBox::Yes) {
0339             // Also Q_EMIT if we are not changing anything, to force the frontend to update and be consistent
0340             // with the model. It is currently out of sync because it reflects the user input that
0341             // was rejected now.
0342             Q_EMIT model->dataChanged(index, index, {BaseModel::ActiveShortcutsRole, BaseModel::CustomShortcutsRole});
0343             return;
0344         }
0345         const_cast<BaseModel *>(static_cast<const BaseModel *>(conflict.model()))->disableShortcut(conflict, newSequence);
0346         if (!oldSequence.isEmpty()) {
0347             model->changeShortcut(index, oldSequence, newSequence);
0348         } else {
0349             model->addShortcut(index, newSequence);
0350         }
0351     });
0352 }
0353 
0354 #include "kcm_keys.moc"
0355 #include "moc_kcm_keys.cpp"