1 /** ===========================================================
2  * @file
3  *
4  * This file is a part of KDE project
5  *
6  *
7  * @date   2004-02-01
8  * @brief  plugin interface
9  *
10  * @author Copyright (C) 2004-2018 by Gilles Caulier
11  *         <a href="mailto:caulier dot gilles at gmail dot com">caulier dot gilles at gmail dot com</a>
12  * @author Copyright (C) 2012      by Victor Dodon
13  *         <a href="mailto:dodonvictor at gmail dot com">dodonvictor at gmail dot com</a>
14  * @author Copyright (C) 2004-2005 by Renchi Raju
15  *         <a href="mailto:renchi dot raju at gmail dot com">renchi dot raju at gmail dot com</a>
16  * @author Copyright (C) 2004-2005 by Jesper K. Pedersen
17  *         <a href="mailto:blackie at kde dot org">blackie at kde dot org</a>
18  * @author Copyright (C) 2004-2005 by Aurelien Gateau
19  *         <a href="mailto:aurelien dot gateau at free dot fr">aurelien dot gateau at free dot fr</a>
20  *
21  * This program is free software; you can redistribute it
22  * and/or modify it under the terms of the GNU General
23  * Public License as published by the Free Software Foundation;
24  * either version 2, or (at your option)
25  * any later version.
26  *
27  * This program is distributed in the hope that it will be useful,
28  * but WITHOUT ANY WARRANTY; without even the implied warranty of
29  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30  * GNU General Public License for more details.
31  *
32  * ============================================================ */
33 
34 #include "plugin.h"
35 
36 // Qt includes
37 
38 #include <QApplication>
39 #include <QWidget>
40 #include <QFile>
41 #include <QDir>
42 #include <QAction>
43 #include <QStandardPaths>
44 
45 // KF includes
46 
47 #include <KActionCollection>
48 
49 // Local includes
50 
51 #include "libkipi_version.h"
52 #include "libkipi_debug.h"
53 #include "interface.h"
54 #include "pluginloader.h"
55 
56 namespace KIPI
57 {
58 
59 class Q_DECL_HIDDEN Plugin::Private
60 {
61 public:
62 
Private()63     Private() :
64         uiBaseName(QString())
65     {
66         defaultWidget   = nullptr;
67         defaultCategory = InvalidCategory;
68     }
69 
70     ActionCategoryMap actionsCat;
71     QWidget*          defaultWidget;
72     QString           uiBaseName;
73     Category          defaultCategory;
74 
75 public:
76 
77     class XMLParser
78     {
79 
80     public:
81 
82         static QDomElement makeElement(QDomDocument& domDoc, const QDomElement& from);
83         static void        buildPaths(const QDomElement& original, const QDomNodeList& localNodes, QHashPath& paths);
84         static int         findByNameAttr(const QDomNodeList& list, const QDomElement& node);
85         static void        removeDisabledActions(QDomElement& elem);
86 
87     private:
88 
89         XMLParser();
90         static void buildPaths(const QDomElement& original, const QDomNodeList& localNodes, QHashPath& paths, QDomElemList& stack);
91     };
92 };
93 
makeElement(QDomDocument & domDoc,const QDomElement & from)94 QDomElement Plugin::Private::XMLParser::makeElement(QDomDocument& domDoc, const QDomElement& from)
95 {
96     if (domDoc.isNull() || from.isNull())
97         return QDomElement();
98 
99     QDomElement elem            = domDoc.createElement(from.tagName());
100     QDomNamedNodeMap attributes = from.attributes();
101 
102     for (int i = 0; i < attributes.size(); ++i)
103     {
104         QDomAttr attr = attributes.item(i).toAttr();
105 
106         if (attr.name() != QString::fromLatin1("alreadyVisited"))
107             elem.setAttributeNode(attr);
108     }
109 
110     return elem;
111 }
112 
buildPaths(const QDomElement & original,const QDomNodeList & localNodes,QHashPath & paths)113 void Plugin::Private::XMLParser::buildPaths(const QDomElement& original, const QDomNodeList& localNodes, QHashPath& paths)
114 {
115     /*
116      * For each child element of "local", we will construct the path from the
117      * "original" element to first appearance of the respective child in the
118      * subtree.
119      */
120     QDomElemList stack;
121     buildPaths(original, localNodes, paths, stack);
122 }
123 
findByNameAttr(const QDomNodeList & list,const QDomElement & node)124 int Plugin::Private::XMLParser::findByNameAttr(const QDomNodeList& list, const QDomElement& node)
125 {
126     const QString nodeName = node.toElement().attribute(QString::fromLatin1("name"));
127     const QString nodeTag  = node.toElement().tagName();
128 
129     for (int i = 0; i < list.size(); ++i)
130     {
131         QDomElement e = list.at(i).toElement();
132 
133         if (e.tagName() == nodeTag && e.attribute(QString::fromLatin1("name")) == nodeName)
134             return i;
135     }
136 
137     return -1;
138 }
139 
removeDisabledActions(QDomElement & elem)140 void Plugin::Private::XMLParser::removeDisabledActions(QDomElement& elem)
141 {
142     QDomNodeList actionList      = elem.elementsByTagName(QString::fromLatin1("Action"));
143     QStringList  disabledActions = PluginLoader::instance()->disabledPluginActions();
144     QDomElemList disabledElements;
145 
146     for(int i = 0; i < actionList.size(); ++i)
147     {
148         QDomElement el = actionList.item(i).toElement();
149 
150         if (el.isNull())
151             continue;
152 
153         if (disabledActions.contains(el.attribute(QString::fromLatin1("name"))))
154         {
155             disabledElements << el;
156         }
157     }
158 
159     foreach(QDomElement element, disabledElements)
160     {
161         //qCDebug(LIBKIPI_LOG) << "Plugin action '" << element.attribute("name") << "' is disabled.";
162         QDomElement parent = element.parentNode().toElement();
163         parent.removeChild(element);
164     }
165 }
166 
buildPaths(const QDomElement & original,const QDomNodeList & localNodes,QHashPath & paths,QDomElemList & stack)167 void Plugin::Private::XMLParser::buildPaths(const QDomElement& original, const QDomNodeList& localNodes,
168                                             QHashPath& paths, QDomElemList& stack)
169 {
170     stack.push_back(original.cloneNode(true).toElement());
171 
172     int idx;
173 
174     if ((idx = findByNameAttr(localNodes, original)) != -1)
175     {
176         paths[localNodes.item(idx).toElement().attribute(QString::fromLatin1("name"))] = stack;
177     }
178 
179     if (!original.hasChildNodes())
180     {
181         stack.pop_back();
182         return;
183     }
184 
185     for (QDomNode n = original.firstChild(); !n.isNull(); n = n.nextSibling())
186     {
187         QDomElement e = n.toElement();
188 
189         if (e.tagName() == QString::fromLatin1("Menu") && e.hasChildNodes())
190         {
191             buildPaths(e, localNodes, paths, stack);
192         }
193     }
194 
195     stack.pop_back();
196 }
197 
198 // --------------------------------------------------------------------------------------------------------------
199 
Plugin(QObject * const parent,const char * name)200 Plugin::Plugin(QObject* const parent, const char* name)
201       : QObject(parent), d(new Private)
202 {
203     setObjectName(QString::fromLatin1(name));
204 }
205 
~Plugin()206 Plugin::~Plugin()
207 {
208     clearActions();
209     delete d;
210 }
211 
actions(QWidget * const widget) const212 QList<QAction *> Plugin::actions(QWidget* const widget) const
213 {
214     QWidget* const w = !widget ? d->defaultWidget : widget;
215 
216     if (!d->actionsCat.contains(w))
217     {
218         qCWarning(LIBKIPI_LOG) << "Error in plugin. It needs to call Plugin::setup(QWidget*) "
219                                << "as the very first line when overriding the setup method.";
220     }
221 
222     return d->actionsCat[w].keys();
223 }
224 
addAction(const QString & name,QAction * const action)225 void Plugin::addAction(const QString& name, QAction* const action)
226 {
227     if (!action || name.isEmpty())
228         return;
229 
230     if (!PluginLoader::instance()->disabledPluginActions().contains(name))
231     {
232         actionCollection()->addAction(name, action);
233         addAction(action);
234     }
235     else
236     {
237         //qCDebug(LIBKIPI_LOG) << "Action '" << name << "' is disabled.";
238     }
239 }
240 
addAction(QAction * const action)241 void Plugin::addAction(QAction* const action)
242 {
243     addAction(action, d->defaultCategory);
244 }
245 
addAction(const QString & name,QAction * const action,Category cat)246 void Plugin::addAction(const QString& name, QAction* const action, Category cat)
247 {
248     if (!action || name.isEmpty())
249         return;
250 
251     if (!PluginLoader::instance()->disabledPluginActions().contains(name))
252     {
253         actionCollection()->addAction(name, action);
254         addAction(action, cat);
255     }
256     else
257     {
258         //qCDebug(LIBKIPI_LOG) << "Action '" << name << "' is disabled.";
259     }
260 }
261 
addAction(QAction * const action,Category cat)262 void Plugin::addAction(QAction* const action, Category cat)
263 {
264     if (cat == InvalidCategory)
265     {
266         qCWarning(LIBKIPI_LOG) << "Error in plugin. Action '" << action->objectName() << "has "
267                                   "invalid category. You must set default plugin category or "
268                                   "to use a valid Category";
269     }
270 
271     d->actionsCat[d->defaultWidget].insert(action, cat);
272 }
273 
setup(QWidget * const widget)274 void Plugin::setup(QWidget* const widget)
275 {
276     clearActions();
277     d->defaultWidget = widget;
278     d->actionsCat.insert(widget, QMap<QAction*, Category>());
279 }
280 
category(QAction * const action) const281 Category Plugin::category(QAction* const action) const
282 {
283     QMap<QAction *, Category>::const_iterator it = d->actionsCat[d->defaultWidget].constFind(action);
284 
285     if (it != d->actionsCat[d->defaultWidget].constEnd())
286     {
287         return it.value();
288     }
289     else
290     {
291         if (d->defaultCategory == InvalidCategory)
292         {
293             qCWarning(LIBKIPI_LOG) << "Error in plugin. Invalid category. "
294                                       "You must set default plugin category.";
295         }
296 
297         return d->defaultCategory;
298     }
299 }
300 
interface() const301 Interface* Plugin::interface() const
302 {
303     return (dynamic_cast<Interface*>(parent()));
304 }
305 
setUiBaseName(const char * name)306 void Plugin::setUiBaseName(const char* name)
307 {
308     if (name && *name)
309         d->uiBaseName = QString::fromLatin1(name);
310 }
311 
uiBaseName() const312 QString Plugin::uiBaseName() const
313 {
314     return d->uiBaseName;
315 }
316 
mergeXMLFile(KXMLGUIClient * const host)317 void Plugin::mergeXMLFile(KXMLGUIClient *const host)
318 {
319     if (!host)
320     {
321         qCCritical(LIBKIPI_LOG) << "Host KXMLGUIClient is null!";
322         return;
323     }
324 
325     if (d->uiBaseName.isEmpty())
326     {
327         qCCritical(LIBKIPI_LOG) << "UI file basename is not set! You must first call setUiBaseName.";
328         return;
329     }
330 
331     const QString componentName = QApplication::applicationName();
332     const QString defaultUI     = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString::fromLatin1("kxmlgui5/kipi/") + d->uiBaseName);
333     const QString localUIdir    = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QString::fromLatin1("/kxmlgui5/") +
334                                                                    componentName;
335     const QString localUI       = localUIdir + QString::fromLatin1("/") + d->uiBaseName;
336 
337     qCDebug(LIBKIPI_LOG) << "UI file :" << defaultUI;
338 
339     QFile        defaultUIFile(defaultUI);
340     QDomDocument defaultDomDoc;
341 
342     if (!defaultUIFile.open(QFile::ReadOnly) || !defaultDomDoc.setContent(&defaultUIFile))
343     {
344         qCCritical(LIBKIPI_LOG) << "Could not open default ui file " << defaultUI << " for ui basename " << d->uiBaseName;
345         return;
346     }
347 
348     defaultUIFile.close();
349     const QDomDocument hostDoc    = host->domDocument();
350 
351     if (hostDoc.isNull() || defaultDomDoc.isNull())
352     {
353         qCCritical(LIBKIPI_LOG) << "Cannot merge the XML files, at least one is null!";
354         return;
355     }
356 
357     QDomElement hostGuiElem       = hostDoc.firstChildElement(QString::fromLatin1("kpartgui"));
358     QDomElement hostMenuBarElem   = hostGuiElem.firstChildElement(QString::fromLatin1("MenuBar"));
359 
360     QDomDocument newPluginDoc(defaultDomDoc.doctype());
361     QDomElement  defGuiElem       = defaultDomDoc.firstChildElement(QString::fromLatin1("gui"));
362 
363     Private::XMLParser::removeDisabledActions(defGuiElem);
364 
365     QDomElement newGuiElem        = Private::XMLParser::makeElement(newPluginDoc, defGuiElem);
366     QDomElement defMenuBarElem    = defGuiElem.firstChildElement(QString::fromLatin1("MenuBar"));
367     QDomElement newMenuBarElem    = Private::XMLParser::makeElement(newPluginDoc, defMenuBarElem);
368     QDomElement defToolBarElem    = defGuiElem.firstChildElement(QString::fromLatin1("ToolBar"));
369     QDomElement defActionPropElem = defGuiElem.firstChildElement(QString::fromLatin1("ActionProperties"));
370 
371     QHashPath paths;
372     Private::XMLParser::buildPaths(hostMenuBarElem, defMenuBarElem.childNodes(), paths);
373 
374     for (QDomNode n = defMenuBarElem.firstChild(); !n.isNull(); n = n.nextSibling())
375     {
376         QDomElemList path    = paths[n.toElement().attribute(QString::fromLatin1("name"))];
377         QDomElement current  = newMenuBarElem;
378         QDomElement origCurr = defMenuBarElem;
379 
380         if (path.empty())
381         {
382             newMenuBarElem.appendChild(n.cloneNode());
383         }
384         else
385         {
386             for (int i = 1; i < path.size() - 1; ++i)
387             {
388                 int idx  = Private::XMLParser::findByNameAttr(current.childNodes(), path[i]);
389                 origCurr = path[i];
390 
391                 if (idx == -1)
392                 {
393                     if (!path[i].isNull())
394                     {
395                         QDomElement newChild = Private::XMLParser::makeElement(newPluginDoc, path[i]);
396                         QDomElement textElem = origCurr.firstChildElement(QString::fromLatin1("text"));
397 
398                         if (!textElem.isNull())
399                         {
400                             newChild.appendChild(textElem.cloneNode());
401                         }
402 
403                         current.appendChild(newChild);
404                         current = newChild;
405                     }
406                 }
407                 else
408                 {
409                     current = current.childNodes().item(idx).toElement();
410                 }
411             }
412         }
413 
414         if (!current.isNull())
415             current.appendChild(n.cloneNode());
416     }
417 
418     newGuiElem.appendChild(newMenuBarElem);
419     QFile        localUIFile(localUI);
420     QDomDocument localDomDoc;
421 // be safe rather than sorry
422 // create the appname folder in kxmlgui5
423     QDir localUIDir(localUIdir);
424     if (!localUIDir.exists())
425         QDir().mkpath(localUIdir);
426 
427    if (!localUIFile.exists() || !localUIFile.open(QFile::ReadOnly) || !localDomDoc.setContent(&localUIFile))
428     {
429         newGuiElem.appendChild(defToolBarElem.cloneNode());
430         newGuiElem.appendChild(defActionPropElem.cloneNode());
431     }
432     else
433     {
434         QDomElement localGuiElem        = localDomDoc.firstChildElement(QString::fromLatin1("gui"));
435 
436         Private::XMLParser::removeDisabledActions(localGuiElem);
437 
438         QDomElement localToolBarElem    = localGuiElem.firstChildElement(QString::fromLatin1("ToolBar"));
439         QDomElement localActionPropElem = localGuiElem.firstChildElement(QString::fromLatin1("ActionProperties"));
440 
441         newGuiElem.appendChild(localToolBarElem.cloneNode());
442         newGuiElem.appendChild(localActionPropElem.cloneNode());
443     }
444 
445     localUIFile.close();
446     QFile writeFile(localUI);
447 
448     if (!writeFile.open(QFile::WriteOnly | QFile::Truncate))
449     {
450         qCCritical(LIBKIPI_LOG) << "Could not open " << localUI << " for writing!";
451         return;
452     }
453 
454     newPluginDoc.appendChild(newGuiElem);
455 
456     writeFile.write(newPluginDoc.toString().toUtf8());
457     writeFile.close();
458 
459     setXMLFile(d->uiBaseName);
460 }
461 
clearActions()462 void Plugin::clearActions()
463 {
464     QList<QAction*> actions = actionCollection()->actions();
465 
466     foreach (QAction* const action, actions)
467     {
468         actionCollection()->removeAction(action);
469     }
470 }
471 
setupXML()472 void Plugin::setupXML()
473 {
474     mergeXMLFile(dynamic_cast<KXMLGUIClient*>(interface()->parent()));
475 }
476 
rebuild()477 void Plugin::rebuild()
478 {
479     QString file = xmlFile();
480 
481     if (!file.isEmpty())
482     {
483         setXMLGUIBuildDocument(QDomDocument());
484         setXMLFile(file, false);
485     }
486 }
487 
setDefaultCategory(Category cat)488 void Plugin::setDefaultCategory(Category cat)
489 {
490     d->defaultCategory = cat;
491 }
492 
defaultCategory() const493 Category Plugin::defaultCategory() const
494 {
495     return d->defaultCategory;
496 }
497 
498 } // namespace KIPI
499