diff --git a/akonadi/agentbase.cpp b/akonadi/agentbase.cpp index c960c7dc7..faf54f816 100644 --- a/akonadi/agentbase.cpp +++ b/akonadi/agentbase.cpp @@ -1,628 +1,663 @@ /* Copyright (c) 2006 Till Adam Copyright (c) 2007 Volker Krause Copyright (c) 2007 Bruno Virlet Copyright (c) 2008 Kevin Krammer 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 "agentbase.h" #include "agentbase_p.h" #include "controladaptor.h" #include "statusadaptor.h" #include "monitor_p.h" #include "xdgbasedirs_p.h" #include "session.h" #include "session_p.h" #include "changerecorder.h" #include "itemfetchjob.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; static AgentBase *sAgentBase = 0; AgentBase::Observer::Observer() { } AgentBase::Observer::~Observer() { } void AgentBase::Observer::itemAdded( const Item &item, const Collection &collection ) { kDebug() << "sAgentBase=" << (void*) sAgentBase << "this=" << (void*) this; Q_UNUSED( item ); Q_UNUSED( collection ); if ( sAgentBase != 0 ) sAgentBase->d_ptr->changeProcessed(); } void AgentBase::Observer::itemChanged( const Item &item, const QSet &partIdentifiers ) { kDebug() << "sAgentBase=" << (void*) sAgentBase << "this=" << (void*) this; Q_UNUSED( item ); Q_UNUSED( partIdentifiers ); if ( sAgentBase != 0 ) sAgentBase->d_ptr->changeProcessed(); } void AgentBase::Observer::itemRemoved( const Item &item ) { kDebug() << "sAgentBase=" << (void*) sAgentBase << "this=" << (void*) this; Q_UNUSED( item ); if ( sAgentBase != 0 ) sAgentBase->d_ptr->changeProcessed(); } void AgentBase::Observer::collectionAdded( const Akonadi::Collection &collection, const Akonadi::Collection &parent ) { kDebug() << "sAgentBase=" << (void*) sAgentBase << "this=" << (void*) this; Q_UNUSED( collection ); Q_UNUSED( parent ); if ( sAgentBase != 0 ) sAgentBase->d_ptr->changeProcessed(); } void AgentBase::Observer::collectionChanged( const Collection &collection ) { kDebug() << "sAgentBase=" << (void*) sAgentBase << "this=" << (void*) this; Q_UNUSED( collection ); if ( sAgentBase != 0 ) sAgentBase->d_ptr->changeProcessed(); } void AgentBase::Observer::collectionRemoved( const Collection &collection ) { kDebug() << "sAgentBase=" << (void*) sAgentBase << "this=" << (void*) this; Q_UNUSED( collection ); if ( sAgentBase != 0 ) sAgentBase->d_ptr->changeProcessed(); } +void AgentBase::Observer2::itemMoved( const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &dest ) +{ + kDebug() << "sAgentBase=" << (void*) sAgentBase << "this=" << (void*) this; + Q_UNUSED( item ); + Q_UNUSED( source ); + Q_UNUSED( dest ); + if ( sAgentBase != 0 ) + sAgentBase->d_ptr->changeProcessed(); +} + +void AgentBase::Observer2::collectionMoved( const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &dest ) +{ + kDebug() << "sAgentBase=" << (void*) sAgentBase << "this=" << (void*) this; + Q_UNUSED( collection ); + Q_UNUSED( source ); + Q_UNUSED( dest ); + if ( sAgentBase != 0 ) + sAgentBase->d_ptr->changeProcessed(); +} + //@cond PRIVATE AgentBasePrivate::AgentBasePrivate( AgentBase *parent ) : q_ptr( parent ), mStatusCode( AgentBase::Idle ), mProgress( 0 ), mNeedsNetwork( false ), mOnline( false ), mSettings( 0 ), mObserver( 0 ) { } AgentBasePrivate::~AgentBasePrivate() { mMonitor->setConfig( 0 ); delete mSettings; } void AgentBasePrivate::init() { Q_Q( AgentBase ); /** * Create a default session for this process. */ SessionPrivate::createDefaultSession( mId.toLatin1() ); mTracer = new org::freedesktop::Akonadi::Tracer( QLatin1String( "org.freedesktop.Akonadi" ), QLatin1String( "/tracing" ), QDBusConnection::sessionBus(), q ); new ControlAdaptor( q ); new StatusAdaptor( q ); if ( !QDBusConnection::sessionBus().registerObject( QLatin1String( "/" ), q, QDBusConnection::ExportAdaptors ) ) q->error( QString::fromLatin1( "Unable to register object at dbus: %1" ).arg( QDBusConnection::sessionBus().lastError().message() ) ); mSettings = new QSettings( QString::fromLatin1( "%1/agent_config_%2" ).arg( XdgBaseDirs::saveDir( "config", QLatin1String( "akonadi" ) ), mId ), QSettings::IniFormat ); mMonitor = new ChangeRecorder( q ); mMonitor->ignoreSession( Session::defaultSession() ); mMonitor->itemFetchScope().setCacheOnly( true ); mMonitor->setConfig( mSettings ); mOnline = mSettings->value( QLatin1String( "Agent/Online" ), true ).toBool(); mName = mSettings->value( QLatin1String( "Agent/Name" ) ).toString(); if ( mName.isEmpty() ) { mName = mSettings->value( QLatin1String( "Resource/Name" ) ).toString(); if ( !mName.isEmpty() ) { mSettings->remove( QLatin1String( "Resource/Name" ) ); mSettings->setValue( QLatin1String( "Agent/Name" ), mName ); } } connect( mMonitor, SIGNAL( itemAdded( const Akonadi::Item&, const Akonadi::Collection& ) ), SLOT( itemAdded( const Akonadi::Item&, const Akonadi::Collection& ) ) ); connect( mMonitor, SIGNAL( itemChanged( const Akonadi::Item&, const QSet& ) ), SLOT( itemChanged( const Akonadi::Item&, const QSet& ) ) ); connect( mMonitor, SIGNAL( itemMoved( const Akonadi::Item&, const Akonadi::Collection&, const Akonadi::Collection& ) ), SLOT( itemMoved( const Akonadi::Item&, const Akonadi::Collection&, const Akonadi::Collection& ) ) ); connect( mMonitor, SIGNAL( itemRemoved( const Akonadi::Item& ) ), SLOT( itemRemoved( const Akonadi::Item& ) ) ); connect( mMonitor, SIGNAL( collectionAdded( const Akonadi::Collection&, const Akonadi::Collection& ) ), SLOT( collectionAdded( const Akonadi::Collection&, const Akonadi::Collection& ) ) ); connect( mMonitor, SIGNAL( collectionChanged( const Akonadi::Collection& ) ), SLOT( collectionChanged( const Akonadi::Collection& ) ) ); connect( mMonitor, SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)), SLOT(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)) ); connect( mMonitor, SIGNAL( collectionRemoved( const Akonadi::Collection& ) ), SLOT( collectionRemoved( const Akonadi::Collection& ) ) ); connect( q, SIGNAL( status( int, const QString& ) ), q, SLOT( slotStatus( int, const QString& ) ) ); connect( q, SIGNAL( percent( int ) ), q, SLOT( slotPercent( int ) ) ); connect( q, SIGNAL( warning( const QString& ) ), q, SLOT( slotWarning( const QString& ) ) ); connect( q, SIGNAL( error( const QString& ) ), q, SLOT( slotError( const QString& ) ) ); // Use reference counting to allow agents to finish internal jobs when the // agent is stopped. KGlobal::ref(); KGlobal::setAllowQuit(true); QTimer::singleShot( 0, q, SLOT( delayedInit() ) ); } void AgentBasePrivate::delayedInit() { Q_Q( AgentBase ); if ( !QDBusConnection::sessionBus().registerService( QLatin1String( "org.freedesktop.Akonadi.Agent." ) + mId ) ) kFatal() << "Unable to register service at dbus:" << QDBusConnection::sessionBus().lastError().message(); q->setOnline( mOnline ); } void AgentBasePrivate::itemAdded( const Akonadi::Item &item, const Akonadi::Collection &collection ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; if ( mObserver != 0 ) mObserver->itemAdded( item, collection ); } void AgentBasePrivate::itemChanged( const Akonadi::Item &item, const QSet &partIdentifiers ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; if ( mObserver != 0 ) mObserver->itemChanged( item, partIdentifiers ); } void AgentBasePrivate::itemMoved( const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &dest ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; + AgentBase::Observer2 *observer2 = dynamic_cast( mObserver ); if ( mObserver ) { // inter-resource moves, requires we know which resources the source and destination are in though if ( !source.resource().isEmpty() && !dest.resource().isEmpty() ) { if ( source.resource() != dest.resource() ) { if ( source.resource() == q_ptr->identifier() ) // moved away from us mObserver->itemRemoved( item ); else if ( dest.resource() == q_ptr->identifier() ) // moved to us mObserver->itemAdded( item, dest ); - else // not for us, not sure if we should get here at all + else if ( observer2 ) + observer2->itemMoved( item, source, dest ); + else + // not for us, not sure if we should get here at all changeProcessed(); return; } } - // either incomplete information or intra-resource move - // ### we cannot just call itemRemoved here as this will already trigger changeProcessed() - // so, just itemAdded() is good enough as no resource can have implemented intra-resource moves anyway - // without using Observer2 - mObserver->itemAdded( item, dest ); - // mObserver->itemRemoved( item ); + // intra-resource move + if ( observer2 ) { + observer2->itemMoved( item, source, dest ); + } else { + // ### we cannot just call itemRemoved here as this will already trigger changeProcessed() + // so, just itemAdded() is good enough as no resource can have implemented intra-resource moves anyway + // without using Observer2 + mObserver->itemAdded( item, dest ); + // mObserver->itemRemoved( item ); + } } } void AgentBasePrivate::itemRemoved( const Akonadi::Item &item ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; if ( mObserver != 0 ) mObserver->itemRemoved( item ); } void AgentBasePrivate::collectionAdded( const Akonadi::Collection &collection, const Akonadi::Collection &parent ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; if ( mObserver != 0 ) mObserver->collectionAdded( collection, parent ); } void AgentBasePrivate::collectionChanged( const Akonadi::Collection &collection ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; if ( mObserver != 0 ) mObserver->collectionChanged( collection ); } void AgentBasePrivate::collectionMoved( const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &dest ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; + AgentBase::Observer2 *observer2 = dynamic_cast( mObserver ); if ( mObserver ) { // inter-resource moves, requires we know which resources the source and destination are in though if ( !source.resource().isEmpty() && !dest.resource().isEmpty() ) { if ( source.resource() != dest.resource() ) { if ( source.resource() == q_ptr->identifier() ) // moved away from us mObserver->collectionRemoved( collection ); else if ( dest.resource() == q_ptr->identifier() ) // moved to us mObserver->collectionAdded( collection, dest ); + else if ( observer2 ) + observer2->collectionMoved( collection, source, dest ); else // not for us, not sure if we should get here at all changeProcessed(); return; } } - // either incomplete information or intra-resource move - // ### we cannot just call collectionRemoved here as this will already trigger changeProcessed() - // so, just collectionAdded() is good enough as no resource can have implemented intra-resource moves anyway - // without using Observer2 - mObserver->collectionAdded( collection, dest ); + // intra-resource move + if ( observer2 ) { + observer2->collectionMoved( collection, source, dest ); + } else { + // ### we cannot just call collectionRemoved here as this will already trigger changeProcessed() + // so, just collectionAdded() is good enough as no resource can have implemented intra-resource moves anyway + // without using Observer2 + mObserver->collectionAdded( collection, dest ); + } } } void AgentBasePrivate::collectionRemoved( const Akonadi::Collection &collection ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; if ( mObserver != 0 ) mObserver->collectionRemoved( collection ); } void AgentBasePrivate::changeProcessed() { mMonitor->changeProcessed(); QTimer::singleShot( 0, mMonitor, SLOT( replayNext() ) ); } void AgentBasePrivate::slotStatus( int status, const QString &message ) { mStatusMessage = message; mStatusCode = 0; switch ( status ) { case AgentBase::Idle: if ( mStatusMessage.isEmpty() ) mStatusMessage = defaultReadyMessage(); mStatusCode = 0; break; case AgentBase::Running: if ( mStatusMessage.isEmpty() ) mStatusMessage = defaultSyncingMessage(); mStatusCode = 1; break; case AgentBase::Broken: if ( mStatusMessage.isEmpty() ) mStatusMessage = defaultErrorMessage(); mStatusCode = 2; break; default: Q_ASSERT( !"Unknown status passed" ); break; } } void AgentBasePrivate::slotPercent( int progress ) { mProgress = progress; } void AgentBasePrivate::slotWarning( const QString& message ) { mTracer->warning( QString::fromLatin1( "AgentBase(%1)" ).arg( mId ), message ); } void AgentBasePrivate::slotError( const QString& message ) { mTracer->error( QString::fromLatin1( "AgentBase(%1)" ).arg( mId ), message ); } void AgentBasePrivate::slotNetworkStatusChange( Solid::Networking::Status stat ) { Q_Q( AgentBase ); q->setOnline( stat == Solid::Networking::Connected ); } AgentBase::AgentBase( const QString & id ) : d_ptr( new AgentBasePrivate( this ) ) { sAgentBase = this; d_ptr->mId = id; d_ptr->init(); if ( KApplication::kApplication() ) KApplication::kApplication()->disableSessionManagement(); } AgentBase::AgentBase( AgentBasePrivate* d, const QString &id ) : d_ptr( d ) { sAgentBase = this; d_ptr->mId = id; d_ptr->init(); } AgentBase::~AgentBase() { delete d_ptr; } QString AgentBase::parseArguments( int argc, char **argv ) { QString identifier; if ( argc < 3 ) { kDebug() << "Not enough arguments passed..."; exit( 1 ); } for ( int i = 1; i < argc - 1; ++i ) { if ( QLatin1String( argv[ i ] ) == QLatin1String( "--identifier" ) ) identifier = QLatin1String( argv[ i + 1 ] ); } if ( identifier.isEmpty() ) { kDebug() << "Identifier argument missing"; exit( 1 ); } QByteArray catalog; char *p = strrchr( argv[0], '/' ); if ( p ) catalog = QByteArray( p + 1 ); else catalog = QByteArray( argv[0] ); KCmdLineArgs::init( argc, argv, identifier.toLatin1(), catalog, ki18n("Akonadi Agent"),"0.1" , ki18n("Akonadi Agent") ); KCmdLineOptions options; options.add("identifier ", ki18n("Agent identifier")); KCmdLineArgs::addCmdLineOptions( options ); return identifier; } // @endcond int AgentBase::init( AgentBase *r ) { QApplication::setQuitOnLastWindowClosed( false ); KGlobal::locale()->insertCatalog( QLatin1String("libakonadi") ); int rv = kapp->exec(); delete r; return rv; } int AgentBase::status() const { Q_D( const AgentBase ); return d->mStatusCode; } QString AgentBase::statusMessage() const { Q_D( const AgentBase ); return d->mStatusMessage; } int AgentBase::progress() const { Q_D( const AgentBase ); return d->mProgress; } QString AgentBase::progressMessage() const { Q_D( const AgentBase ); return d->mProgressMessage; } bool AgentBase::isOnline() const { Q_D( const AgentBase ); return d->mOnline; } void AgentBase::setNeedsNetwork( bool needsNetwork ) { Q_D( AgentBase ); d->mNeedsNetwork = needsNetwork; if ( d->mNeedsNetwork ) { connect( Solid::Networking::notifier() , SIGNAL( statusChanged( Solid::Networking::Status ) ) , this, SLOT( slotNetworkStatusChange( Solid::Networking::Status ) ) ); } else { disconnect( Solid::Networking::notifier(), 0, 0, 0 ); setOnline( true ); } } void AgentBase::setOnline( bool state ) { Q_D( AgentBase ); d->mOnline = state; d->mSettings->setValue( QLatin1String( "Agent/Online" ), state ); doSetOnline( state ); emit onlineChanged( state ); } void AgentBase::doSetOnline( bool online ) { Q_UNUSED( online ); } void AgentBase::configure( WId windowId ) { Q_UNUSED( windowId ); } #ifdef Q_OS_WIN //krazy:exclude=cpp void AgentBase::configure( qlonglong windowId ) { configure( reinterpret_cast( windowId ) ); } #endif WId AgentBase::winIdForDialogs() const { bool registered = QDBusConnection::sessionBus().interface()->isServiceRegistered( QLatin1String("org.freedesktop.akonaditray") ); if ( !registered ) return 0; QDBusInterface dbus( QLatin1String("org.freedesktop.akonaditray"), QLatin1String("/Actions"), QLatin1String("org.freedesktop.Akonadi.Tray") ); QDBusMessage reply = dbus.call( QLatin1String("getWinId") ); if ( reply.type() == QDBusMessage::ErrorMessage ) return 0; WId winid = (WId)reply.arguments().at( 0 ).toLongLong(); return winid; } void AgentBase::quit() { Q_D( AgentBase ); aboutToQuit(); if ( d->mSettings ) { d->mMonitor->setConfig( 0 ); d->mSettings->sync(); } KGlobal::deref(); } void AgentBase::aboutToQuit() { } void AgentBase::cleanup() { Q_D( AgentBase ); // prevent the monitor from picking up deletion signals for our own data if we are a resource // and thus avoid that we kill our own data as last act before our own death d->mMonitor->blockSignals( true ); aboutToQuit(); const QString fileName = d->mSettings->fileName(); /* * First destroy the settings object... */ d->mMonitor->setConfig( 0 ); delete d->mSettings; d->mSettings = 0; /* * ... then remove the file from hd. */ QFile::remove( fileName ); /* * ... and also remove the agent configuration file if there is one. */ QString configFile = KStandardDirs::locateLocal( "config", KGlobal::config()->name() ); QFile::remove( configFile ); KGlobal::deref(); } void AgentBase::registerObserver( Observer *observer ) { kDebug() << "observer=" << (void*) observer << "this=" << (void*) this; d_ptr->mObserver = observer; } QString AgentBase::identifier() const { return d_ptr->mId; } void AgentBase::setAgentName( const QString &name ) { Q_D( AgentBase ); if ( name == d->mName ) return; // TODO: rename collection d->mName = name; if ( d->mName.isEmpty() || d->mName == d->mId ) { d->mSettings->remove( QLatin1String( "Resource/Name" ) ); d->mSettings->remove( QLatin1String( "Agent/Name" ) ); } else d->mSettings->setValue( QLatin1String( "Agent/Name" ), d->mName ); d->mSettings->sync(); emit agentNameChanged( d->mName ); } QString AgentBase::agentName() const { Q_D( const AgentBase ); if ( d->mName.isEmpty() ) return d->mId; else return d->mName; } void AgentBase::changeProcessed() { Q_D( AgentBase ); d->changeProcessed(); } ChangeRecorder * AgentBase::changeRecorder() const { return d_ptr->mMonitor; } void AgentBase::abort() { emit abortRequested(); } void AgentBase::reconfigure() { emit reloadConfiguration(); } #include "agentbase.moc" #include "agentbase_p.moc" diff --git a/akonadi/agentbase.h b/akonadi/agentbase.h index fb6a94859..6914a4044 100644 --- a/akonadi/agentbase.h +++ b/akonadi/agentbase.h @@ -1,509 +1,547 @@ /* This file is part of akonadiresources. Copyright (c) 2006 Till Adam Copyright (c) 2007 Volker Krause Copyright (c) 2008 Kevin Krammer 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. */ #ifndef AKONADI_AGENTBASE_H #define AKONADI_AGENTBASE_H #include "akonadi_export.h" #include #include class ControlAdaptor; class StatusAdaptor; namespace Akonadi { class AgentBasePrivate; class ChangeRecorder; class Collection; class Item; class Session; /** * @short The base class for all Akonadi agents and resources. * * This class is a base class for all Akonadi agents, which covers the real * agent processes and all resources. * * It provides: * - lifetime management * - change monitoring and recording * - configuration interface * - problem reporting * * @author Till Adam , Volker Krause */ class AKONADI_EXPORT AgentBase : public QObject, protected QDBusContext { Q_OBJECT public: /** * @short The interface for reacting on monitored or replayed changes. * * The Observer provides an interface to react on monitored or replayed changes. * * Since the this base class does only tell the change recorder that the change * has been processed, an AgentBase subclass which wants to actually process * the change needs to subclass Observer and reimplement the methods it is * interested in. * * Such an agent specific Observer implementation can either be done * stand-alone, i.e. as a separate object, or by inheriting both AgentBase * and AgentBase::Observer. * * The observer implementation then has registered with the agent, so it * can forward the incoming changes to the observer. * * @note In the multiple inheritance approach the init() method automatically * registers itself as the observer. * * Example for stand-alone observer: * @code * class ExampleAgent : public AgentBase * { * public: * ExampleAgent( const QString &id ); * * ~ExampleAgent(); * * private: * AgentBase::Observer *mObserver; * }; * * class ExampleObserver : public AgentBase::Observer * { * protected: * void itemChanged( const Item &item ); * }; * * ExampleAgent::ExampleAgent( const QString &id ) * : AgentBase( id ), mObserver( 0 ) * { * mObserver = new ExampleObserver(); * registerObserver( mObserver ); * } * * ExampleAgent::~ExampleAgent() * { * delete mObserver; * } * * void ExampleObserver::itemChanged( const Item &item ) * { * // do something with item * kDebug() << "Item id=" << item.id(); * * // let base implementation tell the change recorder that we * // have processed the change * AgentBase::Observer::itemChanged( item ); * } * @endcode * * Example for observer through multiple inheritance: * @code * class ExampleAgent : public AgentBase, public AgentBase::Observer * { * public: * ExampleAgent( const QString &id ); * * protected: * void itemChanged( const Item &item ); * }; * * ExampleAgent::ExampleAgent( const QString &id ) * : AgentBase( id ) * { * // no need to create or register observer since * // we are the observer and registration happens automatically * // in init() * } * * void ExampleAgent::itemChanged( const Item &item ) * { * // do something with item * kDebug() << "Item id=" << item.id(); * * // let base implementation tell the change recorder that we * // have processed the change * AgentBase::Observer::itemChanged( item ); * } * @endcode * * @author Kevin Krammer + * + * @deprecated Use Observer2 instead */ class AKONADI_EXPORT Observer // krazy:exclude=dpointer { public: /** * Creates an observer instance. */ Observer(); /** * Destroys the observer instance. */ virtual ~Observer(); /** * Reimplement to handle adding of new items. * @param item The newly added item. * @param collection The collection @p item got added to. */ virtual void itemAdded( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * Reimplement to handle changes to existing items. * @param item The changed item. * @param partIdentifiers The identifiers of the item parts that has been changed. */ virtual void itemChanged( const Akonadi::Item &item, const QSet &partIdentifiers ); /** * Reimplement to handle deletion of items. * @param item The deleted item. */ virtual void itemRemoved( const Akonadi::Item &item ); /** * Reimplement to handle adding of new collections. * @param collection The newly added collection. * @param parent The parent collection. */ virtual void collectionAdded( const Akonadi::Collection &collection, const Akonadi::Collection &parent ); /** * Reimplement to handle changes to existing collections. * @param collection The changed collection. */ virtual void collectionChanged( const Akonadi::Collection &collection ); /** * Reimplement to handle deletion of collections. * @param collection The deleted collection. */ virtual void collectionRemoved( const Akonadi::Collection &collection ); }; + /** + * BC extension of Observer with support for monitoring item and collection moves. + * Use this one instead of Observer. + * + * @since 4.4 + */ + class AKONADI_EXPORT Observer2 : public Observer // krazy:exclude=dpointer + { + public: + /** + * Reimplement to handle item moves. + * When using this class in combination with Akonadi::ResourceBase, inter-resource + * moves are handled internally already and the corresponding add or delete method + * is called instead. + * + * @param item The moved item. + * @param collectionSource The collection the item has been moved from. + * @param collectionDestination The collection the item has been moved to. + */ + virtual void itemMoved( const Akonadi::Item &item, const Akonadi::Collection &collectionSource, + const Akonadi::Collection &collectionDestination ); + + /** + * Reimplement to handle collection moves. + * When using this class in combination with Akonadi::ResourceBase, inter-resource + * moves are handled internally already and the corresponding add or delete method + * is called instead. + * + * @param collection The moved collection. + * @param source The previous parent collection. + * @param distination The new parent collection. + */ + virtual void collectionMoved( const Akonadi::Collection &collection, const Akonadi::Collection &source, + const Akonadi::Collection &destination ); + }; + /** * This enum describes the different states the * agent can be in. */ enum Status { Idle = 0, ///< The agent does currently nothing. Running, ///< The agent is working on something. Broken ///< The agent encountered an error state. }; /** * Use this method in the main function of your agent * application to initialize your agent subclass. * This method also takes care of creating a KApplication * object and parsing command line arguments. * * @note In case the given class is also derived from AgentBase::Observer * it gets registered as its own observer (see AgentBase::Observer), e.g. * agentInstance->registerObserver( agentInstance ); * * @code * * class MyAgent : public AgentBase * { * ... * }; * * AKONADI_AGENT_MAIN( MyAgent ) * * @endcode */ template static int init( int argc, char **argv ) { const QString id = parseArguments( argc, argv ); KApplication app; T* r = new T( id ); // check if T also inherits AgentBase::Observer and // if it does, automatically register it on itself Observer *observer = dynamic_cast( r ); if ( observer != 0 ) r->registerObserver( observer ); return init( r ); } /** * This method returns the current status code of the agent. * * The following return values are possible: * * - 0 - Idle * - 1 - Running * - 2 - Broken */ virtual int status() const; /** * This method returns an i18n'ed description of the current status code. */ virtual QString statusMessage() const; /** * This method returns the current progress of the agent in percentage. */ virtual int progress() const; /** * This method returns an i18n'ed description of the current progress. */ virtual QString progressMessage() const; /** * This method is called whenever the agent shall show its configuration dialog * to the user. It will be automatically called when the agent is started for * the first time. * @param windowId The parent window id. */ virtual void configure( WId windowId ); /** * This method returns the windows id, which should be used for dialogs. */ WId winIdForDialogs() const; #ifdef Q_OS_WIN /** * Overload of @ref configure needed because WId cannot be automatically casted * to qlonglong on Windows. */ void configure( qlonglong windowId ); #endif /** * Returns the instance identifier of this agent. */ QString identifier() const; /** * This method is called when the agent is removed from * the system, so it can do some cleanup stuff. */ virtual void cleanup(); /** * Registers the given observer for reacting on monitored or recorded changes. * * @param observer The change handler to register. No ownership transfer, i.e. * the caller stays owner of the pointer and can reset * the registration by calling this method with @c 0 */ void registerObserver( Observer *observer ); /** * This method is used to set the name of the agent. * * @since 4.3 */ //FIXME_API: make sure location is renamed to this by agentbase void setAgentName( const QString &name ); /** * Returns the name of the agent. * * @since 4.3 */ QString agentName() const; Q_SIGNALS: /** * This signal is emitted whenever the name of the agent has changed. * * @param name The new name of the agent. * * @since 4.3 */ void agentNameChanged( const QString &name ); /** * This signal should be emitted whenever the status of the agent has been changed. * @param status The new Status code. * @param message A i18n'ed description of the new status. */ void status( int status, const QString &message = QString() ); /** * This signal should be emitted whenever the progress of an action in the agent * (e.g. data transfer, connection establishment to remote server etc.) has changed. * * @param progress The progress of the action in percent. */ void percent( int progress ); /** * This signal shall be used to report warnings. * * @param message The i18n'ed warning message. */ void warning( const QString& message ); /** * This signal shall be used to report errors. * * @param message The i18n'ed error message. */ void error( const QString& message ); /** * Emitted when another application has remotely asked the agent to abort * its current operation. * Connect to this signal if your agent supports abortion. After aborting * and cleaning up, agents should return to Idle status. * * @since 4.4 */ void abortRequested(); /** * Emitted if another application has changed the agent's configuration remotely * and called AgentInstance::reconfigure(). * * @since 4.2 */ void reloadConfiguration(); /** * Emitted when the online state changed. * @param state The online state. * @since 4.2 */ void onlineChanged( bool b ); protected: /** * Creates an agent base. * * @param id The instance id of the agent. */ AgentBase( const QString & id ); /** * Destroys the agent base. */ ~AgentBase(); /** * This method is called whenever the agent application is about to * quit. * * Reimplement this method to do session cleanup (e.g. disconnecting * from groupware server). */ virtual void aboutToQuit(); /** * Returns the Akonadi::ChangeRecorder object used for monitoring. * Use this to configure which parts you want to monitor. */ ChangeRecorder* changeRecorder() const; /** * Marks the current change as processes and replays the next change if change * recording is enabled (noop otherwise). This method is called * from the default implementation of the change notification slots. While not * required when not using change recording, it is nevertheless recommended * to call this method when done with processing a change notification. */ void changeProcessed(); /** * Returns whether the agent is currently online. */ bool isOnline() const; /** * Sets whether the agent needs network or not. * * @since 4.2 * @todo use this in combination with Solid::Networking::Notifier to change * the onLine status of the agent. */ void setNeedsNetwork( bool needsNetwork ); /** * Sets whether the agent shall be online or not. */ void setOnline( bool state ); protected: //@cond PRIVATE AgentBasePrivate *d_ptr; explicit AgentBase( AgentBasePrivate* d, const QString &id ); //@endcond /** * This method is called whenever the @p online status has changed. * Reimplement this method to react on online status changes. */ virtual void doSetOnline( bool online ); private: //@cond PRIVATE static QString parseArguments( int, char** ); static int init( AgentBase *r ); // D-Bus interface stuff void abort(); void reconfigure(); void quit(); // dbus agent interface friend class ::StatusAdaptor; friend class ::ControlAdaptor; Q_DECLARE_PRIVATE( AgentBase ) Q_PRIVATE_SLOT( d_func(), void delayedInit() ) Q_PRIVATE_SLOT( d_func(), void slotStatus( int, const QString& ) ) Q_PRIVATE_SLOT( d_func(), void slotPercent( int ) ) Q_PRIVATE_SLOT( d_func(), void slotWarning( const QString& ) ) Q_PRIVATE_SLOT( d_func(), void slotError( const QString& ) ) Q_PRIVATE_SLOT( d_func(), void slotNetworkStatusChange( Solid::Networking::Status ) ) //@endcond }; } #ifndef AKONADI_AGENT_MAIN /** * Convenience Macro for the most common main() function for Akonadi agents. */ #define AKONADI_AGENT_MAIN( agentClass ) \ int main( int argc, char **argv ) \ { \ return Akonadi::AgentBase::init( argc, argv ); \ } #endif #endif diff --git a/akonadi/entitycache_p.h b/akonadi/entitycache_p.h index eda286102..61772bd79 100644 --- a/akonadi/entitycache_p.h +++ b/akonadi/entitycache_p.h @@ -1,231 +1,232 @@ /* Copyright (c) 2009 Volker Krause 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. */ #ifndef AKONADI_ENTITYCACHE_P_H #define AKONADI_ENTITYCACHE_P_H #include #include #include #include #include #include #include #include #include #include class KJob; namespace Akonadi { /** @internal QObject part of EntityCache. */ class EntityCacheBase : public QObject { Q_OBJECT public: explicit EntityCacheBase (QObject * parent = 0); signals: void dataAvailable(); private slots: virtual void fetchResult( KJob* job ) = 0; }; template struct EntityCacheNode { EntityCacheNode() : pending( false ), invalid( false ) {} EntityCacheNode( typename T::Id id ) : entity( T( id ) ), pending( true ), invalid( false ) {} T entity; bool pending; bool invalid; }; /** * @internal * A in-memory FIFO cache for a small amount of Entity objects. */ template class EntityCache : public EntityCacheBase { public: explicit EntityCache( int maxCapacity, QObject *parent = 0 ) : EntityCacheBase( parent ), mCapacity( maxCapacity ) {} ~EntityCache() { qDeleteAll( mCache ); } /** Object is available in the cache and can be retrieved. */ bool isCached( typename T::Id id ) const { EntityCacheNode* node = cacheNodeForId( id ); return node && !node->pending; } /** Object has been requested but is not yet loaded into the cache or is already available. */ bool isRequested( typename T::Id id ) const { return cacheNodeForId( id ); } /** Returns the cached object if available, an empty instance otherwise. */ T retrieve( typename T::Id id ) const { EntityCacheNode* node = cacheNodeForId( id ); if ( node && !node->pending && !node->invalid ) return node->entity; return T(); } /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */ void invalidate( typename T::Id id ) { EntityCacheNode* node = cacheNodeForId( id ); if ( node ) node->invalid = true; } /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */ void update( typename T::Id id, const FetchScope &scope ) { EntityCacheNode* node = cacheNodeForId( id ); - if ( node ) + if ( node ) { mCache.removeAll( node ); - if ( !node || node->pending ) - request( id, scope ); - delete node; + if ( node->pending ) + request( id, scope ); + delete node; + } } /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */ bool ensureCached( typename T::Id id, const FetchScope &scope ) { EntityCacheNode* node = cacheNodeForId( id ); if ( !node ) { request( id, scope ); return false; } return !node->pending; } /** Asks the cache to retrieve @p id. @p request is used as a token to indicate which request has been finished in the dataAvailable() signal. */ void request( typename T::Id id, const FetchScope &scope ) { Q_ASSERT( !isRequested( id ) ); shrinkCache(); EntityCacheNode *node = new EntityCacheNode( id ); FetchJob* job = createFetchJob( id ); job->setFetchScope( scope ); job->setProperty( "EntityCacheNode", QVariant::fromValue( id ) ); connect( job, SIGNAL(result(KJob*)), SLOT(fetchResult(KJob*)) ); mCache.enqueue( node ); } private: EntityCacheNode* cacheNodeForId( typename T::Id id ) const { for ( typename QQueue*>::const_iterator it = mCache.constBegin(), endIt = mCache.constEnd(); it != endIt; ++it ) { if ( (*it)->entity.id() == id ) return *it; } return 0; } void fetchResult( KJob* job ) { typename T::Id id = job->property( "EntityCacheNode" ).template value(); EntityCacheNode *node = cacheNodeForId( id ); if ( !node ) return; // got replaced in the meantime node->pending = false; extractResult( node, job ); if ( node->entity.id() != id ) { // make sure we find this node again if something went wrong here... kWarning() << "Something went very wrong..."; node->entity.setId( id ); node->invalid = true; } emit dataAvailable(); } void extractResult( EntityCacheNode* node, KJob* job ) const; inline FetchJob* createFetchJob( typename T::Id id ) { return new FetchJob( T( id ), this ); } /** Tries to reduce the cache size until at least one more object fits in. */ void shrinkCache() { while ( mCache.size() >= mCapacity && !mCache.first()->pending ) delete mCache.dequeue(); } private: QQueue*> mCache; int mCapacity; }; template<> inline void EntityCache::extractResult( EntityCacheNode* node, KJob *job ) const { CollectionFetchJob* fetch = qobject_cast( job ); Q_ASSERT( fetch ); if ( fetch->collections().isEmpty() ) node->entity = Collection(); else node->entity = fetch->collections().first(); } template<> inline void EntityCache::extractResult( EntityCacheNode* node, KJob *job ) const { ItemFetchJob* fetch = qobject_cast( job ); Q_ASSERT( fetch ); if ( fetch->items().isEmpty() ) node->entity = Item(); else node->entity = fetch->items().first(); } template<> inline CollectionFetchJob* EntityCache::createFetchJob( Collection::Id id ) { return new CollectionFetchJob( Collection( id ), CollectionFetchJob::Base, this ); } typedef EntityCache CollectionCache; typedef EntityCache ItemCache; } #endif diff --git a/akonadi/entitytreemodel.cpp b/akonadi/entitytreemodel.cpp index 65c32a701..2719356d9 100644 --- a/akonadi/entitytreemodel.cpp +++ b/akonadi/entitytreemodel.cpp @@ -1,947 +1,948 @@ /* Copyright (c) 2008 Stephen Kelly 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 "entitytreemodel.h" #include "entitytreemodel_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "collectionutils_p.h" #include "kdebug.h" using namespace Akonadi; EntityTreeModel::EntityTreeModel( Session *session, Monitor *monitor, QObject *parent ) : QAbstractItemModel( parent ), d_ptr( new EntityTreeModelPrivate( this ) ) { Q_D( EntityTreeModel ); d->m_monitor = monitor; d->m_session = session; d->m_includeStatistics = true; d->m_monitor->fetchCollectionStatistics( true ); d->m_mimeChecker.setWantedMimeTypes( d->m_monitor->mimeTypesMonitored() ); connect( monitor, SIGNAL( mimeTypeMonitored( const QString&, bool ) ), SLOT( monitoredMimeTypeChanged( const QString&, bool ) ) ); // monitor collection changes connect( monitor, SIGNAL( collectionChanged( const Akonadi::Collection& ) ), SLOT( monitoredCollectionChanged( const Akonadi::Collection& ) ) ); connect( monitor, SIGNAL( collectionAdded( const Akonadi::Collection&, const Akonadi::Collection& ) ), SLOT( monitoredCollectionAdded( const Akonadi::Collection&, const Akonadi::Collection& ) ) ); connect( monitor, SIGNAL( collectionRemoved( const Akonadi::Collection& ) ), SLOT( monitoredCollectionRemoved( const Akonadi::Collection&) ) ); // connect( monitor, // SIGNAL( collectionMoved( const Akonadi::Collection &, const Akonadi::Collection &, const Akonadi::Collection & ) ), // SLOT( monitoredCollectionMoved( const Akonadi::Collection &, const Akonadi::Collection &, const Akonadi::Collection & ) ) ); //TODO: Figure out if the monitor emits these signals even without an item fetch scope. // Wrap them in an if() if so. // Don't want to be adding items to a model if NoItemPopulation is set. // If LazyPopulation is set, then we'll have to add items to collections which // have already been lazily populated. // Monitor item changes. connect( monitor, SIGNAL( itemAdded( const Akonadi::Item&, const Akonadi::Collection& ) ), SLOT( monitoredItemAdded( const Akonadi::Item&, const Akonadi::Collection& ) ) ); connect( monitor, SIGNAL( itemChanged( const Akonadi::Item&, const QSet& ) ), SLOT( monitoredItemChanged( const Akonadi::Item&, const QSet& ) ) ); connect( monitor, SIGNAL( itemRemoved( const Akonadi::Item& ) ), SLOT( monitoredItemRemoved( const Akonadi::Item& ) ) ); //connect( monitor, SIGNAL( itemMoved( const Akonadi::Item, const Akonadi::Collection, const Akonadi::Collection ) ), // SLOT( monitoredItemMoved( const Akonadi::Item, const Akonadi::Collection, const Akonadi::Collection ) ) ); connect( monitor, SIGNAL( collectionStatisticsChanged( Akonadi::Collection::Id, const Akonadi::CollectionStatistics& ) ), SLOT(monitoredCollectionStatisticsChanged( Akonadi::Collection::Id, const Akonadi::CollectionStatistics& ) ) ); connect( monitor, SIGNAL( itemLinked( const Akonadi::Item&, const Akonadi::Collection& )), SLOT( monitoredItemLinked( const Akonadi::Item&, const Akonadi::Collection& ))); connect( monitor, SIGNAL( itemUnlinked( const Akonadi::Item&, const Akonadi::Collection& )), SLOT( monitoredItemUnlinked( const Akonadi::Item&, const Akonadi::Collection& ))); // connect( q, SIGNAL( modelReset() ), q, SLOT( slotModelReset() ) ); d->m_rootCollection = Collection::root(); d->m_rootCollectionDisplayName = QLatin1String( "[*]" ); // Initializes the model cleanly. clearAndReset(); } EntityTreeModel::~EntityTreeModel() { Q_D( EntityTreeModel ); foreach( QList list, d->m_childEntities ) { qDeleteAll(list); list.clear(); } delete d_ptr; } void EntityTreeModel::clearAndReset() { Q_D( EntityTreeModel ); d->m_collections.clear(); d->m_items.clear(); d->m_childEntities.clear(); reset(); QTimer::singleShot( 0, this, SLOT( startFirstListJob() ) ); } Collection EntityTreeModel::collectionForId( Collection::Id id ) const { Q_D( const EntityTreeModel ); return d->m_collections.value( id ); } Item EntityTreeModel::itemForId( Item::Id id ) const { Q_D( const EntityTreeModel ); return d->m_items.value( id ); } int EntityTreeModel::columnCount( const QModelIndex & parent ) const { // TODO: Statistics? if ( parent.isValid() && parent.column() != 0 ) return 0; - return 1; + + return qMax( getColumnCount( CollectionTreeHeaders ), getColumnCount( ItemListHeaders ) ); } QVariant EntityTreeModel::getData( const Item &item, int column, int role ) const { if ( column == 0 ) { switch ( role ) { case Qt::DisplayRole: case Qt::EditRole: if ( item.hasAttribute() && !item.attribute()->displayName().isEmpty() ) { return item.attribute()->displayName(); } else { return item.remoteId(); } break; case Qt::DecorationRole: if ( item.hasAttribute() && !item.attribute()->iconName().isEmpty() ) return item.attribute()->icon(); break; case MimeTypeRole: return item.mimeType(); break; case RemoteIdRole: return item.remoteId(); break; case ItemRole: return QVariant::fromValue( item ); break; case ItemIdRole: return item.id(); break; default: break; } } return QVariant(); } QVariant EntityTreeModel::getData( const Collection &collection, int column, int role ) const { Q_D(const EntityTreeModel); if ( column > 0 ) return QString(); if ( collection == Collection::root() ) { // Only display the root collection. It may not be edited. if ( role == Qt::DisplayRole ) return d->m_rootCollectionDisplayName; if ( role == Qt::EditRole ) return QVariant(); } if ( column == 0 && (role == Qt::DisplayRole || role == Qt::EditRole) ) { if ( collection.hasAttribute() && !collection.attribute()->displayName().isEmpty() ) return collection.attribute()->displayName(); return collection.name(); } switch ( role ) { case Qt::DisplayRole: case Qt::EditRole: if ( column == 0 ) { if ( collection.hasAttribute() && !collection.attribute()->displayName().isEmpty() ) { return collection.attribute()->displayName(); } return collection.name(); } break; case Qt::DecorationRole: if ( collection.hasAttribute() && !collection.attribute()->iconName().isEmpty() ) { return collection.attribute()->icon(); } return KIcon( CollectionUtils::defaultIconName( collection ) ); break; case MimeTypeRole: return collection.mimeType(); break; case RemoteIdRole: return collection.remoteId(); break; case CollectionIdRole: return collection.id(); break; case CollectionRole: { return QVariant::fromValue( collection ); break; } default: break; } return QVariant(); } QVariant EntityTreeModel::data( const QModelIndex & index, int role ) const { const int headerSet = (role / TerminalUserRole); role %= TerminalUserRole; if ( !index.isValid() ) { if (ColumnCountRole != role) return QVariant(); return getColumnCount(headerSet); } if (ColumnCountRole == role) return getColumnCount(headerSet); Q_D( const EntityTreeModel ); const Node *node = reinterpret_cast( index.internalPointer() ); if (ParentCollectionRole == role) { const Collection parentCollection = d->m_collections.value( node->parent ); Q_ASSERT(parentCollection.isValid()); return QVariant::fromValue(parentCollection); } if ( Node::Collection == node->type ) { const Collection collection = d->m_collections.value( node->id ); if ( !collection.isValid() ) return QVariant(); return getData( collection, index.column(), role ); } else if ( Node::Item == node->type ) { const Item item = d->m_items.value( node->id ); if ( !item.isValid() ) return QVariant(); return getData( item, index.column(), role ); } return QVariant(); } Qt::ItemFlags EntityTreeModel::flags( const QModelIndex & index ) const { Q_D( const EntityTreeModel ); // Pass modeltest. // http://labs.trolltech.com/forums/topic/79 if ( !index.isValid() ) return 0; Qt::ItemFlags flags = QAbstractItemModel::flags( index ); // Only show and enable items in columns other than 0. if ( index.column() != 0 ) return flags; const Node *node = reinterpret_cast(index.internalPointer()); if ( Node::Collection == node->type ) { const Collection collection = d->m_collections.value( node->id ); if ( collection.isValid() ) { if ( collection == Collection::root() ) { // Selectable and displayable only. return flags; } const int rights = collection.rights(); if ( rights & Collection::CanChangeCollection ) { flags |= Qt::ItemIsEditable; // Changing the collection includes changing the metadata (child entityordering). // Need to allow this by drag and drop. flags |= Qt::ItemIsDropEnabled; } if ( rights & Collection::CanDeleteCollection ) { // If this collection is moved, it will need to be deleted flags |= Qt::ItemIsDragEnabled; } if ( rights & ( Collection::CanCreateCollection | Collection::CanCreateItem ) ) { // Can we drop new collections and items into this collection? flags |= Qt::ItemIsDropEnabled; } } } else if ( Node::Item == node->type ) { // Rights come from the parent collection. const Node *parentNode = reinterpret_cast( index.parent().internalPointer() ); // TODO: Is this right for the root collection? I think so, but only by chance. // But will it work if m_rootCollection is different from Collection::root? // Should probably rely on index.parent().isValid() for that. const Collection parentCollection = d->m_collections.value( parentNode->id ); if ( parentCollection.isValid() ) { const int rights = parentCollection.rights(); // Can't drop onto items. if ( rights & Collection::CanChangeItem ) { flags = flags | Qt::ItemIsEditable; } if ( rights & Collection::CanDeleteItem ) { // If this item is moved, it will need to be deleted from its parent. flags = flags | Qt::ItemIsDragEnabled; } } } return flags; } Qt::DropActions EntityTreeModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList EntityTreeModel::mimeTypes() const { // TODO: Should this return the mimetypes that the items provide? Allow dragging a contact from here for example. return QStringList() << QLatin1String( "text/uri-list" ); } bool EntityTreeModel::dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ) { Q_D( EntityTreeModel ); // TODO Use action and collection rights and return false if necessary // if row and column are -1, then the drop was on parent directly. // data should then be appended on the end of the items of the collections as appropriate. // That will mean begin insert rows etc. // Otherwise it was a sibling of the row^th item of parent. // That will need to be handled by a proxy model. This one can't handle ordering. // if parent is invalid the drop occurred somewhere on the view that is no model, and corresponds to the root. kDebug() << "ismove" << ( action == Qt::MoveAction ); if ( action == Qt::IgnoreAction ) return true; // Shouldn't do this. Need to be able to drop vcards for example. // if (!data->hasFormat("text/uri-list")) // return false; // TODO This is probably wrong and unnecessary. if ( column > 0 ) return false; const Node *node = reinterpret_cast( parent.internalId() ); if ( Node::Item == node->type ) { // Can't drop data onto an item, although we can drop data between items. return false; // TODO: Maybe if it's a drop on an item I should drop below the item instead? // Find out what others do. } if ( Node::Collection == node->type ) { const Collection destCollection = d->m_collections.value( node->id ); // Applications can't create new collections in root. Only resources can. if ( destCollection == Collection::root() ) return false; if ( data->hasFormat( QLatin1String( "text/uri-list" ) ) ) { MimeTypeChecker mimeChecker; mimeChecker.setWantedMimeTypes( destCollection.contentMimeTypes() ); TransactionSequence *transaction = new TransactionSequence( d->m_session ); const KUrl::List urls = KUrl::List::fromMimeData( data ); foreach ( const KUrl &url, urls ) { const Collection collection = d->m_collections.value( Collection::fromUrl( url ).id() ); if ( collection.isValid() ) { if ( !mimeChecker.isWantedCollection( collection ) ) return false; if ( Qt::MoveAction == action ) { // new CollectionMoveJob(col, destCol, transaction); } else if ( Qt::CopyAction == action ) { CollectionCopyJob *collectionCopyJob = new CollectionCopyJob( collection, destCollection, transaction ); connect( collectionCopyJob, SIGNAL( result( KJob* ) ), SLOT( copyJobDone( KJob* ) ) ); } } else { const Item item = d->m_items.value( Item::fromUrl( url ).id() ); if ( item.isValid() ) { if ( Qt::MoveAction == action ) { ItemMoveJob *itemMoveJob = new ItemMoveJob( item, destCollection, transaction ); connect( itemMoveJob, SIGNAL( result( KJob* ) ), SLOT( moveJobDone( KJob* ) ) ); } else if ( Qt::CopyAction == action ) { ItemCopyJob *itemCopyJob = new ItemCopyJob( item, destCollection, transaction); connect( itemCopyJob, SIGNAL( result( KJob* ) ), SLOT( copyJobDone( KJob* ) ) ); } } else { // A uri, but not an akonadi url. What to do? // Should handle known mimetypes like vcards first. // That should make any remaining uris meaningless at this point. } } } return false; // ### Return false so that the view does not update with the dropped // in place where they were dropped. That will be done when the monitor notifies the model // through collectionsReceived that the move was successful. } else { // not a set of uris. Maybe vcards etc. Check if the parent supports them, and maybe do // fromMimeData for them. Hmm, put it in the same transaction with the above? // TODO: This should be handled first, not last. } } return false; } QModelIndex EntityTreeModel::index( int row, int column, const QModelIndex & parent ) const { Q_D( const EntityTreeModel ); if ( parent.column() > 0 ) { return QModelIndex(); } //TODO: don't use column count here? Use some d-> func. if ( column >= columnCount() || column < 0 ) return QModelIndex(); QList childEntities; const Node *parentNode = reinterpret_cast( parent.internalPointer() ); if ( !parentNode || !parent.isValid() ) { if ( d->m_showRootCollection ) childEntities << d->m_childEntities.value( -1 ); else childEntities = d->m_childEntities.value( d->m_rootCollection.id() ); } else { if ( parentNode->id >= 0 ) childEntities = d->m_childEntities.value( parentNode->id ); } const int size = childEntities.size(); if ( row < 0 || row >= size ) return QModelIndex(); Node *node = childEntities.at( row ); return createIndex( row, column, reinterpret_cast( node ) ); } QModelIndex EntityTreeModel::parent( const QModelIndex & index ) const { Q_D( const EntityTreeModel ); if ( !index.isValid() ) return QModelIndex(); const Node *node = reinterpret_cast( index.internalPointer() ); if ( !node ) return QModelIndex(); const Collection collection = d->m_collections.value( node->parent ); if ( !collection.isValid() ) return QModelIndex(); if ( collection.id() == d->m_rootCollection.id() ) { if ( !d->m_showRootCollection ) return QModelIndex(); else return createIndex( 0, 0, reinterpret_cast( d->m_rootNode ) ); } const int row = d->indexOf( d->m_childEntities.value( collection.parentCollection().id()), collection.id() ); Node *parentNode = d->m_childEntities.value( collection.parentCollection().id() ).at( row ); return createIndex( row, 0, reinterpret_cast( parentNode ) ); } int EntityTreeModel::rowCount( const QModelIndex & parent ) const { Q_D( const EntityTreeModel ); const Node *node = reinterpret_cast( parent.internalPointer() ); qint64 id; if ( !parent.isValid() ) { // If we're showing the root collection then it will be the only child of the root. if ( d->m_showRootCollection ) return d->m_childEntities.value( -1 ).size(); id = d->m_rootCollection.id(); } else { if ( !node ) return 0; if ( Node::Item == node->type ) return 0; id = node->id; } if ( parent.column() <= 0 ) return d->m_childEntities.value( id ).size(); return 0; } int EntityTreeModel::getColumnCount(int headerSet) const { // Not needed in this model. Q_UNUSED(headerSet); return columnCount(); } QVariant EntityTreeModel::getHeaderData( int section, Qt::Orientation orientation, int role, int headerSet) const { // Not needed in this model. Q_UNUSED(headerSet); if ( section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole ) return i18nc( "@title:column, name of a thing", "Name" ); return QAbstractItemModel::headerData( section, orientation, role ); } QVariant EntityTreeModel::headerData( int section, Qt::Orientation orientation, int role ) const { const int headerSet = (role / TerminalUserRole); role %= TerminalUserRole; return getHeaderData( section, orientation, role, headerSet ); } QMimeData *EntityTreeModel::mimeData( const QModelIndexList &indexes ) const { Q_D( const EntityTreeModel ); QMimeData *data = new QMimeData(); KUrl::List urls; foreach( const QModelIndex &index, indexes ) { if ( index.column() != 0 ) continue; if (!index.isValid()) continue; const Node *node = reinterpret_cast( index.internalPointer() ); if ( Node::Collection == node->type ) urls << d->m_collections.value(node->id).url(); else if ( Node::Item == node->type ) urls << d->m_items.value( node->id ).url( Item::UrlWithMimeType ); else // if that happens something went horrible wrong Q_ASSERT(false); } urls.populateMimeData( data ); return data; } // Always return false for actions which take place asyncronously, eg via a Job. bool EntityTreeModel::setData( const QModelIndex &index, const QVariant &value, int role ) { Q_D( EntityTreeModel ); const Node *node = reinterpret_cast( index.internalPointer() ); if ( index.column() == 0 && (role & (Qt::EditRole | ItemRole | CollectionRole)) ) { if ( Node::Collection == node->type ) { Collection collection = d->m_collections.value( node->id ); if ( !collection.isValid() || !value.isValid() ) return false; if ( Qt::EditRole == role ) { collection.setName( value.toString() ); if ( collection.hasAttribute() ) { EntityDisplayAttribute *displayAttribute = collection.attribute(); displayAttribute->setDisplayName( value.toString() ); collection.addAttribute( displayAttribute ); } } if ( CollectionRole == role ) collection = value.value(); CollectionModifyJob *job = new CollectionModifyJob( collection, d->m_session ); connect( job, SIGNAL( result( KJob* ) ), SLOT( updateJobDone( KJob* ) ) ); return false; } else if (Node::Item == node->type) { Item item = d->m_items.value( node->id ); if ( !item.isValid() || !value.isValid() ) return false; if ( Qt::EditRole == role ) { if ( item.hasAttribute() ) { EntityDisplayAttribute *displayAttribute = item.attribute( Entity::AddIfMissing ); displayAttribute->setDisplayName( value.toString() ); item.addAttribute( displayAttribute ); } } if ( ItemRole == role ) item = value.value(); ItemModifyJob *itemModifyJob = new ItemModifyJob( item, d->m_session ); connect( itemModifyJob, SIGNAL( result( KJob* ) ), SLOT( updateJobDone( KJob* ) ) ); return false; } } return QAbstractItemModel::setData( index, value, role ); } bool EntityTreeModel::canFetchMore( const QModelIndex & parent ) const { Q_D(const EntityTreeModel); const Item item = parent.data( ItemRole ).value(); if ( item.isValid() ) { // items can't have more rows. // TODO: Should I use this for fetching more of an item, ie more payload parts? return false; } else { // but collections can... const Collection::Id colId = parent.data( CollectionIdRole ).toULongLong(); // But the root collection can't... if ( Collection::root().id() == colId ) { return false; } foreach (Node *node, d->m_childEntities.value( colId ) ) { if ( Node::Item == node->type ) { // Only try to fetch more from a collection if we don't already have items in it. // Otherwise we'd spend all the time listing items in collections. // This means that collections which don't contain items get a lot of item fetch jobs started on them. // Will fix that later. return false; } } return true; } // TODO: It might be possible to get akonadi to tell us if a collection is empty // or not and use that information instead of assuming all collections are not empty. // Using Collection statistics? } void EntityTreeModel::fetchMore( const QModelIndex & parent ) { Q_D( EntityTreeModel ); if (!canFetchMore(parent)) return; if ( d->m_itemPopulation == ImmediatePopulation ) // Nothing to do. The items are already in the model. return; else if ( d->m_itemPopulation == LazyPopulation ) { const Collection collection = parent.data( CollectionRole ).value(); if ( !collection.isValid() ) return; d->fetchItems( collection ); } } bool EntityTreeModel::hasChildren( const QModelIndex &parent ) const { Q_D( const EntityTreeModel ); // TODO: Empty collections right now will return true and get a little + to expand. // There is probably no way to tell if a collection // has child items in akonadi without first attempting an itemFetchJob... // Figure out a way to fix this. (Statistics) return ((rowCount(parent) > 0) || (canFetchMore( parent ) && d->m_itemPopulation == LazyPopulation)); } bool EntityTreeModel::match(const Item &item, const QVariant &value, Qt::MatchFlags flags) const { Q_UNUSED(item); Q_UNUSED(value); Q_UNUSED(flags); return false; } bool EntityTreeModel::match(const Collection &collection, const QVariant &value, Qt::MatchFlags flags) const { Q_UNUSED(collection); Q_UNUSED(value); Q_UNUSED(flags); return false; } QModelIndexList EntityTreeModel::match(const QModelIndex& start, int role, const QVariant& value, int hits, Qt::MatchFlags flags ) const { if (role != AmazingCompletionRole) return QAbstractItemModel::match(start, role, value, hits, flags); // Try to match names, and email addresses. QModelIndexList list; if (role < 0 || !start.isValid() || !value.isValid()) return list; const int column = 0; int row = start.row(); QModelIndex parentIdx = start.parent(); int parentRowCount = rowCount(parentIdx); while (row < parentRowCount && (hits == -1 || list.size() < hits)) { QModelIndex idx = index(row, column, parentIdx); Item item = idx.data(ItemRole).value(); if (!item.isValid()) { Collection col = idx.data(CollectionRole).value(); if (!col.isValid()) { continue; } if (match(col, value, flags)) list << idx; } else { if (match(item, value, flags)) { list << idx; } } ++row; } return list; } bool EntityTreeModel::insertRows( int, int, const QModelIndex& ) { return false; } bool EntityTreeModel::insertColumns( int, int, const QModelIndex& ) { return false; } bool EntityTreeModel::removeRows( int start, int end, const QModelIndex &parent ) { /* beginRemoveRows(start, end, parent); // TODO: Implement me. endRemoveRows(start, end, parent); */ return false; } bool EntityTreeModel::removeColumns( int, int, const QModelIndex& ) { return false; } void EntityTreeModel::setRootCollection( const Collection &collection ) { Q_D(EntityTreeModel); Q_ASSERT( collection.isValid() ); d->m_rootCollection = collection; clearAndReset(); } Collection EntityTreeModel::rootCollection() const { Q_D(const EntityTreeModel); return d->m_rootCollection; } QModelIndex EntityTreeModel::indexForCollection( const Collection &collection ) const { Q_D(const EntityTreeModel); // The id of the parent of Collection::root is not guaranteed to be -1 as assumed by startFirstListJob, // we ensure that we use -1 for the invalid Collection. const Collection::Id parentId = collection.parentCollection().isValid() ? collection.parentCollection().id() : -1; const int row = d->indexOf( d->m_childEntities.value( parentId ), collection.id() ); if ( row < 0 ) return QModelIndex(); Node *node = d->m_childEntities.value( parentId ).at( row ); return createIndex( row, 0, reinterpret_cast( node ) ); } QModelIndexList EntityTreeModel::indexesForItem( const Item &item ) const { Q_D(const EntityTreeModel); QModelIndexList indexes; const Collection::List collections = d->getParentCollections( item ); const qint64 id = item.id(); foreach ( const Collection &collection, collections ) { const int row = d->indexOf( d->m_childEntities.value( collection.id() ), id ); Node *node = d->m_childEntities.value( collection.id() ).at( row ); indexes << createIndex( row, 0, reinterpret_cast( node ) ); } return indexes; } void EntityTreeModel::setItemPopulationStrategy( ItemPopulationStrategy strategy ) { Q_D(EntityTreeModel); d->m_itemPopulation = strategy; clearAndReset(); } EntityTreeModel::ItemPopulationStrategy EntityTreeModel::itemPopulationStrategy() const { Q_D(const EntityTreeModel); return d->m_itemPopulation; } void EntityTreeModel::setIncludeRootCollection( bool include ) { Q_D(EntityTreeModel); d->m_showRootCollection = include; clearAndReset(); } bool EntityTreeModel::includeRootCollection() const { Q_D(const EntityTreeModel); return d->m_showRootCollection; } void EntityTreeModel::setRootCollectionDisplayName( const QString &displayName ) { Q_D(EntityTreeModel); d->m_rootCollectionDisplayName = displayName; // TODO: Emit datachanged if it is being shown. } QString EntityTreeModel::rootCollectionDisplayName() const { Q_D( const EntityTreeModel); return d->m_rootCollectionDisplayName; } void EntityTreeModel::setCollectionFetchStrategy( CollectionFetchStrategy strategy ) { Q_D( EntityTreeModel); d->m_collectionFetchStrategy = strategy; clearAndReset(); } EntityTreeModel::CollectionFetchStrategy EntityTreeModel::collectionFetchStrategy() const { Q_D( const EntityTreeModel); return d->m_collectionFetchStrategy; } #include "entitytreemodel.moc" diff --git a/akonadi/monitor_p.cpp b/akonadi/monitor_p.cpp index 0e6e53afa..6042c1f15 100644 --- a/akonadi/monitor_p.cpp +++ b/akonadi/monitor_p.cpp @@ -1,333 +1,333 @@ /* Copyright (c) 2007 Tobias Koenig 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. */ // @cond PRIVATE #include "monitor_p.h" #include "collectionfetchjob.h" #include "collectionstatistics.h" #include "itemfetchjob.h" #include "notificationmessage_p.h" #include "session.h" #include using namespace Akonadi; static const int PipelineSize = 5; MonitorPrivate::MonitorPrivate(Monitor * parent) : q_ptr( parent ), nm( 0 ), monitorAll( false ), collectionCache( 3*PipelineSize ), // needs to be at least 3x pipeline size for the collection move case itemCache( PipelineSize ), // needs to be at least 1x pipeline size fetchCollection( false ), fetchCollectionStatistics( false ) { } void MonitorPrivate::init() { QObject::connect( &collectionCache, SIGNAL(dataAvailable()), q_ptr, SLOT(dataAvailable()) ); QObject::connect( &itemCache, SIGNAL(dataAvailable()), q_ptr, SLOT(dataAvailable()) ); } bool MonitorPrivate::connectToNotificationManager() { NotificationMessage::registerDBusTypes(); if ( !nm ) nm = new org::freedesktop::Akonadi::NotificationManager( QLatin1String( "org.freedesktop.Akonadi" ), QLatin1String( "/notifications" ), QDBusConnection::sessionBus(), q_ptr ); else return true; if ( !nm ) { kWarning() << "Unable to connect to notification manager"; } else { QObject::connect( nm, SIGNAL(notify(Akonadi::NotificationMessage::List)), q_ptr, SLOT(slotNotify(Akonadi::NotificationMessage::List)) ); return true; } return false; } int MonitorPrivate::pipelineSize() const { return PipelineSize; } bool MonitorPrivate::acceptNotification(const NotificationMessage & msg) { if ( isSessionIgnored( msg.sessionId() ) ) return false; switch ( msg.type() ) { case NotificationMessage::InvalidType: kWarning() << "Received invalid change notification!"; return false; case NotificationMessage::Item: return isItemMonitored( msg.uid(), msg.parentCollection(), msg.parentDestCollection(), msg.mimeType(), msg.resource() ) || isCollectionMonitored( msg.parentCollection(), msg.resource() ) || isCollectionMonitored( msg.parentDestCollection(), msg.resource() ); case NotificationMessage::Collection: return isCollectionMonitored( msg.uid(), msg.resource() ) || isCollectionMonitored( msg.parentCollection(), msg.resource() ) || isCollectionMonitored( msg.parentDestCollection(), msg.resource() ); } Q_ASSERT( false ); return false; } void MonitorPrivate::dispatchNotifications() { while ( pipeline.size() < pipelineSize() && !pendingNotifications.isEmpty() ) { const NotificationMessage msg = pendingNotifications.dequeue(); if ( ensureDataAvailable( msg ) && pipeline.isEmpty() ) emitNotification( msg ); else pipeline.enqueue( msg ); } } bool MonitorPrivate::ensureDataAvailable( const NotificationMessage &msg ) { bool allCached = true; if ( fetchCollection ) { if ( !collectionCache.ensureCached( msg.parentCollection(), mCollectionFetchScope ) ) allCached = false; if ( msg.operation() == NotificationMessage::Move && !collectionCache.ensureCached( msg.parentDestCollection(), mCollectionFetchScope ) ) allCached = false; } if ( msg.operation() == NotificationMessage::Remove ) return allCached; // the actual object is gone already, nothing to fetch there if ( msg.type() == NotificationMessage::Item && !mItemFetchScope.isEmpty() ) { if ( !itemCache.ensureCached( msg.uid(), mItemFetchScope ) ) allCached = false; } else if ( msg.type() == NotificationMessage::Collection && fetchCollection ) { if ( !collectionCache.ensureCached( msg.uid(), mCollectionFetchScope ) ) allCached = false; } return allCached; } void MonitorPrivate::emitNotification( const NotificationMessage &msg ) { const Collection parent = collectionCache.retrieve( msg.parentCollection() ); Collection destParent; if ( msg.operation() == NotificationMessage::Move ) destParent = collectionCache.retrieve( msg.parentDestCollection() ); if ( msg.type() == NotificationMessage::Collection ) { const Collection col = collectionCache.retrieve( msg.uid() ); emitCollectionNotification( msg, col, parent, destParent ); } else if ( msg.type() == NotificationMessage::Item ) { const Item item = itemCache.retrieve( msg.uid() ); emitItemNotification( msg, item, parent, destParent ); } } void MonitorPrivate::dataAvailable() { while ( !pipeline.isEmpty() ) { const NotificationMessage msg = pipeline.head(); if ( ensureDataAvailable( msg ) ) { emitNotification( msg ); pipeline.dequeue(); } else { break; } } dispatchNotifications(); } void MonitorPrivate::updatePendingStatistics( const NotificationMessage &msg ) { if ( msg.type() == NotificationMessage::Item ) { notifyCollectionStatisticsWatchers( msg.parentCollection(), msg.resource() ); } else if ( msg.type() == NotificationMessage::Collection && msg.operation() == NotificationMessage::Remove ) { // no need for statistics updates anymore recentlyChangedCollections.remove( msg.uid() ); } } void MonitorPrivate::slotSessionDestroyed( QObject * object ) { Session* session = qobject_cast( object ); if ( session ) sessions.removeAll( session->sessionId() ); } void MonitorPrivate::slotStatisticsChangedFinished( KJob* job ) { if ( job->error() ) { kWarning() << "Error on fetching collection statistics: " << job->errorText(); } else { CollectionStatisticsJob *statisticsJob = static_cast( job ); emit q_ptr->collectionStatisticsChanged( statisticsJob->collection().id(), statisticsJob->statistics() ); } } void MonitorPrivate::slotFlushRecentlyChangedCollections() { foreach( Collection::Id collection, recentlyChangedCollections ) { if ( fetchCollectionStatistics ) { fetchStatistics( collection ); } else { static const CollectionStatistics dummyStatistics; emit q_ptr->collectionStatisticsChanged( collection, dummyStatistics ); } } recentlyChangedCollections.clear(); } void MonitorPrivate::slotNotify( const NotificationMessage::List &msgs ) { foreach ( const NotificationMessage &msg, msgs ) { invalidateCaches( msg ); if ( acceptNotification( msg ) ) { updatePendingStatistics( msg ); NotificationMessage::appendAndCompress( pendingNotifications, msg ); } } dispatchNotifications(); } void MonitorPrivate::emitItemNotification( const NotificationMessage &msg, const Item &item, const Collection &collection, const Collection &collectionDest ) { Q_ASSERT( msg.type() == NotificationMessage::Item ); Collection col = collection; Collection colDest = collectionDest; if ( !col.isValid() ) { col = Collection( msg.parentCollection() ); col.setResource( QString::fromUtf8( msg.resource() ) ); } if ( !colDest.isValid() ) { colDest = Collection( msg.parentDestCollection() ); // FIXME setResource here required ? } Item it = item; - if ( !it.isValid() ) { + if ( !it.isValid() || msg.operation() == NotificationMessage::Remove ) { it = Item( msg.uid() ); it.setRemoteId( msg.remoteId() ); it.setMimeType( msg.mimeType() ); } if ( !it.parentCollection().isValid() ) { if ( msg.operation() == NotificationMessage::Move ) it.setParentCollection( colDest ); else it.setParentCollection( col ); } switch ( msg.operation() ) { case NotificationMessage::Add: emit q_ptr->itemAdded( it, col ); break; case NotificationMessage::Modify: emit q_ptr->itemChanged( it, msg.itemParts() ); break; case NotificationMessage::Move: emit q_ptr->itemMoved( it, col, colDest ); break; case NotificationMessage::Remove: emit q_ptr->itemRemoved( it ); emit q_ptr->itemRemoved( it, col ); break; case NotificationMessage::Link: emit q_ptr->itemLinked( it, col ); break; case NotificationMessage::Unlink: emit q_ptr->itemUnlinked( it, col ); break; default: kDebug() << "Unknown operation type" << msg.operation() << "in item change notification"; break; } } void MonitorPrivate::emitCollectionNotification( const NotificationMessage &msg, const Collection &col, const Collection &par, const Collection &dest ) { Q_ASSERT( msg.type() == NotificationMessage::Collection ); Collection collection = col; - if ( !collection.isValid() ) { + if ( !collection.isValid() || msg.operation() == NotificationMessage::Remove ) { collection = Collection( msg.uid() ); collection.setResource( QString::fromUtf8( msg.resource() ) ); collection.setRemoteId( msg.remoteId() ); } Collection parent = par; if ( !parent.isValid() ) parent = Collection( msg.parentCollection() ); Collection destination = dest; if ( !destination.isValid() ) destination = Collection( msg.parentDestCollection() ); if ( !collection.parentCollection().isValid() ) { if ( msg.operation() == NotificationMessage::Move ) collection.setParentCollection( destination ); else collection.setParentCollection( parent ); } switch ( msg.operation() ) { case NotificationMessage::Add: emit q_ptr->collectionAdded( collection, parent ); break; case NotificationMessage::Modify: emit q_ptr->collectionChanged( collection ); break; case NotificationMessage::Move: emit q_ptr->collectionMoved( collection, parent, destination ); break; case NotificationMessage::Remove: emit q_ptr->collectionRemoved( collection ); break; default: kDebug() << "Unknown operation type" << msg.operation() << "in collection change notification"; } } void MonitorPrivate::invalidateCaches( const NotificationMessage &msg ) { // remove invalidates if ( msg.operation() == NotificationMessage::Remove ) { if ( msg.type() == NotificationMessage::Collection ) { collectionCache.invalidate( msg.uid() ); } else if ( msg.type() == NotificationMessage::Item ) { itemCache.invalidate( msg.uid() ); } } // modify removes the cache entry, as we need to re-fetch if ( msg.operation() == NotificationMessage::Modify ) { if ( msg.type() == NotificationMessage::Collection ) { collectionCache.update( msg.uid(), mCollectionFetchScope ); } else if ( msg.type() == NotificationMessage::Item ) { itemCache.update( msg.uid(), mItemFetchScope ); } } } // @endcond diff --git a/akonadi/resourcebase.cpp b/akonadi/resourcebase.cpp index f89537c2d..60240d29e 100644 --- a/akonadi/resourcebase.cpp +++ b/akonadi/resourcebase.cpp @@ -1,642 +1,653 @@ /* Copyright (c) 2006 Till Adam Copyright (c) 2007 Volker Krause 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 "resourcebase.h" #include "agentbase_p.h" #include "resourceadaptor.h" #include "collectiondeletejob.h" #include "collectionsync_p.h" #include "itemsync.h" #include "resourcescheduler_p.h" #include "tracerinterface.h" #include "xdgbasedirs_p.h" #include "changerecorder.h" #include "collectionfetchjob.h" #include "collectionfetchscope.h" #include "collectionmodifyjob.h" #include "itemfetchjob.h" #include "itemfetchscope.h" #include "itemmodifyjob.h" #include "itemmodifyjob_p.h" #include "session.h" #include "resourceselectjob_p.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; class Akonadi::ResourceBasePrivate : public AgentBasePrivate { public: ResourceBasePrivate( ResourceBase *parent ) : AgentBasePrivate( parent ), scheduler( 0 ), mItemSyncer( 0 ), - mCollectionSyncer( 0 ) + mCollectionSyncer( 0 ), + mHierarchicalRid( false ) { mStatusMessage = defaultReadyMessage(); } Q_DECLARE_PUBLIC( ResourceBase ) void delayedInit() { if ( !QDBusConnection::sessionBus().registerService( QLatin1String( "org.freedesktop.Akonadi.Resource." ) + mId ) ) kFatal() << "Unable to register service at D-Bus: " << QDBusConnection::sessionBus().lastError().message(); AgentBasePrivate::delayedInit(); } virtual void changeProcessed() { mMonitor->changeProcessed(); if ( !mMonitor->isEmpty() ) scheduler->scheduleChangeReplay(); scheduler->taskDone(); } void slotDeliveryDone( KJob* job ); void slotCollectionSyncDone( KJob *job ); void slotLocalListDone( KJob *job ); void slotSynchronizeCollection( const Collection &col ); void slotCollectionListDone( KJob *job ); void slotItemSyncDone( KJob *job ); void slotPercent( KJob* job, unsigned long percent ); void slotDeleteResourceCollection(); void slotDeleteResourceCollectionDone( KJob *job ); void slotCollectionDeletionDone( KJob *job ); void slotPrepareItemRetrieval( const Akonadi::Item &item ); void slotPrepareItemRetrievalResult( KJob* job ); // synchronize states Collection currentCollection; ResourceScheduler *scheduler; ItemSync *mItemSyncer; CollectionSync *mCollectionSyncer; + bool mHierarchicalRid; }; ResourceBase::ResourceBase( const QString & id ) : AgentBase( new ResourceBasePrivate( this ), id ) { Q_D( ResourceBase ); new ResourceAdaptor( this ); d->scheduler = new ResourceScheduler( this ); d->mMonitor->setChangeRecordingEnabled( true ); connect( d->mMonitor, SIGNAL( changesAdded() ), d->scheduler, SLOT( scheduleChangeReplay() ) ); d->mMonitor->setResourceMonitored( d->mId.toLatin1() ); connect( d->scheduler, SIGNAL( executeFullSync() ), SLOT( retrieveCollections() ) ); connect( d->scheduler, SIGNAL( executeCollectionTreeSync() ), SLOT( retrieveCollections() ) ); connect( d->scheduler, SIGNAL( executeCollectionSync( const Akonadi::Collection& ) ), SLOT( slotSynchronizeCollection( const Akonadi::Collection& ) ) ); connect( d->scheduler, SIGNAL( executeItemFetch( const Akonadi::Item&, const QSet& ) ), SLOT( slotPrepareItemRetrieval(Akonadi::Item)) ); connect( d->scheduler, SIGNAL( executeResourceCollectionDeletion() ), SLOT( slotDeleteResourceCollection() ) ); connect( d->scheduler, SIGNAL( status( int, const QString& ) ), SIGNAL( status( int, const QString& ) ) ); connect( d->scheduler, SIGNAL( executeChangeReplay() ), d->mMonitor, SLOT( replayNext() ) ); connect( d->scheduler, SIGNAL( fullSyncComplete() ), SIGNAL( synchronized() ) ); connect( d->mMonitor, SIGNAL( nothingToReplay() ), d->scheduler, SLOT( taskDone() ) ); connect( this, SIGNAL( synchronized() ), d->scheduler, SLOT( taskDone() ) ); connect( this, SIGNAL( agentNameChanged( const QString& ) ), this, SIGNAL( nameChanged( const QString& ) ) ); d->scheduler->setOnline( d->mOnline ); if ( !d->mMonitor->isEmpty() ) d->scheduler->scheduleChangeReplay(); new ResourceSelectJob( identifier() ); } ResourceBase::~ResourceBase() { } void ResourceBase::synchronize() { d_func()->scheduler->scheduleFullSync(); } void ResourceBase::setName( const QString &name ) { AgentBase::setAgentName( name ); } QString ResourceBase::name() const { return AgentBase::agentName(); } QString ResourceBase::parseArguments( int argc, char **argv ) { QString identifier; if ( argc < 3 ) { kDebug() << "Not enough arguments passed..."; exit( 1 ); } for ( int i = 1; i < argc - 1; ++i ) { if ( QLatin1String( argv[ i ] ) == QLatin1String( "--identifier" ) ) identifier = QLatin1String( argv[ i + 1 ] ); } if ( identifier.isEmpty() ) { kDebug() << "Identifier argument missing"; exit( 1 ); } QByteArray catalog; char *p = strrchr( argv[0], '/' ); if ( p ) catalog = QByteArray( p + 1 ); else catalog = QByteArray( argv[0] ); KCmdLineArgs::init( argc, argv, identifier.toLatin1(), catalog, ki18nc("@title, application name", "Akonadi Resource"), "0.1", ki18nc("@title, application description", "Akonadi Resource") ); KCmdLineOptions options; options.add( "identifier ", ki18nc("@label, commandline option", "Resource identifier") ); KCmdLineArgs::addCmdLineOptions( options ); return identifier; } int ResourceBase::init( ResourceBase *r ) { QApplication::setQuitOnLastWindowClosed( false ); int rv = kapp->exec(); delete r; return rv; } void ResourceBase::itemRetrieved( const Item &item ) { Q_D( ResourceBase ); Q_ASSERT( d->scheduler->currentTask().type == ResourceScheduler::FetchItem ); if ( !item.isValid() ) { QDBusMessage reply( d->scheduler->currentTask().dbusMsg ); reply << false; QDBusConnection::sessionBus().send( reply ); d->scheduler->taskDone(); return; } Item i( item ); QSet requestedParts = d->scheduler->currentTask().itemParts; foreach ( const QByteArray &part, requestedParts ) { if ( !item.loadedPayloadParts().contains( part ) ) { kWarning() << "Item does not provide part" << part; } } ItemModifyJob *job = new ItemModifyJob( i ); // FIXME: remove once the item with which we call retrieveItem() has a revision number job->disableRevisionCheck(); connect( job, SIGNAL( result( KJob* ) ), SLOT( slotDeliveryDone( KJob* ) ) ); } void ResourceBasePrivate::slotDeliveryDone(KJob * job) { Q_Q( ResourceBase ); Q_ASSERT( scheduler->currentTask().type == ResourceScheduler::FetchItem ); QDBusMessage reply( scheduler->currentTask().dbusMsg ); if ( job->error() ) { emit q->error( QLatin1String( "Error while creating item: " ) + job->errorString() ); reply << false; } else { reply << true; } QDBusConnection::sessionBus().send( reply ); scheduler->taskDone(); } void ResourceBasePrivate::slotDeleteResourceCollection() { Q_Q( ResourceBase ); CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::FirstLevel ); job->fetchScope().setResource( q->identifier() ); connect( job, SIGNAL( result( KJob* ) ), q, SLOT( slotDeleteResourceCollectionDone( KJob* ) ) ); } void ResourceBasePrivate::slotDeleteResourceCollectionDone( KJob *job ) { Q_Q( ResourceBase ); if ( job->error() ) { emit q->error( job->errorString() ); scheduler->taskDone(); } else { const CollectionFetchJob *fetchJob = static_cast( job ); if ( !fetchJob->collections().isEmpty() ) { CollectionDeleteJob *job = new CollectionDeleteJob( fetchJob->collections().first() ); connect( job, SIGNAL( result( KJob* ) ), q, SLOT( slotCollectionDeletionDone( KJob* ) ) ); } else { // there is no resource collection, so just ignore the request scheduler->taskDone(); } } } void ResourceBasePrivate::slotCollectionDeletionDone( KJob *job ) { Q_Q( ResourceBase ); if ( job->error() ) { emit q->error( job->errorString() ); } scheduler->taskDone(); } void ResourceBase::changeCommitted( const Item& item ) { Q_D( ResourceBase ); ItemModifyJob *job = new ItemModifyJob( item ); job->d_func()->setClean(); job->disableRevisionCheck(); // TODO: remove, but where/how do we handle the error? job->ignorePayload(); // we only want to reset the dirty flag and update the remote id d->changeProcessed(); } void ResourceBase::changeCommitted( const Collection &collection ) { Q_D( ResourceBase ); CollectionModifyJob *job = new CollectionModifyJob( collection ); Q_UNUSED( job ); //TODO: error checking d->changeProcessed(); } bool ResourceBase::requestItemDelivery( qint64 uid, const QString & remoteId, const QString &mimeType, const QStringList &_parts ) { Q_D( ResourceBase ); if ( !isOnline() ) { emit error( i18nc( "@info", "Cannot fetch item in offline mode." ) ); return false; } setDelayedReply( true ); // FIXME: we need at least the revision number too Item item( uid ); item.setMimeType( mimeType ); item.setRemoteId( remoteId ); QSet parts; Q_FOREACH( const QString &str, _parts ) parts.insert( str.toLatin1() ); d->scheduler->scheduleItemFetch( item, parts, message().createReply() ); return true; } void ResourceBase::collectionsRetrieved( const Collection::List & collections ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, "ResourceBase::collectionsRetrieved()", "Calling collectionsRetrieved() although no collection retrieval is in progress" ); if ( !d->mCollectionSyncer ) { d->mCollectionSyncer = new CollectionSync( identifier() ); + d->mCollectionSyncer->setHierarchicalRemoteIds( d->mHierarchicalRid ); connect( d->mCollectionSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mCollectionSyncer, SIGNAL( result( KJob* ) ), SLOT( slotCollectionSyncDone( KJob* ) ) ); } d->mCollectionSyncer->setRemoteCollections( collections ); } void ResourceBase::collectionsRetrievedIncremental( const Collection::List & changedCollections, const Collection::List & removedCollections ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, "ResourceBase::collectionsRetrievedIncremental()", "Calling collectionsRetrievedIncremental() although no collection retrieval is in progress" ); if ( !d->mCollectionSyncer ) { d->mCollectionSyncer = new CollectionSync( identifier() ); + d->mCollectionSyncer->setHierarchicalRemoteIds( d->mHierarchicalRid ); connect( d->mCollectionSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mCollectionSyncer, SIGNAL( result( KJob* ) ), SLOT( slotCollectionSyncDone( KJob* ) ) ); } d->mCollectionSyncer->setRemoteCollections( changedCollections, removedCollections ); } void ResourceBase::setCollectionStreamingEnabled( bool enable ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, "ResourceBase::setCollectionStreamingEnabled()", "Calling setCollectionStreamingEnabled() although no collection retrieval is in progress" ); if ( !d->mCollectionSyncer ) { d->mCollectionSyncer = new CollectionSync( identifier() ); + d->mCollectionSyncer->setHierarchicalRemoteIds( d->mHierarchicalRid ); connect( d->mCollectionSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mCollectionSyncer, SIGNAL( result( KJob* ) ), SLOT( slotCollectionSyncDone( KJob* ) ) ); } d->mCollectionSyncer->setStreamingEnabled( enable ); } void ResourceBase::collectionsRetrievalDone() { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, "ResourceBase::collectionsRetrievalDone()", "Calling collectionsRetrievalDone() although no collection retrieval is in progress" ); // streaming enabled, so finalize the sync if ( d->mCollectionSyncer ) { d->mCollectionSyncer->retrievalDone(); } // user did the sync himself, we are done now else { // FIXME: we need the same special case for SyncAll as in slotCollectionSyncDone here! d->scheduler->taskDone(); } } void ResourceBasePrivate::slotCollectionSyncDone( KJob * job ) { Q_Q( ResourceBase ); mCollectionSyncer = 0; if ( job->error() ) { emit q->error( job->errorString() ); } else { if ( scheduler->currentTask().type == ResourceScheduler::SyncAll ) { CollectionFetchJob *list = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); list->fetchScope().setResource( mId ); list->fetchScope().setAncestorRetrieval( q->changeRecorder()->collectionFetchScope().ancestorRetrieval() ); q->connect( list, SIGNAL( result( KJob* ) ), q, SLOT( slotLocalListDone( KJob* ) ) ); return; } } scheduler->taskDone(); } void ResourceBasePrivate::slotLocalListDone( KJob * job ) { Q_Q( ResourceBase ); if ( job->error() ) { emit q->error( job->errorString() ); } else { Collection::List cols = static_cast( job )->collections(); foreach ( const Collection &col, cols ) { scheduler->scheduleSync( col ); } scheduler->scheduleFullSyncCompletion(); } scheduler->taskDone(); } void ResourceBasePrivate::slotSynchronizeCollection( const Collection &col ) { Q_Q( ResourceBase ); currentCollection = col; // check if this collection actually can contain anything QStringList contentTypes = currentCollection.contentMimeTypes(); contentTypes.removeAll( Collection::mimeType() ); if ( !contentTypes.isEmpty() ) { emit q->status( AgentBase::Running, i18nc( "@info:status", "Syncing collection '%1'", currentCollection.name() ) ); q->retrieveItems( currentCollection ); return; } scheduler->taskDone(); } void ResourceBasePrivate::slotPrepareItemRetrieval( const Akonadi::Item &item ) { Q_Q( ResourceBase ); ItemFetchJob *fetch = new ItemFetchJob( item, this ); fetch->fetchScope().setAncestorRetrieval( q->changeRecorder()->itemFetchScope().ancestorRetrieval() ); q->connect( fetch, SIGNAL(result(KJob*)), SLOT(slotPrepareItemRetrievalResult(KJob*)) ); } void ResourceBasePrivate::slotPrepareItemRetrievalResult( KJob* job ) { Q_Q( ResourceBase ); Q_ASSERT_X( scheduler->currentTask().type == ResourceScheduler::FetchItem, "ResourceBasePrivate::slotPrepareItemRetrievalResult()", "Preparing item retrieval although no item retrieval is in progress" ); if ( job->error() ) { q->cancelTask( job->errorText() ); return; } ItemFetchJob *fetch = qobject_cast( job ); if ( fetch->items().count() != 1 ) { q->cancelTask( QLatin1String("The requested item does no longer exist") ); return; } const Item item = fetch->items().first(); const QSet parts = scheduler->currentTask().itemParts; if ( !q->retrieveItem( item, parts ) ) q->cancelTask(); } void ResourceBase::itemsRetrievalDone() { Q_D( ResourceBase ); // streaming enabled, so finalize the sync if ( d->mItemSyncer ) { d->mItemSyncer->deliveryDone(); } // user did the sync himself, we are done now else { d->scheduler->taskDone(); } } void ResourceBase::clearCache() { Q_D( ResourceBase ); d->scheduler->scheduleResourceCollectionDeletion(); } Collection ResourceBase::currentCollection() const { Q_D( const ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollection , "ResourceBase::currentCollection()", "Trying to access current collection although no item retrieval is in progress" ); return d->currentCollection; } Item ResourceBase::currentItem() const { Q_D( const ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::FetchItem , "ResourceBase::currentItem()", "Trying to access current item although no item retrieval is in progress" ); return d->scheduler->currentTask().item; } void ResourceBase::synchronizeCollectionTree() { d_func()->scheduler->scheduleCollectionTreeSync(); } void ResourceBase::cancelTask() { Q_D( ResourceBase ); switch ( d->scheduler->currentTask().type ) { case ResourceScheduler::FetchItem: itemRetrieved( Item() ); // sends the error reply and break; case ResourceScheduler::ChangeReplay: d->changeProcessed(); break; default: d->scheduler->taskDone(); } } void ResourceBase::cancelTask( const QString &msg ) { cancelTask(); emit error( msg ); } void ResourceBase::deferTask() { Q_D( ResourceBase ); d->scheduler->deferTask(); } void ResourceBase::doSetOnline( bool state ) { d_func()->scheduler->setOnline( state ); } void ResourceBase::synchronizeCollection( qint64 collectionId ) { CollectionFetchJob* job = new CollectionFetchJob( Collection( collectionId ), CollectionFetchJob::Base ); job->fetchScope().setResource( identifier() ); job->fetchScope().setAncestorRetrieval( changeRecorder()->collectionFetchScope().ancestorRetrieval() ); connect( job, SIGNAL( result( KJob* ) ), SLOT( slotCollectionListDone( KJob* ) ) ); } void ResourceBasePrivate::slotCollectionListDone( KJob *job ) { if ( !job->error() ) { Collection::List list = static_cast( job )->collections(); if ( !list.isEmpty() ) { Collection col = list.first(); scheduler->scheduleSync( col ); } } // TODO: error handling } void ResourceBase::setTotalItems( int amount ) { kDebug() << amount; Q_D( ResourceBase ); setItemStreamingEnabled( true ); d->mItemSyncer->setTotalItems( amount ); } void ResourceBase::setItemStreamingEnabled( bool enable ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollection, "ResourceBase::setItemStreamingEnabled()", "Calling setItemStreamingEnabled() although no item retrieval is in progress" ); if ( !d->mItemSyncer ) { d->mItemSyncer = new ItemSync( currentCollection() ); connect( d->mItemSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mItemSyncer, SIGNAL( result( KJob* ) ), SLOT( slotItemSyncDone( KJob* ) ) ); } d->mItemSyncer->setStreamingEnabled( enable ); } void ResourceBase::itemsRetrieved( const Item::List &items ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollection, "ResourceBase::itemsRetrieved()", "Calling itemsRetrieved() although no item retrieval is in progress" ); if ( !d->mItemSyncer ) { d->mItemSyncer = new ItemSync( currentCollection() ); connect( d->mItemSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mItemSyncer, SIGNAL( result( KJob* ) ), SLOT( slotItemSyncDone( KJob* ) ) ); } d->mItemSyncer->setFullSyncItems( items ); } void ResourceBase::itemsRetrievedIncremental( const Item::List &changedItems, const Item::List &removedItems ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollection, "ResourceBase::itemsRetrievedIncremental()", "Calling itemsRetrievedIncremental() although no item retrieval is in progress" ); if ( !d->mItemSyncer ) { d->mItemSyncer = new ItemSync( currentCollection() ); connect( d->mItemSyncer, SIGNAL( percent( KJob*, unsigned long ) ), SLOT( slotPercent( KJob*, unsigned long ) ) ); connect( d->mItemSyncer, SIGNAL( result( KJob* ) ), SLOT( slotItemSyncDone( KJob* ) ) ); } d->mItemSyncer->setIncrementalSyncItems( changedItems, removedItems ); } void ResourceBasePrivate::slotItemSyncDone( KJob *job ) { mItemSyncer = 0; Q_Q( ResourceBase ); if ( job->error() ) { emit q->error( job->errorString() ); } scheduler->taskDone(); } void ResourceBasePrivate::slotPercent( KJob *job, unsigned long percent ) { Q_Q( ResourceBase ); Q_UNUSED( job ); emit q->percent( percent ); } +void ResourceBase::enableHierarchicalRemoteIdentifiers( bool enable ) +{ + Q_D( ResourceBase ); + d->mHierarchicalRid = enable; +} + #include "resourcebase.moc" diff --git a/akonadi/resourcebase.h b/akonadi/resourcebase.h index 6290b030c..025f4954a 100644 --- a/akonadi/resourcebase.h +++ b/akonadi/resourcebase.h @@ -1,470 +1,475 @@ /* This file is part of akonadiresources. Copyright (c) 2006 Till Adam Copyright (c) 2007 Volker Krause 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. */ #ifndef AKONADI_RESOURCEBASE_H #define AKONADI_RESOURCEBASE_H #include "akonadi_export.h" #include #include #include class KJob; class ResourceAdaptor; namespace Akonadi { class ResourceBasePrivate; /** * @short The base class for all Akonadi resources. * * This class should be used as a base class by all resource agents, * because it encapsulates large parts of the protocol between * resource agent, agent manager and the Akonadi storage. * * It provides many convenience methods to make implementing a * new Akonadi resource agent as simple as possible. * *

