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"