/* This file is part of the KDE libraries Copyright (C) 2000 Simon Hausmann Copyright (C) 2000 Kurt Granroth Copyright 2007 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 "kxmlguiversionhandler_p.h" #include #include #include #include "kxmlguifactory.h" #include #include #include struct DocStruct { QString file; QString data; }; static QList extractToolBars(const QDomDocument& doc) { QList toolbars; QDomElement parent = doc.documentElement(); for (QDomElement e = parent.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) { if (e.tagName().compare(QLatin1String("ToolBar"), Qt::CaseInsensitive) == 0) { toolbars.append(e); } } return toolbars; } static void removeAllToolBars(QDomDocument& doc) { QDomElement parent = doc.documentElement(); const QList toolBars = extractToolBars(doc); foreach(const QDomElement& e, toolBars) { parent.removeChild(e); } } static void insertToolBars(QDomDocument& doc, const QList& toolBars) { QDomElement parent = doc.documentElement(); QDomElement menuBar = parent.namedItem("MenuBar").toElement(); QDomElement insertAfter = menuBar; if (menuBar.isNull()) insertAfter = parent.firstChildElement(); // if null, insertAfter will do an append foreach(const QDomElement& e, toolBars) { QDomNode result = parent.insertAfter(e, insertAfter); Q_ASSERT(!result.isNull()); } } // typedef QMap > ActionPropertiesMap; static ActionPropertiesMap extractActionProperties(const QDomDocument &doc) { ActionPropertiesMap properties; QDomElement actionPropElement = doc.documentElement().namedItem( "ActionProperties" ).toElement(); if ( actionPropElement.isNull() ) return properties; QDomNode n = actionPropElement.firstChild(); while(!n.isNull()) { QDomElement e = n.toElement(); n = n.nextSibling(); // Advance now so that we can safely delete e if ( e.isNull() ) continue; if ( e.tagName().compare("action", Qt::CaseInsensitive) != 0 ) continue; const QString actionName = e.attribute( "name" ); if ( actionName.isEmpty() ) continue; QMap >::Iterator propIt = properties.find( actionName ); if ( propIt == properties.end() ) propIt = properties.insert( actionName, QMap() ); const QDomNamedNodeMap attributes = e.attributes(); // the lenght() method had a TODO which is done in Katie #ifdef QT_KATIE for (int i = 0; i < attributes.length(); i++ ) #else for (uint i = 0; i < attributes.length(); i++ ) #endif { const QDomAttr attr = attributes.item( i ).toAttr(); if ( attr.isNull() ) continue; const QString name = attr.name(); if ( name == "name" || name.isEmpty() ) continue; (*propIt)[ name ] = attr.value(); } } return properties; } static void storeActionProperties( QDomDocument &doc, const ActionPropertiesMap &properties ) { QDomElement actionPropElement = doc.documentElement().namedItem( "ActionProperties" ).toElement(); if ( actionPropElement.isNull() ) { actionPropElement = doc.createElement( "ActionProperties" ); doc.documentElement().appendChild( actionPropElement ); } //Remove only those ActionProperties entries from the document, that are present //in the properties argument. In real life this means that local ActionProperties //takes precedence over global ones, if they exists (think local override of shortcuts). QDomNode actionNode = actionPropElement.firstChild(); while (!actionNode.isNull()) { if (properties.contains(actionNode.toElement().attribute("name"))) { QDomNode nextNode = actionNode.nextSibling(); actionPropElement.removeChild( actionNode ); actionNode = nextNode; } else actionNode = actionNode.nextSibling(); } ActionPropertiesMap::ConstIterator it = properties.begin(); const ActionPropertiesMap::ConstIterator end = properties.end(); for (; it != end; ++it ) { QDomElement action = doc.createElement( "Action" ); action.setAttribute( "name", it.key() ); actionPropElement.appendChild( action ); const QMap attributes = (*it); QMap::ConstIterator attrIt = attributes.begin(); const QMap::ConstIterator attrEnd = attributes.end(); for (; attrIt != attrEnd; ++attrIt ) action.setAttribute( attrIt.key(), attrIt.value() ); } } QString KXmlGuiVersionHandler::findVersionNumber( const QString &xml ) { enum { ST_START, ST_AFTER_OPEN, ST_AFTER_GUI, ST_EXPECT_VERSION, ST_VERSION_NUM} state = ST_START; const int length = xml.length(); for (int pos = 0; pos < length; pos++) { switch (state) { case ST_START: if (xml[pos] == '<') state = ST_AFTER_OPEN; break; case ST_AFTER_OPEN: { //Jump to gui.. const int guipos = xml.indexOf("gui", pos, Qt::CaseInsensitive); if (guipos == -1) return QString(); //Reject pos = guipos + 2; //Position at i, so we're moved ahead to the next character by the ++; state = ST_AFTER_GUI; break; } case ST_AFTER_GUI: state = ST_EXPECT_VERSION; break; case ST_EXPECT_VERSION: { const int verpos = xml.indexOf("version", pos, Qt::CaseInsensitive); if (verpos == -1) return QString(); //Reject pos = verpos + 7; // strlen("version") is 7 while (xml.at(pos).isSpace()) ++pos; if (xml.at(pos++) != '=') return QString(); //Reject while (xml.at(pos).isSpace()) ++pos; state = ST_VERSION_NUM; break; } case ST_VERSION_NUM: { int endpos; for (endpos = pos; endpos < length; endpos++) { const ushort ch = xml[endpos].unicode(); if (ch >= '0' && ch <= '9') continue; //Number.. if (ch == '"') //End of parameter break; else { //This shouldn't be here.. endpos = length; } } if (endpos != pos && endpos < length ) { const QString matchCandidate = xml.mid(pos, endpos - pos); //Don't include " ". return matchCandidate; } state = ST_EXPECT_VERSION; //Try to match a well-formed version.. break; } //case.. } //switch } //for return QString(); } KXmlGuiVersionHandler::KXmlGuiVersionHandler(const QStringList& files) { Q_ASSERT(!files.isEmpty()); if (files.count() == 1) { // No need to parse version numbers if there's only one file anyway m_file = files.first(); m_doc = KXMLGUIFactory::readConfigFile(m_file); return; } QList allDocuments; foreach (const QString &file, files) { DocStruct d; d.file = file; d.data = KXMLGUIFactory::readConfigFile( file ); allDocuments.append( d ); } QList::iterator best = allDocuments.end(); uint bestVersion = 0; QList::iterator docIt = allDocuments.begin(); const QList::iterator docEnd = allDocuments.end(); for (; docIt != docEnd; ++docIt ) { const QString versionStr = findVersionNumber( (*docIt).data ); if ( versionStr.isEmpty() ) { kDebug(260) << "found no version in" << (*docIt).file; continue; } bool ok = false; uint version = versionStr.toUInt( &ok ); if ( !ok ) continue; //kDebug(260) << "found version" << version << "for" << (*docIt).file; if ( version > bestVersion ) { best = docIt; //kDebug(260) << "best version is now " << version; bestVersion = version; } } if ( best != docEnd ) { if ( best != allDocuments.begin() ) { QList::iterator local = allDocuments.begin(); if ( (*local).file.startsWith(KGlobal::dirs()->localkdedir()) || (*local).file.startsWith(KGlobal::dirs()->saveLocation("appdata")) ) { // load the local document and extract the action properties QDomDocument localDocument; localDocument.setContent( (*local).data ); const ActionPropertiesMap properties = extractActionProperties(localDocument); const QList toolbars = extractToolBars(localDocument); // in case the document has a ActionProperties section // we must not delete it but copy over the global doc // to the local and insert the ActionProperties section // TODO: kedittoolbar should mark toolbars as modified so that // we don't keep old toolbars just because the user defined a shortcut if ( !properties.isEmpty() || !toolbars.isEmpty() ) { // now load the global one with the higher version number // into memory QDomDocument document; document.setContent( (*best).data ); // and store the properties in there storeActionProperties( document, properties ); if (!toolbars.isEmpty()) { // remove application toolbars removeAllToolBars(document); // add user toolbars insertToolBars(document, toolbars); } (*local).data = document.toString(); // make sure we pick up the new local doc, when we return later best = local; // write out the new version of the local document QFile f( (*local).file ); if ( f.open( QIODevice::WriteOnly ) ) { const QByteArray utf8data = (*local).data.toUtf8(); f.write( utf8data.constData(), utf8data.length() ); f.close(); } } else { // Move away the outdated local file, to speed things up next time const QString f = (*local).file; const QString backup = f + QLatin1String( ".backup" ); QFile::rename( f, backup ); } } } m_doc = (*best).data; m_file = (*best).file; } else { //kDebug(260) << "returning first one..."; m_doc = allDocuments.first().data; m_file = allDocuments.first().file; } }