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

0001 /*
0002     SPDX-FileCopyrightText: 2011 Andriy Rysin <rysin@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "layout_memory_persister.h"
0008 #include "debug.h"
0009 
0010 #include <KConfigGroup>
0011 #include <KSharedConfig>
0012 
0013 #include <QDir>
0014 #include <QDomDocument> // TODO port to QXmlStreamWriter to save memory, we don't actually need DOM
0015 #include <QFile>
0016 #include <QStandardPaths>
0017 #include <QXmlStreamReader>
0018 
0019 #include "keyboard_config.h"
0020 #include "layout_memory.h"
0021 
0022 static const char VERSION[] = "1.0";
0023 static const char DOC_NAME[] = "LayoutMap";
0024 static const char ROOT_NODE[] = "LayoutMap";
0025 static const char VERSION_ATTRIBUTE[] = "version";
0026 static const char SWITCH_MODE_ATTRIBUTE[] = "SwitchMode";
0027 static const char ITEM_NODE[] = "item";
0028 static const QString CURRENT_LAYOUT_ATTRIBUTE(QStringLiteral("currentLayout"));
0029 static const char OWNER_KEY_ATTRIBUTE[] = "ownerKey";
0030 static const char LAYOUTS_ATTRIBUTE[] = "layouts";
0031 
0032 static const QChar LIST_SEPARATOR_LM = ',';
0033 
0034 static const char REL_SESSION_FILE_PATH[] = "/keyboard/session/layout_memory.xml";
0035 
0036 static bool isDefaultLayoutConfig(const LayoutSet &layout, const QList<LayoutUnit> &defaultLayouts)
0037 {
0038     if (defaultLayouts.isEmpty() || layout.layouts != defaultLayouts || layout.currentLayout != defaultLayouts.first()) {
0039         return false;
0040     }
0041     return true;
0042 }
0043 
0044 QString LayoutMemoryPersister::getLayoutMapAsString()
0045 {
0046     if (!canPersist())
0047         return QString();
0048 
0049     QDomDocument doc(DOC_NAME);
0050     QDomElement root = doc.createElement(ROOT_NODE);
0051     root.setAttribute(VERSION_ATTRIBUTE, VERSION);
0052     root.setAttribute(SWITCH_MODE_ATTRIBUTE, layoutMemory.keyboardConfig.switchMode());
0053     doc.appendChild(root);
0054 
0055     if (layoutMemory.keyboardConfig.switchingPolicy() == KeyboardConfig::SWITCH_POLICY_GLOBAL) {
0056         if (!globalLayout.isValid())
0057             return QString();
0058 
0059         QDomElement item = doc.createElement(ITEM_NODE);
0060         item.setAttribute(CURRENT_LAYOUT_ATTRIBUTE, globalLayout.toString());
0061         root.appendChild(item);
0062     } else {
0063         const QStringList keys = layoutMemory.layoutMap.keys();
0064         for (const QString &key : keys) {
0065             if (isDefaultLayoutConfig(layoutMemory.layoutMap[key], layoutMemory.keyboardConfig.getDefaultLayouts())) {
0066                 continue;
0067             }
0068 
0069             QDomElement item = doc.createElement(ITEM_NODE);
0070             item.setAttribute(OWNER_KEY_ATTRIBUTE, key);
0071             item.setAttribute(CURRENT_LAYOUT_ATTRIBUTE, layoutMemory.layoutMap[key].currentLayout.toString());
0072 
0073             QString layoutSetString;
0074             const QList<LayoutUnit> layouts = layoutMemory.layoutMap[key].layouts;
0075             for (const LayoutUnit &layoutUnit : layouts) {
0076                 if (!layoutSetString.isEmpty()) {
0077                     layoutSetString += LIST_SEPARATOR_LM;
0078                 }
0079                 layoutSetString += layoutUnit.toString();
0080             }
0081             item.setAttribute(LAYOUTS_ATTRIBUTE, layoutSetString);
0082             root.appendChild(item);
0083         }
0084     }
0085 
0086     return doc.toString();
0087 }
0088 
0089 bool LayoutMemoryPersister::save()
0090 {
0091     QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + REL_SESSION_FILE_PATH);
0092 
0093     QDir baseDir(fileInfo.absoluteDir());
0094     if (!baseDir.exists()) {
0095         if (!QDir().mkpath(baseDir.absolutePath())) {
0096             qCWarning(KCM_KEYBOARD) << "Failed to create directory" << baseDir.absolutePath();
0097         }
0098     }
0099 
0100     QFile file(fileInfo.absoluteFilePath());
0101     return saveToFile(file);
0102 }
0103 
0104 bool LayoutMemoryPersister::restore()
0105 {
0106     QFile file(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + REL_SESSION_FILE_PATH);
0107     if (!file.exists()) {
0108         return false;
0109     }
0110     return restoreFromFile(file);
0111 }
0112 
0113 bool LayoutMemoryPersister::saveToFile(const QFile &file_)
0114 {
0115     QString xml = getLayoutMapAsString();
0116     if (xml.isEmpty())
0117         return false;
0118 
0119     QFile file(file_.fileName()); // so we don't expose the file we open/close to the caller
0120     if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
0121         qCWarning(KCM_KEYBOARD) << "Failed to open layout memory xml file for writing" << file.fileName();
0122         return false;
0123     }
0124 
0125     QTextStream out(&file);
0126     out << xml;
0127     out.flush();
0128 
0129     if (file.error() != QFile::NoError) {
0130         qCWarning(KCM_KEYBOARD) << "Failed to store keyboard layout memory, error" << file.error();
0131         file.close();
0132         file.remove();
0133         return false;
0134     } else {
0135         qCDebug(KCM_KEYBOARD) << "Keyboard layout memory stored into" << file.fileName() << "written" << file.pos();
0136         return true;
0137     }
0138 }
0139 
0140 class MapHandler
0141 {
0142 public:
0143     MapHandler(const KeyboardConfig::SwitchingPolicy &switchingPolicy_)
0144         : verified(false)
0145         , switchingPolicy(switchingPolicy_)
0146     {
0147     }
0148 
0149     bool startElement(QXmlStreamReader &xml)
0150     {
0151         if (!verified && xml.name() == QLatin1String(ROOT_NODE)) {
0152             if (xml.attributes().value(VERSION_ATTRIBUTE) != QLatin1String(VERSION)) {
0153                 xml.raiseError("Unexpected version!");
0154                 return false;
0155             }
0156             if (xml.attributes().value(SWITCH_MODE_ATTRIBUTE) != KeyboardConfig::getSwitchingPolicyString(switchingPolicy)) {
0157                 xml.raiseError("Unexpected switching mode!");
0158                 return false;
0159             }
0160             verified = true;
0161         } else if (xml.name() == QLatin1String(ITEM_NODE)) {
0162             if (!verified) {
0163                 xml.raiseError("Malformed xml structure!");
0164                 return false;
0165             }
0166 
0167             if (switchingPolicy == KeyboardConfig::SWITCH_POLICY_GLOBAL) {
0168                 globalLayout = LayoutUnit(xml.attributes().value(CURRENT_LAYOUT_ATTRIBUTE).toString());
0169             } else {
0170                 const auto layoutStrings = xml.attributes().value(LAYOUTS_ATTRIBUTE).split(LIST_SEPARATOR_LM);
0171                 LayoutSet layoutSet;
0172                 for (const auto &layoutString : layoutStrings) {
0173                     layoutSet.layouts.append(LayoutUnit(layoutString.toString()));
0174                 }
0175                 layoutSet.currentLayout = LayoutUnit(xml.attributes().value(CURRENT_LAYOUT_ATTRIBUTE).toString());
0176                 QString ownerKey = xml.attributes().value(OWNER_KEY_ATTRIBUTE).toString();
0177 
0178                 if (ownerKey.trimmed().isEmpty() || !layoutSet.isValid()) {
0179                     xml.raiseError("Invalid layout data!");
0180                     return false;
0181                 }
0182 
0183                 layoutMap[ownerKey] = layoutSet;
0184             }
0185             xml.skipCurrentElement();
0186         } else {
0187             verified = false;
0188             xml.raiseError("Malformed xml structure! Unexpected element!");
0189         }
0190         return verified;
0191     }
0192 
0193     bool verified;
0194     QMap<QString, LayoutSet> layoutMap;
0195     LayoutUnit globalLayout;
0196 
0197 private:
0198     const KeyboardConfig::SwitchingPolicy &switchingPolicy;
0199 };
0200 
0201 template<typename T>
0202 static bool containsAll(const QList<T> &set1, const QList<T> &set2)
0203 {
0204     for (const T &t : set2) {
0205         if (!set1.contains(t))
0206             return false;
0207     }
0208     return true;
0209 }
0210 
0211 bool LayoutMemoryPersister::restoreFromFile(const QFile &file_)
0212 {
0213     globalLayout = LayoutUnit();
0214 
0215     if (!canPersist())
0216         return false;
0217 
0218     QFile file(file_.fileName()); // so we don't expose the file we open/close to the caller
0219     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0220         qCWarning(KCM_KEYBOARD) << "Failed to open layout memory xml file for reading" << file.fileName() << "error:" << file.error();
0221         return false;
0222     }
0223 
0224     auto switchingPolicy = layoutMemory.keyboardConfig.switchingPolicy();
0225     MapHandler mapHandler(switchingPolicy);
0226 
0227     QXmlStreamReader xml(&file);
0228     qCDebug(KCM_KEYBOARD) << "Restoring keyboard layout map from" << file.fileName();
0229 
0230     auto firstElement = xml.readNextStartElement();
0231     do {
0232         if (!firstElement || !mapHandler.startElement(xml)) {
0233             qCWarning(KCM_KEYBOARD) << "Failed to parse the layout memory file" << file.fileName();
0234             qCWarning(KCM_KEYBOARD) << xml.errorString();
0235             return false;
0236         }
0237     } while (xml.readNextStartElement());
0238 
0239     if (xml.hasError()) {
0240         qCWarning(KCM_KEYBOARD) << "XML error:" << xml.errorString();
0241         return false;
0242     }
0243 
0244     if (layoutMemory.keyboardConfig.switchingPolicy() == KeyboardConfig::SWITCH_POLICY_GLOBAL) {
0245         if (mapHandler.globalLayout.isValid() && layoutMemory.keyboardConfig.layouts.contains(mapHandler.globalLayout)) {
0246             globalLayout = mapHandler.globalLayout;
0247             qCDebug(KCM_KEYBOARD) << "Restored global layout" << globalLayout.toString();
0248         }
0249     } else {
0250         layoutMemory.layoutMap.clear();
0251         for (const QString &key : mapHandler.layoutMap.keys()) {
0252             if (containsAll(layoutMemory.keyboardConfig.layouts, mapHandler.layoutMap[key].layouts)) {
0253                 layoutMemory.layoutMap.insert(key, mapHandler.layoutMap[key]);
0254             }
0255         }
0256         qCDebug(KCM_KEYBOARD) << "Restored layouts for" << layoutMemory.layoutMap.size() << "containers";
0257     }
0258     return true;
0259 }
0260 
0261 bool LayoutMemoryPersister::canPersist()
0262 {
0263     // we can't persist per window - as we're using window id which is not preserved between sessions
0264     bool windowMode = layoutMemory.keyboardConfig.switchingPolicy() == KeyboardConfig::SWITCH_POLICY_WINDOW;
0265     if (windowMode) {
0266         qCDebug(KCM_KEYBOARD) << "Not saving session for window mode";
0267     }
0268     return !windowMode;
0269 }