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("<")).replace(QLatin1String(">"), QLatin1String(">")).toUtf8()) 0039 .replace(QLatin1String("<"), QLatin1String("<")) 0040 .replace(QLatin1String(">"), 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 }