/* SPDX-FileCopyrightText: 2010 Andriy Rysin SPDX-License-Identifier: GPL-2.0-or-later */ #include "xkb_rules.h" #include "config-workspace.h" #include "debug.h" #include #include #include #include // for Qt::escape #include #include #include "x11_helper.h" // for findXkbRuleFile #include #include #include #include #include #include class RulesHandler : public QXmlDefaultHandler { public: RulesHandler(Rules *rules_, bool fromExtras_) : rules(rules_) , fromExtras(fromExtras_) { } bool startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &attributes) override; bool endElement(const QString &namespaceURI, const QString &localName, const QString &qName) override; bool characters(const QString &str) override; private: QStringList path; Rules *rules; const bool fromExtras; }; static QString translate_xml_item(const QString &itemText) { if (itemText.isEmpty()) { // i18n warns on empty input strings return itemText; } // messages are already extracted from the source XML files by xkb // the characters '<' and '>' (but not '"') are HTML-escaped in the xkeyboard-config translation files, so we need to convert them before/after looking up // the translation note that we cannot use QString::toHtmlEscaped() here because that would convert '"' as well QString msgid(itemText); return i18nd("xkeyboard-config", msgid.replace(QLatin1String("<"), QLatin1String("<")).replace(QLatin1String(">"), QLatin1String(">")).toUtf8()) .replace(QLatin1String("<"), QLatin1String("<")) .replace(QLatin1String(">"), QLatin1String(">")); } static QString translate_description(ConfigItem *item) { return item->description.isEmpty() ? item->name : translate_xml_item(item->description); } static bool notEmpty(const ConfigItem *item) { return !item->name.isEmpty(); } template void removeEmptyItems(QList &list) { QtConcurrent::blockingFilter(list, notEmpty); } static void postProcess(Rules *rules) { // TODO remove elements with empty names to safeguard us removeEmptyItems(rules->layoutInfos); removeEmptyItems(rules->modelInfos); removeEmptyItems(rules->optionGroupInfos); // bindtextdomain("xkeyboard-config", LOCALE_DIR); for (ModelInfo *modelInfo : qAsConst(rules->modelInfos)) { modelInfo->vendor = translate_xml_item(modelInfo->vendor); modelInfo->description = translate_description(modelInfo); } for (LayoutInfo *layoutInfo : qAsConst(rules->layoutInfos)) { layoutInfo->description = translate_description(layoutInfo); removeEmptyItems(layoutInfo->variantInfos); for (VariantInfo *variantInfo : qAsConst(layoutInfo->variantInfos)) { variantInfo->description = translate_description(variantInfo); } } for (OptionGroupInfo *optionGroupInfo : qAsConst(rules->optionGroupInfos)) { optionGroupInfo->description = translate_description(optionGroupInfo); removeEmptyItems(optionGroupInfo->optionInfos); for (OptionInfo *optionInfo : qAsConst(optionGroupInfo->optionInfos)) { optionInfo->description = translate_description(optionInfo); } } } Rules::Rules() : version(QStringLiteral("1.0")) { } QString Rules::getRulesName() { if (!QX11Info::isPlatformX11()) { return QString(); } XkbRF_VarDefsRec vd; char *tmp = nullptr; if (XkbRF_GetNamesProp(QX11Info::display(), &tmp, &vd) && tmp != nullptr) { // qCDebug(KCM_KEYBOARD) << "namesprop" << tmp ; const QString name(tmp); XFree(tmp); return name; } return {}; } QString Rules::findXkbDir() { return QStringLiteral(XKBDIR); } static QString findXkbRulesFile() { QString rulesFile; QString rulesName = Rules::getRulesName(); const QString xkbDir = Rules::findXkbDir(); if (!rulesName.isNull()) { rulesFile = QStringLiteral("%1/rules/%2.xml").arg(xkbDir, rulesName); } else { // default to evdev rulesFile = QStringLiteral("%1/rules/evdev.xml").arg(xkbDir); } return rulesFile; } static void mergeRules(Rules *rules, Rules *extraRules) { rules->modelInfos.append(extraRules->modelInfos); rules->optionGroupInfos.append(extraRules->optionGroupInfos); // need to iterate and merge? QList layoutsToAdd; for (LayoutInfo *extraLayoutInfo : qAsConst(extraRules->layoutInfos)) { LayoutInfo *layoutInfo = findByName(rules->layoutInfos, extraLayoutInfo->name); if (layoutInfo != nullptr) { layoutInfo->variantInfos.append(extraLayoutInfo->variantInfos); layoutInfo->languages.append(extraLayoutInfo->languages); } else { layoutsToAdd.append(extraLayoutInfo); } } rules->layoutInfos.append(layoutsToAdd); qCDebug(KCM_KEYBOARD) << "Merged from extra rules:" << extraRules->layoutInfos.size() << "layouts," << extraRules->modelInfos.size() << "models," << extraRules->optionGroupInfos.size() << "option groups"; // base rules now own the objects - remove them from extra rules so that it does not try to delete them extraRules->layoutInfos.clear(); extraRules->modelInfos.clear(); extraRules->optionGroupInfos.clear(); } const char Rules::XKB_OPTION_GROUP_SEPARATOR = ':'; Rules *Rules::readRules(ExtrasFlag extrasFlag) { Rules *rules = new Rules(); QString rulesFile = findXkbRulesFile(); if (!readRules(rules, rulesFile, false)) { delete rules; return nullptr; } if (extrasFlag == Rules::READ_EXTRAS) { QRegExp regex(QStringLiteral("\\.xml$")); Rules *rulesExtra = new Rules(); QString extraRulesFile = rulesFile.replace(regex, QStringLiteral(".extras.xml")); if (readRules(rulesExtra, extraRulesFile, true)) { // not fatal if it fails mergeRules(rules, rulesExtra); } delete rulesExtra; } return rules; } Rules *Rules::readRules(Rules *rules, const QString &filename, bool fromExtras) { QFile file(filename); if (!file.open(QFile::ReadOnly | QFile::Text)) { qCCritical(KCM_KEYBOARD) << "Cannot open the rules file" << file.fileName(); return nullptr; } RulesHandler rulesHandler(rules, fromExtras); QXmlSimpleReader reader; reader.setContentHandler(&rulesHandler); reader.setErrorHandler(&rulesHandler); QXmlInputSource xmlInputSource(&file); qCDebug(KCM_KEYBOARD) << "Parsing xkb rules from" << file.fileName(); if (!reader.parse(xmlInputSource)) { qCCritical(KCM_KEYBOARD) << "Failed to parse the rules file" << file.fileName(); return nullptr; } postProcess(rules); return rules; } bool RulesHandler::startElement(const QString & /*namespaceURI*/, const QString & /*localName*/, const QString &qName, const QXmlAttributes &attributes) { path << QString(qName); QString strPath = path.join(QLatin1String("/")); if (strPath.endsWith(QLatin1String("layoutList/layout/configItem"))) { rules->layoutInfos << new LayoutInfo(fromExtras); } else if (strPath.endsWith(QLatin1String("layoutList/layout/variantList/variant"))) { rules->layoutInfos.last()->variantInfos << new VariantInfo(fromExtras); } else if (strPath.endsWith(QLatin1String("modelList/model"))) { rules->modelInfos << new ModelInfo(); } else if (strPath.endsWith(QLatin1String("optionList/group"))) { rules->optionGroupInfos << new OptionGroupInfo(); rules->optionGroupInfos.last()->exclusive = (attributes.value(QStringLiteral("allowMultipleSelection")) != QLatin1String("true")); } else if (strPath.endsWith(QLatin1String("optionList/group/option"))) { rules->optionGroupInfos.last()->optionInfos << new OptionInfo(); } else if (strPath == ("xkbConfigRegistry") && !attributes.value(QStringLiteral("version")).isEmpty()) { rules->version = attributes.value(QStringLiteral("version")); qCDebug(KCM_KEYBOARD) << "xkbConfigRegistry version" << rules->version; } return true; } bool RulesHandler::endElement(const QString & /*namespaceURI*/, const QString & /*localName*/, const QString & /*qName*/) { path.removeLast(); return true; } bool RulesHandler::characters(const QString &str) { if (!str.trimmed().isEmpty()) { QString strPath = path.join(QLatin1String("/")); if (strPath.endsWith(QLatin1String("layoutList/layout/configItem/name"))) { if (rules->layoutInfos.last() != nullptr) { rules->layoutInfos.last()->name = str.trimmed(); } } else if (strPath.endsWith(QLatin1String("layoutList/layout/configItem/description"))) { rules->layoutInfos.last()->description = str.trimmed(); } else if (strPath.endsWith(QLatin1String("layoutList/layout/configItem/languageList/iso639Id"))) { rules->layoutInfos.last()->languages << str.trimmed(); } else if (strPath.endsWith(QLatin1String("layoutList/layout/variantList/variant/configItem/name"))) { rules->layoutInfos.last()->variantInfos.last()->name = str.trimmed(); } else if (strPath.endsWith(QLatin1String("layoutList/layout/variantList/variant/configItem/description"))) { rules->layoutInfos.last()->variantInfos.last()->description = str.trimmed(); } else if (strPath.endsWith(QLatin1String("layoutList/layout/variantList/variant/configItem/languageList/iso639Id"))) { rules->layoutInfos.last()->variantInfos.last()->languages << str.trimmed(); } else if (strPath.endsWith(QLatin1String("modelList/model/configItem/name"))) { rules->modelInfos.last()->name = str.trimmed(); } else if (strPath.endsWith(QLatin1String("modelList/model/configItem/description"))) { rules->modelInfos.last()->description = str.trimmed(); } else if (strPath.endsWith(QLatin1String("modelList/model/configItem/vendor"))) { rules->modelInfos.last()->vendor = str.trimmed(); } else if (strPath.endsWith(QLatin1String("optionList/group/configItem/name"))) { rules->optionGroupInfos.last()->name = str.trimmed(); } else if (strPath.endsWith(QLatin1String("optionList/group/configItem/description"))) { rules->optionGroupInfos.last()->description = str.trimmed(); } else if (strPath.endsWith(QLatin1String("optionList/group/option/configItem/name"))) { rules->optionGroupInfos.last()->optionInfos.last()->name = str.trimmed(); } else if (strPath.endsWith(QLatin1String("optionList/group/option/configItem/description"))) { rules->optionGroupInfos.last()->optionInfos.last()->description = str.trimmed(); } } return true; } bool LayoutInfo::isLanguageSupportedByLayout(const QString &lang) const { return languages.contains(lang) || isLanguageSupportedByVariants(lang); } bool LayoutInfo::isLanguageSupportedByVariants(const QString &lang) const { for (const VariantInfo *info : qAsConst(variantInfos)) { if (info->languages.contains(lang)) return true; } return false; } bool LayoutInfo::isLanguageSupportedByDefaultVariant(const QString &lang) const { if (languages.contains(lang)) return true; if (languages.empty() && isLanguageSupportedByVariants(lang)) return true; return false; } bool LayoutInfo::isLanguageSupportedByVariant(const VariantInfo *variantInfo, const QString &lang) const { if (variantInfo->languages.contains(lang)) return true; // if variant has no languages try to "inherit" them from layout if (variantInfo->languages.empty() && languages.contains(lang)) return true; return false; }