kdelibs/kdeui/xmlgui/kxmlguiclient.cpp
Ivailo Monev e978590acd generic: define KDE_DEFAULT_DEBUG_AREA in the main build file for each library
Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2023-06-09 23:43:20 +03:00

746 lines
24 KiB
C++

/* This file is part of the KDE libraries
Copyright (C) 2000 Simon Hausmann <hausmann@kde.org>
Copyright (C) 2000 Kurt Granroth <granroth@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2 as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "kxmlguiclient.h"
#include "kxmlguiversionhandler_p.h"
#include "kxmlguifactory.h"
#include "kxmlguibuilder.h"
#include "kcomponentdata.h"
#include "kstandarddirs.h"
#include "kdebug.h"
#include "kaction.h"
#include "kactioncollection.h"
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtXml/qdom.h>
#include <QtCore/QTextStream>
#include <QtCore/QRegExp>
#include <QtCore/QPointer>
#include <assert.h>
class KXMLGUIClientPrivate
{
public:
KXMLGUIClientPrivate()
: m_componentData(KGlobal::mainComponent()),
m_actionCollection(0),
m_parent(0L),
m_builder(0L)
{
}
~KXMLGUIClientPrivate()
{
if (m_actionCollection) {
m_actionCollection->deleteLater();
}
if (m_builder) {
delete m_builder;
}
}
bool mergeXML( QDomElement &base, QDomElement &additive,
KActionCollection *actionCollection );
bool isEmptyContainer(const QDomElement& base,
KActionCollection *actionCollection) const;
QDomElement findMatchingElement( const QDomElement &base,
const QDomElement &additive );
KComponentData m_componentData;
QDomDocument m_doc;
KActionCollection *m_actionCollection;
QDomDocument m_buildDocument;
QPointer<KXMLGUIFactory> m_factory;
KXMLGUIClient *m_parent;
//QPtrList<KXMLGUIClient> m_supers;
QList<KXMLGUIClient*> m_children;
KXMLGUIBuilder *m_builder;
QString m_xmlFile;
QString m_localXMLFile;
// Actions to enable/disable on a state change
QMap<QString,KXMLGUIClient::StateChange> m_actionsStateMap;
};
KXMLGUIClient::KXMLGUIClient()
: d( new KXMLGUIClientPrivate )
{
}
KXMLGUIClient::KXMLGUIClient( KXMLGUIClient *parent )
: d( new KXMLGUIClientPrivate )
{
parent->insertChildClient( this );
}
KXMLGUIClient::~KXMLGUIClient()
{
if ( d->m_parent ) {
d->m_parent->removeChildClient( this );
}
if ( d->m_factory ) {
kWarning() << this << "deleted without having been removed from the factory first. This will leak standalone popupmenus and could lead to crashes.";
d->m_factory->forgetClient(this);
}
foreach (KXMLGUIClient* client, d->m_children) {
if (d->m_factory)
d->m_factory->forgetClient(client);
assert( client->d->m_parent == this );
client->d->m_parent = 0;
}
delete d;
}
QAction *KXMLGUIClient::action( const char *name ) const
{
QAction* act = actionCollection()->action( name );
if ( !act ) {
foreach (KXMLGUIClient* client, d->m_children) {
act = client->actionCollection()->action( name );
if ( act )
break;
}
}
return act;
}
KActionCollection *KXMLGUIClient::actionCollection() const
{
if ( !d->m_actionCollection )
{
d->m_actionCollection = new KActionCollection( this );
d->m_actionCollection->setObjectName( "KXMLGUIClient-KActionCollection" );
}
return d->m_actionCollection;
}
QAction *KXMLGUIClient::action( const QDomElement &element ) const
{
static const QString attrName = QString::fromLatin1( "name" );
return actionCollection()->action( qPrintable(element.attribute( attrName )) );
}
KComponentData KXMLGUIClient::componentData() const
{
return d->m_componentData;
}
QDomDocument KXMLGUIClient::domDocument() const
{
return d->m_doc;
}
QString KXMLGUIClient::xmlFile() const
{
return d->m_xmlFile;
}
QString KXMLGUIClient::localXMLFile() const
{
if ( !d->m_localXMLFile.isEmpty() )
return d->m_localXMLFile;
if ( !QDir::isRelativePath(d->m_xmlFile) )
return QString(); // can't save anything here
if (d->m_xmlFile.isEmpty()) // setXMLFile not called at all, can't save. Use case: ToolBarHandler
return QString();
return KStandardDirs::locateLocal( "data", componentData().componentName() + '/' + d->m_xmlFile );
}
void KXMLGUIClient::reloadXML()
{
// TODO: this method can't be used for the KXmlGuiWindow, since it doesn't merge in ui_standards.rc!
// -> KDE5: load ui_standards_rc in setXMLFile using a flag, and remember that flag?
// and then KEditToolBar can use reloadXML.
QString file( xmlFile() );
if ( !file.isEmpty() )
setXMLFile( file );
}
void KXMLGUIClient::setComponentData(const KComponentData &componentData)
{
d->m_componentData = componentData;
actionCollection()->setComponentData( componentData );
if ( d->m_builder )
d->m_builder->setBuilderClient( this );
}
void KXMLGUIClient::loadStandardsXmlFile()
{
const QString file = KStandardDirs::locate("config", "ui/ui_standards.rc", componentData());
if (file.isEmpty()) {
kWarning() << "ui/ui_standards.rc not found in" << componentData().dirs()->resourceDirs("config");
} else {
const QString doc = KXMLGUIFactory::readConfigFile( file );
setXML( doc );
}
}
void KXMLGUIClient::setXMLFile( const QString& _file, bool merge, bool setXMLDoc )
{
// store our xml file name
if ( !_file.isNull() )
d->m_xmlFile = _file;
if ( !setXMLDoc )
return;
QString file = _file;
QStringList allFiles;
if ( !QDir::isRelativePath( file ) ) {
allFiles.append( file );
} else {
const QString filter = componentData().componentName() + '/' + _file;
allFiles = componentData().dirs()->findAllResources("data", filter) +
componentData().dirs()->findAllResources("data", _file);
}
if ( allFiles.isEmpty() && !_file.isEmpty() ) {
// if a non-empty file gets passed and we can't find it,
// inform the developer using some debug output
kWarning() << "cannot find .rc file" << _file << "for component" << componentData().componentName();
}
// make sure to merge the settings from any file specified by setLocalXMLFile()
if ( !d->m_localXMLFile.isEmpty() && !file.endsWith("ui_standards.rc") ) {
const bool exists = QDir::isRelativePath(d->m_localXMLFile) || QFile::exists(d->m_localXMLFile);
if (exists && !allFiles.contains(d->m_localXMLFile))
allFiles.prepend( d->m_localXMLFile );
}
QString doc;
if ( !allFiles.isEmpty() )
file = findMostRecentXMLFile(allFiles, doc);
// Always call setXML, even on error, so that we don't keep all ui_standards.rc menus.
setXML( doc, merge );
}
void KXMLGUIClient::setLocalXMLFile( const QString &file )
{
d->m_localXMLFile = file;
}
void KXMLGUIClient::replaceXMLFile( const QString& xmlfile, const QString& localxmlfile, bool merge )
{
if ( !QDir::isAbsolutePath ( xmlfile ) ) {
kWarning() << "xml file" << xmlfile << "is not an absolute path";
}
setLocalXMLFile ( localxmlfile );
setXMLFile ( xmlfile, merge );
}
void KXMLGUIClient::setXML( const QString &document, bool merge )
{
QDomDocument doc;
QString errorMsg;
int errorLine, errorColumn;
// QDomDocument raises a parse error on empty document, but we accept no app-specific document,
// in which case you only get ui_standards.rc layout.
bool result = document.isEmpty() || doc.setContent(document, &errorMsg, &errorLine, &errorColumn);
if ( result ) {
setDOMDocument( doc, merge );
} else {
#ifdef NDEBUG
kError() << "Error parsing XML document:" << errorMsg << "at line" << errorLine << "column" << errorColumn;
setDOMDocument(QDomDocument(), merge); // otherwise empty menus from ui_standards.rc stay around
#else
kFatal() << "Error parsing XML document:" << errorMsg << "at line" << errorLine << "column" << errorColumn;
#endif
}
}
void KXMLGUIClient::setDOMDocument( const QDomDocument &document, bool merge )
{
if ( merge && !d->m_doc.isNull() )
{
QDomElement base = d->m_doc.documentElement();
QDomElement e = document.documentElement();
// merge our original (global) xml with our new one
d->mergeXML(base, e, actionCollection());
// reassign our pointer as mergeXML might have done something
// strange to it
base = d->m_doc.documentElement();
//kDebug(260) << "Result of xmlgui merging:" << d->m_doc.toString();
// we want some sort of failsafe.. just in case
if ( base.isNull() )
d->m_doc = document;
}
else
{
d->m_doc = document;
}
setXMLGUIBuildDocument( QDomDocument() );
}
// if (equals(a,b)) is more readable than if (a.compare(b, Qt::CaseInsensitive)==0)
static bool equalstr(const QString& a, const QString& b) {
return a.compare(b, Qt::CaseInsensitive) == 0;
}
bool KXMLGUIClientPrivate::mergeXML( QDomElement &base, QDomElement &additive, KActionCollection *actionCollection )
{
static const QString tagAction = QString::fromLatin1( "Action" );
static const QString tagMerge = QString::fromLatin1( "Merge" );
static const QString tagSeparator = QString::fromLatin1( "Separator" );
static const QString attrName = QString::fromLatin1( "name" );
static const QString attrAppend = QString::fromLatin1( "append" );
static const QString attrWeakSeparator = QString::fromLatin1( "weakSeparator" );
static const QString tagMergeLocal = QString::fromLatin1( "MergeLocal" );
static const QString tagText = QString::fromLatin1( "text" );
static const QString attrAlreadyVisited = QString::fromLatin1( "alreadyVisited" );
static const QString attrNoMerge = QString::fromLatin1( "noMerge" );
static const QString attrOne = QString::fromLatin1( "1" );
// there is a possibility that we don't want to merge in the
// additive.. rather, we might want to *replace* the base with the
// additive. this can be for any container.. either at a file wide
// level or a simple container level. we look for the 'noMerge'
// tag, in any event and just replace the old with the new
if ( additive.attribute(attrNoMerge) == attrOne ) // ### use toInt() instead? (Simon)
{
base.parentNode().replaceChild(additive, base);
return true;
} else {
// Merge attributes
{
const QDomNamedNodeMap attribs = additive.attributes();
const uint attribcount = attribs.count();
for(uint i = 0; i < attribcount; ++i) {
const QDomNode node = attribs.item(i);
base.setAttribute(node.nodeName(), node.nodeValue());
}
}
// iterate over all elements in the container (of the global DOM tree)
QDomNode n = base.firstChild();
while ( !n.isNull() )
{
QDomElement e = n.toElement();
n = n.nextSibling(); // Advance now so that we can safely delete e
if (e.isNull())
continue;
const QString tag = e.tagName();
// if there's an action tag in the global tree and the action is
// not implemented, then we remove the element
if (equalstr(tag, tagAction)) {
const QString name = e.attribute(attrName);
if (!actionCollection->action(name))
{
// remove this child as we aren't using it
base.removeChild( e );
continue;
}
}
// if there's a separator defined in the global tree, then add an
// attribute, specifying that this is a "weak" separator
else if (equalstr(tag, tagSeparator)) {
e.setAttribute( attrWeakSeparator, (uint)1 );
// okay, hack time. if the last item was a weak separator OR
// this is the first item in a container, then we nuke the
// current one
QDomElement prev = e.previousSibling().toElement();
if (prev.isNull() ||
(equalstr(prev.tagName(), tagSeparator) && !prev.attribute(attrWeakSeparator).isNull() ) ||
(equalstr(prev.tagName(), tagText))) {
// the previous element was a weak separator or didn't exist
base.removeChild( e );
continue;
}
}
// the MergeLocal tag lets us specify where non-standard elements
// of the local tree shall be merged in. After inserting the
// elements we delete this element
else if (equalstr(tag, tagMergeLocal)) {
QDomNode it = additive.firstChild();
while ( !it.isNull() )
{
QDomElement newChild = it.toElement();
it = it.nextSibling();
if (newChild.isNull() )
continue;
if (equalstr(newChild.tagName(), tagText))
continue;
if ( newChild.attribute( attrAlreadyVisited ) == attrOne )
continue;
QString itAppend( newChild.attribute( attrAppend ) );
QString elemName( e.attribute( attrName ) );
if ( ( itAppend.isNull() && elemName.isEmpty() ) ||
( itAppend == elemName ) )
{
// first, see if this new element matches a standard one in
// the global file. if it does, then we skip it as it will
// be merged in, later
QDomElement matchingElement = findMatchingElement( newChild, base );
if (matchingElement.isNull() || equalstr(newChild.tagName(), tagSeparator))
base.insertBefore( newChild, e );
}
}
base.removeChild( e );
continue;
}
else if (equalstr(tag, tagText)) {
continue;
}
else if (equalstr(tag, tagMerge)) {
continue;
}
// in this last case we check for a separator tag and, if not, we
// can be sure that it is a container --> proceed with child nodes
// recursively and delete the just proceeded container item in
// case it is empty (if the recursive call returns true)
else {
QDomElement matchingElement = findMatchingElement( e, additive );
if ( !matchingElement.isNull() )
{
matchingElement.setAttribute( attrAlreadyVisited, (uint)1 );
if ( mergeXML( e, matchingElement, actionCollection ) )
{
base.removeChild( e );
additive.removeChild(matchingElement); // make sure we don't append it below
continue;
}
continue;
}
else
{
// this is an important case here! We reach this point if the
// "local" tree does not contain a container definition for
// this container. However we have to call mergeXML recursively
// and make it check if there are actions implemented for this
// container. *If* none, then we can remove this container now
QDomElement dummy;
if ( mergeXML( e, dummy, actionCollection ) )
base.removeChild( e );
continue;
}
}
}
//here we append all child elements which were not inserted
//previously via the LocalMerge tag
n = additive.firstChild();
while ( !n.isNull() )
{
QDomElement e = n.toElement();
n = n.nextSibling(); // Advance now so that we can safely delete e
if (e.isNull())
continue;
QDomElement matchingElement = findMatchingElement( e, base );
if ( matchingElement.isNull() )
{
base.appendChild( e );
}
}
// do one quick check to make sure that the last element was not
// a weak separator
QDomElement last = base.lastChild().toElement();
if (equalstr(last.tagName(), tagSeparator) &&
(!last.attribute(attrWeakSeparator).isNull())) {
base.removeChild( last );
}
}
return isEmptyContainer(base, actionCollection);
}
bool KXMLGUIClientPrivate::isEmptyContainer(const QDomElement& base, KActionCollection *actionCollection) const
{
// now we check if we are empty (in which case we return "true", to
// indicate the caller that it can delete "us" (the base element
// argument of "this" call)
QDomNode n = base.firstChild();
while (!n.isNull()) {
const QDomElement e = n.toElement();
n = n.nextSibling(); // Advance now so that we can safely delete e
if (e.isNull())
continue;
const QString tag = e.tagName();
static const QString tagAction = QString::fromLatin1("Action");
static const QString tagSeparator = QString::fromLatin1("Separator");
static const QString tagText = QString::fromLatin1("text");
static const QString tagMerge = QString::fromLatin1("Merge");
if (equalstr(tag, tagAction)) {
// if base contains an implemented action, then we must not get
// deleted (note that the actionCollection contains both,
// "global" and "local" actions)
static const QString attrName = QString::fromLatin1("name");
if (actionCollection->action(e.attribute(attrName))) {
return false;
}
}
else if (equalstr(tag, tagSeparator)) {
// if we have a separator which has *not* the weak attribute
// set, then it must be owned by the "local" tree in which case
// we must not get deleted either
static const QString attrWeakSeparator = QString::fromLatin1("weakSeparator");
const QString weakAttr = e.attribute(attrWeakSeparator);
if (weakAttr.isEmpty() || weakAttr.toInt() != 1) {
return false;
}
}
else if (equalstr(tag, tagMerge)) {
continue;
}
// a text tag is NOT enough to spare this container
else if (equalstr(tag, tagText)) {
continue;
}
// what's left are non-empty containers! *don't* delete us in this
// case (at this position we can be *sure* that the container is
// *not* empty, as the recursive call for it was in the first loop
// which deleted the element in case the call returned "true"
else {
return false;
}
}
return true; // I'm empty, please delete me.
}
QDomElement KXMLGUIClientPrivate::findMatchingElement( const QDomElement &base, const QDomElement &additive )
{
static const QString tagAction = QString::fromLatin1( "Action" );
static const QString tagMergeLocal = QString::fromLatin1( "MergeLocal" );
static const QString attrName = QString::fromLatin1( "name" );
QDomNode n = additive.firstChild();
while ( !n.isNull() )
{
QDomElement e = n.toElement();
n = n.nextSibling(); // Advance now so that we can safely delete e -- TODO we don't, so simplify this
if (e.isNull())
continue;
const QString tag = e.tagName();
// skip all action and merge tags as we will never use them
if (equalstr(tag, tagAction)
|| equalstr(tag, tagMergeLocal)) {
continue;
}
// now see if our tags are equivalent
if (equalstr(tag, base.tagName()) &&
e.attribute(attrName) == base.attribute(attrName)) {
return e;
}
}
// nope, return a (now) null element
return QDomElement();
}
void KXMLGUIClient::setXMLGUIBuildDocument( const QDomDocument &doc )
{
d->m_buildDocument = doc;
}
QDomDocument KXMLGUIClient::xmlguiBuildDocument() const
{
return d->m_buildDocument;
}
void KXMLGUIClient::setFactory( KXMLGUIFactory *factory )
{
d->m_factory = factory;
}
KXMLGUIFactory *KXMLGUIClient::factory() const
{
return d->m_factory;
}
KXMLGUIClient *KXMLGUIClient::parentClient() const
{
return d->m_parent;
}
void KXMLGUIClient::insertChildClient( KXMLGUIClient *child )
{
if ( child->d->m_parent )
child->d->m_parent->removeChildClient( child );
d->m_children.append( child );
child->d->m_parent = this;
}
void KXMLGUIClient::removeChildClient( KXMLGUIClient *child )
{
assert( d->m_children.contains( child ) );
d->m_children.removeAll( child );
child->d->m_parent = 0;
}
/*bool KXMLGUIClient::addSuperClient( KXMLGUIClient *super )
{
if ( d->m_supers.contains( super ) )
return false;
d->m_supers.append( super );
return true;
}*/
QList<KXMLGUIClient*> KXMLGUIClient::childClients()
{
return d->m_children;
}
void KXMLGUIClient::setClientBuilder( KXMLGUIBuilder *builder )
{
d->m_builder = builder;
if ( builder )
builder->setBuilderComponentData( componentData() );
}
KXMLGUIBuilder *KXMLGUIClient::clientBuilder() const
{
return d->m_builder;
}
void KXMLGUIClient::plugActionList( const QString &name, const QList<QAction*> &actionList )
{
if ( !d->m_factory )
return;
d->m_factory->plugActionList( this, name, actionList );
}
void KXMLGUIClient::unplugActionList( const QString &name )
{
if ( !d->m_factory )
return;
d->m_factory->unplugActionList( this, name );
}
QString KXMLGUIClient::findMostRecentXMLFile( const QStringList &files, QString &doc )
{
KXmlGuiVersionHandler versionHandler(files);
doc = versionHandler.finalDocument();
return versionHandler.finalFile();
}
void KXMLGUIClient::addStateActionEnabled(const QString& state,
const QString& action)
{
StateChange stateChange = getActionsToChangeForState(state);
stateChange.actionsToEnable.append( action );
//kDebug(260) << "KXMLGUIClient::addStateActionEnabled( " << state << ", " << action << ")";
d->m_actionsStateMap.insert( state, stateChange );
}
void KXMLGUIClient::addStateActionDisabled(const QString& state,
const QString& action)
{
StateChange stateChange = getActionsToChangeForState(state);
stateChange.actionsToDisable.append( action );
//kDebug(260) << "KXMLGUIClient::addStateActionDisabled( " << state << ", " << action << ")";
d->m_actionsStateMap.insert( state, stateChange );
}
KXMLGUIClient::StateChange KXMLGUIClient::getActionsToChangeForState(const QString& state)
{
return d->m_actionsStateMap[state];
}
void KXMLGUIClient::stateChanged(const QString &newstate, KXMLGUIClient::ReverseStateChange reverse)
{
StateChange stateChange = getActionsToChangeForState(newstate);
bool setTrue = (reverse == StateNoReverse);
bool setFalse = !setTrue;
// Enable actions which need to be enabled...
//
foreach ( const QString it, stateChange.actionsToEnable ) {
QAction *action = actionCollection()->action(qPrintable(it));
if (action) action->setEnabled(setTrue);
}
// and disable actions which need to be disabled...
//
foreach ( const QString it, stateChange.actionsToDisable ) {
QAction *action = actionCollection()->action(qPrintable(it));
if (action) action->setEnabled(setFalse);
}
}
void KXMLGUIClient::beginXMLPlug( QWidget* w )
{
actionCollection()->addAssociatedWidget( w );
foreach (KXMLGUIClient* client, d->m_children)
client->beginXMLPlug( w );
}
void KXMLGUIClient::endXMLPlug()
{
}
void KXMLGUIClient::prepareXMLUnplug( QWidget * w )
{
actionCollection()->removeAssociatedWidget( w );
foreach (KXMLGUIClient* client, d->m_children)
client->prepareXMLUnplug( w );
}