How to write a resource

* * The following provides an overview of what you need to do to implement * your own Akonadi resource. In the following, the term 'backend' refers * to the entity the resource connects with Akonadi, be it a single file * or a remote server. * * @todo Complete this (online/offline state management) * *
Basic %Resource Framework
* * The following is needed to create a new resource: * - A new class deriving from Akonadi::ResourceBase, implementing at least all * pure-virtual methods, see below for further details. * - call init() in your main() function. * - a .desktop file similar to the following example * \code * [Desktop Entry] * Encoding=UTF-8 * Name=My Akonadi Resource * Type=AkonadiResource * Exec=akonadi_my_resource * Icon=my-icon * * X-Akonadi-MimeTypes= * X-Akonadi-Capabilities=Resource * X-Akonadi-Identifier=akonadi_my_resource * \endcode * *
Handling PIM Items
* * To follow item changes in the backend, the following steps are necessary: * - Implement retrieveItems() to synchronize all items in the given * collection. If the backend supports incremental retrieval, * implementing support for that is recommended to improve performance. * - Convert the items provided by the backend to Akonadi items. * This typically happens either in retrieveItems() if you retrieved * the collection synchronously (not recommended for network backends) or * in the result slot of the asynchronous retrieval job. * Converting means to create Akonadi::Item objects for every retrieved * item. It's very important that every object has its remote identifier set. * - Call itemsRetrieved() or itemsRetrievedIncremental() respectively * with the item objects created above. The Akonadi storage will then be * updated automatically. Note that it is usually not necessary to manipulate * any item in the Akonadi storage manually. * * To fetch item data on demand, the method retrieveItem() needs to be * reimplemented. Fetch the requested data there and call itemRetrieved() * with the result item. * * To write local changes back to the backend, you need to re-implement * the following three methods: * - itemAdded() * - itemChanged() * - itemRemoved() * Note that these three functions don't get the full payload of the items by default, * you need to change the item fetch scope of the change recorder to fetch the full * payload. This can be expensive with big payloads, though.
* Once you have handled changes in these methods call changeCommitted(). * These methods are called whenever a local item related to this resource is * added, modified or deleted. They are only called if the resource is online, otherwise * all changes are recorded and replayed as soon the resource is online again. * *
Handling Collections
* * To follow collection changes in the backend, the following steps are necessary: * - Implement retrieveCollections() to retrieve collections from the backend. * If the backend supports incremental collections updates, implementing * support for that is recommended to improve performance. * - Convert the collections of the backend to Akonadi collections. * This typically happens either in retrieveCollections() if you retrieved * the collection synchronously (not recommended for network backends) or * in the result slot of the asynchronous retrieval job. * Converting means to create Akonadi::Collection objects for every retrieved * collection. It's very important that every object has its remote identifier * and its parent remote identifier set. * - Call collectionsRetrieved() or collectionsRetrievedIncremental() respectively * with the collection objects created above. The Akonadi storage will then be * updated automatically. Note that it is usually not necessary to manipulate * any collection in the Akonadi storage manually. * * * To write local collection changes back to the backend, you need to re-implement * the following three methods: * - collectionAdded() * - collectionChanged() * - collectionRemoved() * Once you have handled changes in these methods call changeCommitted(). * These methods are called whenever a local collection related to this resource is * added, modified or deleted. They are only called if the resource is online, otherwise * all changes are recorded and replayed as soon the resource is online again. * * @todo Convenience base class for collection-less resources */ // FIXME_API: API dox need to be updated for Observer approach (kevin) class AKONADI_EXPORT ResourceBase : public AgentBase { Q_OBJECT public: /** * Use this method in the main function of your resource * application to initialize your resource subclass. * This method also takes care of creating a KApplication * object and parsing command line arguments. * * @note In case the given class is also derived from AgentBase::Observer * it gets registered as its own observer (see AgentBase::Observer), e.g. * resourceInstance->registerObserver( resourceInstance ); * * @code * * class MyResource : public ResourceBase * { * ... * }; * * int main( int argc, char **argv ) * { * return ResourceBase::init( argc, argv ); * } * * @endcode */ template static int init( int argc, char **argv ) { const QString id = parseArguments( argc, argv ); KApplication app; T* r = new T( id ); // check if T also inherits AgentBase::Observer and // if it does, automatically register it on itself Observer *observer = dynamic_cast( r ); if ( observer != 0 ) r->registerObserver( observer ); return init( r ); } /** * This method is used to set the name of the resource. */ //FIXME_API: make sure location is renamed to this by resourcebase void setName( const QString &name ); /** * Returns the name of the resource. */ QString name() const; Q_SIGNALS: /** * This signal is emitted whenever the name of the resource has changed. * * @param name The new name of the resource. */ void nameChanged( const QString &name ); /** * Emitted when a full synchronization has been completed. */ void synchronized(); protected Q_SLOTS: /** * Retrieve the collection tree from the remote server and supply it via * collectionsRetrieved() or collectionsRetrievedIncremental(). * @see collectionsRetrieved(), collectionsRetrievedIncremental() */ virtual void retrieveCollections() = 0; /** * Retrieve all (new/changed) items in collection @p collection. * It is recommended to use incremental retrieval if the backend supports that * and provide the result by calling itemsRetrievedIncremental(). * If incremental retrieval is not possible, provide the full listing by calling * itemsRetrieved( const Item::List& ). * In any case, ensure that all items have a correctly set remote identifier * to allow synchronizing with items already existing locally. * In case you don't want to use the built-in item syncing code, store the retrieved * items manually and call itemsRetrieved() once you are done. * @param collection The collection whose items to retrieve. * @see itemsRetrieved( const Item::List& ), itemsRetrievedIncremental(), itemsRetrieved(), currentCollection() */ virtual void retrieveItems( const Akonadi::Collection &collection ) = 0; /** * Retrieve a single item from the backend. The item to retrieve is provided as @p item. * Add the requested payload parts and call itemRetrieved() when done. * @param item The empty item whose payload should be retrieved. Use this object when delivering * the result instead of creating a new item to ensure conflict detection will work. * @param parts The item parts that should be retrieved. * @return false if there is an immediate error when retrieving the item. * @see itemRetrieved() */ virtual bool retrieveItem( const Akonadi::Item &item, const QSet &parts ) = 0; protected: /** * Creates a base resource. * * @param id The instance id of the resource. */ ResourceBase( const QString & id ); /** * Destroys the base resource. */ ~ResourceBase(); /** * Call this method from retrieveItem() once the result is available. * * @param item The retrieved item. */ void itemRetrieved( const Item &item ); /** * Resets the dirty flag of the given item and updates the remote id. * * Call whenever you have successfully written changes back to the server. * This implicitly calls changeProcessed(). * @param item The changed item. */ void changeCommitted( const Item &item ); /** * Call whenever you have successfully handled or ignored a collection * change notification. * * This will update the remote identifier of @p collection if necessary, * as well as any other collection attributes. * This implicitly calls changeProcessed(). * @param collection The collection which changes have been handled. */ void changeCommitted( const Collection &collection ); /** * Call this to supply the full folder tree retrieved from the remote server. * * @param collections A list of collections. * @see collectionsRetrievedIncremental() */ void collectionsRetrieved( const Collection::List &collections ); /** * Call this to supply incrementally retrieved collections from the remote server. * * @param changedCollections Collections that have been added or changed. * @param removedCollections Collections that have been deleted. * @see collectionsRetrieved() */ void collectionsRetrievedIncremental( const Collection::List &changedCollections, const Collection::List &removedCollections ); /** * Enable collection streaming, that is collections don't have to be delivered at once * as result of a retrieveCollections() call but can be delivered by multiple calls * to collectionsRetrieved() or collectionsRetrievedIncremental(). When all collections * have been retrieved, call collectionsRetrievalDone(). * @param enable @c true if collection streaming should be enabled, @c false by default */ void setCollectionStreamingEnabled( bool enable ); /** * Call this method to indicate you finished synchronizing the collection tree. * * This is not needed if you use the built in syncing without collection streaming * and call collectionsRetrieved() or collectionRetrievedIncremental() instead. * If collection streaming is enabled, call this method once all collections have been delivered * using collectionsRetrieved() or collectionsRetrievedIncremental(). */ void collectionsRetrievalDone(); /** * Call this method to supply the full collection listing from the remote server. * * If the remote server supports incremental listing, it's strongly * recommended to use itemsRetrievedIncremental() instead. * @param items A list of items. * @see itemsRetrievedIncremental(). */ void itemsRetrieved( const Item::List &items ); /** * Call this method when you want to use the itemsRetrieved() method * in streaming mode and indicate the amount of items that will arrive * that way. * @deprecated Use setItemStreamingEnabled( true ) + itemsRetrieved[Incremental]() * + itemsRetrieved() instead. */ void setTotalItems( int amount ); /** * Enable item streaming. * Item streaming is disabled by default. * @param enable @c true if items are delivered in chunks rather in one big block. */ void setItemStreamingEnabled( bool enable ); /** * Call this method to supply incrementally retrieved items from the remote server. * * @param changedItems Items changed in the backend. * @param removedItems Items removed from the backend. */ void itemsRetrievedIncremental( const Item::List &changedItems, const Item::List &removedItems ); /** * Call this method to indicate you finished synchronizing the current collection. * * This is not needed if you use the built in syncing without item streaming * and call itemsRetrieved() or itemsRetrievedIncremental() instead. * If item streaming is enabled, call this method once all items have been delivered * using itemsRetrieved() or itemsRetrievedIncremental(). * @see retrieveItems() */ void itemsRetrievalDone(); /** * Call this method to remove all items and collections of the resource from the * server cache. * * The method should be used whenever the configuration of the resource has changed * and therefor the cached items might not be valid any longer. * * @since 4.3 */ void clearCache(); /** * Returns the collection that is currently synchronized. */ Collection currentCollection() const; /** * Returns the item that is currently retrieved. */ Item currentItem() const; /** * This method is called whenever the resource should start synchronize all data. */ void synchronize(); /** * This method is called whenever the collection with the given @p id * shall be synchronized. */ void synchronizeCollection( qint64 id ); /** * Refetches the Collections. */ void synchronizeCollectionTree(); /** * Stops the execution of the current task and continues with the next one. */ void cancelTask(); /** * Stops the execution of the current task and continues with the next one. * Additionally an error message is emitted. */ void cancelTask( const QString &error ); /** * Stops the execution of the current task and continues with the next one. * The current task will be tried again later. * * @since 4.3 */ void deferTask(); /** * Inherited from AgentBase. */ void doSetOnline( bool online ); + /** + * Indicate the use of hierarchical remote identifiers. + */ + void enableHierarchicalRemoteIdentifiers( bool enable ); + private: static QString parseArguments( int, char** ); static int init( ResourceBase *r ); // dbus resource interface friend class ::ResourceAdaptor; bool requestItemDelivery( qint64 uid, const QString &remoteId, const QString &mimeType, const QStringList &parts ); private: Q_DECLARE_PRIVATE( ResourceBase ) Q_PRIVATE_SLOT( d_func(), void slotDeliveryDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotCollectionSyncDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotDeleteResourceCollection() ) Q_PRIVATE_SLOT( d_func(), void slotDeleteResourceCollectionDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotCollectionDeletionDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotLocalListDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotSynchronizeCollection( const Akonadi::Collection& ) ) Q_PRIVATE_SLOT( d_func(), void slotCollectionListDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotItemSyncDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void slotPercent( KJob*, unsigned long ) ) Q_PRIVATE_SLOT( d_func(), void slotPrepareItemRetrieval( const Akonadi::Item& item ) ) Q_PRIVATE_SLOT( d_func(), void slotPrepareItemRetrievalResult( KJob* ) ) }; } #ifndef AKONADI_RESOURCE_MAIN /** * Convenience Macro for the most common main() function for Akonadi resources. */ #define AKONADI_RESOURCE_MAIN( resourceClass ) \ int main( int argc, char **argv ) \ { \ return Akonadi::ResourceBase::init( argc, argv ); \ } #endif #endif diff --git a/akonadi/selftestdialog.cpp b/akonadi/selftestdialog.cpp index af5281d2d..1b904cde3 100644 --- a/akonadi/selftestdialog.cpp +++ b/akonadi/selftestdialog.cpp @@ -1,602 +1,604 @@ /* Copyright (c) 2008 Volker Krause 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 "selftestdialog_p.h" #include "agentmanager.h" #include "session_p.h" #include "servermanager_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // @cond PRIVATE #define AKONADI_CONTROL_SERVICE QLatin1String("org.freedesktop.Akonadi.Control") #define AKONADI_SERVER_SERVICE QLatin1String("org.freedesktop.Akonadi") using namespace Akonadi; static QString makeLink( const QString &file ) { return QString::fromLatin1( "%2" ).arg( file, file ); } enum SelfTestRole { ResultTypeRole = Qt::UserRole, FileIncludeRole, ListDirectoryRole, EnvVarRole, SummaryRole, DetailsRole }; SelfTestDialog::SelfTestDialog(QWidget * parent) : KDialog( parent ) { setCaption( i18n( "Akonadi Server Self-Test" ) ); setButtons( Close | User1 | User2 ); setButtonText( User1, i18n( "Save Report..." ) ); setButtonIcon( User1, KIcon( QString::fromLatin1("document-save") ) ); setButtonText( User2, i18n( "Copy Report to Clipboard" ) ); setButtonIcon( User2, KIcon( QString::fromLatin1("edit-copy") ) ); showButtonSeparator( true ); ui.setupUi( mainWidget() ); mTestModel = new QStandardItemModel( this ); ui.testView->setModel( mTestModel ); connect( ui.testView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(selectionChanged(QModelIndex)) ); connect( ui.detailsLabel, SIGNAL(linkActivated(QString)), SLOT(linkActivated(QString)) ); connect( this, SIGNAL(user1Clicked()), SLOT(saveReport()) ); connect( this, SIGNAL(user2Clicked()), SLOT(copyReport()) ); runTests(); } void SelfTestDialog::hideIntroduction() { ui.introductionLabel->hide(); } QStandardItem* SelfTestDialog::report( ResultType type, const KLocalizedString & summary, const KLocalizedString & details) { QStandardItem *item = new QStandardItem( summary.toString() ); switch ( type ) { case Skip: item->setIcon( KIcon( QString::fromLatin1("dialog-ok") ) ); break; case Success: item->setIcon( KIcon( QString::fromLatin1("dialog-ok-apply") ) ); break; case Warning: item->setIcon( KIcon( QString::fromLatin1("dialog-warning") ) ); break; case Error: default: item->setIcon( KIcon( QString::fromLatin1("dialog-error") ) ); } item->setEditable( false ); item->setWhatsThis( details.toString() ); item->setData( type, ResultTypeRole ); item->setData( summary.toString( 0 ), SummaryRole ); item->setData( details.toString( 0 ), DetailsRole ); mTestModel->appendRow( item ); return item; } void SelfTestDialog::selectionChanged(const QModelIndex &index ) { if ( index.isValid() ) { ui.detailsLabel->setText( index.data( Qt::WhatsThisRole ).toString() ); ui.detailsGroup->setEnabled( true ); } else { ui.detailsLabel->setText( QString() ); ui.detailsGroup->setEnabled( false ); } } void SelfTestDialog::runTests() { const QString driver = serverSetting( QLatin1String("General"), "Driver", QLatin1String("QMYSQL") ).toString(); testSQLDriver(); if (driver == QLatin1String( "QPSQL" )) { testPSQLServer(); } else { testMySQLServer(); testMySQLServerLog(); testMySQLServerConfig(); } testAkonadiCtl(); testServerStatus(); testProtocolVersion(); testResources(); testServerLog(); testControlLog(); } QVariant SelfTestDialog::serverSetting(const QString & group, const char *key, const QVariant &def ) const { const QString serverConfigFile = XdgBaseDirs::akonadiServerConfigFile( XdgBaseDirs::ReadWrite ); QSettings settings( serverConfigFile, QSettings::IniFormat ); settings.beginGroup( group ); return settings.value( QString::fromLatin1(key), def ); } bool SelfTestDialog::useStandaloneMysqlServer() const { const QString driver = serverSetting( QLatin1String("General"), "Driver", QLatin1String("QMYSQL") ).toString(); if ( driver != QLatin1String( "QMYSQL" ) ) return false; const bool startServer = serverSetting( driver, "StartServer", true ).toBool(); if ( !startServer ) return false; return true; } bool SelfTestDialog::runProcess(const QString & app, const QStringList & args, QString & result) const { QProcess proc; proc.start( app, args ); const bool rv = proc.waitForFinished(); result.clear(); result += QString::fromLocal8Bit( proc.readAllStandardError() ); result += QString::fromLocal8Bit( proc.readAllStandardOutput() ); return rv; } void SelfTestDialog::testSQLDriver() { const QString driver = serverSetting( QLatin1String("General"), "Driver", QLatin1String("QMYSQL") ).toString(); const QStringList availableDrivers = QSqlDatabase::drivers(); - const KLocalizedString details = ki18n( "The QtSQL driver '%1' is required by your current Akonadi server configuration.\n" + const KLocalizedString detailsOk = ki18n( "The QtSQL driver '%1' is required by your current Akonadi server configuration and was found on your system." ) + .subs( driver ); + const KLocalizedString detailsFail = ki18n( "The QtSQL driver '%1' is required by your current Akonadi server configuration.\n" "The following drivers are installed: %2.\n" "Make sure the required driver is installed." ) .subs( driver ) .subs( availableDrivers.join( QLatin1String(", ") ) ); QStandardItem *item = 0; if ( availableDrivers.contains( driver ) ) - item = report( Success, ki18n( "Database driver found." ), details ); + item = report( Success, ki18n( "Database driver found." ), detailsOk ); else - item = report( Error, ki18n( "Database driver not found." ), details ); + item = report( Error, ki18n( "Database driver not found." ), detailsFail ); item->setData( XdgBaseDirs::akonadiServerConfigFile( XdgBaseDirs::ReadWrite ), FileIncludeRole ); } void SelfTestDialog::testMySQLServer() { if ( !useStandaloneMysqlServer() ) { report( Skip, ki18n( "MySQL server executable not tested." ), ki18n( "The current configuration does not require an internal MySQL server." ) ); return; } const QString driver = serverSetting( QLatin1String("General"), "Driver", QLatin1String("QMYSQL") ).toString(); const QString serverPath = serverSetting( driver, "ServerPath", QLatin1String("") ).toString(); // ### default? const KLocalizedString details = ki18n( "You currently have configured Akonadi to use the MySQL server '%1'.\n" "Make sure you have the MySQL server installed, set the correct path and ensure you have the " "necessary read and execution rights on the server executable. The server executable is typically " "called 'mysqld', its locations varies depending on the distribution." ).subs( serverPath ); QFileInfo info( serverPath ); if ( !info.exists() ) report( Error, ki18n( "MySQL server not found." ), details ); else if ( !info.isReadable() ) report( Error, ki18n( "MySQL server not readable." ), details ); else if ( !info.isExecutable() ) report( Error, ki18n( "MySQL server not executable." ), details ); else if ( !serverPath.contains( QLatin1String("mysqld") ) ) report( Warning, ki18n( "MySQL found with unexpected name." ), details ); else report( Success, ki18n( "MySQL server found." ), details ); // be extra sure and get the server version while we are at it QString result; if ( runProcess( serverPath, QStringList() << QLatin1String( "--version" ), result ) ) { const KLocalizedString details = ki18n( "MySQL server found: %1" ).subs( result ); report( Success, ki18n( "MySQL server is executable." ), details ); } else { const KLocalizedString details = ki18n( "Executing the MySQL server '%1' failed with the following error message: '%2'" ) .subs( serverPath ).subs( result ); report( Error, ki18n( "Executing the MySQL server failed." ), details ); } } void SelfTestDialog::testMySQLServerLog() { if ( !useStandaloneMysqlServer() ) { report( Skip, ki18n( "MySQL server error log not tested." ), ki18n( "The current configuration does not require an internal MySQL server." ) ); return; } const QString logFileName = XdgBaseDirs::saveDir( "data", QLatin1String( "akonadi/db_data" ) ) + QDir::separator() + QString::fromLatin1( "mysql.err" ); const QFileInfo logFileInfo( logFileName ); if ( !logFileInfo.exists() || logFileInfo.size() == 0 ) { report( Success, ki18n( "No current MySQL error log found." ), ki18n( "The MySQL server did not report any errors during this startup into '%1'." ).subs( logFileName ) ); return; } QFile logFile( logFileName ); if ( !logFile.open( QFile::ReadOnly | QFile::Text ) ) { report( Error, ki18n( "MySQL error log not readable." ), ki18n( "A MySQL server error log file was found but is not readable: %1" ).subs( makeLink( logFileName ) ) ); return; } bool warningsFound = false; QStandardItem *item = 0; while ( !logFile.atEnd() ) { const QString line = QString::fromUtf8( logFile.readLine() ); if ( line.contains( QLatin1String( "error" ), Qt::CaseInsensitive ) ) { item = report( Error, ki18n( "MySQL server log contains errors." ), ki18n( "The MySQL server error log file '%1' contains errors." ).subs( makeLink( logFileName ) ) ); item->setData( logFileName, FileIncludeRole ); return; } if ( !warningsFound && line.contains( QLatin1String( "warn" ), Qt::CaseInsensitive ) ) { warningsFound = true; } } if ( warningsFound ) { item = report( Warning, ki18n( "MySQL server log contains warnings." ), ki18n( "The MySQL server log file '%1' contains warnings." ).subs( makeLink( logFileName ) ) ); } else { item = report( Success, ki18n( "MySQL server log contains no errors." ), ki18n( "The MySQL server log file '%1' does not contain any errors or warnings." ) .subs( makeLink( logFileName ) ) ); } item->setData( logFileName, FileIncludeRole ); logFile.close(); } void SelfTestDialog::testMySQLServerConfig() { if ( !useStandaloneMysqlServer() ) { report( Skip, ki18n( "MySQL server configuration not tested." ), ki18n( "The current configuration does not require an internal MySQL server." ) ); return; } QStandardItem *item = 0; const QString globalConfig = XdgBaseDirs::findResourceFile( "config", QLatin1String( "akonadi/mysql-global.conf" ) ); const QFileInfo globalConfigInfo( globalConfig ); if ( !globalConfig.isEmpty() && globalConfigInfo.exists() && globalConfigInfo.isReadable() ) { item = report( Success, ki18n( "MySQL server default configuration found." ), ki18n( "The default configuration for the MySQL server was found and is readable at %1." ) .subs( makeLink( globalConfig ) ) ); item->setData( globalConfig, FileIncludeRole ); } else { report( Error, ki18n( "MySQL server default configuration not found." ), ki18n( "The default configuration for the MySQL server was not found or was not readable. " "Check your Akonadi installation is complete and you have all required access rights." ) ); } const QString localConfig = XdgBaseDirs::findResourceFile( "config", QLatin1String( "akonadi/mysql-local.conf" ) ); const QFileInfo localConfigInfo( localConfig ); if ( localConfig.isEmpty() || !localConfigInfo.exists() ) { report( Skip, ki18n( "MySQL server custom configuration not available." ), ki18n( "The custom configuration for the MySQL server was not found but is optional." ) ); } else if ( localConfigInfo.exists() && localConfigInfo.isReadable() ) { item = report( Success, ki18n( "MySQL server custom configuration found." ), ki18n( "The custom configuration for the MySQL server was found and is readable at %1" ) .subs( makeLink( localConfig ) ) ); item->setData( localConfig, FileIncludeRole ); } else { report( Error, ki18n( "MySQL server custom configuration not readable." ), ki18n( "The custom configuration for the MySQL server was found at %1 but is not readable. " "Check your access rights." ).subs( makeLink( localConfig ) ) ); } const QString actualConfig = XdgBaseDirs::saveDir( "data", QLatin1String( "akonadi" ) ) + QLatin1String("/mysql.conf"); const QFileInfo actualConfigInfo( actualConfig ); if ( actualConfig.isEmpty() || !actualConfigInfo.exists() || !actualConfigInfo.isReadable() ) { report( Error, ki18n( "MySQL server configuration not found or not readable." ), ki18n( "The MySQL server configuration was not found or is not readable." ) ); } else { item = report( Success, ki18n( "MySQL server configuration is usable." ), ki18n( "The MySQL server configuration was found at %1 and is readable.").subs( makeLink( actualConfig ) ) ); item->setData( actualConfig, FileIncludeRole ); } } void SelfTestDialog::testPSQLServer() { const QString dbname = serverSetting( QLatin1String( "QPSQL" ), "Name", QLatin1String( "akonadi" )).toString(); const QString hostname = serverSetting( QLatin1String( "QPSQL" ), "Host", QLatin1String( "localhost" )).toString(); const QString username = serverSetting( QLatin1String( "QPSQL" ), "User", QLatin1String( "akonadi" )).toString(); const QString password = serverSetting( QLatin1String( "QPSQL" ), "Password", QLatin1String( "akonadi" )).toString(); const int port = serverSetting( QLatin1String( "QPSQL" ), "Port", 5432).toInt(); QSqlDatabase db = QSqlDatabase::addDatabase( QLatin1String( "QPSQL" ) ); db.setHostName( hostname ); db.setDatabaseName( dbname ); db.setUserName( username ); db.setPassword( password ); db.setPort( port ); if ( !db.open() ) { const KLocalizedString details = ki18n( db.lastError().text().toLatin1() ); report( Error, ki18n( "Cannot connect to PostgreSQL server." ), details); } else { report( Success, ki18n( "PostgreSQL server found." ), ki18n( "The PostgreSQL server was found and connection is working.")); } db.close(); } void SelfTestDialog::testAkonadiCtl() { const QString path = KStandardDirs::findExe( QLatin1String("akonadictl") ); if ( path.isEmpty() ) { report( Error, ki18n( "akonadictl not found" ), ki18n( "The program 'akonadictl' needs to be accessible in $PATH. " "Make sure you have the Akonadi server installed." ) ); return; } QString result; if ( runProcess( path, QStringList() << QLatin1String( "--version" ), result ) ) { report( Success, ki18n( "akonadictl found and usable" ), ki18n( "The program '%1' to control the Akonadi server was found " "and could be executed successfully.\nResult:\n%2" ).subs( path ).subs( result ) ); } else { report( Error, ki18n( "akonadictl found but not usable" ), ki18n( "The program '%1' to control the Akonadi server was found " "but could not be executed successfully.\nResult:\n%2\n" "Make sure the Akonadi server is installed correctly." ).subs( path ).subs( result ) ); } } void SelfTestDialog::testServerStatus() { if ( QDBusConnection::sessionBus().interface()->isServiceRegistered( AKONADI_CONTROL_SERVICE ) ) { report( Success, ki18n( "Akonadi control process registered at D-Bus." ), ki18n( "The Akonadi control process is registered at D-Bus which typically indicates it is operational." ) ); } else { report( Error, ki18n( "Akonadi control process not registered at D-Bus." ), ki18n( "The Akonadi control process is not registered at D-Bus which typically means it was not started " "or encountered a fatal error during startup." ) ); } if ( QDBusConnection::sessionBus().interface()->isServiceRegistered( AKONADI_SERVER_SERVICE ) ) { report( Success, ki18n( "Akonadi server process registered at D-Bus." ), ki18n( "The Akonadi server process is registered at D-Bus which typically indicates it is operational." ) ); } else { report( Error, ki18n( "Akonadi server process not registered at D-Bus." ), ki18n( "The Akonadi server process is not registered at D-Bus which typically means it was not started " "or encountered a fatal error during startup." ) ); } } void SelfTestDialog::testProtocolVersion() { if ( Internal::serverProtocolVersion() < 0 ) { report( Skip, ki18n( "Protocol version check not possible." ), ki18n( "Without a connection to the server it is not possible to check if the protocol version meets the requirements." ) ); return; } if ( Internal::serverProtocolVersion() < SessionPrivate::minimumProtocolVersion() ) { report( Error, ki18n( "Server protocol version is too old." ), ki18n( "The server protocol version is %1, but at least version %2 is required. " "Install a newer version of the Akonadi server." ) .subs( Internal::serverProtocolVersion() ) .subs( SessionPrivate::minimumProtocolVersion() ) ); } else { report( Success, ki18n( "Server protocol version is recent enough." ), ki18n( "The server Protocol version is %1, which equal or newer than the required version %2." ) .subs( Internal::serverProtocolVersion() ) .subs( SessionPrivate::minimumProtocolVersion() ) ); } } void SelfTestDialog::testResources() { AgentType::List agentTypes = AgentManager::self()->types(); bool resourceFound = false; foreach ( const AgentType &type, agentTypes ) { if ( type.capabilities().contains( QLatin1String("Resource") ) ) { resourceFound = true; break; } } const QStringList pathList = XdgBaseDirs::findAllResourceDirs( "data", QLatin1String( "akonadi/agents" ) ); QStandardItem *item = 0; if ( resourceFound ) { item = report( Success, ki18n( "Resource agents found." ), ki18n( "At least one resource agent has been found." ) ); } else { item = report( Error, ki18n( "No resource agents found." ), ki18n( "No resource agents have been found, Akonadi is not usable without at least one. " "This usually means that no resource agents are installed or that there is a setup problem. " "The following paths have been searched: '%1'. " "The XDG_DATA_DIRS environment variable is set to '%2', make sure this includes all paths " "where Akonadi agents are installed to." ) .subs( pathList.join( QLatin1String(" ") ) ) .subs( QString::fromLocal8Bit( qgetenv( "XDG_DATA_DIRS" ) ) ) ); } item->setData( pathList, ListDirectoryRole ); item->setData( QByteArray( "XDG_DATA_DIRS" ), EnvVarRole ); } void Akonadi::SelfTestDialog::testServerLog() { QString serverLog = XdgBaseDirs::saveDir( "data", QLatin1String( "akonadi" ) ) + QDir::separator() + QString::fromLatin1( "akonadiserver.error" ); QFileInfo info( serverLog ); if ( !info.exists() || info.size() <= 0 ) { report( Success, ki18n( "No current Akonadi server error log found." ), ki18n( "The Akonadi server did not report any errors during its current startup." ) ); } else { QStandardItem *item = report( Error, ki18n( "Current Akonadi server error log found." ), ki18n( "The Akonadi server did report error during startup into %1." ).subs( makeLink( serverLog ) ) ); item->setData( serverLog, FileIncludeRole ); } serverLog += QLatin1String(".old"); info.setFile( serverLog ); if ( !info.exists() || info.size() <= 0 ) { report( Success, ki18n( "No previous Akonadi server error log found." ), ki18n( "The Akonadi server did not report any errors during its previous startup." ) ); } else { QStandardItem *item = report( Error, ki18n( "Previous Akonadi server error log found." ), ki18n( "The Akonadi server did report error during its previous startup into %1." ).subs( makeLink( serverLog ) ) ); item->setData( serverLog, FileIncludeRole ); } } void SelfTestDialog::testControlLog() { QString controlLog = XdgBaseDirs::saveDir( "data", QLatin1String( "akonadi" ) ) + QDir::separator() + QString::fromLatin1( "akonadi_control.error" ); QFileInfo info( controlLog ); if ( !info.exists() || info.size() <= 0 ) { report( Success, ki18n( "No current Akonadi control error log found." ), ki18n( "The Akonadi control process did not report any errors during its current startup." ) ); } else { QStandardItem *item = report( Error, ki18n( "Current Akonadi control error log found." ), ki18n( "The Akonadi control process did report error during startup into %1." ).subs( makeLink( controlLog ) ) ); item->setData( controlLog, FileIncludeRole ); } controlLog += QLatin1String(".old"); info.setFile( controlLog ); if ( !info.exists() || info.size() <= 0 ) { report( Success, ki18n( "No previous Akonadi control error log found." ), ki18n( "The Akonadi control process did not report any errors during its previous startup." ) ); } else { QStandardItem *item = report( Error, ki18n( "Previous Akonadi control error log found." ), ki18n( "The Akonadi control process did report error during its previous startup into %1." ).subs( makeLink( controlLog ) ) ); item->setData( controlLog, FileIncludeRole ); } } QString SelfTestDialog::createReport() { QString result; QTextStream s( &result ); s << "Akonadi Server Self-Test Report" << endl; s << "===============================" << endl; for ( int i = 0; i < mTestModel->rowCount(); ++i ) { QStandardItem *item = mTestModel->item( i ); s << endl; s << "Test " << (i + 1) << ": "; switch ( item->data( ResultTypeRole ).toInt() ) { case Skip: s << "SKIP"; break; case Success: s << "SUCCESS"; break; case Warning: s << "WARNING"; break; case Error: default: s << "ERROR"; break; } s << endl << "--------" << endl; s << endl; s << item->data( SummaryRole ).toString() << endl; s << "Details: " << item->data( DetailsRole ).toString() << endl; if ( item->data( FileIncludeRole ).isValid() ) { s << endl; const QString fileName = item->data( FileIncludeRole ).toString(); QFile f( fileName ); if ( f.open( QFile::ReadOnly ) ) { s << "File content of '" << fileName << "':" << endl; s << f.readAll() << endl; } else { s << "File '" << fileName << "' could not be opened" << endl; } } if ( item->data( ListDirectoryRole ).isValid() ) { s << endl; const QStringList pathList = item->data( ListDirectoryRole ).toStringList(); if ( pathList.isEmpty() ) s << "Directory list is empty." << endl; foreach ( const QString &path, pathList ) { s << "Directory listing of '" << path << "':" << endl; QDir dir( path ); dir.setFilter( QDir::AllEntries | QDir::NoDotAndDotDot ); foreach ( const QString &entry, dir.entryList() ) s << entry << endl; } } if ( item->data( EnvVarRole ).isValid() ) { s << endl; const QByteArray envVarName = item->data( EnvVarRole ).toByteArray(); const QByteArray envVarValue = qgetenv( envVarName ); s << "Environment variable " << envVarName << " is set to '" << envVarValue << "'" << endl; } } s << endl; s.flush(); return result; } void SelfTestDialog::saveReport() { const QString fileName = KFileDialog::getSaveFileName( KUrl(), QString(), this, i18n("Save Test Report") ); if ( fileName.isEmpty() ) return; QFile file( fileName ); if ( !file.open( QFile::ReadWrite ) ) { KMessageBox::error( this, i18n( "Could not open file '%1'", fileName ) ); return; } file.write( createReport().toUtf8() ); file.close(); } void SelfTestDialog::copyReport() { QApplication::clipboard()->setText( createReport() ); } void SelfTestDialog::linkActivated(const QString & link) { KRun::runUrl( KUrl::fromPath( link ), QLatin1String("text/plain"), this ); } // @endcond #include "selftestdialog_p.moc" diff --git a/akonadi/standardactionmanager.cpp b/akonadi/standardactionmanager.cpp index 3797f2c91..c392828e7 100644 --- a/akonadi/standardactionmanager.cpp +++ b/akonadi/standardactionmanager.cpp @@ -1,709 +1,720 @@ /* Copyright (c) 2008 Volker Krause 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 "standardactionmanager.h" #include "agentmanager.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionmodel.h" #include "collectionutils_p.h" #include "collectionpropertiesdialog.h" +#include "entitytreemodel.h" #include "favoritecollectionsmodel.h" #include "itemdeletejob.h" #include "itemmodel.h" #include "pastehelper_p.h" #include "subscriptiondialog_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(QModelIndex) using namespace Akonadi; //@cond PRIVATE static const struct { const char *name; const char *label; const char *icon; int shortcut; const char* slot; bool isActionMenu; } actionData[] = { { "akonadi_collection_create", I18N_NOOP("&New Folder..."), "folder-new", 0, SLOT(slotCreateCollection()), false }, { "akonadi_collection_copy", 0, "edit-copy", 0, SLOT(slotCopyCollections()), false }, { "akonadi_collection_delete", I18N_NOOP("&Delete Folder"), "edit-delete", 0, SLOT(slotDeleteCollection()), false }, { "akonadi_collection_sync", I18N_NOOP("&Synchronize Folder"), "view-refresh", Qt::Key_F5, SLOT(slotSynchronizeCollection()), false }, { "akonadi_collection_properties", I18N_NOOP("Folder &Properties"), "configure", 0, SLOT(slotCollectionProperties()), false }, { "akonadi_item_copy", 0, "edit-copy", 0, SLOT(slotCopyItems()), false }, { "akonadi_paste", I18N_NOOP("&Paste"), "edit-paste", Qt::CTRL + Qt::Key_V, SLOT(slotPaste()), false }, { "akonadi_item_delete", 0, "edit-delete", Qt::Key_Delete, SLOT(slotDeleteItems()), false }, { "akonadi_manage_local_subscriptions", I18N_NOOP("Manage Local &Subscriptions..."), 0, 0, SLOT(slotLocalSubscription()), false }, { "akonadi_collection_add_to_favorites", I18N_NOOP("Add to Favorite Folders"), "bookmark-new", 0, SLOT(slotAddToFavorites()), false }, { "akonadi_collection_remove_from_favorites", I18N_NOOP("Remove from Favorite Folders"), "edit-delete", 0, SLOT(slotRemoveFromFavorites()), false }, { "akonadi_collection_rename_favorite", I18N_NOOP("Rename Favorite..."), "edit-rename", 0, SLOT(slotRenameFavorite()), false }, { "akonadi_collection_copy_to_menu", I18N_NOOP("Copy Folder To..."), "edit-copy", 0, SLOT(slotCopyCollectionTo(QAction*)), true }, { "akonadi_item_copy_to_menu", I18N_NOOP("Copy Item To..."), "edit-copy", 0, SLOT(slotCopyItemTo(QAction*)), true } }; static const int numActionData = sizeof actionData / sizeof *actionData; BOOST_STATIC_ASSERT( numActionData == StandardActionManager::LastType ); static bool canCreateCollection( const Collection &collection ) { if ( !( collection.rights() & Collection::CanCreateCollection ) ) return false; if ( !collection.contentMimeTypes().contains( Collection::mimeType() ) ) return false; return true; } /** * @internal */ class StandardActionManager::Private { public: Private( StandardActionManager *parent ) : q( parent ), collectionSelectionModel( 0 ), itemSelectionModel( 0 ), favoritesModel( 0 ), favoriteSelectionModel( 0 ) { actions.fill( 0, StandardActionManager::LastType ); pluralLabels.insert( StandardActionManager::CopyCollections, ki18np( "&Copy Folder", "&Copy %1 Folders" ) ); pluralLabels.insert( StandardActionManager::CopyItems, ki18np( "&Copy Item", "&Copy %1 Items" ) ); pluralLabels.insert( StandardActionManager::DeleteItems, ki18np( "&Delete Item", "&Delete %1 Items" ) ); } void enableAction( StandardActionManager::Type type, bool enable ) { Q_ASSERT( type >= 0 && type < StandardActionManager::LastType ); if ( actions[type] ) actions[type]->setEnabled( enable ); // Update the action menu KActionMenu *actionMenu = qobject_cast( actions[type] ); if ( actionMenu ) { actionMenu->menu()->clear(); if ( enable ) { fillFoldersMenu( type, actionMenu->menu(), collectionSelectionModel->model(), QModelIndex() ); } } } void updatePluralLabel( StandardActionManager::Type type, int count ) { Q_ASSERT( type >= 0 && type < StandardActionManager::LastType ); if ( actions[type] && pluralLabels.contains( type ) && !pluralLabels.value( type ).isEmpty() ) { actions[type]->setText( pluralLabels.value( type ).subs( qMax( count, 1 ) ).toString() ); } } void copy( QItemSelectionModel* selModel ) { Q_ASSERT( selModel ); if ( selModel->selectedRows().count() <= 0 ) return; QMimeData *mimeData = selModel->model()->mimeData( selModel->selectedRows() ); QApplication::clipboard()->setMimeData( mimeData ); } void updateActions() { bool singleColSelected = false; bool multiColSelected = false; int colCount = 0; QModelIndex selectedIndex; if ( collectionSelectionModel ) { colCount = collectionSelectionModel->selectedRows().count(); singleColSelected = colCount == 1; multiColSelected = colCount > 0; if ( singleColSelected ) selectedIndex = collectionSelectionModel->selectedRows().first(); } enableAction( CopyCollections, multiColSelected ); enableAction( CollectionProperties, singleColSelected ); Collection col; if ( singleColSelected && selectedIndex.isValid() ) { col = selectedIndex.data( CollectionModel::CollectionRole ).value(); enableAction( CreateCollection, canCreateCollection( col ) ); enableAction( DeleteCollections, col.rights() & Collection::CanDeleteCollection ); enableAction( CopyCollections, multiColSelected && (col != Collection::root()) ); enableAction( CollectionProperties, singleColSelected && (col != Collection::root()) ); enableAction( SynchronizeCollections, CollectionUtils::isResource( col ) || CollectionUtils::isFolder( col ) ); enableAction( Paste, PasteHelper::canPaste( QApplication::clipboard()->mimeData(), col ) ); enableAction( AddToFavoriteCollections, (favoritesModel!=0) && (!favoritesModel->collections().contains(col)) && singleColSelected && (col != Collection::root()) ); enableAction( RemoveFromFavoriteCollections, (favoritesModel!=0) && (favoritesModel->collections().contains(col)) && singleColSelected && (col != Collection::root()) ); enableAction( RenameFavoriteCollection, (favoritesModel!=0) && (favoritesModel->collections().contains(col)) && singleColSelected && (col != Collection::root()) ); enableAction( CopyCollectionToMenu, multiColSelected && (col != Collection::root()) ); } else { enableAction( CreateCollection, false ); enableAction( DeleteCollections, false ); enableAction( SynchronizeCollections, false ); enableAction( Paste, false ); enableAction( AddToFavoriteCollections, false ); enableAction( RemoveFromFavoriteCollections, false ); enableAction( RenameFavoriteCollection, false ); } bool multiItemSelected = false; + bool canDeleteItem = true; int itemCount = 0; if ( itemSelectionModel ) { - itemCount = itemSelectionModel->selectedRows().count(); + const QModelIndexList rows = itemSelectionModel->selectedRows(); + + itemCount = rows.count(); multiItemSelected = itemCount > 0; + + foreach ( const QModelIndex &itemIndex, rows ) { + const Collection parentCollection = itemIndex.data( EntityTreeModel::ParentCollectionRole ).value(); + if ( !parentCollection.isValid() ) + continue; + + canDeleteItem = canDeleteItem && (parentCollection.rights() & Collection::CanDeleteItem); + } } enableAction( CopyItems, multiItemSelected ); - const bool canDeleteItem = !col.isValid() || (col.rights() & Collection::CanDeleteItem); enableAction( DeleteItems, multiItemSelected && canDeleteItem ); enableAction( CopyItemToMenu, multiItemSelected ); updatePluralLabel( CopyCollections, colCount ); updatePluralLabel( CopyItems, itemCount ); updatePluralLabel( DeleteItems, itemCount ); emit q->actionStateUpdated(); } void clipboardChanged( QClipboard::Mode mode ) { if ( mode == QClipboard::Clipboard ) updateActions(); } QItemSelection mapToEntityTreeModel( const QAbstractItemModel *model, const QItemSelection &selection ) { const QAbstractProxyModel *proxy = qobject_cast( model ); if ( proxy ) { return mapToEntityTreeModel( proxy->sourceModel(), proxy->mapSelectionToSource( selection ) ); } else { return selection; } } QItemSelection mapFromEntityTreeModel( const QAbstractItemModel *model, const QItemSelection &selection ) { const QAbstractProxyModel *proxy = qobject_cast( model ); if ( proxy ) { QItemSelection select = mapFromEntityTreeModel( proxy->sourceModel(), selection ); return proxy->mapSelectionFromSource( select ); } else { return selection; } } void collectionSelectionChanged() { q->blockSignals(true); QItemSelection selection = collectionSelectionModel->selection(); selection = mapToEntityTreeModel( collectionSelectionModel->model(), selection ); selection = mapFromEntityTreeModel( favoritesModel, selection ); if ( favoriteSelectionModel ) favoriteSelectionModel->select( selection, QItemSelectionModel::ClearAndSelect ); q->blockSignals(false); updateActions(); } void favoriteSelectionChanged() { q->blockSignals(true); QItemSelection selection = favoriteSelectionModel->selection(); if ( selection.indexes().isEmpty() ) return; selection = mapToEntityTreeModel( favoritesModel, selection ); selection = mapFromEntityTreeModel( collectionSelectionModel->model(), selection ); collectionSelectionModel->select( selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); q->blockSignals(false); updateActions(); } void slotCreateCollection() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); if ( !canCreateCollection( collection ) ) return; const QString name = KInputDialog::getText( i18nc( "@title:window", "New Folder"), i18nc( "@label:textbox, name of a thing", "Name"), QString(), 0, parentWidget ); if ( name.isEmpty() ) return; Collection::Id parentId = index.data( CollectionModel::CollectionIdRole ).toLongLong(); if ( parentId <= 0 ) return; Collection col; col.setName( name ); col.parentCollection().setId( parentId ); CollectionCreateJob *job = new CollectionCreateJob( col ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(collectionCreationResult(KJob*)) ); } void slotCopyCollections() { copy( collectionSelectionModel ); } void slotDeleteCollection() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); QString text = i18n( "Do you really want to delete folder '%1' and all its sub-folders?", index.data().toString() ); if ( CollectionUtils::isVirtual( collection ) ) text = i18n( "Do you really want to delete the search view '%1'?", index.data().toString() ); if ( KMessageBox::questionYesNo( parentWidget, text, i18n("Delete folder?"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), QString(), KMessageBox::Dangerous ) != KMessageBox::Yes ) return; const Collection::Id colId = index.data( CollectionModel::CollectionIdRole ).toLongLong(); if ( colId <= 0 ) return; CollectionDeleteJob *job = new CollectionDeleteJob( Collection( colId ), q ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(collectionDeletionResult(KJob*)) ); } void slotSynchronizeCollection() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); AgentManager::self()->synchronizeCollection( col ); } void slotCollectionProperties() { if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); CollectionPropertiesDialog* dlg = new CollectionPropertiesDialog( col, parentWidget ); dlg->show(); } void slotCopyItems() { copy( itemSelectionModel ); } void slotPaste() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); KJob *job = PasteHelper::paste( QApplication::clipboard()->mimeData(), col ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(pasteResult(KJob*)) ); } void slotDeleteItems() { if ( KMessageBox::questionYesNo( parentWidget, i18n( "Do you really want to delete all selected items?" ), i18n("Delete?"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), QString(), KMessageBox::Dangerous ) != KMessageBox::Yes ) return; Q_ASSERT( itemSelectionModel ); // TODO: fix this once ItemModifyJob can handle item lists foreach ( const QModelIndex &index, itemSelectionModel->selectedRows() ) { bool ok; qlonglong id = index.data( ItemModel::IdRole ).toLongLong(&ok); Q_ASSERT(ok); new ItemDeleteJob( Item( id ), q ); } } void slotLocalSubscription() { SubscriptionDialog* dlg = new SubscriptionDialog( parentWidget ); dlg->show(); } void slotAddToFavorites() { Q_ASSERT( collectionSelectionModel ); Q_ASSERT( favoritesModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); favoritesModel->addCollection( collection ); } void slotRemoveFromFavorites() { Q_ASSERT( collectionSelectionModel ); Q_ASSERT( favoritesModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); favoritesModel->removeCollection( collection ); } void slotRenameFavorite() { Q_ASSERT( collectionSelectionModel ); Q_ASSERT( favoritesModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); bool ok; QString label = KInputDialog::getText( i18n( "Rename Favorite" ), i18nc( "@label:textbox New name of the folder.", "Name:" ), index.data().toString(), &ok, parentWidget ); if ( !ok ) return; favoritesModel->setFavoriteLabel( collection, label ); } void slotCopyCollectionTo( QAction *action ) { copyTo( collectionSelectionModel, action ); } void slotCopyItemTo( QAction *action ) { copyTo( itemSelectionModel, action ); } void copyTo( QItemSelectionModel *selectionModel, QAction *action ) { Q_ASSERT( selectionModel ); if ( selectionModel->selectedRows().count() <= 0 ) return; QMimeData *mimeData = selectionModel->model()->mimeData( selectionModel->selectedRows() ); Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); KJob *job = PasteHelper::paste( mimeData, col ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(copyToResult(KJob*)) ); } void collectionCreationResult( KJob *job ) { if ( job->error() ) { KMessageBox::error( parentWidget, i18n("Could not create folder: %1", job->errorString()), i18n("Folder creation failed") ); } } void collectionDeletionResult( KJob *job ) { if ( job->error() ) { KMessageBox::error( parentWidget, i18n("Could not delete folder: %1", job->errorString()), i18n("Folder deletion failed") ); } } void pasteResult( KJob *job ) { if ( job->error() ) { KMessageBox::error( parentWidget, i18n("Could not paste data: %1", job->errorString()), i18n("Paste failed") ); } } void copyToResult( KJob *job ) { if ( job->error() ) { KMessageBox::error( parentWidget, i18n("Could not copy data: %1", job->errorString()), i18n("Copy failed") ); } } void fillFoldersMenu( StandardActionManager::Type type, QMenu *menu, const QAbstractItemModel *model, QModelIndex parentIndex ) { int rowCount = model->rowCount( parentIndex ); for ( int row = 0; row < rowCount; row++ ) { QModelIndex index = model->index( row, 0, parentIndex ); Collection collection = model->data( index, CollectionModel::CollectionRole ).value(); if ( CollectionUtils::isVirtual( collection ) ) { continue; } QString label = model->data( index ).toString(); label.replace( QString::fromUtf8( "&" ), QString::fromUtf8( "&&" ) ); QIcon icon = model->data( index, Qt::DecorationRole ).value(); bool readOnly = CollectionUtils::isStructural( collection ) || ( type == CopyItemToMenu && !( collection.rights() & Collection::CanCreateItem ) ) || ( type == CopyCollectionToMenu && !( collection.rights() & Collection::CanCreateCollection ) ); if ( model->rowCount( index ) > 0 ) { // new level QMenu* popup = new QMenu( menu ); popup->setObjectName( QString::fromUtf8( "subMenu" ) ); popup->setTitle( label ); popup->setIcon( icon ); fillFoldersMenu( type, popup, model, index ); if ( !readOnly ) { popup->addSeparator(); QAction *act = popup->addAction( i18n("Copy to This Folder") ); act->setData( QVariant::fromValue( index ) ); } menu->addMenu( popup ); } else { // insert an item QAction* act = menu->addAction( icon, label ); act->setData( QVariant::fromValue( index ) ); act->setEnabled( !readOnly ); } } } void checkModelsConsistency() { if ( favoritesModel==0 || favoriteSelectionModel==0 ) { // No need to check when the favorite collections feature is not used return; } // Check that the collection selection model maps to the same // EntityTreeModel than favoritesModel if ( collectionSelectionModel!=0 ) { const QAbstractItemModel *model = collectionSelectionModel->model(); while ( const QAbstractProxyModel *proxy = qobject_cast( model ) ) { model = proxy->sourceModel(); } Q_ASSERT( model == favoritesModel->sourceModel() ); } // Check that the favorite selection model maps to favoritesModel const QAbstractItemModel *model = favoriteSelectionModel->model(); while ( const QAbstractProxyModel *proxy = qobject_cast( model ) ) { model = proxy->sourceModel(); } Q_ASSERT( model == favoritesModel->sourceModel() ); } StandardActionManager *q; KActionCollection *actionCollection; QWidget *parentWidget; QItemSelectionModel *collectionSelectionModel; QItemSelectionModel *itemSelectionModel; FavoriteCollectionsModel *favoritesModel; QItemSelectionModel *favoriteSelectionModel; QVector actions; AgentManager *agentManager; QHash pluralLabels; }; //@endcond StandardActionManager::StandardActionManager( KActionCollection * actionCollection, QWidget * parent) : QObject( parent ), d( new Private( this ) ) { d->parentWidget = parent; d->actionCollection = actionCollection; connect( QApplication::clipboard(), SIGNAL(changed(QClipboard::Mode)), SLOT(clipboardChanged(QClipboard::Mode)) ); } StandardActionManager::~ StandardActionManager() { delete d; } void StandardActionManager::setCollectionSelectionModel( QItemSelectionModel * selectionModel ) { d->collectionSelectionModel = selectionModel; connect( selectionModel, SIGNAL(selectionChanged( const QItemSelection&, const QItemSelection& )), SLOT(collectionSelectionChanged()) ); d->checkModelsConsistency(); } void StandardActionManager::setItemSelectionModel( QItemSelectionModel * selectionModel ) { d->itemSelectionModel = selectionModel; connect( selectionModel, SIGNAL(selectionChanged( const QItemSelection&, const QItemSelection& )), SLOT(updateActions()) ); } void StandardActionManager::setFavoriteCollectionsModel( FavoriteCollectionsModel *favoritesModel ) { d->favoritesModel = favoritesModel; d->checkModelsConsistency(); } void StandardActionManager::setFavoriteSelectionModel( QItemSelectionModel *selectionModel ) { d->favoriteSelectionModel = selectionModel; connect( selectionModel, SIGNAL(selectionChanged( const QItemSelection&, const QItemSelection& )), SLOT(favoriteSelectionChanged()) ); d->checkModelsConsistency(); } KAction* StandardActionManager::createAction( Type type ) { Q_ASSERT( type >= 0 && type < LastType ); Q_ASSERT( actionData[type].name ); if ( d->actions[type] ) return d->actions[type]; KAction *action; if ( !actionData[type].isActionMenu ) { action = new KAction( d->parentWidget ); } else { action = new KActionMenu( d->parentWidget ); } if ( d->pluralLabels.contains( type ) && !d->pluralLabels.value( type ).isEmpty() ) action->setText( d->pluralLabels.value( type ).subs( 1 ).toString() ); else if ( actionData[type].label ) action->setText( i18n( actionData[type].label ) ); if ( actionData[type].icon ) action->setIcon( KIcon( QString::fromLatin1( actionData[type].icon ) ) ); action->setShortcut( actionData[type].shortcut ); if ( actionData[type].slot && !actionData[type].isActionMenu ) { connect( action, SIGNAL(triggered()), actionData[type].slot ); } else if ( actionData[type].slot ) { KActionMenu *actionMenu = qobject_cast( action ); connect( actionMenu->menu(), SIGNAL(triggered(QAction*)), actionData[type].slot ); } d->actionCollection->addAction( QString::fromLatin1(actionData[type].name), action ); d->actions[type] = action; d->updateActions(); return action; } void StandardActionManager::createAllActions() { for ( int i = 0; i < LastType; ++i ) createAction( (Type)i ); } KAction * StandardActionManager::action( Type type ) const { Q_ASSERT( type >= 0 && type < LastType ); return d->actions[type]; } void StandardActionManager::setActionText( Type type, const KLocalizedString & text ) { Q_ASSERT( type >= 0 && type < LastType ); d->pluralLabels.insert( type, text ); d->updateActions(); } #include "standardactionmanager.moc" diff --git a/kpimidentities/signature.cpp b/kpimidentities/signature.cpp index 6ce71c6dc..5bd23bbe5 100644 --- a/kpimidentities/signature.cpp +++ b/kpimidentities/signature.cpp @@ -1,386 +1,549 @@ /* Copyright (c) 2002-2004 Marc Mutz Copyright (c) 2007 Tom Albers + Copyright (c) 2009 Thomas McGuire 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 "signature.h" #include #include #include #include #include #include #include #include #include +#include +#include + #include +#include +#include using namespace KPIMIdentities; +class SignaturePrivate +{ + public: + struct EmbeddedImage + { + QImage image; + QString name; + }; + typedef QSharedPointer EmbeddedImagePtr; + + /// List of images that belong to this signature. Either added by addImage() or + /// by readConfig(). + QList embeddedImages; + + /// The directory where the images will be saved to. + QString saveLocation; +}; + +QDataStream &operator<< ( QDataStream &stream, const SignaturePrivate::EmbeddedImagePtr &img ) +{ + return stream << img->image << img->name; +} + +QDataStream &operator>> ( QDataStream &stream, SignaturePrivate::EmbeddedImagePtr &img ) +{ + return stream >> img->image >> img->name; +} + +// TODO: KDE5: BIC: Add a real d-pointer. +// This QHash is just a workaround around BIC issues, for more info see +// http://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C++ +typedef QHash SigPrivateHash; +Q_GLOBAL_STATIC(SigPrivateHash, d_func) + +static SignaturePrivate* d( const Signature *sig ) +{ + SignaturePrivate *ret = d_func()->value( sig, 0 ); + if ( !ret ) { + ret = new SignaturePrivate; + d_func()->insert( sig, ret ); + } + return ret; +} + +static void delete_d( const Signature* sig ) +{ + SignaturePrivate *ret = d_func()->value( sig, 0 ); + delete ret; + d_func()->remove( sig ); +} + Signature::Signature() : mType( Disabled ), mInlinedHtml( false ) {} Signature::Signature( const QString &text ) : mText( text ), mType( Inlined ), mInlinedHtml( false ) {} Signature::Signature( const QString &url, bool isExecutable ) : mUrl( url ), mType( isExecutable ? FromCommand : FromFile ), mInlinedHtml( false ) {} +void Signature::assignFrom ( const KPIMIdentities::Signature &that ) +{ + mUrl = that.mUrl; + mInlinedHtml = that.mInlinedHtml; + mText = that.mText; + mType = that.mType; + d( this )->saveLocation = d( &that )->saveLocation; + d( this )->embeddedImages = d( &that )->embeddedImages; +} + +Signature::Signature ( const Signature &that ) +{ + assignFrom( that ); +} + +Signature& Signature::operator= ( const KPIMIdentities::Signature & that ) +{ + if ( this == &that ) + return *this; + + assignFrom( that ); + return *this; +} + +Signature::~Signature() +{ + delete_d( this ); +} + QString Signature::rawText( bool *ok ) const { switch ( mType ) { case Disabled: if ( ok ) { *ok = true; } return QString(); case Inlined: if ( ok ) { *ok = true; } return mText; case FromFile: return textFromFile( ok ); case FromCommand: return textFromCommand( ok ); }; kFatal(5325) << "Signature::type() returned unknown value!"; return QString(); // make compiler happy } QString Signature::textFromCommand( bool *ok ) const { assert( mType == FromCommand ); // handle pathological cases: if ( mUrl.isEmpty() ) { if ( ok ) { *ok = true; } return QString(); } // create a shell process: KProcess proc; proc.setOutputChannelMode( KProcess::SeparateChannels ); proc.setShellCommand( mUrl ); int rc = proc.execute(); // handle errors, if any: if ( rc != 0 ) { if ( ok ) { *ok = false; } QString wmsg = i18n( "Failed to execute signature script

