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

0001 /*
0002     SPDX-FileCopyrightText: 2010 Andriy Rysin <rysin@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "xkb_rules.h"
0008 #include "config-workspace.h"
0009 #include "debug.h"
0010 
0011 #include <KLocalizedString>
0012 
0013 #include <QDir>
0014 #include <QRegularExpression>
0015 #include <QTextDocument> // for Qt::escape
0016 #include <QXmlStreamReader>
0017 #include <QtConcurrentFilter>
0018 
0019 #include "x11_helper.h"
0020 
0021 // for findXkbRuleFile
0022 #include <QtGui/private/qtx11extras_p.h>
0023 #include <X11/XKBlib.h>
0024 #include <X11/Xatom.h>
0025 #include <X11/Xlib.h>
0026 #include <X11/extensions/XKBrules.h>
0027 #include <fixx11h.h>
0028 
0029 static QString translate_xml_item(const QString &itemText)
0030 {
0031     if (itemText.isEmpty()) { // i18n warns on empty input strings
0032         return itemText;
0033     }
0034     // messages are already extracted from the source XML files by xkb
0035     // the characters '<' and '>' (but not '"') are HTML-escaped in the xkeyboard-config translation files, so we need to convert them before/after looking up
0036     // the translation note that we cannot use QString::toHtmlEscaped() here because that would convert '"' as well
0037     QString msgid(itemText);
0038     return i18nd("xkeyboard-config", msgid.replace(QLatin1String("<"), QLatin1String("&lt;")).replace(QLatin1String(">"), QLatin1String("&gt;")).toUtf8())
0039         .replace(QLatin1String("&lt;"), QLatin1String("<"))
0040         .replace(QLatin1String("&gt;"), QLatin1String(">"));
0041 }
0042 
0043 static QString translate_description(ConfigItem *item)
0044 {
0045     return item->description.isEmpty() ? item->name : translate_xml_item(item->description);
0046 }
0047 
0048 static bool notEmpty(const ConfigItem *item)
0049 {
0050     return !item->name.isEmpty();
0051 }
0052 
0053 template<class T>
0054 void removeEmptyItems(QList<T *> &list)
0055 {
0056     QtConcurrent::blockingFilter(list, notEmpty);
0057 }
0058 
0059 static void postProcess(Rules *rules)
0060 {
0061     // TODO remove elements with empty names to safeguard us
0062     removeEmptyItems(rules->layoutInfos);
0063     removeEmptyItems(rules->modelInfos);
0064     removeEmptyItems(rules->optionGroupInfos);
0065 
0066     //  bindtextdomain("xkeyboard-config", LOCALE_DIR);
0067     for (ModelInfo *modelInfo : std::as_const(rules->modelInfos)) {
0068         modelInfo->vendor = translate_xml_item(modelInfo->vendor);
0069         modelInfo->description = translate_description(modelInfo);
0070     }
0071 
0072     for (LayoutInfo *layoutInfo : std::as_const(rules->layoutInfos)) {
0073         layoutInfo->description = translate_description(layoutInfo);
0074 
0075         removeEmptyItems(layoutInfo->variantInfos);
0076         for (VariantInfo *variantInfo : std::as_const(layoutInfo->variantInfos)) {
0077             variantInfo->description = translate_description(variantInfo);
0078         }
0079     }
0080     for (OptionGroupInfo *optionGroupInfo : std::as_const(rules->optionGroupInfos)) {
0081         optionGroupInfo->description = translate_description(optionGroupInfo);
0082 
0083         removeEmptyItems(optionGroupInfo->optionInfos);
0084         for (OptionInfo *optionInfo : std::as_const(optionGroupInfo->optionInfos)) {
0085             optionInfo->description = translate_description(optionInfo);
0086         }
0087     }
0088 }
0089 
0090 Rules::Rules()
0091     : version(QStringLiteral("1.0"))
0092 {
0093 }
0094 
0095 QString Rules::getRulesName()
0096 {
0097     if (!QX11Info::isPlatformX11()) {
0098         return QString();
0099     }
0100     XkbRF_VarDefsRec vd;
0101     char *tmp = nullptr;
0102 
0103     if (XkbRF_GetNamesProp(QX11Info::display(), &tmp, &vd) && tmp != nullptr) {
0104         //          qCDebug(KCM_KEYBOARD) << "namesprop" << tmp ;
0105         const QString name(tmp);
0106         XFree(tmp);
0107         return name;
0108     }
0109 
0110     return {};
0111 }
0112 
0113 QString Rules::findXkbDir()
0114 {
0115     return QStringLiteral(XKBDIR);
0116 }
0117 
0118 static QString findXkbRulesFile()
0119 {
0120     QString rulesFile;
0121     QString rulesName = Rules::getRulesName();
0122 
0123     const QString xkbDir = Rules::findXkbDir();
0124     if (!rulesName.isNull()) {
0125         rulesFile = QStringLiteral("%1/rules/%2.xml").arg(xkbDir, rulesName);
0126     } else {
0127         // default to evdev
0128         rulesFile = QStringLiteral("%1/rules/evdev.xml").arg(xkbDir);
0129     }
0130 
0131     return rulesFile;
0132 }
0133 
0134 static void mergeRules(Rules *rules, Rules *extraRules)
0135 {
0136     rules->modelInfos.append(extraRules->modelInfos);
0137     rules->optionGroupInfos.append(extraRules->optionGroupInfos); // need to iterate and merge?
0138 
0139     QList<LayoutInfo *> layoutsToAdd;
0140     for (LayoutInfo *extraLayoutInfo : std::as_const(extraRules->layoutInfos)) {
0141         LayoutInfo *layoutInfo = findByName(rules->layoutInfos, extraLayoutInfo->name);
0142         if (layoutInfo != nullptr) {
0143             layoutInfo->variantInfos.append(extraLayoutInfo->variantInfos);
0144             layoutInfo->languages.append(extraLayoutInfo->languages);
0145         } else {
0146             layoutsToAdd.append(extraLayoutInfo);
0147         }
0148     }
0149     rules->layoutInfos.append(layoutsToAdd);
0150     qCDebug(KCM_KEYBOARD) << "Merged from extra rules:" << extraRules->layoutInfos.size() << "layouts," << extraRules->modelInfos.size() << "models,"
0151                           << extraRules->optionGroupInfos.size() << "option groups";
0152 
0153     // base rules now own the objects - remove them from extra rules so that it does not try to delete them
0154     extraRules->layoutInfos.clear();
0155     extraRules->modelInfos.clear();
0156     extraRules->optionGroupInfos.clear();
0157 }
0158 
0159 const char Rules::XKB_OPTION_GROUP_SEPARATOR = ':';
0160 
0161 Rules *Rules::readRules(ExtrasFlag extrasFlag)
0162 {
0163     Rules *rules = new Rules();
0164     QString rulesFile = findXkbRulesFile();
0165     if (!readRules(rules, rulesFile, false)) {
0166         delete rules;
0167         return nullptr;
0168     }
0169     if (extrasFlag == Rules::READ_EXTRAS) {
0170         QRegularExpression regex(QStringLiteral("\\.xml$"));
0171         Rules *rulesExtra = new Rules();
0172         QString extraRulesFile = rulesFile.replace(regex, QStringLiteral(".extras.xml"));
0173         if (readRules(rulesExtra, extraRulesFile, true)) { // not fatal if it fails
0174             mergeRules(rules, rulesExtra);
0175         }
0176         delete rulesExtra;
0177     }
0178     return rules;
0179 }
0180 
0181 Rules *Rules::readRules(Rules *rules, const QString &filename, bool fromExtras)
0182 {
0183     QFile file(filename);
0184     if (!file.open(QFile::ReadOnly | QFile::Text)) {
0185         qCCritical(KCM_KEYBOARD) << "Cannot open the rules file" << file.fileName();
0186         return nullptr;
0187     }
0188 
0189     QStringList path;
0190     QXmlStreamReader reader(&file);
0191     while (!reader.atEnd()) {
0192         const auto token = reader.readNext();
0193         if (token == QXmlStreamReader::StartElement) {
0194             path << reader.name().toString();
0195             QString strPath = path.join(QLatin1String("/"));
0196 
0197             if (strPath.endsWith(QLatin1String("layoutList/layout/configItem"))) {
0198                 rules->layoutInfos << new LayoutInfo(fromExtras);
0199             } else if (strPath.endsWith(QLatin1String("layoutList/layout/variantList/variant"))) {
0200                 rules->layoutInfos.last()->variantInfos << new VariantInfo(fromExtras);
0201             } else if (strPath.endsWith(QLatin1String("modelList/model"))) {
0202                 rules->modelInfos << new ModelInfo();
0203             } else if (strPath.endsWith(QLatin1String("optionList/group"))) {
0204                 rules->optionGroupInfos << new OptionGroupInfo();
0205                 rules->optionGroupInfos.last()->exclusive = (reader.attributes().value(QStringLiteral("allowMultipleSelection")) != QLatin1String("true"));
0206             } else if (strPath.endsWith(QLatin1String("optionList/group/option"))) {
0207                 rules->optionGroupInfos.last()->optionInfos << new OptionInfo();
0208             } else if (strPath == ("xkbConfigRegistry") && !reader.attributes().value(QStringLiteral("version")).isEmpty()) {
0209                 rules->version = reader.attributes().value(QStringLiteral("version")).toString();
0210                 qCDebug(KCM_KEYBOARD) << "xkbConfigRegistry version" << rules->version;
0211             }
0212 
0213             if (strPath.endsWith(QLatin1String("layoutList/layout/configItem/name"))) {
0214                 if (rules->layoutInfos.last() != nullptr) {
0215                     rules->layoutInfos.last()->name = reader.readElementText().trimmed();
0216                 }
0217             } else if (strPath.endsWith(QLatin1String("layoutList/layout/configItem/description"))) {
0218                 rules->layoutInfos.last()->description = reader.readElementText().trimmed();
0219             } else if (strPath.endsWith(QLatin1String("layoutList/layout/configItem/languageList/iso639Id"))) {
0220                 rules->layoutInfos.last()->languages << reader.readElementText().trimmed();
0221             } else if (strPath.endsWith(QLatin1String("layoutList/layout/variantList/variant/configItem/name"))) {
0222                 rules->layoutInfos.last()->variantInfos.last()->name = reader.readElementText().trimmed();
0223             } else if (strPath.endsWith(QLatin1String("layoutList/layout/variantList/variant/configItem/description"))) {
0224                 rules->layoutInfos.last()->variantInfos.last()->description = reader.readElementText().trimmed();
0225             } else if (strPath.endsWith(QLatin1String("layoutList/layout/variantList/variant/configItem/languageList/iso639Id"))) {
0226                 rules->layoutInfos.last()->variantInfos.last()->languages << reader.readElementText().trimmed();
0227             } else if (strPath.endsWith(QLatin1String("modelList/model/configItem/name"))) {
0228                 rules->modelInfos.last()->name = reader.readElementText().trimmed();
0229             } else if (strPath.endsWith(QLatin1String("modelList/model/configItem/description"))) {
0230                 rules->modelInfos.last()->description = reader.readElementText().trimmed();
0231             } else if (strPath.endsWith(QLatin1String("modelList/model/configItem/vendor"))) {
0232                 rules->modelInfos.last()->vendor = reader.readElementText().trimmed();
0233             } else if (strPath.endsWith(QLatin1String("optionList/group/configItem/name"))) {
0234                 rules->optionGroupInfos.last()->name = reader.readElementText().trimmed();
0235             } else if (strPath.endsWith(QLatin1String("optionList/group/configItem/description"))) {
0236                 rules->optionGroupInfos.last()->description = reader.readElementText().trimmed();
0237             } else if (strPath.endsWith(QLatin1String("optionList/group/option/configItem/name"))) {
0238                 rules->optionGroupInfos.last()->optionInfos.last()->name = reader.readElementText().trimmed();
0239             } else if (strPath.endsWith(QLatin1String("optionList/group/option/configItem/description"))) {
0240                 rules->optionGroupInfos.last()->optionInfos.last()->description = reader.readElementText().trimmed();
0241             }
0242         }
0243         // don't use token here, readElementText() above can have moved us forward meanwhile
0244         if (reader.tokenType() == QXmlStreamReader::EndElement) {
0245             path.removeLast();
0246         }
0247     }
0248 
0249     qCDebug(KCM_KEYBOARD) << "Parsing xkb rules from" << file.fileName();
0250 
0251     if (reader.hasError()) {
0252         qCCritical(KCM_KEYBOARD) << "Failed to parse the rules file" << file.fileName();
0253         return nullptr;
0254     }
0255 
0256     postProcess(rules);
0257 
0258     return rules;
0259 }
0260 
0261 bool LayoutInfo::isLanguageSupportedByLayout(const QString &lang) const
0262 {
0263     return languages.contains(lang) || isLanguageSupportedByVariants(lang);
0264 }
0265 
0266 bool LayoutInfo::isLanguageSupportedByVariants(const QString &lang) const
0267 {
0268     for (const VariantInfo *info : std::as_const(variantInfos)) {
0269         if (info->languages.contains(lang))
0270             return true;
0271     }
0272     return false;
0273 }
0274 
0275 bool LayoutInfo::isLanguageSupportedByDefaultVariant(const QString &lang) const
0276 {
0277     if (languages.contains(lang))
0278         return true;
0279 
0280     if (languages.empty() && isLanguageSupportedByVariants(lang))
0281         return true;
0282 
0283     return false;
0284 }
0285 
0286 bool LayoutInfo::isLanguageSupportedByVariant(const VariantInfo *variantInfo, const QString &lang) const
0287 {
0288     if (variantInfo->languages.contains(lang))
0289         return true;
0290 
0291     // if variant has no languages try to "inherit" them from layout
0292     if (variantInfo->languages.empty() && languages.contains(lang))
0293         return true;
0294 
0295     return false;
0296 }