1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2000 Simon Hausmann <hausmann@kde.org> |
4 | SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org> |
5 | SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org> |
6 | |
7 | SPDX-License-Identifier: LGPL-2.0-or-later |
8 | */ |
9 | |
10 | #include "kxmlguiversionhandler_p.h" |
11 | |
12 | #include "kxmlguiclient.h" |
13 | #include "kxmlguifactory.h" |
14 | |
15 | #include <QDomDocument> |
16 | #include <QDomElement> |
17 | #include <QFile> |
18 | #include <QMap> |
19 | #include <QStandardPaths> |
20 | |
21 | struct DocStruct { |
22 | QString file; |
23 | QString data; |
24 | }; |
25 | |
26 | static QList<QDomElement> (const QDomDocument &doc) |
27 | { |
28 | QList<QDomElement> toolbars; |
29 | QDomElement parent = doc.documentElement(); |
30 | for (QDomElement e = parent.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) { |
31 | if (e.tagName().compare(QStringLiteral("ToolBar" ), cs: Qt::CaseInsensitive) == 0) { |
32 | toolbars.append(t: e); |
33 | } |
34 | } |
35 | return toolbars; |
36 | } |
37 | |
38 | static QStringList toolBarNames(const QList<QDomElement> &toolBars) |
39 | { |
40 | QStringList names; |
41 | names.reserve(asize: toolBars.count()); |
42 | for (const QDomElement &e : toolBars) { |
43 | names.append(t: e.attribute(QStringLiteral("name" ))); |
44 | } |
45 | return names; |
46 | } |
47 | |
48 | static void removeToolBars(QDomDocument &doc, const QStringList &toolBarNames) |
49 | { |
50 | QDomElement parent = doc.documentElement(); |
51 | const QList<QDomElement> toolBars = extractToolBars(doc); |
52 | for (const QDomElement &e : toolBars) { |
53 | if (toolBarNames.contains(str: e.attribute(QStringLiteral("name" )))) { |
54 | parent.removeChild(oldChild: e); |
55 | } |
56 | } |
57 | } |
58 | |
59 | static void insertToolBars(QDomDocument &doc, const QList<QDomElement> &toolBars) |
60 | { |
61 | QDomElement parent = doc.documentElement(); |
62 | QDomElement = parent.namedItem(QStringLiteral("MenuBar" )).toElement(); |
63 | QDomElement insertAfter = menuBar; |
64 | if (menuBar.isNull()) { |
65 | insertAfter = parent.firstChildElement(); // if null, insertAfter will do an append |
66 | } |
67 | for (const QDomElement &e : toolBars) { |
68 | QDomNode result = parent.insertAfter(newChild: e, refChild: insertAfter); |
69 | Q_ASSERT(!result.isNull()); |
70 | } |
71 | } |
72 | |
73 | // |
74 | |
75 | typedef QMap<QString, QMap<QString, QString>> ActionPropertiesMap; |
76 | |
77 | static ActionPropertiesMap (const QDomDocument &doc) |
78 | { |
79 | ActionPropertiesMap properties; |
80 | |
81 | QDomElement actionPropElement = doc.documentElement().namedItem(QStringLiteral("ActionProperties" )).toElement(); |
82 | |
83 | if (actionPropElement.isNull()) { |
84 | return properties; |
85 | } |
86 | |
87 | QDomNode n = actionPropElement.firstChild(); |
88 | while (!n.isNull()) { |
89 | QDomElement e = n.toElement(); |
90 | n = n.nextSibling(); // Advance now so that we can safely delete e |
91 | if (e.isNull()) { |
92 | continue; |
93 | } |
94 | |
95 | if (e.tagName().compare(QStringLiteral("action" ), cs: Qt::CaseInsensitive) != 0) { |
96 | continue; |
97 | } |
98 | |
99 | const QString actionName = e.attribute(QStringLiteral("name" )); |
100 | if (actionName.isEmpty()) { |
101 | continue; |
102 | } |
103 | |
104 | QMap<QString, QMap<QString, QString>>::Iterator propIt = properties.find(key: actionName); |
105 | if (propIt == properties.end()) { |
106 | propIt = properties.insert(key: actionName, value: QMap<QString, QString>()); |
107 | } |
108 | |
109 | const QDomNamedNodeMap attributes = e.attributes(); |
110 | const int attributeslength = attributes.length(); |
111 | |
112 | for (int i = 0; i < attributeslength; ++i) { |
113 | const QDomAttr attr = attributes.item(index: i).toAttr(); |
114 | |
115 | if (attr.isNull()) { |
116 | continue; |
117 | } |
118 | |
119 | const QString name = attr.name(); |
120 | |
121 | if (name == QLatin1String("name" ) || name.isEmpty()) { |
122 | continue; |
123 | } |
124 | |
125 | (*propIt)[name] = attr.value(); |
126 | } |
127 | } |
128 | |
129 | return properties; |
130 | } |
131 | |
132 | static void storeActionProperties(QDomDocument &doc, const ActionPropertiesMap &properties) |
133 | { |
134 | QDomElement actionPropElement = doc.documentElement().namedItem(QStringLiteral("ActionProperties" )).toElement(); |
135 | |
136 | if (actionPropElement.isNull()) { |
137 | actionPropElement = doc.createElement(QStringLiteral("ActionProperties" )); |
138 | doc.documentElement().appendChild(newChild: actionPropElement); |
139 | } |
140 | |
141 | // Remove only those ActionProperties entries from the document, that are present |
142 | // in the properties argument. In real life this means that local ActionProperties |
143 | // takes precedence over global ones, if they exists (think local override of shortcuts). |
144 | QDomNode actionNode = actionPropElement.firstChild(); |
145 | while (!actionNode.isNull()) { |
146 | if (properties.contains(key: actionNode.toElement().attribute(QStringLiteral("name" )))) { |
147 | QDomNode nextNode = actionNode.nextSibling(); |
148 | actionPropElement.removeChild(oldChild: actionNode); |
149 | actionNode = nextNode; |
150 | } else { |
151 | actionNode = actionNode.nextSibling(); |
152 | } |
153 | } |
154 | |
155 | ActionPropertiesMap::ConstIterator it = properties.begin(); |
156 | const ActionPropertiesMap::ConstIterator end = properties.end(); |
157 | for (; it != end; ++it) { |
158 | QDomElement action = doc.createElement(QStringLiteral("Action" )); |
159 | action.setAttribute(QStringLiteral("name" ), value: it.key()); |
160 | actionPropElement.appendChild(newChild: action); |
161 | |
162 | const QMap<QString, QString> attributes = (*it); |
163 | QMap<QString, QString>::ConstIterator attrIt = attributes.begin(); |
164 | const QMap<QString, QString>::ConstIterator attrEnd = attributes.end(); |
165 | for (; attrIt != attrEnd; ++attrIt) { |
166 | action.setAttribute(name: attrIt.key(), value: attrIt.value()); |
167 | } |
168 | } |
169 | } |
170 | |
171 | KXmlGuiVersionHandler::KXmlGuiVersionHandler(const QStringList &files) |
172 | { |
173 | Q_ASSERT(!files.isEmpty()); |
174 | |
175 | if (files.count() == 1) { |
176 | // No need to parse version numbers if there's only one file anyway |
177 | m_file = files.first(); |
178 | m_doc = KXMLGUIFactory::readConfigFile(filename: m_file); |
179 | return; |
180 | } |
181 | |
182 | std::vector<DocStruct> allDocuments; |
183 | allDocuments.reserve(n: files.size()); |
184 | |
185 | for (const QString &file : files) { |
186 | allDocuments.push_back(x: {.file: file, .data: KXMLGUIFactory::readConfigFile(filename: file)}); |
187 | } |
188 | |
189 | auto best = allDocuments.end(); |
190 | uint bestVersion = 0; |
191 | |
192 | auto docIt = allDocuments.begin(); |
193 | const auto docEnd = allDocuments.end(); |
194 | for (; docIt != docEnd; ++docIt) { |
195 | const QString versionStr = KXMLGUIClient::findVersionNumber(xml: (*docIt).data); |
196 | if (versionStr.isEmpty()) { |
197 | // qCDebug(DEBUG_KXMLGUI) << "found no version in" << (*docIt).file; |
198 | continue; |
199 | } |
200 | |
201 | bool ok = false; |
202 | uint version = versionStr.toUInt(ok: &ok); |
203 | if (!ok) { |
204 | continue; |
205 | } |
206 | // qCDebug(DEBUG_KXMLGUI) << "found version" << version << "for" << (*docIt).file; |
207 | |
208 | if (version > bestVersion) { |
209 | best = docIt; |
210 | // qCDebug(DEBUG_KXMLGUI) << "best version is now " << version; |
211 | bestVersion = version; |
212 | } |
213 | } |
214 | |
215 | if (best != docEnd) { |
216 | if (best != allDocuments.begin()) { |
217 | auto local = allDocuments.begin(); |
218 | |
219 | if ((*local).file.startsWith(s: QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation))) { |
220 | // load the local document and extract the action properties |
221 | QDomDocument localDocument; |
222 | localDocument.setContent(data: (*local).data); |
223 | |
224 | const ActionPropertiesMap properties = extractActionProperties(doc: localDocument); |
225 | const QList<QDomElement> toolbars = extractToolBars(doc: localDocument); |
226 | |
227 | // in case the document has a ActionProperties section |
228 | // we must not delete it but copy over the global doc |
229 | // to the local and insert the ActionProperties section |
230 | |
231 | // TODO: kedittoolbar should mark toolbars as modified so that |
232 | // we don't keep old toolbars just because the user defined a shortcut |
233 | |
234 | if (!properties.isEmpty() || !toolbars.isEmpty()) { |
235 | // now load the global one with the higher version number |
236 | // into memory |
237 | QDomDocument document; |
238 | document.setContent(data: (*best).data); |
239 | // and store the properties in there |
240 | storeActionProperties(doc&: document, properties); |
241 | if (!toolbars.isEmpty()) { |
242 | // remove application toolbars present in the user file |
243 | // (not others, that the app might have added since) |
244 | removeToolBars(doc&: document, toolBarNames: toolBarNames(toolBars: toolbars)); |
245 | // add user toolbars |
246 | insertToolBars(doc&: document, toolBars: toolbars); |
247 | } |
248 | |
249 | (*local).data = document.toString(); |
250 | // make sure we pick up the new local doc, when we return later |
251 | best = local; |
252 | |
253 | // write out the new version of the local document |
254 | QFile f((*local).file); |
255 | if (f.open(flags: QIODevice::WriteOnly)) { |
256 | const QByteArray utf8data = (*local).data.toUtf8(); |
257 | f.write(data: utf8data.constData(), len: utf8data.length()); |
258 | f.close(); |
259 | } |
260 | } else { |
261 | // Move away the outdated local file, to speed things up next time |
262 | const QString f = (*local).file; |
263 | const QString backup = f + QLatin1String(".backup" ); |
264 | QFile::rename(oldName: f, newName: backup); |
265 | } |
266 | } |
267 | } |
268 | m_doc = (*best).data; |
269 | m_file = (*best).file; |
270 | } else { |
271 | // qCDebug(DEBUG_KXMLGUI) << "returning first one..."; |
272 | const auto &[file, data] = allDocuments.at(n: 0); |
273 | m_file = file; |
274 | m_doc = data; |
275 | } |
276 | } |
277 | |