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 }