%1:

" "

%2

", mUrl, QString( proc.readAllStandardError() ) ); KMessageBox::error( 0, wmsg ); return QString(); } // no errors: if ( ok ) { *ok = true; } // get output: QByteArray output = proc.readAllStandardOutput(); // TODO: hmm, should we allow other encodings, too? return QString::fromLocal8Bit( output.data(), output.size() ); } QString Signature::textFromFile( bool *ok ) const { assert( mType == FromFile ); // TODO: Use KIO::NetAccess to download non-local files! if ( !KUrl( mUrl ).isLocalFile() && !( QFileInfo( mUrl ).isRelative() && QFileInfo( mUrl ).exists() ) ) { kDebug(5325) << "Signature::textFromFile:" << "non-local URLs are unsupported"; if ( ok ) { *ok = false; } return QString(); } if ( ok ) { *ok = true; } // TODO: hmm, should we allow other encodings, too? const QByteArray ba = KPIMUtils::kFileToByteArray( mUrl, false ); return QString::fromLocal8Bit( ba.data(), ba.size() ); } QString Signature::withSeparator( bool *ok ) const { QString signature = rawText( ok ); if ( ok && (*ok) == false ) return QString(); if ( signature.isEmpty() ) { return signature; // don't add a separator in this case } QString newline = ( isInlinedHtml() && mType == Inlined ) ? "
" : "\n"; if ( signature.startsWith( QString::fromLatin1( "-- " ) + newline ) || ( signature.indexOf( newline + QString::fromLatin1( "-- " ) + newline ) != -1 ) ) { // already have signature separator at start of sig or inside sig: return signature; } else { // need to prepend one: return QString::fromLatin1( "-- " ) + newline + signature; } } void Signature::setUrl( const QString &url, bool isExecutable ) { mUrl = url; mType = isExecutable ? FromCommand : FromFile; } void Signature::setInlinedHtml( bool isHtml ) { mInlinedHtml = isHtml; } bool Signature::isInlinedHtml() const { return mInlinedHtml; } // config keys and values: static const char sigTypeKey[] = "Signature Type"; static const char sigTypeInlineValue[] = "inline"; static const char sigTypeFileValue[] = "file"; static const char sigTypeCommandValue[] = "command"; static const char sigTypeDisabledValue[] = "disabled"; static const char sigTextKey[] = "Inline Signature"; static const char sigFileKey[] = "Signature File"; static const char sigCommandKey[] = "Signature Command"; static const char sigTypeInlinedHtmlKey[] = "Inlined Html"; +static const char sigImageLocation[] = "Image Location"; void Signature::readConfig( const KConfigGroup &config ) { QString sigType = config.readEntry( sigTypeKey ); if ( sigType == sigTypeInlineValue ) { mType = Inlined; mInlinedHtml = config.readEntry( sigTypeInlinedHtmlKey, false ); } else if ( sigType == sigTypeFileValue ) { mType = FromFile; mUrl = config.readPathEntry( sigFileKey, QString() ); } else if ( sigType == sigTypeCommandValue ) { mType = FromCommand; mUrl = config.readPathEntry( sigCommandKey, QString() ); } else { mType = Disabled; } mText = config.readEntry( sigTextKey ); + d( this )->saveLocation = config.readEntry( sigImageLocation ); + + if ( isInlinedHtml() && !d( this )->saveLocation.isEmpty() ) { + QDir dir( d( this )->saveLocation ); + foreach( const QString &fileName, dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ) ) { + if ( fileName.toLower().endsWith( ".png" ) ) { + QImage image; + if ( image.load( dir.path() + '/' + fileName ) ) { + addImage( image, fileName ); + } + else { + kWarning() << "Unable to load image" << dir.path() + '/' + fileName; + } + } + } + } } void Signature::writeConfig( KConfigGroup &config ) const { switch ( mType ) { case Inlined: config.writeEntry( sigTypeKey, sigTypeInlineValue ); config.writeEntry( sigTypeInlinedHtmlKey, mInlinedHtml ); break; case FromFile: config.writeEntry( sigTypeKey, sigTypeFileValue ); config.writePathEntry( sigFileKey, mUrl ); break; case FromCommand: config.writeEntry( sigTypeKey, sigTypeCommandValue ); config.writePathEntry( sigCommandKey, mUrl ); break; case Disabled: config.writeEntry( sigTypeKey, sigTypeDisabledValue ); default: break; } config.writeEntry( sigTextKey, mText ); + config.writeEntry( sigImageLocation, d( this )->saveLocation ); + + // First delete the old image files + if ( !d( this )->saveLocation.isEmpty() ) { + QDir dir( d( this )->saveLocation ); + foreach( const QString &fileName, dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ) ) { + if ( fileName.toLower().endsWith( ".png" ) ) { + kDebug() << "Deleting old image" << dir.path() + fileName; + dir.remove( fileName ); + } + } + } + + // Then, save the new images + if ( isInlinedHtml() && !d( this )->saveLocation.isEmpty() ) { + foreach( const SignaturePrivate::EmbeddedImagePtr &image, d( this )->embeddedImages ) { + QString location = d( this )->saveLocation + '/' + image->name; + if ( !image->image.save( location, "PNG" ) ) { + kWarning() << "Failed to save image" << location; + } + } + } } void Signature::insertIntoTextEdit( KRichTextEdit *textEdit, Placement placement, bool addSeparator ) +{ + // Bah. + const_cast( this )->insertIntoTextEdit( textEdit, placement, addSeparator ); +} + +void Signature::insertIntoTextEdit( KRichTextEdit *textEdit, + Placement placement, bool addSeparator ) const { QString signature; if ( addSeparator ) signature = withSeparator(); else signature = rawText(); insertPlainSignatureIntoTextEdit( signature, textEdit, placement, ( isInlinedHtml() && type() == KPIMIdentities::Signature::Inlined ) ); + + // We added the text of the signature above, now it is time to add the images as well. + KPIMTextEdit::TextEdit *pimEdit = dynamic_cast( textEdit ); + if ( pimEdit && isInlinedHtml() ) { + foreach( const SignaturePrivate::EmbeddedImagePtr &image, d( this )->embeddedImages ) { + pimEdit->loadImage( image->image, image->name, image->name ); + } + } } void Signature::insertPlainSignatureIntoTextEdit( const QString &signature, KRichTextEdit *textEdit, Signature::Placement placement, bool isHtml ) { if ( !signature.isEmpty() ) { // Save the modified state of the document, as inserting a signature // shouldn't change this. Restore it at the end of this function. bool isModified = textEdit->document()->isModified(); // Move to the desired position, where the signature should be inserted QTextCursor cursor = textEdit->textCursor(); QTextCursor oldCursor = cursor; cursor.beginEditBlock(); if ( placement == End ) cursor.movePosition( QTextCursor::End ); else if ( placement == Start ) cursor.movePosition( QTextCursor::Start ); textEdit->setTextCursor( cursor ); // Insert the signature and newlines depending on where it was inserted. bool hackForCursorsAtEnd = false; int oldCursorPos = -1; if ( placement == End ) { if ( oldCursor.position() == textEdit->toPlainText().length() ) { hackForCursorsAtEnd = true; oldCursorPos = oldCursor.position(); } if ( isHtml ) { textEdit->insertHtml( QLatin1String( "
" ) + signature ); } else { textEdit->insertPlainText( QLatin1Char( '\n' ) + signature ); } } else if ( placement == Start || placement == AtCursor ) { if ( isHtml ) { textEdit->insertHtml( QLatin1String( "
" ) + signature + QLatin1String( "
" ) ); } else { textEdit->insertPlainText( QLatin1Char( '\n' ) + signature + QLatin1Char( '\n' ) ); } } cursor.endEditBlock(); // There is one special case when re-setting the old cursor: The cursor // was at the end. In this case, QTextEdit has no way to know // if the signature was added before or after the cursor, and just decides // that it was added before (and the cursor moves to the end, but it should // not when appending a signature). See bug 167961 if ( hackForCursorsAtEnd ) oldCursor.setPosition( oldCursorPos ); textEdit->setTextCursor( oldCursor ); textEdit->ensureCursorVisible(); textEdit->document()->setModified( isModified ); - if ( isHtml ) + if ( isHtml ) { textEdit->enableRichTextMode(); + } } } // --------------------- Operators -------------------// QDataStream &KPIMIdentities::operator<< ( QDataStream &stream, const KPIMIdentities::Signature &sig ) { - return stream << static_cast( sig.mType ) << sig.mUrl << sig.mText; + return stream << static_cast( sig.mType ) << sig.mUrl << sig.mText + << d( &sig )->saveLocation << d( &sig )->embeddedImages; } QDataStream &KPIMIdentities::operator>> ( QDataStream &stream, KPIMIdentities::Signature &sig ) { quint8 s; - stream >> s >> sig.mUrl >> sig.mText; + stream >> s >> sig.mUrl >> sig.mText >> d( &sig )->saveLocation >> d( &sig )->embeddedImages; sig.mType = static_cast( s ); return stream; } bool Signature::operator== ( const Signature &other ) const { if ( mType != other.mType ) { return false; } + if ( mType == Inlined && mInlinedHtml ) { + if ( d( this )->saveLocation != d( &other )->saveLocation ) + return false; + if ( d( this )->embeddedImages != d( &other )->embeddedImages ) + return false; + } + switch ( mType ) { case Inlined: return mText == other.mText; case FromFile: case FromCommand: return mUrl == other.mUrl; default: case Disabled: return true; } } QString Signature::plainText() const { QString sigText = rawText(); if ( isInlinedHtml() && type() == Inlined ) { // Use a QTextDocument as a helper, it does all the work for us and // strips all HTML tags. QTextDocument helper; QTextCursor helperCursor( &helper ); helperCursor.insertHtml( sigText ); sigText = helper.toPlainText(); } return sigText; } +void Signature::addImage ( const QImage& imageData, const QString& imageName ) +{ + Q_ASSERT( !( d( this )->saveLocation.isEmpty() ) ); + SignaturePrivate::EmbeddedImagePtr image( new SignaturePrivate::EmbeddedImage() ); + image->image = imageData; + image->name = imageName; + d( this )->embeddedImages.append( image ); +} + +void Signature::setImageLocation ( const QString& path ) +{ + d( this )->saveLocation = path; +} + // --------------- Getters -----------------------// QString Signature::text() const { return mText; } QString Signature::url() const { return mUrl; } Signature::Type Signature::type() const { return mType; } // --------------- Setters -----------------------// void Signature::setText( const QString &text ) { mText = text; mType = Inlined; } void Signature::setType( Type type ) { mType = type; } diff --git a/kpimidentities/signature.h b/kpimidentities/signature.h index f6aa795e4..0de5aa3dd 100644 --- a/kpimidentities/signature.h +++ b/kpimidentities/signature.h @@ -1,195 +1,265 @@ /* Copyright (c) 2002-2004 Marc Mutz Copyright (c) 2007 Tom Albers + Copyright (c) 2009 Thomas McGuire Author: Stefan Taferner 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. */ #ifndef KPIMIDENTITIES_SIGNATURE_H #define KPIMIDENTITIES_SIGNATURE_H #include "kpimidentities_export.h" #include #include #include #include #include #include namespace KPIMIdentities { class Signature; + class Identity; } class KConfigGroup; class KRichTextEdit; namespace KPIMIdentities { KPIMIDENTITIES_EXPORT QDataStream &operator<< ( QDataStream &stream, const KPIMIdentities::Signature &sig ); KPIMIDENTITIES_EXPORT QDataStream &operator>> ( QDataStream &stream, KPIMIdentities::Signature &sig ); /** - * @short abstraction of a signature (aka "footer"). - * @author Marc Mutz + * @short Abstraction of a signature (aka "footer"). + * + * The signature can either be plain text, HTML text, text returned from a command or text stored + * in a file. + * + * In case of HTML text, the signature can contain images. + * Since you set the HTML source with setText(), there also needs to be a way to add the images + * to the signature, as the HTML source contains only the img tags that reference those images. + * To add the image to the signature, call addImage(). The name given there must match the name + * of the img tag in the HTML source. + * + * The images need to be stored somewhere. The Signature class handles that by storing all images + * in a directory. You must set that directory with setImageLocation(), before calling addImage(). + * The images added with addImage() are then saved to that directory when calling writeConfig(). + * When loading a signature, readConfig() automatically loads the images as well. + * To actually add the images to a text edit, call insertIntoTextEdit(). */ class KPIMIDENTITIES_EXPORT Signature { friend class Identity; friend KPIMIDENTITIES_EXPORT QDataStream &operator<< ( QDataStream &stream, const Signature &sig ); friend KPIMIDENTITIES_EXPORT QDataStream &operator>> ( QDataStream &stream, Signature &sig ); public: /** Type of signature (ie. way to obtain the signature text) */ enum Type { Disabled = 0, Inlined = 1, FromFile = 2, FromCommand = 3 }; /** * Describes the placement of the signature text when it is to be inserted into a * text edit */ enum Placement { Start, ///< The signature is placed at the start of the textedit End, ///< The signature is placed at the end of the textedit AtCursor ///< The signature is placed at the current cursor position }; /** Used for comparison */ bool operator== ( const Signature &other ) const; /** Constructor for disabled signature */ Signature(); /** Constructor for inline text */ Signature( const QString &text ); /** Constructor for text from a file or from output of a command */ Signature( const QString &url, bool isExecutable ); + /** Copy constructor */ + Signature( const Signature &that ); + /** Assignment operator */ + Signature& operator= ( const Signature &that ); + /** Destructor */ + ~Signature(); /** @return the raw signature text as entered resp. read from file. */ QString rawText( bool *ok=0 ) const; /** @return the signature text with a "-- \n" separator added, if necessary. A newline will not be appended or prepended. */ QString withSeparator( bool *ok=0 ) const; /** Set the signature text and mark this signature as being of "inline text" type. */ void setText( const QString &text ); QString text() const; /** * Returns the text of the signature. If the signature is HTML, the HTML * tags will be stripped. * @since 4.4 */ QString plainText() const; /** Set the signature URL and mark this signature as being of "from file" resp. "from output of command" type. */ void setUrl( const QString &url, bool isExecutable=false ); QString url() const; /// @return the type of signature (ie. way to obtain the signature text) Type type() const; void setType( Type type ); /** * Sets the inlined signature to text or html * @param isHtml sets the inlined signature to html * @since 4.1 */ void setInlinedHtml( bool isHtml ); /** * @return boolean whether the inlined signature is html * @since 4.1 */ bool isInlinedHtml() const; + /** + * Sets the location where the copies of the signature images will be stored. + * The images will be stored there when calling writeConfig(). The image location + * is stored in the config, so the next readConfig() call knows where to look for + * images. + * It is recommended to use KStandardDirs::locateLocal( "data", "emailidentities/%1" ) + * for the location, where %1 is the unique identifier of the identity. + * + * @warning readConfig will delete all other PNG files in this directory, as they could + * be stale inline image files + * + * Like with addImage(), the SignatureConfigurator will handle this for you. + * + * @since 4.4 + */ + void setImageLocation( const QString &path ); + + /** + * Adds the given image to the signature. + * This is needed if you use setText() to set some HTML source that references images. Those + * referenced images needed to be added by calling this function. The @imageName has to match + * the src attribute of the img tag. + * + * If you use SignatureConfigurator, you don't need to call this function, as the configurator + * will handle this for you. + * setImageLocation() needs to be called once before. + * @since 4.4 + */ + void addImage( const QImage &image, const QString &imageName ); /** * Inserts this signature into the given text edit. * The cursor position is preserved. * A leading or trailing newline is also added automatically, depending on * the placement. * For undo/redo, this is treated as one operation. * * Rich text mode of the text edit will be enabled if the signature is in * inlined HTML format. * + * If this signature uses images, they will be added automatically. + * * @param textEdit the signature will be inserted into this text edit. * @param placement defines where in the text edit the signature should be * inserted. * @param addSeparator if true, the separator '-- \n' will be added in front * of the signature * * @since 4.3 + * TODO: KDE5: BIC: remove, as we have a const version below + * TODO: KDE5: BIC: Change from KRichTextEdit to KPIMTextEdit::TextEdit, to avoid + * the dynamic_cast used here */ void insertIntoTextEdit( KRichTextEdit *textEdit, Placement placement = End, bool addSeparator = true ); + /** + * Same as the other insertIntoTextEdit(), only that this is the const version + * @since 4.4 + */ + void insertIntoTextEdit( KRichTextEdit *textEdit, + Placement placement = End, bool addSeparator = true ) const; + /** * Inserts this given signature into the given text edit. * The cursor position is preserved. * A leading or trailing newline is also added automatically, depending on * the placement. * For undo/redo, this is treated as one operation. * A separator is not added. * * Use the insertIntoTextEdit() function if possible, as it has support * for separators and does HTML detection automatically. * * Rich text mode of the text edit will be enabled if @p isHtml is true. * * @param signature the signature, either as plain text or as HTML * @param textEdit the text edit to insert the signature into * @param placement defines where in the textedit the signature should be * inserted. * @param isHtml defines whether the signature should be inserted as text or html * * @since 4.3 */ static void insertPlainSignatureIntoTextEdit( const QString &signature, KRichTextEdit *textEdit, Placement placement = End, bool isHtml = false ); protected: void writeConfig( KConfigGroup &config ) const; void readConfig( const KConfigGroup &config ); + /** + * Helper used for the copy constructor and the assignment operator + */ + void assignFrom( const Signature &that ); + private: QString textFromFile( bool *ok ) const; QString textFromCommand( bool *ok ) const; + // TODO: KDE5: BIC: Add a d-pointer!!! + // There is already a pseude private class in the .cpp, using a hash. QString mUrl; QString mText; Type mType; bool mInlinedHtml; }; } #endif /*kpim_signature_h*/ diff --git a/kpimidentities/signatureconfigurator.cpp b/kpimidentities/signatureconfigurator.cpp index 802f3d75e..52fcda16b 100644 --- a/kpimidentities/signatureconfigurator.cpp +++ b/kpimidentities/signatureconfigurator.cpp @@ -1,418 +1,465 @@ /* -*- c++ -*- Copyright 2008 Thomas McGuire Copyright 2008 Edwin Schepers Copyright 2004 Marc Mutz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "signatureconfigurator.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KPIMIdentities; namespace KPIMIdentities { /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE -class KPIMIdentities::SignatureConfigurator::Private +class SignatureConfigurator::Private { public: + Private( SignatureConfigurator *parent ); + void init(); + + SignatureConfigurator *q; bool inlinedHtml; + HtmlImageMode imageMode; + QString imageLocation; }; //@endcond -SignatureConfigurator::SignatureConfigurator( QWidget * parent ) - : QWidget( parent ), d( new Private ) +SignatureConfigurator::Private::Private( SignatureConfigurator *parent ) + :q( parent ), imageMode( DisableHtmlImages ) +{ +} + +void SignatureConfigurator::Private::init() +{ + // tmp. vars: + QLabel * label; + QWidget * page; + QHBoxLayout * hlay; + QVBoxLayout * vlay; + QVBoxLayout * page_vlay; + + vlay = new QVBoxLayout( q ); + vlay->setObjectName( "main layout" ); + vlay->setMargin( 0 ); + + // "enable signatue" checkbox: + q->mEnableCheck = new QCheckBox( i18n("&Enable signature"), q ); + q->mEnableCheck->setWhatsThis( + i18n("Check this box if you want KMail to append a signature to mails " + "written with this identity.")); + vlay->addWidget( q->mEnableCheck ); + + // "obtain signature text from" combo and label: + hlay = new QHBoxLayout(); // inherits spacing + vlay->addLayout( hlay ); + q->mSourceCombo = new KComboBox( q ); + q->mSourceCombo->setEditable( false ); + q->mSourceCombo->setWhatsThis( + i18n("Click on the widgets below to obtain help on the input methods.")); + q->mSourceCombo->setEnabled( false ); // since !mEnableCheck->isChecked() + q->mSourceCombo->addItems( QStringList() + << i18nc("continuation of \"obtain signature text from\"", + "Input Field Below") + << i18nc("continuation of \"obtain signature text from\"", + "File") + << i18nc("continuation of \"obtain signature text from\"", + "Output of Command") + ); + label = new QLabel( i18n("Obtain signature &text from:"), q ); + label->setBuddy( q->mSourceCombo ); + label->setEnabled( false ); // since !mEnableCheck->isChecked() + hlay->addWidget( label ); + hlay->addWidget( q->mSourceCombo, 1 ); + + // widget stack that is controlled by the source combo: + QStackedWidget * widgetStack = new QStackedWidget( q ); + widgetStack->setEnabled( false ); // since !mEnableCheck->isChecked() + vlay->addWidget( widgetStack, 1 ); + q->connect( q->mSourceCombo, SIGNAL(currentIndexChanged(int)), + widgetStack, SLOT(setCurrentIndex (int)) ); + q->connect( q->mSourceCombo, SIGNAL(highlighted(int)), + widgetStack, SLOT(setCurrentIndex (int)) ); + // connects for the enabling of the widgets depending on + // signatureEnabled: + q->connect( q->mEnableCheck, SIGNAL(toggled(bool)), + q->mSourceCombo, SLOT(setEnabled(bool)) ); + q->connect( q->mEnableCheck, SIGNAL(toggled(bool)), + widgetStack, SLOT(setEnabled(bool)) ); + q->connect( q->mEnableCheck, SIGNAL(toggled(bool)), + label, SLOT(setEnabled(bool)) ); + // The focus might be still in the widget that is disabled + q->connect( q->mEnableCheck, SIGNAL(clicked()), + q->mEnableCheck, SLOT(setFocus()) ); + + int pageno = 0; + // page 0: input field for direct entering: + page = new QWidget( widgetStack ); + widgetStack->insertWidget( pageno, page ); + page_vlay = new QVBoxLayout( page ); + + q->mEditToolBar = new KToolBar( q ); + q->mEditToolBar->setToolButtonStyle( Qt::ToolButtonIconOnly ); + page_vlay->addWidget( q->mEditToolBar, 0 ); + + q->mFormatToolBar = new KToolBar( q ); + q->mFormatToolBar->setToolButtonStyle( Qt::ToolButtonIconOnly ); + page_vlay->addWidget( q->mFormatToolBar, 1 ); + + q->mTextEdit = new KPIMTextEdit::TextEdit( q ); + static_cast( q->mTextEdit )->enableImageActions(); + page_vlay->addWidget( q->mTextEdit, 2 ); + q->mTextEdit->setWhatsThis( i18n("Use this field to enter an arbitrary static signature.")); + // exclude SupportToPlainText. + q->mTextEdit->setRichTextSupport( KRichTextWidget::FullTextFormattingSupport | + KRichTextWidget::FullListSupport | + KRichTextWidget::SupportAlignment | + KRichTextWidget::SupportRuleLine | + KRichTextWidget::SupportHyperlinks | + KRichTextWidget::SupportFormatPainting ); + + // Fill the toolbars. + KActionCollection *actionCollection = new KActionCollection( q ); + q->mTextEdit->createActions( actionCollection ); + q->mEditToolBar->addAction( actionCollection->action( "format_text_bold" ) ); + q->mEditToolBar->addAction( actionCollection->action( "format_text_italic" ) ); + q->mEditToolBar->addAction( actionCollection->action( "format_text_underline" ) ); + q->mEditToolBar->addAction( actionCollection->action( "format_text_strikeout" ) ); + q->mEditToolBar->addAction( actionCollection->action( "format_text_foreground_color" ) ); + q->mEditToolBar->addAction( actionCollection->action( "format_text_background_color" ) ); + q->mEditToolBar->addAction( actionCollection->action( "format_font_family" ) ); + q->mEditToolBar->addAction( actionCollection->action( "format_font_size" ) ); + + q->mFormatToolBar->addAction( actionCollection->action( "format_list_style" ) ); + q->mFormatToolBar->addAction( actionCollection->action( "format_list_indent_more" ) ); + q->mFormatToolBar->addAction( actionCollection->action( "format_list_indent_less" ) ); + q->mFormatToolBar->addAction( actionCollection->action( "format_list_indent_less" ) ); + q->mFormatToolBar->addSeparator(); + + q->mFormatToolBar->addAction( actionCollection->action( "format_align_left" ) ); + q->mFormatToolBar->addAction( actionCollection->action( "format_align_center" ) ); + q->mFormatToolBar->addAction( actionCollection->action( "format_align_right" ) ); + q->mFormatToolBar->addAction( actionCollection->action( "format_align_justify" ) ); + q->mFormatToolBar->addSeparator(); + + q->mFormatToolBar->addAction( actionCollection->action( "insert_horizontal_rule" ) ); + q->mFormatToolBar->addAction( actionCollection->action( "manage_link" ) ); + q->mFormatToolBar->addAction( actionCollection->action( "format_painter" ) ); + + if ( imageMode == EnableHtmlImages ) { + q->mFormatToolBar->addSeparator(); + q->mFormatToolBar->addAction( actionCollection->action( "add_image" ) ); + } + + hlay = new QHBoxLayout(); // inherits spacing + page_vlay->addLayout( hlay ); + q->mHtmlCheck = new QCheckBox( i18n("&Use HTML"), page ); + q->connect( q->mHtmlCheck, SIGNAL(clicked()), + q, SLOT(slotSetHtml()) ); + hlay->addWidget( q->mHtmlCheck ); + inlinedHtml = true; + + widgetStack->setCurrentIndex( 0 ); // since mSourceCombo->currentItem() == 0 + + // page 1: "signature file" requester, label, "edit file" button: + ++pageno; + page = new QWidget( widgetStack ); + widgetStack->insertWidget( pageno, page ); // force sequential numbers (play safe) + page_vlay = new QVBoxLayout( page ); + page_vlay->setMargin( 0 ); + hlay = new QHBoxLayout(); // inherits spacing + page_vlay->addLayout( hlay ); + q->mFileRequester = new KUrlRequester( page ); + q->mFileRequester->setWhatsThis( + i18n("Use this requester to specify a text file that contains your " + "signature. It will be read every time you create a new mail or " + "append a new signature.")); + label = new QLabel( i18n("S&pecify file:"), page ); + label->setBuddy( q->mFileRequester ); + hlay->addWidget( label ); + hlay->addWidget( q->mFileRequester, 1 ); + q->mFileRequester->button()->setAutoDefault( false ); + q->connect( q->mFileRequester, SIGNAL(textChanged(const QString &)), + q, SLOT(slotEnableEditButton(const QString &)) ); + q->mEditButton = new QPushButton( i18n("Edit &File"), page ); + q->mEditButton->setWhatsThis( i18n("Opens the specified file in a text editor.")); + q->connect( q->mEditButton, SIGNAL(clicked()), + q, SLOT(slotEdit()) ); + q->mEditButton->setAutoDefault( false ); + q->mEditButton->setEnabled( false ); // initially nothing to edit + hlay->addWidget( q->mEditButton ); + page_vlay->addStretch( 1 ); // spacer + + // page 2: "signature command" requester and label: + ++pageno; + page = new QWidget( widgetStack ); + widgetStack->insertWidget( pageno,page ); + page_vlay = new QVBoxLayout( page ); + page_vlay->setMargin( 0 ); + hlay = new QHBoxLayout(); // inherits spacing + page_vlay->addLayout( hlay ); + q->mCommandEdit = new KLineEdit( page ); + q->mCommandEdit->setCompletionObject( new KShellCompletion() ); + q->mCommandEdit->setAutoDeleteCompletionObject( true ); + q->mCommandEdit->setWhatsThis( + i18n("You can add an arbitrary command here, either with or without path " + "depending on whether or not the command is in your Path. For every " + "new mail, KMail will execute the command and use what it outputs (to " + "standard output) as a signature. Usual commands for use with this " + "mechanism are \"fortune\" or \"ksig -random\".")); + label = new QLabel( i18n("S&pecify command:"), page ); + label->setBuddy( q->mCommandEdit ); + hlay->addWidget( label ); + hlay->addWidget( q->mCommandEdit, 1 ); + page_vlay->addStretch( 1 ); // spacer +} + + SignatureConfigurator::SignatureConfigurator( QWidget * parent ) + : QWidget( parent ), d( new Private( this ) ) { - // tmp. vars: - QLabel * label; - QWidget * page; - QHBoxLayout * hlay; - QVBoxLayout * vlay; - QVBoxLayout * page_vlay; - - vlay = new QVBoxLayout( this ); - vlay->setObjectName( "main layout" ); - vlay->setMargin( 0 ); - - // "enable signatue" checkbox: - mEnableCheck = new QCheckBox( i18n("&Enable signature"), this ); - mEnableCheck->setWhatsThis( - i18n("Check this box if you want KMail to append a signature to mails " - "written with this identity.")); - vlay->addWidget( mEnableCheck ); - - // "obtain signature text from" combo and label: - hlay = new QHBoxLayout(); // inherits spacing - vlay->addLayout( hlay ); - mSourceCombo = new KComboBox( this ); - mSourceCombo->setEditable( false ); - mSourceCombo->setWhatsThis( - i18n("Click on the widgets below to obtain help on the input methods.")); - mSourceCombo->setEnabled( false ); // since !mEnableCheck->isChecked() - mSourceCombo->addItems( QStringList() - << i18nc("continuation of \"obtain signature text from\"", - "Input Field Below") - << i18nc("continuation of \"obtain signature text from\"", - "File") - << i18nc("continuation of \"obtain signature text from\"", - "Output of Command") - ); - label = new QLabel( i18n("Obtain signature &text from:"), this ); - label->setBuddy( mSourceCombo ); - label->setEnabled( false ); // since !mEnableCheck->isChecked() - hlay->addWidget( label ); - hlay->addWidget( mSourceCombo, 1 ); - - // widget stack that is controlled by the source combo: - QStackedWidget * widgetStack = new QStackedWidget( this ); - widgetStack->setEnabled( false ); // since !mEnableCheck->isChecked() - vlay->addWidget( widgetStack, 1 ); - connect( mSourceCombo, SIGNAL(currentIndexChanged(int)), - widgetStack, SLOT(setCurrentIndex (int)) ); - connect( mSourceCombo, SIGNAL(highlighted(int)), - widgetStack, SLOT(setCurrentIndex (int)) ); - // connects for the enabling of the widgets depending on - // signatureEnabled: - connect( mEnableCheck, SIGNAL(toggled(bool)), - mSourceCombo, SLOT(setEnabled(bool)) ); - connect( mEnableCheck, SIGNAL(toggled(bool)), - widgetStack, SLOT(setEnabled(bool)) ); - connect( mEnableCheck, SIGNAL(toggled(bool)), - label, SLOT(setEnabled(bool)) ); - // The focus might be still in the widget that is disabled - connect( mEnableCheck, SIGNAL(clicked()), - mEnableCheck, SLOT(setFocus()) ); - - int pageno = 0; - // page 0: input field for direct entering: - page = new QWidget( widgetStack ); - widgetStack->insertWidget( pageno, page ); - page_vlay = new QVBoxLayout( page ); - - mEditToolBar = new KToolBar( this ); - mEditToolBar->setToolButtonStyle( Qt::ToolButtonIconOnly ); - page_vlay->addWidget( mEditToolBar, 0 ); - - mFormatToolBar = new KToolBar( this ); - mFormatToolBar->setToolButtonStyle( Qt::ToolButtonIconOnly ); - page_vlay->addWidget( mFormatToolBar, 1 ); - - mTextEdit = new KPIMTextEdit::TextEdit( this ); - page_vlay->addWidget( mTextEdit, 2 ); - mTextEdit->setWhatsThis( i18n("Use this field to enter an arbitrary static signature.")); - // exclude SupportToPlainText. - mTextEdit->setRichTextSupport( KRichTextWidget::FullTextFormattingSupport | - KRichTextWidget::FullListSupport | - KRichTextWidget::SupportAlignment | - KRichTextWidget::SupportRuleLine | - KRichTextWidget::SupportHyperlinks | - KRichTextWidget::SupportFormatPainting ); - - // Fill the toolbars. - KActionCollection *actionCollection = new KActionCollection(this); - mTextEdit->createActions( actionCollection ); - mEditToolBar->addAction( actionCollection->action( "format_text_bold" ) ); - mEditToolBar->addAction( actionCollection->action( "format_text_italic" ) ); - mEditToolBar->addAction( actionCollection->action( "format_text_underline" ) ); - mEditToolBar->addAction( actionCollection->action( "format_text_strikeout" ) ); - mEditToolBar->addAction( actionCollection->action( "format_text_foreground_color" ) ); - mEditToolBar->addAction( actionCollection->action( "format_text_background_color" ) ); - mEditToolBar->addAction( actionCollection->action( "format_font_family" ) ); - mEditToolBar->addAction( actionCollection->action( "format_font_size" ) ); - - mFormatToolBar->addAction( actionCollection->action( "format_list_style" ) ); - mFormatToolBar->addAction( actionCollection->action( "format_list_indent_more" ) ); - mFormatToolBar->addAction( actionCollection->action( "format_list_indent_less" ) ); - mFormatToolBar->addAction( actionCollection->action( "format_list_indent_less" ) ); - mFormatToolBar->addSeparator(); - - mFormatToolBar->addAction( actionCollection->action( "format_align_left" ) ); - mFormatToolBar->addAction( actionCollection->action( "format_align_center" ) ); - mFormatToolBar->addAction( actionCollection->action( "format_align_right" ) ); - mFormatToolBar->addAction( actionCollection->action( "format_align_justify" ) ); - mFormatToolBar->addSeparator(); - - mFormatToolBar->addAction( actionCollection->action( "insert_horizontal_rule" ) ); - mFormatToolBar->addAction( actionCollection->action( "manage_link" ) ); - mFormatToolBar->addAction( actionCollection->action( "format_painter" ) ); - - hlay = new QHBoxLayout(); // inherits spacing - page_vlay->addLayout( hlay ); - mHtmlCheck = new QCheckBox( i18n("&Use HTML"), page ); - connect( mHtmlCheck, SIGNAL(clicked()), - this, SLOT(slotSetHtml()) ); - hlay->addWidget( mHtmlCheck ); - d->inlinedHtml = true; - - widgetStack->setCurrentIndex( 0 ); // since mSourceCombo->currentItem() == 0 - - // page 1: "signature file" requester, label, "edit file" button: - ++pageno; - page = new QWidget( widgetStack ); - widgetStack->insertWidget( pageno, page ); // force sequential numbers (play safe) - page_vlay = new QVBoxLayout( page ); - page_vlay->setMargin( 0 ); - hlay = new QHBoxLayout(); // inherits spacing - page_vlay->addLayout( hlay ); - mFileRequester = new KUrlRequester( page ); - mFileRequester->setWhatsThis( - i18n("Use this requester to specify a text file that contains your " - "signature. It will be read every time you create a new mail or " - "append a new signature.")); - label = new QLabel( i18n("S&pecify file:"), page ); - label->setBuddy( mFileRequester ); - hlay->addWidget( label ); - hlay->addWidget( mFileRequester, 1 ); - mFileRequester->button()->setAutoDefault( false ); - connect( mFileRequester, SIGNAL(textChanged(const QString &)), - this, SLOT(slotEnableEditButton(const QString &)) ); - mEditButton = new QPushButton( i18n("Edit &File"), page ); - mEditButton->setWhatsThis( i18n("Opens the specified file in a text editor.")); - connect( mEditButton, SIGNAL(clicked()), SLOT(slotEdit()) ); - mEditButton->setAutoDefault( false ); - mEditButton->setEnabled( false ); // initially nothing to edit - hlay->addWidget( mEditButton ); - page_vlay->addStretch( 1 ); // spacer - - // page 2: "signature command" requester and label: - ++pageno; - page = new QWidget( widgetStack ); - widgetStack->insertWidget( pageno,page ); - page_vlay = new QVBoxLayout( page ); - page_vlay->setMargin( 0 ); - hlay = new QHBoxLayout(); // inherits spacing - page_vlay->addLayout( hlay ); - mCommandEdit = new KLineEdit( page ); - mCommandEdit->setCompletionObject( new KShellCompletion() ); - mCommandEdit->setAutoDeleteCompletionObject( true ); - mCommandEdit->setWhatsThis( - i18n("You can add an arbitrary command here, either with or without path " - "depending on whether or not the command is in your Path. For every " - "new mail, KMail will execute the command and use what it outputs (to " - "standard output) as a signature. Usual commands for use with this " - "mechanism are \"fortune\" or \"ksig -random\".")); - label = new QLabel( i18n("S&pecify command:"), page ); - label->setBuddy( mCommandEdit ); - hlay->addWidget( label ); - hlay->addWidget( mCommandEdit, 1 ); - page_vlay->addStretch( 1 ); // spacer + d->init(); + } + + SignatureConfigurator::SignatureConfigurator( HtmlImageMode imageMode, QWidget * parent ) + : QWidget( parent ), d( new Private( this ) ) + { + d->imageMode = imageMode; + d->init(); } SignatureConfigurator::~SignatureConfigurator() { delete d; } bool SignatureConfigurator::isSignatureEnabled() const { return mEnableCheck->isChecked(); } void SignatureConfigurator::setSignatureEnabled( bool enable ) { mEnableCheck->setChecked( enable ); } Signature::Type SignatureConfigurator::signatureType() const { if ( !isSignatureEnabled() ) return Signature::Disabled; switch ( mSourceCombo->currentIndex() ) { case 0: return Signature::Inlined; case 1: return Signature::FromFile; case 2: return Signature::FromCommand; default: return Signature::Disabled; } } void SignatureConfigurator::setSignatureType( Signature::Type type ) { setSignatureEnabled( type != Signature::Disabled ); int idx = 0; switch( type ) { case Signature::Inlined: idx = 0; break; case Signature::FromFile: idx = 1; break; case Signature::FromCommand: idx = 2; break; default: idx = 0; break; }; mSourceCombo->setCurrentIndex( idx ); } void SignatureConfigurator::setInlineText( const QString & text ) { mTextEdit->setTextOrHtml( text ); } QString SignatureConfigurator::fileURL() const { QString file = mFileRequester->url().path(); // Force the filename to be relative to ~ instead of $PWD depending // on the rest of the code (KRun::run in Edit and KFileItem on save) if ( !file.isEmpty() && QFileInfo( file ).isRelative() ) file = QDir::home().absolutePath() + QDir::separator() + file; return file; } void SignatureConfigurator::setFileURL( const QString & url ) { mFileRequester->setUrl( url ); } QString SignatureConfigurator::commandURL() const { return mCommandEdit->text(); } void SignatureConfigurator::setCommandURL( const QString & url ) { mCommandEdit->setText( url ); } Signature SignatureConfigurator::signature() const { Signature sig; const Signature::Type sigType = signatureType(); switch ( sigType ) { case Signature::Inlined: sig.setInlinedHtml( d->inlinedHtml ); sig.setText( d->inlinedHtml ? asCleanedHTML() : mTextEdit->textOrHtml() ); + if ( d->inlinedHtml ) { + if ( !d->imageLocation.isEmpty() ) + sig.setImageLocation( d->imageLocation ); + KPIMTextEdit::ImageWithNameList images = static_cast< KPIMTextEdit::TextEdit*>( mTextEdit )->imagesWithName(); + foreach( const KPIMTextEdit::ImageWithNamePtr &image, images ) { + sig.addImage( image->image, image->name ); + } + } break; case Signature::FromCommand: sig.setUrl( commandURL(), true ); break; case Signature::FromFile: sig.setUrl( fileURL(), false ); break; case Signature::Disabled: /* do nothing */ break; } + sig.setType( sigType ); return sig; } void SignatureConfigurator::setSignature( const Signature & sig ) { setSignatureType( sig.type() ); if ( sig.isInlinedHtml() ) mHtmlCheck->setCheckState( Qt::Checked ); else mHtmlCheck->setCheckState( Qt::Unchecked ); slotSetHtml(); - setInlineText( sig.text() ); + + // Let insertIntoTextEdit() handle setting the text, as that function also adds the images. + mTextEdit->clear(); + sig.insertIntoTextEdit( mTextEdit, KPIMIdentities::Signature::Start, false /* no seperator*/ ); if ( sig.type() == Signature::FromFile ) setFileURL( sig.url() ); else setFileURL( QString() ); if ( sig.type() == Signature::FromCommand ) setCommandURL( sig.url() ); else setCommandURL( QString() ); } void SignatureConfigurator::slotEnableEditButton( const QString & url ) { mEditButton->setDisabled( url.trimmed().isEmpty() ); } void SignatureConfigurator::slotEdit() { QString url = fileURL(); // slotEnableEditButton should prevent this assert from being hit: assert( !url.isEmpty() ); (void)KRun::runUrl( KUrl( url ), QString::fromLatin1("text/plain"), this ); } QString SignatureConfigurator::asCleanedHTML() const { QString text = mTextEdit->toHtml(); // Beautiful little hack to find the html headers produced by Qt. QTextDocument textDocument; QString html = textDocument.toHtml(); // Now remove each line from the text, the result is clean html. foreach( const QString& line, html.split( '\n' ) ){ text.remove( line + '\n' ); } return text; } // "use HTML"-checkbox (un)checked void SignatureConfigurator::slotSetHtml() { if ( mHtmlCheck->checkState() == Qt::Unchecked ) { mHtmlCheck->setText( i18n("&Use HTML") ); mEditToolBar->setVisible( false ); mEditToolBar->setEnabled( false ); mFormatToolBar->setVisible( false ); mFormatToolBar->setEnabled( false ); mTextEdit->switchToPlainText(); d->inlinedHtml = false; } else { mHtmlCheck->setText( i18n("&Use HTML (disabling removes formatting)") ); d->inlinedHtml = true; mEditToolBar->setVisible( true ); mEditToolBar->setEnabled( true ); mFormatToolBar->setVisible( true ); mFormatToolBar->setEnabled( true ); mTextEdit->enableRichTextMode(); } } + void SignatureConfigurator::setImageLocation ( const QString& path ) + { + d->imageLocation = path; + } + } #include "signatureconfigurator.moc" diff --git a/kpimidentities/signatureconfigurator.h b/kpimidentities/signatureconfigurator.h index aee9f3900..15309bcae 100644 --- a/kpimidentities/signatureconfigurator.h +++ b/kpimidentities/signatureconfigurator.h @@ -1,168 +1,193 @@ /* -*- c++ -*- Copyright 2008 Thomas McGuire Copyright 2008 Edwin Schepers Copyright 2008 Tom Albers Copyright 2004 Marc Mutz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #ifndef KPIMIDENTITIES_SIGNATURECONFIGURATOR_H #define KPIMIDENTITIES_SIGNATURECONFIGURATOR_H #include "kpimidentities_export.h" #include "signature.h" // for Signature::Type #include using KPIMIdentities::Signature; class QCheckBox; class KComboBox; class KUrlRequester; class KLineEdit; class KToolBar; class KRichTextWidget; class QString; class QPushButton; class QTextEdit; class QTextCharFormat; namespace KPIMIdentities { /** * This widget gives an interface so users can edit their signature. * You can set a signature via setSignature(), let the user edit the * signature and when done, read the signature back. */ class KPIMIDENTITIES_EXPORT SignatureConfigurator : public QWidget { Q_OBJECT public: /** * Constructor + * @deprecated: Use the other constructor + * TODO: KDE5: BIC: remove */ - SignatureConfigurator( QWidget * parent=0 ); + KDE_DEPRECATED SignatureConfigurator( QWidget * parent = 0 ); + + /// Used to decide whether to allow the user to add images or not + enum HtmlImageMode { EnableHtmlImages, DisableHtmlImages }; + + /** + * Constructor. If you want that the user sees a button to add images in the HTML toolbar, + * enable HTML images by setting @p HtmlImageMode to EnableHtmlImages. + * + * @since 4.4 + */ + explicit SignatureConfigurator( HtmlImageMode imageMode = DisableHtmlImages, QWidget * parent = 0 ); /** * destructor */ virtual ~SignatureConfigurator(); /** * Enum for the different viemodes. */ enum ViewMode { ShowCode, ShowHtml }; /** * Indicated if the user wants a signature */ bool isSignatureEnabled() const; /** * Use this to activate the signature. */ void setSignatureEnabled( bool enable ); /** * This returns the type of the signature, * so that can be Disabled, Inline, fromFile, etc. */ Signature::Type signatureType() const; /** * Set the signature type to @p type. */ void setSignatureType( Signature::Type type ); /** * Returns the inline text, only useful * when this is the appropriate Signature::Type */ QString inlineText() const; /** * Make @p text the text for the signature. */ void setInlineText( const QString & text ); /** * Returns the file url which the user wants * to use as a signature. */ QString fileURL() const; /** * Set @p url for the file url part of the * widget. */ void setFileURL( const QString & url ); /** * Returns the url of the command which the * users wants to use as signature. */ QString commandURL() const; /** * Sets @p url as the command to execute. */ void setCommandURL( const QString & url ); /** Conveniece method. @return a Signature object representing the state of the widgets. **/ Signature signature() const; /** Convenience method. Sets the widgets according to @p sig **/ void setSignature( const Signature & sig ); + /** + * Sets the directory where the images used in the HTML signature will be stored. + * Needs to be called before calling setSignature(), as each signature should use + * a different location. + * The directory needs to exist, it will not be created. + * @since 4.4 + * @sa Signature::setImageLocation + */ + void setImageLocation( const QString &path ); + private: void toggleHtmlBtnState( ViewMode state ); void initHtmlState(); // Returns the current text of the textedit as HTML code, but strips // unnecessary tags Qt inserts QString asCleanedHTML() const; protected Q_SLOTS: void slotEnableEditButton( const QString & ); void slotEdit(); void slotSetHtml(); protected: + + // TODO: KDE5: BIC: Move to private class! QCheckBox * mEnableCheck; QCheckBox * mHtmlCheck; KComboBox * mSourceCombo; KUrlRequester * mFileRequester; QPushButton * mEditButton; KLineEdit * mCommandEdit; KToolBar * mEditToolBar; KToolBar * mFormatToolBar; KRichTextWidget * mTextEdit; // Grmbl, why is this not in the private class? // This is a KPIMTextEdit::TextEdit, really. private: //@cond PRIVATE class Private; Private *const d; //@endcond }; } #endif diff --git a/kpimtextedit/textedit.cpp b/kpimtextedit/textedit.cpp index 7584da7f3..092aa71a0 100644 --- a/kpimtextedit/textedit.cpp +++ b/kpimtextedit/textedit.cpp @@ -1,666 +1,712 @@ /* Copyright (c) 2009 Thomas McGuire Based on KMail and libkdepim code by: Copyright 2007 Laurent Montel 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 "textedit.h" #include "emailquotehighlighter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KPIMTextEdit { class TextEditPrivate { public: TextEditPrivate( TextEdit *parent ) : actionAddImage( 0 ), actionDeleteLine( 0 ), q( parent ), imageSupportEnabled( false ) { } /** * Helper function for addImage(), which does the actual work of adding the QImage as a * resource to the document, pasting it and adding it to the image name list. * * @param imageName the desired image name. If it is already taken, a number will * be appended to it * @param image the actual image to add */ void addImageHelper( const QString &imageName, const QImage &image ); /** * Helper function to get the list of all QTextImageFormats in the document. */ QList embeddedImageFormats() const; /** * Removes embedded image markers, converts non-breaking spaces to normal spaces * and other fixes for strings that came from toPlainText()/toHtml(). */ void fixupTextEditString( QString &text ) const; /** * Does the constructor work */ void init(); /** * Opens a file dialog to let the user choose an image and then pastes that * image to the editor */ void _k_slotAddImage(); void _k_slotDeleteLine(); /// The action that triggers _k_slotAddImage() KAction *actionAddImage; /// The action that triggers _k_slotDeleteLine() KAction *actionDeleteLine; /// The parent class TextEdit *q; /// Whether or not adding or pasting images is supported bool imageSupportEnabled; /** * The names of embedded images. * Used to easily obtain the names of the images. * New images are compared to the the list and not added as resource if already present. */ QStringList mImageNames; /** * Although KTextEdit keeps track of the spell checking state, we override * it here, because we have a highlighter which does quote highlighting. * And since disabling spellchecking in KTextEdit simply would turn off our * quote highlighter, we never actually deactivate spell checking in the * base class, but only tell our own email highlighter to not highlight * spelling mistakes. * For this, we use the KTextEditSpellInterface, which is basically a hack * that makes it possible to have our own enabled/disabled state in a binary * compatible way. */ bool spellCheckingEnabled; }; } // namespace using namespace KPIMTextEdit; void TextEditPrivate::fixupTextEditString( QString &text ) const { // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here text.remove( QChar::LineSeparator ); // Get rid of embedded images, see QTextImageFormat documentation: // "Inline images are represented by an object replacement character (0xFFFC in Unicode) " text.remove( 0xFFFC ); // In plaintext mode, each space is non-breaking. text.replace( QChar::Nbsp, QChar::fromAscii( ' ' ) ); } TextEdit::TextEdit( const QString& text, QWidget *parent ) : KRichTextWidget( text, parent ), d( new TextEditPrivate( this ) ) { d->init(); } TextEdit::TextEdit( QWidget *parent ) : KRichTextWidget( parent ), d( new TextEditPrivate( this ) ) { d->init(); } TextEdit::~TextEdit() { } bool TextEdit::eventFilter( QObject*o, QEvent* e ) { if ( o == this ) KCursor::autoHideEventFilter( o, e ); return KRichTextWidget::eventFilter( o, e ); } void TextEditPrivate::init() { q->setSpellInterface( q ); // We tell the KRichTextWidget to enable spell checking, because only then it will // call createHighlighter() which will create our own highlighter which also // does quote highlighting. // However, *our* spellchecking is still disabled. Our own highlighter only // cares about our spellcheck status, it will not highlight missspelled words // if our spellchecking is disabled. // See also KEMailQuotingHighlighter::highlightBlock(). spellCheckingEnabled = false; q->setCheckSpellingEnabledInternal( true ); KCursor::setAutoHideCursor( q, true, true ); q->installEventFilter( q ); } void TextEdit::keyPressEvent ( QKeyEvent * e ) { if ( e->key() == Qt::Key_Return ) { QTextCursor cursor = textCursor(); int oldPos = cursor.position(); int blockPos = cursor.block().position(); //selection all the line. cursor.movePosition( QTextCursor::StartOfBlock ); cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); QString lineText = cursor.selectedText(); if ( ( ( oldPos -blockPos ) > 0 ) && ( ( oldPos-blockPos ) < int( lineText.length() ) ) ) { bool isQuotedLine = false; int bot = 0; // bot = begin of text after quote indicators while ( bot < lineText.length() ) { if( ( lineText[bot] == QChar::fromAscii( '>' ) ) || ( lineText[bot] == QChar::fromAscii( '|' ) ) ) { isQuotedLine = true; ++bot; } else if ( lineText[bot].isSpace() ) { ++bot; } else { break; } } KRichTextWidget::keyPressEvent( e ); // duplicate quote indicators of the previous line before the new // line if the line actually contained text (apart from the quote // indicators) and the cursor is behind the quote indicators if ( isQuotedLine && ( bot != lineText.length() ) && ( ( oldPos-blockPos ) >= int( bot ) ) ) { // The cursor position might have changed unpredictably if there was selected // text which got replaced by a new line, so we query it again: cursor.movePosition( QTextCursor::StartOfBlock ); cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); QString newLine = cursor.selectedText(); // remove leading white space from the new line and instead // add the quote indicators of the previous line int leadingWhiteSpaceCount = 0; while ( ( leadingWhiteSpaceCount < newLine.length() ) && newLine[leadingWhiteSpaceCount].isSpace() ) { ++leadingWhiteSpaceCount; } newLine = newLine.replace( 0, leadingWhiteSpaceCount, lineText.left( bot ) ); cursor.insertText( newLine ); //cursor.setPosition( cursor.position() + 2); cursor.movePosition( QTextCursor::StartOfBlock ); setTextCursor( cursor ); } } else KRichTextWidget::keyPressEvent( e ); } else { KRichTextWidget::keyPressEvent( e ); } } bool TextEdit::isSpellCheckingEnabled() const { return d->spellCheckingEnabled; } void TextEdit::setSpellCheckingEnabled( bool enable ) { EMailQuoteHighlighter *hlighter = dynamic_cast( highlighter() ); if ( hlighter ) hlighter->toggleSpellHighlighting( enable ); d->spellCheckingEnabled = enable; emit checkSpellingChanged( enable ); } bool TextEdit::shouldBlockBeSpellChecked( const QString& block ) const { return !isLineQuoted( block ); } bool KPIMTextEdit::TextEdit::isLineQuoted( const QString& line ) const { return quoteLength( line ) > 0; } int KPIMTextEdit::TextEdit::quoteLength( const QString& line ) const { bool quoteFound = false; int startOfText = -1; for ( int i = 0; i < line.length(); i++ ) { if ( line[i] == QLatin1Char( '>' ) || line[i] == QLatin1Char( '|' ) ) quoteFound = true; else if ( line[i] != QLatin1Char( ' ' ) ) { startOfText = i; break; } } if ( quoteFound ) { if ( startOfText == -1 ) startOfText = line.length() - 1; return startOfText; } else return 0; } const QString KPIMTextEdit::TextEdit::defaultQuoteSign() const { return QLatin1String( "> " ); } void TextEdit::createHighlighter() { EMailQuoteHighlighter *emailHighLighter = new EMailQuoteHighlighter( this ); setHighlighterColors( emailHighLighter ); //TODO change config KRichTextWidget::setHighlighter( emailHighLighter ); if ( !spellCheckingLanguage().isEmpty() ) setSpellCheckingLanguage( spellCheckingLanguage() ); setSpellCheckingEnabled( isSpellCheckingEnabled() ); } void TextEdit::setHighlighterColors( EMailQuoteHighlighter *highlighter ) { Q_UNUSED( highlighter ); } QString TextEdit::toWrappedPlainText() const { QString temp; QTextDocument* doc = document(); QTextBlock block = doc->begin(); while ( block.isValid() ) { QTextLayout* layout = block.layout(); for ( int i = 0; i < layout->lineCount(); i++ ) { QTextLine line = layout->lineAt( i ); temp += block.text().mid( line.textStart(), line.textLength() ) + QLatin1Char( '\n' ); } block = block.next(); } // Remove the last superfluous newline added above if ( temp.endsWith( QLatin1Char( '\n' ) ) ) temp.chop( 1 ); d->fixupTextEditString( temp ); return temp; } QString TextEdit::toCleanPlainText() const { QString temp = toPlainText(); d->fixupTextEditString( temp ); return temp; } void TextEdit::createActions( KActionCollection *actionCollection ) { KRichTextWidget::createActions( actionCollection ); if ( d->imageSupportEnabled ) { d->actionAddImage = new KAction( KIcon( QLatin1String( "insert-image" ) ), i18n( "Add Image" ), this ); actionCollection->addAction( QLatin1String( "add_image" ), d->actionAddImage ); connect( d->actionAddImage, SIGNAL(triggered(bool) ), SLOT( _k_slotAddImage() ) ); } d->actionDeleteLine = new KAction( i18n( "Delete Line" ), this ); d->actionDeleteLine->setShortcut( QKeySequence( Qt::CTRL + Qt::Key_K ) ); actionCollection->addAction( QLatin1String( "delete_line" ), d->actionDeleteLine ); connect( d->actionDeleteLine, SIGNAL(triggered(bool)), SLOT(_k_slotDeleteLine()) ); } void TextEdit::addImage( const KUrl &url ) { QImage image; if ( !image.load( url.path() ) ) { KMessageBox::error( this, i18nc( "@info", "Unable to load image %1.", url.path() ) ); return; } QFileInfo fi( url.path() ); QString imageName = fi.baseName().isEmpty() ? QLatin1String( "image.png" ) : fi.baseName() + QLatin1String( ".png" ); d->addImageHelper( imageName, image ); } +void TextEdit::loadImage ( const QImage& image, const QString& matchName, const QString& resourceName ) +{ + QSet cursorPositionsToSkip; + QTextBlock currentBlock = document()->begin(); + QTextBlock::iterator it; + while ( currentBlock.isValid() ) { + for (it = currentBlock.begin(); !(it.atEnd()); ++it) { + QTextFragment fragment = it.fragment(); + if ( fragment.isValid() ) { + QTextImageFormat imageFormat = fragment.charFormat().toImageFormat(); + if ( imageFormat.isValid() && imageFormat.name() == matchName ) { + int pos = fragment.position(); + if ( !cursorPositionsToSkip.contains( pos ) ) { + QTextCursor cursor( document() ); + cursor.setPosition( pos ); + cursor.setPosition( pos + 1, QTextCursor::KeepAnchor ); + cursor.removeSelectedText(); + document()->addResource( QTextDocument::ImageResource, QUrl( resourceName ), QVariant( image ) ); + cursor.insertImage( resourceName ); + + // The textfragment iterator is now invalid, restart from the beginning + // Take care not to replace the same fragment again, or we would be in an infinite loop. + cursorPositionsToSkip.insert( pos ); + it = currentBlock.begin(); + } + } + } + } + currentBlock = currentBlock.next(); + } +} + void TextEditPrivate::addImageHelper( const QString &imageName, const QImage &image ) { QString imageNameToAdd = imageName; QTextDocument *document = q->document(); // determine the imageNameToAdd int imageNumber = 1; while ( mImageNames.contains( imageNameToAdd ) ) { QVariant qv = document->resource( QTextDocument::ImageResource, QUrl( imageNameToAdd ) ); if ( qv == image ) { // use the same name break; } int firstDot = imageName.indexOf( QLatin1Char( '.' ) ); if ( firstDot == -1 ) imageNameToAdd = imageName + QString::number( imageNumber++ ); else imageNameToAdd = imageName.left( firstDot ) + QString::number( imageNumber++ ) + imageName.mid( firstDot ); } if ( !mImageNames.contains( imageNameToAdd ) ) { document->addResource( QTextDocument::ImageResource, QUrl( imageNameToAdd ), image ); mImageNames << imageNameToAdd; } q->textCursor().insertImage( imageNameToAdd ); q->enableRichTextMode(); } -QList< QSharedPointer > TextEdit::embeddedImages() const +ImageWithNameList TextEdit::imagesWithName() const { - QList< QSharedPointer > retImages; + ImageWithNameList retImages; QStringList seenImageNames; QList imageFormats = d->embeddedImageFormats(); foreach( const QTextImageFormat &imageFormat, imageFormats ) { if ( !seenImageNames.contains( imageFormat.name() ) ) { QVariant data = document()->resource( QTextDocument::ImageResource, QUrl( imageFormat.name() ) ); QImage image = qvariant_cast( data ); - QBuffer buffer; - buffer.open( QIODevice::WriteOnly ); - image.save( &buffer, "PNG" ); - - qsrand( QDateTime::currentDateTime().toTime_t() + qHash( imageFormat.name() ) ); - QSharedPointer embeddedImage( new EmbeddedImage() ); - retImages.append( embeddedImage ); - embeddedImage->image = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() ); - embeddedImage->imageName = imageFormat.name(); - embeddedImage->contentID = QString( QLatin1String( "%1@KDE" ) ).arg( qrand() ); + QString name = imageFormat.name(); + ImageWithNamePtr newImage( new ImageWithName ); + newImage->image = image; + newImage->name = name; + retImages.append( newImage ); seenImageNames.append( imageFormat.name() ); } } return retImages; } +QList< QSharedPointer > TextEdit::embeddedImages() const +{ + ImageWithNameList normalImages = imagesWithName(); + QList< QSharedPointer > retImages; + foreach( const ImageWithNamePtr &normalImage, normalImages ) { + QBuffer buffer; + buffer.open( QIODevice::WriteOnly ); + normalImage->image.save( &buffer, "PNG" ); + + qsrand( QDateTime::currentDateTime().toTime_t() + qHash( normalImage->name ) ); + QSharedPointer embeddedImage( new EmbeddedImage() ); + retImages.append( embeddedImage ); + embeddedImage->image = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() ); + embeddedImage->imageName = normalImage->name; + embeddedImage->contentID = QString( QLatin1String( "%1@KDE" ) ).arg( qrand() ); + } + return retImages; +} + QList TextEditPrivate::embeddedImageFormats() const { QTextDocument *doc = q->document(); QList retList; QTextBlock currentBlock = doc->begin(); while ( currentBlock.isValid() ) { QTextBlock::iterator it; for ( it = currentBlock.begin(); !it.atEnd(); ++it ) { QTextFragment fragment = it.fragment(); if ( fragment.isValid() ) { QTextImageFormat imageFormat = fragment.charFormat().toImageFormat(); if ( imageFormat.isValid() ) { retList.append( imageFormat ); } } } currentBlock = currentBlock.next(); } return retList; } void TextEditPrivate::_k_slotAddImage() { QPointer fdlg = new KFileDialog( QString(), QString(), q ); fdlg->setOperationMode( KFileDialog::Other ); fdlg->setCaption( i18n("Add Image") ); fdlg->okButton()->setGuiItem( KGuiItem( i18n("&Add"), QLatin1String( "document-open" ) ) ); fdlg->setMode( KFile::Files ); if ( fdlg->exec() != KDialog::Accepted ) { delete fdlg; return; } const KUrl::List files = fdlg->selectedUrls(); foreach ( const KUrl& url, files ) { q->addImage( url ); } delete fdlg; } void KPIMTextEdit::TextEdit::enableImageActions() { d->imageSupportEnabled = true; } QByteArray KPIMTextEdit::TextEdit::imageNamesToContentIds( const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList ) { QByteArray result = htmlBody; if ( imageList.size() > 0 ) { foreach( const QSharedPointer &image, imageList ) { const QString newImageName = QLatin1String( "cid:" ) + image->contentID; QByteArray quote( "\"" ); result.replace( QByteArray( quote + image->imageName.toLocal8Bit() + quote ), QByteArray( quote + newImageName.toLocal8Bit() + quote ) ); } } return result; } void TextEdit::insertFromMimeData( const QMimeData *source ) { // Add an image if that is on the clipboard if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) { QImage image = qvariant_cast( source->imageData() ); QFileInfo fi( source->text() ); QString imageName = fi.baseName().isEmpty() ? i18nc( "Start of the filename for an image", "image" ) : fi.baseName(); d->addImageHelper( imageName, image ); return; } // Attempt to paste HTML contents into the text edit in plain text mode, // prevent this and prevent plain text instead. if ( textMode() == KRichTextEdit::Plain && source->hasHtml() ) { if ( source->hasText() ) { insertPlainText( source->text() ); return; } } KRichTextWidget::insertFromMimeData( source ); } bool KPIMTextEdit::TextEdit::canInsertFromMimeData( const QMimeData *source ) const { if ( source->hasHtml() && textMode() == KRichTextEdit::Rich ) return true; if ( source->hasText() ) return true; if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) return true; return KRichTextWidget::canInsertFromMimeData( source ); } static bool isCharFormatFormatted( const QTextCharFormat &format, const QFont &defaultFont, const QTextCharFormat &defaultBlockFormat ) { if ( !format.anchorHref().isEmpty() || format.font() != defaultFont || format.isAnchor() || format.verticalAlignment() != defaultBlockFormat.verticalAlignment() || format.underlineStyle() != defaultBlockFormat.underlineStyle() || format.foreground().color() != defaultBlockFormat.foreground().color() || format.background().color() != defaultBlockFormat.background().color() ) return true; return false; } static bool isBlockFormatFormatted( const QTextBlockFormat &format, const QTextBlockFormat &defaultFormat ) { if ( format.alignment() != defaultFormat.alignment() || format.indent() != defaultFormat.indent() || format.textIndent() != defaultFormat.textIndent() ) return true; return false; } /// @return true if the format represents a list, table, image or something like that. static bool isSpecial( const QTextFormat &charFormat ) { return charFormat.isFrameFormat() || charFormat.isImageFormat() || charFormat.isListFormat() || charFormat.isTableFormat(); } bool TextEdit::isFormattingUsed() const { if ( textMode() == Plain ) return false; // Below, we walk through all text blocks and through all text fragments in them // and check if any of those has any formatting. // To check if they have formatting, we use the functions isBlockFormatFormatted() and // isCharFormatFormatted(). Those do not check all the exising formatting possibilities on // earth, but everything that KRichTextEdit supports at the moment. // // Also, we have to compare the formats against those of a default text edit. For example, // we can't compare the foreground color against black, because the user might have another // color scheme. Therefore we compare the foreground color against a default text edit. QTextEdit defaultTextEdit; QTextCharFormat defaultCharFormat = defaultTextEdit.document()->begin().charFormat(); QTextBlockFormat defaultBlockFormat = defaultTextEdit.document()->begin().blockFormat(); QFont defaultFont = document()->defaultFont(); QTextBlock block = document()->firstBlock(); while ( block.isValid() ) { if ( isBlockFormatFormatted( block.blockFormat(), defaultBlockFormat ) ) { return true; } if ( isSpecial( block.charFormat() ) || isSpecial( block.blockFormat() ) || block.textList() ) { return true; } QTextBlock::iterator it = block.begin(); while ( !it.atEnd() ) { QTextFragment fragment = it.fragment(); QTextCharFormat charFormat = fragment.charFormat(); if ( isSpecial( charFormat ) ) { return true; } if ( isCharFormatFormatted( fragment.charFormat(), defaultFont, defaultCharFormat ) ) { return true; } it++; } block = block.next(); } if ( toHtml().contains( QLatin1String( "
" ) ) ) return true; return false; } void TextEditPrivate::_k_slotDeleteLine() { q->deleteCurrentLine(); } void TextEdit::deleteCurrentLine() { QTextCursor cursor = textCursor(); QTextBlock block = cursor.block(); const QTextLayout* layout = block.layout(); // The current text block can have several lines due to word wrapping. // Search the line the cursor is in, and then delete it. for ( int lineNumber = 0; lineNumber < layout->lineCount(); lineNumber++ ) { QTextLine line = layout->lineAt( lineNumber ); const bool lastLineInBlock = ( line.textStart() + line.textLength() == block.length() - 1 ); const bool oneLineBlock = ( layout->lineCount() == 1 ); const int startOfLine = block.position() + line.textStart(); int endOfLine = block.position() + line.textStart() + line.textLength(); if ( !lastLineInBlock ) endOfLine -= 1; // Found the line where the cursor is in if ( cursor.position() >= startOfLine && cursor.position() <= endOfLine ) { int deleteStart = startOfLine; int deleteLength = line.textLength(); if ( oneLineBlock ) deleteLength++; // The trailing newline // When deleting the last line in the document, // remove the newline of the line before the last line instead if ( deleteStart + deleteLength >= document()->characterCount() && deleteStart > 0 ) deleteStart--; cursor.beginEditBlock(); cursor.setPosition( deleteStart ); cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength ); cursor.removeSelectedText(); cursor.endEditBlock(); return; } } } #include "textedit.moc" diff --git a/kpimtextedit/textedit.h b/kpimtextedit/textedit.h index 5358e689a..0100903ed 100644 --- a/kpimtextedit/textedit.h +++ b/kpimtextedit/textedit.h @@ -1,271 +1,305 @@ /* Copyright (c) 2009 Thomas McGuire Based on KMail and libkdepim code by: Copyright 2007 Laurent Montel 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. */ #ifndef KPIMTEXTEDIT_TEXTEDIT_H #define KPIMTEXTEDIT_TEXTEDIT_H #include "kpimtextedit_export.h" #include #include #include #include class KUrl; namespace KPIMTextEdit { class TextEditPrivate; class EMailQuoteHighlighter; /** - * Holds information about an embedded HTML image. + * Holds information about an embedded HTML image that will be useful for mail clients. * A list with all images can be retrieved with TextEdit::embeddedImages(). */ struct EmbeddedImage { QByteArray image; ///< The image, encoded as PNG with base64 encoding QString contentID; ///< The content id of the embedded image QString imageName; ///< Name of the image as it is available as a resource in the editor }; +/** + * Holds information about an embedded HTML image that will be generally useful. + * A list with all images can be retrieved with TextEdit::imagesWithName(). + * + * @since 4.4 + */ +struct ImageWithName +{ + QImage image; ///< The image + QString name; ///< The name of the image as it is available as a resource in the editor +}; + +typedef QSharedPointer ImageWithNamePtr; +typedef QList< ImageWithNamePtr > ImageWithNameList; typedef QList< QSharedPointer > ImageList; /** * Special textedit that provides additional features which are useful for PIM applications * like mail clients. * Additional features this class provides: * - Highlighting quoted text * - Handling of inline images * - Auto-Hiding the cursor * - Handling of pastes and drops of images * * @since 4.3 */ class KPIMTEXTEDIT_EXPORT TextEdit : public KRichTextWidget, protected KTextEditSpellInterface // TODO: KDE5: get rid of the spell interface { Q_OBJECT public: /** * Constructs a TextEdit object * @param text the initial plain text of the text edit, interpreted as HTML * @param parent the parent widget */ explicit TextEdit( const QString& text, QWidget *parent = 0 ); /** * Constructs a TextEdit object. * @param parent the parent widget */ explicit TextEdit( QWidget *parent = 0 ); /** * Calling this allows createActions() to create the add image actions. - * Call this method before callilng createActions(), otherwise the action + * Call this method before calling createActions(), otherwise the action * will not be added. * Also, if image actions is enabled, the user can paste PNG images. * * Don't call this if you don't want to support adding images. */ void enableImageActions(); /** * Destructor */ ~TextEdit(); /** * Reimplemented from KMEditor, to support more actions. * * The additional action XML names are: * - add_image * - delete_line * * The add_image actions is only added if enableImageActions() is called before. */ virtual void createActions( KActionCollection *actionCollection ); /** * Adds an image. The image is loaded from file and then pasted to the current * cursor position. * * @param url The URL of the file which contains the image */ void addImage( const KUrl &url ); + /** + * Loads an image into the textedit. The difference to addImage() is that this function expects + * that the image tag is already present in the HTML source. + * + * So what this message does is that it scans the HTML source for the image tag that matches the + * @p matchName, and then inserts the @p image as a resource, giving that resource the name + * @P resourceName. + * + * @since 4.4 + */ + void loadImage( const QImage &image, const QString &matchName, const QString &resourceName ); + /** * Deletes the line at the current cursor position. * @since 4.4 */ void deleteCurrentLine(); /** * Get a list with all embedded HTML images. * If the same image is contained twice or more in the editor, it will have only * one entry in this list. * * @return a list of embedded HTML images of the editor. */ ImageList embeddedImages() const; + /** + * Same as embeddedImages(), only that this returns a list of general purpose information, + * whereas the embeddedImages() function returns a list with mail-specific information. + * + * @since 4.4 + */ + ImageWithNameList imagesWithName() const; + /** * Returns the text of the editor as plain text, with linebreaks inserted * where word-wrapping occurred. */ QString toWrappedPlainText() const; /** * Same as toPlainText() from QTextEdit, only that it removes embedded images and * converts non-breaking space characters to normal spaces. */ QString toCleanPlainText() const; /** * This method is called after the highlighter is created. * If you use custom colors for highlighting, override this method and set the colors * to the highlighter in it. * * The default implementation does nothing, therefore the default colors of the * EMailQuoteHighlighter class will be used. * * @param highlighter the highlighter that was just created. You need to set the colors * of this highlighter. */ virtual void setHighlighterColors( EMailQuoteHighlighter *highlighter ); /** * Convenience method for qouteLength( line ) > 0 */ bool isLineQuoted( const QString &line ) const; /** * This is called whenever the editor needs to find out the length of the quote, * i.e. the length of the quote prefix before the real text starts. * The default implementation counts the number of spaces, '>' and '|' chars in * front of the line. * * @param line the line of which the length of the quote prefix should be returned * @return 0 if the line is not quoted, the length of the quote prefix otherwise * FIXME: Not yet used in all places, e.g. keypressEvent() or the quote highlighter */ virtual int quoteLength( const QString &line ) const; /** * Returns the prefix that is added to a line that is quoted. * By default, this is "> ". */ virtual const QString defaultQuoteSign() const; /** * For all given embedded images, this function replace the image name in the tag of the * HTML body with cid:content-id, * so that the HTML references the image body parts, see RFC 2557. * * This is useful when building a MIME message with inline images. * * Note that this function works on encoded content already. * * @param htmlBody the HTML code in which the tag will be modified. * The HTML code here could come from toHtml(), for example. * * @param imageList the list of images of which the tag will be modified. * You can get such a list from the embeddedImages() function. * * @return a modified HTML code, where the tags got replaced */ static QByteArray imageNamesToContentIds( const QByteArray &htmlBody, const ImageList &imageList ); /** * Checks if rich text formatting is used anywhere. * This is not the same as checking whether textMode() returns "Rich", since * that only tells that rich text mode is enabled, but not if any special formatting * is actually used. * * @return true if formatting is used anywhere */ bool isFormattingUsed() const; protected: /** * Reimplemented for inline image support */ virtual bool canInsertFromMimeData( const QMimeData *source ) const; /** * Reimplemented for inline image support */ virtual void insertFromMimeData( const QMimeData *source ); /** * Reimplemented from KRichTextWidget to hide the mouse cursor when there * was no mouse movement for some time, using KCursor */ virtual bool eventFilter( QObject*o, QEvent* e ); /** * Reimplemented to add qoute signs when the user presses enter * on a quoted line. */ virtual void keyPressEvent ( QKeyEvent * e ); // For the explaination for these four methods, see the comment at the // spellCheckingEnabled variable of the private class. /** * Reimplemented from KTextEditSpellInterface */ virtual bool isSpellCheckingEnabled() const; /** * Reimplemented from KTextEditSpellInterface */ virtual void setSpellCheckingEnabled( bool enable ); /** * Reimplemented from KTextEditSpellInterface, to avoid spellchecking * quoted text. */ virtual bool shouldBlockBeSpellChecked( const QString& block ) const; /** * Reimplemented to create our own highlighter which does quote and * spellcheck highlighting */ virtual void createHighlighter(); private: std::auto_ptr const d; friend class TextEditPrivate; Q_PRIVATE_SLOT( d, void _k_slotAddImage() ) Q_PRIVATE_SLOT( d, void _k_slotDeleteLine() ) }; } // namespace #endif