diff --git a/MAINTAINERS b/MAINTAINERS index 52db74f78..d3c311ef5 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1,29 +1,28 @@ akonadi Volker Krause gpgme++ Marc Mutz kabc Cornelius Schumacher kblog Christian Weilbach kcal Cornelius Schumacher Reinhold Kainhofer kcal/libical Allen Winter Cornelius Schumacher Reinhold Kainhofer kholidays Allen Winter kimap Allen Winter - Tom Albers Volker Krause kioslave various kldap Szombathelyi György kmime Volker Krause Marc Mutz -kpimidenties Tom Albers +kpimidenties - kpimutils Matt Douhan kresources Cornelius Schumacher Tobias Koenig ktnef Michael Goffioul kxmlrpcclient Narayan Newton -mailtransport Tom Albers +mailtransport - microblog Tom Albers qgpgme Marc Mutz syndication Frank Osterfeld kpimtextedit Thomas McGuire Stephen Kelly diff --git a/akonadi/CMakeLists.txt b/akonadi/CMakeLists.txt index 61897ea3f..7c2cb65f8 100644 --- a/akonadi/CMakeLists.txt +++ b/akonadi/CMakeLists.txt @@ -1,244 +1,264 @@ project(akonadi-kde) +add_definitions( -DKDE_DEFAULT_DEBUG_AREA=5250 ) + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}" ) if(CMAKE_COMPILE_GCOV) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") endif(CMAKE_COMPILE_GCOV) if (KDE4_BUILD_TESTS) # only with this macro the AKONADI_TESTS_EXPORT macro will do something add_definitions(-DCOMPILING_TESTS) add_subdirectory( tests ) endif (KDE4_BUILD_TESTS) add_definitions( -DQT_NO_CAST_FROM_ASCII ) add_definitions( -DQT_NO_CAST_TO_ASCII ) add_subdirectory( kabc ) add_subdirectory( kmime ) include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${QT_QTDBUS_INCLUDE_DIR} ${Boost_INCLUDE_DIR} ${KDE4_INCLUDE_DIR} ${AKONADI_INCLUDE_DIR} ${AKONADI_INCLUDE_DIR}/akonadi/private ) # libakonadi-kde set( akonadikde_LIB_SRC entity.cpp # keep it at top to not break enable-final agentbase.cpp agentfilterproxymodel.cpp agentinstance.cpp agentinstancecreatejob.cpp agentinstancemodel.cpp agentinstancewidget.cpp agentmanager.cpp agenttype.cpp agenttypemodel.cpp agenttypewidget.cpp agenttypedialog.cpp attribute.cpp attributefactory.cpp cachepolicy.cpp cachepolicypage.cpp changerecorder.cpp collection.cpp collectioncopyjob.cpp collectioncreatejob.cpp collectiondeletejob.cpp collectiondialog.cpp collectionfilterproxymodel.cpp collectiongeneralpropertiespage.cpp collectionfetchjob.cpp collectionmodel.cpp collectionmodel_p.cpp collectionmodifyjob.cpp collectionmovejob.cpp collectionpathresolver.cpp collectionpropertiesdialog.cpp collectionpropertiespage.cpp collectionrequester.cpp collectionrightsattribute.cpp collectionselectjob.cpp collectionstatistics.cpp collectionstatisticsdelegate.cpp collectionstatisticsjob.cpp collectionstatisticsmodel.cpp collectionsync.cpp collectionview.cpp control.cpp + descendantsproxymodel.cpp entitydisplayattribute.cpp - #entitysortfilterproxymodel.cpp + entitytreemodel.cpp + entitytreemodel_p.cpp + entityfilterproxymodel.cpp + entitytreeviewstatesaver.cpp erroroverlay.cpp exception.cpp expungejob.cpp + favoritecollectionsmodel.cpp firstrun.cpp flatcollectionproxymodel.cpp item.cpp itemcreatejob.cpp itemcopyjob.cpp itemdeletejob.cpp itemfetchjob.cpp itemfetchscope.cpp itemmodel.cpp itemmonitor.cpp itemmovejob.cpp itemserializer.cpp itemserializerplugin.cpp itemmodifyjob.cpp itemsync.cpp itemview.cpp job.cpp linkjob.cpp filteractionjob.cpp mimetypechecker.cpp monitor.cpp monitor_p.cpp pastehelper.cpp + preprocessorbase.cpp protocolhelper.cpp resourcebase.cpp resourcescheduler.cpp resourceselectjob.cpp searchcreatejob.cpp + selectionproxymodel.cpp selftestdialog.cpp session.cpp servermanager.cpp standardactionmanager.cpp + statisticsproxymodel.cpp + statisticstooltipproxymodel.cpp subscriptionjob.cpp subscriptionchangeproxymodel.cpp subscriptiondialog.cpp subscriptionmodel.cpp transactionjobs.cpp transactionsequence.cpp transportresourcebase.cpp unlinkjob.cpp # Temporary until ported to Qt-plugin framework pluginloader.cpp ) # DBus interfaces and adaptors set(akonadi_xml ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.NotificationManager.xml) set_source_files_properties(${akonadi_xml} PROPERTIES INCLUDE "notificationmessage_p.h") qt4_add_dbus_interface( akonadikde_LIB_SRC ${akonadi_xml} notificationmanagerinterface ) qt4_add_dbus_interfaces( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.AgentManager.xml ) qt4_add_dbus_interfaces( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Tracer.xml ) qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Resource.xml resourcebase.h Akonadi::ResourceBase ) +qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Preprocessor.xml preprocessorbase.h Akonadi::PreprocessorBase ) qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Agent.Status.xml agentbase.h Akonadi::AgentBase ) # TODO move this to kdesupport/akonadi/interfaces qt4_add_dbus_adaptor( akonadikde_LIB_SRC interfaces/org.freedesktop.Akonadi.Resource.Transport.xml transportresourcebase_p.h Akonadi::TransportResourceBasePrivate ) # TODO merge with Akonadi.Control in kdesupport/akonadi/interfaces qt4_add_dbus_adaptor( akonadikde_LIB_SRC interfaces/org.freedesktop.Akonadi.Agent.Control.xml agentbase.h Akonadi::AgentBase ) #qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Agent.Control.xml agentbase.h Akonadi::AgentBase ) kde4_add_ui_files( akonadikde_LIB_SRC cachepolicypage.ui collectiongeneralpropertiespage.ui subscriptiondialog.ui controlprogressindicator.ui selftestdialog.ui ) kde4_add_library( akonadi-kde SHARED ${akonadikde_LIB_SRC} ) macro_ensure_version( "4.2.0" ${KDE_VERSION} KDE_IS_AT_LEAST_42 ) target_link_libraries( akonadi-kde ${KDE4_SOLID_LIBS} ${QT_QTNETWORK_LIBRARY} ${QT_QTDBUS_LIBRARY} ${QT_QTSQL_LIBRARY} ${KDE4_KDEUI_LIBS} ${KDE4_KIO_LIBS} ${AKONADI_COMMON_LIBRARIES} ) set( AKONADI_KDE_DEPS ${KDE4_KDEUI_LIBS} ${QT_QTDBUS_LIBRARY} ${QT_QTCORE_LIBRARY} ) if(${KDE_IS_AT_LEAST_42}) target_link_libraries( akonadi-kde LINK_INTERFACE_LIBRARIES ${AKONADI_KDE_DEPS}) else(${KDE_IS_AT_LEAST_42}) target_link_libraries( akonadi-kde ${AKONADI_KDE_DEPS}) endif(${KDE_IS_AT_LEAST_42}) set_target_properties( akonadi-kde PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION} ) install( TARGETS akonadi-kde EXPORT kdepimlibsLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS} ) ########### install files ############### install( FILES akonadi_export.h agentbase.h agentfilterproxymodel.h agentinstance.h agentinstancecreatejob.h agentinstancemodel.h agentinstancewidget.h agentmanager.h agenttype.h agenttypemodel.h agenttypewidget.h agenttypedialog.h attribute.h attributefactory.h cachepolicy.h changerecorder.h collection.h collectioncopyjob.h collectioncreatejob.h collectiondeletejob.h collectiondialog.h collectionfilterproxymodel.h collectionfetchjob.h collectionmodel.h collectionmodifyjob.h collectionpropertiesdialog.h collectionpropertiespage.h collectionrequester.h collectionstatisticsdelegate.h collectionstatisticsmodel.h collectionstatistics.h collectionstatisticsjob.h collectionview.h control.h + descendantsproxymodel.h entity.h entitydisplayattribute.h - # entitysortfilterproxymodel.h + entitytreemodel.h + entityfilterproxymodel.h + entitytreeviewstatesaver.h exception.h + favoritecollectionsmodel.h item.h itemcreatejob.h itemcopyjob.h itemdeletejob.h itemfetchjob.h itemfetchscope.h itemmodel.h itemmodifyjob.h itemmonitor.h itemmovejob.h itempayloadinternals_p.h itemserializerplugin.h itemsync.h itemview.h job.h linkjob.h filteractionjob.h mimetypechecker.h monitor.h qtest_akonadi.h + preprocessorbase.h resourcebase.h searchcreatejob.h + selectionproxymodel.h session.h servermanager.h standardactionmanager.h + statisticsproxymodel.h + statisticstooltipproxymodel.h transactionjobs.h transactionsequence.h transportresourcebase.h unlinkjob.h DESTINATION ${INCLUDE_INSTALL_DIR}/akonadi COMPONENT Devel ) install( FILES collectionpathresolver_p.h DESTINATION ${INCLUDE_INSTALL_DIR}/akonadi/private COMPONENT Devel ) install( FILES kcfg2dbus.xsl DESTINATION ${DATA_INSTALL_DIR}/akonadi-kde ) diff --git a/akonadi/agentbase.cpp b/akonadi/agentbase.cpp index 3aeece8c2..6d6aea36b 100644 --- a/akonadi/agentbase.cpp +++ b/akonadi/agentbase.cpp @@ -1,602 +1,602 @@ /* 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(); } //@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( 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; 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 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 ); } } 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::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( 5250 ) << "Not enough arguments passed..."; + 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( 5250 ) << "Identifier argument missing"; + 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/collectionfetchjob.cpp b/akonadi/collectionfetchjob.cpp index 2631fa2aa..579997d4a 100644 --- a/akonadi/collectionfetchjob.cpp +++ b/akonadi/collectionfetchjob.cpp @@ -1,241 +1,241 @@ /* Copyright (c) 2006 - 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 "collectionfetchjob.h" #include "imapparser_p.h" #include "job_p.h" #include "protocol_p.h" #include "protocolhelper_p.h" #include "entity_p.h" #include #include #include #include using namespace Akonadi; class Akonadi::CollectionFetchJobPrivate : public JobPrivate { public: CollectionFetchJobPrivate( CollectionFetchJob *parent ) : JobPrivate( parent ), mUnsubscribed( false ), mStatistics( false ) { } Q_DECLARE_PUBLIC( CollectionFetchJob ) CollectionFetchJob::Type mType; Collection mBase; Collection::List mBaseList; Collection::List mCollections; QString mResource; QList mMimeTypes; Collection::List mPendingCollections; QTimer *mEmitTimer; bool mUnsubscribed; bool mStatistics; void timeout() { Q_Q( CollectionFetchJob ); mEmitTimer->stop(); // in case we are called by result() if ( !mPendingCollections.isEmpty() ) { emit q->collectionsReceived( mPendingCollections ); mPendingCollections.clear(); } } }; CollectionFetchJob::CollectionFetchJob( const Collection &collection, Type type, QObject *parent ) : Job( new CollectionFetchJobPrivate( this ), parent ) { Q_D( CollectionFetchJob ); d->mBase = collection; d->mType = type; d->mEmitTimer = new QTimer( this ); d->mEmitTimer->setSingleShot( true ); d->mEmitTimer->setInterval( 100 ); connect( d->mEmitTimer, SIGNAL(timeout()), this, SLOT(timeout()) ); connect( this, SIGNAL(result(KJob*)), this, SLOT(timeout()) ); } CollectionFetchJob::CollectionFetchJob( const Collection::List & cols, QObject * parent ) : Job( new CollectionFetchJobPrivate( this ), parent ) { Q_D( CollectionFetchJob ); Q_ASSERT( !cols.isEmpty() ); if ( cols.size() == 1 ) { d->mBase = cols.first(); d->mType = CollectionFetchJob::Base; } else { d->mBaseList = cols; } d->mEmitTimer = new QTimer( this ); d->mEmitTimer->setSingleShot( true ); d->mEmitTimer->setInterval( 100 ); connect( d->mEmitTimer, SIGNAL(timeout()), this, SLOT(timeout()) ); connect( this, SIGNAL(result(KJob*)), this, SLOT(timeout()) ); } CollectionFetchJob::~CollectionFetchJob() { } Collection::List CollectionFetchJob::collections() const { Q_D( const CollectionFetchJob ); return d->mCollections; } void CollectionFetchJob::doStart() { Q_D( CollectionFetchJob ); if ( !d->mBaseList.isEmpty() ) { foreach ( const Collection &col, d->mBaseList ) { new CollectionFetchJob( col, CollectionFetchJob::Base, this ); } return; } if ( !d->mBase.isValid() && d->mBase.remoteId().isEmpty() ) { setError( Unknown ); setErrorText( QLatin1String( "Invalid collection given." ) ); emitResult(); return; } QByteArray command = d->newTag(); if ( !d->mBase.isValid() ) command += " " AKONADI_CMD_RID; if ( d->mUnsubscribed ) command += " X-AKLIST "; else command += " X-AKLSUB "; if ( d->mBase.isValid() ) command += QByteArray::number( d->mBase.id() ); else command += ImapParser::quote( d->mBase.remoteId().toUtf8() ); command += ' '; switch ( d->mType ) { case Base: command += "0 ("; break; case FirstLevel: command += "1 ("; break; case Recursive: command += "INF ("; break; default: Q_ASSERT( false ); } if ( !d->mResource.isEmpty() ) { command += "RESOURCE \""; command += d->mResource.toUtf8(); command += '"'; } if ( !d->mMimeTypes.isEmpty() ) { command += " MIMETYPE ("; command += ImapParser::join( d->mMimeTypes, " " ); command += ')'; } if ( d->mStatistics ) { command += ") (STATISTICS true"; } command += ")\n"; d->writeData( command ); } void CollectionFetchJob::doHandleResponse( const QByteArray & tag, const QByteArray & data ) { Q_D( CollectionFetchJob ); if ( tag == "*" ) { Collection collection; ProtocolHelper::parseCollection( data, collection ); if ( !collection.isValid() ) return; collection.d_ptr->resetChangeLog(); d->mCollections.append( collection ); d->mPendingCollections.append( collection ); if ( !d->mEmitTimer->isActive() ) d->mEmitTimer->start(); return; } - kDebug( 5250 ) << "Unhandled server response" << tag << data; + kDebug() << "Unhandled server response" << tag << data; } void CollectionFetchJob::setResource(const QString & resource) { Q_D( CollectionFetchJob ); d->mResource = resource; } void CollectionFetchJob::setContentMimeTypes( const QStringList &contentMimeTypes ) { Q_D( CollectionFetchJob ); foreach ( const QString &mt, contentMimeTypes ) d->mMimeTypes.append( mt.toUtf8() ); } void CollectionFetchJob::slotResult(KJob * job) { Q_D( CollectionFetchJob ); CollectionFetchJob *list = dynamic_cast( job ); Q_ASSERT( job ); d->mCollections += list->collections(); Job::slotResult( job ); if ( !job->error() && !hasSubjobs() ) emitResult(); } void CollectionFetchJob::includeUnsubscribed(bool include) { Q_D( CollectionFetchJob ); d->mUnsubscribed = include; } void CollectionFetchJob::includeStatistics(bool include) { Q_D( CollectionFetchJob ); d->mStatistics = include; } #include "collectionfetchjob.moc" diff --git a/akonadi/collectiongeneralpropertiespage.cpp b/akonadi/collectiongeneralpropertiespage.cpp index 31a121c8d..ad66a9553 100644 --- a/akonadi/collectiongeneralpropertiespage.cpp +++ b/akonadi/collectiongeneralpropertiespage.cpp @@ -1,84 +1,85 @@ /* 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 "collectiongeneralpropertiespage_p.h" #include "collection.h" #include "entitydisplayattribute.h" #include "collectionstatistics.h" #include "collectionutils_p.h" #include using namespace Akonadi; //@cond PRIVATE CollectionGeneralPropertiesPage::CollectionGeneralPropertiesPage(QWidget * parent) : CollectionPropertiesPage( parent ) { setPageTitle( i18nc( "@title:tab general properties page", "General" ) ); ui.setupUi( this ); } void CollectionGeneralPropertiesPage::load(const Collection & collection) { QString displayName; QString iconName; if ( collection.hasAttribute() ) { displayName = collection.attribute()->displayName(); iconName = collection.attribute()->iconName(); } if ( displayName.isEmpty() ) ui.nameEdit->setText( collection.name() ); else ui.nameEdit->setText( displayName ); if ( iconName.isEmpty() ) ui.customIcon->setIcon( CollectionUtils::defaultIconName( collection ) ); else ui.customIcon->setIcon( iconName ); ui.customIconCheckbox->setChecked( !iconName.isEmpty() ); if ( collection.statistics().count() >= 0 ) { ui.countLabel->setText( i18ncp( "@label", "One object", "%1 objects", collection.statistics().count() ) ); + ui.sizeLabel->setText( KGlobal::locale()->formatByteSize( collection.statistics().size() ) ); } else { ui.statsBox->hide(); } } void CollectionGeneralPropertiesPage::save(Collection & collection) { if ( collection.hasAttribute() && !collection.attribute()->displayName().isEmpty() ) collection.attribute()->setDisplayName( ui.nameEdit->text() ); else collection.setName( ui.nameEdit->text() ); if ( ui.customIconCheckbox->isChecked() ) collection.attribute( Collection::AddIfMissing )->setIconName( ui.customIcon->icon() ); else if ( collection.hasAttribute() ) collection.attribute()->setIconName( QString() ); } //@endcond #include "collectiongeneralpropertiespage_p.moc" diff --git a/akonadi/collectionmodel_p.cpp b/akonadi/collectionmodel_p.cpp index 008ac2b48..7e7a896f5 100644 --- a/akonadi/collectionmodel_p.cpp +++ b/akonadi/collectionmodel_p.cpp @@ -1,334 +1,334 @@ /* Copyright (c) 2006 - 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. */ //@cond PRIVATE #include "collectionmodel_p.h" #include "collectionmodel.h" #include "collectionutils_p.h" #include "collectionfetchjob.h" #include "collectionstatistics.h" #include "collectionstatisticsjob.h" #include "monitor.h" #include "session.h" #include #include #include #include #include using namespace Akonadi; void CollectionModelPrivate::collectionRemoved( const Akonadi::Collection &collection ) { Q_Q( CollectionModel ); QModelIndex colIndex = indexForId( collection.id() ); if ( colIndex.isValid() ) { QModelIndex parentIndex = q->parent( colIndex ); // collection is still somewhere in the hierarchy removeRowFromModel( colIndex.row(), parentIndex ); } else { if ( collections.contains( collection.id() ) ) { // collection is orphan, ie. the parent has been removed already collections.remove( collection.id() ); childCollections.remove( collection.id() ); } } } void CollectionModelPrivate::collectionChanged( const Akonadi::Collection &collection ) { Q_Q( CollectionModel ); // What kind of change is it ? Collection::Id oldParentId = collections.value( collection.id() ).parent(); Collection::Id newParentId = collection.parent(); if ( newParentId != oldParentId && oldParentId >= 0 ) { // It's a move removeRowFromModel( indexForId( collections[ collection.id() ].id() ).row(), indexForId( oldParentId ) ); Collection newParent; if ( newParentId == Collection::root().id() ) newParent = Collection::root(); else newParent = collections.value( newParentId ); CollectionFetchJob *job = new CollectionFetchJob( newParent, CollectionFetchJob::Recursive, session ); job->includeUnsubscribed( unsubscribed ); job->includeStatistics( fetchStatistics ); q->connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(collectionsChanged(Akonadi::Collection::List)) ); q->connect( job, SIGNAL( result( KJob* ) ), q, SLOT( listDone( KJob* ) ) ); } else { // It's a simple change CollectionFetchJob *job = new CollectionFetchJob( collection, CollectionFetchJob::Base, session ); job->includeUnsubscribed( unsubscribed ); job->includeStatistics( fetchStatistics ); q->connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(collectionsChanged(Akonadi::Collection::List)) ); q->connect( job, SIGNAL( result( KJob* ) ), q, SLOT( listDone( KJob* ) ) ); } } void CollectionModelPrivate::updateDone( KJob *job ) { if ( job->error() ) { // TODO: handle job errors - kWarning( 5250 ) << "Job error:" << job->errorString(); + kWarning() << "Job error:" << job->errorString(); } else { CollectionStatisticsJob *csjob = static_cast( job ); Collection result = csjob->collection(); collectionStatisticsChanged( result.id(), csjob->statistics() ); } } void CollectionModelPrivate::collectionStatisticsChanged( Collection::Id collection, const Akonadi::CollectionStatistics &statistics ) { Q_Q( CollectionModel ); if ( !collections.contains( collection ) ) - kWarning( 5250 ) << "Got statistics response for non-existing collection:" << collection; + kWarning() << "Got statistics response for non-existing collection:" << collection; else { collections[ collection ].setStatistics( statistics ); Collection col = collections.value( collection ); QModelIndex startIndex = indexForId( col.id() ); QModelIndex endIndex = indexForId( col.id(), q->columnCount( q->parent( startIndex ) ) - 1 ); emit q->dataChanged( startIndex, endIndex ); } } void CollectionModelPrivate::listDone( KJob *job ) { Q_Q( CollectionModel ); if ( job->error() ) { - kWarning( 5250 ) << "Job error: " << job->errorString() << endl; + kWarning() << "Job error: " << job->errorString() << endl; } emit q->listDone(); } void CollectionModelPrivate::editDone( KJob * job ) { if ( job->error() ) { - kWarning( 5250 ) << "Edit failed: " << job->errorString(); + kWarning() << "Edit failed: " << job->errorString(); } } void CollectionModelPrivate::dropResult(KJob * job) { if ( job->error() ) { - kWarning( 5250 ) << "Paste failed:" << job->errorString(); + kWarning() << "Paste failed:" << job->errorString(); // TODO: error handling } } void CollectionModelPrivate::collectionsChanged( const Collection::List &cols ) { Q_Q( CollectionModel ); foreach ( Collection col, cols ) { //krazy:exclude=foreach non-const is needed here if ( collections.contains( col.id() ) ) { // If the collection is already known to the model, we simply update it... col.setStatistics( collections.value( col.id() ).statistics() ); collections[ col.id() ] = col; QModelIndex startIndex = indexForId( col.id() ); QModelIndex endIndex = indexForId( col.id(), q->columnCount( q->parent( startIndex ) ) - 1 ); emit q->dataChanged( startIndex, endIndex ); continue; } // ... otherwise we add it to the set of collections we need to handle. m_newChildCollections[ col.parent() ].append( col.id() ); m_newCollections.insert( col.id(), col ); } // Handle the collections in m_newChildCollections. If the collections // parent is already in the model, the collection can be added to the model. // Otherwise it is persisted until it has a valid parent in the model. int currentSize = m_newChildCollections.size(); int lastSize = -1; while ( currentSize > 0 ) { lastSize = currentSize; QMutableHashIterator< Collection::Id, QList< Collection::Id > > i( m_newChildCollections ); while ( i.hasNext() ) { i.next(); // the key is the parent of new collections. It may itself also be new, // but that will be handled later. Collection::Id colId = i.key(); QList< Collection::Id > newChildCols = i.value(); int newChildCount = newChildCols.size(); // if ( newChildCount == 0 ) // { // // Sanity check. // kDebug() << "No new child collections have been added to the collection:" << colId; // i.remove(); // currentSize--; // break; // } if ( collections.contains( colId ) || colId == Collection::root().id() ) { QModelIndex parentIndex = indexForId( colId ); int currentChildCount = childCollections.value( colId ).size(); q->beginInsertRows( parentIndex, currentChildCount, // Start index is at the end of existing collections. currentChildCount + newChildCount - 1 ); // End index is the result of the insertion. foreach( Collection::Id id, newChildCols ) { Collection c = m_newCollections.take( id ); collections.insert( id, c ); } childCollections[ colId ] << newChildCols; q->endInsertRows(); i.remove(); currentSize--; break; } } // We iterated through once without adding any more collections to the model. if ( currentSize == lastSize ) { // The remaining collections in the list do not have a valid parent in the model yet. They // might arrive in the next batch from the monitor, so they're still in m_newCollections // and m_newChildCollections. kDebug() << "Some collections did not have a parent in the model yet!"; break; } } } QModelIndex CollectionModelPrivate::indexForId( Collection::Id id, int column ) const { Q_Q( const CollectionModel ); if ( !collections.contains( id ) ) return QModelIndex(); Collection::Id parentId = collections.value( id ).parent(); // check if parent still exist or if this is an orphan collection if ( parentId != Collection::root().id() && !collections.contains( parentId ) ) return QModelIndex(); QList list = childCollections.value( parentId ); int row = list.indexOf( id ); if ( row >= 0 ) return q->createIndex( row, column, reinterpret_cast( collections.value( list.at(row) ).id() ) ); return QModelIndex(); } bool CollectionModelPrivate::removeRowFromModel( int row, const QModelIndex & parent ) { Q_Q( CollectionModel ); QList list; Collection parentCol; if ( parent.isValid() ) { parentCol = collections.value( parent.internalId() ); Q_ASSERT( parentCol.id() == parent.internalId() ); list = childCollections.value( parentCol.id() ); } else { parentCol = Collection::root(); list = childCollections.value( Collection::root().id() ); } if ( row < 0 || row >= list.size() ) { - kWarning( 5250 ) << "Index out of bounds:" << row <<" parent:" << parentCol.id(); + kWarning() << "Index out of bounds:" << row <<" parent:" << parentCol.id(); return false; } q->beginRemoveRows( parent, row, row ); Collection::Id delColId = list.takeAt( row ); foreach( Collection::Id childColId, childCollections[ delColId ] ) collections.remove( childColId ); collections.remove( delColId ); childCollections.remove( delColId ); // remove children of deleted collection childCollections.insert( parentCol.id(), list ); // update children of parent q->endRemoveRows(); return true; } bool CollectionModelPrivate::supportsContentType(const QModelIndex & index, const QStringList & contentTypes) { if ( !index.isValid() ) return false; Collection col = collections.value( index.internalId() ); Q_ASSERT( col.isValid() ); QStringList ct = col.contentMimeTypes(); foreach ( const QString &a, ct ) { if ( contentTypes.contains( a ) ) return true; } return false; } void CollectionModelPrivate::init() { Q_Q( CollectionModel ); session = new Session( QCoreApplication::instance()->applicationName().toUtf8() + QByteArray("-CollectionModel-") + QByteArray::number( qrand() ), q ); QTimer::singleShot( 0, q, SLOT(startFirstListJob()) ); // monitor collection changes monitor = new Monitor(); monitor->setCollectionMonitored( Collection::root() ); monitor->fetchCollection( true ); // ### Hack to get the kmail resource folder icons KIconLoader::global()->addAppDir( QLatin1String( "kmail" ) ); KIconLoader::global()->addAppDir( QLatin1String( "kdepim" ) ); // monitor collection changes q->connect( monitor, SIGNAL(collectionChanged(const Akonadi::Collection&)), q, SLOT(collectionChanged(const Akonadi::Collection&)) ); q->connect( monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)), q, SLOT(collectionChanged(Akonadi::Collection)) ); q->connect( monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), q, SLOT(collectionRemoved(Akonadi::Collection)) ); q->connect( monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)), q, SLOT(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)) ); } void CollectionModelPrivate::startFirstListJob() { Q_Q( CollectionModel ); // start a list job CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, session ); job->includeUnsubscribed( unsubscribed ); job->includeStatistics( fetchStatistics ); q->connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(collectionsChanged(Akonadi::Collection::List)) ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(listDone(KJob*)) ); } //@endcond diff --git a/akonadi/collectionstatisticsjob.cpp b/akonadi/collectionstatisticsjob.cpp index a3ce70869..05aae658b 100644 --- a/akonadi/collectionstatisticsjob.cpp +++ b/akonadi/collectionstatisticsjob.cpp @@ -1,108 +1,108 @@ /* Copyright (c) 2006 - 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 "collectionstatisticsjob.h" #include "collection.h" #include "collectionstatistics.h" #include "imapparser_p.h" #include "job_p.h" #include using namespace Akonadi; class Akonadi::CollectionStatisticsJobPrivate : public JobPrivate { public: CollectionStatisticsJobPrivate( CollectionStatisticsJob *parent ) : JobPrivate( parent ) { } Collection mCollection; CollectionStatistics mStatistics; }; CollectionStatisticsJob::CollectionStatisticsJob( const Collection &collection, QObject * parent ) : Job( new CollectionStatisticsJobPrivate( this ), parent ) { Q_D( CollectionStatisticsJob ); d->mCollection = collection; } CollectionStatisticsJob::~CollectionStatisticsJob() { } void CollectionStatisticsJob::doStart( ) { Q_D( CollectionStatisticsJob ); d->writeData( d->newTag() + " STATUS " + QByteArray::number( d->mCollection.id() ) + " (MESSAGES UNSEEN SIZE)\n" ); } void CollectionStatisticsJob::doHandleResponse( const QByteArray & tag, const QByteArray & data ) { Q_D( CollectionStatisticsJob ); if ( tag == "*" ) { QByteArray token; int current = ImapParser::parseString( data, token ); if ( token == "STATUS" ) { // folder path current = ImapParser::parseString( data, token, current ); // result list QList list; current = ImapParser::parseParenthesizedList( data, list, current ); for ( int i = 0; i < list.count() - 1; i += 2 ) { if ( list[i] == "MESSAGES" ) { d->mStatistics.setCount( list[i+1].toLongLong() ); } else if ( list[i] == "UNSEEN" ) { d->mStatistics.setUnreadCount( list[i+1].toLongLong() ); } else if ( list[i] == "SIZE" ) { d->mStatistics.setSize( list[i+1].toLongLong() ); } else { - kDebug( 5250 ) << "Unknown STATUS response: " << list[i]; + kDebug() << "Unknown STATUS response: " << list[i]; } } d->mCollection.setStatistics( d->mStatistics ); return; } } - kDebug( 5250 ) << "Unhandled response: " << tag << data; + kDebug() << "Unhandled response: " << tag << data; } Collection CollectionStatisticsJob::collection() const { Q_D( const CollectionStatisticsJob ); return d->mCollection; } CollectionStatistics Akonadi::CollectionStatisticsJob::statistics() const { Q_D( const CollectionStatisticsJob ); return d->mStatistics; } #include "collectionstatisticsjob.moc" diff --git a/akonadi/collectionsync.cpp b/akonadi/collectionsync.cpp index 4c6618212..8071eb11b 100644 --- a/akonadi/collectionsync.cpp +++ b/akonadi/collectionsync.cpp @@ -1,236 +1,236 @@ /* 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 "collectionsync_p.h" #include "collection.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionfetchjob.h" #include "collectionmodifyjob.h" #include using namespace Akonadi; /** * @internal */ class CollectionSync::Private { public: Private() : pendingJobs( 0 ), incremental( false ), streaming( false ) { } QString resourceId; // local: mapped remote id -> collection, id -> collection QHash localCollections; QSet unprocessedLocalCollections; // remote: mapped id -> collection QHash remoteCollections; // remote collections waiting for a parent QList orphanRemoteCollections; // removed remote collections Collection::List removedRemoteCollections; // create counter int pendingJobs; bool incremental; bool streaming; }; CollectionSync::CollectionSync( const QString &resourceId, QObject *parent ) : TransactionSequence( parent ), d( new Private ) { d->resourceId = resourceId; } CollectionSync::~CollectionSync() { delete d; } void CollectionSync::setRemoteCollections(const Collection::List & remoteCollections) { foreach ( const Collection &c, remoteCollections ) { d->remoteCollections.insert( c.id(), c ); } if ( !d->streaming ) retrievalDone(); } void CollectionSync::setRemoteCollections(const Collection::List & changedCollections, const Collection::List & removedCollections) { d->incremental = true; foreach ( const Collection &c, changedCollections ) { d->remoteCollections.insert( c.id(), c ); } d->removedRemoteCollections += removedCollections; if ( !d->streaming ) retrievalDone(); } void CollectionSync::doStart() { } void CollectionSync::slotLocalListDone(KJob * job) { if ( job->error() ) return; Collection::List list = static_cast( job )->collections(); foreach ( const Collection &c, list ) { d->localCollections.insert( c.remoteId(), c ); d->unprocessedLocalCollections.insert( c ); } // added / updated foreach ( const Collection &c, d->remoteCollections ) { if ( c.remoteId().isEmpty() ) { - kWarning( 5250 ) << "Collection '" << c.name() <<"' does not have a remote identifier - skipping"; + kWarning() << "Collection '" << c.name() <<"' does not have a remote identifier - skipping"; continue; } Collection local = d->localCollections.value( c.remoteId() ); d->unprocessedLocalCollections.remove( local ); // missing locally if ( !local.isValid() ) { // determine local parent Collection localParent; if ( c.parent() >= 0 ) localParent = Collection( c.parent() ); if ( c.parentRemoteId().isEmpty() ) localParent = Collection::root(); else localParent = d->localCollections.value( c.parentRemoteId() ); // no local parent found, create later if ( !localParent.isValid() ) { d->orphanRemoteCollections << c; continue; } createLocalCollection( c, localParent ); continue; } // update local collection d->pendingJobs++; Collection upd( c ); upd.setId( local.id() ); CollectionModifyJob *mod = new CollectionModifyJob( upd, this ); connect( mod, SIGNAL(result(KJob*)), SLOT(slotLocalChangeDone(KJob*)) ); } // removed if ( !d->incremental ) d->removedRemoteCollections = d->unprocessedLocalCollections.toList(); foreach ( const Collection &c, d->removedRemoteCollections ) { d->pendingJobs++; CollectionDeleteJob *job = new CollectionDeleteJob( c, this ); connect( job, SIGNAL(result(KJob*)), SLOT(slotLocalChangeDone(KJob*)) ); } d->localCollections.clear(); checkDone(); } void CollectionSync::slotLocalCreateDone(KJob * job) { d->pendingJobs--; if ( job->error() ) return; Collection newLocal = static_cast( job )->collection(); // d->localCollections.insert( newLocal.remoteId(), newLocal ); // search for children we can create now Collection::List stillOrphans; foreach ( const Collection &orphan, d->orphanRemoteCollections ) { if ( orphan.parentRemoteId() == newLocal.remoteId() ) { createLocalCollection( orphan, newLocal ); } else { stillOrphans << orphan; } } d->orphanRemoteCollections = stillOrphans; checkDone(); } void CollectionSync::createLocalCollection(const Collection & c, const Collection & parent) { d->pendingJobs++; Collection col( c ); col.setParent( parent ); CollectionCreateJob *create = new CollectionCreateJob( col, this ); connect( create, SIGNAL(result(KJob*)), SLOT(slotLocalCreateDone(KJob*)) ); } void CollectionSync::checkDone() { // still running jobs if ( d->pendingJobs > 0 ) return; // still orphan collections if ( !d->orphanRemoteCollections.isEmpty() ) { setError( Unknown ); setErrorText( QLatin1String( "Found unresolved orphan collections" ) ); foreach ( const Collection &col, d->orphanRemoteCollections ) kDebug() << "found orphan collection:" << col.remoteId() << "parent:" << col.parentRemoteId(); } commit(); } void CollectionSync::slotLocalChangeDone(KJob * job) { if ( job->error() ) return; d->pendingJobs--; checkDone(); } void CollectionSync::setStreamingEnabled( bool streaming ) { d->streaming = streaming; } void CollectionSync::retrievalDone() { CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); job->setResource( d->resourceId ); connect( job, SIGNAL(result(KJob*)), SLOT(slotLocalListDone(KJob*)) ); } #include "collectionsync_p.moc" diff --git a/akonadi/control.cpp b/akonadi/control.cpp index 92af7ea83..7f39579c8 100644 --- a/akonadi/control.cpp +++ b/akonadi/control.cpp @@ -1,258 +1,258 @@ /* 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 "control.h" #include "servermanager.h" #include "ui_controlprogressindicator.h" #include "selftestdialog_p.h" #include "erroroverlay_p.h" #include "firstrun_p.h" #include #include #include #include #include #include #include using namespace Akonadi; class ControlProgressIndicator : public QFrame { public: ControlProgressIndicator( QWidget *parent = 0 ) : QFrame( parent ) { setWindowModality( Qt::ApplicationModal ); resize( 400, 100 ); setWindowFlags( Qt::FramelessWindowHint | Qt::Dialog ); ui.setupUi( this ); setFrameShadow( QFrame::Plain ); setFrameShape( QFrame::Box ); } void setMessage( const QString &msg ) { ui.statusLabel->setText( msg ); } Ui::ControlProgressIndicator ui; }; class StaticControl : public Control { public: StaticControl() : Control() {} }; K_GLOBAL_STATIC( StaticControl, s_instance ) /** * @internal */ class Control::Private { public: Private( Control *parent ) : mParent( parent ), mEventLoop( 0 ), mProgressIndicator( 0 ), mFirstRunner( 0 ), mSuccess( false ), mStarting( false ), mStopping( false ) { KGlobal::locale()->insertCatalog( QString::fromLatin1("libakonadi") ); if ( ServerManager::isRunning() ) mFirstRunner = new Firstrun( mParent ); } ~Private() { delete mProgressIndicator; } void setupProgressIndicator( const QString &msg, QWidget *parent = 0 ) { if ( mProgressIndicator ) return; mProgressIndicator = new ControlProgressIndicator( parent ); mProgressIndicator->setMessage( msg ); } void createErrorOverlays() { foreach ( QWidget* widget, mPendingOverlays ) new ErrorOverlay( widget ); mPendingOverlays.clear(); } void cleanup() { s_instance.destroy(); } bool exec(); void serverStarted(); void serverStopped(); QPointer mParent; QEventLoop *mEventLoop; QPointer mProgressIndicator; QList mPendingOverlays; Firstrun *mFirstRunner; bool mSuccess; bool mStarting; bool mStopping; }; bool Control::Private::exec() { if ( mProgressIndicator ) mProgressIndicator->show(); - kDebug( 5250 ) << "Starting Akonadi (using an event loop)."; + kDebug() << "Starting Akonadi (using an event loop)."; mEventLoop = new QEventLoop( mParent ); // safety timeout QTimer::singleShot( 10000, mEventLoop, SLOT(quit()) ); mEventLoop->exec(); mEventLoop->deleteLater(); mEventLoop = 0; if ( !mSuccess ) { - kWarning( 5250 ) << "Could not start/stop Akonadi!"; + kWarning() << "Could not start/stop Akonadi!"; if ( mProgressIndicator && mStarting ) { QPointer dlg = new SelfTestDialog( mProgressIndicator->parentWidget() ); dlg->exec(); delete dlg; if ( !mParent ) return false; } } delete mProgressIndicator; mProgressIndicator = 0; mStarting = false; mStopping = false; const bool rv = mSuccess; mSuccess = false; return rv; } void Control::Private::serverStarted() { if ( mEventLoop && mEventLoop->isRunning() && mStarting ) { mEventLoop->quit(); mSuccess = true; } if ( !mFirstRunner ) mFirstRunner = new Firstrun( mParent ); } void Control::Private::serverStopped() { if ( mEventLoop && mEventLoop->isRunning() && mStopping ) { mEventLoop->quit(); mSuccess = true; } } Control::Control() : d( new Private( this ) ) { connect( ServerManager::self(), SIGNAL( started() ), SLOT( serverStarted() ) ); connect( ServerManager::self(), SIGNAL( stopped() ), SLOT( serverStopped() ) ); // mProgressIndicator is a widget, so it better be deleted before the QApplication is deleted // Otherwise we get a crash in QCursor code with Qt-4.5 if ( QCoreApplication::instance() ) connect( QCoreApplication::instance(), SIGNAL( aboutToQuit() ), this, SLOT( cleanup() ) ); } Control::~Control() { delete d; } bool Control::start() { if ( s_instance->d->mStopping ) return false; if ( ServerManager::isRunning() || s_instance->d->mEventLoop ) return true; s_instance->d->mStarting = true; if ( !ServerManager::start() ) return false; return s_instance->d->exec(); } bool Control::stop() { if ( s_instance->d->mStarting ) return false; if ( !ServerManager::isRunning() || s_instance->d->mEventLoop ) return true; s_instance->d->mStopping = true; if ( !ServerManager::stop() ) return false; return s_instance->d->exec(); } bool Control::restart() { if ( ServerManager::isRunning() ) { if ( !stop() ) return false; } return start(); } bool Control::start(QWidget * parent) { s_instance->d->setupProgressIndicator( i18n( "Starting Akonadi server..." ), parent ); return start(); } bool Control::stop(QWidget * parent) { s_instance->d->setupProgressIndicator( i18n( "Stopping Akonadi server..." ), parent ); return stop(); } bool Control::restart(QWidget * parent) { if ( ServerManager::isRunning() ) { if ( !stop( parent ) ) return false; } return start( parent ); } void Control::widgetNeedsAkonadi(QWidget * widget) { s_instance->d->mPendingOverlays.append( widget ); // delay the overlay creation since we rely on widget being reparented // correctly already QTimer::singleShot( 0, s_instance, SLOT(createErrorOverlays()) ); } #include "control.moc" diff --git a/akonadi/descendantsproxymodel.cpp b/akonadi/descendantsproxymodel.cpp new file mode 100644 index 000000000..1b98ffae9 --- /dev/null +++ b/akonadi/descendantsproxymodel.cpp @@ -0,0 +1,101 @@ +/* + Copyright (c) 2009 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 "descendantsproxymodel.h" +#include "entitytreemodel.h" + +using namespace Akonadi; + +namespace Akonadi +{ + +class DescendantsProxyModelPrivate +{ +public: + DescendantsProxyModelPrivate(DescendantsProxyModel *model) + : m_headerSet(EntityTreeModel::EntityTreeHeaders), + q_ptr(model) + { + + } +private: + + int m_headerSet; + + Q_DECLARE_PUBLIC(DescendantsProxyModel) + DescendantsProxyModel *q_ptr; + +}; + +} + +DescendantsProxyModel::DescendantsProxyModel(QObject *parent) + : KDescendantsProxyModel(parent), d_ptr(new DescendantsProxyModelPrivate(this)) +{ + +} + +DescendantsProxyModel::~DescendantsProxyModel() +{ + delete d_ptr; +} + +QVariant DescendantsProxyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + Q_D(const DescendantsProxyModel); + int adjustedRole; + + adjustedRole = role + ( Akonadi::EntityTreeModel::TerminalUserRole * d->m_headerSet ); + return sourceModel()->headerData(section, orientation, adjustedRole); +} + +int DescendantsProxyModel::headerSet() const +{ + Q_D(const DescendantsProxyModel); + return d->m_headerSet; +} + +void DescendantsProxyModel::setHeaderSet(int set) +{ + Q_D(DescendantsProxyModel); + d->m_headerSet = set; +} + +bool DescendantsProxyModel::dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ) +{ + Q_ASSERT(sourceModel()); + const QModelIndex sourceParent = mapToSource(parent); + return sourceModel()->dropMimeData(data, action, row, column, sourceParent); +} + +QMimeData* DescendantsProxyModel::mimeData( const QModelIndexList & indexes ) const +{ + Q_ASSERT(sourceModel()); + QModelIndexList sourceIndexes; + foreach(const QModelIndex& index, indexes) + sourceIndexes << mapToSource(index); + return sourceModel()->mimeData(sourceIndexes); +} + +QStringList DescendantsProxyModel::mimeTypes() const +{ + Q_ASSERT(sourceModel()); + return sourceModel()->mimeTypes(); +} diff --git a/akonadi/descendantsproxymodel.h b/akonadi/descendantsproxymodel.h new file mode 100644 index 000000000..f72524b2d --- /dev/null +++ b/akonadi/descendantsproxymodel.h @@ -0,0 +1,82 @@ +/* + Copyright (c) 2009 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. +*/ + +#ifndef AKONADI_DESCENDANTSPROXYMODEL_H +#define AKONADI_DESCENDANTSPROXYMODEL_H + +#include "akonadi_export.h" + +#include + +namespace Akonadi +{ +class DescendantsProxyModelPrivate; + +/** + * @short A proxy model that flattens a tree-based model to a list model. + * + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADI_EXPORT DescendantsProxyModel : public KDescendantsProxyModel +{ + Q_OBJECT + + public: + /** + * Creates a new descendants proxy model. + * + * @param parent The parent object. + */ + DescendantsProxyModel( QObject *parent = 0 ); + + /** + * Destroys the descendants proxy model. + */ + virtual ~DescendantsProxyModel(); + + /** + * Sets the header @p set that shall be used by the proxy model. + * + * \s EntityTreeModel::HeaderGroup + */ + void setHeaderSet( int set ); + + /** + * Returns the header set used by the proxy model. + */ + int headerSet() const; + + // QAbstractProxyModel does not proxy all methods... + virtual bool dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ); + virtual QMimeData* mimeData( const QModelIndexList & indexes ) const; + virtual QStringList mimeTypes() const; + + virtual QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const; + + private: + //@cond PRIVATE + Q_DECLARE_PRIVATE(DescendantsProxyModel) + DescendantsProxyModelPrivate *d_ptr; + //@endcond +}; + +} + +#endif diff --git a/akonadi/entityfilterproxymodel.cpp b/akonadi/entityfilterproxymodel.cpp new file mode 100644 index 000000000..414e7e9d4 --- /dev/null +++ b/akonadi/entityfilterproxymodel.cpp @@ -0,0 +1,248 @@ +/* + Copyright (c) 2007 Bruno Virlet + Copyright (c) 2009 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 "entityfilterproxymodel.h" + +#include "entitytreemodel.h" + +#include +#include + +#include +#include + +using namespace Akonadi; + +namespace Akonadi { +/** + * @internal + */ +class EntityFilterProxyModelPrivate +{ + public: + EntityFilterProxyModelPrivate( EntityFilterProxyModel *parent ) + : q_ptr( parent ), + m_headerSet(0) + { + } + + QStringList includedMimeTypes; + QStringList excludedMimeTypes; + + QPersistentModelIndex m_rootIndex; + + int m_headerSet; + + Q_DECLARE_PUBLIC(EntityFilterProxyModel) + EntityFilterProxyModel *q_ptr; +}; + +} + +EntityFilterProxyModel::EntityFilterProxyModel( QObject *parent ) + : QSortFilterProxyModel( parent ), + d_ptr( new EntityFilterProxyModelPrivate( this ) ) +{ + // TODO: Override setSourceModel and do this there? + setSupportedDragActions( Qt::CopyAction | Qt::MoveAction ); +} + +EntityFilterProxyModel::~EntityFilterProxyModel() +{ + delete d_ptr; +} + +void EntityFilterProxyModel::addMimeTypeInclusionFilters(const QStringList &typeList) +{ + Q_D(EntityFilterProxyModel); + d->includedMimeTypes << typeList; + invalidateFilter(); +} + +void EntityFilterProxyModel::addMimeTypeExclusionFilters(const QStringList &typeList) +{ + Q_D(EntityFilterProxyModel); + d->excludedMimeTypes << typeList; + invalidateFilter(); +} + +void EntityFilterProxyModel::addMimeTypeInclusionFilter(const QString &type) +{ + Q_D(EntityFilterProxyModel); + d->includedMimeTypes << type; + invalidateFilter(); +} + +void EntityFilterProxyModel::addMimeTypeExclusionFilter(const QString &type) +{ + Q_D(EntityFilterProxyModel); + d->excludedMimeTypes << type; + invalidateFilter(); +} + +bool EntityFilterProxyModel::filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent) const +{ + Q_D(const EntityFilterProxyModel); + // All rows that are not below m_rootIndex are unfiltered. + + bool found = false; + + const bool rootIsValid = d->m_rootIndex.isValid(); + QModelIndex _parent = sourceParent; + while (true) + { + if (_parent == d->m_rootIndex) + { + found = true; + break; + } + _parent = _parent.parent(); + if (!_parent.isValid() && rootIsValid) + { + break; + } + } + + if (!found) + { + return true; + } + + const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent); + + const QString rowMimetype = idx.data( EntityTreeModel::MimeTypeRole ).toString(); + + if ( d->excludedMimeTypes.contains( rowMimetype ) ) + return false; + if ( d->includedMimeTypes.isEmpty() || d->includedMimeTypes.contains( rowMimetype ) ) + return true; + + return false; +} + +QStringList EntityFilterProxyModel::mimeTypeInclusionFilters() const +{ + Q_D(const EntityFilterProxyModel); + return d->includedMimeTypes; +} + +QStringList EntityFilterProxyModel::mimeTypeExclusionFilters() const +{ + Q_D(const EntityFilterProxyModel); + return d->excludedMimeTypes; +} + +void EntityFilterProxyModel::clearFilters() +{ + Q_D(EntityFilterProxyModel); + d->includedMimeTypes.clear(); + d->excludedMimeTypes.clear(); + invalidateFilter(); +} + +void EntityFilterProxyModel::setRootIndex(const QModelIndex &srcIndex) +{ + Q_D(EntityFilterProxyModel); + d->m_rootIndex = srcIndex; + reset(); +} + +void EntityFilterProxyModel::setHeaderSet(int set) +{ + Q_D(EntityFilterProxyModel); + d->m_headerSet = set; +} + + +QVariant EntityFilterProxyModel::headerData(int section, Qt::Orientation orientation, int role ) const +{ + Q_D(const EntityFilterProxyModel); + role += (EntityTreeModel::TerminalUserRole * d->m_headerSet); + return sourceModel()->headerData(section, orientation, role); +} + +bool EntityFilterProxyModel::dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ) +{ + Q_ASSERT(sourceModel()); + const QModelIndex sourceParent = mapToSource(parent); + return sourceModel()->dropMimeData(data, action, row, column, sourceParent); +} + +QMimeData* EntityFilterProxyModel::mimeData( const QModelIndexList & indexes ) const +{ + Q_ASSERT(sourceModel()); + QModelIndexList sourceIndexes; + foreach(const QModelIndex& index, indexes) + sourceIndexes << mapToSource(index); + return sourceModel()->mimeData(sourceIndexes); +} + +QStringList EntityFilterProxyModel::mimeTypes() const +{ + Q_ASSERT(sourceModel()); + return sourceModel()->mimeTypes(); +} + +QModelIndexList EntityFilterProxyModel::match(const QModelIndex& start, int role, const QVariant& value, int hits, Qt::MatchFlags flags) const +{ + if (EntityTreeModel::AmazingCompletionRole != role) + return QSortFilterProxyModel::match(start, role, value, hits, flags); + + // We match everything in the source model because sorting will change what we should show. + const int allHits = -1; + + QModelIndexList proxyList; + QMap proxyMap; + QModelIndexList sourceList = sourceModel()->match(mapToSource(start), role, value, allHits, flags); + QModelIndexList::const_iterator it; + const QModelIndexList::const_iterator begin = sourceList.constBegin(); + const QModelIndexList::const_iterator end = sourceList.constEnd(); + QModelIndex proxyIndex; + for (it = begin; it != end; ++it) + { + proxyIndex = mapFromSource(*it); + + // Any filtered indexes will be invalid when mapped. + if (!proxyIndex.isValid()) + continue; + + // Inserting in a QMap gives us sorting by key for free. + proxyMap.insert(proxyIndex.row(), proxyIndex); + } + + if (hits == -1) + return proxyMap.values(); + + return proxyMap.values().mid(0, hits); +} + +int EntityFilterProxyModel::columnCount(const QModelIndex &parent) const +{ + Q_D(const EntityFilterProxyModel); +// return QSortFilterProxyModel::columnCount(parent); + QVariant var = sourceModel()->data(parent, EntityTreeModel::ColumnCountRole + (EntityTreeModel::TerminalUserRole * d->m_headerSet)); + if( !var.isValid() ) + return 0; + return var.toInt(); +} + +#include "entityfilterproxymodel.moc" + diff --git a/akonadi/entityfilterproxymodel.h b/akonadi/entityfilterproxymodel.h new file mode 100644 index 000000000..73af5296f --- /dev/null +++ b/akonadi/entityfilterproxymodel.h @@ -0,0 +1,158 @@ +/* + Copyright (c) 2007 Bruno Virlet + Copyright (c) 2009 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. +*/ + +#ifndef AKONADI_ENTITYFILTERPROXYMODEL_H +#define AKONADI_ENTITYFILTERPROXYMODEL_H + +#include "akonadi_export.h" + +#include + +namespace Akonadi { + +class EntityFilterProxyModelPrivate; + +/** + * @short A proxy model that filters entities by mime type. + * + * This class can be used on top of an EntityTreeModel to exclude entities by mimetype + * or to include only certain mimetypes. + * + * @code + * + * Akonadi::EntityTreeModel *model = new Akonadi::EntityTreeModel( this ); + * + * Akonadi::EntityFilterProxyModel *proxy = new Akonadi::EntityFilterProxyModel(); + * proxy->addMimeTypeInclusionFilter( "message/rfc822" ); + * proxy->setSourceModel( model ); + * + * Akonadi::EntityTreeView *view = new Akonadi::EntityTreeView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @li If a mimetype is in both the exclusion list and the inclusion list, it is excluded. + * @li If the mimeTypeInclusionFilter is empty, all mimetypes are + * accepted (except if they are in the exclusion filter of course). + * + * + * @author Bruno Virlet + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADI_EXPORT EntityFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + public: + /** + * Creates a new proxy filter model. + * + * @param parent The parent object. + */ + explicit EntityFilterProxyModel( QObject *parent = 0 ); + + /** + * Destroys the proxy filter model. + */ + virtual ~EntityFilterProxyModel(); + + /** + * Add mime types to be shown by the filter. + * + * @param mimeTypes A list of mime types to be included. + */ + void addMimeTypeInclusionFilters( const QStringList &mimeTypes ); + + /** + * Add mimetypes to filter out + * + * @param mimeTypes A list to exclude from the model. + */ + void addMimeTypeExclusionFilters( const QStringList &mimeTypes ); + + /** + * Add mime type to be shown by the filter. + * + * @param mimeType A mime type to be shown. + */ + void addMimeTypeInclusionFilter( const QString &mimeType ); + + /** + * Add mime type to be excluded by the filter. + * + * @param mimeType A mime type to be excluded. + */ + void addMimeTypeExclusionFilter( const QString &mimeType ); + + /** + * Returns the list of mime type inclusion filters. + */ + QStringList mimeTypeInclusionFilters() const; + + /** + * Returns the list of mime type exclusion filters. + */ + QStringList mimeTypeExclusionFilters() const; + + /** + * Clear all mime type filters. + */ + void clearFilters(); + + /** + * Sets the @p index that shall be used as the root for this model. + */ + void setRootIndex( const QModelIndex &index ); + + /** + * Sets the header @p set of the filter model. + * + * \sa EntityTreeModel::HeaderGroup + */ + void setHeaderSet( int set ); + + virtual QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const; + + // QAbstractProxyModel does not proxy all methods... + virtual bool dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ); + virtual QMimeData* mimeData( const QModelIndexList & indexes ) const; + virtual QStringList mimeTypes() const; + + /** + Reimplemented to handle the AmazingCompletionRole. + */ + virtual QModelIndexList match( const QModelIndex& start, int role, const QVariant& value, int hits = 1, Qt::MatchFlags flags = Qt::MatchFlags( Qt::MatchStartsWith | Qt::MatchWrap ) ) const; + + virtual int columnCount(const QModelIndex& parent = QModelIndex()) const; + + protected: + virtual bool filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const; + + private: + //@cond PRIVATE + Q_DECLARE_PRIVATE( EntityFilterProxyModel ) + EntityFilterProxyModelPrivate *d_ptr; + //@endcond +}; + +} + +#endif diff --git a/akonadi/entitytreemodel.cpp b/akonadi/entitytreemodel.cpp new file mode 100644 index 000000000..03ff881f4 --- /dev/null +++ b/akonadi/entitytreemodel.cpp @@ -0,0 +1,914 @@ +/* + 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; +} + + +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 (ParentCollection == 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 ); + + //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.parent()), collection.id() ); + + Node *parentNode = d->m_childEntities.value( collection.parent() ).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 +{ + 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... + 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 ( 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 Akonadi::EntityTreeModel::match(start, role, value, hits, flags); + + // Try to match names, and email addresses. + QModelIndexList 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); + + // TODO: will this work for collection::root while showing it? + + const int row = d->indexOf( d->m_childEntities.value( collection.parent() ), collection.id() ); + + if ( row < 0 ) + return QModelIndex(); + + Node *node = d->m_childEntities.value( collection.parent() ).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/entitytreemodel.h b/akonadi/entitytreemodel.h new file mode 100644 index 000000000..a9b0cb2d0 --- /dev/null +++ b/akonadi/entitytreemodel.h @@ -0,0 +1,348 @@ +/* + 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. +*/ + +#ifndef AKONADI_ENTITYTREEMODEL_H +#define AKONADI_ENTITYTREEMODEL_H + +#include "akonadi_export.h" + +#include +#include + +#include +#include + +// TODO (Applies to all these 'new' models, not just EntityTreeModel): +// * Figure out how LazyPopulation and signals from monitor containing items should +// fit together. Possibly store a list of collections whose items have already +// been lazily fetched. +// * Fgure out whether DescendantEntitiesProxyModel needs to use fetchMore. +// * Profile this and DescendantEntitiesProxyModel. Make sure it's faster than +// FlatCollectionProxyModel. See if the cache in that class can be cleared less often. +// * Unit tests. Much of the stuff here is not covered by modeltest, and some of +// it is akonadi specific, such as setting root collection etc. +// * Implement support for includeUnsubscribed. +// * Use CollectionStatistics for item count stuff. Find out if I can get stats by mimetype. +// * Make sure there are applications using it before committing to it until KDE5. +// Some API/ virtual methods might need to be added when real applications are made. +// * Implement ordering support. +// * Implement some proxy models for time-table like uses, eg KOrganizer events. +// * Apidox++ + +namespace Akonadi +{ +class CollectionStatistics; +class Item; +class ItemFetchScope; +class Monitor; +class Session; + +class EntityTreeModelPrivate; + +/** + * @short A model for collections and items together. + * + * This class is a wrapper around a Akonadi::Monitor object. The model represents a + * part of the collection and item tree configured in the Monitor. + * + * @code + * + * Monitor *monitor = new Monitor(this); + * monitor->setCollectionMonitored(Collection::root()); + * monitor->setMimeTypeMonitored(KABC::addresseeMimeType()); + * + * EntityTreeModel *model = new EntityTreeModel( session, monitor, this ); + * + * EntityTreeView *view = new EntityTreeView( this ); + * view->setModel( model ); + * + * @endcode + * + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADI_EXPORT EntityTreeModel : public QAbstractItemModel +{ + Q_OBJECT + + public: + /** + * Describes the roles for items. Roles for collections are defined by the superclass. + */ + enum Roles { + //sebsauer, 2009-05-07; to be able here to keep the akonadi_next EntityTreeModel compatible with + //the akonadi_old ItemModel and CollectionModel, we need to use the same int-values for + //ItemRole, ItemIdRole and MimeTypeRole like the Akonadi::ItemModel is using and the same + //CollectionIdRole and CollectionRole like the Akonadi::CollectionModel is using. + ItemIdRole = Qt::UserRole + 1, ///< The item id + ItemRole = Qt::UserRole + 2, ///< The Item + MimeTypeRole = Qt::UserRole + 3, ///< The mimetype of the entity + + CollectionIdRole = Qt::UserRole + 10, ///< The collection id. + CollectionRole = Qt::UserRole + 11, ///< The collection. + + RemoteIdRole, ///< The remoteId of the entity + CollectionChildOrderRole, ///< Ordered list of child items if available + AmazingCompletionRole, ///< Role used to implement amazing completion + ParentCollection, ///< The parent collection of the entity + ColumnCountRole, ///< @internal Used by proxies to determine the number of columns for a header group. + UserRole = Qt::UserRole + 1000, ///< Role for user extensions. + TerminalUserRole = 10000 ///< Last role for user extensions. Don't use a role beyond this or headerData will break. + }; + + /** + * Describes what header information the model shall return. + */ + enum HeaderGroup { + EntityTreeHeaders, ///< Header information for a tree with collections and items + CollectionTreeHeaders, ///< Header information for a collection-only tree + ItemListHeaders, ///< Header information for a list of items + UserHeaders = 1000 ///< Last header information for submodel extensions + }; + + /** + * Creates a new entity tree model. + * + * @param session The Session to use to communicate with Akonadi. + * @param monitor The Monitor whose entities should be represented in the model. + * @param parent The parent object. + */ + EntityTreeModel( Session *session, Monitor *monitor, QObject *parent = 0 ); + + /** + * Destroys the entity tree model. + */ + virtual ~EntityTreeModel(); + + /** + * Describes how the model should populated its items. + */ + enum ItemPopulationStrategy { + NoItemPopulation, ///< Do not include items in the model. + ImmediatePopulation, ///< Retrieve items immediately when their parent is in the model. This is the default. + LazyPopulation ///< Fetch items only when requested (using canFetchMore/fetchMore) + }; + + /** + * Sets the item population @p strategy of the model. + */ + void setItemPopulationStrategy( ItemPopulationStrategy strategy ); + + /** + * Returns the item population strategy of the model. + */ + ItemPopulationStrategy itemPopulationStrategy() const; + + /** + * Sets the root collection to create an entity tree for. + * The @p collection must be a valid Collection object. + * + * By default the Collection::root() is used. + */ + void setRootCollection( const Collection &collection ); + + /** + * Returns the root collection of the entity tree. + */ + Collection rootCollection() const; + + /** + * Sets whether the root collection shall be provided by the model. + * + * @see setRootCollectionDisplayName() + */ + void setIncludeRootCollection( bool include ); + + /** + * Returns whether the root collection is provided by the model. + */ + bool includeRootCollection() const; + + /** + * Sets the display @p name of the root collection of the model. + * The default display name is "[*]". + * + * @note The display name for the root collection is only used if + * the root collection has been included with setIncludeRootCollection(). + */ + void setRootCollectionDisplayName( const QString &name ); + + /** + * Returns the display name of the root collection. + */ + QString rootCollectionDisplayName() const; + + /** + * Describes what collections shall be fetched by and represent in the model. + */ + enum CollectionFetchStrategy { + FetchNoCollections, ///< Fetches nothing. This creates an empty model. + FetchFirstLevelChildCollections, ///< Fetches first level collections in the root collection. + FetchCollectionsRecursive ///< Fetches collections in the root collection recursively. This is the default. + }; + + /** + * Sets the collection fetch @p strategy of the model. + */ + void setCollectionFetchStrategy( CollectionFetchStrategy strategy ); + + /** + * Returns the collection fetch strategy of the model. + */ + CollectionFetchStrategy collectionFetchStrategy() const; + + /** + * Returns the model index for the given @p collection. + */ + QModelIndex indexForCollection( const Collection &collection ) const; + + /** + * Returns the model indexes for the given @p item. + */ + QModelIndexList indexesForItem( const Item &item ) const; + + /** + * Returns the collection for the given collection @p id. + */ + Collection collectionForId( Collection::Id id ) const; + + /** + * Returns the item for the given item @p id. + */ + Item itemForId( Item::Id id ) const; + + // TODO: Remove these and use the Monitor instead. Need to add api to Monitor for this. + void setIncludeUnsubscribed( bool include ); + bool includeUnsubscribed() const; + + virtual int columnCount( const QModelIndex & parent = QModelIndex() ) const; + virtual int rowCount( const QModelIndex & parent = QModelIndex() ) const; + + virtual QVariant data( const QModelIndex & index, int role = Qt::DisplayRole ) const; + virtual QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const; + + virtual Qt::ItemFlags flags( const QModelIndex &index ) const; + virtual QStringList mimeTypes() const; + + virtual Qt::DropActions supportedDropActions() const; + virtual QMimeData *mimeData( const QModelIndexList &indexes ) const; + virtual bool dropMimeData( const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent ); + virtual bool setData( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole ); + + virtual QModelIndex index( int row, int column, const QModelIndex & parent = QModelIndex() ) const; + virtual QModelIndex parent( const QModelIndex & index ) const; + + // TODO: Review the implementations of these. I think they could be better. + virtual bool canFetchMore( const QModelIndex & parent ) const; + virtual void fetchMore( const QModelIndex & parent ); + virtual bool hasChildren( const QModelIndex &parent = QModelIndex() ) const; + + /** + * Reimplemented to handle the AmazingCompletionRole. + */ + virtual QModelIndexList match( const QModelIndex& start, int role, const QVariant& value, int hits = 1, Qt::MatchFlags flags = Qt::MatchFlags( Qt::MatchStartsWith | Qt::MatchWrap ) ) const; + + /** + * Reimplement this in a subclass to return true if @p item matches @p value with @p flags in the AmazingCompletionRole. + */ + virtual bool match( const Item &item, const QVariant &value, Qt::MatchFlags flags ) const; + + /** + * Reimplement this in a subclass to return true if @p collection matches @p value with @p flags in the AmazingCompletionRole. + */ + virtual bool match( const Collection &collection, const QVariant &value, Qt::MatchFlags flags ) const; + + protected: + /** + * Clears and resets the model. Always call this instead of the reset method in the superclass. + * Using the reset method will not reliably clear or refill the model. + */ + void clearAndReset(); + + /** + * Provided for convenience of subclasses. + */ + virtual QVariant getData( const Item &item, int column, int role = Qt::DisplayRole ) const; + + /** + * Provided for convenience of subclasses. + */ + virtual QVariant getData( const Collection &collection, int column, int role = Qt::DisplayRole ) const; + + /** + * Reimplement this to provide different header data. This is needed when using one model + * with multiple proxies and views, and each should show different header data. + */ + virtual QVariant getHeaderData( int section, Qt::Orientation orientation, int role, int headerSet ) const; + + virtual int getColumnCount(int headerSet) const; + + /** + * Removes the rows from @p start to @p end from @parent + */ + virtual bool removeRows( int start, int end, const QModelIndex &parent = QModelIndex() ); + + private: + //@cond PRIVATE + Q_DECLARE_PRIVATE( EntityTreeModel ) + EntityTreeModelPrivate *d_ptr; + + // Make these private, they shouldn't be called by applications + virtual bool insertRows( int , int, const QModelIndex& = QModelIndex() ); + virtual bool insertColumns( int, int, const QModelIndex& = QModelIndex() ); + virtual bool removeColumns( int, int, const QModelIndex& = QModelIndex() ); + + Q_PRIVATE_SLOT( d_func(), void monitoredCollectionStatisticsChanged( Akonadi::Collection::Id, + const Akonadi::CollectionStatistics& ) ) + + Q_PRIVATE_SLOT( d_func(), void startFirstListJob() ) + // Q_PRIVATE_SLOT( d_func(), void slotModelReset() ) + + // TODO: Can I merge these into one jobResult slot? + Q_PRIVATE_SLOT( d_func(), void fetchJobDone( KJob *job ) ) + Q_PRIVATE_SLOT( d_func(), void copyJobDone( KJob *job ) ) + Q_PRIVATE_SLOT( d_func(), void moveJobDone( KJob *job ) ) + Q_PRIVATE_SLOT( d_func(), void updateJobDone( KJob *job ) ) + + Q_PRIVATE_SLOT( d_func(), void itemsFetched( Akonadi::Item::List ) ) + Q_PRIVATE_SLOT( d_func(), void collectionsFetched( Akonadi::Collection::List ) ) + Q_PRIVATE_SLOT( d_func(), void ancestorsFetched( Akonadi::Collection::List ) ) + + Q_PRIVATE_SLOT( d_func(), void monitoredMimeTypeChanged( const QString&, bool ) ) + + Q_PRIVATE_SLOT( d_func(), void monitoredCollectionAdded( const Akonadi::Collection&, const Akonadi::Collection& ) ) + Q_PRIVATE_SLOT( d_func(), void monitoredCollectionRemoved( const Akonadi::Collection& ) ) + Q_PRIVATE_SLOT( d_func(), void monitoredCollectionChanged( const Akonadi::Collection& ) ) + Q_PRIVATE_SLOT( d_func(), void monitoredCollectionMoved( const Akonadi::Collection&, const Akonadi::Collection&, + const Akonadi::Collection&) ) + + Q_PRIVATE_SLOT( d_func(), void monitoredItemAdded( const Akonadi::Item&, const Akonadi::Collection& ) ) + Q_PRIVATE_SLOT( d_func(), void monitoredItemRemoved( const Akonadi::Item& ) ) + Q_PRIVATE_SLOT( d_func(), void monitoredItemChanged( const Akonadi::Item&, const QSet& ) ) + Q_PRIVATE_SLOT( d_func(), void monitoredItemMoved( const Akonadi::Item&, const Akonadi::Collection&, + const Akonadi::Collection& ) ) + + Q_PRIVATE_SLOT( d_func(), void monitoredItemLinked( const Akonadi::Item&, const Akonadi::Collection& ) ) + Q_PRIVATE_SLOT( d_func(), void monitoredItemUnlinked( const Akonadi::Item&, const Akonadi::Collection& ) ) + //@endcond +}; + +} // namespace + +#endif diff --git a/akonadi/entitytreemodel_p.cpp b/akonadi/entitytreemodel_p.cpp new file mode 100644 index 000000000..8028d5ece --- /dev/null +++ b/akonadi/entitytreemodel_p.cpp @@ -0,0 +1,675 @@ +/* + 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_p.h" +#include "entitytreemodel.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Akonadi; + +EntityTreeModelPrivate::EntityTreeModelPrivate( EntityTreeModel *parent ) + : q_ptr( parent ), + m_collectionFetchStrategy( EntityTreeModel::FetchCollectionsRecursive ), + m_itemPopulation( EntityTreeModel::ImmediatePopulation ), + m_includeUnsubscribed( true ), + m_includeStatistics( false ), + m_showRootCollection( false ) +{ +} + + +int EntityTreeModelPrivate::indexOf( const QList &nodes, Entity::Id id ) const +{ + int i = 0; + foreach ( const Node *node, nodes ) { + if ( node->id == id ) + return i; + i++; + } + + return -1; +} + +void EntityTreeModelPrivate::fetchItems( const Collection &parent ) +{ + Q_Q( EntityTreeModel ); +// kDebug() << parent.remoteId(); + Akonadi::ItemFetchJob *itemJob = new Akonadi::ItemFetchJob( parent, m_session ); + itemJob->setFetchScope( m_monitor->itemFetchScope() ); + + // ### HACK: itemsReceivedFromJob needs to know which collection items were added to. + // That is not provided by akonadi, so we attach it in a property. + itemJob->setProperty( ItemFetchCollectionId(), QVariant( parent.id() ) ); + + q->connect( itemJob, SIGNAL( itemsReceived( const Akonadi::Item::List& ) ), + q, SLOT( itemsFetched( const Akonadi::Item::List& ) ) ); + q->connect( itemJob, SIGNAL( result( KJob* ) ), + q, SLOT( fetchJobDone( KJob* ) ) ); +} + +void EntityTreeModelPrivate::fetchCollections( const Collection &collection, CollectionFetchJob::Type type ) +{ + Q_Q( EntityTreeModel ); + CollectionFetchJob *job = new CollectionFetchJob( collection, type, m_session ); + job->includeUnsubscribed( m_includeUnsubscribed ); + job->includeStatistics( m_includeStatistics ); + job->setContentMimeTypes( m_monitor->mimeTypesMonitored() ); + q->connect( job, SIGNAL( collectionsReceived( const Akonadi::Collection::List& ) ), + q, SLOT( collectionsFetched( const Akonadi::Collection::List& ) ) ); + q->connect( job, SIGNAL( result( KJob* ) ), + q, SLOT( fetchJobDone( KJob* ) ) ); +} + +void EntityTreeModelPrivate::collectionsFetched( const Akonadi::Collection::List& collections ) +{ + // TODO: refactor this stuff into separate methods for listing resources in Collection::root, and listing collections within resources. + Q_Q( EntityTreeModel ); + + Akonadi::AgentManager *agentManager = Akonadi::AgentManager::self(); + + Collection::List _collections = collections; + + forever + { + int collectionsSize = _collections.size(); + + QMutableListIterator it(_collections); + while (it.hasNext()) + { + const Collection col = it.next(); + const Collection::Id parentId = col.parent(); + const Collection::Id colId = col.id(); + + if ( m_collections.contains( parentId ) ) { + insertCollection( col, m_collections.value( parentId ) ); + + if ( m_pendingChildCollections.contains( colId ) ) + { + QList pendingParentIds = m_pendingChildCollections.value( colId ); + + foreach(const Collection::Id &id, pendingParentIds) + { + Collection pendingCollection = m_pendingCollections.value(id); + Q_ASSERT( pendingCollection.isValid() ); + insertPendingCollection( pendingCollection, col, it ); + m_pendingCollections.remove(id); + } + if ( !it.findNext(col) && it.findPrevious(col)) + { + Q_ASSERT("Something went very wrong" == "false"); + } + m_pendingChildCollections.remove( colId ); + } + + it.remove(); + } else { + m_pendingCollections.insert( colId, col ); + m_pendingChildCollections[ parentId ].append( colId ); + } + } + + if ( _collections.isEmpty() ) + break; // forever + + if( _collections.size() == collectionsSize ) + { + // Didn't process any collections this iteration. + // Persist them until the next time collectionsFetched recieves collections. + kWarning() << "Some collections could not be inserted into the model yet."; + break; // forever + } + } +} + +void EntityTreeModelPrivate::itemsFetched( const Akonadi::Item::List& items ) +{ + Q_Q( EntityTreeModel ); + QObject *job = q->sender(); + if ( job ) { + const Collection::Id collectionId = job->property( ItemFetchCollectionId() ).value(); + Item::List itemsToInsert; + Item::List itemsToUpdate; + + const Collection collection = m_collections.value( collectionId ); + + const QList collectionEntities = m_childEntities.value( collectionId ); + foreach ( const Item &item, items ) { + if ( indexOf( collectionEntities, item.id() ) != -1 ) { + itemsToUpdate << item; + } else { + if ( m_mimeChecker.wantedMimeTypes().isEmpty() || m_mimeChecker.isWantedItem( item ) ) { + itemsToInsert << item; + } + } + } + if ( itemsToInsert.size() > 0 ) { + const int startRow = m_childEntities.value( collectionId ).size(); + + const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collectionId ) ); + q->beginInsertRows( parentIndex, startRow, startRow + items.size() - 1 ); + foreach ( const Item &item, items ) { + Item::Id itemId = item.id(); + m_items.insert( itemId, item ); + + Node *node = new Node; + node->id = itemId; + node->parent = collectionId; + node->type = Node::Item; + + m_childEntities[ collectionId ].append( node ); + } + q->endInsertRows(); + } + } +} + +void EntityTreeModelPrivate::monitoredMimeTypeChanged( const QString & mimeType, bool monitored ) +{ + if ( monitored ) + m_mimeChecker.addWantedMimeType( mimeType ); + else + m_mimeChecker.removeWantedMimeType( mimeType ); +} + +void EntityTreeModelPrivate::retrieveAncestors(const Akonadi::Collection& collection) +{ + Q_Q( EntityTreeModel ); + // Unlike fetchCollections, this method fetches collections by traversing up, not down. + CollectionFetchJob *job = new CollectionFetchJob( Collection( collection.parent() ), CollectionFetchJob::Base, m_session ); + job->includeUnsubscribed( m_includeUnsubscribed ); + job->includeStatistics( m_includeStatistics ); + q->connect( job, SIGNAL( collectionsReceived( const Akonadi::Collection::List& ) ), + q, SLOT( ancestorsFetched( const Akonadi::Collection::List& ) ) ); + q->connect( job, SIGNAL( result( KJob* ) ), + q, SLOT( fetchJobDone( KJob* ) ) ); +} + +void EntityTreeModelPrivate::ancestorsFetched(const Akonadi::Collection::List& collectionList) +{ + // List is a size of one. + foreach(const Collection &collection, collectionList) + { + // We should find a collection already in the tree before we reach the collection root. + // We're looking to bridge a gap here. + Q_ASSERT(collection != Collection::root()); + + // We already checked this either on the previous recursion or in monitoredCollectionAdded. + Q_ASSERT(!m_collections.contains(collection.id())); + + m_ancestors.prepend(collection); + if (m_collections.contains(collection.parent())) + { + m_ancestors.prepend( m_collections.value(collection.parent()) ); + insertAncestors(m_ancestors); + } else { + retrieveAncestors(collection); + } + } +} + +void EntityTreeModelPrivate::insertAncestors(const Akonadi::Collection::List& collectionList) +{ + Collection::List::const_iterator it; + const Collection::List::const_iterator begin = collectionList.constBegin() + 1; + const Collection::List::const_iterator end = collectionList.constEnd(); + for (it = begin; it != end; ++it) + { + insertCollection(*it, *(it-1)); + } + m_ancestors.clear(); +} + +void EntityTreeModelPrivate::insertCollection( const Akonadi::Collection& collection, const Akonadi::Collection& parent ) +{ + Q_ASSERT(collection.isValid()); + Q_ASSERT(parent.isValid()); + + Q_Q( EntityTreeModel ); + // TODO: Use order attribute of parent if available + // Otherwise prepend collections and append items. Currently this prepends all collections. + + // Or I can prepend and append for single signals, then 'change' the parent. + +// QList childCols = m_childEntities.value( parent.id() ); +// int row = childCols.size(); +// int numChildCols = childCollections.value(parent.id()).size(); + + const int row = 0; + const QModelIndex parentIndex = q->indexForCollection( parent ); + q->beginInsertRows( parentIndex, row, row ); + m_collections.insert( collection.id(), collection ); + + Node *node = new Node; + node->id = collection.id(); + node->parent = parent.id(); + node->type = Node::Collection; + m_childEntities[ parent.id() ].prepend( node ); + q->endInsertRows(); + + if ( m_itemPopulation == EntityTreeModel::ImmediatePopulation ) + fetchItems( collection ); +} + +void EntityTreeModelPrivate::insertPendingCollection( const Akonadi::Collection& collection, const Akonadi::Collection& parent, QMutableListIterator &colIt ) +{ + insertCollection(collection, parent); + m_pendingCollections.remove( collection.id() ); + + if (colIt.findPrevious(collection) || colIt.findNext(collection)) + { + colIt.remove(); + } + + Q_ASSERT(m_collections.contains(parent.id())); + + QList pendingChildCollectionsToInsert = m_pendingChildCollections.value(collection.id()); + + QList::const_iterator it; + const QList::const_iterator begin = pendingChildCollectionsToInsert.constBegin(); + const QList::const_iterator end = pendingChildCollectionsToInsert.constEnd(); + + for ( it = begin; it != end; ++it ) + { + insertPendingCollection(m_pendingCollections.value( *it ), collection, colIt); + } + + m_pendingChildCollections.remove( parent.id() ); +} + +void EntityTreeModelPrivate::monitoredCollectionAdded( const Akonadi::Collection& collection, const Akonadi::Collection& parent ) +{ + + // Some collection trees contain multiple mimetypes. Even though server side filtering ensures we + // only get the ones we're interested in from the job, we have to filter on collections received through signals too. + if ( !m_mimeChecker.wantedMimeTypes().isEmpty() || !m_mimeChecker.isWantedCollection( collection ) ) + return; + + if (!m_collections.contains(parent.id())) + { + // The collection we're interested in is contained in a collection we're not interested in. + // We download the ancestors of the collection we're interested in to complete the tree. + m_ancestors.prepend(collection); + retrieveAncestors(collection); + return; + } + + insertCollection(collection, parent); + +} + +void EntityTreeModelPrivate::monitoredCollectionRemoved( const Akonadi::Collection& collection ) +{ + Q_Q( EntityTreeModel ); + + + // This may be a signal for a collection we've already removed by removing its ancestor. + if (!m_collections.contains(collection.id())) + return; + + const int row = indexOf( m_childEntities.value( collection.parent() ), collection.id() ); + + const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.parent() ) ); + + q->beginRemoveRows( parentIndex, row, row ); + + // Delete all descendant collections and items. + removeChildEntities(collection.id()); + + // Remove deleted collection from its parent. + m_childEntities[ collection.parent() ].removeAt( row ); + + q->endRemoveRows(); +} + +void EntityTreeModelPrivate::removeChildEntities(Collection::Id colId) +{ + + QList::const_iterator it; + QList childList = m_childEntities.value(colId); + const QList::const_iterator begin = childList.constBegin(); + const QList::const_iterator end = childList.constEnd(); + for (it = begin; it != end; ++it) + { + if (Node::Item == (*it)->type) + { + m_items.remove((*it)->id); + } else { + removeChildEntities((*it)->id); + m_collections.remove((*it)->id); + } + } + m_childEntities.remove(colId); +} + +void EntityTreeModelPrivate::monitoredCollectionMoved( const Akonadi::Collection& collection, + const Akonadi::Collection& sourceCollection, + const Akonadi::Collection& destCollection ) +{ + Q_Q( EntityTreeModel ); + + const int srcRow = indexOf( m_childEntities.value( sourceCollection.id() ), collection.id() ); + + const QModelIndex srcParentIndex = q->indexForCollection( sourceCollection ); + const QModelIndex destParentIndex = q->indexForCollection( destCollection ); + + const int destRow = 0; // Prepend collections + +// TODO: Uncomment for Qt4.6 +// q->beginMoveRows( srcParentIndex, srcRow, srcRow, destParentIndex, destRow ); +// Node *node = m_childEntities[ sourceCollection.id() ].takeAt( srcRow ); +// m_childEntities[ destCollection.id() ].prepend( node ); +// q->endMoveRows(); +} + +void EntityTreeModelPrivate::monitoredCollectionChanged( const Akonadi::Collection &collection ) +{ + Q_Q( EntityTreeModel ); + + if ( m_collections.contains( collection.id() ) ) + m_collections[ collection.id() ] = collection; + + const QModelIndex index = q->indexForCollection( collection ); + q->dataChanged( index, index ); +} + +void EntityTreeModelPrivate::monitoredCollectionStatisticsChanged( Akonadi::Collection::Id id, + const Akonadi::CollectionStatistics &statistics ) +{ + Q_Q( EntityTreeModel ); + + if ( !m_collections.contains( id ) ) { + kWarning() << "Got statistics response for non-existing collection:" << id; + } else { + m_collections[ id ].setStatistics( statistics ); + + const QModelIndex index = q->indexForCollection( m_collections[ id ] ); + q->dataChanged( index, index ); + } +} + +void EntityTreeModelPrivate::monitoredItemAdded( const Akonadi::Item& item, const Akonadi::Collection& collection ) +{ + Q_Q( EntityTreeModel ); + + if ( !m_mimeChecker.wantedMimeTypes().isEmpty() || !m_mimeChecker.isWantedItem( item ) ) + return; + + const int row = m_childEntities.value( collection.id() ).size(); + + const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.id() ) ); + + q->beginInsertRows( parentIndex, row, row ); + m_items.insert( item.id(), item ); + Node *node = new Node; + node->id = item.id(); + node->parent = collection.id(); + node->type = Node::Item; + m_childEntities[ collection.id() ].append( node ); + q->endInsertRows(); +} + +void EntityTreeModelPrivate::monitoredItemRemoved( const Akonadi::Item &item ) +{ + Q_Q( EntityTreeModel ); + + const Collection::List parents = getParentCollections( item ); + if ( parents.isEmpty() ) + return; + const Collection collection = parents.first(); + + const int row = indexOf( m_childEntities.value( collection.id() ), item.id() ); + + const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.id() ) ); + + q->beginRemoveRows( parentIndex, row, row ); + m_items.remove( item.id() ); + m_childEntities[ collection.id() ].removeAt( row ); + q->endRemoveRows(); +} + +void EntityTreeModelPrivate::monitoredItemChanged( const Akonadi::Item &item, const QSet& ) +{ + Q_Q( EntityTreeModel ); + m_items[ item.id() ] = item; + + const QModelIndexList indexes = q->indexesForItem( item ); + foreach ( const QModelIndex &index, indexes ) + q->dataChanged( index, index ); +} + +void EntityTreeModelPrivate::monitoredItemMoved( const Akonadi::Item& item, + const Akonadi::Collection& sourceItem, + const Akonadi::Collection& destItem ) +{ + Q_Q( EntityTreeModel ); + + const Item::Id itemId = item.id(); + + const int srcRow = indexOf( m_childEntities.value( sourceItem.id() ), itemId ); + + const QModelIndex srcIndex = q->indexForCollection( sourceItem ); + const QModelIndex destIndex = q->indexForCollection( destItem ); + + // Where should it go? Always append items and prepend collections and reorganize them with separate reactions to Attributes? + + const int destRow = q->rowCount( destIndex ); + +// TODO: Uncomment for Qt4.6 +// q->beginMoveRows( srcIndex, srcRow, srcRow, destIndex, destRow ); +// Node *node = m_childEntities[ sourceItem.id() ].takeAt( srcRow ); +// m_childEntities[ destItem.id() ].append( node ); +// q->endMoveRows(); +} + +void EntityTreeModelPrivate::monitoredItemLinked( const Akonadi::Item& item, const Akonadi::Collection& collection ) +{ + Q_Q( EntityTreeModel ); + + if ( !m_mimeChecker.wantedMimeTypes().isEmpty() || !m_mimeChecker.isWantedItem( item ) ) + return; + + const int row = m_childEntities.value( collection.id() ).size(); + + const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.id() ) ); + + q->beginInsertRows( parentIndex, row, row ); + Node *node = new Node; + node->id = item.id(); + node->parent = collection.id(); + node->type = Node::Item; + m_childEntities[ collection.id()].append( node ); + q->endInsertRows(); +} + +void EntityTreeModelPrivate::monitoredItemUnlinked( const Akonadi::Item& item, const Akonadi::Collection& collection ) +{ + Q_Q( EntityTreeModel ); + + const int row = indexOf( m_childEntities.value( collection.id() ), item.id() ); + + const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.id() ) ); + + q->beginInsertRows( parentIndex, row, row ); + m_childEntities[ collection.id() ].removeAt( row ); + q->endInsertRows(); +} + +void EntityTreeModelPrivate::fetchJobDone( KJob *job ) +{ + Q_ASSERT(m_pendingCollections.isEmpty()); + Q_ASSERT(m_pendingChildCollections.isEmpty()); + + if ( job->error() ) { + kWarning() << "Job error: " << job->errorString() << endl; + } +} + +void EntityTreeModelPrivate::copyJobDone( KJob *job ) +{ + if ( job->error() ) { + kWarning() << "Job error: " << job->errorString() << endl; + } +} + +void EntityTreeModelPrivate::moveJobDone( KJob *job ) +{ + if ( job->error() ) { + kWarning() << "Job error: " << job->errorString() << endl; + } +} + +void EntityTreeModelPrivate::updateJobDone( KJob *job ) +{ + if ( job->error() ) { + // TODO: handle job errors + kWarning() << "Job error:" << job->errorString(); + } else { + // TODO: Is this trying to do the job of collectionstatisticschanged? +// CollectionStatisticsJob *csjob = static_cast( job ); +// Collection result = csjob->collection(); +// collectionStatisticsChanged( result.id(), csjob->statistics() ); + } +} + +void EntityTreeModelPrivate::startFirstListJob() +{ + Q_Q(EntityTreeModel); + + if (m_collections.size() > 0) + return; + + Collection rootCollection; + // Even if the root collection is the invalid collection, we still need to start + // the first list job with Collection::root. + if ( m_showRootCollection ) { + rootCollection = Collection::root(); + // Notify the outside that we're putting collection::root into the model. +// kDebug() << "begin"; + q->beginInsertRows( QModelIndex(), 0, 0 ); + m_collections.insert( rootCollection.id(), rootCollection ); + m_rootNode = new Node; + m_rootNode->id = rootCollection.id(); + m_rootNode->parent = -1; + m_rootNode->type = Node::Collection; + m_childEntities[ -1 ].append( m_rootNode ); +// kDebug() << "why"; + q->endInsertRows(); + } else { + // Otherwise store it silently because it's not part of the usable model. + rootCollection = m_rootCollection; + m_rootNode = new Node; + m_rootNode->id = rootCollection.id(); + m_rootNode->parent = -1; + m_rootNode->type = Node::Collection; + m_collections.insert( rootCollection.id(), rootCollection ); + } + +// kDebug() << "inserting" << rootCollection.id(); + + // Includes recursive trees. Lower levels are fetched in the onRowsInserted slot if + // necessary. + // HACK: fix this for recursive listing if we filter on mimetypes that only exit deeper + // in the hierarchy + if ( ( m_collectionFetchStrategy == EntityTreeModel::FetchFirstLevelChildCollections) + /*|| ( m_collectionFetchStrategy == EntityTreeModel::FetchCollectionsRecursive )*/ ) { + fetchCollections( rootCollection, CollectionFetchJob::FirstLevel ); + } + if ( m_collectionFetchStrategy == EntityTreeModel::FetchCollectionsRecursive ) + fetchCollections( rootCollection, CollectionFetchJob::Recursive ); + // If the root collection is not collection::root, then it could have items, and they will need to be + // retrieved now. + + if ( m_itemPopulation != EntityTreeModel::NoItemPopulation ) { +// kDebug() << (rootCollection == Collection::root()); + if (rootCollection != Collection::root()) + fetchItems( rootCollection ); + } +} + +Collection EntityTreeModelPrivate::getParentCollection( Entity::Id id ) const +{ + QHashIterator > iter( m_childEntities ); + while ( iter.hasNext() ) { + iter.next(); + if ( indexOf( iter.value(), id ) != -1 ) { + return m_collections.value( iter.key() ); + } + } + + return Collection(); +} + +Collection::List EntityTreeModelPrivate::getParentCollections( const Item &item ) const +{ + Collection::List list; + QHashIterator > iter( m_childEntities ); + while ( iter.hasNext() ) { + iter.next(); + if ( indexOf( iter.value(), item.id() ) != -1 ) { + list << m_collections.value( iter.key() ); + } + } + + return list; +} + +Collection EntityTreeModelPrivate::getParentCollection( const Collection &collection ) const +{ + return m_collections.value( collection.parent() ); +} + +Entity::Id EntityTreeModelPrivate::childAt( Collection::Id id, int position, bool *ok ) const +{ + const QList list = m_childEntities.value( id ); + if ( list.size() <= position ) { + *ok = false; + return 0; + } + + *ok = true; + return list.at( position )->id; +} + + +int EntityTreeModelPrivate::indexOf( Collection::Id parent, Collection::Id collectionId ) const +{ + return indexOf( m_childEntities.value( parent ), collectionId ); +} + +Item EntityTreeModelPrivate::getItem( Item::Id id) const +{ + if ( id > 0 ) + id *= -1; + + return m_items.value( id ); +} diff --git a/akonadi/entitytreemodel_p.h b/akonadi/entitytreemodel_p.h new file mode 100644 index 000000000..8213120bc --- /dev/null +++ b/akonadi/entitytreemodel_p.h @@ -0,0 +1,149 @@ +/* + 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. +*/ + +#ifndef ENTITYTREEMODELPRIVATE_H +#define ENTITYTREEMODELPRIVATE_H + +#include +#include + +#include +#include +#include + +#include "entitytreemodel.h" + +struct Node +{ + Akonadi::Entity::Id id; + Akonadi::Entity::Id parent; + + enum Type + { + Item, + Collection + }; + + int type; +}; + +namespace Akonadi +{ +/** + * @internal + */ +class EntityTreeModelPrivate +{ +public: + + EntityTreeModelPrivate( EntityTreeModel *parent ); + EntityTreeModel *q_ptr; + +// void collectionStatisticsChanged( Collection::Id, const Akonadi::CollectionStatistics& ); + + enum RetrieveDepth { + Base, + Recursive + }; + + void fetchCollections( const Collection &collection, CollectionFetchJob::Type = CollectionFetchJob::FirstLevel ); + void fetchItems( const Collection &collection ); + void collectionsFetched( const Akonadi::Collection::List& ); +// void resourceTopCollectionsFetched( const Akonadi::Collection::List& ); + void itemsFetched( const Akonadi::Item::List& ); + + void monitoredCollectionAdded( const Akonadi::Collection&, const Akonadi::Collection& ); + void monitoredCollectionRemoved( const Akonadi::Collection& ); + void monitoredCollectionChanged( const Akonadi::Collection& ); + void monitoredCollectionStatisticsChanged( Akonadi::Collection::Id, const Akonadi::CollectionStatistics& ); + void monitoredCollectionMoved( const Akonadi::Collection&, const Akonadi::Collection&, const Akonadi::Collection& ); + void monitoredItemAdded( const Akonadi::Item&, const Akonadi::Collection& ); + void monitoredItemRemoved( const Akonadi::Item& ); + void monitoredItemChanged( const Akonadi::Item&, const QSet& ); + void monitoredItemMoved( const Akonadi::Item&, const Akonadi::Collection&, const Akonadi::Collection& ); + + void monitoredItemLinked( const Akonadi::Item&, const Akonadi::Collection& ); + void monitoredItemUnlinked( const Akonadi::Item&, const Akonadi::Collection& ); + + void monitoredMimeTypeChanged( const QString &mimeType, bool monitored ); + + Collection getParentCollection( Entity::Id id ) const; + Collection::List getParentCollections( const Item &item ) const; + Collection getParentCollection( const Collection &collection ) const; + Entity::Id childAt( Collection::Id, int position, bool *ok ) const; + int indexOf( Collection::Id parent, Collection::Id id ) const; + Item getItem( Item::Id id ) const; + void removeChildEntities(Collection::Id colId); + void retrieveAncestors(const Akonadi::Collection& collection); + void ancestorsFetched(const Akonadi::Collection::List& collectionList); + void insertCollection(const Akonadi::Collection &collection, const Akonadi::Collection& parent ); + void insertPendingCollection(const Akonadi::Collection &collection, const Akonadi::Collection& parent, QMutableListIterator &it ); + void insertAncestors(const Akonadi::Collection::List &collectionList ); + + + QHash m_collections; + QHash m_items; + QHash > m_childEntities; + QSet m_populatedCols; + Collection::List m_ancestors; + + QHash m_pendingCollections; + QHash > m_pendingChildCollections; + + Monitor *m_monitor; + Collection m_rootCollection; + Node *m_rootNode; + QString m_rootCollectionDisplayName; + QStringList m_mimeTypeFilter; + MimeTypeChecker m_mimeChecker; + EntityTreeModel::CollectionFetchStrategy m_collectionFetchStrategy; + EntityTreeModel::ItemPopulationStrategy m_itemPopulation; + bool m_includeUnsubscribed; + bool m_includeStatistics; + bool m_showRootCollection; + + void startFirstListJob(); + + void fetchJobDone( KJob *job ); + void copyJobDone( KJob *job ); + void moveJobDone( KJob *job ); + void updateJobDone( KJob *job ); + + /** + * Returns the index of the node in @p list with the id @p id. Returns -1 if not found. + */ + int indexOf( const QList &list, Entity::Id id ) const; + + /** + * The id of the collection which starts an item fetch job. This is part of a hack with QObject::sender + * in itemsReceivedFromJob to correctly insert items into the model. + */ + static QByteArray ItemFetchCollectionId() { + return "ItemFetchCollectionId"; + } + + Session *m_session; + + Q_DECLARE_PUBLIC( EntityTreeModel ) +}; + +} + +#endif + diff --git a/akonadi/entitytreeviewstatesaver.cpp b/akonadi/entitytreeviewstatesaver.cpp new file mode 100644 index 000000000..72e158925 --- /dev/null +++ b/akonadi/entitytreeviewstatesaver.cpp @@ -0,0 +1,207 @@ +/* + 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. +*/ + +#include "entitytreeviewstatesaver.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace Akonadi { + +struct State +{ + State() : selected( false ), expanded( false ) {} + bool selected; + bool expanded; +}; + +class EntityTreeViewStateSaverPrivate +{ + public: + EntityTreeViewStateSaverPrivate( EntityTreeViewStateSaver *parent ) : + q( parent ), + view( 0 ), + horizontalScrollBarValue( -1 ), + verticalScrollBarValue( -1 ) + { + } + + inline bool hasChanges() const + { + return !pendingCollectionChanges.isEmpty() || !pendingItemChanges.isEmpty(); + } + + static inline QString key( const QModelIndex &index ) + { + const Collection c = index.data( EntityTreeModel::CollectionRole ).value(); + if ( c.isValid() ) + return QString::fromLatin1( "c%1" ).arg( c.id() ); + return QString::fromLatin1( "i%1" ).arg( index.data( EntityTreeModel::ItemIdRole ).value() ); + } + + void saveState( const QModelIndex &index, QStringList &selection, QStringList &expansion ) + { + const QString cfgKey = key( index ); + if ( view->selectionModel()->isSelected( index ) ) + selection.append( cfgKey ); + if ( view->isExpanded( index ) ) + expansion.append( cfgKey ); + for ( int i = 0; i < view->model()->rowCount( index ); ++i ) { + const QModelIndex child = view->model()->index( i, 0, index ); + saveState( child, selection, expansion ); + } + } + + inline void restoreState( const QModelIndex &index, const State &state ) + { + if ( state.selected ) + view->selectionModel()->select( index, QItemSelectionModel::Select | QItemSelectionModel::Rows ); + if ( state.expanded ) + view->setExpanded( index, true ); + QTimer::singleShot( 0, q, SLOT(restoreScrollBarState()) ); + } + + void restoreState( const QModelIndex &index ) + { + const Collection c = index.data( EntityTreeModel::CollectionRole ).value(); + if ( c.isValid() ) { + if ( pendingCollectionChanges.contains( c.id() ) ) { + restoreState( index, pendingCollectionChanges.value( c.id() ) ); + pendingCollectionChanges.remove( c.id() ); + } + } else { + Entity::Id itemId = index.data( EntityTreeModel::ItemIdRole ).value(); + if ( pendingItemChanges.contains( itemId ) ) { + restoreState( index, pendingItemChanges.value( itemId ) ); + pendingItemChanges.remove( itemId ); + } + } + for ( int i = 0; i < view->model()->rowCount( index ) && hasChanges(); ++i ) { + const QModelIndex child = view->model()->index( i, 0, index ); + restoreState( child ); + } + } + + inline void restoreScrollBarState() + { + if ( horizontalScrollBarValue >= 0 && horizontalScrollBarValue <= view->horizontalScrollBar()->maximum() ) { + view->horizontalScrollBar()->setValue( horizontalScrollBarValue ); + horizontalScrollBarValue = -1; + } + if ( verticalScrollBarValue >= 0 && verticalScrollBarValue <= view->verticalScrollBar()->maximum() ) { + view->verticalScrollBar()->setValue( verticalScrollBarValue ); + verticalScrollBarValue = -1; + } + } + + void rowsInserted( const QModelIndex &index, int start, int end ) + { + if ( !hasChanges() ) { + QObject::disconnect( view->model(), SIGNAL(rowsInserted(QModelIndex,int,int)), q, SLOT(rowsInserted(QModelIndex,int,int)) ); + return; + } + + for ( int i = start; i <= end && hasChanges(); ++i ) { + const QModelIndex child = index.child( i, 0 ); + restoreState( child ); + } + } + + EntityTreeViewStateSaver *q; + QTreeView *view; + QHash pendingCollectionChanges, pendingItemChanges; + int horizontalScrollBarValue, verticalScrollBarValue; +}; + +EntityTreeViewStateSaver::EntityTreeViewStateSaver( QTreeView * view ) : + QObject( view ), + d( new EntityTreeViewStateSaverPrivate( this ) ) +{ + d->view = view; +} + +EntityTreeViewStateSaver::~EntityTreeViewStateSaver() +{ + delete d; +} + +void EntityTreeViewStateSaver::saveState( KConfigGroup &configGroup ) const +{ + configGroup.deleteGroup(); + QStringList selection, expansion; + for ( int i = 0; i < d->view->model()->rowCount(); ++i ) { + const QModelIndex index = d->view->model()->index( i, 0 ); + d->saveState( index, selection, expansion ); + } + configGroup.writeEntry( "Selection", selection ); + configGroup.writeEntry( "Expansion", expansion ); + configGroup.writeEntry( "ScrollBarHorizontal", d->view->horizontalScrollBar()->value() ); + configGroup.writeEntry( "ScrollBarVertical", d->view->verticalScrollBar()->value() ); +} + +void EntityTreeViewStateSaver::restoreState (const KConfigGroup & configGroup) const +{ + const QStringList selection = configGroup.readEntry( "Selection", QStringList() ); + foreach( const QString &key, selection ) { + Entity::Id id = key.mid( 1 ).toLongLong(); + if ( id < 0 ) + continue; + if ( key.startsWith( QLatin1Char( 'c' ) ) ) + d->pendingCollectionChanges[id].selected = true; + else if ( key.startsWith( QLatin1Char( 'i' ) ) ) + d->pendingItemChanges[id].selected = true; + } + + const QStringList expansion = configGroup.readEntry( "Expansion", QStringList() ); + foreach( const QString &key, expansion ) { + Entity::Id id = key.mid( 1 ).toLongLong(); + if ( id < 0 ) + continue; + if ( key.startsWith( QLatin1Char( 'c' ) ) ) + d->pendingCollectionChanges[id].expanded = true; + else if ( key.startsWith( QLatin1Char( 'i' ) ) ) + d->pendingItemChanges[id].expanded = true; + } + + d->horizontalScrollBarValue = configGroup.readEntry( "ScrollBarHorizontal", -1 ); + d->verticalScrollBarValue = configGroup.readEntry( "ScrollBarVertical", -1 ); + + // initial restore run, for everything already loaded + for ( int i = 0; i < d->view->model()->rowCount() && d->hasChanges(); ++i ) { + const QModelIndex index = d->view->model()->index( i, 0 ); + d->restoreState( index ); + } + d->restoreScrollBarState(); + + // watch the model for stuff coming in delayed + if ( d->hasChanges() ) + connect( d->view->model(), SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int)) ); +} + +} // namespace Akonadi + +#include "entitytreeviewstatesaver.moc" diff --git a/akonadi/entitytreeviewstatesaver.h b/akonadi/entitytreeviewstatesaver.h new file mode 100644 index 000000000..f23aa144b --- /dev/null +++ b/akonadi/entitytreeviewstatesaver.h @@ -0,0 +1,130 @@ +/* + 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_ENTITYTREEVIEWSTATESAVER_H +#define AKONADI_ENTITYTREEVIEWSTATESAVER_H + +#include "akonadi_export.h" + +#include + +class QTreeView; +class KConfigGroup; + +namespace Akonadi { + +class EntityTreeViewStateSaverPrivate; + +/** + * @short A helper class that saves and restores state of an EntityTreeView + * + * This class saves and restores the state of a QTreeView showing an EntityTreeModel, + * or whatever proxies are stacked on top of one. State so far means the current selection + * and expansion of the items and collections. + * + * The EntityTreeViewStateSaver is needed because populating the EntityTreeModel + * is asynchronous and finding the right point in time to restore the state is a + * difficult task that is hidden inside this class. + * + * Example: + * + * @code + * + * class MyWidget : public QWidget + * { + * public: + * MyWidget( QWidget* parent ); + * ~MyWidget(); + * + * ... + * + * private: + * KConfig *mGlobalConfig; + * QTreeView *mTreeView; + * Akonadi::EntityTreeViewStateSaver *mStateSaver; + * }; + * + * MyWidget::MyWidget( QWidget *parent ) + * : QWidget( parent ) + * { + * mGlobalConfig = ... + * mTreeView = new QTreeView( this ); + * ... + * + * mStateSaver = new Akonadi::EntityTreeViewStateSaver( mTreeView ); + * + * KConfigGroup group( mGlobalConfig, "treeViewState" ); + * mStateSaver->restoreState( group ); + * } + * + * MyWidget::~MyWidget() + * { + * KConfigGroup group( mGlobalConfig, "treeViewState" ); + * mStateSaver->saveState( group ); + * } + * + * @endcode + * + * @author Volker Krause + * @since 4.4 + */ +class AKONADI_EXPORT EntityTreeViewStateSaver : public QObject +{ + Q_OBJECT + + public: + /** + * Creates a new state saver, for saving or restoring. + * + * @param view The QTreeView which state should be saved/restored. + */ + EntityTreeViewStateSaver( QTreeView* view ); + + /** + * Destroys this state saver. + */ + ~EntityTreeViewStateSaver(); + + /** + * Stores the current state in the given config group. + * + * @param configGroup Config file group into which the state is supposed to be stored. + */ + void saveState( KConfigGroup &configGroup ) const; + + /** + * Restore the state stored in @p configGroup as soon as the corresponding entities + * become available in the model (the model is populated asynchronously, which is the + * main reason why you want to use this class). + * + * @param configGroup Config file group containing a previously stored ETM state. + */ + void restoreState( const KConfigGroup &configGroup ) const; + + private: + //@cond PRIVATE + EntityTreeViewStateSaverPrivate* const d; + Q_PRIVATE_SLOT( d, void rowsInserted( const QModelIndex&, int, int ) ) + Q_PRIVATE_SLOT( d, void restoreScrollBarState() ) + //@endcond PRIVATE +}; + +} + +#endif diff --git a/akonadi/favoritecollectionsmodel.cpp b/akonadi/favoritecollectionsmodel.cpp new file mode 100644 index 000000000..a85c962eb --- /dev/null +++ b/akonadi/favoritecollectionsmodel.cpp @@ -0,0 +1,208 @@ +/* + Copyright (c) 2009 Kevin Ottens + + + 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 "favoritecollectionsmodel.h" + +#include + +#include +#include +#include + +#include "entitytreemodel.h" + +using namespace Akonadi; + +/** + * @internal + */ +class FavoriteCollectionsModel::Private +{ + public: + Private( FavoriteCollectionsModel *parent ) + : q( parent ) + { + } + + QString labelForCollection( const Collection &c ) + { + if ( labelMap.contains( c.id() ) ) { + return labelMap[c.id()]; + } + + EntityTreeModel *model = qobject_cast( q->sourceModel() ); + Q_ASSERT( model!=0 ); + + QModelIndex index = model->indexForCollection( model->collectionForId( c.id() ) ); + return model->data(index).toString(); + } + + void clearAndUpdateSelection() + { + q->selectionModel()->clear(); + updateSelection(); + } + + void updateSelection() + { + EntityTreeModel *model = qobject_cast( q->sourceModel() ); + Q_ASSERT( model!=0 ); + + foreach (const Collection &c, collections) { + QModelIndex index = model->indexForCollection( model->collectionForId( c.id() ) ); + q->selectionModel()->select( index, + QItemSelectionModel::Select ); + } + } + + void loadConfig() + { + KSharedConfigPtr config = KGlobal::config(); + KConfigGroup group = config->group( "FavoriteCollectionsModel" ); + QList ids = group.readEntry( "FavoriteCollectionIds", QList() ); + QStringList labels = group.readEntry( "FavoriteCollectionLabels", QStringList() ); + + for ( int i=0; i ids; + QStringList labels; + + foreach ( const Collection &c, collections ) { + ids << c.id(); + labels << labelForCollection( c ); + } + + KSharedConfigPtr config = KGlobal::config(); + KConfigGroup group = config->group( "FavoriteCollectionsModel" ); + group.writeEntry( "FavoriteCollectionIds", ids ); + group.writeEntry( "FavoriteCollectionLabels", labels ); + config->sync(); + } + + FavoriteCollectionsModel * const q; + + Collection::List collections; + QHash labelMap; +}; + +FavoriteCollectionsModel::FavoriteCollectionsModel( EntityTreeModel *source, QObject *parent ) + : SelectionProxyModel( new QItemSelectionModel( source, parent ), parent ), + d( new Private( this ) ) +{ + setSourceModel( source ); + setFilterBehavior(OnlySelected); + + connect( source, SIGNAL( modelReset() ), this, SLOT( clearAndUpdateSelection() ) ); + connect( source, SIGNAL( layoutChanged() ), this, SLOT( clearAndUpdateSelection() ) ); + connect( source, SIGNAL( rowsInserted(QModelIndex, int, int) ), this, SLOT( updateSelection() ) ); + + d->loadConfig(); + d->clearAndUpdateSelection(); +} + +FavoriteCollectionsModel::~FavoriteCollectionsModel() +{ + delete d; +} + +void FavoriteCollectionsModel::setCollections( const Collection::List &collections ) +{ + d->collections = collections; + d->labelMap.clear(); + d->clearAndUpdateSelection(); + d->saveConfig(); +} + +void FavoriteCollectionsModel::addCollection( const Collection &collection ) +{ + d->collections << collection; + d->updateSelection(); + d->saveConfig(); +} + +void FavoriteCollectionsModel::removeCollection( const Collection &collection ) +{ + d->collections.removeAll( collection ); + d->labelMap.remove( collection.id() ); + + EntityTreeModel *model = qobject_cast( sourceModel() ); + Q_ASSERT( model!=0 ); + + QModelIndex index = model->indexForCollection( model->collectionForId( collection.id() ) ); + selectionModel()->select( index, + QItemSelectionModel::Deselect ); + + d->updateSelection(); + d->saveConfig(); +} + +Collection::List FavoriteCollectionsModel::collections() const +{ + return d->collections; +} + +void Akonadi::FavoriteCollectionsModel::setFavoriteLabel( const Collection &collection, const QString &label ) +{ + Q_ASSERT( d->collections.contains( collection ) ); + d->labelMap[ collection.id() ] = label; + d->saveConfig(); + + EntityTreeModel *model = qobject_cast( sourceModel() ); + Q_ASSERT( model!=0 ); + + QModelIndex sourceIndex = model->indexForCollection( model->collectionForId( collection.id() ) ); + QModelIndex index = mapFromSource( sourceIndex ); + emit dataChanged( index, index ); +} + +QVariant Akonadi::FavoriteCollectionsModel::data( const QModelIndex &index, int role) const +{ + if ( index.column()==0 && role == Qt::DisplayRole ) { + QModelIndex sourceIndex = mapToSource( index ); + Collection c = sourceModel()->data( sourceIndex, EntityTreeModel::CollectionRole ).value(); + + return d->labelForCollection( c ); + } else { + return SelectionProxyModel::data( index, role ); + } +} + +QVariant FavoriteCollectionsModel::headerData( int section, Qt::Orientation orientation, int role) const +{ + if ( section == 0 + && orientation == Qt::Horizontal + && role == Qt::DisplayRole ) { + return QLatin1String("Favorite Folders"); + } else { + return SelectionProxyModel::headerData( section, orientation, role ); + } +} + +#include "favoritecollectionsmodel.moc" + diff --git a/akonadi/favoritecollectionsmodel.h b/akonadi/favoritecollectionsmodel.h new file mode 100644 index 000000000..285df1697 --- /dev/null +++ b/akonadi/favoritecollectionsmodel.h @@ -0,0 +1,84 @@ +/* + Copyright (c) 2009 Kevin Ottens + + 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_FAVORITECOLLECTIONSMODEL_H +#define AKONADI_FAVORITECOLLECTIONSMODEL_H + +#include "akonadi_export.h" + +#include "selectionproxymodel.h" + +#include + +namespace Akonadi { + +class EntityTreeModel; + +/** + * @short A model that lists a set of favorite collections. + * + * @author Kevin Ottens + * @since 4.4 + */ +class AKONADI_EXPORT FavoriteCollectionsModel : public SelectionProxyModel +{ + Q_OBJECT + + public: + /** + * Creates a new model listing a chosen set of favorite collections. + * + * @param parent The parent object. + */ + explicit FavoriteCollectionsModel( EntityTreeModel *source, QObject *parent = 0 ); + + /** + * Destroys the favorite collections model. + */ + virtual ~FavoriteCollectionsModel(); + + /** + * Returns the list of favorite collections. + */ + Collection::List collections() const; + + virtual QVariant data( const QModelIndex & index, int role = Qt::DisplayRole ) const; + virtual QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const; + + public Q_SLOTS: + void setCollections( const Collection::List &collections ); + void addCollection( const Collection &collection ); + void removeCollection( const Collection &collection ); + void setFavoriteLabel( const Collection &collection, const QString &label ); + + private: + using SelectionProxyModel::setSourceModel; + + Q_PRIVATE_SLOT( d, void clearAndUpdateSelection() ) + Q_PRIVATE_SLOT( d, void updateSelection() ) + + //@cond PRIVATE + class Private; + Private* const d; + //@endcond +}; + +} + +#endif diff --git a/akonadi/itemcreatejob.cpp b/akonadi/itemcreatejob.cpp index 74cf39325..7b212fbaa 100644 --- a/akonadi/itemcreatejob.cpp +++ b/akonadi/itemcreatejob.cpp @@ -1,175 +1,175 @@ /* Copyright (c) 2006 - 2007 Volker Krause Copyright (c) 2007 Robert Zwerus 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 "itemcreatejob.h" #include "collection.h" #include "imapparser_p.h" #include "item.h" #include "itemserializer_p.h" #include "job_p.h" #include "protocolhelper_p.h" #include #include using namespace Akonadi; class Akonadi::ItemCreateJobPrivate : public JobPrivate { public: ItemCreateJobPrivate( ItemCreateJob *parent ) : JobPrivate( parent ) { } Collection mCollection; Item mItem; QSet mParts; Item::Id mUid; QDateTime mDatetime; QByteArray mData; }; ItemCreateJob::ItemCreateJob( const Item &item, const Collection &collection, QObject * parent ) : Job( new ItemCreateJobPrivate( this ), parent ) { Q_D( ItemCreateJob ); Q_ASSERT( !item.mimeType().isEmpty() ); d->mItem = item; d->mParts = d->mItem.loadedPayloadParts(); d->mCollection = collection; } ItemCreateJob::~ItemCreateJob() { } void ItemCreateJob::doStart() { Q_D( ItemCreateJob ); QByteArray remoteId; QList flags; flags.append( "\\MimeType[" + d->mItem.mimeType().toLatin1() + ']' ); if ( !d->mItem.remoteId().isEmpty() ) flags.append( ImapParser::quote( "\\RemoteId[" + d->mItem.remoteId().toUtf8() + ']' ) ); flags += d->mItem.flags().toList(); // switch between a normal APPEND and a multipart X-AKAPPEND, based on the number of parts if ( d->mItem.attributes().isEmpty() && ( d->mParts.isEmpty() || (d->mParts.size() == 1 && d->mParts.contains( Item::FullPayload )) ) ) { if ( d->mItem.hasPayload() ) { int version = 0; ItemSerializer::serialize( d->mItem, Item::FullPayload, d->mData, version ); } int dataSize = d->mData.size(); d->writeData( d->newTag() + " APPEND " + QByteArray::number( d->mCollection.id() ) + ' ' + QByteArray::number( d->mItem.size() ) + " (" + ImapParser::join( flags, " " ) + ") {" + QByteArray::number( dataSize ) + "}\n" ); } else { // do a multipart X-AKAPPEND QByteArray command = d->newTag() + " X-AKAPPEND " + QByteArray::number( d->mCollection.id() ) + ' ' + QByteArray::number( d->mItem.size() ) + " (" + ImapParser::join( flags, " " ) + ") "; QList partSpecs; int totalSize = 0; foreach( const QByteArray &partName, d->mParts ) { QByteArray partData; int version = 0; ItemSerializer::serialize( d->mItem, partName, partData, version ); totalSize += partData.size(); const QByteArray partId = ProtocolHelper::encodePartIdentifier( ProtocolHelper::PartPayload, partName, version ); partSpecs.append( ImapParser::quote( partId ) + ':' + QByteArray::number( partData.size() ) ); d->mData += partData; } foreach ( const Attribute* attr, d->mItem.attributes() ) { const QByteArray data = attr->serialized(); totalSize += data.size(); const QByteArray partId = ProtocolHelper::encodePartIdentifier( ProtocolHelper::PartAttribute, attr->type() ); partSpecs.append( ImapParser::quote( partId ) + ':' + QByteArray::number( data.size() ) ); d->mData += data; } command += '(' + ImapParser::join( partSpecs, "," ) + ") " + '{' + QByteArray::number( totalSize ) + "}\n"; d->writeData( command ); } } void ItemCreateJob::doHandleResponse( const QByteArray & tag, const QByteArray & data ) { Q_D( ItemCreateJob ); if ( tag == "+" ) { // ready for literal data d->writeData( d->mData ); if ( !d->mData.endsWith( '\n' ) ) d->writeData( "\n" ); return; } if ( tag == d->tag() ) { int uidNextPos = data.indexOf( "UIDNEXT" ); if ( uidNextPos != -1 ) { bool ok = false; ImapParser::parseNumber( data, d->mUid, &ok, uidNextPos + 7 ); if ( !ok ) { - kDebug( 5250 ) << "Invalid UIDNEXT response to APPEND command: " - << tag << data; + kDebug() << "Invalid UIDNEXT response to APPEND command: " + << tag << data; } } int dateTimePos = data.indexOf( "DATETIME" ); if ( dateTimePos != -1 ) { int resultPos = ImapParser::parseDateTime( data, d->mDatetime, dateTimePos + 8 ); if ( resultPos == (dateTimePos + 8) ) { - kDebug( 5250 ) << "Invalid DATETIME response to APPEND command: " - << tag << data; + kDebug() << "Invalid DATETIME response to APPEND command: " + << tag << data; } } } } Item ItemCreateJob::item() const { Q_D( const ItemCreateJob ); if ( d->mUid == 0 ) return Item(); Item item( d->mItem ); item.setId( d->mUid ); item.setRevision( 0 ); item.setModificationTime( d->mDatetime ); return item; } Collection ItemCreateJob::collection() const { Q_D( const ItemCreateJob ); return d->mCollection; } #include "itemcreatejob.moc" diff --git a/akonadi/itemfetchjob.cpp b/akonadi/itemfetchjob.cpp index 52fe3df34..5ebdb9ea2 100644 --- a/akonadi/itemfetchjob.cpp +++ b/akonadi/itemfetchjob.cpp @@ -1,334 +1,334 @@ /* Copyright (c) 2006 - 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 "itemfetchjob.h" #include "attributefactory.h" #include "collection.h" #include "collectionselectjob_p.h" #include "imapparser_p.h" #include "itemfetchscope.h" #include "itemserializer_p.h" #include "itemserializerplugin.h" #include "job_p.h" #include "entity_p.h" #include "protocol_p.h" #include "protocolhelper_p.h" #include #include #include #include #include using namespace Akonadi; class Akonadi::ItemFetchJobPrivate : public JobPrivate { public: ItemFetchJobPrivate( ItemFetchJob *parent ) : JobPrivate( parent ) { } void timeout() { Q_Q( ItemFetchJob ); mEmitTimer->stop(); // in case we are called by result() if ( !mPendingItems.isEmpty() ) { emit q->itemsReceived( mPendingItems ); mPendingItems.clear(); } } void startFetchJob(); void selectDone( KJob * job ); Q_DECLARE_PUBLIC( ItemFetchJob ) Collection mCollection; Item mItem; Item::List mItems; ItemFetchScope mFetchScope; Item::List mPendingItems; // items pending for emitting itemsReceived() QTimer* mEmitTimer; }; void ItemFetchJobPrivate::startFetchJob() { QByteArray command = newTag(); if ( mItem.isValid() ) command += " " AKONADI_CMD_UID " " AKONADI_CMD_ITEMFETCH " " + QByteArray::number( mItem.id() ); else if ( !mItem.remoteId().isEmpty() ) command += " " AKONADI_CMD_RID " " AKONADI_CMD_ITEMFETCH " " + mItem.remoteId().toUtf8(); else command += " " AKONADI_CMD_ITEMFETCH " 1:*"; if ( mFetchScope.fullPayload() ) command += " " AKONADI_PARAM_FULLPAYLOAD; if ( mFetchScope.allAttributes() ) command += " " AKONADI_PARAM_ALLATTRIBUTES; if ( mFetchScope.cacheOnly() ) command += " " AKONADI_PARAM_CACHEONLY; //TODO: detect somehow if server supports external payload attribute command += " " AKONADI_PARAM_EXTERNALPAYLOAD; command += " (UID REMOTEID COLLECTIONID FLAGS SIZE DATETIME"; foreach ( const QByteArray &part, mFetchScope.payloadParts() ) command += ' ' + ProtocolHelper::encodePartIdentifier( ProtocolHelper::PartPayload, part ); foreach ( const QByteArray &part, mFetchScope.attributes() ) command += ' ' + ProtocolHelper::encodePartIdentifier( ProtocolHelper::PartAttribute, part ); command += ")\n"; writeData( command ); } void ItemFetchJobPrivate::selectDone( KJob * job ) { if ( !job->error() ) // the collection is now selected, fetch the message(s) startFetchJob(); } ItemFetchJob::ItemFetchJob( const Collection &collection, QObject * parent ) : Job( new ItemFetchJobPrivate( this ), parent ) { Q_D( ItemFetchJob ); d->mEmitTimer = new QTimer( this ); d->mEmitTimer->setSingleShot( true ); d->mEmitTimer->setInterval( 100 ); connect( d->mEmitTimer, SIGNAL(timeout()), this, SLOT(timeout()) ); connect( this, SIGNAL(result(KJob*)), this, SLOT(timeout()) ); d->mCollection = collection; } ItemFetchJob::ItemFetchJob( const Item & item, QObject * parent) : Job( new ItemFetchJobPrivate( this ), parent ) { Q_D( ItemFetchJob ); d->mEmitTimer = new QTimer( this ); d->mEmitTimer->setSingleShot( true ); d->mEmitTimer->setInterval( 100 ); connect( d->mEmitTimer, SIGNAL(timeout()), this, SLOT(timeout()) ); connect( this, SIGNAL(result(KJob*)), this, SLOT(timeout()) ); d->mCollection = Collection::root(); d->mItem = item; } ItemFetchJob::~ItemFetchJob() { } void ItemFetchJob::doStart() { Q_D( ItemFetchJob ); if ( !d->mItem.isValid() ) { // collection content listing if ( d->mCollection == Collection::root() ) { setErrorText( QLatin1String("Cannot list root collection.") ); setError( Unknown ); emitResult(); } CollectionSelectJob *job = new CollectionSelectJob( d->mCollection, this ); connect( job, SIGNAL(result(KJob*)), SLOT(selectDone(KJob*)) ); addSubjob( job ); } else d->startFetchJob(); } void ItemFetchJob::doHandleResponse( const QByteArray & tag, const QByteArray & data ) { Q_D( ItemFetchJob ); if ( tag == "*" ) { int begin = data.indexOf( "FETCH" ); if ( begin >= 0 ) { // split fetch response into key/value pairs QList fetchResponse; ImapParser::parseParenthesizedList( data, fetchResponse, begin + 6 ); // create a new item object Item::Id uid = -1; int rev = -1; QString rid; QString mimeType; Entity::Id cid = -1; for ( int i = 0; i < fetchResponse.count() - 1; i += 2 ) { const QByteArray key = fetchResponse.value( i ); const QByteArray value = fetchResponse.value( i + 1 ); if ( key == "UID" ) uid = value.toLongLong(); else if ( key == "REV" ) rev = value.toInt(); else if ( key == "REMOTEID" ) { if ( !value.isEmpty() ) rid = QString::fromUtf8( value ); else rid.clear(); } else if ( key == "COLLECTIONID" ) { cid = value.toInt(); } else if ( key == "MIMETYPE" ) mimeType = QString::fromLatin1( value ); } if ( uid < 0 || rev < 0 || mimeType.isEmpty() ) { - kWarning( 5250 ) << "Broken fetch response: UID, RID, REV or MIMETYPE missing!"; + kWarning() << "Broken fetch response: UID, RID, REV or MIMETYPE missing!"; return; } Item item( uid ); item.setRemoteId( rid ); item.setRevision( rev ); item.setMimeType( mimeType ); item.setStorageCollectionId( cid ); if ( !item.isValid() ) return; // parse fetch response fields for ( int i = 0; i < fetchResponse.count() - 1; i += 2 ) { const QByteArray key = fetchResponse.value( i ); // skip stuff we dealt with already if ( key == "UID" || key == "REV" || key == "REMOTEID" || key == "MIMETYPE" || key == "COLLECTIONID") continue; // flags if ( key == "FLAGS" ) { QList flags; ImapParser::parseParenthesizedList( fetchResponse[i + 1], flags ); foreach ( const QByteArray &flag, flags ) { item.setFlag( flag ); } } else if ( key == "SIZE" ) { const quint64 size = fetchResponse[i + 1].toLongLong(); item.setSize( size ); } else if ( key == "DATETIME" ) { QDateTime datetime; ImapParser::parseDateTime( fetchResponse[i + 1], datetime ); item.setModificationTime( datetime ); } else { int version = 0; QByteArray plainKey( key ); ProtocolHelper::PartNamespace ns; ImapParser::splitVersionedKey( key, plainKey, version ); plainKey = ProtocolHelper::decodePartIdentifier( plainKey, ns ); switch ( ns ) { case ProtocolHelper::PartPayload: { bool isExternal = false; QByteArray fileKey = fetchResponse.value( i + 1 ); if (fileKey == "[FILE]") { isExternal = true; i++; - kDebug( 5250 ) << "Payload is external: " << isExternal << " filename: " << fetchResponse.value( i + 1 ); + kDebug() << "Payload is external: " << isExternal << " filename: " << fetchResponse.value( i + 1 ); } ItemSerializer::deserialize( item, plainKey, fetchResponse.value( i + 1 ), version, isExternal ); break; } case ProtocolHelper::PartAttribute: { Attribute* attr = AttributeFactory::createAttribute( plainKey ); Q_ASSERT( attr ); if ( fetchResponse.value( i + 1 ) == "[FILE]" ) { ++i; QFile f( QString::fromUtf8( fetchResponse.value( i + 1 ) ) ); if ( f.open( QFile::ReadOnly ) ) attr->deserialize( f.readAll() ); else { kWarning() << "Failed to open attribute file: " << fetchResponse.value( i + 1 ); delete attr; } } else { attr->deserialize( fetchResponse.value( i + 1 ) ); } item.addAttribute( attr ); break; } case ProtocolHelper::PartGlobal: default: kWarning() << "Unknown item part type:" << key; } } } item.d_ptr->resetChangeLog(); d->mItems.append( item ); d->mPendingItems.append( item ); if ( !d->mEmitTimer->isActive() ) d->mEmitTimer->start(); return; } } - kDebug( 5250 ) << "Unhandled response: " << tag << data; + kDebug() << "Unhandled response: " << tag << data; } Item::List ItemFetchJob::items() const { Q_D( const ItemFetchJob ); return d->mItems; } void ItemFetchJob::setFetchScope( ItemFetchScope &fetchScope ) { Q_D( ItemFetchJob ); d->mFetchScope = fetchScope; } ItemFetchScope &ItemFetchJob::fetchScope() { Q_D( ItemFetchJob ); return d->mFetchScope; } Item ItemFetchJob::item() const { Q_D( const ItemFetchJob ); return d->mItem; } Collection ItemFetchJob::collection() const { Q_D( const ItemFetchJob ); return d->mCollection; } void ItemFetchJob::setCollection(const Akonadi::Collection& collection) { Q_D( ItemFetchJob ); d->mCollection = collection; } #include "itemfetchjob.moc" diff --git a/akonadi/itemfetchjob.h b/akonadi/itemfetchjob.h index e9ccaf4e4..01cbd5920 100644 --- a/akonadi/itemfetchjob.h +++ b/akonadi/itemfetchjob.h @@ -1,170 +1,170 @@ /* Copyright (c) 2006 - 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_ITEMFETCHJOB_H #define AKONADI_ITEMFETCHJOB_H #include #include namespace Akonadi { class Collection; class ItemFetchJobPrivate; class ItemFetchScope; /** * @short Job that fetches items from the Akonadi storage. * * This class is used to fetch items from the Akonadi storage. * Which parts of the items (e.g. headers only, attachments or all) * can be specified by the ItemFetchScope. * * Example: * * @code * * // Fetch all items with full payload from the root collection * Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob( Akonadi::Collection::root() ); * job->fetchScope().fetchFullPayload(); * * if ( job->exec() ) { * Akonadi::Item::List items = job->items(); * foreach( const Akonadi::Item &item, items ) { * qDebug() << "Item ID:" << item.id(); * } * } else { * qDebug() << "Error occurred"; * } * * @endcode * * @author Volker Krause */ class AKONADI_EXPORT ItemFetchJob : public Job { Q_OBJECT public: /** * Creates a new item fetch job that retrieves all items inside the given collection. * * @param collection The parent collection to fetch all items from. * @param parent The parent object. */ explicit ItemFetchJob( const Collection &collection, QObject *parent = 0 ); /** * Creates a new item fetch job that retrieves the specified item. * If the item has an uid set, this is used to identify the item on the Akonadi * server. If only a remote identifier is available, that one is used. * However, as remote identifier are not necessarily globally unique, you * need to specify the resource and/or collection to search in in that case, * using setCollection() or Akonadi::ResourceSelectJob. * * @param item The item to fetch. * @param parent The parent object. */ explicit ItemFetchJob( const Item &item, QObject *parent = 0 ); /** * Destroys the item fetch job. */ virtual ~ItemFetchJob(); /** * Returns the fetched item. * * @note The items are invalid before the result( KJob* ) * signal has been emitted or if an error occurred. */ Item::List items() const; /** * Sets the item fetch scope. * * The ItemFetchScope controls how much of an item's data is fetched * from the server, e.g. whether to fetch the full item payload or * only meta data. * * @param fetchScope The new scope for item fetch operations. * * @see fetchScope() */ void setFetchScope( ItemFetchScope &fetchScope ); /** * Returns the item fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the ItemFetchScope documentation * for an example. * * @return a reference to the current item fetch scope * * @see setFetchScope() for replacing the current item fetch scope */ ItemFetchScope &fetchScope(); /** * Returns the item passed in on in the constructor or an invalid * item if another constructor, e.g. those with the collection, * was called. */ Item item() const; /** * Returns the collection passed in on in the constructor or set with the * setCollection method. */ Collection collection() const; /** - Specifies the collection the item is in. - This is only required when retrieving an item based on its remote id which might not be - unique globally. - - @see Akonadi::ResourceSelectJob - */ + * Specifies the collection the item is in. + * This is only required when retrieving an item based on its remote id which might not be + * unique globally. + * + * @see Akonadi::ResourceSelectJob + */ void setCollection( const Collection &collection ); Q_SIGNALS: /** * This signal is emitted when the items are fetched completely. * * @param items The fetched items. */ void itemsReceived( const Akonadi::Item::List &items ); protected: virtual void doStart(); virtual void doHandleResponse( const QByteArray &tag, const QByteArray &data ); private: Q_DECLARE_PRIVATE( ItemFetchJob ) //@cond PRIVATE Q_PRIVATE_SLOT( d_func(), void selectDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void timeout() ) //@endcond }; } #endif diff --git a/akonadi/itemmodel.cpp b/akonadi/itemmodel.cpp index db8769201..9c4baff59 100644 --- a/akonadi/itemmodel.cpp +++ b/akonadi/itemmodel.cpp @@ -1,445 +1,445 @@ /* Copyright (c) 2006 - 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 "itemmodel.h" #include "itemfetchjob.h" #include "collectionfetchjob.h" #include "itemfetchscope.h" #include "monitor.h" #include "pastehelper_p.h" #include "session.h" #include #include #include #include #include #include #include using namespace Akonadi; /** * @internal * * This struct is used for optimization reasons. * because it embeds the row. * * Semantically, we could have used an item instead. */ struct ItemContainer { ItemContainer( const Item& i, int r ) { item = i; row = r; } Item item; int row; }; /** * @internal */ class ItemModel::Private { public: Private( ItemModel *parent ) : mParent( parent ), monitor( new Monitor() ) { session = new Session( QCoreApplication::instance()->applicationName().toUtf8() + QByteArray("-ItemModel-") + QByteArray::number( qrand() ), mParent ); monitor->ignoreSession( session ); mParent->connect( monitor, SIGNAL(itemChanged( const Akonadi::Item&, const QSet& )), mParent, SLOT(itemChanged( const Akonadi::Item&, const QSet& )) ); mParent->connect( monitor, SIGNAL(itemMoved( const Akonadi::Item&, const Akonadi::Collection&, const Akonadi::Collection& )), mParent, SLOT(itemMoved( const Akonadi::Item&, const Akonadi::Collection&, const Akonadi::Collection& ) ) ); mParent->connect( monitor, SIGNAL(itemAdded( const Akonadi::Item&, const Akonadi::Collection& )), mParent, SLOT(itemAdded( const Akonadi::Item& )) ); mParent->connect( monitor, SIGNAL(itemRemoved(Akonadi::Item)), mParent, SLOT(itemRemoved(Akonadi::Item)) ); mParent->connect( monitor, SIGNAL(itemLinked(const Akonadi::Item&, const Akonadi::Collection&)), mParent, SLOT(itemAdded(const Akonadi::Item&)) ); mParent->connect( monitor, SIGNAL(itemUnlinked(const Akonadi::Item&, const Akonadi::Collection&)), mParent, SLOT(itemRemoved(const Akonadi::Item&)) ); } ~Private() { delete monitor; } void listingDone( KJob* ); void collectionFetchResult( KJob* ); void itemChanged( const Akonadi::Item&, const QSet& ); void itemsAdded( const Akonadi::Item::List &list ); void itemAdded( const Akonadi::Item &item ); void itemMoved( const Akonadi::Item&, const Akonadi::Collection& src, const Akonadi::Collection& dst ); void itemRemoved( const Akonadi::Item& ); int rowForItem( const Akonadi::Item& ); bool collectionIsCompatible() const; ItemModel *mParent; QList items; QHash itemHash; Collection collection; Monitor *monitor; Session *session; }; bool ItemModel::Private::collectionIsCompatible() const { // in the generic case, we show any collection if ( mParent->mimeTypes() == QStringList( QLatin1String("text/uri-list") ) ) return true; // if the model's mime types are more specific, limit to those // collections that have matching types Q_FOREACH( QString type, mParent->mimeTypes() ) { if ( collection.contentMimeTypes().contains( type ) ) { return true; } } return false; } void ItemModel::Private::listingDone( KJob * job ) { ItemFetchJob *fetch = static_cast( job ); Q_UNUSED( fetch ); if ( job->error() ) { // TODO - kWarning( 5250 ) << "Item query failed:" << job->errorString(); + kWarning() << "Item query failed:" << job->errorString(); } } void ItemModel::Private::collectionFetchResult( KJob * job ) { CollectionFetchJob *fetch = static_cast( job ); Q_ASSERT( fetch->collections().count() == 1 ); // we only listed base Collection c = fetch->collections().first(); // avoid recursion, if this fails for some reason if ( !c.contentMimeTypes().isEmpty() ) { mParent->setCollection(c); } else { - kWarning( 5250 ) << "Failed to retrieve the contents mime type of the collection: " << c; + kWarning() << "Failed to retrieve the contents mime type of the collection: " << c; mParent->setCollection(Collection()); } } int ItemModel::Private::rowForItem( const Akonadi::Item& item ) { ItemContainer *container = itemHash.value( item ); if ( !container ) return -1; /* Try to find the item directly; If items have been removed, this first try won't succeed because the ItemContainer rows have not been updated (costs too much). */ if ( container->row < items.count() && items.at( container->row ) == container ) return container->row; else { // Slow solution if the fist one has not succeeded int row = -1; for ( int i = 0; i < items.size(); ++i ) { if ( items.at( i )->item == item ) { row = i; break; } } return row; } } void ItemModel::Private::itemChanged( const Akonadi::Item &item, const QSet& ) { int row = rowForItem( item ); if ( row < 0 ) return; items[ row ]->item = item; itemHash.remove( item ); itemHash[ item ] = items[ row ]; QModelIndex start = mParent->index( row, 0, QModelIndex() ); QModelIndex end = mParent->index( row, mParent->columnCount( QModelIndex() ) - 1 , QModelIndex() ); mParent->dataChanged( start, end ); } void ItemModel::Private::itemMoved( const Akonadi::Item &item, const Akonadi::Collection& colSrc, const Akonadi::Collection& colDst ) { if ( colSrc == collection && colDst != collection ) // item leaving this model { itemRemoved( item ); return; } if ( colDst == collection && colSrc != collection ) { itemAdded( item ); return; } } void ItemModel::Private::itemsAdded( const Akonadi::Item::List &list ) { if ( list.isEmpty() ) return; mParent->beginInsertRows( QModelIndex(), items.count(), items.count() + list.count() - 1 ); foreach( const Item &item, list ) { ItemContainer *c = new ItemContainer( item, items.count() ); items.append( c ); itemHash[ item ] = c; } mParent->endInsertRows(); } void ItemModel::Private::itemAdded( const Akonadi::Item &item ) { Item::List l; l << item; itemsAdded( l ); } void ItemModel::Private::itemRemoved( const Akonadi::Item &_item ) { int row = rowForItem( _item ); if ( row < 0 ) return; mParent->beginRemoveRows( QModelIndex(), row, row ); const Item item = items.at( row )->item; Q_ASSERT( item.isValid() ); itemHash.remove( item ); delete items.takeAt( row ); mParent->endRemoveRows(); } ItemModel::ItemModel( QObject *parent ) : QAbstractTableModel( parent ), d( new Private( this ) ) { setSupportedDragActions( Qt::MoveAction | Qt::CopyAction ); } ItemModel::~ItemModel() { delete d; } QVariant ItemModel::data( const QModelIndex & index, int role ) const { if ( !index.isValid() ) return QVariant(); if ( index.row() >= d->items.count() ) return QVariant(); const Item item = d->items.at( index.row() )->item; if ( !item.isValid() ) return QVariant(); if ( role == Qt::DisplayRole ) { switch ( index.column() ) { case Id: return QString::number( item.id() ); case RemoteId: return item.remoteId(); case MimeType: return item.mimeType(); default: return QVariant(); } } if ( role == IdRole ) return item.id(); if ( role == ItemRole ) { QVariant var; var.setValue( item ); return var; } if ( role == MimeTypeRole ) return item.mimeType(); return QVariant(); } int ItemModel::rowCount( const QModelIndex & parent ) const { if ( !parent.isValid() ) return d->items.count(); return 0; } int ItemModel::columnCount(const QModelIndex & parent) const { if ( !parent.isValid() ) return 3; // keep in sync with Column enum return 0; } QVariant ItemModel::headerData( int section, Qt::Orientation orientation, int role ) const { if ( orientation == Qt::Horizontal && role == Qt::DisplayRole ) { switch ( section ) { case Id: return i18n( "Id" ); case RemoteId: return i18n( "Remote Id" ); case MimeType: return i18n( "MimeType" ); default: return QString(); } } return QAbstractTableModel::headerData( section, orientation, role ); } void ItemModel::setCollection( const Collection &collection ) { - kDebug( 5250 ); + kDebug(); if ( d->collection == collection ) return; // if we don't know anything about this collection yet, fetch it if ( collection.isValid() && collection.contentMimeTypes().isEmpty() ) { CollectionFetchJob* job = new CollectionFetchJob( collection, CollectionFetchJob::Base, this ); connect( job, SIGNAL(result(KJob*)), this, SLOT(collectionFetchResult(KJob*)) ); return; } d->monitor->setCollectionMonitored( d->collection, false ); d->collection = collection; d->monitor->setCollectionMonitored( d->collection, true ); // the query changed, thus everything we have already is invalid qDeleteAll( d->items ); d->items.clear(); reset(); // stop all running jobs d->session->clear(); // start listing job if ( d->collectionIsCompatible() ) { ItemFetchJob* job = new ItemFetchJob( collection, session() ); job->setFetchScope( d->monitor->itemFetchScope() ); connect( job, SIGNAL(itemsReceived(Akonadi::Item::List)), SLOT(itemsAdded(Akonadi::Item::List)) ); connect( job, SIGNAL(result(KJob*)), SLOT(listingDone(KJob*)) ); } emit collectionChanged( collection ); } void ItemModel::setFetchScope( const ItemFetchScope &fetchScope ) { d->monitor->setItemFetchScope( fetchScope ); } ItemFetchScope &ItemModel::fetchScope() { return d->monitor->itemFetchScope(); } Item ItemModel::itemForIndex( const QModelIndex & index ) const { if ( !index.isValid() ) return Akonadi::Item(); if ( index.row() >= d->items.count() ) return Akonadi::Item(); Item item = d->items.at( index.row() )->item; Q_ASSERT( item.isValid() ); return item; } Qt::ItemFlags ItemModel::flags( const QModelIndex &index ) const { Qt::ItemFlags defaultFlags = QAbstractTableModel::flags(index); if (index.isValid()) return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; else return Qt::ItemIsDropEnabled | defaultFlags; } QStringList ItemModel::mimeTypes() const { return QStringList() << QLatin1String("text/uri-list"); } Session * ItemModel::session() const { return d->session; } QMimeData *ItemModel::mimeData( const QModelIndexList &indexes ) const { QMimeData *data = new QMimeData(); // Add item uri to the mimedata for dropping in external applications KUrl::List urls; foreach ( const QModelIndex &index, indexes ) { if ( index.column() != 0 ) continue; urls << itemForIndex( index ).url( Item::UrlWithMimeType ); } urls.populateMimeData( data ); return data; } QModelIndex ItemModel::indexForItem( const Akonadi::Item &item, const int column ) const { return index( d->rowForItem( item ), column ); } bool ItemModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) { Q_UNUSED( row ); Q_UNUSED( column ); Q_UNUSED( parent ); KJob* job = PasteHelper::paste( data, d->collection, action != Qt::MoveAction ); // TODO: error handling return job; } Collection ItemModel::collection() const { return d->collection; } #include "itemmodel.moc" diff --git a/akonadi/itemmodifyjob.cpp b/akonadi/itemmodifyjob.cpp index c274684bc..1ecc7f52e 100644 --- a/akonadi/itemmodifyjob.cpp +++ b/akonadi/itemmodifyjob.cpp @@ -1,231 +1,230 @@ /* Copyright (c) 2006 - 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 "itemmodifyjob.h" #include "itemmodifyjob_p.h" #include "collection.h" #include "entity_p.h" #include "imapparser_p.h" #include "itemserializer_p.h" #include "job_p.h" #include "item_p.h" #include "protocolhelper_p.h" #include using namespace Akonadi; ItemModifyJobPrivate::ItemModifyJobPrivate( ItemModifyJob *parent, const Item &item ) : JobPrivate( parent ), mItem( item ), mRevCheck( true ), mIgnorePayload( false ) { mParts = mItem.loadedPayloadParts(); } void ItemModifyJobPrivate::setClean() { mOperations.insert( Dirty ); } QByteArray ItemModifyJobPrivate::nextPartHeader() { QByteArray command; if ( !mParts.isEmpty() ) { QSetIterator it( mParts ); const QByteArray label = it.next(); mParts.remove( label ); mPendingData.clear(); int version = 0; ItemSerializer::serialize( mItem, label, mPendingData, version ); command += ' ' + ProtocolHelper::encodePartIdentifier( ProtocolHelper::PartPayload, label, version ); command += ".SILENT"; if ( mPendingData.size() > 0 ) { command += " {" + QByteArray::number( mPendingData.size() ) + "}\n"; } else { if ( mPendingData.isNull() ) command += " NIL"; else command += " \"\""; command += nextPartHeader(); } } else { command += ")\n"; } return command; } ItemModifyJob::ItemModifyJob( const Item &item, QObject * parent ) : Job( new ItemModifyJobPrivate( this, item ), parent ) { Q_D( ItemModifyJob ); d->mOperations.insert( ItemModifyJobPrivate::RemoteId ); } ItemModifyJob::~ItemModifyJob() { } void ItemModifyJob::doStart() { Q_D( ItemModifyJob ); QList changes; foreach ( int op, d->mOperations ) { switch ( op ) { case ItemModifyJobPrivate::RemoteId: if ( !d->mItem.remoteId().isNull() ) { changes << "REMOTEID.SILENT"; changes << ImapParser::quote( d->mItem.remoteId().toUtf8() ); } break; case ItemModifyJobPrivate::Dirty: changes << "DIRTY.SILENT"; changes << "false"; break; } } if ( d->mItem.d_func()->mFlagsOverwritten ) { changes << "FLAGS.SILENT"; changes << '(' + ImapParser::join( d->mItem.flags(), " " ) + ')'; } else { if ( !d->mItem.d_func()->mAddedFlags.isEmpty() ) { changes << "+FLAGS.SILENT"; changes << '(' + ImapParser::join( d->mItem.d_func()->mAddedFlags, " " ) + ')'; } if ( !d->mItem.d_func()->mDeletedFlags.isEmpty() ) { changes << "-FLAGS.SILENT"; changes << '(' + ImapParser::join( d->mItem.d_func()->mDeletedFlags, " " ) + ')'; } } if ( !d->mItem.d_func()->mDeletedAttributes.isEmpty() ) { changes << "-PARTS.SILENT"; QList attrs; foreach ( const QByteArray &attr, d->mItem.d_func()->mDeletedAttributes ) attrs << ProtocolHelper::encodePartIdentifier( ProtocolHelper::PartAttribute, attr ); changes << '(' + ImapParser::join( attrs, " " ) + ')'; } // nothing to do if ( changes.isEmpty() && d->mParts.isEmpty() && d->mItem.attributes().isEmpty() ) { emitResult(); return; } d->mTag = d->newTag(); QByteArray command = d->mTag; command += " UID STORE " + QByteArray::number( d->mItem.id() ) + ' '; if ( !d->mRevCheck ) { command += "NOREV "; } else { command += "REV " + QByteArray::number( d->mItem.revision() ) + ' '; } if ( d->mItem.d_func()->mSizeChanged ) command += "SIZE " + QByteArray::number( d->mItem.size() ); command += " (" + ImapParser::join( changes, " " ); const QByteArray attrs = ProtocolHelper::attributesToByteArray( d->mItem, true ); if ( !attrs.isEmpty() ) command += ' ' + attrs; command += d->nextPartHeader(); d->writeData( command ); d->newTag(); // hack to circumvent automatic response handling } void ItemModifyJob::doHandleResponse(const QByteArray &_tag, const QByteArray & data) { Q_D( ItemModifyJob ); if ( _tag == "+" ) { // ready for literal data d->writeData( d->mPendingData ); d->writeData( d->nextPartHeader() ); return; } if ( _tag == d->mTag ) { if ( data.startsWith( "OK" ) ) { //krazy:exclude=strings QDateTime modificationDateTime; int dateTimePos = data.indexOf( "DATETIME" ); if ( dateTimePos != -1 ) { int resultPos = ImapParser::parseDateTime( data, modificationDateTime, dateTimePos + 8 ); if ( resultPos == (dateTimePos + 8) ) { - kDebug( 5250 ) << "Invalid DATETIME response to STORE command: " - << _tag << data; + kDebug() << "Invalid DATETIME response to STORE command: " << _tag << data; } } // increase item revision of own copy of item d->mItem.setRevision( d->mItem.revision() + 1 ); d->mItem.setModificationTime( modificationDateTime ); d->mItem.d_ptr->resetChangeLog(); } else { setError( Unknown ); setErrorText( QString::fromUtf8( data ) ); } emitResult(); return; } - kDebug( 5250 ) << "Unhandled response: " << _tag << data; + kDebug() << "Unhandled response: " << _tag << data; } void ItemModifyJob::setIgnorePayload( bool ignore ) { Q_D( ItemModifyJob ); if ( d->mIgnorePayload == ignore ) return; d->mIgnorePayload = ignore; if ( d->mIgnorePayload ) d->mParts = QSet(); else { Q_ASSERT( !d->mItem.mimeType().isEmpty() ); d->mParts = d->mItem.loadedPayloadParts(); } } bool ItemModifyJob::ignorePayload() const { Q_D( const ItemModifyJob ); return d->mIgnorePayload; } void ItemModifyJob::disableRevisionCheck() { Q_D( ItemModifyJob ); d->mRevCheck = false; } Item ItemModifyJob::item() const { Q_D( const ItemModifyJob ); return d->mItem; } #include "itemmodifyjob.moc" diff --git a/akonadi/itemserializer.cpp b/akonadi/itemserializer.cpp index 4bd3d162e..085e4b26c 100644 --- a/akonadi/itemserializer.cpp +++ b/akonadi/itemserializer.cpp @@ -1,296 +1,296 @@ /* Copyright (c) 2007 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 "itemserializer_p.h" #include "item.h" #include "itemserializerplugin.h" #include "attributefactory.h" // KDE core #include #include #include // Qt #include #include #include #include #include #include #include #include // temporary #include "pluginloader_p.h" namespace Akonadi { class DefaultItemSerializerPlugin; class DefaultItemSerializerPlugin : public ItemSerializerPlugin { public: DefaultItemSerializerPlugin() { } bool deserialize( Item& item, const QByteArray& label, QIODevice& data, int ) { if ( label != Item::FullPayload ) return false; item.setPayload( data.readAll() ); return true; } void serialize( const Item& item, const QByteArray& label, QIODevice& data, int& ) { Q_ASSERT( label == Item::FullPayload ); if ( item.hasPayload() ) data.write( item.payload() ); } }; K_GLOBAL_STATIC( DefaultItemSerializerPlugin, s_defaultItemSerializerPlugin ) } using namespace Akonadi; class PluginEntry { public: PluginEntry() : mPlugin( 0 ) { } explicit PluginEntry( const QString &identifier, ItemSerializerPlugin *plugin = 0 ) : mIdentifier( identifier ), mPlugin( plugin ) { } inline ItemSerializerPlugin* plugin() const { if ( mPlugin ) return mPlugin; QObject *object = PluginLoader::self()->createForName( mIdentifier ); if ( !object ) { - kWarning( 5250 ) << "ItemSerializerPluginLoader: " - << "plugin" << mIdentifier << "is not valid!" << endl; + kWarning() << "ItemSerializerPluginLoader: " + << "plugin" << mIdentifier << "is not valid!" << endl; // we try to use the default in that case mPlugin = s_defaultItemSerializerPlugin; } mPlugin = qobject_cast( object ); if ( !mPlugin ) { - kWarning( 5250 ) << "ItemSerializerPluginLoader: " - << "plugin" << mIdentifier << "doesn't provide interface ItemSerializerPlugin!" << endl; + kWarning() << "ItemSerializerPluginLoader: " + << "plugin" << mIdentifier << "doesn't provide interface ItemSerializerPlugin!" << endl; // we try to use the default in that case mPlugin = s_defaultItemSerializerPlugin; } Q_ASSERT( mPlugin ); return mPlugin; } QString type() const { return mIdentifier; } bool operator<( const PluginEntry &other ) const { return mIdentifier < other.mIdentifier; } bool operator<( const QString &type ) const { return mIdentifier < type; } private: QString mIdentifier; mutable ItemSerializerPlugin *mPlugin; }; static bool operator<( const QString &type, const PluginEntry &entry ) { return type < entry.type(); } class PluginRegistry { public: PluginRegistry() : mDefaultPlugin( PluginEntry( QLatin1String("application/octet-stream"), s_defaultItemSerializerPlugin ) ) { const PluginLoader* pl = PluginLoader::self(); if ( !pl ) { - kWarning( 5250 ) << "Cannot instantiate plugin loader!" << endl; + kWarning() << "Cannot instantiate plugin loader!" << endl; return; } const QStringList types = pl->types(); - kDebug( 5250 ) << "ItemSerializerPluginLoader: " - << "found" << types.size() << "plugins." << endl; + kDebug() << "ItemSerializerPluginLoader: " + << "found" << types.size() << "plugins." << endl; allPlugins.reserve( types.size() + 1 ); foreach ( const QString &type, types ) allPlugins.append( PluginEntry( type ) ); allPlugins.append( mDefaultPlugin ); std::sort( allPlugins.begin(), allPlugins.end() ); } const PluginEntry& findBestMatch( const QString &type ) { KMimeType::Ptr mimeType = KMimeType::mimeType( type, KMimeType::ResolveAliases ); if ( mimeType.isNull() ) return mDefaultPlugin; // step 1: find all plugins that match at all QVector matchingIndexes; for ( int i = 0, end = allPlugins.size(); i < end; ++i ) { if ( mimeType->is( allPlugins[i].type() ) ) matchingIndexes.append( i ); } // 0 matches: no luck (shouldn't happend though, as application/octet-stream matches everything) if ( matchingIndexes.isEmpty() ) return mDefaultPlugin; // 1 match: we are done if ( matchingIndexes.size() == 1 ) return allPlugins[matchingIndexes.first()]; // step 2: if we have more than one match, find the most specific one using topological sort boost::adjacency_list<> graph( matchingIndexes.size() ); for ( int i = 0, end = matchingIndexes.size() ; i != end ; ++i ) { KMimeType::Ptr mimeType = KMimeType::mimeType( allPlugins[matchingIndexes[i]].type(), KMimeType::ResolveAliases ); if ( mimeType.isNull() ) continue; for ( int j = 0; j != end; ++j ) { if ( i != j && mimeType->is( allPlugins[matchingIndexes[j]].type() ) ) boost::add_edge( j, i, graph ); } } QVector order; order.reserve( allPlugins.size() ); try { boost::topological_sort( graph, std::back_inserter( order ) ); } catch ( boost::not_a_dag &e ) { kWarning() << "Mimetype tree is not a DAG!"; return mDefaultPlugin; } return allPlugins[matchingIndexes[order.first()]]; } QVector allPlugins; QHash cachedPlugins; private: PluginEntry mDefaultPlugin; }; K_GLOBAL_STATIC( PluginRegistry, s_pluginRegistry ) /*static*/ void ItemSerializer::deserialize( Item& item, const QByteArray& label, const QByteArray& data, int version, bool external ) { if ( external ) { QFile file( QString::fromUtf8(data) ); if ( file.open( QIODevice:: ReadOnly ) ) { deserialize( item, label, file, version ); file.close(); } } else { QBuffer buffer; buffer.setData( data ); buffer.open( QIODevice::ReadOnly ); buffer.seek( 0 ); deserialize( item, label, buffer, version ); buffer.close(); } } /*static*/ void ItemSerializer::deserialize( Item& item, const QByteArray& label, QIODevice& data, int version ) { if ( !ItemSerializer::pluginForMimeType( item.mimeType() ).deserialize( item, label, data, version ) ) kWarning() << "Unable to deserialize payload part:" << label; } /*static*/ void ItemSerializer::serialize( const Item& item, const QByteArray& label, QByteArray& data, int &version ) { QBuffer buffer; buffer.setBuffer( &data ); buffer.open( QIODevice::WriteOnly ); buffer.seek( 0 ); serialize( item, label, buffer, version ); buffer.close(); } /*static*/ void ItemSerializer::serialize( const Item& item, const QByteArray& label, QIODevice& data, int &version ) { if ( !item.hasPayload() ) return; ItemSerializerPlugin& plugin = pluginForMimeType( item.mimeType() ); plugin.serialize( item, label, data, version ); } QSet ItemSerializer::parts(const Item & item) { if ( !item.hasPayload() ) return QSet(); return pluginForMimeType( item.mimeType() ).parts( item ); } /*static*/ ItemSerializerPlugin& ItemSerializer::pluginForMimeType( const QString & mimetype ) { // plugin cached, so let's take that one if ( s_pluginRegistry->cachedPlugins.contains( mimetype ) ) return *(s_pluginRegistry->cachedPlugins.value( mimetype )); ItemSerializerPlugin *plugin = 0; // check if we have one that matches exactly const QVector::const_iterator it = qBinaryFind( s_pluginRegistry->allPlugins.constBegin(), s_pluginRegistry->allPlugins.constEnd(), mimetype ); if ( it != s_pluginRegistry->allPlugins.constEnd() ) { plugin = (*it).plugin(); } else { // check if we have a more generic plugin const PluginEntry &entry = s_pluginRegistry->findBestMatch( mimetype ); - kDebug( 5250 ) << "Did not find exactly matching serializer plugin for type" << mimetype - << ", taking" << entry.type() << "as the closest match"; + kDebug() << "Did not find exactly matching serializer plugin for type" << mimetype + << ", taking" << entry.type() << "as the closest match"; plugin = entry.plugin(); } Q_ASSERT(plugin); s_pluginRegistry->cachedPlugins.insert( mimetype, plugin ); return *plugin; } diff --git a/akonadi/itemsync.cpp b/akonadi/itemsync.cpp index 328085082..895fa25a6 100644 --- a/akonadi/itemsync.cpp +++ b/akonadi/itemsync.cpp @@ -1,397 +1,397 @@ /* Copyright (c) 2007 Tobias Koenig 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 "itemsync.h" #include "collection.h" #include "item.h" #include "itemcreatejob.h" #include "itemdeletejob.h" #include "itemfetchjob.h" #include "itemmodifyjob.h" #include "transactionsequence.h" #include "itemfetchscope.h" #include #include using namespace Akonadi; /** * @internal */ class ItemSync::Private { public: Private( ItemSync *parent ) : q( parent ), mTransactionMode( Single ), mCurrentTransaction( 0 ), mTransactionJobs( 0 ), mPendingJobs( 0 ), mProgress( 0 ), mTotalItems( -1 ), mTotalItemsProcessed( 0 ), mStreaming( false ), mIncremental( false ), mLocalListDone( false ), mDeliveryDone( false ) { // we want to fetch all data by default mFetchScope.fetchFullPayload(); mFetchScope.fetchAllAttributes(); } void createLocalItem( const Item &item ); void checkDone(); void slotLocalListDone( KJob* ); void slotLocalChangeDone( KJob* ); void execute(); void processItems(); void deleteItems( const Item::List &items ); void slotTransactionResult( KJob *job ); Job* subjobParent() const; ItemSync *q; Collection mSyncCollection; QHash mLocalItemsById; QHash mLocalItemsByRemoteId; QSet mUnprocessedLocalItems; // transaction mode, TODO: make this public API? enum TransactionMode { Single, Chunkwise, None }; TransactionMode mTransactionMode; TransactionSequence *mCurrentTransaction; int mTransactionJobs; // fetch scope for initial item listing ItemFetchScope mFetchScope; // remote items Akonadi::Item::List mRemoteItems; // removed remote items Item::List mRemovedRemoteItems; // create counter int mPendingJobs; int mProgress; int mTotalItems; int mTotalItemsProcessed; bool mStreaming; bool mIncremental; bool mLocalListDone; bool mDeliveryDone; }; void ItemSync::Private::createLocalItem( const Item & item ) { mPendingJobs++; ItemCreateJob *create = new ItemCreateJob( item, mSyncCollection, subjobParent() ); q->connect( create, SIGNAL( result( KJob* ) ), q, SLOT( slotLocalChangeDone( KJob* ) ) ); } void ItemSync::Private::checkDone() { q->setProcessedAmount( KJob::Bytes, mProgress ); if ( mPendingJobs > 0 || !mDeliveryDone || mTransactionJobs > 0 ) return; q->emitResult(); } ItemSync::ItemSync( const Collection &collection, QObject *parent ) : Job( parent ), d( new Private( this ) ) { d->mSyncCollection = collection; } ItemSync::~ItemSync() { delete d; } void ItemSync::setFullSyncItems( const Item::List &items ) { Q_ASSERT( !d->mIncremental ); if ( !d->mStreaming ) d->mDeliveryDone = true; d->mRemoteItems += items; d->mTotalItemsProcessed += items.count(); kDebug() << "Received: " << items.count() << "In total: " << d->mTotalItemsProcessed << " Wanted: " << d->mTotalItems; setTotalAmount( KJob::Bytes, d->mTotalItemsProcessed ); if ( d->mTotalItemsProcessed == d->mTotalItems ) d->mDeliveryDone = true; d->execute(); } void ItemSync::setTotalItems( int amount ) { Q_ASSERT( !d->mIncremental ); Q_ASSERT( amount >= 0 ); setStreamingEnabled( true ); kDebug() << amount; d->mTotalItems = amount; setTotalAmount( KJob::Bytes, amount ); if ( d->mTotalItems == 0 ) { d->mDeliveryDone = true; d->execute(); } } void ItemSync::setIncrementalSyncItems( const Item::List &changedItems, const Item::List &removedItems ) { d->mIncremental = true; if ( !d->mStreaming ) d->mDeliveryDone = true; d->mRemoteItems += changedItems; d->mRemovedRemoteItems += removedItems; d->mTotalItemsProcessed += changedItems.count() + removedItems.count(); setTotalAmount( KJob::Bytes, d->mTotalItemsProcessed ); if ( d->mTotalItemsProcessed == d->mTotalItems ) d->mDeliveryDone = true; d->execute(); } void ItemSync::setFetchScope( ItemFetchScope &fetchScope ) { d->mFetchScope = fetchScope; } ItemFetchScope &ItemSync::fetchScope() { return d->mFetchScope; } void ItemSync::doStart() { ItemFetchJob* job = new ItemFetchJob( d->mSyncCollection, this ); job->setFetchScope( d->mFetchScope ); // we only can fetch parts already in the cache, otherwise this will deadlock job->fetchScope().setCacheOnly( true ); connect( job, SIGNAL( result( KJob* ) ), SLOT( slotLocalListDone( KJob* ) ) ); } bool ItemSync::updateItem( const Item &storedItem, Item &newItem ) { /* * We know that this item has changed (as it is part of the * incremental changed list), so we just put it into the * storage. */ if ( d->mIncremental ) return true; // Check whether the flags differ if ( storedItem.flags() != newItem.flags() ) { - kDebug( 5250 ) << "Stored flags " << storedItem.flags() - << "new flags " << newItem.flags(); + kDebug() << "Stored flags " << storedItem.flags() + << "new flags " << newItem.flags(); return true; } // Check whether the new item contains unknown parts QSet missingParts = storedItem.loadedPayloadParts(); missingParts.subtract( newItem.loadedPayloadParts() ); if ( !missingParts.isEmpty() ) return true; // ### FIXME SLOW!!! // If the available part identifiers don't differ, check // whether the content of the payload differs if ( storedItem.payloadData() != newItem.payloadData() ) return true; // check if remote attributes have been changed foreach ( Attribute* attr, newItem.attributes() ) { if ( !storedItem.hasAttribute( attr->type() ) ) return true; if ( attr->serialized() != storedItem.attribute( attr->type() )->serialized() ) return true; } return false; } void ItemSync::Private::slotLocalListDone( KJob * job ) { if ( job->error() ) return; const Item::List list = static_cast( job )->items(); foreach ( const Item &item, list ) { mLocalItemsById.insert( item.id(), item ); mLocalItemsByRemoteId.insert( item.remoteId(), item ); mUnprocessedLocalItems.insert( item ); } mLocalListDone = true; execute(); } void ItemSync::Private::execute() { if ( !mLocalListDone ) return; if ( (mTransactionMode == Single && !mCurrentTransaction) || mTransactionMode == Chunkwise ) { ++mTransactionJobs; mCurrentTransaction = new TransactionSequence( q ); connect( mCurrentTransaction, SIGNAL(result(KJob*)), q, SLOT(slotTransactionResult(KJob*)) ); } processItems(); if ( !mDeliveryDone ) { if ( mTransactionMode == Chunkwise && mCurrentTransaction ) { mCurrentTransaction->commit(); mCurrentTransaction = 0; } return; } // removed if ( !mIncremental ) { mRemovedRemoteItems = mUnprocessedLocalItems.toList(); mUnprocessedLocalItems.clear(); } deleteItems( mRemovedRemoteItems ); mLocalItemsById.clear(); mLocalItemsByRemoteId.clear(); mRemovedRemoteItems.clear(); if ( mCurrentTransaction ) { mCurrentTransaction->commit(); mCurrentTransaction = 0; } checkDone(); } void ItemSync::Private::processItems() { // added / updated foreach ( Item remoteItem, mRemoteItems ) { //krazy:exclude=foreach non-const is needed here #ifndef NDEBUG if ( remoteItem.remoteId().isEmpty() ) { - kWarning( 5250 ) << "Item " << remoteItem.id() << " does not have a remote identifier"; + kWarning() << "Item " << remoteItem.id() << " does not have a remote identifier"; } #endif Item localItem = mLocalItemsById.value( remoteItem.id() ); if ( !localItem.isValid() ) localItem = mLocalItemsByRemoteId.value( remoteItem.remoteId() ); mUnprocessedLocalItems.remove( localItem ); // missing locally if ( !localItem.isValid() ) { createLocalItem( remoteItem ); continue; } if ( q->updateItem( localItem, remoteItem ) ) { mPendingJobs++; remoteItem.setId( localItem.id() ); remoteItem.setRevision( localItem.revision() ); remoteItem.setSize( localItem.size() ); remoteItem.setRemoteId( localItem.remoteId() ); // in case someone clears remoteId by accident ItemModifyJob *mod = new ItemModifyJob( remoteItem, subjobParent() ); q->connect( mod, SIGNAL( result( KJob* ) ), q, SLOT( slotLocalChangeDone( KJob* ) ) ); } else { mProgress++; } } mRemoteItems.clear(); } void ItemSync::Private::deleteItems( const Item::List &items ) { foreach ( const Item &item, items ) { Item delItem( item ); if ( !item.isValid() ) { delItem = mLocalItemsByRemoteId.value( item.remoteId() ); } if ( !delItem.isValid() ) { #ifndef NDEBUG - kWarning( 5250 ) << "Delete item (remoteeId=" << delItem.remoteId() - << "mimeType=" << delItem.mimeType() - << ") does not have a valid UID and no item with that remote ID exists either"; + kWarning() << "Delete item (remoteeId=" << delItem.remoteId() + << "mimeType=" << delItem.mimeType() + << ") does not have a valid UID and no item with that remote ID exists either"; #endif continue; } mPendingJobs++; ItemDeleteJob *job = new ItemDeleteJob( delItem, subjobParent() ); q->connect( job, SIGNAL( result( KJob* ) ), q, SLOT( slotLocalChangeDone( KJob* ) ) ); } } void ItemSync::Private::slotLocalChangeDone( KJob * job ) { if ( job->error() ) return; mPendingJobs--; mProgress++; checkDone(); } void ItemSync::Private::slotTransactionResult( KJob *job ) { if ( job->error() ) return; --mTransactionJobs; if ( mCurrentTransaction == job ) mCurrentTransaction = 0; checkDone(); } Job * ItemSync::Private::subjobParent() const { if ( mCurrentTransaction && mTransactionMode != None ) return mCurrentTransaction; return q; } void ItemSync::setStreamingEnabled(bool enable) { d->mStreaming = enable; } void ItemSync::deliveryDone() { Q_ASSERT( d->mStreaming ); d->mDeliveryDone = true; d->execute(); } #include "itemsync.moc" diff --git a/akonadi/job.cpp b/akonadi/job.cpp index f462f690c..775629862 100644 --- a/akonadi/job.cpp +++ b/akonadi/job.cpp @@ -1,290 +1,290 @@ /* Copyright (c) 2006 Tobias Koenig 2006 Marc Mutz 2006 - 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 "job.h" #include "job_p.h" #include "imapparser_p.h" #include "session.h" #include "session_p.h" #include #include #include #include #include #include #include #include #include using namespace Akonadi; static QDBusAbstractInterface *s_jobtracker = 0; //@cond PRIVATE void JobPrivate::handleResponse( const QByteArray & tag, const QByteArray & data ) { Q_Q( Job ); if ( mCurrentSubJob ) { mCurrentSubJob->d_ptr->handleResponse( tag, data ); return; } if ( tag == mTag ) { if ( data.startsWith( "NO " ) || data.startsWith( "BAD " ) ) { //krazy:exclude=strings QString msg = QString::fromUtf8( data ); msg.remove( 0, msg.startsWith( QLatin1String( "NO " ) ) ? 3 : 4 ); if ( msg.endsWith( QLatin1String( "\r\n" ) ) ) msg.chop( 2 ); q->setError( Job::Unknown ); q->setErrorText( msg ); q->emitResult(); return; } else if ( data.startsWith( "OK" ) ) { //krazy:exclude=strings q->emitResult(); return; } } q->doHandleResponse( tag, data ); } void JobPrivate::init( QObject *parent ) { Q_Q( Job ); mParentJob = dynamic_cast( parent ); mSession = dynamic_cast( parent ); if ( !mSession ) { if ( !mParentJob ) mSession = Session::defaultSession(); else mSession = mParentJob->d_ptr->mSession; } if ( !mParentJob ) mSession->d->addJob( q ); else mParentJob->addSubjob( q ); // if there's a job tracer running, tell it about the new job if ( !s_jobtracker && QDBusConnection::sessionBus().interface()->isServiceRegistered(QLatin1String("org.kde.akonadiconsole") ) ) { s_jobtracker = new QDBusInterface( QLatin1String("org.kde.akonadiconsole"), QLatin1String("/jobtracker"), QLatin1String("org.freedesktop.Akonadi.JobTracker"), QDBusConnection::sessionBus(), 0 ); } QMetaObject::invokeMethod( q, "signalCreationToJobTracker", Qt::QueuedConnection ); } void JobPrivate::signalCreationToJobTracker() { Q_Q( Job ); if ( s_jobtracker ) { // We do these dbus calls manually, so as to avoid having to install (or copy) the console's // xml interface document. Since this is purely a debugging aid, that seems preferable to // publishing something not intended for public consumption. QList argumentList; argumentList << QLatin1String( mSession->sessionId() ) << QString::number(reinterpret_cast( q ), 16) << ( mParentJob ? QString::number( reinterpret_cast( mParentJob ), 16) : QString() ) << QString::fromLatin1( q->metaObject()->className() ); s_jobtracker->asyncCallWithArgumentList(QLatin1String("jobCreated"), argumentList); } } void JobPrivate::startQueued() { Q_Q( Job ); mStarted = true; emit q->aboutToStart( q ); q->doStart(); QTimer::singleShot( 0, q, SLOT(startNext()) ); // if there's a job tracer running, tell it a job started if ( s_jobtracker ) { QList argumentList; argumentList << QString::number(reinterpret_cast( q ), 16); s_jobtracker->asyncCallWithArgumentList(QLatin1String("jobStarted"), argumentList); } } void JobPrivate::lostConnection() { Q_Q( Job ); if ( mCurrentSubJob ) { mCurrentSubJob->d_ptr->lostConnection(); } else { q->setError( Job::ConnectionFailed ); q->kill( KJob::EmitResult ); } } void JobPrivate::slotSubJobAboutToStart( Job * job ) { Q_ASSERT( mCurrentSubJob == 0 ); mCurrentSubJob = job; } void JobPrivate::startNext() { Q_Q( Job ); if ( mStarted && !mCurrentSubJob && q->hasSubjobs() ) { Job *job = dynamic_cast( q->subjobs().first() ); Q_ASSERT( job ); job->d_ptr->startQueued(); } } QByteArray JobPrivate::newTag( ) { if ( mParentJob ) mTag = mParentJob->d_ptr->newTag(); else mTag = QByteArray::number( mSession->d->nextTag() ); return mTag; } QByteArray JobPrivate::tag() const { return mTag; } void JobPrivate::writeData( const QByteArray & data ) { Q_ASSERT_X( !mWriteFinished, "Job::writeData()", "Calling writeData() after emitting writeFinished()" ); mSession->d->writeData( data ); } //@endcond Job::Job( QObject *parent ) : KCompositeJob( parent ), d_ptr( new JobPrivate( this ) ) { d_ptr->init( parent ); } Job::Job( JobPrivate *dd, QObject *parent ) : KCompositeJob( parent ), d_ptr( dd ) { d_ptr->init( parent ); } Job::~Job() { delete d_ptr; // if there is a job tracer listening, tell it the job is done now if ( s_jobtracker ) { QList argumentList; argumentList << QString::number(reinterpret_cast( this ), 16) << errorString(); s_jobtracker->asyncCallWithArgumentList(QLatin1String("jobEnded"), argumentList); } } void Job::start() { } bool Job::doKill() { return true; } QString Job::errorString() const { QString str; switch ( error() ) { case NoError: break; case ConnectionFailed: str = i18n( "Cannot connect to the Akonadi service." ); break; case ProtocolVersionMismatch: str = i18n( "The protocol version of the Akonadi server is incompatible. Make sure you have a compatible version installed." ); break; case UserCanceled: str = i18n( "User canceled operation." ); break; case Unknown: default: str = i18n( "Unknown error." ); break; } if ( !errorText().isEmpty() ) { str += QString::fromLatin1( " (%1)" ).arg( errorText() ); } return str; } bool Job::addSubjob( KJob * job ) { bool rv = KCompositeJob::addSubjob( job ); if ( rv ) { connect( job, SIGNAL(aboutToStart(Akonadi::Job*)), SLOT(slotSubJobAboutToStart(Akonadi::Job*)) ); QTimer::singleShot( 0, this, SLOT(startNext()) ); } return rv; } bool Job::removeSubjob(KJob * job) { bool rv = KCompositeJob::removeSubjob( job ); if ( job == d_ptr->mCurrentSubJob ) { d_ptr->mCurrentSubJob = 0; QTimer::singleShot( 0, this, SLOT(startNext()) ); } return rv; } void Job::doHandleResponse(const QByteArray & tag, const QByteArray & data) { - kDebug( 5250 ) << "Unhandled response: " << tag << data; + kDebug() << "Unhandled response: " << tag << data; } void Job::slotResult(KJob * job) { Q_ASSERT( job == d_ptr->mCurrentSubJob ); d_ptr->mCurrentSubJob = 0; KCompositeJob::slotResult( job ); if ( !job->error() ) QTimer::singleShot( 0, this, SLOT(startNext()) ); } void Job::emitWriteFinished() { d_ptr->mWriteFinished = true; emit writeFinished( this ); } #include "job.moc" diff --git a/akonadi/monitor_p.cpp b/akonadi/monitor_p.cpp index e2b1adcd6..cd0ffed2f 100644 --- a/akonadi/monitor_p.cpp +++ b/akonadi/monitor_p.cpp @@ -1,391 +1,391 @@ /* 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; MonitorPrivate::MonitorPrivate(Monitor * parent) : q_ptr( parent ), nm( 0 ), monitorAll( false ), fetchCollection( false ), fetchCollectionStatistics( false ) { } 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( 5250 ) << "Unable to connect to notification manager"; + 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; } bool MonitorPrivate::acceptNotification(const NotificationMessage & msg) { if ( isSessionIgnored( msg.sessionId() ) ) return false; switch ( msg.type() ) { case NotificationMessage::InvalidType: - kWarning( 5250 ) << "Received invalid change notification!"; + 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; } bool MonitorPrivate::processNotification(const NotificationMessage & msg) { if ( !acceptNotification( msg ) ) return false; if ( msg.type() == NotificationMessage::Item ) { notifyCollectionStatisticsWatchers( msg.parentCollection(), msg.resource() ); if ( !mItemFetchScope.isEmpty() && ( msg.operation() == NotificationMessage::Add || msg.operation() == NotificationMessage::Move || msg.operation() == NotificationMessage::Link ) ) { Item item( msg.uid() ); item.setRemoteId( msg.remoteId() ); ItemCollectionFetchJob *job = new ItemCollectionFetchJob( item, msg.parentCollection(), msg.parentDestCollection(), q_ptr ); job->setFetchScope( mItemFetchScope ); pendingJobs.insert( job, msg ); QObject::connect( job, SIGNAL(result(KJob*)), q_ptr, SLOT(slotItemJobFinished(KJob*)) ); return true; } if ( !mItemFetchScope.isEmpty() && msg.operation() == NotificationMessage::Modify ) { Item item( msg.uid() ); item.setRemoteId( msg.remoteId() ); ItemFetchJob *job = new ItemFetchJob( item, q_ptr ); job->setFetchScope( mItemFetchScope ); pendingJobs.insert( job, msg ); QObject::connect( job, SIGNAL(result(KJob*)), q_ptr, SLOT(slotItemJobFinished(KJob*)) ); return true; } emitItemNotification( msg ); return true; } else if ( msg.type() == NotificationMessage::Collection ) { if ( msg.operation() != NotificationMessage::Remove && fetchCollection ) { Collection::List list; list << Collection( msg.uid() ); if ( msg.operation() == NotificationMessage::Add ) list << Collection( msg.parentCollection() ); CollectionFetchJob *job = new CollectionFetchJob( list, q_ptr ); pendingJobs.insert( job, msg ); QObject::connect( job, SIGNAL(result(KJob*)), q_ptr, SLOT(slotCollectionJobFinished(KJob*)) ); return true; } if ( msg.operation() == NotificationMessage::Remove ) { // no need for statistics updates anymore recentlyChangedCollections.remove( msg.uid() ); } emitCollectionNotification( msg ); return true; } else { - kWarning( 5250 ) << "Received unknown change notification!"; + kWarning() << "Received unknown change notification!"; } return false; } 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( 5250 ) << "Error on fetching collection statistics: " << job->errorText(); + 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 ) processNotification( msg ); } 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() ) { it = Item( msg.uid() ); it.setRemoteId( msg.remoteId() ); it.setMimeType( msg.mimeType() ); } 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 ) { Q_ASSERT( msg.type() == NotificationMessage::Collection ); Collection collection = col; if ( !collection.isValid() ) { collection = Collection( msg.uid() ); collection.setParent( msg.parentCollection() ); collection.setResource( QString::fromUtf8( msg.resource() ) ); collection.setRemoteId( msg.remoteId() ); } Collection parent = par; if ( !parent.isValid() ) parent = Collection( msg.parentCollection() ); switch ( msg.operation() ) { case NotificationMessage::Add: emit q_ptr->collectionAdded( collection, parent ); break; case NotificationMessage::Modify: emit q_ptr->collectionChanged( collection ); break; case NotificationMessage::Remove: emit q_ptr->collectionRemoved( collection ); break; default: Q_ASSERT_X( false, "MonitorPrivate::emitCollectionNotification", "Invalid enum value" ); } } void MonitorPrivate::slotItemJobFinished( KJob* job ) { if ( !pendingJobs.contains( job ) ) { - kWarning( 5250 ) << "Unknown job - wtf is going on here?"; + kWarning() << "Unknown job - wtf is going on here?"; return; } NotificationMessage msg = pendingJobs.take( job ); Item item; Collection col; Collection destCol; if ( job->error() ) { - kWarning( 5250 ) << "Error on fetching item:" << job->errorText(); + kWarning() << "Error on fetching item:" << job->errorText(); } else { ItemFetchJob *fetchJob = qobject_cast( job ); if ( fetchJob && fetchJob->items().count() > 0 ) item = fetchJob->items().first(); ItemCollectionFetchJob *cfjob = qobject_cast( job ); if ( cfjob ) { item = cfjob->item(); col = cfjob->collection(); destCol = cfjob->destCollection(); } } emitItemNotification( msg, item, col, destCol ); } void MonitorPrivate::slotCollectionJobFinished( KJob* job ) { if ( !pendingJobs.contains( job ) ) { - kWarning( 5250 ) << "Unknown job - wtf is going on here?"; + kWarning() << "Unknown job - wtf is going on here?"; return; } NotificationMessage msg = pendingJobs.take( job ); if ( job->error() ) { - kWarning( 5250 ) << "Error on fetching collection:" << job->errorText(); + kWarning() << "Error on fetching collection:" << job->errorText(); } else { Collection col, parent; CollectionFetchJob *listJob = qobject_cast( job ); if ( listJob && listJob->collections().count() > 0 ) col = listJob->collections().first(); if ( listJob && listJob->collections().count() > 1 && msg.operation() == NotificationMessage::Add ) { parent = listJob->collections().at( 1 ); if ( col.id() != msg.uid() ) qSwap( col, parent ); } emitCollectionNotification( msg, col, parent ); } } ItemCollectionFetchJob::ItemCollectionFetchJob( const Item &item, Collection::Id collectionId, Collection::Id destCollectionId, QObject *parent ) : Job( parent ), mReferenceItem( item ), mCollectionId( collectionId ), mDestCollectionId( destCollectionId ) { } ItemCollectionFetchJob::~ItemCollectionFetchJob() { } Item ItemCollectionFetchJob::item() const { return mItem; } Collection ItemCollectionFetchJob::collection() const { return mCollection; } Collection ItemCollectionFetchJob::destCollection() const { return mDestCollection; } void ItemCollectionFetchJob::setFetchScope( const ItemFetchScope &fetchScope ) { mFetchScope = fetchScope; } void ItemCollectionFetchJob::doStart() { CollectionFetchJob *listJob = new CollectionFetchJob( Collection( mCollectionId ), CollectionFetchJob::Base, this ); connect( listJob, SIGNAL( result( KJob* ) ), SLOT( collectionJobDone( KJob* ) ) ); addSubjob( listJob ); if ( mDestCollectionId > 0 ) { CollectionFetchJob *destListJob = new CollectionFetchJob( Collection( mDestCollectionId ), CollectionFetchJob::Base, this ); connect( destListJob, SIGNAL( result( KJob* ) ), SLOT( destCollectionJobDone( KJob* ) ) ); addSubjob( destListJob ); } ItemFetchJob *fetchJob = new ItemFetchJob( mReferenceItem, this ); fetchJob->setFetchScope( mFetchScope ); connect( fetchJob, SIGNAL( result( KJob* ) ), SLOT( itemJobDone( KJob* ) ) ); addSubjob( fetchJob ); } void ItemCollectionFetchJob::collectionJobDone( KJob* job ) { if ( !job->error() ) { CollectionFetchJob *listJob = qobject_cast( job ); if ( listJob->collections().isEmpty() ) { setError( 1 ); setErrorText( QLatin1String( "No collection found" ) ); } else mCollection = listJob->collections().first(); } } void ItemCollectionFetchJob::destCollectionJobDone( KJob* job ) { if ( !job->error() ) { CollectionFetchJob *listJob = qobject_cast( job ); if ( listJob->collections().isEmpty() ) { setError( 1 ); setErrorText( QLatin1String( "No collection found" ) ); } else mDestCollection = listJob->collections().first(); } } void ItemCollectionFetchJob::itemJobDone( KJob* job ) { if ( !job->error() ) { ItemFetchJob *fetchJob = qobject_cast( job ); if ( fetchJob->items().isEmpty() ) { setError( 2 ); setErrorText( QLatin1String( "No item found" ) ); } else mItem = fetchJob->items().first(); emitResult(); } } // @endcond #include "monitor_p.moc" diff --git a/akonadi/preprocessorbase.cpp b/akonadi/preprocessorbase.cpp new file mode 100644 index 000000000..e8655717f --- /dev/null +++ b/akonadi/preprocessorbase.cpp @@ -0,0 +1,107 @@ +/****************************************************************************** + * + * Copyright (c) 2009 Szymon Stefanek + * + * 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 "preprocessorbase.h" + +#include "agentbase_p.h" +#include "item.h" +#include "preprocessoradaptor.h" + +#include + +namespace Akonadi +{ + +class PreprocessorBasePrivate : public AgentBasePrivate +{ + public: + PreprocessorBasePrivate( PreprocessorBase *parent ) + : AgentBasePrivate( parent ), + mInDelayedProcessing( false ), + mDelayedProcessingItemId( 0 ) + { + } + + Q_DECLARE_PUBLIC( PreprocessorBase ) + + void delayedInit() + { + if ( !QDBusConnection::sessionBus().registerService( QLatin1String( "org.freedesktop.Akonadi.Preprocessor." ) + mId ) ) + kFatal() << "Unable to register service at D-Bus: " << QDBusConnection::sessionBus().lastError().message(); + AgentBasePrivate::delayedInit(); + } + + bool mInDelayedProcessing; + qlonglong mDelayedProcessingItemId; +}; + +} + +using namespace Akonadi; + +PreprocessorBase::PreprocessorBase( const QString &id ) + : AgentBase( new PreprocessorBasePrivate( this ), id ) +{ + new PreprocessorAdaptor( this ); +} + +PreprocessorBase::~PreprocessorBase() +{ +} + +void PreprocessorBase::terminateProcessing( ProcessingResult result ) +{ + Q_D( PreprocessorBase ); + + Q_ASSERT_X( result != ProcessingDelayed, "PreprocessorBase::terminateProcessing", "You should never pass ProcessingDelayed to this function" ); + Q_ASSERT_X( d->mInDelayedProcessing, "PreprocessorBase::terminateProcessing", "terminateProcessing() called while not in delayed processing mode" ); + + d->mInDelayedProcessing = false; + emit itemProcessed( d->mDelayedProcessingItemId ); +} + +void PreprocessorBase::beginProcessItem( qlonglong id ) +{ + Q_D( PreprocessorBase ); + + kDebug() << "PreprocessorBase: about to process item " << id; + + switch( processItem( Item( id ) ) ) + { + case ProcessingFailed: + case ProcessingRefused: + case ProcessingCompleted: + kDebug() << "PreprocessorBase: item processed, emitting signal (" << id << ")"; + + emit itemProcessed( id ); + + kDebug() << "PreprocessorBase: item processed, signal emitted (" << id << ")"; + break; + case ProcessingDelayed: + kDebug() << "PreprocessorBase: item processing delayed (" << id << ")"; + + d->mInDelayedProcessing = true; + d->mDelayedProcessingItemId = id; + break; + } +} + +#include "preprocessorbase.moc" diff --git a/akonadi/preprocessorbase.h b/akonadi/preprocessorbase.h new file mode 100644 index 000000000..77227b040 --- /dev/null +++ b/akonadi/preprocessorbase.h @@ -0,0 +1,169 @@ +/****************************************************************************** + * + * Copyright (c) 2009 Szymon Stefanek + * + * 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_PREPROCESSORBASE_H +#define AKONADI_PREPROCESSORBASE_H + +#include "akonadi_export.h" + +#include + +class PreprocessorAdaptor; + +namespace Akonadi +{ + +class Item; +class PreprocessorBasePrivate; + +/** + * @short The base class for all Akonadi preprocessor agents. + * + * This class should be used as a base class by all preprocessor agents + * since it encapsulates large parts of the protocol between + * preprocessor agent, agent manager and the Akonadi storage. + * + * Preprocessor agents are special agents that are informed about newly + * added items before any other agents. This allows them to do filtering + * on the items or any other task that shall be done before the new item + * is visible in the Akonadi storage system. + * + * The method all the preprocessors must implement is processItem(). + * + * @author Szymon Stefanek + * @since 4.4 + */ +class AKONADI_EXPORT PreprocessorBase : public AgentBase +{ + friend class PreprocessorAdaptor; + + Q_OBJECT + + public: + /** + * Describes the possible return values of the processItem() method. + */ + enum ProcessingResult + { + /** + * Processing completed succesfully for this item. + * The Akonadi server will push in a new item when it's available. + */ + ProcessingCompleted, + + /** + * Processing was delayed to a later stage. + * This must be returned when implementing asynchronous preprocessing. + * + * If this value is returned, terminateProcessing() has to be called + * when processing is done. + */ + ProcessingDelayed, + + /** + * Processing for this item failed (and the failure is unrecoverable). + * The Akonadi server will push in a new item when it's available, + * after possibly logging the failure. + */ + ProcessingFailed, + + /** + * Processing for this item was refused. This is very + * similar to ProcessingFailed above but additionally remarks + * that the item that the Akonadi server pushed in wasn't + * meant for this Preprocessor. + * The Akonadi server will push in a new item when it's available, + * after possibly logging the failure and maybe taking some additional action. + */ + ProcessingRefused + }; + + /** + * This method has to be implement by every preprocessor subclass. + * + * Returns ProcessingCompleted on success, ProcessingDelayed + * if processing is implemented asynchronously and + * ProcessingRefused or ProcessingFailed if the processing + * didn't complete. + */ + virtual ProcessingResult processItem( const Item &item ) = 0; + + /** + * This method must be called if processing is implemented asynchronously. + * + * Valid values for @p result are ProcessingCompleted, + * PocessingRefused and ProcessingFailed. Passing any + * other value will lead to a runtime assertion. + */ + void terminateProcessing( ProcessingResult result ); + + Q_SIGNALS: + /** + * This signal is emitted to report item processing termination + * to the Akonadi server. + * + * @note This signal is only for internal use. + */ + void itemProcessed( qlonglong id ); + + protected: + /** + * Creates a new preprocessor base agent. + * + * @param id The instance id of the preprocessor base agent. + */ + PreprocessorBase( const QString &id ); + + /** + * Destroys the preprocessor base agent. + */ + virtual ~PreprocessorBase(); + + /** + * This dbus method is called by the Akonadi server + * in order to trigger the processing of an item. + * + * @note Do not call it manually! + */ + void beginProcessItem( qlonglong itemId ); + + private: + // dbus Preprocessor interface + friend class ::PreprocessorAdaptor; + + Q_DECLARE_PRIVATE( PreprocessorBase ) + +}; // class PreprocessorBase + +} // namespace Akonadi + +#ifndef AKONADI_PREPROCESSOR_MAIN +/** + * Convenience Macro for the most common main() function for Akonadi preprocessors. + */ +#define AKONADI_PREPROCESSOR_MAIN( preProcessorClass ) \ + int main( int argc, char **argv ) \ + { \ + return Akonadi::PreprocessorBase::init( argc, argv ); \ + } +#endif //!AKONADI_RESOURCE_MAIN + +#endif //!_PREPROCESSORBASE_H_ diff --git a/akonadi/protocolhelper.cpp b/akonadi/protocolhelper.cpp index 7780add05..0a81c21f3 100644 --- a/akonadi/protocolhelper.cpp +++ b/akonadi/protocolhelper.cpp @@ -1,229 +1,229 @@ /* 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 "protocolhelper_p.h" #include "attributefactory.h" #include "collectionstatistics.h" #include "exception.h" #include #include #include #include #include #include #include using namespace Akonadi; int ProtocolHelper::parseCachePolicy(const QByteArray & data, CachePolicy & policy, int start) { QVarLengthArray params; int end = Akonadi::ImapParser::parseParenthesizedList( data, params, start ); for ( int i = 0; i < params.count() - 1; i += 2 ) { const QByteArray key = params[i]; const QByteArray value = params[i + 1]; if ( key == "INHERIT" ) policy.setInheritFromParent( value == "true" ); else if ( key == "INTERVAL" ) policy.setIntervalCheckTime( value.toInt() ); else if ( key == "CACHETIMEOUT" ) policy.setCacheTimeout( value.toInt() ); else if ( key == "SYNCONDEMAND" ) policy.setSyncOnDemand( value == "true" ); else if ( key == "LOCALPARTS" ) { QVarLengthArray tmp; QStringList parts; Akonadi::ImapParser::parseParenthesizedList( value, tmp ); for ( int j=0; j attributes; pos = ImapParser::parseParenthesizedList( data, attributes, pos ); for ( int i = 0; i < attributes.count() - 1; i += 2 ) { const QByteArray key = attributes[i]; const QByteArray value = attributes[i + 1]; if ( key == "NAME" ) { collection.setName( QString::fromUtf8( value ) ); } else if ( key == "REMOTEID" ) { collection.setRemoteId( QString::fromUtf8( value ) ); } else if ( key == "RESOURCE" ) { collection.setResource( QString::fromUtf8( value ) ); } else if ( key == "MIMETYPE" ) { QVarLengthArray ct; ImapParser::parseParenthesizedList( value, ct ); QStringList ct2; for ( int j = 0; j < ct.size(); j++ ) ct2 << QString::fromLatin1( ct[j] ); collection.setContentMimeTypes( ct2 ); } else if ( key == "MESSAGES" ) { CollectionStatistics s = collection.statistics(); s.setCount( value.toLongLong() ); collection.setStatistics( s ); } else if ( key == "UNSEEN" ) { CollectionStatistics s = collection.statistics(); s.setUnreadCount( value.toLongLong() ); collection.setStatistics( s ); } else if ( key == "SIZE" ) { CollectionStatistics s = collection.statistics(); s.setSize( value.toLongLong() ); collection.setStatistics( s ); } else if ( key == "CACHEPOLICY" ) { CachePolicy policy; ProtocolHelper::parseCachePolicy( value, policy ); collection.setCachePolicy( policy ); } else { Attribute* attr = AttributeFactory::createAttribute( key ); Q_ASSERT( attr ); attr->deserialize( value ); collection.addAttribute( attr ); } } return pos; } QByteArray ProtocolHelper::attributesToByteArray(const Entity & entity, bool ns ) { QList l; foreach ( const Attribute *attr, entity.attributes() ) { l << encodePartIdentifier( ns ? PartAttribute : PartGlobal, attr->type() ); l << ImapParser::quote( attr->serialized() ); } return ImapParser::join( l, " " ); } QByteArray ProtocolHelper::encodePartIdentifier(PartNamespace ns, const QByteArray & label, int version ) { const QByteArray versionString( version != 0 ? '[' + QByteArray::number( version ) + ']' : "" ); switch ( ns ) { case PartGlobal: return label + versionString; case PartPayload: return "PLD:" + label + versionString; case PartAttribute: return "ATR:" + label + versionString; default: Q_ASSERT( false ); } return QByteArray(); } QByteArray ProtocolHelper::decodePartIdentifier( const QByteArray &data, PartNamespace & ns ) { if ( data.startsWith( "PLD:" ) ) { //krazy:exclude=strings ns = PartPayload; return data.mid( 4 ); } else if ( data.startsWith( "ATR:" ) ) { //krazy:exclude=strings ns = PartAttribute; return data.mid( 4 ); } else { ns = PartGlobal; return data; } } QByteArray ProtocolHelper::itemSetToByteArray( const Item::List &_items, const QByteArray &command ) { if ( _items.isEmpty() ) throw Exception( "No items specified" ); Item::List items( _items ); QByteArray rv; std::sort( items.begin(), items.end(), boost::bind( &Item::id, _1 ) < boost::bind( &Item::id, _2 ) ); if ( items.first().isValid() ) { // all items have a uid set rv += " " AKONADI_CMD_UID " "; rv += command; rv += ' '; QList uids; foreach ( const Item &item, items ) uids << item.id(); ImapSet set; set.add( uids ); rv += set.toImapSequenceSet(); } else { // check if all items have a remote id QList rids; foreach ( const Item &item, items ) { if ( item.remoteId().isEmpty() ) throw Exception( i18n( "No remote identifier specified" ) ); rids << ImapParser::quote( item.remoteId().toUtf8() ); } rv += " " AKONADI_CMD_RID " "; rv += command; rv += " ("; rv += ImapParser::join( rids, " " ); rv += ')'; } return rv; } diff --git a/akonadi/resourcebase.cpp b/akonadi/resourcebase.cpp index 8c4eccee6..60844c582 100644 --- a/akonadi/resourcebase.cpp +++ b/akonadi/resourcebase.cpp @@ -1,606 +1,606 @@ /* 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 "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 ) { 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 ); // synchronize states Collection currentCollection; ResourceScheduler *scheduler; ItemSync *mItemSyncer; CollectionSync *mCollectionSyncer; }; 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( retrieveItem( const Akonadi::Item&, const QSet& ) ) ); 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( 5250 ) << "Not enough arguments passed..."; + 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( 5250 ) << "Identifier argument missing"; + 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( 5250 ) << "Item does not provide part" << 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->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() ); 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() ); 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() ); 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 { 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->setResource( mId ); 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 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->setResource( identifier() ); 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 ); } #include "resourcebase.moc" diff --git a/akonadi/selectionproxymodel.cpp b/akonadi/selectionproxymodel.cpp new file mode 100644 index 000000000..790cbdae4 --- /dev/null +++ b/akonadi/selectionproxymodel.cpp @@ -0,0 +1,102 @@ +/* + Copyright (c) 2009 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 "selectionproxymodel.h" +#include "entitytreemodel.h" + +using namespace Akonadi; + +namespace Akonadi +{ + +class SelectionProxyModelPrivate +{ +public: + SelectionProxyModelPrivate(SelectionProxyModel *model) + : m_headerSet(EntityTreeModel::EntityTreeHeaders), + q_ptr(model) + { + + } +private: + + int m_headerSet; + + Q_DECLARE_PUBLIC(SelectionProxyModel) + SelectionProxyModel *q_ptr; + +}; + +} + +SelectionProxyModel::SelectionProxyModel(QItemSelectionModel *selectionModel, QObject *parent) + : KSelectionProxyModel(selectionModel, parent), d_ptr(new SelectionProxyModelPrivate(this)) +{ + +} + +SelectionProxyModel::~SelectionProxyModel() +{ + delete d_ptr; +} + +QVariant SelectionProxyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + Q_D(const SelectionProxyModel); + int adjustedRole; + + adjustedRole = role + ( Akonadi::EntityTreeModel::TerminalUserRole * d->m_headerSet ); + return sourceModel()->headerData(section, orientation, adjustedRole); +} + +int SelectionProxyModel::headerSet() const +{ + Q_D(const SelectionProxyModel); + return d->m_headerSet; +} + +void SelectionProxyModel::setHeaderSet(int set) +{ + Q_D(SelectionProxyModel); + d->m_headerSet = set; +} + +bool SelectionProxyModel::dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ) +{ + Q_ASSERT(sourceModel()); + const QModelIndex sourceParent = mapToSource(parent); + return sourceModel()->dropMimeData(data, action, row, column, sourceParent); +} + +QMimeData* SelectionProxyModel::mimeData( const QModelIndexList & indexes ) const +{ + Q_ASSERT(sourceModel()); + QModelIndexList sourceIndexes; + foreach(const QModelIndex& index, indexes) + sourceIndexes << mapToSource(index); + return sourceModel()->mimeData(sourceIndexes); +} + +QStringList SelectionProxyModel::mimeTypes() const +{ + Q_ASSERT(sourceModel()); + return sourceModel()->mimeTypes(); +} + diff --git a/akonadi/selectionproxymodel.h b/akonadi/selectionproxymodel.h new file mode 100644 index 000000000..7c288fe35 --- /dev/null +++ b/akonadi/selectionproxymodel.h @@ -0,0 +1,86 @@ +/* + Copyright (c) 2009 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. +*/ + +#ifndef AKONADI_SELECTIONPROXYMODEL_H +#define AKONADI_SELECTIONPROXYMODEL_H + +#include "akonadi_export.h" + +#include + +#include + +namespace Akonadi +{ + +class SelectionProxyModelPrivate; + +/** + * @short A proxy model that provides data depending on the selection of a view. + * + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADI_EXPORT SelectionProxyModel : public KSelectionProxyModel +{ + Q_OBJECT + + public: + /** + * Creates a new selection proxy model. + * + * @param selectionModel The selection model to work on. + * @param parent The parent object. + */ + explicit SelectionProxyModel( QItemSelectionModel *selectionModel, QObject *parent = 0 ); + + /** + * Destroys the selection proxy model. + */ + virtual ~SelectionProxyModel(); + + /** + * Sets the header @p set that shall be used by the proxy model. + * + * \s EntityTreeModel::HeaderGroup + */ + void setHeaderSet( int set ); + + /** + * Returns the header set used by the proxy model. + */ + int headerSet() const; + + // QAbstractProxyModel does not proxy all methods... + virtual bool dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ); + virtual QMimeData* mimeData( const QModelIndexList & indexes ) const; + virtual QStringList mimeTypes() const; + + virtual QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const; + + private: + //@cond PRIVATE + Q_DECLARE_PRIVATE( SelectionProxyModel ) + SelectionProxyModelPrivate *d_ptr; + //@endcond +}; + +} + +#endif diff --git a/akonadi/servermanager.cpp b/akonadi/servermanager.cpp index faf134d98..93b3a323a 100644 --- a/akonadi/servermanager.cpp +++ b/akonadi/servermanager.cpp @@ -1,178 +1,178 @@ /* 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 "servermanager.h" #include "servermanager_p.h" #include "agenttype.h" #include "agentbase.h" #include "agentmanager.h" #include "selftestdialog_p.h" #include "session_p.h" #include #include #include #define AKONADI_CONTROL_SERVICE QLatin1String("org.freedesktop.Akonadi.Control") #define AKONADI_SERVER_SERVICE QLatin1String("org.freedesktop.Akonadi") using namespace Akonadi; class Akonadi::ServerManagerPrivate { public: ServerManagerPrivate() : instance( new ServerManager( this ) ) { operational = instance->isRunning(); } ~ServerManagerPrivate() { delete instance; } void serviceOwnerChanged( const QString &service, const QString &oldOwner, const QString &newOwner ) { Q_UNUSED( oldOwner ); Q_UNUSED( newOwner ); if ( service != AKONADI_SERVER_SERVICE && service != AKONADI_CONTROL_SERVICE ) return; serverProtocolVersion = -1, checkStatusChanged(); } void checkStatusChanged() { const bool status = instance->isRunning(); if ( status == operational ) return; operational = status; if ( operational ) emit instance->started(); else emit instance->stopped(); } ServerManager *instance; static int serverProtocolVersion; bool operational; }; int ServerManagerPrivate::serverProtocolVersion = -1; K_GLOBAL_STATIC( ServerManagerPrivate, sInstance ) ServerManager::ServerManager(ServerManagerPrivate * dd ) : d( dd ) { connect( QDBusConnection::sessionBus().interface(), SIGNAL(serviceOwnerChanged(QString,QString,QString)), SLOT(serviceOwnerChanged(QString,QString,QString)) ); // HACK see if we are a agent ourselves and skip AgentManager creation since that can cause deadlocks QObject *obj = QDBusConnection::sessionBus().objectRegisteredAt( QLatin1String("/") ); if ( obj && dynamic_cast( obj ) ) return; connect( AgentManager::self(), SIGNAL(typeAdded(Akonadi::AgentType)), SLOT(checkStatusChanged()) ); connect( AgentManager::self(), SIGNAL(typeRemoved(Akonadi::AgentType)), SLOT(checkStatusChanged()) ); } ServerManager * Akonadi::ServerManager::self() { return sInstance->instance; } bool ServerManager::start() { const bool ok = QProcess::startDetached( QLatin1String("akonadi_control") ); if ( !ok ) { - kWarning( 5250 ) << "Unable to execute akonadi_control, falling back to D-Bus auto-launch"; + kWarning() << "Unable to execute akonadi_control, falling back to D-Bus auto-launch"; QDBusReply reply = QDBusConnection::sessionBus().interface()->startService( AKONADI_CONTROL_SERVICE ); if ( !reply.isValid() ) { - kDebug( 5250 ) << "Akonadi server could not be started via D-Bus either: " - << reply.error().message(); + kDebug() << "Akonadi server could not be started via D-Bus either: " + << reply.error().message(); return false; } } return true; } bool ServerManager::stop() { QDBusInterface iface( AKONADI_CONTROL_SERVICE, QString::fromLatin1("/ControlManager"), QString::fromLatin1("org.freedesktop.Akonadi.ControlManager") ); if ( !iface.isValid() ) return false; iface.call( QDBus::NoBlock, QString::fromLatin1("shutdown") ); return true; } void ServerManager::showSelfTestDialog( QWidget *parent ) { Akonadi::SelfTestDialog dlg( parent ); dlg.hideIntroduction(); dlg.exec(); } bool ServerManager::isRunning() { if ( !QDBusConnection::sessionBus().interface()->isServiceRegistered( AKONADI_CONTROL_SERVICE ) || !QDBusConnection::sessionBus().interface()->isServiceRegistered( AKONADI_SERVER_SERVICE ) ) { return false; } // check if the server protocol is recent enough if ( sInstance.exists() ) { if ( Internal::serverProtocolVersion() >= 0 && Internal::serverProtocolVersion() < SessionPrivate::minimumProtocolVersion() ) return false; } // HACK see if we are a agent ourselves and skip the test below which can in some cases deadlock the server // and is not really needed in this case anyway since we happen to know at least one agent is available QObject *obj = QDBusConnection::sessionBus().objectRegisteredAt( QLatin1String("/") ); if ( obj && dynamic_cast( obj ) ) return true; // besides the running server processes we also need at least one resource to be operational AgentType::List agentTypes = AgentManager::self()->types(); foreach ( const AgentType &type, agentTypes ) { if ( type.capabilities().contains( QLatin1String("Resource") ) ) return true; } return false; } int Internal::serverProtocolVersion() { return ServerManagerPrivate::serverProtocolVersion; } void Internal::setServerProtocolVersion( int version ) { ServerManagerPrivate::serverProtocolVersion = version; if ( sInstance.exists() ) sInstance->checkStatusChanged(); } #include "servermanager.moc" diff --git a/akonadi/session.cpp b/akonadi/session.cpp index 2fe1c2ee2..5acc6cc11 100644 --- a/akonadi/session.cpp +++ b/akonadi/session.cpp @@ -1,306 +1,306 @@ /* 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 "session.h" #include "session_p.h" #include "imapparser_p.h" #include "job.h" #include "job_p.h" #include "servermanager_p.h" #include "xdgbasedirs_p.h" #include #include #include #include #include #include #include #include #define PIPELINE_LENGTH 2 using namespace Akonadi; //@cond PRIVATE void SessionPrivate::startNext() { QTimer::singleShot( 0, mParent, SLOT(doStartNext()) ); } void SessionPrivate::reconnect() { // should be checking connection method and value validity if ( socket->state() != QLocalSocket::ConnectedState && socket->state() != QLocalSocket::ConnectingState ) { #ifdef Q_OS_WIN //krazy:exclude=cpp const QString namedPipe = mConnectionSettings->value( QLatin1String( "Data/NamedPipe" ), QLatin1String( "Akonadi" ) ).toString(); socket->connectToServer( namedPipe ); #else const QString defaultSocketDir = XdgBaseDirs::saveDir( "data", QLatin1String( "akonadi" ) ); const QString path = mConnectionSettings->value( QLatin1String( "Data/UnixPath" ), defaultSocketDir + QLatin1String( "/akonadiserver.socket" ) ).toString(); socket->connectToServer( path ); #endif } } void SessionPrivate::socketError( QLocalSocket::LocalSocketError error ) { Q_ASSERT( mParent->sender() == socket ); - kWarning( 5250 ) << "Socket error occurred:" << socket->errorString(); + kWarning() << "Socket error occurred:" << socket->errorString(); socketDisconnected(); } void SessionPrivate::socketDisconnected() { if ( currentJob ) currentJob->d_ptr->lostConnection(); connected = false; QTimer::singleShot( 1000, mParent, SLOT(reconnect()) ); } void SessionPrivate::dataReceived() { while ( socket->bytesAvailable() > 0 ) { if ( parser->continuationSize() > 1 ) { const QByteArray data = socket->read( qMin( socket->bytesAvailable(), parser->continuationSize() - 1 ) ); parser->parseBlock( data ); } else if ( socket->canReadLine() ) { if ( !parser->parseNextLine( socket->readLine() ) ) continue; // response not yet completed // handle login response if ( parser->tag() == QByteArray("0") ) { if ( parser->data().startsWith( "OK" ) ) { //krazy:exclude=strings connected = true; startNext(); } else { - kWarning( 5250 ) << "Unable to login to Akonadi server:" << parser->data(); + kWarning() << "Unable to login to Akonadi server:" << parser->data(); socket->close(); QTimer::singleShot( 1000, mParent, SLOT(reconnect()) ); } } // send login command if ( parser->tag() == "*" && parser->data().startsWith( "OK Akonadi" ) ) { const int pos = parser->data().indexOf( "[PROTOCOL" ); if ( pos > 0 ) { qint64 tmp = 0; ImapParser::parseNumber( parser->data(), tmp, 0, pos + 9 ); protocolVersion = tmp; Internal::setServerProtocolVersion( tmp ); } - kDebug( 5250 ) << "Server protocol version is:" << protocolVersion; + kDebug() << "Server protocol version is:" << protocolVersion; writeData( "0 LOGIN " + ImapParser::quote( sessionId ) + '\n' ); // work for the current job } else { if ( currentJob ) currentJob->d_ptr->handleResponse( parser->tag(), parser->data() ); } // reset parser stuff parser->reset(); } else { break; // nothing we can do for now } } } bool SessionPrivate::canPipelineNext() { if ( queue.isEmpty() || pipeline.count() >= PIPELINE_LENGTH ) return false; if ( pipeline.isEmpty() && currentJob ) return currentJob->d_ptr->mWriteFinished; if ( !pipeline.isEmpty() ) return pipeline.last()->d_ptr->mWriteFinished; return false; } void SessionPrivate::doStartNext() { if ( !connected || (queue.isEmpty() && pipeline.isEmpty()) ) return; if ( canPipelineNext() ) { Akonadi::Job *nextJob = queue.dequeue(); pipeline.enqueue( nextJob ); startJob( nextJob ); } if ( jobRunning ) return; jobRunning = true; if ( !pipeline.isEmpty() ) { currentJob = pipeline.dequeue(); } else { currentJob = queue.dequeue(); startJob( currentJob ); } } void SessionPrivate::startJob( Job *job ) { if ( protocolVersion < minimumProtocolVersion() ) { job->setError( Job::ProtocolVersionMismatch ); job->setErrorText( i18n( "Protocol version %1 found, expected at least %2", protocolVersion, minimumProtocolVersion() ) ); job->emitResult(); } else { job->d_ptr->startQueued(); } } void SessionPrivate::jobDone(KJob * job) { if( job == currentJob ) { if ( pipeline.isEmpty() ) { jobRunning = false; currentJob = 0; } else { currentJob = pipeline.dequeue(); } startNext(); } // ### better handle the other cases too, user might have canceled jobs else { - kDebug( 5250 ) << job << "Non-current job finished."; + kDebug() << job << "Non-current job finished."; } } void SessionPrivate::jobWriteFinished( Akonadi::Job* job ) { Q_ASSERT( (job == currentJob && pipeline.isEmpty()) || (job = pipeline.last()) ); startNext(); } void SessionPrivate::jobDestroyed(QObject * job) { queue.removeAll( static_cast( job ) ); // ### likely not enough to really cancel already running jobs pipeline.removeAll( static_cast( job ) ); if ( currentJob == job ) { currentJob = 0; jobRunning = false; } } void SessionPrivate::addJob(Job * job) { queue.append( job ); QObject::connect( job, SIGNAL(result(KJob*)), mParent, SLOT(jobDone(KJob*)) ); QObject::connect( job, SIGNAL(writeFinished(Akonadi::Job*)), mParent, SLOT(jobWriteFinished(Akonadi::Job*)) ); QObject::connect( job, SIGNAL(destroyed(QObject*)), mParent, SLOT(jobDestroyed(QObject*)) ); startNext(); } int SessionPrivate::nextTag() { return theNextTag++; } void SessionPrivate::writeData(const QByteArray & data) { socket->write( data ); } //@endcond Session::Session(const QByteArray & sessionId, QObject * parent) : QObject( parent ), d( new SessionPrivate( this ) ) { if ( !sessionId.isEmpty() ) { d->sessionId = sessionId; } else { d->sessionId = QCoreApplication::instance()->applicationName().toUtf8() + '-' + QByteArray::number( qrand() ); } d->connected = false; d->theNextTag = 1; d->currentJob = 0; d->jobRunning = false; const QString connectionConfigFile = XdgBaseDirs::akonadiConnectionConfigFile(); QFileInfo fileInfo( connectionConfigFile ); if ( !fileInfo.exists() ) { - kWarning( 5250 ) << "Akonadi Client Session: connection config file '" - << "akonadi/akonadiconnectionrc can not be found in '" - << XdgBaseDirs::homePath( "config" ) << "' nor in any of " - << XdgBaseDirs::systemPathList( "config" ); + kWarning() << "Akonadi Client Session: connection config file '" + << "akonadi/akonadiconnectionrc can not be found in '" + << XdgBaseDirs::homePath( "config" ) << "' nor in any of " + << XdgBaseDirs::systemPathList( "config" ); } d->mConnectionSettings = new QSettings( connectionConfigFile, QSettings::IniFormat ); // should check connection method d->socket = new QLocalSocket( this ); connect( d->socket, SIGNAL(disconnected()), SLOT(socketDisconnected()) ); connect( d->socket, SIGNAL(error(QLocalSocket::LocalSocketError)), SLOT(socketError(QLocalSocket::LocalSocketError)) ); connect( d->socket, SIGNAL(readyRead()), SLOT(dataReceived()) ); d->reconnect(); } Session::~Session() { clear(); delete d; } QByteArray Session::sessionId() const { return d->sessionId; } QThreadStorage instances; void SessionPrivate::createDefaultSession( const QByteArray &sessionId ) { Q_ASSERT_X( !sessionId.isEmpty(), "SessionPrivate::createDefaultSession", "You tried to create a default session with empty session id!" ); Q_ASSERT_X( !instances.hasLocalData(), "SessionPrivate::createDefaultSession", "You tried to create a default session twice!" ); instances.setLocalData( new Session( sessionId ) ); } Session* Session::defaultSession() { if ( !instances.hasLocalData() ) instances.setLocalData( new Session() ); return instances.localData(); } void Session::clear() { foreach ( Job* job, d->queue ) job->kill( KJob::EmitResult ); d->queue.clear(); if ( d->currentJob ) d->currentJob->kill( KJob::EmitResult ); } #include "session.moc" diff --git a/akonadi/standardactionmanager.cpp b/akonadi/standardactionmanager.cpp index f7aaeffe1..4afc68062 100644 --- a/akonadi/standardactionmanager.cpp +++ b/akonadi/standardactionmanager.cpp @@ -1,634 +1,709 @@ /* 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 "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 ) ); - //FIXME: remove the reinterpret_cast once FavoriteCollectionsModel is in kdepimlibs/akonadi - enableAction( AddToFavoriteCollections, (favoritesModel!=0) && (selectedIndex.model()!=reinterpret_cast(favoritesModel)) + enableAction( AddToFavoriteCollections, (favoritesModel!=0) && (!favoritesModel->collections().contains(col)) && singleColSelected && (col != Collection::root()) ); - //FIXME: better check if the collection is in the model, todo once FavoriteCollectionsModel is in kdepimlibs/akonadi - enableAction( RemoveFromFavoriteCollections, (favoriteSelectionModel!=0) && (selectedIndex.model()!=reinterpret_cast(favoritesModel)) + enableAction( RemoveFromFavoriteCollections, (favoritesModel!=0) && (favoritesModel->collections().contains(col)) && singleColSelected && (col != Collection::root()) ); - enableAction( RenameFavoriteCollection, (favoriteSelectionModel!=0) && (selectedIndex.model()!=reinterpret_cast(favoritesModel)) + 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; int itemCount = 0; if ( itemSelectionModel ) { itemCount = itemSelectionModel->selectedRows().count(); multiItemSelected = itemCount > 0; } 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.setParent( 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() ); - //FIXME: remove the reinterpret_cast and invokeMethod once FavoriteCollectionsModel is in kdepimlibs/akonadi - QAbstractItemModel *model = reinterpret_cast( favoritesModel ); - QMetaObject::invokeMethod( model, "addCollection", Q_ARG(Collection, collection) ); + 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() ); - //FIXME: remove the reinterpret_cast and invokeMethod once FavoriteCollectionsModel is in kdepimlibs/akonadi - QAbstractItemModel *model = reinterpret_cast( favoritesModel ); - QMetaObject::invokeMethod( model, "removeCollection", Q_ARG(Collection, collection) ); + 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; - //FIXME: remove the reinterpret_cast and invokeMethod once FavoriteCollectionsModel is in kdepimlibs/akonadi - QAbstractItemModel *model = reinterpret_cast( favoritesModel ); - QMetaObject::invokeMethod( model, "setFavoriteLabel", Q_ARG(Collection, collection), Q_ARG(QString, label) ); + 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; - QItemSelectionModel *favoriteSelectionModel; 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) +void StandardActionManager::setCollectionSelectionModel( QItemSelectionModel * selectionModel ) { d->collectionSelectionModel = selectionModel; connect( selectionModel, SIGNAL(selectionChanged( const QItemSelection&, const QItemSelection& )), - SLOT(updateActions()) ); + SLOT(collectionSelectionChanged()) ); + + d->checkModelsConsistency(); } -void StandardActionManager::setItemSelectionModel(QItemSelectionModel * selectionModel) +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(updateActions()) ); + 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) +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/akonadi/standardactionmanager.h b/akonadi/standardactionmanager.h index 6179fc6e6..4fdb08888 100644 --- a/akonadi/standardactionmanager.h +++ b/akonadi/standardactionmanager.h @@ -1,228 +1,230 @@ /* 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. */ #ifndef AKONADI_STANDARDACTIONMANAGER_H #define AKONADI_STANDARDACTIONMANAGER_H #include "akonadi_export.h" #include class KAction; class KActionCollection; class KLocalizedString; class QItemSelectionModel; class QWidget; namespace Akonadi { class FavoriteCollectionsModel; /** * @short Manages generic actions for collection and item views. * * Manages generic Akonadi actions common for all types. This covers * creating of the actions with appropriate labels, icons, shortcuts * etc., updating the action state depending on the current selection * as well as default implementations for the actual operations. * * If the default implementation is not appropriate for your application * you can still use the state tracking by disconnecting the triggered() * signal and re-connecting it to your implementation. The actual KAction * objects can be retrieved by calling createAction() or action() for that. * * If the default look and feel (labels, icons, shortcuts) of the actions * is not appropriate for your application, you can access them as noted * above and customize them to your needs. Additionally, you can set a * KLocalizedString which should be used as a action label with correct * plural handling for actions operating on multiple objects with * setActionText(). * * Finally, if you have special needs for the action states, connect to * the actionStateUpdated() signal and adjust the state accordingly. * * The following actions are provided (KAction name in parenthesis): * - Creation of a new collection (@c akonadi_collection_create) * - Copying of selected collections (@c akonadi_collection_copy) * - Deletion of selected collections (@c akonadi_collection_delete) * - Synchronization of selected collections (@c akonadi_collection_sync) * - Showing the collection properties dialog for the current collection (@c akonadi_collection_properties) * - Copying of selected items (@c akonadi_itemcopy) * - Pasting collections, items or raw data (@c akonadi_paste) * - Deleting of selected items (@c akonadi_item_delete) * - Managing local subscriptions (@c akonadi_manage_local_subscriptions) * * The following example shows how to use standard actions in your application: * * @code * * Akonadi::StandardActionManager *actMgr = new Akonadi::StandardActionManager( actionCollection(), this ); * actMgr->setCollectionSelectionModel( collectionView->collectionSelectionModel() ); * actMgr->createAllActions(); * * @endcode * * Additionally you have to add the actions to the KXMLGUI file of your application, * using the names listed above. * * If you only need a subset of the actions provided, you can call createAction() * instead of createAllActions() for the action types you want. * * @todo collection deleting and sync do not support multi-selection yet * * @author Volker Krause */ class AKONADI_EXPORT StandardActionManager : public QObject { Q_OBJECT public: /** * Describes the supported actions. */ enum Type { CreateCollection, ///< Creates an collection CopyCollections, ///< Copies collections DeleteCollections, ///< Deletes collections SynchronizeCollections, ///< Synchronizes collections CollectionProperties, ///< Provides collection properties CopyItems, ///< Copies items Paste, ///< Paste collections or items DeleteItems, ///< Deletes items ManageLocalSubscriptions, ///< Manages local subscriptions AddToFavoriteCollections, ///< Add the collection to the favorite collections model RemoveFromFavoriteCollections, ///< Remove the collection from the favorite collections model RenameFavoriteCollection, ///< Rename the collection of the favorite collections model CopyCollectionToMenu, ///< Menu allowing to quickly copy a collection into another collection CopyItemToMenu, ///< Menu allowing to quickly copy an item into a collection LastType ///< Marks last action }; /** * Creates a new standard action manager. * * @param actionCollection The action collection to operate on. * @param parent The parent widget. */ explicit StandardActionManager( KActionCollection *actionCollection, QWidget *parent = 0 ); /** * Destroys the standard action manager. */ ~StandardActionManager(); /** * Sets the collection selection model based on which the collection * related actions should operate. If none is set, all collection actions * will be disabled. */ void setCollectionSelectionModel( QItemSelectionModel *selectionModel ); /** * Sets the item selection model based on which the item related actions * should operate. If none is set, all item actions will be disabled. */ void setItemSelectionModel( QItemSelectionModel* selectionModel ); /** * Sets the favorite collections model based on which the collection * relatedactions should operate. If none is set, the "Add to Favorite Folders" action * will be disabled. */ void setFavoriteCollectionsModel( FavoriteCollectionsModel *favoritesModel ); /** * Sets the favorite collection selection model based on which the favorite * collection related actions should operate. If none is set, all favorite modifications * actions will be disabled. */ void setFavoriteSelectionModel( QItemSelectionModel *selectionModel ); /** * Creates the action of the given type and adds it to the action collection * specified in the constructor if it does not exist yet. The action is * connected to its default implementation provided by this class. */ KAction* createAction( Type type ); /** * Convenience method to create all standard actions. * @see createAction() */ void createAllActions(); /** * Returns the action of the given type, 0 if it has not been created (yet). */ KAction* action( Type type ) const; /** * Sets the label of the action @p type to @p text, which is used during * updating the action state and substituted according to the number of * selected objects. This is mainly useful to customize the label of actions * that can operate on multiple objects. * * Example: * @code * acctMgr->setActionText( Akonadi::StandardActionManager::CopyItems, * ki18np( "Copy Mail", "Copy %1 Mails" ) ); * @endcode */ void setActionText( Type type, const KLocalizedString &text ); Q_SIGNALS: /** * This signal is emitted whenever the action state has been updated. * In case you have special needs for changing the state of some actions, * connect to this signal and adjust the action state. */ void actionStateUpdated(); private: //@cond PRIVATE class Private; Private* const d; Q_PRIVATE_SLOT( d, void updateActions() ) Q_PRIVATE_SLOT( d, void clipboardChanged(QClipboard::Mode) ) + Q_PRIVATE_SLOT( d, void collectionSelectionChanged() ) + Q_PRIVATE_SLOT( d, void favoriteSelectionChanged() ) Q_PRIVATE_SLOT( d, void slotCreateCollection() ) Q_PRIVATE_SLOT( d, void slotCopyCollections() ) Q_PRIVATE_SLOT( d, void slotDeleteCollection() ) Q_PRIVATE_SLOT( d, void slotSynchronizeCollection() ) Q_PRIVATE_SLOT( d, void slotCollectionProperties() ) Q_PRIVATE_SLOT( d, void slotCopyItems() ) Q_PRIVATE_SLOT( d, void slotPaste() ) Q_PRIVATE_SLOT( d, void slotDeleteItems() ) Q_PRIVATE_SLOT( d, void slotLocalSubscription() ) Q_PRIVATE_SLOT( d, void slotAddToFavorites() ) Q_PRIVATE_SLOT( d, void slotRemoveFromFavorites() ) Q_PRIVATE_SLOT( d, void slotRenameFavorite() ) Q_PRIVATE_SLOT( d, void slotCopyCollectionTo(QAction*) ) Q_PRIVATE_SLOT( d, void slotCopyItemTo(QAction*) ) Q_PRIVATE_SLOT( d, void collectionCreationResult(KJob*) ) Q_PRIVATE_SLOT( d, void collectionDeletionResult(KJob*) ) Q_PRIVATE_SLOT( d, void pasteResult(KJob*) ) Q_PRIVATE_SLOT( d, void copyToResult(KJob*) ) //@endcond }; } #endif diff --git a/akonadi/statisticsproxymodel.cpp b/akonadi/statisticsproxymodel.cpp new file mode 100644 index 000000000..66796b896 --- /dev/null +++ b/akonadi/statisticsproxymodel.cpp @@ -0,0 +1,164 @@ +/* + Copyright (c) 2009 Kevin Ottens + + + 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 "statisticsproxymodel.h" + +#include "entitytreemodel.h" +#include "collectionutils_p.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class StatisticsProxyModel::Private +{ + public: + Private( StatisticsProxyModel *parent ) + : mParent( parent ) + { + } + + StatisticsProxyModel *mParent; +}; + +StatisticsProxyModel::StatisticsProxyModel( QObject *parent ) + : QSortFilterProxyModel( parent ), + d( new Private( this ) ) +{ + setSupportedDragActions( Qt::CopyAction | Qt::MoveAction ); +} + +StatisticsProxyModel::~StatisticsProxyModel() +{ + delete d; +} + +QModelIndex Akonadi::StatisticsProxyModel::index( int row, int column, const QModelIndex & parent ) const +{ + if (!hasIndex(row, column, parent)) + return QModelIndex(); + + + int sourceColumn = column; + + if ( column>=columnCount( parent)-3 ) { + sourceColumn = 0; + } + + QModelIndex i = QSortFilterProxyModel::index( row, sourceColumn, parent ); + return createIndex( i.row(), column, i.internalPointer() ); +} + +QVariant StatisticsProxyModel::data( const QModelIndex & index, int role) const +{ + if ( role == Qt::DisplayRole && index.column()>=columnCount( index.parent() )-3 ) { + const QModelIndex sourceIndex = mapToSource( index.sibling( index.row(), 0 ) ); + Collection collection = sourceModel()->data( sourceIndex, EntityTreeModel::CollectionRole ).value(); + + if ( collection.isValid() && collection.statistics().count()>=0 ) { + if ( index.column() == columnCount( QModelIndex() )-1 ) { + return KIO::convertSize( (KIO::filesize_t)( collection.statistics().size() ) ); + } else if ( index.column() == columnCount( QModelIndex() )-2 ) { + return collection.statistics().count(); + } else if ( index.column() == columnCount( QModelIndex() )-3 ) { + if ( collection.statistics().unreadCount() > 0 ) { + return collection.statistics().unreadCount(); + } else { + return QString(); + } + } else { + kWarning() << "We shouldn't get there for a column which is not total, unread or size."; + return QVariant(); + } + } + } else if ( role == Qt::TextAlignmentRole && index.column()>=columnCount( index.parent() )-3 ) { + return Qt::AlignRight; + } + + return QAbstractProxyModel::data( index, role ); +} + +QVariant StatisticsProxyModel::headerData( int section, Qt::Orientation orientation, int role) const +{ + if ( orientation == Qt::Horizontal && role == Qt::DisplayRole ) { + if ( section == columnCount( QModelIndex() )-1 ) { + return i18n( "Size" ); + } else if ( section == columnCount( QModelIndex() )-2 ) { + return i18n( "Total" ); + } else if ( section == columnCount( QModelIndex() )-3 ) { + return i18n( "Unread" ); + } + } + + return QSortFilterProxyModel::headerData( section, orientation, role ); +} + +Qt::ItemFlags StatisticsProxyModel::flags( const QModelIndex & index ) const +{ + if ( index.column()>=columnCount( index.parent() )-3 ) { + return QSortFilterProxyModel::flags( index.sibling( index.row(), 0 ) ) + & ( Qt::ItemIsSelectable | Qt::ItemIsDragEnabled // Allowed flags + | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled ); + } + + return QSortFilterProxyModel::flags( index ); +} + +int StatisticsProxyModel::columnCount( const QModelIndex & parent ) const +{ + return sourceModel()->columnCount( mapToSource( parent ) ) + 3; +} + +bool StatisticsProxyModel::dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ) +{ + Q_ASSERT(sourceModel()); + const QModelIndex sourceParent = mapToSource(parent); + return sourceModel()->dropMimeData(data, action, row, column, sourceParent); +} + +QMimeData* StatisticsProxyModel::mimeData( const QModelIndexList & indexes ) const +{ + Q_ASSERT(sourceModel()); + QModelIndexList sourceIndexes; + foreach(const QModelIndex& index, indexes) + sourceIndexes << mapToSource(index); + return sourceModel()->mimeData(sourceIndexes); +} + +QStringList StatisticsProxyModel::mimeTypes() const +{ + Q_ASSERT(sourceModel()); + return sourceModel()->mimeTypes(); +} + +#include "statisticsproxymodel.moc" + diff --git a/akonadi/statisticsproxymodel.h b/akonadi/statisticsproxymodel.h new file mode 100644 index 000000000..022bfb4d1 --- /dev/null +++ b/akonadi/statisticsproxymodel.h @@ -0,0 +1,87 @@ +/* + Copyright (c) 2009 Kevin Ottens + + 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_STATISTICSPROXYMODEL_H +#define AKONADI_STATISTICSPROXYMODEL_H + +#include "akonadi_export.h" + +#include + +namespace Akonadi { + +/** + * @short A proxy model that exposes collection statistics through extra columns. + * + * This class can be used on top of an EntityTreeModel to display extra columns + * summarizing statistics of collections. + * + * @code + * + * Akonadi::EntityTreeModel *model = new Akonadi::EntityTreeModel( ... ); + * + * Akonadi::StatisticsProxyModel *proxy = new Akonadi::StatisticsProxyModel(); + * proxy->setSourceModel( model ); + * + * Akonadi::EntityTreeView *view = new Akonadi::EntityTreeView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @author Kevin Ottens + * @since 4.4 + */ +class AKONADI_EXPORT StatisticsProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + public: + /** + * Creates a new statistics proxy model. + * + * @param parent The parent object. + */ + explicit StatisticsProxyModel( QObject *parent = 0 ); + + /** + * Destroys the statistics proxy model. + */ + virtual ~StatisticsProxyModel(); + + virtual QModelIndex index ( int row, int column, const QModelIndex & parent = QModelIndex() ) const; + virtual QVariant data( const QModelIndex & index, int role = Qt::DisplayRole ) const; + virtual QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const; + virtual Qt::ItemFlags flags ( const QModelIndex & index ) const; + virtual int columnCount( const QModelIndex & parent = QModelIndex() ) const; + + // QAbstractProxyModel does not proxy all methods... + virtual bool dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ); + virtual QMimeData* mimeData( const QModelIndexList & indexes ) const; + virtual QStringList mimeTypes() const; + + private: + //@cond PRIVATE + class Private; + Private* const d; + //@endcond +}; + +} + +#endif diff --git a/akonadi/statisticstooltipproxymodel.cpp b/akonadi/statisticstooltipproxymodel.cpp new file mode 100644 index 000000000..d81072664 --- /dev/null +++ b/akonadi/statisticstooltipproxymodel.cpp @@ -0,0 +1,174 @@ +/* + Copyright (c) 2009 Kevin Ottens + + + 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 "statisticstooltipproxymodel.h" + +#include "entitytreemodel.h" +#include "collectionutils_p.h" + +#include +#include + +#include +#include +#include + +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class StatisticsToolTipProxyModel::Private +{ + public: + Private( StatisticsToolTipProxyModel *parent ) + : mParent( parent ) + { + } + + StatisticsToolTipProxyModel *mParent; +}; + +StatisticsToolTipProxyModel::StatisticsToolTipProxyModel( QObject *parent ) + : QSortFilterProxyModel( parent ), + d( new Private( this ) ) +{ + setSupportedDragActions( Qt::CopyAction | Qt::MoveAction ); +} + +StatisticsToolTipProxyModel::~StatisticsToolTipProxyModel() +{ + delete d; +} + +QVariant StatisticsToolTipProxyModel::data( const QModelIndex & index, int role) const +{ + if ( role == Qt::ToolTipRole ) { + const QModelIndex sourceIndex = mapToSource( index ); + Collection collection = sourceModel()->data( sourceIndex, EntityTreeModel::CollectionRole ).value(); + + if ( collection.isValid() && collection.statistics().count()>0 ) { + + QString bckColor = QApplication::palette().color( QPalette::ToolTipBase ).name(); + QString txtColor = QApplication::palette().color( QPalette::ToolTipText ).name(); + + QString tip = QString::fromLatin1( + "\n" + ); + + tip += QString::fromLatin1( + " \n" + " \n" + " \n" + ).arg( txtColor ).arg( bckColor ).arg( data( index, Qt::DisplayRole ).toString() ); + + + tip += QString::fromLatin1( + " \n" + " \n" + " \n" + " \n" + ).arg( iconPath ); + + tip += QString::fromLatin1( + "
\n" + "
\n" + " %3\n" + "
\n" + "
\n" + ); + + tip += QString::fromLatin1( + " %1: %2
\n" + " %3: %4

\n" + ).arg( i18n("Total Messages") ).arg( collection.statistics().count() ) + .arg( i18n("Unread Messages") ).arg( collection.statistics().unreadCount() ); + + //TODO: Quota missing + //tip += QString::fromLatin1( + // " %1: %2
\n" + // ).arg( i18n( "Quota" ) ).arg( "0" ); + + tip += QString::fromLatin1( + " %1: %2
\n" + ).arg( i18n("Storage Size") ).arg( KIO::convertSize( (KIO::filesize_t)( collection.statistics().size() ) ) ); + + + QString iconName = CollectionUtils::defaultIconName( collection ); + if ( collection.hasAttribute() && + !collection.attribute()->iconName().isEmpty() ) { + iconName = collection.attribute()->iconName(); + } + + int iconSizes[] = { 32, 22 }; + QString iconPath; + + for ( int i = 0; i < 2; i++ ) { + iconPath = KIconLoader::global()->iconPath( iconName, -iconSizes[ i ], true ); + if ( !iconPath.isEmpty() ) + break; + } + + if ( iconPath.isEmpty() ) { + iconPath = KIconLoader::global()->iconPath( QLatin1String("folder"), -32, false ); + } + + tip += QString::fromLatin1( + "
\n" + "
\n" + " \n" + "
\n" + "
" + ); + + return tip; + } + } + + return QAbstractProxyModel::data( index, role ); +} + +bool StatisticsToolTipProxyModel::dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ) +{ + Q_ASSERT(sourceModel()); + const QModelIndex sourceParent = mapToSource(parent); + return sourceModel()->dropMimeData(data, action, row, column, sourceParent); +} + +QMimeData* StatisticsToolTipProxyModel::mimeData( const QModelIndexList & indexes ) const +{ + Q_ASSERT(sourceModel()); + QModelIndexList sourceIndexes; + foreach(const QModelIndex& index, indexes) + sourceIndexes << mapToSource(index); + return sourceModel()->mimeData(sourceIndexes); +} + +QStringList StatisticsToolTipProxyModel::mimeTypes() const +{ + Q_ASSERT(sourceModel()); + return sourceModel()->mimeTypes(); +} + +#include "statisticstooltipproxymodel.moc" + diff --git a/akonadi/statisticstooltipproxymodel.h b/akonadi/statisticstooltipproxymodel.h new file mode 100644 index 000000000..9a8b7d42b --- /dev/null +++ b/akonadi/statisticstooltipproxymodel.h @@ -0,0 +1,80 @@ +/* + Copyright (c) 2009 Kevin Ottens + + 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_STATISTICSTOOLTIPPROXYMODEL_H +#define AKONADI_STATISTICSTOOLTIPPROXYMODEL_H + +#include +#include "akonadi_export.h" +namespace Akonadi { + +/** + * @short A proxy model that exposes collection statistics through a tooltip. + * + * This class can be used on top of an EntityTreeModel to display a tooltip + * summarizing statistics of collections. + * + * @code + * + * Akonadi::EntityTreeModel *model = new Akonadi::EntityTreeModel( ... ); + * + * Akonadi::StatisticsToolTipProxyModel *proxy = new Akonadi::StatisticsToolTipProxyModel(); + * proxy->setSourceModel( model ); + * + * Akonadi::EntityTreeView *view = new Akonadi::EntityTreeView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @author Kevin Ottens + */ +class AKONADI_EXPORT StatisticsToolTipProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + public: + /** + * Creates a new statistics tooltip proxy model. + * + * @param parent The parent object. + */ + explicit StatisticsToolTipProxyModel( QObject *parent = 0 ); + + /** + * Destroys the statistics tooltip proxy model. + */ + virtual ~StatisticsToolTipProxyModel(); + + virtual QVariant data( const QModelIndex & index, int role = Qt::DisplayRole ) const; + + // QAbstractProxyModel does not proxy all methods... + virtual bool dropMimeData( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ); + virtual QMimeData* mimeData( const QModelIndexList & indexes ) const; + virtual QStringList mimeTypes() const; + + private: + //@cond PRIVATE + class Private; + Private* const d; + //@endcond +}; + +} + +#endif diff --git a/includes/Akonadi/DescendantsProxyModel b/includes/Akonadi/DescendantsProxyModel new file mode 100644 index 000000000..2a0041f06 --- /dev/null +++ b/includes/Akonadi/DescendantsProxyModel @@ -0,0 +1 @@ +#include "../../akonadi/descendantsproxymodel.h" diff --git a/includes/Akonadi/PreprocessorBase b/includes/Akonadi/PreprocessorBase new file mode 100644 index 000000000..fe968ac47 --- /dev/null +++ b/includes/Akonadi/PreprocessorBase @@ -0,0 +1 @@ +#include "../../akonadi/preprocessorbase.h" diff --git a/includes/Akonadi/SelectionProxyModel b/includes/Akonadi/SelectionProxyModel new file mode 100644 index 000000000..9d9e45a00 --- /dev/null +++ b/includes/Akonadi/SelectionProxyModel @@ -0,0 +1 @@ +#include "../../akonadi/selectionproxymodel.h" diff --git a/kcal/calendar.cpp b/kcal/calendar.cpp index f13d9e097..9a0ae194d 100644 --- a/kcal/calendar.cpp +++ b/kcal/calendar.cpp @@ -1,1188 +1,1198 @@ /* This file is part of the kcal library. Copyright (c) 1998 Preston Brown Copyright (c) 2000-2004 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (c) 2006 David Jarvie 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. */ /** @file This file is part of the API for handling calendar data and defines the Calendar class. @brief Represents the main calendar class. @author Preston Brown \ @author Cornelius Schumacher \ @author Reinhold Kainhofer \ @author David Jarvie \ */ #include "calendar.h" #include "exceptions.h" #include "calfilter.h" #include "icaltimezones.h" #include #include extern "C" { #include } using namespace KCal; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KCal::Calendar::Private { public: Private() : mTimeZones( new ICalTimeZones ), mModified( false ), mNewObserver( false ), mObserversEnabled( true ), mDefaultFilter( new CalFilter ) { // Setup default filter, which does nothing mFilter = mDefaultFilter; mFilter->setEnabled( false ); // user information... mOwner.setName( i18n( "Unknown Name" ) ); mOwner.setEmail( i18n( "unknown@nowhere" ) ); } ~Private() { delete mTimeZones; delete mDefaultFilter; } KDateTime::Spec timeZoneIdSpec( const QString &timeZoneId, bool view ); QString mProductId; Person mOwner; ICalTimeZones *mTimeZones; // collection of time zones used in this calendar ICalTimeZone mBuiltInTimeZone; // cached time zone lookup ICalTimeZone mBuiltInViewTimeZone; // cached viewing time zone lookup KDateTime::Spec mTimeSpec; mutable KDateTime::Spec mViewTimeSpec; bool mModified; bool mNewObserver; bool mObserversEnabled; QList mObservers; CalFilter *mDefaultFilter; CalFilter *mFilter; // These lists are used to put together related To-dos QMultiHash mOrphans; QMultiHash mOrphanUids; }; //@endcond Calendar::Calendar( const KDateTime::Spec &timeSpec ) : d( new KCal::Calendar::Private ) { d->mTimeSpec = timeSpec; d->mViewTimeSpec = timeSpec; } Calendar::Calendar( const QString &timeZoneId ) : d( new KCal::Calendar::Private ) { setTimeZoneId( timeZoneId ); } Calendar::~Calendar() { delete d; } Person Calendar::owner() const { return d->mOwner; } void Calendar::setOwner( const Person &owner ) { d->mOwner = owner; setModified( true ); } void Calendar::setTimeSpec( const KDateTime::Spec &timeSpec ) { d->mTimeSpec = timeSpec; d->mBuiltInTimeZone = ICalTimeZone(); setViewTimeSpec( timeSpec ); doSetTimeSpec( d->mTimeSpec ); } KDateTime::Spec Calendar::timeSpec() const { return d->mTimeSpec; } void Calendar::setTimeZoneId( const QString &timeZoneId ) { d->mTimeSpec = d->timeZoneIdSpec( timeZoneId, false ); d->mViewTimeSpec = d->mTimeSpec; d->mBuiltInViewTimeZone = d->mBuiltInTimeZone; doSetTimeSpec( d->mTimeSpec ); } //@cond PRIVATE KDateTime::Spec Calendar::Private::timeZoneIdSpec( const QString &timeZoneId, bool view ) { if ( view ) { mBuiltInViewTimeZone = ICalTimeZone(); } else { mBuiltInTimeZone = ICalTimeZone(); } if ( timeZoneId == QLatin1String( "UTC" ) ) { return KDateTime::UTC; } ICalTimeZone tz = mTimeZones->zone( timeZoneId ); if ( !tz.isValid() ) { ICalTimeZoneSource tzsrc; tz = tzsrc.parse( icaltimezone_get_builtin_timezone( timeZoneId.toLatin1() ) ); if ( view ) { mBuiltInViewTimeZone = tz; } else { mBuiltInTimeZone = tz; } } if ( tz.isValid() ) { return tz; } else { return KDateTime::ClockTime; } } //@endcond QString Calendar::timeZoneId() const { KTimeZone tz = d->mTimeSpec.timeZone(); return tz.isValid() ? tz.name() : QString(); } void Calendar::setViewTimeSpec( const KDateTime::Spec &timeSpec ) const { d->mViewTimeSpec = timeSpec; d->mBuiltInViewTimeZone = ICalTimeZone(); } void Calendar::setViewTimeZoneId( const QString &timeZoneId ) const { d->mViewTimeSpec = d->timeZoneIdSpec( timeZoneId, true ); } KDateTime::Spec Calendar::viewTimeSpec() const { return d->mViewTimeSpec; } QString Calendar::viewTimeZoneId() const { KTimeZone tz = d->mViewTimeSpec.timeZone(); return tz.isValid() ? tz.name() : QString(); } ICalTimeZones *Calendar::timeZones() const { return d->mTimeZones; } void Calendar::shiftTimes( const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec ) { setTimeSpec( newSpec ); int i, end; Event::List ev = events(); for ( i = 0, end = ev.count(); i < end; ++i ) { ev[i]->shiftTimes( oldSpec, newSpec ); } Todo::List to = todos(); for ( i = 0, end = to.count(); i < end; ++i ) { to[i]->shiftTimes( oldSpec, newSpec ); } Journal::List jo = journals(); for ( i = 0, end = jo.count(); i < end; ++i ) { jo[i]->shiftTimes( oldSpec, newSpec ); } } void Calendar::setFilter( CalFilter *filter ) { if ( filter ) { d->mFilter = filter; } else { d->mFilter = d->mDefaultFilter; } } CalFilter *Calendar::filter() { return d->mFilter; } QStringList Calendar::categories() { Incidence::List rawInc( rawIncidences() ); QStringList cats, thisCats; // @TODO: For now just iterate over all incidences. In the future, // the list of categories should be built when reading the file. for ( Incidence::List::ConstIterator i = rawInc.constBegin(); i != rawInc.constEnd(); ++i ) { thisCats = (*i)->categories(); for ( QStringList::ConstIterator si = thisCats.constBegin(); si != thisCats.constEnd(); ++si ) { if ( !cats.contains( *si ) ) { cats.append( *si ); } } } return cats; } Incidence::List Calendar::incidences( const QDate &date ) { return mergeIncidenceList( events( date ), todos( date ), journals( date ) ); } Incidence::List Calendar::incidences() { return mergeIncidenceList( events(), todos(), journals() ); } Incidence::List Calendar::rawIncidences() { return mergeIncidenceList( rawEvents(), rawTodos(), rawJournals() ); } Event::List Calendar::sortEvents( Event::List *eventList, EventSortField sortField, SortDirection sortDirection ) { Event::List eventListSorted; Event::List tempList, t; Event::List alphaList; Event::List::Iterator sortIt; Event::List::Iterator eit; // Notice we alphabetically presort Summaries first. // We do this so comparison "ties" stay in a nice order. switch( sortField ) { case EventSortUnsorted: eventListSorted = *eventList; break; case EventSortStartDate: alphaList = sortEvents( eventList, EventSortSummary, sortDirection ); for ( eit = alphaList.begin(); eit != alphaList.end(); ++eit ) { if ( (*eit)->dtStart().isDateOnly() ) { tempList.append( *eit ); continue; } sortIt = eventListSorted.begin(); if ( sortDirection == SortDirectionAscending ) { while ( sortIt != eventListSorted.end() && (*eit)->dtStart() >= (*sortIt)->dtStart() ) { ++sortIt; } } else { while ( sortIt != eventListSorted.end() && (*eit)->dtStart() < (*sortIt)->dtStart() ) { ++sortIt; } } eventListSorted.insert( sortIt, *eit ); } if ( sortDirection == SortDirectionAscending ) { // Prepend the list of Events without End DateTimes tempList += eventListSorted; eventListSorted = tempList; } else { // Append the list of Events without End DateTimes eventListSorted += tempList; } break; case EventSortEndDate: alphaList = sortEvents( eventList, EventSortSummary, sortDirection ); for ( eit = alphaList.begin(); eit != alphaList.end(); ++eit ) { if ( (*eit)->hasEndDate() ) { sortIt = eventListSorted.begin(); if ( sortDirection == SortDirectionAscending ) { while ( sortIt != eventListSorted.end() && (*eit)->dtEnd() >= (*sortIt)->dtEnd() ) { ++sortIt; } } else { while ( sortIt != eventListSorted.end() && (*eit)->dtEnd() < (*sortIt)->dtEnd() ) { ++sortIt; } } } else { // Keep a list of the Events without End DateTimes tempList.append( *eit ); } eventListSorted.insert( sortIt, *eit ); } if ( sortDirection == SortDirectionAscending ) { // Append the list of Events without End DateTimes eventListSorted += tempList; } else { // Prepend the list of Events without End DateTimes tempList += eventListSorted; eventListSorted = tempList; } break; case EventSortSummary: for ( eit = eventList->begin(); eit != eventList->end(); ++eit ) { sortIt = eventListSorted.begin(); if ( sortDirection == SortDirectionAscending ) { while ( sortIt != eventListSorted.end() && (*eit)->summary() >= (*sortIt)->summary() ) { ++sortIt; } } else { while ( sortIt != eventListSorted.end() && (*eit)->summary() < (*sortIt)->summary() ) { ++sortIt; } } eventListSorted.insert( sortIt, *eit ); } break; } return eventListSorted; } Event::List Calendar::events( const QDate &date, const KDateTime::Spec &timeSpec, EventSortField sortField, SortDirection sortDirection ) { Event::List el = rawEventsForDate( date, timeSpec, sortField, sortDirection ); d->mFilter->apply( &el ); return el; } Event::List Calendar::events( const KDateTime &dt ) { Event::List el = rawEventsForDate( dt ); d->mFilter->apply( &el ); return el; } Event::List Calendar::events( const QDate &start, const QDate &end, const KDateTime::Spec &timeSpec, bool inclusive ) { Event::List el = rawEvents( start, end, timeSpec, inclusive ); d->mFilter->apply( &el ); return el; } Event::List Calendar::events( EventSortField sortField, SortDirection sortDirection ) { Event::List el = rawEvents( sortField, sortDirection ); d->mFilter->apply( &el ); return el; } bool Calendar::addIncidence( Incidence *incidence ) { Incidence::AddVisitor v( this ); return incidence->accept( v ); } bool Calendar::deleteIncidence( Incidence *incidence ) { if ( beginChange( incidence ) ) { Incidence::DeleteVisitor v( this ); bool result = incidence->accept( v ); endChange( incidence ); return result; } else { return false; } } // Dissociate a single occurrence or all future occurrences from a recurring // sequence. The new incidence is returned, but not automatically inserted // into the calendar, which is left to the calling application. Incidence *Calendar::dissociateOccurrence( Incidence *incidence, const QDate &date, const KDateTime::Spec &spec, bool single ) { if ( !incidence || !incidence->recurs() ) { return 0; } Incidence *newInc = incidence->clone(); newInc->recreate(); // Do not call setRelatedTo() when dissociating recurring to-dos, otherwise the new to-do // will appear as a child. Originally, we planned to set a relation with reltype SIBLING // when dissociating to-dos, but currently kcal only supports reltype PARENT. // We can uncomment the following line when we support the PARENT reltype. //newInc->setRelatedTo( incidence ); Recurrence *recur = newInc->recurrence(); if ( single ) { recur->clear(); } else { // Adjust the recurrence for the future incidences. In particular adjust // the "end after n occurrences" rules! "No end date" and "end by ..." // don't need to be modified. int duration = recur->duration(); if ( duration > 0 ) { int doneduration = recur->durationTo( date.addDays( -1 ) ); if ( doneduration >= duration ) { kDebug() << "The dissociated event already occurred more often" << "than it was supposed to ever occur. ERROR!"; recur->clear(); } else { recur->setDuration( duration - doneduration ); } } } // Adjust the date of the incidence if ( incidence->type() == "Event" ) { Event *ev = static_cast( newInc ); KDateTime start( ev->dtStart() ); int daysTo = start.toTimeSpec( spec ).date().daysTo( date ); ev->setDtStart( start.addDays( daysTo ) ); ev->setDtEnd( ev->dtEnd().addDays( daysTo ) ); } else if ( incidence->type() == "Todo" ) { Todo *td = static_cast( newInc ); bool haveOffset = false; int daysTo = 0; if ( td->hasDueDate() ) { KDateTime due( td->dtDue() ); daysTo = due.toTimeSpec( spec ).date().daysTo( date ); td->setDtDue( due.addDays( daysTo ), true ); haveOffset = true; } if ( td->hasStartDate() ) { KDateTime start( td->dtStart() ); if ( !haveOffset ) { daysTo = start.toTimeSpec( spec ).date().daysTo( date ); } td->setDtStart( start.addDays( daysTo ) ); haveOffset = true; } } recur = incidence->recurrence(); if ( recur ) { if ( single ) { recur->addExDate( date ); } else { // Make sure the recurrence of the past events ends // at the corresponding day recur->setEndDate( date.addDays(-1) ); } } return newInc; } Incidence *Calendar::incidence( const QString &uid ) { Incidence *i = event( uid ); if ( i ) { return i; } i = todo( uid ); if ( i ) { return i; } i = journal( uid ); return i; } Incidence::List Calendar::incidencesFromSchedulingID( const QString &sid ) { Incidence::List result; const Incidence::List incidences = rawIncidences(); Incidence::List::const_iterator it = incidences.begin(); for ( ; it != incidences.end(); ++it ) { if ( (*it)->schedulingID() == sid ) { result.append( *it ); } } return result; } Incidence *Calendar::incidenceFromSchedulingID( const QString &UID ) { const Incidence::List incidences = rawIncidences(); Incidence::List::const_iterator it = incidences.begin(); for ( ; it != incidences.end(); ++it ) { if ( (*it)->schedulingID() == UID ) { // Touchdown, and the crowd goes wild return *it; } } // Not found return 0; } Todo::List Calendar::sortTodos( Todo::List *todoList, TodoSortField sortField, SortDirection sortDirection ) { Todo::List todoListSorted; Todo::List tempList, t; Todo::List alphaList; Todo::List::Iterator sortIt; Todo::List::Iterator eit; // Notice we alphabetically presort Summaries first. // We do this so comparison "ties" stay in a nice order. // Note that To-dos may not have Start DateTimes nor due DateTimes. switch( sortField ) { case TodoSortUnsorted: todoListSorted = *todoList; break; case TodoSortStartDate: alphaList = sortTodos( todoList, TodoSortSummary, sortDirection ); for ( eit = alphaList.begin(); eit != alphaList.end(); ++eit ) { if ( (*eit)->hasStartDate() ) { sortIt = todoListSorted.begin(); if ( sortDirection == SortDirectionAscending ) { while ( sortIt != todoListSorted.end() && (*eit)->dtStart() >= (*sortIt)->dtStart() ) { ++sortIt; } } else { while ( sortIt != todoListSorted.end() && (*eit)->dtStart() < (*sortIt)->dtStart() ) { ++sortIt; } } todoListSorted.insert( sortIt, *eit ); } else { // Keep a list of the To-dos without Start DateTimes tempList.append( *eit ); } } if ( sortDirection == SortDirectionAscending ) { // Append the list of To-dos without Start DateTimes todoListSorted += tempList; } else { // Prepend the list of To-dos without Start DateTimes tempList += todoListSorted; todoListSorted = tempList; } break; case TodoSortDueDate: alphaList = sortTodos( todoList, TodoSortSummary, sortDirection ); for ( eit = alphaList.begin(); eit != alphaList.end(); ++eit ) { if ( (*eit)->hasDueDate() ) { sortIt = todoListSorted.begin(); if ( sortDirection == SortDirectionAscending ) { while ( sortIt != todoListSorted.end() && (*eit)->dtDue() >= (*sortIt)->dtDue() ) { ++sortIt; } } else { while ( sortIt != todoListSorted.end() && (*eit)->dtDue() < (*sortIt)->dtDue() ) { ++sortIt; } } todoListSorted.insert( sortIt, *eit ); } else { // Keep a list of the To-dos without Due DateTimes tempList.append( *eit ); } } if ( sortDirection == SortDirectionAscending ) { // Append the list of To-dos without Due DateTimes todoListSorted += tempList; } else { // Prepend the list of To-dos without Due DateTimes tempList += todoListSorted; todoListSorted = tempList; } break; case TodoSortPriority: alphaList = sortTodos( todoList, TodoSortSummary, sortDirection ); for ( eit = alphaList.begin(); eit != alphaList.end(); ++eit ) { sortIt = todoListSorted.begin(); if ( sortDirection == SortDirectionAscending ) { while ( sortIt != todoListSorted.end() && (*eit)->priority() >= (*sortIt)->priority() ) { ++sortIt; } } else { while ( sortIt != todoListSorted.end() && (*eit)->priority() < (*sortIt)->priority() ) { ++sortIt; } } todoListSorted.insert( sortIt, *eit ); } break; case TodoSortPercentComplete: alphaList = sortTodos( todoList, TodoSortSummary, sortDirection ); for ( eit = alphaList.begin(); eit != alphaList.end(); ++eit ) { sortIt = todoListSorted.begin(); if ( sortDirection == SortDirectionAscending ) { while ( sortIt != todoListSorted.end() && (*eit)->percentComplete() >= (*sortIt)->percentComplete() ) { ++sortIt; } } else { while ( sortIt != todoListSorted.end() && (*eit)->percentComplete() < (*sortIt)->percentComplete() ) { ++sortIt; } } todoListSorted.insert( sortIt, *eit ); } break; case TodoSortSummary: for ( eit = todoList->begin(); eit != todoList->end(); ++eit ) { sortIt = todoListSorted.begin(); if ( sortDirection == SortDirectionAscending ) { while ( sortIt != todoListSorted.end() && (*eit)->summary() >= (*sortIt)->summary() ) { ++sortIt; } } else { while ( sortIt != todoListSorted.end() && (*eit)->summary() < (*sortIt)->summary() ) { ++sortIt; } } todoListSorted.insert( sortIt, *eit ); } break; } return todoListSorted; } Todo::List Calendar::todos( TodoSortField sortField, SortDirection sortDirection ) { Todo::List tl = rawTodos( sortField, sortDirection ); d->mFilter->apply( &tl ); return tl; } Todo::List Calendar::todos( const QDate &date ) { Todo::List el = rawTodosForDate( date ); d->mFilter->apply( &el ); return el; } Journal::List Calendar::sortJournals( Journal::List *journalList, JournalSortField sortField, SortDirection sortDirection ) { Journal::List journalListSorted; Journal::List::Iterator sortIt; Journal::List::Iterator eit; switch( sortField ) { case JournalSortUnsorted: journalListSorted = *journalList; break; case JournalSortDate: for ( eit = journalList->begin(); eit != journalList->end(); ++eit ) { sortIt = journalListSorted.begin(); if ( sortDirection == SortDirectionAscending ) { while ( sortIt != journalListSorted.end() && (*eit)->dtStart() >= (*sortIt)->dtStart() ) { ++sortIt; } } else { while ( sortIt != journalListSorted.end() && (*eit)->dtStart() < (*sortIt)->dtStart() ) { ++sortIt; } } journalListSorted.insert( sortIt, *eit ); } break; case JournalSortSummary: for ( eit = journalList->begin(); eit != journalList->end(); ++eit ) { sortIt = journalListSorted.begin(); if ( sortDirection == SortDirectionAscending ) { while ( sortIt != journalListSorted.end() && (*eit)->summary() >= (*sortIt)->summary() ) { ++sortIt; } } else { while ( sortIt != journalListSorted.end() && (*eit)->summary() < (*sortIt)->summary() ) { ++sortIt; } } journalListSorted.insert( sortIt, *eit ); } break; } return journalListSorted; } Journal::List Calendar::journals( JournalSortField sortField, SortDirection sortDirection ) { Journal::List jl = rawJournals( sortField, sortDirection ); d->mFilter->apply( &jl ); return jl; } Journal::List Calendar::journals( const QDate &date ) { Journal::List el = rawJournalsForDate( date ); d->mFilter->apply( &el ); return el; } +void Calendar::beginBatchAdding() +{ + emit batchAddingBegins(); +} + +void Calendar::endBatchAdding() +{ + emit batchAddingEnds(); +} + // When this is called, the to-dos have already been added to the calendar. // This method is only about linking related to-dos. void Calendar::setupRelations( Incidence *forincidence ) { if ( !forincidence ) { return; } QString uid = forincidence->uid(); // First, go over the list of orphans and see if this is their parent QList l = d->mOrphans.values( uid ); d->mOrphans.remove( uid ); for ( int i = 0, end = l.count(); i < end; ++i ) { l[i]->setRelatedTo( forincidence ); forincidence->addRelation( l[i] ); d->mOrphanUids.remove( l[i]->uid() ); } // Now see about this incidences parent if ( !forincidence->relatedTo() && !forincidence->relatedToUid().isEmpty() ) { // Incidence has a uid it is related to but is not registered to it yet. // Try to find it Incidence *parent = incidence( forincidence->relatedToUid() ); if ( parent ) { // Found it forincidence->setRelatedTo( parent ); parent->addRelation( forincidence ); } else { // Not found, put this in the mOrphans list // Note that the mOrphans dict might contain multiple entries with the // same key! which are multiple children that wait for the parent // incidence to be inserted. d->mOrphans.insert( forincidence->relatedToUid(), forincidence ); d->mOrphanUids.insert( forincidence->uid(), forincidence ); } } } // If a to-do with sub-to-dos is deleted, move it's sub-to-dos to the orphan list void Calendar::removeRelations( Incidence *incidence ) { if ( !incidence ) { kDebug() << "Warning: incidence is 0"; return; } QString uid = incidence->uid(); foreach ( Incidence *i, incidence->relations() ) { if ( !d->mOrphanUids.contains( i->uid() ) ) { d->mOrphans.insert( uid, i ); d->mOrphanUids.insert( i->uid(), i ); i->setRelatedTo( 0 ); i->setRelatedToUid( uid ); } } // If this incidence is related to something else, tell that about it if ( incidence->relatedTo() ) { incidence->relatedTo()->removeRelation( incidence ); } // Remove this one from the orphans list if ( d->mOrphanUids.remove( uid ) ) { // This incidence is located in the orphans list - it should be removed // Since the mOrphans dict might contain the same key (with different // child incidence pointers!) multiple times, take care that we remove // the correct one. So we need to remove all items with the given // parent UID, and readd those that are not for this item. Also, there // might be other entries with differnet UID that point to this // incidence (this might happen when the relatedTo of the item is // changed before its parent is inserted. This might happen with // groupware servers....). Remove them, too QStringList relatedToUids; // First, create a list of all keys in the mOrphans list which point // to the removed item relatedToUids << incidence->relatedToUid(); for ( QMultiHash::Iterator it = d->mOrphans.begin(); it != d->mOrphans.end(); ++it ) { if ( it.value()->uid() == uid ) { relatedToUids << it.key(); } } // now go through all uids that have one entry that point to the incidence for ( QStringList::const_iterator uidit = relatedToUids.constBegin(); uidit != relatedToUids.constEnd(); ++uidit ) { Incidence::List tempList; // Remove all to get access to the remaining entries QList l = d->mOrphans.values( *uidit ); d->mOrphans.remove( *uidit ); foreach ( Incidence *i, l ) { if ( i != incidence ) { tempList.append( i ); } } // Readd those that point to a different orphan incidence for ( Incidence::List::Iterator incit = tempList.begin(); incit != tempList.end(); ++incit ) { d->mOrphans.insert( *uidit, *incit ); } } } } void Calendar::CalendarObserver::calendarModified( bool modified, Calendar *calendar ) { Q_UNUSED( modified ); Q_UNUSED( calendar ); } void Calendar::CalendarObserver::calendarIncidenceAdded( Incidence *incidence ) { Q_UNUSED( incidence ); } void Calendar::CalendarObserver::calendarIncidenceChanged( Incidence *incidence ) { Q_UNUSED( incidence ); } void Calendar::CalendarObserver::calendarIncidenceDeleted( Incidence *incidence ) { Q_UNUSED( incidence ); } void Calendar::registerObserver( CalendarObserver *observer ) { if ( !d->mObservers.contains( observer ) ) { d->mObservers.append( observer ); } d->mNewObserver = true; } void Calendar::unregisterObserver( CalendarObserver *observer ) { d->mObservers.removeAll( observer ); } bool Calendar::isSaving() { return false; } void Calendar::setModified( bool modified ) { if ( modified != d->mModified || d->mNewObserver ) { d->mNewObserver = false; foreach ( CalendarObserver *observer, d->mObservers ) { observer->calendarModified( modified, this ); } d->mModified = modified; } } bool Calendar::isModified() const { return d->mModified; } void Calendar::incidenceUpdated( IncidenceBase *incidence ) { incidence->setLastModified( KDateTime::currentUtcDateTime() ); // we should probably update the revision number here, // or internally in the Event itself when certain things change. // need to verify with ical documentation. // The static_cast is ok as the CalendarLocal only observes Incidence objects notifyIncidenceChanged( static_cast( incidence ) ); setModified( true ); } void Calendar::doSetTimeSpec( const KDateTime::Spec &timeSpec ) { Q_UNUSED( timeSpec ); } void Calendar::notifyIncidenceAdded( Incidence *i ) { if ( !d->mObserversEnabled ) { return; } foreach ( CalendarObserver *observer, d->mObservers ) { observer->calendarIncidenceAdded( i ); } } void Calendar::notifyIncidenceChanged( Incidence *i ) { if ( !d->mObserversEnabled ) { return; } foreach ( CalendarObserver *observer, d->mObservers ) { observer->calendarIncidenceChanged( i ); } } void Calendar::notifyIncidenceDeleted( Incidence *i ) { if ( !d->mObserversEnabled ) { return; } foreach ( CalendarObserver *observer, d->mObservers ) { observer->calendarIncidenceDeleted( i ); } } void Calendar::customPropertyUpdated() { setModified( true ); } void Calendar::setProductId( const QString &id ) { d->mProductId = id; } QString Calendar::productId() const { return d->mProductId; } Incidence::List Calendar::mergeIncidenceList( const Event::List &events, const Todo::List &todos, const Journal::List &journals ) { Incidence::List incidences; int i, end; for ( i = 0, end = events.count(); i < end; ++i ) { incidences.append( events[i] ); } for ( i = 0, end = todos.count(); i < end; ++i ) { incidences.append( todos[i] ); } for ( i = 0, end = journals.count(); i < end; ++i ) { incidences.append( journals[i] ); } return incidences; } bool Calendar::beginChange( Incidence *incidence ) { Q_UNUSED( incidence ); return true; } bool Calendar::endChange( Incidence *incidence ) { Q_UNUSED( incidence ); return true; } void Calendar::setObserversEnabled( bool enabled ) { d->mObserversEnabled = enabled; } void Calendar::appendAlarms( Alarm::List &alarms, Incidence *incidence, const KDateTime &from, const KDateTime &to ) { KDateTime preTime = from.addSecs(-1); Alarm::List alarmlist = incidence->alarms(); for ( int i = 0, iend = alarmlist.count(); i < iend; ++i ) { if ( alarmlist[i]->enabled() ) { KDateTime dt = alarmlist[i]->nextRepetition( preTime ); if ( dt.isValid() && dt <= to ) { kDebug() << incidence->summary() << "':" << dt.toString(); alarms.append( alarmlist[i] ); } } } } void Calendar::appendRecurringAlarms( Alarm::List &alarms, Incidence *incidence, const KDateTime &from, const KDateTime &to ) { KDateTime dt; bool endOffsetValid = false; Duration endOffset( 0 ); Duration period( from, to ); Alarm::List alarmlist = incidence->alarms(); for ( int i = 0, iend = alarmlist.count(); i < iend; ++i ) { Alarm *a = alarmlist[i]; if ( a->enabled() ) { if ( a->hasTime() ) { // The alarm time is defined as an absolute date/time dt = a->nextRepetition( from.addSecs(-1) ); if ( !dt.isValid() || dt > to ) { continue; } } else { // Alarm time is defined by an offset from the event start or end time. // Find the offset from the event start time, which is also used as the // offset from the recurrence time. Duration offset( 0 ); if ( a->hasStartOffset() ) { offset = a->startOffset(); } else if ( a->hasEndOffset() ) { offset = a->endOffset(); if ( !endOffsetValid ) { endOffset = Duration( incidence->dtStart(), incidence->dtEnd() ); endOffsetValid = true; } } // Find the incidence's earliest alarm KDateTime alarmStart = offset.end( a->hasEndOffset() ? incidence->dtEnd() : incidence->dtStart() ); // KDateTime alarmStart = incidence->dtStart().addSecs( offset ); if ( alarmStart > to ) { continue; } KDateTime baseStart = incidence->dtStart(); if ( from > alarmStart ) { alarmStart = from; // don't look earlier than the earliest alarm baseStart = (-offset).end( (-endOffset).end( alarmStart ) ); } // Adjust the 'alarmStart' date/time and find the next recurrence at or after it. // Treate the two offsets separately in case one is daily and the other not. dt = incidence->recurrence()->getNextDateTime( baseStart.addSecs(-1) ); if ( !dt.isValid() || ( dt = endOffset.end( offset.end( dt ) ) ) > to ) // adjust 'dt' to get the alarm time { // The next recurrence is too late. if ( !a->repeatCount() ) { continue; } // The alarm has repetitions, so check whether repetitions of previous // recurrences fall within the time period. bool found = false; Duration alarmDuration = a->duration(); for ( KDateTime base = baseStart; ( dt = incidence->recurrence()->getPreviousDateTime( base ) ).isValid(); base = dt ) { if ( a->duration().end( dt ) < base ) { break; // this recurrence's last repetition is too early, so give up } // The last repetition of this recurrence is at or after 'alarmStart' time. // Check if a repetition occurs between 'alarmStart' and 'to'. int snooze = a->snoozeTime().value(); // in seconds or days if ( a->snoozeTime().isDaily() ) { Duration toFromDuration( dt, base ); int toFrom = toFromDuration.asDays(); if ( a->snoozeTime().end( from ) <= to || ( toFromDuration.isDaily() && toFrom % snooze == 0 ) || ( toFrom / snooze + 1 ) * snooze <= toFrom + period.asDays() ) { found = true; #ifndef NDEBUG // for debug output dt = offset.end( dt ).addDays( ( ( toFrom - 1 ) / snooze + 1 ) * snooze ); #endif break; } } else { int toFrom = dt.secsTo( base ); if ( period.asSeconds() >= snooze || toFrom % snooze == 0 || ( toFrom / snooze + 1 ) * snooze <= toFrom + period.asSeconds() ) { found = true; #ifndef NDEBUG // for debug output dt = offset.end( dt ).addSecs( ( ( toFrom - 1 ) / snooze + 1 ) * snooze ); #endif break; } } } if ( !found ) { continue; } } } kDebug() << incidence->summary() << "':" << dt.toString(); alarms.append( a ); } } } #include "calendar.moc" diff --git a/kcal/calendar.h b/kcal/calendar.h index 125acea35..afd9e6647 100644 --- a/kcal/calendar.h +++ b/kcal/calendar.h @@ -1,1076 +1,1112 @@ /* This file is part of the kcal library. Copyright (c) 1998 Preston Brown Copyright (c) 2001,2003,2004 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (c) 2006 David Jarvie 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. */ /** @file This file is part of the API for handling calendar data and defines the Calendar class. @author Preston Brown \ @author Cornelius Schumacher \ @author Reinhold Kainhofer \ @author David Jarvie \ */ #ifndef KCAL_CALENDAR_H #define KCAL_CALENDAR_H #include #include #include #include #include #include "customproperties.h" #include "event.h" #include "todo.h" #include "journal.h" #include "kcalversion.h" namespace KCal { class ICalTimeZone; class ICalTimeZones; class CalFilter; class Person; /** Calendar Incidence sort directions. */ enum SortDirection { SortDirectionAscending, /**< Sort in ascending order (first to last) */ SortDirectionDescending /**< Sort in descending order (last to first) */ }; /** Calendar Event sort keys. */ enum EventSortField { EventSortUnsorted, /**< Do not sort Events */ EventSortStartDate, /**< Sort Events chronologically, by start date */ EventSortEndDate, /**< Sort Events chronologically, by end date */ EventSortSummary /**< Sort Events alphabetically, by summary */ }; /** Calendar Todo sort keys. */ enum TodoSortField { TodoSortUnsorted, /**< Do not sort Todos */ TodoSortStartDate, /**< Sort Todos chronologically, by start date */ TodoSortDueDate, /**< Sort Todos chronologically, by due date */ TodoSortPriority, /**< Sort Todos by priority */ TodoSortPercentComplete, /**< Sort Todos by percentage completed */ TodoSortSummary /**< Sort Todos alphabetically, by summary */ }; /** Calendar Journal sort keys. */ enum JournalSortField { JournalSortUnsorted, /**< Do not sort Journals */ JournalSortDate, /**< Sort Journals chronologically by date */ JournalSortSummary /**< Sort Journals alphabetically, by summary */ }; /** @brief Represents the main calendar class. A calendar contains information like incidences (events, to-dos, journals), alarms, time zones, and other useful information. This is an abstract base class defining the interface to a calendar. It is implemented by subclasses like CalendarLocal, which use different methods to store and access the data. Ownership of Incidences: Incidence ownership is handled by the following policy: as soon as an incidence (or any other subclass of IncidenceBase) is added to the Calendar by an add...() method it is owned by the Calendar object. The Calendar takes care of deleting the incidence using the delete...() methods. All Incidences returned by the query functions are returned as pointers so that changes to the returned Incidences are immediately visible in the Calendar. Do Not attempt to 'delete' any Incidence object you get from Calendar -- use the delete...() methods. */ class KCAL_EXPORT Calendar : public QObject, public CustomProperties, public IncidenceBase::IncidenceObserver { Q_OBJECT public: /** Constructs a calendar with a specified time zone @p timeZoneid. The time specification is used as the default for creating or modifying incidences in the Calendar. The time specification does not alter existing incidences. The constructor also calls setViewTimeSpec(@p timeSpec). @param timeSpec time specification */ explicit Calendar( const KDateTime::Spec &timeSpec ); /** Construct Calendar object using a time zone ID. The time zone ID is used as the default for creating or modifying incidences in the Calendar. The time zone does not alter existing incidences. The constructor also calls setViewTimeZoneId(@p timeZoneId). @param timeZoneId is a string containing a time zone ID, which is assumed to be valid. If no time zone is found, the viewing time specification is set to local clock time. @e Example: "Europe/Berlin" */ explicit Calendar( const QString &timeZoneId ); /** Destroys the calendar. */ virtual ~Calendar(); /** Sets the calendar Product ID to @p id. @param id is a string containing the Product ID. @see productId() const */ void setProductId( const QString &id ); /** Returns the calendar's Product ID. @see setProductId() */ QString productId() const; /** Sets the owner of the calendar to @p owner. @param owner is a Person object. @see owner() */ void setOwner( const Person &owner ); /** Returns the owner of the calendar. @return the owner Person object. @see setOwner() */ Person owner() const; /** Sets the default time specification (time zone, etc.) used for creating or modifying incidences in the Calendar. The method also calls setViewTimeSpec(@p timeSpec). @param timeSpec time specification */ void setTimeSpec( const KDateTime::Spec &timeSpec ); /** Get the time specification (time zone etc.) used for creating or modifying incidences in the Calendar. @return time specification */ KDateTime::Spec timeSpec() const; /** Sets the time zone ID used for creating or modifying incidences in the Calendar. This method has no effect on existing incidences. The method also calls setViewTimeZoneId(@p timeZoneId). @param timeZoneId is a string containing a time zone ID, which is assumed to be valid. The time zone ID is used to set the time zone for viewing Incidence date/times. If no time zone is found, the viewing time specification is set to local clock time. @e Example: "Europe/Berlin" @see setTimeSpec() */ void setTimeZoneId( const QString &timeZoneId ); /** Returns the time zone ID used for creating or modifying incidences in the calendar. @return the string containing the time zone ID, or empty string if the creation/modification time specification is not a time zone. */ QString timeZoneId() const; /** Notes the time specification which the client application intends to use for viewing the incidences in this calendar. This is simply a convenience method which makes a note of the new time zone so that it can be read back by viewTimeSpec(). The client application must convert date/time values to the desired time zone itself. The time specification is not used in any way by the Calendar or its incidences; it is solely for use by the client application. @param timeSpec time specification @see viewTimeSpec() */ void setViewTimeSpec( const KDateTime::Spec &timeSpec ) const; /** Notes the time zone Id which the client application intends to use for viewing the incidences in this calendar. This is simply a convenience method which makes a note of the new time zone so that it can be read back by viewTimeId(). The client application must convert date/time values to the desired time zone itself. The Id is not used in any way by the Calendar or its incidences. It is solely for use by the client application. @param timeZoneId is a string containing a time zone ID, which is assumed to be valid. The time zone ID is used to set the time zone for viewing Incidence date/times. If no time zone is found, the viewing time specification is set to local clock time. @e Example: "Europe/Berlin" @see viewTimeZoneId() */ void setViewTimeZoneId( const QString &timeZoneId ) const; /** Returns the time specification used for viewing the incidences in this calendar. This simply returns the time specification last set by setViewTimeSpec(). @see setViewTimeSpec(). */ KDateTime::Spec viewTimeSpec() const; /** Returns the time zone Id used for viewing the incidences in this calendar. This simply returns the time specification last set by setViewTimeSpec(). @see setViewTimeZoneId(). */ QString viewTimeZoneId() const; /** Shifts the times of all incidences so that they appear at the same clock time as before but in a new time zone. The shift is done from a viewing time zone rather than from the actual incidence time zone. For example, shifting an incidence whose start time is 09:00 America/New York, using an old viewing time zone (@p oldSpec) of Europe/London, to a new time zone (@p newSpec) of Europe/Paris, will result in the time being shifted from 14:00 (which is the London time of the incidence start) to 14:00 Paris time. @param oldSpec the time specification which provides the clock times @param newSpec the new time specification @see isLocalTime() */ void shiftTimes( const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec ); /** Returns the time zone collection used by the calendar. @return the time zones collection. @see setLocalTime() */ ICalTimeZones *timeZones() const; /** Set the time zone collection used by the calendar. @param zones time zones collection. Important: all time zones references in the calendar must be included in the collection. */ void setTimeZones( const ICalTimeZones &zones ); /** Sets if the calendar has been modified. @param modified is true if the calendar has been modified since open or last save. @see isModified() */ void setModified( bool modified ); /** Determine the calendar's modification status. @return true if the calendar has been modified since open or last save. @see setModified() */ bool isModified() const; /** Clears out the current calendar, freeing all used memory etc. */ virtual void close() = 0; /** Syncs changes in memory to persistent storage. @return true if the save was successful; false otherwise. */ virtual bool save() = 0; /** Loads the calendar contents from storage. This requires that the calendar has been previously loaded (initialized). @return true if the reload was successful; otherwise false. */ virtual bool reload() = 0; /** Determine if the calendar is currently being saved. @return true if the calendar is currently being saved; false otherwise. */ virtual bool isSaving(); /** Returns a list of all categories used by Incidences in this Calendar. @return a QStringList containing all the categories. */ QStringList categories(); // Incidence Specific Methods // /** Inserts an Incidence into the calendar. @param incidence is a pointer to the Incidence to insert. @return true if the Incidence was successfully inserted; false otherwise. @see deleteIncidence() */ virtual bool addIncidence( Incidence *incidence ); /** Removes an Incidence from the calendar. @param incidence is a pointer to the Incidence to remove. @return true if the Incidence was successfully removed; false otherwise. @see addIncidence() */ virtual bool deleteIncidence( Incidence *incidence ); /** Returns a filtered list of all Incidences for this Calendar. @return the list of all filtered Incidences. */ virtual Incidence::List incidences(); /** Returns a filtered list of all Incidences which occur on the given date. @param date request filtered Incidence list for this QDate only. @return the list of filtered Incidences occurring on the specified date. */ virtual Incidence::List incidences( const QDate &date ); /** Returns an unfiltered list of all Incidences for this Calendar. @return the list of all unfiltered Incidences. */ virtual Incidence::List rawIncidences(); /** Returns the Incidence associated with the given unique identifier. @param uid is a unique identifier string. @return a pointer to the Incidence. A null pointer is returned if no such Incidence exists. */ Incidence *incidence( const QString &uid ); /** Returns the Incidence associated with the given scheduling identifier. @param sid is a unique scheduling identifier string. @return a pointer to the Incidence. A null pointer is returned if no such Incidence exists. */ Incidence *incidenceFromSchedulingID( const QString &sid ); /** Searches all events and todos for an incidence with this scheduling identifiere. Returns a list of matching results. @param sid is a unique scheduling identifier string. */ Incidence::List incidencesFromSchedulingID( const QString &sid ); /** Create a merged list of Events, Todos, and Journals. @param events is an Event list to merge. @param todos is a Todo list to merge. @param journals is a Journal list to merge. @return a list of merged Incidences. */ static Incidence::List mergeIncidenceList( const Event::List &events, const Todo::List &todos, const Journal::List &journals ); /** Flag that a change to a Calendar Incidence is starting. @param incidence is a pointer to the Incidence that will be changing. */ virtual bool beginChange( Incidence *incidence ); /** Flag that a change to a Calendar Incidence has completed. @param incidence is a pointer to the Incidence that was changed. */ virtual bool endChange( Incidence *incidence ); /** Dissociate an Incidence from a recurring Incidence. By default, only one single Incidence for the specified @a date will be dissociated and returned. If @a single is false, then the recurrence will be split at @a date, the old Incidence will have its recurrence ending at @a date and the new Incidence will have all recurrences past the @a date. @param incidence is a pointer to a recurring Incidence. @param date is the QDate within the recurring Incidence on which the dissociation will be performed. @param spec is the spec in which the @a date is formulated. @param single is a flag meaning that a new Incidence should be created from the recurring Incidences after @a date. @return a pointer to a new recurring Incidence if @a single is false. */ Incidence *dissociateOccurrence( Incidence *incidence, const QDate &date, const KDateTime::Spec &spec, bool single = true ); // Event Specific Methods // /** Inserts an Event into the calendar. @param event is a pointer to the Event to insert. @return true if the Event was successfully inserted; false otherwise. @see deleteEvent() */ virtual bool addEvent( Event *event ) = 0; /** Removes an Event from the calendar. @param event is a pointer to the Event to remove. @return true if the Event was successfully remove; false otherwise. @see addEvent(), deleteAllEvents() */ virtual bool deleteEvent( Event *event ) = 0; /** Removes all Events from the calendar. @see deleteEvent() */ virtual void deleteAllEvents() = 0; /** Sort a list of Events. @param eventList is a pointer to a list of Events. @param sortField specifies the EventSortField. @param sortDirection specifies the SortDirection. @return a list of Events sorted as specified. */ static Event::List sortEvents( Event::List *eventList, EventSortField sortField, SortDirection sortDirection ); /** Returns a sorted, filtered list of all Events for this Calendar. @param sortField specifies the EventSortField. @param sortDirection specifies the SortDirection. @return the list of all filtered Events sorted as specified. */ virtual Event::List events( EventSortField sortField = EventSortUnsorted, SortDirection sortDirection = SortDirectionAscending ); /** Returns a filtered list of all Events which occur on the given timestamp. @param dt request filtered Event list for this KDateTime only. @return the list of filtered Events occurring on the specified timestamp. */ Event::List events( const KDateTime &dt ); /** Returns a filtered list of all Events occurring within a date range. @param start is the starting date. @param end is the ending date. @param timeSpec time zone etc. to interpret @p start and @p end, or the calendar's default time spec if none is specified @param inclusive if true only Events which are completely included within the date range are returned. @return the list of filtered Events occurring within the specified date range. */ Event::List events( const QDate &start, const QDate &end, const KDateTime::Spec &timeSpec = KDateTime::Spec(), bool inclusive = false ); /** Returns a sorted, filtered list of all Events which occur on the given date. The Events are sorted according to @a sortField and @a sortDirection. @param date request filtered Event list for this QDate only. @param timeSpec time zone etc. to interpret @p start and @p end, or the calendar's default time spec if none is specified @param sortField specifies the EventSortField. @param sortDirection specifies the SortDirection. @return the list of sorted, filtered Events occurring on @a date. */ Event::List events( const QDate &date, const KDateTime::Spec &timeSpec = KDateTime::Spec(), EventSortField sortField = EventSortUnsorted, SortDirection sortDirection = SortDirectionAscending ); /** Returns a sorted, unfiltered list of all Events for this Calendar. @param sortField specifies the EventSortField. @param sortDirection specifies the SortDirection. @return the list of all unfiltered Events sorted as specified. */ virtual Event::List rawEvents( EventSortField sortField = EventSortUnsorted, SortDirection sortDirection = SortDirectionAscending ) = 0; /** Returns an unfiltered list of all Events which occur on the given timestamp. @param dt request unfiltered Event list for this KDateTime only. @return the list of unfiltered Events occurring on the specified timestamp. */ virtual Event::List rawEventsForDate( const KDateTime &dt ) = 0; /** Returns an unfiltered list of all Events occurring within a date range. @param start is the starting date @param end is the ending date @param timeSpec time zone etc. to interpret @p start and @p end, or the calendar's default time spec if none is specified @param inclusive if true only Events which are completely included within the date range are returned. @return the list of unfiltered Events occurring within the specified date range. */ virtual Event::List rawEvents( const QDate &start, const QDate &end, const KDateTime::Spec &timeSpec = KDateTime::Spec(), bool inclusive = false ) = 0; /** Returns a sorted, unfiltered list of all Events which occur on the given date. The Events are sorted according to @a sortField and @a sortDirection. @param date request unfiltered Event list for this QDate only @param timeSpec time zone etc. to interpret @p date, or the calendar's default time spec if none is specified @param sortField specifies the EventSortField @param sortDirection specifies the SortDirection @return the list of sorted, unfiltered Events occurring on @p date */ virtual Event::List rawEventsForDate( const QDate &date, const KDateTime::Spec &timeSpec = KDateTime::Spec(), EventSortField sortField = EventSortUnsorted, SortDirection sortDirection = SortDirectionAscending ) = 0; /** Returns the Event associated with the given unique identifier. @param uid is a unique identifier string. @return a pointer to the Event. A null pointer is returned if no such Event exists. */ virtual Event *event( const QString &uid ) = 0; // Todo Specific Methods // /** Inserts a Todo into the calendar. @param todo is a pointer to the Todo to insert. @return true if the Todo was successfully inserted; false otherwise. @see deleteTodo() */ virtual bool addTodo( Todo *todo ) = 0; /** Removes a Todo from the calendar. @param todo is a pointer to the Todo to remove. @return true if the Todo was successfully removed; false otherwise. @see addTodo(), deleteAllTodos() */ virtual bool deleteTodo( Todo *todo ) = 0; /** Removes all To-dos from the calendar. @see deleteTodo() */ virtual void deleteAllTodos() = 0; /** Sort a list of Todos. @param todoList is a pointer to a list of Todos. @param sortField specifies the TodoSortField. @param sortDirection specifies the SortDirection. @return a list of Todos sorted as specified. */ static Todo::List sortTodos( Todo::List *todoList, TodoSortField sortField, SortDirection sortDirection ); /** Returns a sorted, filtered list of all Todos for this Calendar. @param sortField specifies the TodoSortField. @param sortDirection specifies the SortDirection. @return the list of all filtered Todos sorted as specified. */ virtual Todo::List todos( TodoSortField sortField = TodoSortUnsorted, SortDirection sortDirection = SortDirectionAscending ); /** Returns a filtered list of all Todos which are due on the specified date. @param date request filtered Todos due on this QDate. @return the list of filtered Todos due on the specified date. */ virtual Todo::List todos( const QDate &date ); /** Returns a sorted, unfiltered list of all Todos for this Calendar. @param sortField specifies the TodoSortField. @param sortDirection specifies the SortDirection. @return the list of all unfiltered Todos sorted as specified. */ virtual Todo::List rawTodos( TodoSortField sortField = TodoSortUnsorted, SortDirection sortDirection = SortDirectionAscending ) = 0; /** Returns an unfiltered list of all Todos which due on the specified date. @param date request unfiltered Todos due on this QDate. @return the list of unfiltered Todos due on the specified date. */ virtual Todo::List rawTodosForDate( const QDate &date ) = 0; /** Returns the Todo associated with the given unique identifier. @param uid is a unique identifier string. @return a pointer to the Todo. A null pointer is returned if no such Todo exists. */ virtual Todo *todo( const QString &uid ) = 0; // Journal Specific Methods // /** Inserts a Journal into the calendar. @param journal is a pointer to the Journal to insert. @return true if the Journal was successfully inserted; false otherwise. @see deleteJournal() */ virtual bool addJournal( Journal *journal ) = 0; /** Removes a Journal from the calendar. @param journal is a pointer to the Journal to remove. @return true if the Journal was successfully removed; false otherwise. @see addJournal(), deleteAllJournals() */ virtual bool deleteJournal( Journal *journal ) = 0; /** Removes all Journals from the calendar. @see deleteJournal() */ virtual void deleteAllJournals() = 0; /** Sort a list of Journals. @param journalList is a pointer to a list of Journals. @param sortField specifies the JournalSortField. @param sortDirection specifies the SortDirection. @return a list of Journals sorted as specified. */ static Journal::List sortJournals( Journal::List *journalList, JournalSortField sortField, SortDirection sortDirection ); /** Returns a sorted, filtered list of all Journals for this Calendar. @param sortField specifies the JournalSortField. @param sortDirection specifies the SortDirection. @return the list of all filtered Journals sorted as specified. */ virtual Journal::List journals( JournalSortField sortField = JournalSortUnsorted, SortDirection sortDirection = SortDirectionAscending ); /** Returns a filtered list of all Journals for on the specified date. @param date request filtered Journals for this QDate only. @return the list of filtered Journals for the specified date. */ virtual Journal::List journals( const QDate &date ); /** Returns a sorted, unfiltered list of all Journals for this Calendar. @param sortField specifies the JournalSortField. @param sortDirection specifies the SortDirection. @return the list of all unfiltered Journals sorted as specified. */ virtual Journal::List rawJournals( JournalSortField sortField = JournalSortUnsorted, SortDirection sortDirection = SortDirectionAscending ) = 0; /** Returns an unfiltered list of all Journals for on the specified date. @param date request unfiltered Journals for this QDate only. @return the list of unfiltered Journals for the specified date. */ virtual Journal::List rawJournalsForDate( const QDate &date ) = 0; /** Returns the Journal associated with the given unique identifier. @param uid is a unique identifier string. @return a pointer to the Journal. A null pointer is returned if no such Journal exists. */ virtual Journal *journal( const QString &uid ) = 0; + /** + Emits the beginBatchAdding() signal. + + This should be called before adding a batch of incidences with + addIncidence( Incidence *), addTodo( Todo *), addEvent( Event *) + or addJournal( Journal *). Some Calendars are connected to this + signal, e.g: CalendarResources uses it to know a series of + incidenceAdds are related so the user isn't prompted multiple + times which resource to save the incidence to + + @since 4.4 + */ + void beginBatchAdding(); + + /** + Emits the endBatchAdding() signal. + + Used with beginBatchAdding(). Should be called after + adding all incidences. + + @since 4.4 + */ + void endBatchAdding(); + // Relations Specific Methods // /** Setup Relations for an Incidence. @param incidence is a pointer to the Incidence to have a Relation setup. */ virtual void setupRelations( Incidence *incidence ); /** Removes all Relations from an Incidence. @param incidence is a pointer to the Incidence to have a Relation removed. */ virtual void removeRelations( Incidence *incidence ); // Filter Specific Methods // /** Sets the calendar filter. @param filter a pointer to a CalFilter object which will be used to filter Calendar Incidences. @see filter() */ void setFilter( CalFilter *filter ); /** Returns the calendar filter. @return a pointer to the calendar CalFilter. A null pointer is returned if no such CalFilter exists. @see setFilter() */ CalFilter *filter(); // Alarm Specific Methods // /** Returns a list of Alarms within a time range for this Calendar. @param from is the starting timestamp. @param to is the ending timestamp. @return the list of Alarms for the for the specified time range. */ virtual Alarm::List alarms( const KDateTime &from, const KDateTime &to ) = 0; // Observer Specific Methods // /** @class CalendarObserver The CalendarObserver class. */ class KCAL_EXPORT CalendarObserver //krazy:exclude=dpointer { public: /** Destructor. */ virtual ~CalendarObserver() {} /** Notify the Observer that a Calendar has been modified. @param modified set if the calendar has been modified. @param calendar is a pointer to the Calendar object that is being observed. */ virtual void calendarModified( bool modified, Calendar *calendar ); /** Notify the Observer that an Incidence has been inserted. @param incidence is a pointer to the Incidence that was inserted. */ virtual void calendarIncidenceAdded( Incidence *incidence ); /** Notify the Observer that an Incidence has been modified. @param incidence is a pointer to the Incidence that was modified. */ virtual void calendarIncidenceChanged( Incidence *incidence ); /** Notify the Observer that an Incidence has been removed. @param incidence is a pointer to the Incidence that was removed. */ virtual void calendarIncidenceDeleted( Incidence *incidence ); }; /** Registers an Observer for this Calendar. @param observer is a pointer to an Observer object that will be watching this Calendar. @see unregisterObserver() */ void registerObserver( CalendarObserver *observer ); /** Unregisters an Observer for this Calendar. @param observer is a pointer to an Observer object that has been watching this Calendar. @see registerObserver() */ void unregisterObserver( CalendarObserver *observer ); using QObject::event; // prevent warning about hidden virtual method Q_SIGNALS: /** Signals that the calendar has been modified. */ void calendarChanged(); /** Signals that the calendar has been saved. */ void calendarSaved(); /** Signals that the calendar has been loaded into memory. */ void calendarLoaded(); + /** + @see beginBatchAdding() + @since 4.4 + */ + void batchAddingBegins(); + + /** + @see endBatchAdding() + @since 4.4 + */ + void batchAddingEnds(); + protected: /** The Observer interface. So far not implemented. @param incidenceBase is a pointer an IncidenceBase object. */ void incidenceUpdated( IncidenceBase *incidenceBase ); /** Let Calendar subclasses set the time specification. @param timeSpec is the time specification (time zone, etc.) for viewing Incidence dates.\n */ virtual void doSetTimeSpec( const KDateTime::Spec &timeSpec ); /** Let Calendar subclasses notify that they inserted an Incidence. @param incidence is a pointer to the Incidence object that was inserted. */ void notifyIncidenceAdded( Incidence *incidence ); /** Let Calendar subclasses notify that they modified an Incidence. @param incidence is a pointer to the Incidence object that was modified. */ void notifyIncidenceChanged( Incidence *incidence ); /** Let Calendar subclasses notify that they removed an Incidence. @param incidence is a pointer to the Incidence object that was removed. */ void notifyIncidenceDeleted( Incidence *incidence ); /** @copydoc CustomProperties::customPropertyUpdated() */ virtual void customPropertyUpdated(); /** Let Calendar subclasses notify that they enabled an Observer. @param enabled if true tells the calendar that a subclass has enabled an Observer. */ void setObserversEnabled( bool enabled ); /** Appends alarms of incidence in interval to list of alarms. @param alarms is a List of Alarms to be appended onto. @param incidence is a pointer to an Incidence containing the Alarm to be appended. @param from is the lower range of the next Alarm repitition. @param to is the upper range of the next Alarm repitition. */ void appendAlarms( Alarm::List &alarms, Incidence *incidence, const KDateTime &from, const KDateTime &to ); /** Appends alarms of recurring events in interval to list of alarms. @param alarms is a List of Alarms to be appended onto. @param incidence is a pointer to an Incidence containing the Alarm to be appended. @param from is the lower range of the next Alarm repitition. @param to is the upper range of the next Alarm repitition. */ void appendRecurringAlarms( Alarm::List &alarms, Incidence *incidence, const KDateTime &from, const KDateTime &to ); private: //@cond PRIVATE class Private; Private *const d; //@endcond Q_DISABLE_COPY( Calendar ) }; } #endif diff --git a/kcal/calendarresources.cpp b/kcal/calendarresources.cpp index 1e5253293..59bfb8425 100644 --- a/kcal/calendarresources.cpp +++ b/kcal/calendarresources.cpp @@ -1,954 +1,983 @@ /* This file is part of the kcal library. Copyright (c) 2003 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer 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. */ /** @file This file is part of the API for handling calendar data and defines the CalendarResources class. @brief This class provides a Calendar which is composed of other Calendars known as "Resources". @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "calendarresources.moc" #include "incidence.h" #include "journal.h" #include "resourcecalendar.h" #include "kresources/manager.h" #include "kresources/selectdialog.h" #include "kabc/lock.h" #include #include #include #include #include #include #include using namespace KCal; /** Private classes that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KCal::CalendarResources::Private { public: Private( const QString &family ) - : mManager( new CalendarResourceManager( family ) ), + : mAddingInProgress( false ), + mLastUsedResource( 0 ), + mManager( new CalendarResourceManager( family ) ), mStandardPolicy( new StandardDestinationPolicy( mManager ) ), mDestinationPolicy( mStandardPolicy ), mAskPolicy( new AskDestinationPolicy( mManager ) ), mException( 0 ), mPendingDeleteFromResourceMap( false ) {} ~Private() { delete mManager; delete mStandardPolicy; delete mAskPolicy; } + bool mAddingInProgress; + ResourceCalendar *mLastUsedResource; + bool mOpen; //flag that indicates if the resources are "open" KRES::Manager* mManager; QMap mResourceMap; StandardDestinationPolicy *mStandardPolicy; DestinationPolicy *mDestinationPolicy; AskDestinationPolicy *mAskPolicy; QMap mTickets; QMap mChangeCounts; ErrorFormat *mException; bool mPendingDeleteFromResourceMap; template< class IncidenceList > void appendIncidences( IncidenceList &result, const IncidenceList &extra, ResourceCalendar * ); }; class KCal::CalendarResources::DestinationPolicy::Private { public: Private( CalendarResourceManager *manager, QWidget *parent ) : mManager( manager ), mParent( parent ) {} CalendarResourceManager *mManager; QWidget *mParent; }; class KCal::CalendarResources::StandardDestinationPolicy::Private { public: Private() {} }; class KCal::CalendarResources::AskDestinationPolicy::Private { public: Private() {} }; class KCal::CalendarResources::Ticket::Private { public: Private( ResourceCalendar *resource ) : mResource( resource ) {} ResourceCalendar *mResource; }; //@endcond CalendarResources::DestinationPolicy::DestinationPolicy( CalendarResourceManager *manager, QWidget *parent ) : d( new KCal::CalendarResources::DestinationPolicy::Private( manager, parent ) ) { } CalendarResources::DestinationPolicy::~DestinationPolicy() { delete d; } QWidget *CalendarResources::DestinationPolicy::parent() { return d->mParent; } void CalendarResources::DestinationPolicy::setParent( QWidget *parent ) { d->mParent = parent; } CalendarResourceManager *CalendarResources::DestinationPolicy::resourceManager() { return d->mManager; } bool CalendarResources::DestinationPolicy::hasCalendarResources() { CalendarResourceManager::ActiveIterator it; for ( it = resourceManager()->activeBegin(); it != resourceManager()->activeEnd(); ++it ) { if ( !(*it)->readOnly() ) { if ( resourceManager()->standardResource() == *it ) { return true; } else { return true; } } } return false; } CalendarResources::StandardDestinationPolicy::StandardDestinationPolicy( CalendarResourceManager *manager, QWidget *parent ) : DestinationPolicy( manager, parent ), d( new KCal::CalendarResources::StandardDestinationPolicy::Private ) { } CalendarResources::StandardDestinationPolicy::~StandardDestinationPolicy() { delete d; } ResourceCalendar *CalendarResources::StandardDestinationPolicy::destination( Incidence *incidence ) { Q_UNUSED( incidence ); return resourceManager()->standardResource(); } CalendarResources::AskDestinationPolicy::AskDestinationPolicy( CalendarResourceManager *manager, QWidget *parent ) : DestinationPolicy( manager, parent ), d( new KCal::CalendarResources::AskDestinationPolicy::Private ) { } CalendarResources::AskDestinationPolicy::~AskDestinationPolicy() { delete d; } ResourceCalendar *CalendarResources::AskDestinationPolicy::destination( Incidence *incidence ) { Q_UNUSED( incidence ); QList list; CalendarResourceManager::ActiveIterator it; for ( it = resourceManager()->activeBegin(); it != resourceManager()->activeEnd(); ++it ) { if ( !(*it)->readOnly() ) { //Insert the first the Standard resource to get be the default selected. if ( resourceManager()->standardResource() == *it ) { list.insert( 0, *it ); } else { list.append( *it ); } } } KRES::Resource *r; r = KRES::SelectDialog::getResource( list, parent() ); return static_cast( r ); } CalendarResources::CalendarResources( const KDateTime::Spec &timeSpec, const QString &family ) : Calendar( timeSpec ), d( new KCal::CalendarResources::Private( family ) ) { + + connect( this, SIGNAL(batchAddingBegins()), this, SLOT(beginAddingIncidences()) ); + connect( this, SIGNAL(batchAddingEnds()), this, SLOT(endAddingIncidences()) ); + d->mManager->addObserver( this ); } CalendarResources::CalendarResources( const QString &timeZoneId, const QString &family ) : Calendar( timeZoneId ), d( new KCal::CalendarResources::Private( family ) ) { + connect( this, SIGNAL(batchAddingBegins()), this, SLOT(beginAddingIncidences()) ); + connect( this, SIGNAL(batchAddingEnds()), this, SLOT(endAddingIncidences()) ); + d->mManager->addObserver( this ); } CalendarResources::~CalendarResources() { close(); clearException(); delete d; } void CalendarResources::clearException() { delete d->mException; d->mException = 0; } ErrorFormat *CalendarResources::exception() { return d->mException; } void CalendarResources::readConfig( KConfig *config ) { d->mManager->readConfig( config ); CalendarResourceManager::Iterator it; for ( it = d->mManager->begin(); it != d->mManager->end(); ++it ) { connectResource( *it ); } } void CalendarResources::load() { if ( !d->mManager->standardResource() ) { kDebug() << "Warning! No standard resource yet."; } // set the timezone for all resources. Otherwise we'll have those terrible tz // troubles ;-(( CalendarResourceManager::Iterator i1; for ( i1 = d->mManager->begin(); i1 != d->mManager->end(); ++i1 ) { (*i1)->setTimeSpec( timeSpec() ); } QList failed; // Open all active resources CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { if ( !(*it)->load() ) { failed.append( *it ); } Incidence::List incidences = (*it)->rawIncidences(); Incidence::List::Iterator incit; for ( incit = incidences.begin(); incit != incidences.end(); ++incit ) { (*incit)->registerObserver( this ); notifyIncidenceAdded( *incit ); } } QList::ConstIterator it2; for ( it2 = failed.constBegin(); it2 != failed.constEnd(); ++it2 ) { (*it2)->setActive( false ); emit signalResourceModified( *it2 ); } d->mOpen = true; emit calendarLoaded(); } bool CalendarResources::reload() { save(); close(); load(); return true; } CalendarResourceManager *CalendarResources::resourceManager() const { return d->mManager; } void CalendarResources::setStandardDestinationPolicy() { d->mDestinationPolicy = d->mStandardPolicy; } void CalendarResources::setAskDestinationPolicy() { d->mDestinationPolicy = d->mAskPolicy; } QWidget *CalendarResources::dialogParentWidget() { return d->mDestinationPolicy->parent(); } void CalendarResources::setDialogParentWidget( QWidget *parent ) { d->mDestinationPolicy->setParent( parent ); } void CalendarResources::close() { if ( d->mOpen ) { CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { (*it)->close(); } setModified( false ); d->mOpen = false; } } bool CalendarResources::save() { bool status = true; if ( d->mOpen && isModified() ) { status = false; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { status = (*it)->save() || status; } setModified( false ); } return status; } bool CalendarResources::isSaving() { CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { if ( (*it)->isSaving() ) { return true; } } return false; } bool CalendarResources::addIncidence( Incidence *incidence, ResourceCalendar *resource ) { // FIXME: Use proper locking via begin/endChange! bool validRes = false; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { if ( (*it) == resource ) { validRes = true; } } ResourceCalendar *oldResource = 0; if ( d->mResourceMap.contains( incidence ) ) { oldResource = d->mResourceMap[incidence]; } d->mResourceMap[incidence] = resource; if ( validRes && beginChange( incidence ) && resource->addIncidence( incidence ) ) { // d->mResourceMap[incidence] = resource; incidence->registerObserver( this ); notifyIncidenceAdded( incidence ); setModified( true ); endChange( incidence ); return true; } else { if ( oldResource ) { d->mResourceMap[incidence] = oldResource; } else { d->mResourceMap.remove( incidence ); } } return false; } bool CalendarResources::hasCalendarResources() { return d->mDestinationPolicy->hasCalendarResources(); } bool CalendarResources::addIncidence( Incidence *incidence ) { clearException(); - ResourceCalendar *resource = d->mDestinationPolicy->destination( incidence ); + + ResourceCalendar *resource = d->mLastUsedResource; + + if ( !d->mAddingInProgress || d->mLastUsedResource == 0 ) { + resource = d->mDestinationPolicy->destination( incidence ); + d->mLastUsedResource = resource; + } if ( resource ) { d->mResourceMap[ incidence ] = resource; if ( beginChange( incidence ) && resource->addIncidence( incidence ) ) { incidence->registerObserver( this ); notifyIncidenceAdded( incidence ); d->mResourceMap[ incidence ] = resource; setModified( true ); endChange( incidence ); return true; } else { d->mResourceMap.remove( incidence ); } } else { d->mException = new ErrorFormat( ErrorFormat::UserCancel ); } return false; } bool CalendarResources::addEvent( Event *event ) { return addIncidence( event ); } bool CalendarResources::addEvent( Event *Event, ResourceCalendar *resource ) { return addIncidence( Event, resource ); } bool CalendarResources::deleteEvent( Event *event ) { bool status; if ( d->mResourceMap.find( event ) != d->mResourceMap.end() ) { status = d->mResourceMap[event]->deleteEvent( event ); if ( status ) { d->mPendingDeleteFromResourceMap = true; } } else { status = false; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { status = (*it)->deleteEvent( event ) || status; } } if ( status ) { notifyIncidenceDeleted( event ); } setModified( status ); return status; } void CalendarResources::deleteAllEvents() { CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { (*it)->deleteAllEvents(); } } Event *CalendarResources::event( const QString &uid ) { CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { Event *event = (*it)->event( uid ); if ( event ) { d->mResourceMap[event] = *it; return event; } } // Not found return 0; } bool CalendarResources::addTodo( Todo *todo ) { return addIncidence( todo ); } bool CalendarResources::addTodo( Todo *todo, ResourceCalendar *resource ) { return addIncidence( todo, resource ); } bool CalendarResources::deleteTodo( Todo *todo ) { bool status; if ( d->mResourceMap.find( todo ) != d->mResourceMap.end() ) { status = d->mResourceMap[todo]->deleteTodo( todo ); if ( status ) { d->mPendingDeleteFromResourceMap = true; } } else { CalendarResourceManager::ActiveIterator it; status = false; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { status = (*it)->deleteTodo( todo ) || status; } } setModified( status ); return status; } void CalendarResources::deleteAllTodos() { CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { (*it)->deleteAllTodos(); } } Todo::List CalendarResources::rawTodos( TodoSortField sortField, SortDirection sortDirection ) { Todo::List result; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { d->appendIncidences( result, (*it)->rawTodos( TodoSortUnsorted ), *it ); } return sortTodos( &result, sortField, sortDirection ); } Todo *CalendarResources::todo( const QString &uid ) { CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { Todo *todo = (*it)->todo( uid ); if ( todo ) { d->mResourceMap[todo] = *it; return todo; } } // Not found return 0; } Todo::List CalendarResources::rawTodosForDate( const QDate &date ) { Todo::List result; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { d->appendIncidences( result, (*it)->rawTodosForDate( date ), *it ); } return result; } Alarm::List CalendarResources::alarmsTo( const KDateTime &to ) { Alarm::List result; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { result += (*it)->alarmsTo( to ); } return result; } Alarm::List CalendarResources::alarms( const KDateTime &from, const KDateTime &to ) { Alarm::List result; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { result += (*it)->alarms( from, to ); } return result; } /****************************** PROTECTED METHODS ****************************/ Event::List CalendarResources::rawEventsForDate( const QDate &date, const KDateTime::Spec &timeSpec, EventSortField sortField, SortDirection sortDirection ) { Event::List result; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { d->appendIncidences( result, (*it)->rawEventsForDate( date, timeSpec ), *it ); } return sortEvents( &result, sortField, sortDirection ); } Event::List CalendarResources::rawEvents( const QDate &start, const QDate &end, const KDateTime::Spec &timeSpec, bool inclusive ) { Event::List result; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { d->appendIncidences( result, (*it)->rawEvents( start, end, timeSpec, inclusive ), *it ); } return result; } Event::List CalendarResources::rawEventsForDate( const KDateTime &kdt ) { // @TODO: Remove the code duplication by the resourcemap iteration block. Event::List result; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { d->appendIncidences( result, (*it)->rawEventsForDate( kdt ), *it ); } return result; } Event::List CalendarResources::rawEvents( EventSortField sortField, SortDirection sortDirection ) { Event::List result; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { d->appendIncidences( result, (*it)->rawEvents( EventSortUnsorted ), *it ); } return sortEvents( &result, sortField, sortDirection ); } bool CalendarResources::addJournal( Journal *journal ) { return addIncidence( journal ); } bool CalendarResources::deleteJournal( Journal *journal ) { bool status; if ( d->mResourceMap.find( journal ) != d->mResourceMap.end() ) { status = d->mResourceMap[journal]->deleteJournal( journal ); if ( status ) { d->mPendingDeleteFromResourceMap = true; } } else { CalendarResourceManager::ActiveIterator it; status = false; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { status = (*it)->deleteJournal( journal ) || status; } } setModified( status ); return status; } void CalendarResources::deleteAllJournals() { CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { (*it)->deleteAllJournals(); } } bool CalendarResources::addJournal( Journal *journal, ResourceCalendar *resource ) { return addIncidence( journal, resource ); } Journal *CalendarResources::journal( const QString &uid ) { CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { Journal *journal = (*it)->journal( uid ); if ( journal ) { d->mResourceMap[journal] = *it; return journal; } } // Not found return 0; } Journal::List CalendarResources::rawJournals( JournalSortField sortField, SortDirection sortDirection ) { Journal::List result; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { d->appendIncidences( result, (*it)->rawJournals( JournalSortUnsorted ), *it ); } return sortJournals( &result, sortField, sortDirection ); } Journal::List CalendarResources::rawJournalsForDate( const QDate &date ) { Journal::List result; CalendarResourceManager::ActiveIterator it; for ( it = d->mManager->activeBegin(); it != d->mManager->activeEnd(); ++it ) { d->appendIncidences( result, (*it)->rawJournalsForDate( date ), *it ); } return result; } //@cond PRIVATE template< class IncidenceList > void CalendarResources::Private::appendIncidences( IncidenceList &result, const IncidenceList &extra, ResourceCalendar *resource ) { result += extra; for ( typename IncidenceList::ConstIterator it = extra.begin(); it != extra.end(); ++it ) { mResourceMap[ *it ] = resource; } } //@endcond void CalendarResources::connectResource( ResourceCalendar *resource ) { connect( resource, SIGNAL( resourceChanged( ResourceCalendar * ) ), SIGNAL( calendarChanged() ) ); connect( resource, SIGNAL( resourceSaved( ResourceCalendar * ) ), SIGNAL( calendarSaved() ) ); connect( resource, SIGNAL( resourceLoadError( ResourceCalendar *, const QString & ) ), SLOT( slotLoadError( ResourceCalendar *, const QString & ) ) ); connect( resource, SIGNAL( resourceSaveError( ResourceCalendar *, const QString & ) ), SLOT( slotSaveError( ResourceCalendar *, const QString & ) ) ); } ResourceCalendar *CalendarResources::resource( Incidence *incidence ) { if ( d->mResourceMap.find( incidence ) != d->mResourceMap.end() ) { return d->mResourceMap[ incidence ]; } return 0; } void CalendarResources::resourceAdded( ResourceCalendar *resource ) { if ( !resource->isActive() ) { return; } if ( resource->open() ) { resource->load(); } connectResource( resource ); emit signalResourceAdded( resource ); } void CalendarResources::resourceModified( ResourceCalendar *resource ) { emit signalResourceModified( resource ); } void CalendarResources::resourceDeleted( ResourceCalendar *resource ) { emit signalResourceDeleted( resource ); } void CalendarResources::doSetTimeSpec( const KDateTime::Spec &timeSpec ) { // set the timezone for all resources. Otherwise we'll have those terrible // tz troubles ;-(( CalendarResourceManager::Iterator i1; for ( i1 = d->mManager->begin(); i1 != d->mManager->end(); ++i1 ) { (*i1)->setTimeSpec( timeSpec ); } } CalendarResources::Ticket::Ticket( ResourceCalendar *resource ) : d( new KCal::CalendarResources::Ticket::Private( resource ) ) { } CalendarResources::Ticket::~Ticket() { delete d; } CalendarResources::Ticket *CalendarResources::requestSaveTicket( ResourceCalendar *resource ) { KABC::Lock *lock = resource->lock(); if ( !lock ) { return 0; } if ( lock->lock() ) { return new Ticket( resource ); } else { return 0; } } ResourceCalendar *CalendarResources::Ticket::resource() const { return d->mResource; } bool CalendarResources::save( Ticket *ticket, Incidence *incidence ) { if ( !ticket || !ticket->resource() ) { return false; } // @TODO: Check if the resource was changed at all. If not, don't save. if ( ticket->resource()->save( incidence ) ) { releaseSaveTicket( ticket ); return true; } return false; } void CalendarResources::releaseSaveTicket( Ticket *ticket ) { ticket->resource()->lock()->unlock(); delete ticket; } bool CalendarResources::beginChange( Incidence *incidence ) { ResourceCalendar *r = resource( incidence ); if ( !r ) { r = d->mDestinationPolicy->destination( incidence ); if ( !r ) { kError() << "Unable to get destination resource."; return false; } d->mResourceMap[ incidence ] = r; } d->mPendingDeleteFromResourceMap = false; int count = incrementChangeCount( r ); if ( count == 1 ) { Ticket *ticket = requestSaveTicket( r ); if ( !ticket ) { kDebug() << "unable to get ticket."; decrementChangeCount( r ); return false; } else { d->mTickets[ r ] = ticket; } } return true; } bool CalendarResources::endChange( Incidence *incidence ) { ResourceCalendar *r = resource( incidence ); if ( !r ) { return false; } int count = decrementChangeCount( r ); if ( d->mPendingDeleteFromResourceMap ) { d->mResourceMap.remove( incidence ); d->mPendingDeleteFromResourceMap = false; } if ( count == 0 ) { bool ok = save( d->mTickets[ r ], incidence ); if ( ok ) { d->mTickets.remove( r ); } else { return false; } } return true; } +void CalendarResources::beginAddingIncidences() +{ + d->mAddingInProgress = true; +} + +void CalendarResources::endAddingIncidences() +{ + d->mAddingInProgress = false; + d->mLastUsedResource = 0; +} + int CalendarResources::incrementChangeCount( ResourceCalendar *r ) { if ( !d->mChangeCounts.contains( r ) ) { d->mChangeCounts.insert( r, 0 ); } int count = d->mChangeCounts[ r ]; ++count; d->mChangeCounts[ r ] = count; return count; } int CalendarResources::decrementChangeCount( ResourceCalendar *r ) { if ( !d->mChangeCounts.contains( r ) ) { kError() << "No change count for resource."; return 0; } int count = d->mChangeCounts[ r ]; --count; if ( count < 0 ) { kError() << "Can't decrement change count. It already is 0."; count = 0; } d->mChangeCounts[ r ] = count; return count; } void CalendarResources::slotLoadError( ResourceCalendar *r, const QString &err ) { Q_UNUSED( r ); emit signalErrorMessage( err ); } void CalendarResources::slotSaveError( ResourceCalendar *r, const QString &err ) { Q_UNUSED( r ); emit signalErrorMessage( err ); } diff --git a/kcal/calendarresources.h b/kcal/calendarresources.h index 7bf286183..ef4a64f53 100644 --- a/kcal/calendarresources.h +++ b/kcal/calendarresources.h @@ -1,745 +1,759 @@ /* This file is part of the kcal library. Copyright (c) 2003 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer 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. */ /** @file This file is part of the API for handling calendar data and defines the CalendarResources class. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #ifndef KCAL_CALENDARRESOURCES_H #define KCAL_CALENDARRESOURCES_H #include #include "calendar.h" #include "exceptions.h" #include "resourcecalendar.h" #include "kcal_export.h" class QWidget; namespace KCal { /** @brief This class provides a Calendar which is composed of other Calendars known as "Resources". Examples of Calendar Resources are: - Calendars stored as local ICS formatted files - a set of incidences (one-per-file) within a local directory - birthdays and anniversaries contained in an addressbook */ class KCAL_EXPORT CalendarResources : public Calendar, public KRES::ManagerObserver { Q_OBJECT public: /** @class DestinationPolicy */ class DestinationPolicy { public: /** Constructs a destination policy. @param manager is a pointer to the CalendarResourceManager. @param parent is a pointer to a QWidget to use for new dialogs. */ explicit DestinationPolicy( CalendarResourceManager *manager, QWidget *parent = 0 ); /** Destructor. */ virtual ~DestinationPolicy(); /** Returns parent widget to use for new dialogs. */ virtual QWidget *parent(); /** Sets the parent widget for new dialogs. @param parent is a pointer to a QWidget containing the parent. */ virtual void setParent( QWidget *parent ); /** Returns the destination ResourceCalendar for the specified incidence. @param incidence is a pointer to a valid Incidence object. */ virtual ResourceCalendar *destination( Incidence *incidence ) = 0; /** Return true if we have resources configure. Otherwise returns false. @since 4.3 */ bool hasCalendarResources(); protected: /** Returns the CalendarResourceManager used by this calendar. */ CalendarResourceManager *resourceManager(); private: //@cond PRIVATE Q_DISABLE_COPY( DestinationPolicy ) class Private; Private *d; //@endcond }; /** @class StandardDestinationPolicy */ class StandardDestinationPolicy : public DestinationPolicy { public: /** Constructs a standard destination policy. @param manager is a pointer to the CalendarResourceManager. @param parent is a pointer to a QWidget to use for new dialogs. */ explicit StandardDestinationPolicy( CalendarResourceManager *manager, QWidget *parent = 0 ); /** Destructor. */ virtual ~StandardDestinationPolicy(); /** Returns the destination ResourceCalendar for the specified incidence. @param incidence is a pointer to a valid Incidence object. */ ResourceCalendar *destination( Incidence *incidence ); private: //@cond PRIVATE Q_DISABLE_COPY( StandardDestinationPolicy ) class Private; Private *d; //@endcond }; /** @class AskDestinationPolicy */ class AskDestinationPolicy : public DestinationPolicy { public: /** Constructs an Ask destination policy. @param manager is a pointer to the CalendarResourceManager. @param parent is a pointer to a QWidget to use for new dialogs. */ explicit AskDestinationPolicy( CalendarResourceManager *manager, QWidget *parent = 0 ); /** Destructor. */ virtual ~AskDestinationPolicy(); /** Returns the destination ResourceCalendar for the specified incidence. @param incidence is a pointer to a valid Incidence object. */ ResourceCalendar *destination( Incidence *incidence ); private: //@cond PRIVATE Q_DISABLE_COPY( AskDestinationPolicy ) class Private; Private *d; //@endcond }; /** @class Ticket */ class Ticket { friend class CalendarResources; public: /** Returns the ResourceCalendar associated with the ticket. */ ResourceCalendar *resource() const; /** Destructor. */ ~Ticket(); private: /** Constructs a Ticket for a ResourceCalendar. @param resource is a pointer to a valid ResourceCalendar object. */ Ticket( ResourceCalendar *resource ); //@cond PRIVATE Q_DISABLE_COPY( Ticket ) class Private; Private *d; //@endcond }; /** Construct CalendarResource object using a time specification (time zone, etc.) and a Family name. @param timeSpec is a time specification which is used for creating or modifying incidences in the Calendar. It is also used for viewing incidences (see setViewTimeSpec()). @param family is any QString representing a unique name. */ CalendarResources( const KDateTime::Spec &timeSpec, const QString &family = QLatin1String( "calendar" ) ); /** Construct CalendarResource object using a time zone ID and a Family name. @param timeZoneId is used for creating or modifying incidences in the Calendar. It is also used for viewing incidences. The time zone does not alter existing incidences. @param family is any QString representing a unique name. */ CalendarResources( const QString &timeZoneId, const QString &family = QLatin1String( "calendar" ) ); /** Destroys the Calendar Resources. */ ~CalendarResources(); /** Clears the exception status. @since 4.2 */ void clearException(); /** Returns an exception, if there is any, containing information about the last error that occurred. @since 4.2 */ ErrorFormat *exception(); /** Loads all Incidences from the Resources. The Resources must be added first using either readConfig(KConfig *config), which adds the system Resources, or manually using resourceAdded(ResourceCalendar *resource). */ void load(); /** * Reloads all Incidences from all Resources. * @return true if the reload was successful; otherwise failure. */ bool reload(); /** @copydoc Calendar::close() */ void close(); /** Saves this Calendar. If the save is successful the Ticket is deleted. Otherwise, the caller must release the Ticket with releaseSaveTicket() to abandon the save operation or call save() to try the save again. @param ticket is a pointer to the Ticket object. @param incidence is a pointer to the Incidence object. If incidence is null, save the entire Calendar (all Resources) else only the specified Incidence is saved. @return true if the save was successful; false otherwise. */ virtual bool save( Ticket *ticket, Incidence *incidence = 0 ); /** @copydoc Calendar::save() */ bool save(); /** @copydoc Calendar::isSaving() */ bool isSaving(); /** Returns the CalendarResourceManager used by this calendar. */ CalendarResourceManager *resourceManager() const; /** Returns the Resource associated with a specified Incidence. @param incidence is a pointer to an Incidence whose Resource is to be located. */ ResourceCalendar *resource( Incidence *incidence ); /** Reads the Resources settings from a config file. @param config The KConfig object which points to the config file. If no object is given (@p config is 0) the standard config file is used. @note Call this method before load(). */ void readConfig( KConfig *config = 0 ); /** Set the destination policy such that Incidences are always added to the standard Resource. */ void setStandardDestinationPolicy(); /** Set the destination policy such that Incidences are added to a Resource which is queried. */ void setAskDestinationPolicy(); /** Return true if we have resources configure. Otherwise returns false. @since 4.3 */ bool hasCalendarResources(); /** Returns the current parent for new dialogs. This is a bad hack, but we need to properly set the parent for the resource selection dialog. Otherwise the dialog will not be modal to the editor dialog in korganizer and the user can still work in the editor dialog (and thus crash korganizer). Afterwards we need to reset it (to avoid pointers to widgets that are already deleted) so we also need the accessor @return a pointer to the parent QWidget. @see setDialogParentWidget() */ QWidget *dialogParentWidget(); /** Set the widget parent for new dialogs. This is a bad hack, but we need to properly set the parent for the resource selection dialog. Otherwise the dialog will not be modal to the editor dialog in korganizer and the user can still work in the editor dialog (and thus crash korganizer). @param parent is a pointer to the parent QWidget. @see dialogparentWidget() */ void setDialogParentWidget( QWidget *parent ); /** Requests a ticket for saving the Calendar. If a ticket is returned the Calendar is locked for write access until save() or releaseSaveTicket() is called. @param resource is a pointer to the ResourceCalendar object. @return a pointer to a Ticket object indicating that the Calendar is locked for write access; otherwise a null pointer. @see releaseSaveTicket() */ Ticket *requestSaveTicket( ResourceCalendar *resource ); /** Releases the save Ticket. The Calendar is unlocked without saving. @param ticket is a pointer to a Ticket object. @see requestSaveTicket() */ virtual void releaseSaveTicket( Ticket *ticket ); /** Add an active Resource to the Calendar, and loads that resource if it is open. Additionally, emits the @b signalResourceAdded signal. @note This method must be public, because in-process added Resources do not emit the corresponding signal, so this method has to be called manually! @param resource is a pointer to the ResourceCalendar to add. @see signalResourceAdded() */ void resourceAdded( ResourceCalendar *resource ); // Incidence Specific Methods // /** Inserts an Incidence into the calendar. @param incidence is a pointer to the Incidence to insert. @return true if the Incidence was successfully inserted; false otherwise. @return Will also return false if there are multiple writable resources and the user declines to select one to those resources in which to save the Incidence. */ bool addIncidence( Incidence *incidence ); /** Inserts an Incidence into a Calendar Resource. @param incidence is a pointer to the Incidence to insert. @param resource is a pointer to the ResourceCalendar to be added to. @return true if the Incidence was successfully inserted; false otherwise. */ bool addIncidence( Incidence *incidence, ResourceCalendar *resource ); /** @copydoc Calendar::beginChange() */ bool beginChange( Incidence *incidence ); /** @copydoc Calendar::endChange() */ bool endChange( Incidence *incidence ); // Event Specific Methods // /** @copydoc Calendar::addEvent() */ bool addEvent( Event *event ); /** Inserts an Event into a Calendar Resource. @param event is a pointer to the Event to insert. @param resource is a pointer to the ResourceCalendar to be added to. @return true if the Event was successfully inserted; false otherwise. @note In most cases use addIncidence( Incidence *incidence, ResourceCalendar *resource ) instead. */ bool addEvent( Event *event, ResourceCalendar *resource ); /** @copydoc Calendar::deleteEvent() */ bool deleteEvent( Event *event ); /** @copydoc Calendar::deleteAllEvents() */ void deleteAllEvents(); /** @copydoc Calendar::rawEvents(EventSortField, SortDirection) */ Event::List rawEvents( EventSortField sortField = EventSortUnsorted, SortDirection sortDirection = SortDirectionAscending ); /** @copydoc Calendar::rawEventsForDate( const KDateTime &) */ Event::List rawEventsForDate( const KDateTime &dt ); /** @copydoc Calendar::rawEvents(const QDate &, const QDate &, const KDateTime::Spec &, bool) */ Event::List rawEvents( const QDate &start, const QDate &end, const KDateTime::Spec &timeSpec = KDateTime::Spec(), bool inclusive = false ); /** Returns an unfiltered list of all Events which occur on the given date. @param date request unfiltered Event list for this QDate only. @param timeSpec time zone etc. to interpret @p date, or the calendar's default time spec if none is specified @param sortField specifies the EventSortField. @param sortDirection specifies the SortDirection. @return the list of unfiltered Events occurring on the specified QDate. */ Event::List rawEventsForDate( const QDate &date, const KDateTime::Spec &timeSpec = KDateTime::Spec(), EventSortField sortField = EventSortUnsorted, SortDirection sortDirection = SortDirectionAscending ); /** @copydoc Calendar::event() */ Event *event( const QString &uid ); // Todo Specific Methods // /** @copydoc Calendar::addTodo() */ bool addTodo( Todo *todo ); /** Inserts a Todo into a Calendar Resource. @param todo is a pointer to the Todo to insert. @param resource is a pointer to the ResourceCalendar to be added to. @return true if the Todo was successfully inserted; false otherwise. @note In most cases use addIncidence( Incidence *incidence, ResourceCalendar *resource ) instead. */ bool addTodo( Todo *todo, ResourceCalendar *resource ); /** @copydoc Calendar::deleteTodo() */ bool deleteTodo( Todo *todo ); /** @copydoc Calendar::deleteAllTodos() */ void deleteAllTodos(); /** @copydoc Calendar::rawTodos() */ Todo::List rawTodos( TodoSortField sortField = TodoSortUnsorted, SortDirection sortDirection = SortDirectionAscending ); /** @copydoc Calendar::rawTodosForDate() */ Todo::List rawTodosForDate( const QDate &date ); /** @copydoc Calendar::todo() */ Todo *todo( const QString &uid ); // Journal Specific Methods // /** @copydoc Calendar::addJournal() */ bool addJournal( Journal *journal ); /** Inserts a Journal into a Calendar Resource. @param journal is a pointer to the Journal to insert. @param resource is a pointer to the ResourceCalendar to be added to. @return true if the Journal was successfully inserted; false otherwise. @note In most cases use addIncidence( Incidence *incidence, ResourceCalendar *resource ) instead. */ bool addJournal( Journal *journal, ResourceCalendar *resource ); /** @copydoc Calendar::deleteJournal() */ bool deleteJournal( Journal *journal ); /** @copydoc Calendar::deleteAllJournals() */ void deleteAllJournals(); /** @copydoc Calendar::rawJournals() */ Journal::List rawJournals( JournalSortField sortField = JournalSortUnsorted, SortDirection sortDirection = SortDirectionAscending ); /** @copydoc Calendar::rawJournalsForDate() */ Journal::List rawJournalsForDate( const QDate &date ); /** @copydoc Calendar::journal() */ Journal *journal( const QString &uid ); // Alarm Specific Methods // /** @copydoc Calendar::alarms() */ Alarm::List alarms( const KDateTime &from, const KDateTime &to ); /** Return a list of Alarms that occur before the specified timestamp. @param to is the ending timestamp. @return the list of Alarms occurring before the specified KDateTime. */ Alarm::List alarmsTo( const KDateTime &to ); using QObject::event; // prevent warning about hidden virtual method Q_SIGNALS: /** Signals that the Resource has been modified. @param resource is a pointer to a ResourceCalendar that was changed. @see resourceModified() */ void signalResourceModified( ResourceCalendar *resource ); /** Signals that an Incidence has been inserted to the Resource. @param resource is a pointer to a ResourceCalendar that was added. @see resourceAdded() */ void signalResourceAdded( ResourceCalendar *resource ); /** Signals that an Incidence has been removed from the Resource. @param resource is a pointer to a ResourceCalendar that was removed. @see resourceDeleted() */ void signalResourceDeleted( ResourceCalendar *resource ); /** Signals an error message. @param err is the error message. */ void signalErrorMessage( const QString &err ); protected: /** Connects all necessary signals and slots to the resource. @param resource is a pointer to a ResourceCalendar. */ void connectResource( ResourceCalendar *resource ); /** Emits the @b signalResourceModified signal for the specified @p resource. @param resource is a pointer to a ResourceCalendar that was changed. @see signalResourceDeleted() */ void resourceModified( ResourceCalendar *resource ); /** Emits the @b signalResourceDeleted signal for the specified @p resource. @param resource is a pointer to a ResourceCalendar that was removed. @see signalResourceModified() */ void resourceDeleted( ResourceCalendar *resource ); /** @copydoc Calendar::doSetTimeSpec() */ virtual void doSetTimeSpec( const KDateTime::Spec &timeSpec ); /** Increment the number of times this Resource has been changed by 1. @param resource is a pointer to the ResourceCalendar to be counted. @return the new number of times this Resource has been changed. @see decrementChangeCount() */ int incrementChangeCount( ResourceCalendar *resource ); /** Decrement the number of times this Resource has been changed by 1. @param resource is a pointer to the ResourceCalendar to be counted. @return the new number of times this Resource has been changed. @see incrementChangeCount() */ int decrementChangeCount( ResourceCalendar *resource ); protected Q_SLOTS: /** Emits the @b signalErrorMessage signal with an error message when an error occurs loading a ResourceCalendar. @param resource is a pointer to the ResourceCalendar that failed to load. @param err is the error message. @see slotSaveError() */ void slotLoadError( ResourceCalendar *resource, const QString &err ); /** Emits the @b signalErrorMessage signal with an error message when an error occurs saving a ResourceCalendar. @param resource is a pointer to the ResourceCalendar that failed to save. @param err is the error message. @see slotLoadError() */ void slotSaveError( ResourceCalendar *resource, const QString &err ); + /** + All addIncidence( Incidence * ), addTodo( Todo * ) addEvent( Event * ) + and addJournal( Journal * ) calls made between beginAddingIncidences() + and endAddingIncidences() will only ask the user to choose a resource once. + @since 4.4 + */ + void beginAddingIncidences(); + + /** + @see beginAddingIncidences() + @since 4.4 + */ + void endAddingIncidences(); + private: //@cond PRIVATE Q_DISABLE_COPY( CalendarResources ) class Private; Private *d; //@endcond }; } #endif diff --git a/kcal/icalformat_p.cpp b/kcal/icalformat_p.cpp index 5e232b740..0662cf4a9 100644 --- a/kcal/icalformat_p.cpp +++ b/kcal/icalformat_p.cpp @@ -1,2671 +1,2675 @@ /* This file is part of the kcal library. Copyright (c) 2001 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (c) 2006 David Jarvie 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. */ /** @file This file is part of the API for handling calendar data and defines the internal ICalFormat classes. @brief This class provides the libical dependent functions for ICalFormat. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ @author David Jarvie \ */ #include "icalformat_p.h" #include "icalformat.h" #include "icaltimezones.h" #include "calendar.h" #include "compat.h" #include "journal.h" extern "C" { #include #include #include } #include #include #include #include #include #include #include #include #include #include #include using namespace KCal; /* Static helpers */ /* static void _dumpIcaltime( const icaltimetype& t) { kDebug() << "--- Y:" << t.year << "M:" << t.month << "D:" << t.day; kDebug() << "--- H:" << t.hour << "M:" << t.minute << "S:" << t.second; kDebug() << "--- isUtc:" << icaltime_is_utc( t ); kDebug() << "--- zoneId:" << icaltimezone_get_tzid( const_cast( t.zone ) ); } */ //@cond PRIVATE static QString quoteForParam( const QString &text ) { QString tmp = text; tmp.remove( '"' ); if ( tmp.contains( ';' ) || tmp.contains( ':' ) || tmp.contains( ',' ) ) { return tmp; // libical quotes in this case already, see icalparameter_as_ical_string() } return QString::fromLatin1( "\"" ) + tmp + QString::fromLatin1( "\"" ); } const int gSecondsPerMinute = 60; const int gSecondsPerHour = gSecondsPerMinute * 60; const int gSecondsPerDay = gSecondsPerHour * 24; const int gSecondsPerWeek = gSecondsPerDay * 7; class ToComponentVisitor : public IncidenceBase::Visitor { public: ToComponentVisitor( ICalFormatImpl *impl, iTIPMethod m ) : mImpl( impl ), mComponent( 0 ), mMethod( m ) {} bool visit( Event *e ) { mComponent = mImpl->writeEvent( e ); return true; } bool visit( Todo *e ) { mComponent = mImpl->writeTodo( e ); return true; } bool visit( Journal *e ) { mComponent = mImpl->writeJournal( e ); return true; } bool visit( FreeBusy *fb ) { mComponent = mImpl->writeFreeBusy( fb, mMethod ); return true; } icalcomponent *component() { return mComponent; } private: ICalFormatImpl *mImpl; icalcomponent *mComponent; iTIPMethod mMethod; }; class ICalFormatImpl::Private { public: Private( ICalFormatImpl *impl, ICalFormat *parent ) : mImpl( impl ), mParent( parent ), mCompat( new Compat ) {} ~Private() { delete mCompat; } void writeIncidenceBase( icalcomponent *parent, IncidenceBase * ); void readIncidenceBase( icalcomponent *parent, IncidenceBase * ); void writeCustomProperties( icalcomponent *parent, CustomProperties * ); void readCustomProperties( icalcomponent *parent, CustomProperties * ); ICalFormatImpl *mImpl; ICalFormat *mParent; QString mLoadedProductId; // PRODID string loaded from calendar file Event::List mEventsRelate; // events with relations Todo::List mTodosRelate; // todos with relations Compat *mCompat; }; //@endcond inline icaltimetype ICalFormatImpl::writeICalUtcDateTime ( const KDateTime &dt ) { return writeICalDateTime( dt.toUtc() ); } ICalFormatImpl::ICalFormatImpl( ICalFormat *parent ) : d( new Private( this, parent ) ) { } ICalFormatImpl::~ICalFormatImpl() { delete d; } QString ICalFormatImpl::loadedProductId() const { return d->mLoadedProductId; } icalcomponent *ICalFormatImpl::writeIncidence( IncidenceBase *incidence, iTIPMethod method ) { ToComponentVisitor v( this, method ); if ( incidence->accept(v) ) { return v.component(); } else { return 0; } } icalcomponent *ICalFormatImpl::writeTodo( Todo *todo, ICalTimeZones *tzlist, ICalTimeZones *tzUsedList ) { QString tmpStr; QStringList tmpStrList; icalcomponent *vtodo = icalcomponent_new( ICAL_VTODO_COMPONENT ); writeIncidence( vtodo, todo, tzlist, tzUsedList ); // due date icalproperty *prop; if ( todo->hasDueDate() ) { icaltimetype due; if ( todo->allDay() ) { due = writeICalDate( todo->dtDue( true ).date() ); prop = icalproperty_new_due(due); } else { prop = writeICalDateTimeProperty( ICAL_DUE_PROPERTY, todo->dtDue(true), tzlist, tzUsedList ); } icalcomponent_add_property( vtodo, prop ); } // start time if ( todo->hasStartDate() ) { icaltimetype start; if ( todo->allDay() ) { start = writeICalDate( todo->dtStart( true ).date() ); prop = icalproperty_new_dtstart( start ); } else { prop = writeICalDateTimeProperty( ICAL_DTSTART_PROPERTY, todo->dtStart( true ), tzlist, tzUsedList ); } icalcomponent_add_property( vtodo, prop ); } // completion date (UTC) if ( todo->isCompleted() ) { if ( !todo->hasCompletedDate() ) { // If the todo was created by KOrganizer<2.2 it does not have // a correct completion date. Set one now. todo->setCompleted( KDateTime::currentUtcDateTime() ); } icaltimetype completed = writeICalUtcDateTime( todo->completed() ); icalcomponent_add_property( vtodo, icalproperty_new_completed ( completed ) ); } icalcomponent_add_property( vtodo, icalproperty_new_percentcomplete( todo->percentComplete() ) ); if ( todo->recurs() ) { icalcomponent_add_property( vtodo, writeICalDateTimeProperty( ICAL_RECURRENCEID_PROPERTY, todo->dtDue(), tzlist, tzUsedList ) ); } return vtodo; } icalcomponent *ICalFormatImpl::writeEvent( Event *event, ICalTimeZones *tzlist, ICalTimeZones *tzUsedList ) { icalcomponent *vevent = icalcomponent_new( ICAL_VEVENT_COMPONENT ); writeIncidence( vevent, event, tzlist, tzUsedList ); // start time icalproperty *prop; icaltimetype start; if ( event->allDay() ) { start = writeICalDate( event->dtStart().date() ); prop = icalproperty_new_dtstart( start ); } else { prop = writeICalDateTimeProperty( ICAL_DTSTART_PROPERTY, event->dtStart(), tzlist, tzUsedList ); } icalcomponent_add_property( vevent, prop ); if ( event->hasEndDate() ) { // End time. // RFC2445 says that if DTEND is present, it has to be greater than DTSTART. icaltimetype end; KDateTime dt = event->dtEnd(); if ( event->allDay() ) { // +1 day because end date is non-inclusive. end = writeICalDate( dt.date().addDays( 1 ) ); icalcomponent_add_property( vevent, icalproperty_new_dtend(end) ); } else { if ( dt != event->dtStart() ) { icalcomponent_add_property( vevent, writeICalDateTimeProperty( ICAL_DTEND_PROPERTY, dt, tzlist, tzUsedList ) ); } } } // TODO: resources #if 0 // resources QStringList tmpStrList = anEvent->resources(); QString tmpStr = tmpStrList.join( ";" ); if ( !tmpStr.isEmpty() ) { addPropValue( vevent, VCResourcesProp, tmpStr.toUtf8() ); } #endif // Transparency switch( event->transparency() ) { case Event::Transparent: icalcomponent_add_property( vevent, icalproperty_new_transp( ICAL_TRANSP_TRANSPARENT ) ); break; case Event::Opaque: icalcomponent_add_property( vevent, icalproperty_new_transp( ICAL_TRANSP_OPAQUE ) ); break; } return vevent; } icalcomponent *ICalFormatImpl::writeFreeBusy( FreeBusy *freebusy, iTIPMethod method ) { icalcomponent *vfreebusy = icalcomponent_new( ICAL_VFREEBUSY_COMPONENT ); d->writeIncidenceBase( vfreebusy, freebusy ); icalcomponent_add_property( vfreebusy, icalproperty_new_dtstart( writeICalUtcDateTime( freebusy->dtStart() ) ) ); icalcomponent_add_property( vfreebusy, icalproperty_new_dtend( writeICalUtcDateTime( freebusy->dtEnd() ) ) ); if ( method == iTIPRequest ) { icalcomponent_add_property( vfreebusy, icalproperty_new_uid( freebusy->uid().toUtf8() ) ); } //Loops through all the periods in the freebusy object QList list = freebusy->busyPeriods(); icalperiodtype period = icalperiodtype_null_period(); for ( int i = 0, count = list.count(); i < count; ++i ) { period.start = writeICalUtcDateTime( list[i].start() ); if ( list[i].hasDuration() ) { period.duration = writeICalDuration( list[i].duration() ); } else { period.end = writeICalUtcDateTime( list[i].end() ); } icalcomponent_add_property( vfreebusy, icalproperty_new_freebusy( period ) ); } return vfreebusy; } icalcomponent *ICalFormatImpl::writeJournal( Journal *journal, ICalTimeZones *tzlist, ICalTimeZones *tzUsedList ) { icalcomponent *vjournal = icalcomponent_new( ICAL_VJOURNAL_COMPONENT ); writeIncidence( vjournal, journal, tzlist, tzUsedList ); // start time icalproperty *prop; KDateTime dt = journal->dtStart(); if ( dt.isValid() ) { icaltimetype start; if ( journal->allDay() ) { start = writeICalDate( dt.date() ); prop = icalproperty_new_dtstart( start ); } else { prop = writeICalDateTimeProperty( ICAL_DTSTART_PROPERTY, dt, tzlist, tzUsedList ); } icalcomponent_add_property( vjournal, prop ); } return vjournal; } void ICalFormatImpl::writeIncidence( icalcomponent *parent, Incidence *incidence, ICalTimeZones *tzlist, ICalTimeZones *tzUsedList ) { if ( incidence->schedulingID() != incidence->uid() ) { // We need to store the UID in here. The rawSchedulingID will // go into the iCal UID component incidence->setCustomProperty( "LIBKCAL", "ID", incidence->uid() ); } else { incidence->removeCustomProperty( "LIBKCAL", "ID" ); } d->writeIncidenceBase( parent, incidence ); // creation date icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_CREATED_PROPERTY, incidence->created() ) ); // unique id // If the scheduling ID is different from the real UID, the real // one is stored on X-REALID above if ( !incidence->schedulingID().isEmpty() ) { icalcomponent_add_property( parent, icalproperty_new_uid( incidence->schedulingID().toUtf8() ) ); } // revision if ( incidence->revision() > 0 ) { // 0 is default, so don't write that out icalcomponent_add_property( parent, icalproperty_new_sequence( incidence->revision() ) ); } // last modification date if ( incidence->lastModified().isValid() ) { icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_LASTMODIFIED_PROPERTY, incidence->lastModified() ) ); } // description if ( !incidence->description().isEmpty() ) { icalcomponent_add_property( parent, writeDescription( incidence->description(), incidence->descriptionIsRich() ) ); } // summary if ( !incidence->summary().isEmpty() ) { icalcomponent_add_property( parent, writeSummary( incidence->summary(), incidence->summaryIsRich() ) ); } // location if ( !incidence->location().isEmpty() ) { icalcomponent_add_property( parent, writeLocation( incidence->location(), incidence->locationIsRich() ) ); } // status icalproperty_status status = ICAL_STATUS_NONE; switch ( incidence->status() ) { case Incidence::StatusTentative: status = ICAL_STATUS_TENTATIVE; break; case Incidence::StatusConfirmed: status = ICAL_STATUS_CONFIRMED; break; case Incidence::StatusCompleted: status = ICAL_STATUS_COMPLETED; break; case Incidence::StatusNeedsAction: status = ICAL_STATUS_NEEDSACTION; break; case Incidence::StatusCanceled: status = ICAL_STATUS_CANCELLED; break; case Incidence::StatusInProcess: status = ICAL_STATUS_INPROCESS; break; case Incidence::StatusDraft: status = ICAL_STATUS_DRAFT; break; case Incidence::StatusFinal: status = ICAL_STATUS_FINAL; break; case Incidence::StatusX: { icalproperty *p = icalproperty_new_status( ICAL_STATUS_X ); icalvalue_set_x( icalproperty_get_value( p ), incidence->statusStr().toUtf8() ); icalcomponent_add_property( parent, p ); break; } case Incidence::StatusNone: default: break; } if ( status != ICAL_STATUS_NONE ) { icalcomponent_add_property( parent, icalproperty_new_status( status ) ); } // secrecy icalproperty_class secClass; switch ( incidence->secrecy() ) { case Incidence::SecrecyPublic: secClass = ICAL_CLASS_PUBLIC; break; case Incidence::SecrecyConfidential: secClass = ICAL_CLASS_CONFIDENTIAL; break; case Incidence::SecrecyPrivate: default: secClass = ICAL_CLASS_PRIVATE; break; } if ( secClass != ICAL_CLASS_PUBLIC ) { icalcomponent_add_property( parent, icalproperty_new_class( secClass ) ); } // geo if ( incidence->hasGeo() ) { icalgeotype geo; geo.lat = incidence->geoLatitude(); geo.lon = incidence->geoLongitude(); icalcomponent_add_property( parent, icalproperty_new_geo( geo ) ); } // priority if ( incidence->priority() > 0 ) { // 0 is undefined priority icalcomponent_add_property( parent, icalproperty_new_priority( incidence->priority() ) ); } // categories QString categories = incidence->categories().join( "," ); if ( !categories.isEmpty() ) { icalcomponent_add_property( parent, icalproperty_new_categories( categories.toUtf8() ) ); } // related event if ( !incidence->relatedToUid().isEmpty() ) { icalcomponent_add_property( parent, icalproperty_new_relatedto( incidence->relatedToUid().toUtf8() ) ); } RecurrenceRule::List rrules( incidence->recurrence()->rRules() ); RecurrenceRule::List::ConstIterator rit; for ( rit = rrules.constBegin(); rit != rrules.constEnd(); ++rit ) { icalcomponent_add_property( parent, icalproperty_new_rrule( writeRecurrenceRule( (*rit) ) ) ); } RecurrenceRule::List exrules( incidence->recurrence()->exRules() ); RecurrenceRule::List::ConstIterator exit; for ( exit = exrules.constBegin(); exit != exrules.constEnd(); ++exit ) { icalcomponent_add_property( parent, icalproperty_new_exrule( writeRecurrenceRule( (*exit) ) ) ); } DateList dateList = incidence->recurrence()->exDates(); DateList::ConstIterator exIt; for ( exIt = dateList.constBegin(); exIt != dateList.constEnd(); ++exIt ) { icalcomponent_add_property( parent, icalproperty_new_exdate( writeICalDate(*exIt) ) ); } DateTimeList dateTimeList = incidence->recurrence()->exDateTimes(); DateTimeList::ConstIterator extIt; for ( extIt = dateTimeList.constBegin(); extIt != dateTimeList.constEnd(); ++extIt ) { icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_EXDATE_PROPERTY, *extIt, tzlist, tzUsedList ) ); } dateList = incidence->recurrence()->rDates(); DateList::ConstIterator rdIt; for ( rdIt = dateList.constBegin(); rdIt != dateList.constEnd(); ++rdIt ) { icalcomponent_add_property( parent, icalproperty_new_rdate( writeICalDatePeriod(*rdIt) ) ); } dateTimeList = incidence->recurrence()->rDateTimes(); DateTimeList::ConstIterator rdtIt; for ( rdtIt = dateTimeList.constBegin(); rdtIt != dateTimeList.constEnd(); ++rdtIt ) { icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_RDATE_PROPERTY, *rdtIt, tzlist, tzUsedList ) ); } // attachments Attachment::List attachments = incidence->attachments(); Attachment::List::ConstIterator atIt; for ( atIt = attachments.constBegin(); atIt != attachments.constEnd(); ++atIt ) { icalcomponent_add_property( parent, writeAttachment( *atIt ) ); } // alarms Alarm::List::ConstIterator alarmIt; for ( alarmIt = incidence->alarms().constBegin(); alarmIt != incidence->alarms().constEnd(); ++alarmIt ) { if ( (*alarmIt)->enabled() ) { icalcomponent_add_component( parent, writeAlarm( *alarmIt ) ); } } // duration if ( incidence->hasDuration() ) { icaldurationtype duration; duration = writeICalDuration( incidence->duration() ); icalcomponent_add_property( parent, icalproperty_new_duration( duration ) ); } } //@cond PRIVATE void ICalFormatImpl::Private::writeIncidenceBase( icalcomponent *parent, IncidenceBase *incidenceBase ) { icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_DTSTAMP_PROPERTY, KDateTime::currentUtcDateTime() ) ); // organizer stuff if ( !incidenceBase->organizer().isEmpty() ) { icalproperty *p = mImpl->writeOrganizer( incidenceBase->organizer() ); if ( p ) { icalcomponent_add_property( parent, p ); } } // attendees if ( incidenceBase->attendeeCount() > 0 ) { Attendee::List::ConstIterator it; for ( it = incidenceBase->attendees().constBegin(); it != incidenceBase->attendees().constEnd(); ++it ) { icalproperty *p = mImpl->writeAttendee( *it ); if ( p ) { icalcomponent_add_property( parent, p ); } } } // comments QStringList comments = incidenceBase->comments(); for ( QStringList::const_iterator it = comments.constBegin(); it != comments.constEnd(); ++it ) { icalcomponent_add_property( parent, icalproperty_new_comment( (*it).toUtf8() ) ); } // custom properties writeCustomProperties( parent, incidenceBase ); } void ICalFormatImpl::Private::writeCustomProperties( icalcomponent *parent, CustomProperties *properties ) { const QMap custom = properties->customProperties(); for ( QMap::ConstIterator c = custom.begin(); c != custom.end(); ++c ) { icalproperty *p = icalproperty_new_x( c.value().toUtf8() ); if ( !c.key().startsWith( "X-KDE" ) && //krazy:exclude=strings !c.key().startsWith( "X-LibKCal" ) && //krazy:exclude=strings !c.key().startsWith( "X-MICROSOFT" ) && //krazy:exclude=strings !c.key().startsWith( "X-MOZILLA" ) && //krazy:exclude=strings !c.key().startsWith( "X-PILOT" ) ) { //krazy:exclude=strings // use text values for the typical X-FOO property. // except for vendor specific X-FOO properties. icalvalue *text = icalvalue_new_text( c.value().toUtf8().data() ); icalproperty_set_value( p, text ); } icalproperty_set_x_name( p, c.key() ); icalcomponent_add_property( parent, p ); } } //@endcond icalproperty *ICalFormatImpl::writeOrganizer( const Person &organizer ) { if ( organizer.email().isEmpty() ) { return 0; } icalproperty *p = icalproperty_new_organizer( "MAILTO:" + organizer.email().toUtf8() ); if ( !organizer.name().isEmpty() ) { icalproperty_add_parameter( p, icalparameter_new_cn( quoteForParam( organizer.name() ).toUtf8() ) ); } // TODO: Write dir, sent-by and language return p; } icalproperty *ICalFormatImpl::writeDescription( const QString &description, bool isRich ) { icalproperty *p = icalproperty_new_description( description.toUtf8() ); if ( isRich ) { icalproperty_add_parameter( p, icalparameter_new_from_string( "X-KDE-TEXTFORMAT=HTML" ) ); } return p; } icalproperty *ICalFormatImpl::writeSummary( const QString &summary, bool isRich ) { icalproperty *p = icalproperty_new_summary( summary.toUtf8() ); if ( isRich ) { icalproperty_add_parameter( p, icalparameter_new_from_string( "X-KDE-TEXTFORMAT=HTML" ) ); } return p; } icalproperty *ICalFormatImpl::writeLocation( const QString &location, bool isRich ) { icalproperty *p = icalproperty_new_location( location.toUtf8() ); if ( isRich ) { icalproperty_add_parameter( p, icalparameter_new_from_string( "X-KDE-TEXTFORMAT=HTML" ) ); } return p; } icalproperty *ICalFormatImpl::writeAttendee( Attendee *attendee ) { if ( attendee->email().isEmpty() ) { return 0; } icalproperty *p = icalproperty_new_attendee( "mailto:" + attendee->email().toUtf8() ); if ( !attendee->name().isEmpty() ) { icalproperty_add_parameter( p, icalparameter_new_cn( quoteForParam( attendee->name() ).toUtf8() ) ); } icalproperty_add_parameter( p, icalparameter_new_rsvp( attendee->RSVP() ? ICAL_RSVP_TRUE : ICAL_RSVP_FALSE ) ); icalparameter_partstat status = ICAL_PARTSTAT_NEEDSACTION; switch ( attendee->status() ) { default: case Attendee::NeedsAction: status = ICAL_PARTSTAT_NEEDSACTION; break; case Attendee::Accepted: status = ICAL_PARTSTAT_ACCEPTED; break; case Attendee::Declined: status = ICAL_PARTSTAT_DECLINED; break; case Attendee::Tentative: status = ICAL_PARTSTAT_TENTATIVE; break; case Attendee::Delegated: status = ICAL_PARTSTAT_DELEGATED; break; case Attendee::Completed: status = ICAL_PARTSTAT_COMPLETED; break; case Attendee::InProcess: status = ICAL_PARTSTAT_INPROCESS; break; } icalproperty_add_parameter( p, icalparameter_new_partstat( status ) ); icalparameter_role role = ICAL_ROLE_REQPARTICIPANT; switch ( attendee->role() ) { case Attendee::Chair: role = ICAL_ROLE_CHAIR; break; default: case Attendee::ReqParticipant: role = ICAL_ROLE_REQPARTICIPANT; break; case Attendee::OptParticipant: role = ICAL_ROLE_OPTPARTICIPANT; break; case Attendee::NonParticipant: role = ICAL_ROLE_NONPARTICIPANT; break; } icalproperty_add_parameter( p, icalparameter_new_role( role ) ); if ( !attendee->uid().isEmpty() ) { icalparameter *icalparameter_uid = icalparameter_new_x( attendee->uid().toUtf8() ); icalparameter_set_xname( icalparameter_uid, "X-UID" ); icalproperty_add_parameter( p, icalparameter_uid ); } if ( !attendee->delegate().isEmpty() ) { icalparameter *icalparameter_delegate = icalparameter_new_delegatedto( attendee->delegate().toUtf8() ); icalproperty_add_parameter( p, icalparameter_delegate ); } if ( !attendee->delegator().isEmpty() ) { icalparameter *icalparameter_delegator = icalparameter_new_delegatedfrom( attendee->delegator().toUtf8() ); icalproperty_add_parameter( p, icalparameter_delegator ); } return p; } icalproperty *ICalFormatImpl::writeAttachment( Attachment *att ) { icalattach *attach; if ( att->isUri() ) { attach = icalattach_new_from_url( att->uri().toUtf8().data() ); } else { attach = icalattach_new_from_data ( ( unsigned char * )att->data(), 0, 0 ); } icalproperty *p = icalproperty_new_attach( attach ); if ( !att->mimeType().isEmpty() ) { icalproperty_add_parameter( p, icalparameter_new_fmttype( att->mimeType().toUtf8().data() ) ); } if ( att->isBinary() ) { icalproperty_add_parameter( p, icalparameter_new_value( ICAL_VALUE_BINARY ) ); icalproperty_add_parameter( p, icalparameter_new_encoding( ICAL_ENCODING_BASE64 ) ); } if ( att->showInline() ) { icalparameter *icalparameter_inline = icalparameter_new_x( "inline" ); icalparameter_set_xname( icalparameter_inline, "X-CONTENT-DISPOSITION" ); icalproperty_add_parameter( p, icalparameter_inline ); } if ( !att->label().isEmpty() ) { icalparameter *icalparameter_label = icalparameter_new_x( att->label().toUtf8() ); icalparameter_set_xname( icalparameter_label, "X-LABEL" ); icalproperty_add_parameter( p, icalparameter_label ); } if ( att->isLocal() ) { icalparameter *icalparameter_local = icalparameter_new_x( "local" ); icalparameter_set_xname( icalparameter_local, "X-KONTACT-TYPE" ); icalproperty_add_parameter( p, icalparameter_local ); } return p; } icalrecurrencetype ICalFormatImpl::writeRecurrenceRule( RecurrenceRule *recur ) { icalrecurrencetype r; icalrecurrencetype_clear( &r ); switch( recur->recurrenceType() ) { case RecurrenceRule::rSecondly: r.freq = ICAL_SECONDLY_RECURRENCE; break; case RecurrenceRule::rMinutely: r.freq = ICAL_MINUTELY_RECURRENCE; break; case RecurrenceRule::rHourly: r.freq = ICAL_HOURLY_RECURRENCE; break; case RecurrenceRule::rDaily: r.freq = ICAL_DAILY_RECURRENCE; break; case RecurrenceRule::rWeekly: r.freq = ICAL_WEEKLY_RECURRENCE; break; case RecurrenceRule::rMonthly: r.freq = ICAL_MONTHLY_RECURRENCE; break; case RecurrenceRule::rYearly: r.freq = ICAL_YEARLY_RECURRENCE; break; default: r.freq = ICAL_NO_RECURRENCE; kDebug() << "no recurrence"; break; } int index = 0; QList bys; QList::ConstIterator it; // Now write out the BY* parts: bys = recur->bySeconds(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_second[index++] = *it; } bys = recur->byMinutes(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_minute[index++] = *it; } bys = recur->byHours(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_hour[index++] = *it; } bys = recur->byMonthDays(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_month_day[index++] = icalrecurrencetype_day_position( (*it) * 8 ); } bys = recur->byYearDays(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_year_day[index++] = *it; } bys = recur->byWeekNumbers(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_week_no[index++] = *it; } bys = recur->byMonths(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_month[index++] = *it; } bys = recur->bySetPos(); index = 0; for ( it = bys.constBegin(); it != bys.constEnd(); ++it ) { r.by_set_pos[index++] = *it; } QList byd = recur->byDays(); int day; index = 0; for ( QList::ConstIterator dit = byd.constBegin(); dit != byd.constEnd(); ++dit ) { day = (*dit).day() % 7 + 1; // convert from Monday=1 to Sunday=1 if ( (*dit).pos() < 0 ) { day += ( -(*dit).pos() ) * 8; day = -day; } else { day += (*dit).pos() * 8; } r.by_day[index++] = day; } r.week_start = static_cast( recur->weekStart() % 7 + 1 ); if ( recur->frequency() > 1 ) { // Dont' write out INTERVAL=1, because that's the default anyway r.interval = recur->frequency(); } if ( recur->duration() > 0 ) { r.count = recur->duration(); } else if ( recur->duration() == -1 ) { r.count = 0; } else { if ( recur->allDay() ) { r.until = writeICalDate( recur->endDt().date() ); } else { r.until = writeICalUtcDateTime( recur->endDt() ); } } return r; } icalcomponent *ICalFormatImpl::writeAlarm( Alarm *alarm ) { icalcomponent *a = icalcomponent_new( ICAL_VALARM_COMPONENT ); icalproperty_action action; icalattach *attach = 0; switch ( alarm->type() ) { case Alarm::Procedure: action = ICAL_ACTION_PROCEDURE; attach = icalattach_new_from_url( QFile::encodeName( alarm->programFile() ).data() ); icalcomponent_add_property( a, icalproperty_new_attach( attach ) ); if ( !alarm->programArguments().isEmpty() ) { icalcomponent_add_property( a, icalproperty_new_description( alarm->programArguments().toUtf8() ) ); } break; case Alarm::Audio: action = ICAL_ACTION_AUDIO; if ( !alarm->audioFile().isEmpty() ) { attach = icalattach_new_from_url( QFile::encodeName( alarm->audioFile() ).data() ); icalcomponent_add_property( a, icalproperty_new_attach( attach ) ); } break; case Alarm::Email: { action = ICAL_ACTION_EMAIL; const QList addresses = alarm->mailAddresses(); for ( QList::ConstIterator ad = addresses.constBegin(); ad != addresses.constEnd(); ++ad ) { if ( !(*ad).email().isEmpty() ) { icalproperty *p = icalproperty_new_attendee( "MAILTO:" + (*ad).email().toUtf8() ); if ( !(*ad).name().isEmpty() ) { icalproperty_add_parameter( p, icalparameter_new_cn( quoteForParam( (*ad).name() ).toUtf8() ) ); } icalcomponent_add_property( a, p ); } } icalcomponent_add_property( a, icalproperty_new_summary( alarm->mailSubject().toUtf8() ) ); icalcomponent_add_property( a, icalproperty_new_description( alarm->mailText().toUtf8() ) ); QStringList attachments = alarm->mailAttachments(); if ( attachments.count() > 0 ) { for ( QStringList::const_iterator at = attachments.constBegin(); at != attachments.constEnd(); ++at ) { attach = icalattach_new_from_url( QFile::encodeName( *at ).data() ); icalcomponent_add_property( a, icalproperty_new_attach( attach ) ); } } break; } case Alarm::Display: action = ICAL_ACTION_DISPLAY; icalcomponent_add_property( a, icalproperty_new_description( alarm->text().toUtf8() ) ); break; case Alarm::Invalid: default: kDebug() << "Unknown type of alarm"; action = ICAL_ACTION_NONE; break; } icalcomponent_add_property( a, icalproperty_new_action( action ) ); // Trigger time icaltriggertype trigger; if ( alarm->hasTime() ) { trigger.time = writeICalUtcDateTime( alarm->time() ); trigger.duration = icaldurationtype_null_duration(); } else { trigger.time = icaltime_null_time(); Duration offset; if ( alarm->hasStartOffset() ) { offset = alarm->startOffset(); } else { offset = alarm->endOffset(); } trigger.duration = writeICalDuration( offset ); } icalproperty *p = icalproperty_new_trigger( trigger ); if ( alarm->hasEndOffset() ) { icalproperty_add_parameter( p, icalparameter_new_related( ICAL_RELATED_END ) ); } icalcomponent_add_property( a, p ); // Repeat count and duration if ( alarm->repeatCount() ) { icalcomponent_add_property( a, icalproperty_new_repeat( alarm->repeatCount() ) ); icalcomponent_add_property( a, icalproperty_new_duration( writeICalDuration( alarm->snoozeTime() ) ) ); } // Custom properties const QMap custom = alarm->customProperties(); for ( QMap::ConstIterator c = custom.begin(); c != custom.end(); ++c ) { icalproperty *p = icalproperty_new_x( c.value().toUtf8() ); icalproperty_set_x_name( p, c.key() ); icalcomponent_add_property( a, p ); } return a; } Todo *ICalFormatImpl::readTodo( icalcomponent *vtodo, ICalTimeZones *tzlist ) { Todo *todo = new Todo; readIncidence( vtodo, todo, tzlist ); icalproperty *p = icalcomponent_get_first_property( vtodo, ICAL_ANY_PROPERTY ); while ( p ) { icalproperty_kind kind = icalproperty_isa(p); switch ( kind ) { case ICAL_DUE_PROPERTY: { // due date/time KDateTime kdt = readICalDateTimeProperty( p, tzlist ); todo->setDtDue( kdt, true ); todo->setHasDueDate( true ); todo->setAllDay( kdt.isDateOnly() ); break; } case ICAL_COMPLETED_PROPERTY: // completion date/time todo->setCompleted( readICalDateTimeProperty( p, tzlist ) ); break; case ICAL_PERCENTCOMPLETE_PROPERTY: // Percent completed todo->setPercentComplete( icalproperty_get_percentcomplete( p ) ); break; case ICAL_RELATEDTO_PROPERTY: // related todo (parent) todo->setRelatedToUid( QString::fromUtf8( icalproperty_get_relatedto( p ) ) ); d->mTodosRelate.append( todo ); break; case ICAL_DTSTART_PROPERTY: // Flag that todo has start date. Value is read in by readIncidence(). if ( todo->comments().filter( "NoStartDate" ).count() ) { todo->setHasStartDate( false ); } else { todo->setHasStartDate( true ); } break; case ICAL_RECURRENCEID_PROPERTY: todo->setDtRecurrence( readICalDateTimeProperty( p, tzlist ) ); break; default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property( vtodo, ICAL_ANY_PROPERTY ); } if ( d->mCompat ) { d->mCompat->fixEmptySummary( todo ); } return todo; } Event *ICalFormatImpl::readEvent( icalcomponent *vevent, ICalTimeZones *tzlist ) { Event *event = new Event; readIncidence( vevent, event, tzlist ); icalproperty *p = icalcomponent_get_first_property( vevent, ICAL_ANY_PROPERTY ); bool dtEndProcessed = false; while ( p ) { icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_DTEND_PROPERTY: { // end date and time KDateTime kdt = readICalDateTimeProperty( p, tzlist ); if ( kdt.isDateOnly() ) { // End date is non-inclusive QDate endDate = kdt.date().addDays( -1 ); if ( d->mCompat ) { d->mCompat->fixFloatingEnd( endDate ); } if ( endDate < event->dtStart().date() ) { endDate = event->dtStart().date(); } event->setDtEnd( KDateTime( endDate, event->dtStart().timeSpec() ) ); } else { event->setDtEnd( kdt ); event->setAllDay( false ); } dtEndProcessed = true; break; } case ICAL_RELATEDTO_PROPERTY: // related event (parent) event->setRelatedToUid( QString::fromUtf8( icalproperty_get_relatedto( p ) ) ); d->mEventsRelate.append( event ); break; case ICAL_TRANSP_PROPERTY: // Transparency { icalproperty_transp transparency = icalproperty_get_transp( p ); if ( transparency == ICAL_TRANSP_TRANSPARENT ) { event->setTransparency( Event::Transparent ); } else { event->setTransparency( Event::Opaque ); } break; } default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property( vevent, ICAL_ANY_PROPERTY ); } // according to rfc2445 the dtend shouldn't be written when it equals // start date. so assign one equal to start date. if ( !dtEndProcessed && !event->hasDuration() ) { event->setDtEnd( event->dtStart() ); event->setHasEndDate( false ); } QString msade = event->nonKDECustomProperty( "X-MICROSOFT-CDO-ALLDAYEVENT" ); if ( !msade.isEmpty() ) { bool allDay = ( msade == QLatin1String( "TRUE" ) ); event->setAllDay( allDay ); } if ( d->mCompat ) { d->mCompat->fixEmptySummary( event ); } return event; } FreeBusy *ICalFormatImpl::readFreeBusy( icalcomponent *vfreebusy ) { FreeBusy *freebusy = new FreeBusy; d->readIncidenceBase( vfreebusy, freebusy ); icalproperty *p = icalcomponent_get_first_property( vfreebusy, ICAL_ANY_PROPERTY ); FreeBusyPeriod::List periods; while ( p ) { icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_DTSTART_PROPERTY: // start date and time (UTC) freebusy->setDtStart( readICalUtcDateTimeProperty( p ) ); break; case ICAL_DTEND_PROPERTY: // end Date and Time (UTC) freebusy->setDtEnd( readICalUtcDateTimeProperty( p ) ); break; case ICAL_FREEBUSY_PROPERTY: //Any FreeBusy Times (UTC) { icalperiodtype icalperiod = icalproperty_get_freebusy( p ); KDateTime period_start = readICalUtcDateTime( p, icalperiod.start ); FreeBusyPeriod period; if ( !icaltime_is_null_time( icalperiod.end ) ) { KDateTime period_end = readICalUtcDateTime( p, icalperiod.end ); period = FreeBusyPeriod( period_start, period_end ); } else { Duration duration ( readICalDuration( icalperiod.duration ) ); period = FreeBusyPeriod( period_start, duration ); } QByteArray param = icalproperty_get_parameter_as_string( p, "X-SUMMARY" ); period.setSummary( QString::fromUtf8( KCodecs::base64Decode( param ) ) ); param = icalproperty_get_parameter_as_string( p, "X-LOCATION" ); period.setLocation( QString::fromUtf8( KCodecs::base64Decode( param ) ) ); periods.append( period ); break; } default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property( vfreebusy, ICAL_ANY_PROPERTY ); } freebusy->addPeriods( periods ); return freebusy; } Journal *ICalFormatImpl::readJournal( icalcomponent *vjournal, ICalTimeZones *tzlist ) { Journal *journal = new Journal; readIncidence( vjournal, journal, tzlist ); return journal; } Attendee *ICalFormatImpl::readAttendee( icalproperty *attendee ) { // the following is a hack to support broken calendars (like WebCalendar 1.0.x) // that include non-RFC-compliant attendees. Otherwise libical 0.42 asserts. if ( !icalproperty_get_value( attendee ) ) { return 0; } icalparameter *p = 0; QString email = QString::fromUtf8( icalproperty_get_attendee( attendee ) ); if ( email.startsWith( QLatin1String( "mailto:" ), Qt::CaseInsensitive ) ) { email = email.mid( 7 ); } QString name; QString uid; p = icalproperty_get_first_parameter( attendee, ICAL_CN_PARAMETER ); if ( p ) { name = QString::fromUtf8( icalparameter_get_cn( p ) ); } else { } bool rsvp = false; p = icalproperty_get_first_parameter( attendee, ICAL_RSVP_PARAMETER ); if ( p ) { icalparameter_rsvp rsvpParameter = icalparameter_get_rsvp( p ); if ( rsvpParameter == ICAL_RSVP_TRUE ) { rsvp = true; } } Attendee::PartStat status = Attendee::NeedsAction; p = icalproperty_get_first_parameter( attendee, ICAL_PARTSTAT_PARAMETER ); if ( p ) { icalparameter_partstat partStatParameter = icalparameter_get_partstat( p ); switch( partStatParameter ) { default: case ICAL_PARTSTAT_NEEDSACTION: status = Attendee::NeedsAction; break; case ICAL_PARTSTAT_ACCEPTED: status = Attendee::Accepted; break; case ICAL_PARTSTAT_DECLINED: status = Attendee::Declined; break; case ICAL_PARTSTAT_TENTATIVE: status = Attendee::Tentative; break; case ICAL_PARTSTAT_DELEGATED: status = Attendee::Delegated; break; case ICAL_PARTSTAT_COMPLETED: status = Attendee::Completed; break; case ICAL_PARTSTAT_INPROCESS: status = Attendee::InProcess; break; } } Attendee::Role role = Attendee::ReqParticipant; p = icalproperty_get_first_parameter( attendee, ICAL_ROLE_PARAMETER ); if ( p ) { icalparameter_role roleParameter = icalparameter_get_role( p ); switch( roleParameter ) { case ICAL_ROLE_CHAIR: role = Attendee::Chair; break; default: case ICAL_ROLE_REQPARTICIPANT: role = Attendee::ReqParticipant; break; case ICAL_ROLE_OPTPARTICIPANT: role = Attendee::OptParticipant; break; case ICAL_ROLE_NONPARTICIPANT: role = Attendee::NonParticipant; break; } } p = icalproperty_get_first_parameter( attendee, ICAL_X_PARAMETER ); while ( p ) { QString xname = QString( icalparameter_get_xname( p ) ).toUpper(); QString xvalue = QString::fromUtf8( icalparameter_get_xvalue( p ) ); if ( xname == "X-UID" ) { uid = xvalue; } p = icalproperty_get_next_parameter( attendee, ICAL_X_PARAMETER ); } Attendee *a = new Attendee( name, email, rsvp, status, role, uid ); p = icalproperty_get_first_parameter( attendee, ICAL_DELEGATEDTO_PARAMETER ); if ( p ) { a->setDelegate( icalparameter_get_delegatedto( p ) ); } p = icalproperty_get_first_parameter( attendee, ICAL_DELEGATEDFROM_PARAMETER ); if ( p ) { a->setDelegator( icalparameter_get_delegatedfrom( p ) ); } return a; } Person ICalFormatImpl::readOrganizer( icalproperty *organizer ) { QString email = QString::fromUtf8( icalproperty_get_organizer( organizer ) ); if ( email.startsWith( QLatin1String( "mailto:" ), Qt::CaseInsensitive ) ) { email = email.mid( 7 ); } QString cn; icalparameter *p = icalproperty_get_first_parameter( organizer, ICAL_CN_PARAMETER ); if ( p ) { cn = QString::fromUtf8( icalparameter_get_cn( p ) ); } Person org( cn, email ); // TODO: Treat sent-by, dir and language here, too return org; } Attachment *ICalFormatImpl::readAttachment( icalproperty *attach ) { Attachment *attachment = 0; const char *p; icalvalue *value = icalproperty_get_value( attach ); switch( icalvalue_isa( value ) ) { case ICAL_ATTACH_VALUE: { icalattach *a = icalproperty_get_attach( attach ); if ( !icalattach_get_is_url( a ) ) { p = (const char *)icalattach_get_data( a ); if ( p ) { attachment = new Attachment( p ); } } else { p = icalattach_get_url( a ); if ( p ) { attachment = new Attachment( QString::fromUtf8( p ) ); } } break; } case ICAL_BINARY_VALUE: { icalattach *a = icalproperty_get_attach( attach ); p = (const char *)icalattach_get_data( a ); if ( p ) { attachment = new Attachment( p ); } break; } case ICAL_URI_VALUE: p = icalvalue_get_uri( value ); attachment = new Attachment( QString::fromUtf8( p ) ); break; default: break; } if ( attachment ) { icalparameter *p = icalproperty_get_first_parameter( attach, ICAL_FMTTYPE_PARAMETER ); if ( p ) { attachment->setMimeType( QString( icalparameter_get_fmttype( p ) ) ); } p = icalproperty_get_first_parameter( attach, ICAL_X_PARAMETER ); while ( p ) { QString xname = QString( icalparameter_get_xname( p ) ).toUpper(); QString xvalue = QString::fromUtf8( icalparameter_get_xvalue( p ) ); if ( xname == "X-CONTENT-DISPOSITION" ) { attachment->setShowInline( xvalue.toLower() == "inline" ); } if ( xname == "X-LABEL" ) { attachment->setLabel( xvalue ); } if ( xname == "X-KONTACT-TYPE" ) { attachment->setLocal( xvalue.toLower() == "local" ); } p = icalproperty_get_next_parameter( attach, ICAL_X_PARAMETER ); } p = icalproperty_get_first_parameter( attach, ICAL_X_PARAMETER ); while ( p ) { if ( strncmp ( icalparameter_get_xname( p ), "X-LABEL", 7 ) == 0 ) { attachment->setLabel( icalparameter_get_xvalue( p ) ); } p = icalproperty_get_next_parameter( attach, ICAL_X_PARAMETER ); } } return attachment; } void ICalFormatImpl::readIncidence( icalcomponent *parent, Incidence *incidence, ICalTimeZones *tzlist ) { d->readIncidenceBase( parent, incidence ); icalproperty *p = icalcomponent_get_first_property( parent, ICAL_ANY_PROPERTY ); const char *text; int intvalue, inttext; icaldurationtype icalduration; KDateTime kdt; QStringList categories; while ( p ) { icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_CREATED_PROPERTY: incidence->setCreated( readICalDateTimeProperty( p, tzlist ) ); break; case ICAL_SEQUENCE_PROPERTY: // sequence intvalue = icalproperty_get_sequence( p ); incidence->setRevision( intvalue ); break; case ICAL_LASTMODIFIED_PROPERTY: // last modification UTC date/time incidence->setLastModified( readICalDateTimeProperty( p, tzlist ) ); break; case ICAL_DTSTART_PROPERTY: // start date and time kdt = readICalDateTimeProperty( p, tzlist ); incidence->setDtStart( kdt ); incidence->setAllDay( kdt.isDateOnly() ); break; case ICAL_DURATION_PROPERTY: // start date and time icalduration = icalproperty_get_duration( p ); incidence->setDuration( readICalDuration( icalduration ) ); break; case ICAL_DESCRIPTION_PROPERTY: // description { QString textStr = QString::fromUtf8( icalproperty_get_description( p ) ); if ( !textStr.isEmpty() ) { QString valStr = QString::fromUtf8( icalproperty_get_parameter_as_string( p, "X-KDE-TEXTFORMAT" ) ); if ( !valStr.compare( "HTML", Qt::CaseInsensitive ) ) { incidence->setDescription( textStr, true ); } else { incidence->setDescription( textStr, false ); } } } break; case ICAL_SUMMARY_PROPERTY: // summary { QString textStr = QString::fromUtf8( icalproperty_get_summary( p ) ); if ( !textStr.isEmpty() ) { QString valStr = QString::fromUtf8( icalproperty_get_parameter_as_string( p, "X-KDE-TEXTFORMAT" ) ); if ( !valStr.compare( "HTML", Qt::CaseInsensitive ) ) { incidence->setSummary( textStr, true ); } else { incidence->setSummary( textStr, false ); } } } break; case ICAL_LOCATION_PROPERTY: // location { if ( !icalproperty_get_value( p ) ) { //Fix for #191472. This is a pre-crash guard in case libical was //compiled in superstrict mode (--enable-icalerrors-are-fatal) //TODO: pre-crash guard other property getters too. break; } QString textStr = QString::fromUtf8( icalproperty_get_location( p ) ); if ( !textStr.isEmpty() ) { QString valStr = QString::fromUtf8( icalproperty_get_parameter_as_string( p, "X-KDE-TEXTFORMAT" ) ); if ( !valStr.compare( "HTML", Qt::CaseInsensitive ) ) { incidence->setLocation( textStr, true ); } else { incidence->setLocation( textStr, false ); } } } break; case ICAL_STATUS_PROPERTY: // status { Incidence::Status stat; switch ( icalproperty_get_status( p ) ) { case ICAL_STATUS_TENTATIVE: stat = Incidence::StatusTentative; break; case ICAL_STATUS_CONFIRMED: stat = Incidence::StatusConfirmed; break; case ICAL_STATUS_COMPLETED: stat = Incidence::StatusCompleted; break; case ICAL_STATUS_NEEDSACTION: stat = Incidence::StatusNeedsAction; break; case ICAL_STATUS_CANCELLED: stat = Incidence::StatusCanceled; break; case ICAL_STATUS_INPROCESS: stat = Incidence::StatusInProcess; break; case ICAL_STATUS_DRAFT: stat = Incidence::StatusDraft; break; case ICAL_STATUS_FINAL: stat = Incidence::StatusFinal; break; case ICAL_STATUS_X: incidence->setCustomStatus( QString::fromUtf8( icalvalue_get_x( icalproperty_get_value( p ) ) ) ); stat = Incidence::StatusX; break; case ICAL_STATUS_NONE: default: stat = Incidence::StatusNone; break; } if ( stat != Incidence::StatusX ) { incidence->setStatus( stat ); } break; } case ICAL_GEO_PROPERTY: // geo { icalgeotype geo = icalproperty_get_geo( p ); incidence->setGeoLatitude( geo.lat ); incidence->setGeoLongitude( geo.lon ); incidence->setHasGeo( true ); break; } case ICAL_PRIORITY_PROPERTY: // priority intvalue = icalproperty_get_priority( p ); if ( d->mCompat ) { intvalue = d->mCompat->fixPriority( intvalue ); } incidence->setPriority( intvalue ); break; case ICAL_CATEGORIES_PROPERTY: // categories { // We have always supported multiple CATEGORIES properties per component // even though the RFC seems to indicate only 1 is permitted. // We can't change that -- in order to retain backwards compatibility. text = icalproperty_get_categories( p ); const QString val = QString::fromUtf8( text ); foreach ( const QString &cat, val.split( ',', QString::SkipEmptyParts ) ) { // ensure no duplicates if ( !categories.contains( cat ) ) { categories.append( cat ); } } break; } case ICAL_RRULE_PROPERTY: readRecurrenceRule( p, incidence ); break; case ICAL_RDATE_PROPERTY: kdt = readICalDateTimeProperty( p, tzlist ); if ( kdt.isValid() ) { if ( kdt.isDateOnly() ) { incidence->recurrence()->addRDate( kdt.date() ); } else { incidence->recurrence()->addRDateTime( kdt ); } } else { // TODO: RDates as period are not yet implemented! } break; case ICAL_EXRULE_PROPERTY: readExceptionRule( p, incidence ); break; case ICAL_EXDATE_PROPERTY: kdt = readICalDateTimeProperty( p, tzlist ); if ( kdt.isDateOnly() ) { incidence->recurrence()->addExDate( kdt.date() ); } else { incidence->recurrence()->addExDateTime( kdt ); } break; case ICAL_CLASS_PROPERTY: inttext = icalproperty_get_class( p ); if ( inttext == ICAL_CLASS_PUBLIC ) { incidence->setSecrecy( Incidence::SecrecyPublic ); } else if ( inttext == ICAL_CLASS_CONFIDENTIAL ) { incidence->setSecrecy( Incidence::SecrecyConfidential ); } else { incidence->setSecrecy( Incidence::SecrecyPrivate ); } break; case ICAL_ATTACH_PROPERTY: // attachments incidence->addAttachment( readAttachment( p ) ); break; default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property( parent, ICAL_ANY_PROPERTY ); } // Set the scheduling ID const QString uid = incidence->customProperty( "LIBKCAL", "ID" ); if ( !uid.isNull() ) { // The UID stored in incidencebase is actually the scheduling ID // It has to be stored in the iCal UID component for compatibility // with other iCal applications incidence->setSchedulingID( incidence->uid() ); incidence->setUid( uid ); } // Now that recurrence and exception stuff is completely set up, // do any backwards compatibility adjustments. if ( incidence->recurs() && d->mCompat ) { d->mCompat->fixRecurrence( incidence ); } // add categories incidence->setCategories( categories ); // iterate through all alarms for ( icalcomponent *alarm = icalcomponent_get_first_component( parent, ICAL_VALARM_COMPONENT ); alarm; alarm = icalcomponent_get_next_component( parent, ICAL_VALARM_COMPONENT ) ) { readAlarm( alarm, incidence, tzlist ); } // Fix incorrect alarm settings by other applications (like outloook 9) if ( d->mCompat ) { d->mCompat->fixAlarms( incidence ); } } //@cond PRIVATE void ICalFormatImpl::Private::readIncidenceBase( icalcomponent *parent, IncidenceBase *incidenceBase ) { icalproperty *p = icalcomponent_get_first_property( parent, ICAL_ANY_PROPERTY ); while ( p ) { icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_UID_PROPERTY: // unique id incidenceBase->setUid( QString::fromUtf8( icalproperty_get_uid( p ) ) ); break; case ICAL_ORGANIZER_PROPERTY: // organizer incidenceBase->setOrganizer( mImpl->readOrganizer( p ) ); break; case ICAL_ATTENDEE_PROPERTY: // attendee incidenceBase->addAttendee( mImpl->readAttendee( p ) ); break; case ICAL_COMMENT_PROPERTY: incidenceBase->addComment( QString::fromUtf8( icalproperty_get_comment( p ) ) ); break; default: break; } p = icalcomponent_get_next_property( parent, ICAL_ANY_PROPERTY ); } // custom properties readCustomProperties( parent, incidenceBase ); } void ICalFormatImpl::Private::readCustomProperties( icalcomponent *parent, CustomProperties *properties ) { QMap customProperties; QString lastProperty; icalproperty *p = icalcomponent_get_first_property( parent, ICAL_X_PROPERTY ); while ( p ) { QString value = QString::fromUtf8( icalproperty_get_x( p ) ); const char *name = icalproperty_get_x_name( p ); if ( lastProperty != name ) { customProperties[name] = value; } else { customProperties[name] = customProperties[name].append( "," ).append( value ); } p = icalcomponent_get_next_property( parent, ICAL_X_PROPERTY ); lastProperty = name; } properties->setCustomProperties( customProperties ); } //@endcond void ICalFormatImpl::readRecurrenceRule( icalproperty *rrule, Incidence *incidence ) { Recurrence *recur = incidence->recurrence(); struct icalrecurrencetype r = icalproperty_get_rrule( rrule ); // dumpIcalRecurrence(r); RecurrenceRule *recurrule = new RecurrenceRule( /*incidence*/); recurrule->setStartDt( incidence->dtStart() ); readRecurrence( r, recurrule ); recur->addRRule( recurrule ); } void ICalFormatImpl::readExceptionRule( icalproperty *rrule, Incidence *incidence ) { struct icalrecurrencetype r = icalproperty_get_exrule( rrule ); // dumpIcalRecurrence(r); RecurrenceRule *recurrule = new RecurrenceRule( /*incidence*/); recurrule->setStartDt( incidence->dtStart() ); readRecurrence( r, recurrule ); Recurrence *recur = incidence->recurrence(); recur->addExRule( recurrule ); } void ICalFormatImpl::readRecurrence( const struct icalrecurrencetype &r, RecurrenceRule *recur ) { // Generate the RRULE string recur->setRRule( QString( icalrecurrencetype_as_string( const_cast( &r ) ) ) ); // Period switch ( r.freq ) { case ICAL_SECONDLY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rSecondly ); break; case ICAL_MINUTELY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rMinutely ); break; case ICAL_HOURLY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rHourly ); break; case ICAL_DAILY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rDaily ); break; case ICAL_WEEKLY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rWeekly ); break; case ICAL_MONTHLY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rMonthly ); break; case ICAL_YEARLY_RECURRENCE: recur->setRecurrenceType( RecurrenceRule::rYearly ); break; case ICAL_NO_RECURRENCE: default: recur->setRecurrenceType( RecurrenceRule::rNone ); } // Frequency recur->setFrequency( r.interval ); // Duration & End Date if ( !icaltime_is_null_time( r.until ) ) { icaltimetype t = r.until; recur->setEndDt( readICalUtcDateTime( 0, t ) ); } else { if ( r.count == 0 ) { recur->setDuration( -1 ); } else { recur->setDuration( r.count ); } } // Week start setting int wkst = ( r.week_start + 5 ) % 7 + 1; recur->setWeekStart( wkst ); // And now all BY* QList lst; int i; int index = 0; //@cond PRIVATE #define readSetByList( rrulecomp, setfunc ) \ index = 0; \ lst.clear(); \ while ( ( i = r.rrulecomp[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { \ lst.append( i ); \ } \ if ( !lst.isEmpty() ) { \ recur->setfunc( lst ); \ } //@endcond // BYSECOND, MINUTE and HOUR, MONTHDAY, YEARDAY, WEEKNUMBER, MONTH // and SETPOS are standard int lists, so we can treat them with the // same macro readSetByList( by_second, setBySeconds ); readSetByList( by_minute, setByMinutes ); readSetByList( by_hour, setByHours ); readSetByList( by_month_day, setByMonthDays ); readSetByList( by_year_day, setByYearDays ); readSetByList( by_week_no, setByWeekNumbers ); readSetByList( by_month, setByMonths ); readSetByList( by_set_pos, setBySetPos ); #undef readSetByList // BYDAY is a special case, since it's not an int list QList wdlst; short day; index=0; while ( ( day = r.by_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { RecurrenceRule::WDayPos pos; pos.setDay( ( icalrecurrencetype_day_day_of_week( day ) + 5 ) % 7 + 1 ); pos.setPos( icalrecurrencetype_day_position( day ) ); wdlst.append( pos ); } if ( !wdlst.isEmpty() ) { recur->setByDays( wdlst ); } // TODO: Store all X- fields of the RRULE inside the recurrence (so they are // preserved } void ICalFormatImpl::readAlarm( icalcomponent *alarm, Incidence *incidence, ICalTimeZones *tzlist ) { Alarm *ialarm = incidence->newAlarm(); ialarm->setRepeatCount( 0 ); ialarm->setEnabled( true ); // Determine the alarm's action type icalproperty *p = icalcomponent_get_first_property( alarm, ICAL_ACTION_PROPERTY ); Alarm::Type type = Alarm::Display; icalproperty_action action = ICAL_ACTION_DISPLAY; if ( !p ) { kDebug() << "Unknown type of alarm, using default"; // TODO: do something about unknown alarm type? } else { action = icalproperty_get_action( p ); switch ( action ) { case ICAL_ACTION_DISPLAY: type = Alarm::Display; break; case ICAL_ACTION_AUDIO: type = Alarm::Audio; break; case ICAL_ACTION_PROCEDURE: type = Alarm::Procedure; break; case ICAL_ACTION_EMAIL: type = Alarm::Email; break; default: break; // TODO: do something about invalid alarm type? } } ialarm->setType( type ); p = icalcomponent_get_first_property( alarm, ICAL_ANY_PROPERTY ); while ( p ) { icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_TRIGGER_PROPERTY: { icaltriggertype trigger = icalproperty_get_trigger( p ); if ( !icaltime_is_null_time( trigger.time ) ) { //set the trigger to a specific time (which is not in rfc2445, btw) ialarm->setTime( readICalUtcDateTime( p, trigger.time, tzlist ) ); } else { //set the trigger to an offset from the incidence start or end time. if ( !icaldurationtype_is_bad_duration( trigger.duration ) ) { Duration duration( readICalDuration( trigger.duration ) ); icalparameter *param = icalproperty_get_first_parameter( p, ICAL_RELATED_PARAMETER ); if ( param && icalparameter_get_related( param ) == ICAL_RELATED_END ) { ialarm->setEndOffset( duration ); } else { ialarm->setStartOffset( duration ); } } else { // a bad duration was encountered, just set a 0 duration from start ialarm->setStartOffset( Duration( 0 ) ); } } break; } case ICAL_DURATION_PROPERTY: { icaldurationtype duration = icalproperty_get_duration( p ); ialarm->setSnoozeTime( readICalDuration( duration ) ); break; } case ICAL_REPEAT_PROPERTY: ialarm->setRepeatCount( icalproperty_get_repeat( p ) ); break; case ICAL_DESCRIPTION_PROPERTY: { // Only in DISPLAY and EMAIL and PROCEDURE alarms QString description = QString::fromUtf8( icalproperty_get_description( p ) ); switch ( action ) { case ICAL_ACTION_DISPLAY: ialarm->setText( description ); break; case ICAL_ACTION_PROCEDURE: ialarm->setProgramArguments( description ); break; case ICAL_ACTION_EMAIL: ialarm->setMailText( description ); break; default: break; } break; } case ICAL_SUMMARY_PROPERTY: // Only in EMAIL alarm ialarm->setMailSubject( QString::fromUtf8( icalproperty_get_summary( p ) ) ); break; case ICAL_ATTENDEE_PROPERTY: { // Only in EMAIL alarm QString email = QString::fromUtf8( icalproperty_get_attendee( p ) ); if ( email.startsWith( QLatin1String( "mailto:" ), Qt::CaseInsensitive ) ) { email = email.mid( 7 ); } QString name; icalparameter *param = icalproperty_get_first_parameter( p, ICAL_CN_PARAMETER ); if ( param ) { name = QString::fromUtf8( icalparameter_get_cn( param ) ); } ialarm->addMailAddress( Person( name, email ) ); break; } case ICAL_ATTACH_PROPERTY: { // Only in AUDIO and EMAIL and PROCEDURE alarms Attachment *attach = readAttachment( p ); if ( attach && attach->isUri() ) { switch ( action ) { case ICAL_ACTION_AUDIO: ialarm->setAudioFile( attach->uri() ); break; case ICAL_ACTION_PROCEDURE: ialarm->setProgramFile( attach->uri() ); break; case ICAL_ACTION_EMAIL: ialarm->addMailAttachment( attach->uri() ); break; default: break; } } else { kDebug() << "Alarm attachments currently only support URIs," << "but no binary data"; } delete attach; break; } default: break; } p = icalcomponent_get_next_property( alarm, ICAL_ANY_PROPERTY ); } // custom properties d->readCustomProperties( alarm, ialarm ); // TODO: check for consistency of alarm properties } icaldatetimeperiodtype ICalFormatImpl::writeICalDatePeriod( const QDate &date ) { icaldatetimeperiodtype t; t.time = writeICalDate( date ); t.period = icalperiodtype_null_period(); return t; } icaltimetype ICalFormatImpl::writeICalDate( const QDate &date ) { icaltimetype t = icaltime_null_time(); t.year = date.year(); t.month = date.month(); t.day = date.day(); t.hour = 0; t.minute = 0; t.second = 0; t.is_date = 1; t.is_utc = 0; t.zone = 0; return t; } icaltimetype ICalFormatImpl::writeICalDateTime( const KDateTime &datetime ) { icaltimetype t = icaltime_null_time(); t.year = datetime.date().year(); t.month = datetime.date().month(); t.day = datetime.date().day(); t.hour = datetime.time().hour(); t.minute = datetime.time().minute(); t.second = datetime.time().second(); t.is_date = 0; t.zone = 0; // zone is NOT set t.is_utc = datetime.isUtc() ? 1 : 0; // _dumpIcaltime( t ); return t; } icalproperty *ICalFormatImpl::writeICalDateTimeProperty( const icalproperty_kind type, const KDateTime &dt, ICalTimeZones *tzlist, ICalTimeZones *tzUsedList ) { icaltimetype t; switch ( type ) { case ICAL_DTSTAMP_PROPERTY: case ICAL_CREATED_PROPERTY: case ICAL_LASTMODIFIED_PROPERTY: t = writeICalDateTime( dt.toUtc() ); break; default: t = writeICalDateTime( dt ); break; } icalproperty *p; switch ( type ) { case ICAL_DTSTAMP_PROPERTY: p = icalproperty_new_dtstamp( t ); break; case ICAL_CREATED_PROPERTY: p = icalproperty_new_created( t ); break; case ICAL_LASTMODIFIED_PROPERTY: p = icalproperty_new_lastmodified( t ); break; case ICAL_DTSTART_PROPERTY: // start date and time p = icalproperty_new_dtstart( t ); break; case ICAL_DTEND_PROPERTY: // end date and time p = icalproperty_new_dtend( t ); break; case ICAL_DUE_PROPERTY: p = icalproperty_new_due( t ); break; case ICAL_RECURRENCEID_PROPERTY: p = icalproperty_new_recurrenceid( t ); break; case ICAL_EXDATE_PROPERTY: p = icalproperty_new_exdate( t ); break; default: { icaldatetimeperiodtype tp; tp.time = t; tp.period = icalperiodtype_null_period(); switch ( type ) { case ICAL_RDATE_PROPERTY: p = icalproperty_new_rdate( tp ); break; default: return 0; } } } KTimeZone ktz; if ( !t.is_utc ) { ktz = dt.timeZone(); } if ( ktz.isValid() ) { if ( tzlist ) { ICalTimeZone tz = tzlist->zone( ktz.name() ); if ( !tz.isValid() ) { // The time zone isn't in the list of known zones for the calendar // - add it to the calendar's zone list ICalTimeZone tznew( ktz ); tzlist->add( tznew ); tz = tznew; } if ( tzUsedList ) { tzUsedList->add( tz ); } } icalproperty_add_parameter( p, icalparameter_new_tzid( ktz.name().toUtf8() ) ); } return p; } KDateTime ICalFormatImpl::readICalDateTime( icalproperty *p, const icaltimetype &t, ICalTimeZones *tzlist, bool utc ) { // kDebug(); // _dumpIcaltime( t ); KDateTime::Spec timeSpec; if ( t.is_utc || t.zone == icaltimezone_get_utc_timezone() ) { timeSpec = KDateTime::UTC; // the time zone is UTC utc = false; // no need to convert to UTC } else { if ( !tzlist ) { utc = true; // should be UTC, but it isn't } icalparameter *param = p ? icalproperty_get_first_parameter( p, ICAL_TZID_PARAMETER ) : 0; const char *tzid = param ? icalparameter_get_tzid( param ) : 0; if ( !tzid ) { timeSpec = KDateTime::ClockTime; } else { QString tzidStr = QString::fromUtf8( tzid ); ICalTimeZone tz; if ( tzlist ) { tz = tzlist->zone( tzidStr ); } if ( !tz.isValid() ) { // The time zone is not in the existing list for the calendar. // Try to read it from the system or libical databases. ICalTimeZoneSource tzsource; ICalTimeZone newtz = tzsource.standardZone( tzidStr ); if ( newtz.isValid() && tzlist ) { tzlist->add( newtz ); } tz = newtz; } timeSpec = tz.isValid() ? KDateTime::Spec( tz ) : KDateTime::LocalZone; } } KDateTime result( QDate( t.year, t.month, t.day ), QTime( t.hour, t.minute, t.second ), timeSpec ); return utc ? result.toUtc() : result; } QDate ICalFormatImpl::readICalDate( icaltimetype t ) { return QDate( t.year, t.month, t.day ); } KDateTime ICalFormatImpl::readICalDateTimeProperty( icalproperty *p, ICalTimeZones *tzlist, bool utc ) { icaldatetimeperiodtype tp; icalproperty_kind kind = icalproperty_isa( p ); switch ( kind ) { case ICAL_CREATED_PROPERTY: // UTC date/time tp.time = icalproperty_get_created( p ); utc = true; break; case ICAL_LASTMODIFIED_PROPERTY: // last modification UTC date/time tp.time = icalproperty_get_lastmodified( p ); utc = true; break; case ICAL_DTSTART_PROPERTY: // start date and time (UTC for freebusy) tp.time = icalproperty_get_dtstart( p ); break; case ICAL_DTEND_PROPERTY: // end date and time (UTC for freebusy) tp.time = icalproperty_get_dtend( p ); break; case ICAL_DUE_PROPERTY: // due date/time tp.time = icalproperty_get_due( p ); break; case ICAL_COMPLETED_PROPERTY: // UTC completion date/time tp.time = icalproperty_get_completed( p ); utc = true; break; case ICAL_RECURRENCEID_PROPERTY: tp.time = icalproperty_get_recurrenceid( p ); break; case ICAL_EXDATE_PROPERTY: tp.time = icalproperty_get_exdate( p ); break; default: switch ( kind ) { case ICAL_RDATE_PROPERTY: tp = icalproperty_get_rdate( p ); break; default: return KDateTime(); } if ( !icaltime_is_valid_time( tp.time ) ) { return KDateTime(); // a time period was found (not implemented yet) } break; } if ( tp.time.is_date ) { return KDateTime( readICalDate( tp.time ), KDateTime::Spec::ClockTime() ); } else { return readICalDateTime( p, tp.time, tzlist, utc ); } } icaldurationtype ICalFormatImpl::writeICalDuration( const Duration &duration ) { // should be able to use icaldurationtype_from_int(), except we know // that some older tools do not properly support weeks. So we never // set a week duration, only days icaldurationtype d; int value = duration.value(); d.is_neg = ( value < 0 ) ? 1 : 0; if ( value < 0 ) { value = -value; } if ( duration.isDaily() ) { d.weeks = 0; d.days = value; d.hours = d.minutes = d.seconds = 0; } else { d.weeks = 0; d.days = value / gSecondsPerDay; value %= gSecondsPerDay; d.hours = value / gSecondsPerHour; value %= gSecondsPerHour; d.minutes = value / gSecondsPerMinute; value %= gSecondsPerMinute; d.seconds = value; } return d; } Duration ICalFormatImpl::readICalDuration( icaldurationtype d ) { int days = d.weeks * 7; days += d.days; int seconds = d.hours * gSecondsPerHour; seconds += d.minutes * gSecondsPerMinute; seconds += d.seconds; if ( seconds ) { seconds += days * gSecondsPerDay; if ( d.is_neg ) { seconds = -seconds; } return Duration( seconds, Duration::Seconds ); } else { if ( d.is_neg ) { days = -days; } return Duration( days, Duration::Days ); } } icalcomponent *ICalFormatImpl::createCalendarComponent( Calendar *cal ) { icalcomponent *calendar; // Root component calendar = icalcomponent_new( ICAL_VCALENDAR_COMPONENT ); icalproperty *p; // Product Identifier p = icalproperty_new_prodid( CalFormat::productId().toUtf8() ); icalcomponent_add_property( calendar, p ); // iCalendar version (2.0) p = icalproperty_new_version( const_cast(_ICAL_VERSION) ); icalcomponent_add_property( calendar, p ); // Add time zone if ( cal && cal->timeZones() ) { const ICalTimeZones::ZoneMap zmaps = cal->timeZones()->zones(); for ( ICalTimeZones::ZoneMap::ConstIterator it=zmaps.constBegin(); it != zmaps.constEnd(); ++it ) { icaltimezone *icaltz = (*it).icalTimezone(); if ( !icaltz ) { kError() << "bad time zone"; } else { icalcomponent *tz = icalcomponent_new_clone( icaltimezone_get_component( icaltz ) ); icalcomponent_add_component( calendar, tz ); icaltimezone_free( icaltz, 1 ); } } } // Custom properties if( cal != 0 ) { d->writeCustomProperties( calendar, cal ); } return calendar; } // take a raw vcalendar (i.e. from a file on disk, clipboard, etc. etc. // and break it down from its tree-like format into the dictionary format // that is used internally in the ICalFormatImpl. bool ICalFormatImpl::populate( Calendar *cal, icalcomponent *calendar ) { // this function will populate the caldict dictionary and other event // lists. It turns vevents into Events and then inserts them. if ( !calendar ) { return false; } // TODO: check for METHOD icalproperty *p; p = icalcomponent_get_first_property( calendar, ICAL_PRODID_PROPERTY ); if ( !p ) { kDebug() << "No PRODID property found"; d->mLoadedProductId = ""; } else { d->mLoadedProductId = QString::fromUtf8( icalproperty_get_prodid( p ) ); delete d->mCompat; d->mCompat = CompatFactory::createCompat( d->mLoadedProductId ); } p = icalcomponent_get_first_property( calendar, ICAL_VERSION_PROPERTY ); if ( !p ) { kDebug() << "No VERSION property found"; d->mParent->setException( new ErrorFormat( ErrorFormat::CalVersionUnknown ) ); return false; } else { const char *version = icalproperty_get_version( p ); if ( !version ) { kDebug() << "No VERSION property found"; d->mParent->setException( new ErrorFormat( ErrorFormat::CalVersionUnknown, i18n( "No VERSION property found" ) ) ); return false; } if ( strcmp( version, "1.0" ) == 0 ) { kDebug() << "Expected iCalendar, got vCalendar"; d->mParent->setException( new ErrorFormat( ErrorFormat::CalVersion1, i18n( "Expected iCalendar, got vCalendar format" ) ) ); return false; } else if ( strcmp( version, "2.0" ) != 0 ) { kDebug() << "Expected iCalendar, got unknown format"; d->mParent->setException( new ErrorFormat( ErrorFormat::CalVersionUnknown, i18n( "Expected iCalendar, got unknown format" ) ) ); return false; } } // Populate the calendar's time zone collection with all VTIMEZONE components ICalTimeZones *tzlist = cal->timeZones(); ICalTimeZoneSource tzs; tzs.parse( calendar, *tzlist ); // custom properties d->readCustomProperties( calendar, cal ); // Store all events with a relatedTo property in a list for post-processing d->mEventsRelate.clear(); d->mTodosRelate.clear(); // TODO: make sure that only actually added events go to this lists. icalcomponent *c; // Iterate through all todos + cal->beginBatchAdding(); + c = icalcomponent_get_first_component( calendar, ICAL_VTODO_COMPONENT ); while ( c ) { Todo *todo = readTodo( c, tzlist ); if ( todo ) { Todo *old = cal->todo( todo->uid() ); if ( old ) { cal->deleteTodo( old ); d->mTodosRelate.removeAll( old ); } cal->addTodo( todo ); } c = icalcomponent_get_next_component( calendar, ICAL_VTODO_COMPONENT ); } // Iterate through all events c = icalcomponent_get_first_component( calendar, ICAL_VEVENT_COMPONENT ); while ( c ) { Event *event = readEvent( c, tzlist ); if ( event ) { Event *old = cal->event( event->uid() ); if ( old ) { cal->deleteEvent( old ); d->mEventsRelate.removeAll( old ); } cal->addEvent( event ); } c = icalcomponent_get_next_component( calendar, ICAL_VEVENT_COMPONENT ); } // Iterate through all journals c = icalcomponent_get_first_component( calendar, ICAL_VJOURNAL_COMPONENT ); while ( c ) { Journal *journal = readJournal( c, tzlist ); if ( journal ) { Journal *old = cal->journal( journal->uid() ); if ( old ) { cal->deleteJournal( old ); } cal->addJournal( journal ); } c = icalcomponent_get_next_component( calendar, ICAL_VJOURNAL_COMPONENT ); } + cal->endBatchAdding(); + // Post-Process list of events with relations, put Event objects in relation Event::List::ConstIterator eIt; for ( eIt = d->mEventsRelate.constBegin(); eIt != d->mEventsRelate.constEnd(); ++eIt ) { (*eIt)->setRelatedTo( cal->incidence( (*eIt)->relatedToUid() ) ); } Todo::List::ConstIterator tIt; for ( tIt = d->mTodosRelate.constBegin(); tIt != d->mTodosRelate.constEnd(); ++tIt ) { (*tIt)->setRelatedTo( cal->incidence( (*tIt)->relatedToUid() ) ); } // TODO: Remove any previous time zones no longer referenced in the calendar return true; } QString ICalFormatImpl::extractErrorProperty( icalcomponent *c ) { QString errorMessage; icalproperty *error; error = icalcomponent_get_first_property( c, ICAL_XLICERROR_PROPERTY ); while ( error ) { errorMessage += icalproperty_get_xlicerror( error ); errorMessage += '\n'; error = icalcomponent_get_next_property( c, ICAL_XLICERROR_PROPERTY ); } return errorMessage; } void ICalFormatImpl::dumpIcalRecurrence( icalrecurrencetype r ) { int i; kDebug() << " Freq:" << r.freq; kDebug() << " Until:" << icaltime_as_ical_string( r.until ); kDebug() << " Count:" << r.count; if ( r.by_day[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Day: "; while ( ( i = r.by_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } kDebug() << out; } if ( r.by_month_day[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Month Day: "; while ( ( i = r.by_month_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } kDebug() << out; } if ( r.by_year_day[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Year Day: "; while ( ( i = r.by_year_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } kDebug() << out; } if ( r.by_month[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Month: "; while ( ( i = r.by_month[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } kDebug() << out; } if ( r.by_set_pos[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Set Pos: "; while ( ( i = r.by_set_pos[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { kDebug() << "=========" << i; out.append( QString::number( i ) + ' ' ); } kDebug() << out; } } icalcomponent *ICalFormatImpl::createScheduleComponent( IncidenceBase *incidence, iTIPMethod method ) { icalcomponent *message = createCalendarComponent(); // Create VTIMEZONE components for this incidence ICalTimeZones zones; if ( incidence ) { if ( incidence->type() == "Event" ) { Event *ev = static_cast( incidence ); if ( ev ) { if ( ev->dtStart().isValid() ) { zones.add( ICalTimeZone( ev->dtStart().timeZone() ) ); } if ( ev->hasEndDate() && ev->dtEnd().isValid() ) { zones.add( ICalTimeZone( ev->dtEnd().timeZone() ) ); } } } else if ( incidence->type() == "Todo" ) { Todo *t = static_cast( incidence ); if ( t ) { if ( t->hasStartDate() && t->dtStart().isValid() ) { zones.add( ICalTimeZone( t->dtStart( true ).timeZone() ) ); } if ( t->hasDueDate() && t->dtDue().isValid() ) { zones.add( ICalTimeZone( t->dtDue().timeZone() ) ); } } } else if ( incidence->type() == "Journal" ) { Journal *j = static_cast( incidence ); if ( j ) { if ( j->dtStart().isValid() ) { zones.add( ICalTimeZone( j->dtStart().timeZone() ) ); } } } const ICalTimeZones::ZoneMap zmaps = zones.zones(); for ( ICalTimeZones::ZoneMap::ConstIterator it=zmaps.constBegin(); it != zmaps.constEnd(); ++it ) { icaltimezone *icaltz = (*it).icalTimezone(); if ( !icaltz ) { kError() << "bad time zone"; } else { icalcomponent *tz = icalcomponent_new_clone( icaltimezone_get_component( icaltz ) ); icalcomponent_add_component( message, tz ); icaltimezone_free( icaltz, 1 ); } } } icalproperty_method icalmethod = ICAL_METHOD_NONE; switch (method) { case iTIPPublish: icalmethod = ICAL_METHOD_PUBLISH; break; case iTIPRequest: icalmethod = ICAL_METHOD_REQUEST; break; case iTIPRefresh: icalmethod = ICAL_METHOD_REFRESH; break; case iTIPCancel: icalmethod = ICAL_METHOD_CANCEL; break; case iTIPAdd: icalmethod = ICAL_METHOD_ADD; break; case iTIPReply: icalmethod = ICAL_METHOD_REPLY; break; case iTIPCounter: icalmethod = ICAL_METHOD_COUNTER; break; case iTIPDeclineCounter: icalmethod = ICAL_METHOD_DECLINECOUNTER; break; default: kDebug() << "Unknown method"; return message; } icalcomponent_add_property( message, icalproperty_new_method( icalmethod ) ); icalcomponent *inc = writeIncidence( incidence, method ); /* * RFC 2446 states in section 3.4.3 ( REPLY to a VTODO ), that * a REQUEST-STATUS property has to be present. For the other two, event and * free busy, it can be there, but is optional. Until we do more * fine grained handling, assume all is well. Note that this is the * status of the _request_, not the attendee. Just to avoid confusion. * - till */ if ( icalmethod == ICAL_METHOD_REPLY ) { struct icalreqstattype rst; rst.code = ICAL_2_0_SUCCESS_STATUS; rst.desc = 0; rst.debug = 0; icalcomponent_add_property( inc, icalproperty_new_requeststatus( rst ) ); } icalcomponent_add_component( message, inc ); return message; } diff --git a/kcal/incidence.cpp b/kcal/incidence.cpp index 1dcf47e06..cfe2c4d25 100644 --- a/kcal/incidence.cpp +++ b/kcal/incidence.cpp @@ -1,1012 +1,1008 @@ /* This file is part of the kcal library. Copyright (c) 2001 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer 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. */ /** @file This file is part of the API for handling calendar data and defines the Incidence class. @brief Provides the class common to non-FreeBusy (Events, To-dos, Journals) calendar components known as incidences. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "incidence.h" #include "calformat.h" #include #include #include #include -#include // for Qt::escape() and Qt::mightBeRichText() +#include // for Qt::escape() and Qt::mightBeRichText() using namespace KCal; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KCal::Incidence::Private { public: Private() : mDescriptionIsRich( false ), mSummaryIsRich( false ), mLocationIsRich( false ), mRecurrence( 0 ), mStatus( StatusNone ), mSecrecy( SecrecyPublic ), mPriority( 0 ), mRelatedTo( 0 ), mGeoLatitude( 0 ), mGeoLongitude( 0 ), mHasGeo( false ) { mAlarms.setAutoDelete( true ); mAttachments.setAutoDelete( true ); } Private( const Private &p ) : mCreated( p.mCreated ), mRevision( p.mRevision ), mDescription( p.mDescription ), mDescriptionIsRich( p.mDescriptionIsRich ), mSummary( p.mSummary ), mSummaryIsRich( p.mSummaryIsRich ), mLocation( p.mLocation ), mLocationIsRich( p.mLocationIsRich ), mCategories( p.mCategories ), + mRecurrence( p.mRecurrence ), mResources( p.mResources ), mStatus( p.mStatus ), mStatusString( p.mStatusString ), mSecrecy( p.mSecrecy ), mPriority( p.mPriority ), mSchedulingID( p.mSchedulingID ), - mRelatedTo( 0 ), + mRelatedTo( p.mRelatedTo ), mRelatedToUid( p.mRelatedToUid ), mGeoLatitude( p.mGeoLatitude ), mGeoLongitude( p.mGeoLongitude ), mHasGeo( p.mHasGeo ) -// TODO: reenable attributes currently commented out. -// Incidence *mRelatedTo; Incidence *mRelatedTo; -// Incidence::List mRelations; Incidence::List mRelations; { mAlarms.setAutoDelete( true ); mAttachments.setAutoDelete( true ); } void clear() { mAlarms.clearAll(); mAttachments.clearAll(); delete mRecurrence; } KDateTime mCreated; // creation datetime int mRevision; // revision number QString mDescription; // description string bool mDescriptionIsRich; // description string is richtext. QString mSummary; // summary string bool mSummaryIsRich; // summary string is richtext. QString mLocation; // location string bool mLocationIsRich; // location string is richtext. QStringList mCategories; // category list mutable Recurrence *mRecurrence; // recurrence Attachment::List mAttachments; // attachments list Alarm::List mAlarms; // alarms list QStringList mResources; // resources list (not calendar resources) Status mStatus; // status QString mStatusString; // status string, for custom status Secrecy mSecrecy; // secrecy int mPriority; // priority: 1 = highest, 2 = less, etc. QString mSchedulingID; // ID for scheduling mails Incidence *mRelatedTo; // incidence this is related to QString mRelatedToUid; // incidence (by Uid) this is related to - Incidence::List mRelations; // a list of incidences this is related to + Incidence::List mRelations; // a list of incidences related to this float mGeoLatitude; // Specifies latitude in decimal degrees float mGeoLongitude; // Specifies longitude in decimal degrees bool mHasGeo; // if incidence has geo data }; //@endcond Incidence::Incidence() : IncidenceBase(), d( new KCal::Incidence::Private ) { recreate(); } Incidence::Incidence( const Incidence &i ) : IncidenceBase( i ), Recurrence::RecurrenceObserver(), d( new KCal::Incidence::Private( *i.d ) ) { init( i ); } void Incidence::init( const Incidence &i ) { -// TODO: reenable attributes currently commented out. d->mRevision = i.d->mRevision; d->mCreated = i.d->mCreated; d->mDescription = i.d->mDescription; d->mSummary = i.d->mSummary; d->mCategories = i.d->mCategories; -// Incidence *mRelatedTo; Incidence *mRelatedTo; - d->mRelatedTo = 0; + d->mRelatedTo = i.d->mRelatedTo; d->mRelatedToUid = i.d->mRelatedToUid; -// Incidence::List mRelations; Incidence::List mRelations; + d->mRelations = i.d->mRelations; d->mResources = i.d->mResources; d->mStatusString = i.d->mStatusString; d->mStatus = i.d->mStatus; d->mSecrecy = i.d->mSecrecy; d->mPriority = i.d->mPriority; d->mLocation = i.d->mLocation; d->mGeoLatitude = i.d->mGeoLatitude; d->mGeoLongitude = i.d->mGeoLongitude; d->mHasGeo = i.d->mHasGeo; // Alarms and Attachments are stored in ListBase<...>, which is a QValueList<...*>. // We need to really duplicate the objects stored therein, otherwise deleting // i will also delete all attachments from this object (setAutoDelete...) foreach ( Alarm *alarm, i.d->mAlarms ) { Alarm *b = new Alarm( *alarm ); b->setParent( this ); d->mAlarms.append( b ); } foreach ( Attachment *attachment, i.d->mAttachments ) { Attachment *a = new Attachment( *attachment ); d->mAttachments.append( a ); } if ( i.d->mRecurrence ) { d->mRecurrence = new Recurrence( *( i.d->mRecurrence ) ); d->mRecurrence->addObserver( this ); } else { d->mRecurrence = 0; } } Incidence::~Incidence() { Incidence::List relations = d->mRelations; foreach ( Incidence *incidence, relations ) { if ( incidence->relatedTo() == this ) { incidence->setRelatedTo( 0 ); } } if ( relatedTo() ) { relatedTo()->removeRelation( this ); } delete d->mRecurrence; delete d; } //@cond PRIVATE // A string comparison that considers that null and empty are the same static bool stringCompare( const QString &s1, const QString &s2 ) { return ( s1.isEmpty() && s2.isEmpty() ) || ( s1 == s2 ); } //@endcond Incidence &Incidence::operator=( const Incidence &other ) { // check for self assignment if ( &other == this ) { return *this; } d->clear(); //TODO: should relations be cleared out, as in destructor??? IncidenceBase::operator=( other ); init( other ); return *this; } bool Incidence::operator==( const Incidence &i2 ) const { if ( alarms().count() != i2.alarms().count() ) { return false; // no need to check further } Alarm::List::ConstIterator a1 = alarms().constBegin(); Alarm::List::ConstIterator a1end = alarms().constEnd(); Alarm::List::ConstIterator a2 = i2.alarms().begin(); Alarm::List::ConstIterator a2end = i2.alarms().constEnd(); for ( ; a1 != a1end && a2 != a2end; ++a1, ++a2 ) { if ( **a1 == **a2 ) { continue; } else { return false; } } if ( !IncidenceBase::operator==( i2 ) ) { return false; } bool recurrenceEqual = ( d->mRecurrence == 0 && i2.d->mRecurrence == 0 ); if ( !recurrenceEqual ) { recurrenceEqual = d->mRecurrence != 0 && i2.d->mRecurrence != 0 && *d->mRecurrence == *i2.d->mRecurrence; } return recurrenceEqual && created() == i2.created() && stringCompare( description(), i2.description() ) && stringCompare( summary(), i2.summary() ) && categories() == i2.categories() && // no need to compare mRelatedTo stringCompare( relatedToUid(), i2.relatedToUid() ) && relations() == i2.relations() && attachments() == i2.attachments() && resources() == i2.resources() && d->mStatus == i2.d->mStatus && ( d->mStatus == StatusNone || stringCompare( d->mStatusString, i2.d->mStatusString ) ) && secrecy() == i2.secrecy() && priority() == i2.priority() && stringCompare( location(), i2.location() ) && stringCompare( schedulingID(), i2.schedulingID() ); } void Incidence::recreate() { KDateTime nowUTC = KDateTime::currentUtcDateTime(); setCreated( nowUTC ); setUid( CalFormat::createUniqueId() ); setSchedulingID( QString() ); setRevision( 0 ); setLastModified( nowUTC ); } void Incidence::setReadOnly( bool readOnly ) { IncidenceBase::setReadOnly( readOnly ); if ( d->mRecurrence ) { d->mRecurrence->setRecurReadOnly( readOnly ); } } void Incidence::setAllDay( bool allDay ) { if ( mReadOnly ) { return; } if ( recurrence() ) { recurrence()->setAllDay( allDay ); } IncidenceBase::setAllDay( allDay ); } void Incidence::setCreated( const KDateTime &created ) { if ( mReadOnly ) { return; } d->mCreated = created.toUtc(); // FIXME: Shouldn't we call updated for the creation date, too? // updated(); } KDateTime Incidence::created() const { return d->mCreated; } void Incidence::setRevision( int rev ) { if ( mReadOnly ) { return; } d->mRevision = rev; updated(); } int Incidence::revision() const { return d->mRevision; } void Incidence::setDtStart( const KDateTime &dt ) { if ( d->mRecurrence ) { d->mRecurrence->setStartDateTime( dt ); d->mRecurrence->setAllDay( allDay() ); } IncidenceBase::setDtStart( dt ); } KDateTime Incidence::dtEnd() const { return KDateTime(); } void Incidence::shiftTimes( const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec ) { IncidenceBase::shiftTimes( oldSpec, newSpec ); if ( d->mRecurrence ) { d->mRecurrence->shiftTimes( oldSpec, newSpec ); } for ( int i = 0, end = d->mAlarms.count(); i < end; ++i ) { d->mAlarms[i]->shiftTimes( oldSpec, newSpec ); } } void Incidence::setDescription( const QString &description, bool isRich ) { if ( mReadOnly ) { return; } d->mDescription = description; d->mDescriptionIsRich = isRich; updated(); } void Incidence::setDescription( const QString &description ) { setDescription( description, Qt::mightBeRichText( description ) ); } QString Incidence::description() const { return d->mDescription; } QString Incidence::richDescription() const { if ( descriptionIsRich() ) { return d->mDescription; } else { return Qt::escape( d->mDescription ).replace( '\n', "
" ); } } bool Incidence::descriptionIsRich() const { return d->mDescriptionIsRich; } void Incidence::setSummary( const QString &summary, bool isRich ) { if ( mReadOnly ) { return; } d->mSummary = summary; d->mSummaryIsRich = isRich; updated(); } void Incidence::setSummary( const QString &summary ) { setSummary( summary, Qt::mightBeRichText( summary ) ); } QString Incidence::summary() const { return d->mSummary; } QString Incidence::richSummary() const { if ( summaryIsRich() ) { return d->mSummary; } else { return Qt::escape( d->mSummary ).replace( '\n', "
" ); } } bool Incidence::summaryIsRich() const { return d->mSummaryIsRich; } void Incidence::setCategories( const QStringList &categories ) { if ( mReadOnly ) { return; } d->mCategories = categories; updated(); } void Incidence::setCategories( const QString &catStr ) { if ( mReadOnly ) { return; } d->mCategories.clear(); if ( catStr.isEmpty() ) { return; } d->mCategories = catStr.split( ',' ); QStringList::Iterator it; for ( it = d->mCategories.begin();it != d->mCategories.end(); ++it ) { *it = (*it).trimmed(); } updated(); } QStringList Incidence::categories() const { return d->mCategories; } QString Incidence::categoriesStr() const { return d->mCategories.join( "," ); } void Incidence::setRelatedToUid( const QString &relatedToUid ) { if ( d->mRelatedToUid == relatedToUid ) { return; } d->mRelatedToUid = relatedToUid; updated(); } QString Incidence::relatedToUid() const { return d->mRelatedToUid; } void Incidence::setRelatedTo( Incidence *incidence ) { if ( d->mRelatedTo == incidence ) { return; } if ( d->mRelatedTo ) { d->mRelatedTo->removeRelation( this ); } d->mRelatedTo = incidence; if ( d->mRelatedTo ) { d->mRelatedTo->addRelation( this ); if ( d->mRelatedTo->uid() != d->mRelatedToUid ) { setRelatedToUid( d->mRelatedTo->uid() ); } } else { setRelatedToUid( QString() ); } } Incidence *Incidence::relatedTo() const { return d->mRelatedTo; } Incidence::List Incidence::relations() const { return d->mRelations; } void Incidence::addRelation( Incidence *incidence ) { if ( !d->mRelations.contains( incidence ) ) { d->mRelations.append( incidence ); } } void Incidence::removeRelation( Incidence *incidence ) { d->mRelations.removeRef( incidence ); if ( d->mRelatedToUid == incidence->uid() ) { d->mRelatedToUid.clear(); } // if (incidence->getRelatedTo() == this) incidence->setRelatedTo(0); } // %%%%%%%%%%%% Recurrence-related methods %%%%%%%%%%%%%%%%%%%% Recurrence *Incidence::recurrence() const { if ( !d->mRecurrence ) { d->mRecurrence = new Recurrence(); d->mRecurrence->setStartDateTime( IncidenceBase::dtStart() ); d->mRecurrence->setAllDay( allDay() ); d->mRecurrence->setRecurReadOnly( mReadOnly ); d->mRecurrence->addObserver( const_cast( this ) ); } return d->mRecurrence; } void Incidence::clearRecurrence() { delete d->mRecurrence; d->mRecurrence = 0; } ushort Incidence::recurrenceType() const { if ( d->mRecurrence ) { return d->mRecurrence->recurrenceType(); } else { return Recurrence::rNone; } } bool Incidence::recurs() const { if ( d->mRecurrence ) { return d->mRecurrence->recurs(); } else { return false; } } bool Incidence::recursOn( const QDate &date, const KDateTime::Spec &timeSpec ) const { return d->mRecurrence && d->mRecurrence->recursOn( date, timeSpec ); } bool Incidence::recursAt( const KDateTime &qdt ) const { return d->mRecurrence && d->mRecurrence->recursAt( qdt ); } QList Incidence::startDateTimesForDate( const QDate &date, const KDateTime::Spec &timeSpec ) const { KDateTime start = dtStart(); KDateTime end = endDateRecurrenceBase(); QList result; // TODO_Recurrence: Also work if only due date is given... if ( !start.isValid() && ! end.isValid() ) { return result; } // if the incidence doesn't recur, KDateTime kdate( date, timeSpec ); if ( !recurs() ) { if ( !( start > kdate || end < kdate ) ) { result << start; } return result; } int days = start.daysTo( end ); // Account for possible recurrences going over midnight, while the original event doesn't QDate tmpday( date.addDays( -days - 1 ) ); KDateTime tmp; while ( tmpday <= date ) { if ( recurrence()->recursOn( tmpday, timeSpec ) ) { QList times = recurrence()->recurTimesOn( tmpday, timeSpec ); foreach ( const QTime &time, times ) { tmp = KDateTime( tmpday, time, start.timeSpec() ); if ( endDateForStart( tmp ) >= kdate ) { result << tmp; } } } tmpday = tmpday.addDays( 1 ); } return result; } QList Incidence::startDateTimesForDateTime( const KDateTime &datetime ) const { KDateTime start = dtStart(); KDateTime end = endDateRecurrenceBase(); QList result; // TODO_Recurrence: Also work if only due date is given... if ( !start.isValid() && ! end.isValid() ) { return result; } // if the incidence doesn't recur, if ( !recurs() ) { if ( !( start > datetime || end < datetime ) ) { result << start; } return result; } int days = start.daysTo( end ); // Account for possible recurrences going over midnight, while the original event doesn't QDate tmpday( datetime.date().addDays( -days - 1 ) ); KDateTime tmp; while ( tmpday <= datetime.date() ) { if ( recurrence()->recursOn( tmpday, datetime.timeSpec() ) ) { // Get the times during the day (in start date's time zone) when recurrences happen QList times = recurrence()->recurTimesOn( tmpday, start.timeSpec() ); foreach ( const QTime &time, times ) { tmp = KDateTime( tmpday, time, start.timeSpec() ); if ( !( tmp > datetime || endDateForStart( tmp ) < datetime ) ) { result << tmp; } } } tmpday = tmpday.addDays( 1 ); } return result; } KDateTime Incidence::endDateForStart( const KDateTime &startDt ) const { KDateTime start = dtStart(); KDateTime end = endDateRecurrenceBase(); if ( !end.isValid() ) { return start; } if ( !start.isValid() ) { return end; } return startDt.addSecs( start.secsTo( end ) ); } void Incidence::addAttachment( Attachment *attachment ) { if ( mReadOnly || !attachment ) { return; } d->mAttachments.append( attachment ); updated(); } void Incidence::deleteAttachment( Attachment *attachment ) { d->mAttachments.removeRef( attachment ); } void Incidence::deleteAttachments( const QString &mime ) { Attachment::List::Iterator it = d->mAttachments.begin(); while ( it != d->mAttachments.end() ) { if ( (*it)->mimeType() == mime ) { d->mAttachments.removeRef( it ); } else { ++it; } } } Attachment::List Incidence::attachments() const { return d->mAttachments; } Attachment::List Incidence::attachments( const QString &mime ) const { Attachment::List attachments; Attachment::List::ConstIterator it; foreach ( Attachment *attachment, d->mAttachments ) { if ( attachment->mimeType() == mime ) { attachments.append( attachment ); } } return attachments; } void Incidence::clearAttachments() { d->mAttachments.clearAll(); } void Incidence::setResources( const QStringList &resources ) { if ( mReadOnly ) { return; } d->mResources = resources; updated(); } QStringList Incidence::resources() const { return d->mResources; } void Incidence::setPriority( int priority ) { if ( mReadOnly ) { return; } d->mPriority = priority; updated(); } int Incidence::priority() const { return d->mPriority; } void Incidence::setStatus( Incidence::Status status ) { if ( mReadOnly || status == StatusX ) { return; } d->mStatus = status; d->mStatusString.clear(); updated(); } void Incidence::setCustomStatus( const QString &status ) { if ( mReadOnly ) { return; } d->mStatus = status.isEmpty() ? StatusNone : StatusX; d->mStatusString = status; updated(); } Incidence::Status Incidence::status() const { return d->mStatus; } QString Incidence::statusStr() const { if ( d->mStatus == StatusX ) { return d->mStatusString; } return statusName( d->mStatus ); } QString Incidence::statusName( Incidence::Status status ) { switch ( status ) { case StatusTentative: return i18nc( "@item event is tentative", "Tentative" ); case StatusConfirmed: return i18nc( "@item event is definite", "Confirmed" ); case StatusCompleted: return i18nc( "@item to-do is complete", "Completed" ); case StatusNeedsAction: return i18nc( "@item to-do needs action", "Needs-Action" ); case StatusCanceled: return i18nc( "@item event orto-do is canceled; journal is removed", "Canceled" ); case StatusInProcess: return i18nc( "@item to-do is in process", "In-Process" ); case StatusDraft: return i18nc( "@item journal is in draft form", "Draft" ); case StatusFinal: return i18nc( "@item journal is in final form", "Final" ); case StatusX: case StatusNone: default: return QString(); } } void Incidence::setSecrecy( Incidence::Secrecy secrecy ) { if ( mReadOnly ) { return; } d->mSecrecy = secrecy; updated(); } Incidence::Secrecy Incidence::secrecy() const { return d->mSecrecy; } QString Incidence::secrecyStr() const { return secrecyName( d->mSecrecy ); } QString Incidence::secrecyName( Incidence::Secrecy secrecy ) { switch ( secrecy ) { case SecrecyPublic: return i18nc( "@item incidence access if for everyone", "Public" ); case SecrecyPrivate: return i18nc( "@item incidence access is by owner only", "Private" ); case SecrecyConfidential: return i18nc( "@item incidence access is by owner and a controlled group", "Confidential" ); default: return QString(); // to make compilers happy } } QStringList Incidence::secrecyList() { QStringList list; list << secrecyName( SecrecyPublic ); list << secrecyName( SecrecyPrivate ); list << secrecyName( SecrecyConfidential ); return list; } const Alarm::List &Incidence::alarms() const { return d->mAlarms; } Alarm *Incidence::newAlarm() { Alarm *alarm = new Alarm( this ); d->mAlarms.append( alarm ); return alarm; } void Incidence::addAlarm( Alarm *alarm ) { d->mAlarms.append( alarm ); updated(); } void Incidence::removeAlarm( Alarm *alarm ) { d->mAlarms.removeRef( alarm ); updated(); } void Incidence::clearAlarms() { d->mAlarms.clearAll(); updated(); } bool Incidence::isAlarmEnabled() const { foreach ( Alarm *alarm, d->mAlarms ) { if ( alarm->enabled() ) { return true; } } return false; } void Incidence::setLocation( const QString &location, bool isRich ) { if ( mReadOnly ) { return; } d->mLocation = location; d->mLocationIsRich = isRich; updated(); } void Incidence::setLocation( const QString &location ) { setLocation( location, Qt::mightBeRichText( location ) ); } QString Incidence::location() const { return d->mLocation; } QString Incidence::richLocation() const { if ( locationIsRich() ) { return d->mLocation; } else { return Qt::escape( d->mLocation ).replace( '\n', "
" ); } } bool Incidence::locationIsRich() const { return d->mLocationIsRich; } void Incidence::setSchedulingID( const QString &sid ) { d->mSchedulingID = sid; } QString Incidence::schedulingID() const { if ( d->mSchedulingID.isNull() ) { // Nothing set, so use the normal uid return uid(); } return d->mSchedulingID; } bool Incidence::hasGeo() const { return d->mHasGeo; } void Incidence::setHasGeo( bool hasGeo ) { if ( mReadOnly ) { return; } d->mHasGeo = hasGeo; updated(); } float &Incidence::geoLatitude() const { return d->mGeoLatitude; } void Incidence::setGeoLatitude( float geolatitude ) { if ( mReadOnly ) { return; } d->mGeoLatitude = geolatitude; updated(); } float &Incidence::geoLongitude() const { return d->mGeoLongitude; } void Incidence::setGeoLongitude( float geolongitude ) { if ( mReadOnly ) { return; } d->mGeoLongitude = geolongitude; updated(); } /** Observer interface for the recurrence class. If the recurrence is changed, this method will be called for the incidence the recurrence object belongs to. */ void Incidence::recurrenceUpdated( Recurrence *recurrence ) { if ( recurrence == d->mRecurrence ) { updated(); } } diff --git a/kmime/kmime_charfreq.cpp b/kmime/kmime_charfreq.cpp index 6880a2417..2de7b69a3 100644 --- a/kmime/kmime_charfreq.cpp +++ b/kmime/kmime_charfreq.cpp @@ -1,252 +1,252 @@ /* kmime_charfreq.cpp KMime, the KDE internet mail/usenet news message library. Copyright (c) 2001-2002 Marc Mutz 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. */ /** @file This file is part of the API for handling MIME data and defines the CharFreq class. @brief Defines the CharFreq class. @authors Marc Mutz \ */ #include "kmime_charfreq.h" using namespace KMime; /** * Private class that helps to provide binary compatibility between releases. * @internal */ //@cond PRIVATE //class KMime::CharFreq::Private //{ // public: //}; //@endcond CharFreq::CharFreq( const QByteArray &buf ) : mNUL( 0 ), mCTL( 0 ), mCR( 0 ), mLF( 0 ), mCRLF( 0 ), mPrintable( 0 ), mEightBit( 0 ), mTotal( 0 ), mLineMin( 0xffffffff ), mLineMax( 0 ), mTrailingWS( false ), mLeadingFrom( false ) { if ( !buf.isEmpty() ) { count( buf.data(), buf.size() ); } } CharFreq::CharFreq( const char *buf, size_t len ) : mNUL( 0 ), mCTL( 0 ), mCR( 0 ), mLF( 0 ), mCRLF( 0 ), mPrintable( 0 ), mEightBit( 0 ), mTotal( 0 ), mLineMin( 0xffffffff ), mLineMax( 0 ), mTrailingWS( false ), mLeadingFrom( false ) { if ( buf && len > 0 ) { count( buf, len ); } } //@cond PRIVATE static inline bool isWS( char ch ) { return ( ch == '\t' || ch == ' ' ); } //@endcond void CharFreq::count( const char *it, size_t len ) { const char *end = it + len; uint currentLineLength = 0; // initialize the prevChar with LF so that From_ detection works w/o // special-casing: char prevChar = '\n'; char prevPrevChar = 0; for ( ; it != end ; ++it ) { ++currentLineLength; switch ( *it ) { case '\0': ++mNUL; break; case '\r': ++mCR; break; case '\n': ++mLF; if ( prevChar == '\r' ) { --currentLineLength; ++mCRLF; } if ( currentLineLength >= mLineMax ) { mLineMax = currentLineLength-1; } if ( currentLineLength <= mLineMin ) { mLineMin = currentLineLength-1; } if ( !mTrailingWS ) { if ( isWS( prevChar ) || ( prevChar == '\r' && isWS( prevPrevChar ) ) ) { mTrailingWS = true; } } currentLineLength = 0; break; case 'F': // check for lines starting with From_ if not found already: if ( !mLeadingFrom ) { if ( prevChar == '\n' && end - it >= 5 && !qstrncmp( "From ", it, 5 ) ) { mLeadingFrom = true; } } ++mPrintable; break; default: { uchar c = *it; if ( c == '\t' || ( c >= ' ' && c <= '~' ) ) { ++mPrintable; } else if ( c == 127 || c < ' ' ) { ++mCTL; } else { ++mEightBit; } } } prevPrevChar = prevChar; prevChar = *it; } // consider the length of the last line if ( currentLineLength >= mLineMax ) { mLineMax = currentLineLength; } if ( currentLineLength <= mLineMin ) { mLineMin = currentLineLength; } // check whether the last character is tab or space if ( isWS( prevChar ) ) { mTrailingWS = true; } mTotal = len; } bool CharFreq::isEightBitData() const { return type() == EightBitData; } bool CharFreq::isEightBitText() const { return type() == EightBitText; } bool CharFreq::isSevenBitData() const { return type() == SevenBitData; } bool CharFreq::isSevenBitText() const { return type() == SevenBitText; } bool CharFreq::hasTrailingWhitespace() const { return mTrailingWS; } bool CharFreq::hasLeadingFrom() const { return mLeadingFrom; } CharFreq::Type CharFreq::type() const { #if 0 qDebug( "Total: %d; NUL: %d; CTL: %d;\n" "CR: %d; LF: %d; CRLF: %d;\n" "lineMin: %d; lineMax: %d;\n" "printable: %d; eightBit: %d;\n" "trailing whitespace: %s;\n" "leading 'From ': %s;\n", total, NUL, CTL, CR, LF, CRLF, lineMin, lineMax, printable, eightBit, mTrailingWS ? "yes" : "no" , mLeadingFrom ? "yes" : "no" ); #endif if ( mNUL ) { // must be binary return Binary; } // doesn't contain NUL's: if ( mEightBit ) { if ( mLineMax > 988 ) { return EightBitData; // not allowed in 8bit } - if ( mLF != mCRLF || mCR != mCRLF || controlCodesRatio() > 0.2 ) { + if ( ( mLF != mCRLF && mCRLF > 0 ) || mCR != mCRLF || controlCodesRatio() > 0.2 ) { return EightBitData; } return EightBitText; } // doesn't contain NUL's, nor 8bit chars: if ( mLineMax > 988 ) { return SevenBitData; } - if ( mLF != mCRLF || mCR != mCRLF || controlCodesRatio() > 0.2 ) { + if ( ( mLF != mCRLF && mCRLF > 0 ) || mCR != mCRLF || controlCodesRatio() > 0.2 ) { return SevenBitData; } // no NUL, no 8bit chars, no excessive CTLs and no lines > 998 chars: return SevenBitText; } float CharFreq::printableRatio() const { if ( mTotal ) { return float(mPrintable) / float(mTotal); } else { return 0; } } float CharFreq::controlCodesRatio() const { if ( mTotal ) { return float(mCTL) / float(mTotal); } else { return 0; } } diff --git a/kmime/kmime_header_parsing.cpp b/kmime/kmime_header_parsing.cpp index 7f628dd59..5b292062f 100644 --- a/kmime/kmime_header_parsing.cpp +++ b/kmime/kmime_header_parsing.cpp @@ -1,2035 +1,2038 @@ /* -*- c++ -*- kmime_header_parsing.cpp KMime, the KDE internet mail/usenet news message library. Copyright (c) 2001-2002 Marc Mutz 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 "kmime_header_parsing.h" #include "kmime_codecs.h" #include "kmime_util.h" #include "kmime_dateformatter.h" #include "kmime_warning.h" #include #include #include #include #include #include #include // for isdigit #include using namespace KMime; using namespace KMime::Types; namespace KMime { namespace Types { // QUrl::fromAce is extremely expensive, so only use it when necessary. // Fortunately, the presence of IDNA is readily detected with a substring match... static inline QString QUrl_fromAce_wrapper( const QString & domain ) { if ( domain.contains( QLatin1String( "xn--" ) ) ) return QUrl::fromAce( domain.toLatin1() ); else return domain; } static QString addr_spec_as_string( const AddrSpec & as, bool pretty ) { if ( as.isEmpty() ) { return QString(); } bool needsQuotes = false; QString result; result.reserve( as.localPart.length() + as.domain.length() + 1 ); for ( int i = 0 ; i < as.localPart.length() ; ++i ) { const char ch = as.localPart[i].toLatin1(); if ( ch == '.' || isAText( ch ) ) { result += ch; } else { needsQuotes = true; if ( ch == '\\' || ch == '"' ) { result += '\\'; } result += ch; } } const QString dom = pretty ? QUrl_fromAce_wrapper( as.domain ) : as.domain ; if ( needsQuotes ) { return '"' + result + "\"@" + dom; } else { return result + '@' + dom; } } QString AddrSpec::asString() const { return addr_spec_as_string( *this, false ); } QString AddrSpec::asPrettyString() const { return addr_spec_as_string( *this, true ); } bool AddrSpec::isEmpty() const { return localPart.isEmpty() && domain.isEmpty(); } QByteArray Mailbox::address() const { return mAddrSpec.asString().toLatin1(); } AddrSpec Mailbox::addrSpec() const { return mAddrSpec; } QString Mailbox::name() const { return mDisplayName; } void Mailbox::setAddress( const AddrSpec &addr ) { mAddrSpec = addr; } void Mailbox::setAddress( const QByteArray &addr ) { const char *cursor = addr.constData(); if ( !HeaderParsing::parseAngleAddr( cursor, cursor + addr.length(), mAddrSpec ) ) { if ( !HeaderParsing::parseAddrSpec( cursor, cursor + addr.length(), mAddrSpec ) ) { kWarning() << "Invalid address"; return; } } } void Mailbox::setName( const QString &name ) { mDisplayName = name; } void Mailbox::setNameFrom7Bit( const QByteArray &name, const QByteArray &defaultCharset ) { QByteArray cs; mDisplayName = decodeRFC2047String( name, cs, defaultCharset, false ); } bool Mailbox::hasAddress() const { return !mAddrSpec.isEmpty(); } bool Mailbox::hasName() const { return !mDisplayName.isEmpty(); } QString Mailbox::prettyAddress() const { if ( !hasName() ) { return address(); } QString s = name(); if ( hasAddress() ) { s += QLatin1String(" <") + address() + QLatin1Char('>'); } return s; } void Mailbox::fromUnicodeString( const QString &s ) { from7BitString( encodeRFC2047String( s, "utf-8", false ) ); } void Mailbox::from7BitString( const QByteArray &s ) { const char *cursor = s.constData(); HeaderParsing::parseMailbox( cursor, cursor + s.length(), *this ); } QByteArray KMime::Types::Mailbox::as7BitString( const QByteArray &encCharset ) const { if ( !hasName() ) { return address(); } QByteArray rv; if ( isUsAscii( name() ) ) { QByteArray tmp = name().toLatin1(); addQuotes( tmp, false ); rv += tmp; } else { rv += encodeRFC2047String( name(), encCharset, true ); } if ( hasAddress() ) { rv += " <" + address() + '>'; } return rv; } } // namespace Types namespace HeaderParsing { // parse the encoded-word (scursor points to after the initial '=') bool parseEncodedWord( const char* &scursor, const char * const send, QString &result, QByteArray &language, QByteArray &usedCS, const QByteArray &defaultCS, bool forceCS ) { // make sure the caller already did a bit of the work. assert( *(scursor-1) == '=' ); // // STEP 1: // scan for the charset/language portion of the encoded-word // char ch = *scursor++; if ( ch != '?' ) { // kDebug(5320) << "first"; KMIME_WARN_PREMATURE_END_OF( EncodedWord ); return false; } // remember start of charset (ie. just after the initial "=?") and // language (just after the first '*') fields: const char * charsetStart = scursor; const char * languageStart = 0; // find delimiting '?' (and the '*' separating charset and language // tags, if any): for ( ; scursor != send ; scursor++ ) { if ( *scursor == '?') { break; } else if ( *scursor == '*' && languageStart == 0 ) { languageStart = scursor + 1; } } // not found? can't be an encoded-word! if ( scursor == send || *scursor != '?' ) { // kDebug(5320) << "second"; KMIME_WARN_PREMATURE_END_OF( EncodedWord ); return false; } // extract the language information, if any (if languageStart is 0, // language will be null, too): QByteArray maybeLanguage( languageStart, scursor - languageStart ); // extract charset information (keep in mind: the size given to the // ctor is one off due to the \0 terminator): QByteArray maybeCharset( charsetStart, ( languageStart ? languageStart - 1 : scursor ) - charsetStart ); // // STEP 2: // scan for the encoding portion of the encoded-word // // remember start of encoding (just _after_ the second '?'): scursor++; const char * encodingStart = scursor; // find next '?' (ending the encoding tag): for ( ; scursor != send ; scursor++ ) { if ( *scursor == '?' ) { break; } } // not found? Can't be an encoded-word! if ( scursor == send || *scursor != '?' ) { // kDebug(5320) << "third"; KMIME_WARN_PREMATURE_END_OF( EncodedWord ); return false; } // extract the encoding information: QByteArray maybeEncoding( encodingStart, scursor - encodingStart ); // kDebug(5320) << "parseEncodedWord: found charset == \"" << maybeCharset // << "\"; language == \"" << maybeLanguage // << "\"; encoding == \"" << maybeEncoding << "\""; // // STEP 3: // scan for encoded-text portion of encoded-word // // remember start of encoded-text (just after the third '?'): scursor++; const char * encodedTextStart = scursor; - // find next '?' (ending the encoded-text): + // find the '?=' sequence (ending the encoded-text): for ( ; scursor != send ; scursor++ ) { if ( *scursor == '?' ) { - break; + if ( scursor + 1 != send ) { + if ( *( scursor + 1 ) != '=' ) { // We expect a '=' after the '?', but we got something else; ignore + KMIME_WARN << "Stray '?' in q-encoded word, ignoring this."; + continue; + } + else { // yep, found a '?=' sequence + scursor += 2; + break; + } + } + else { // The '?' is the last char, but we need a '=' after it! + KMIME_WARN_PREMATURE_END_OF( EncodedWord ); + return false; + } } } - // not found? Can't be an encoded-word! - // ### maybe evaluate it nonetheless if the rest is OK? - if ( scursor == send || *scursor != '?' ) { - // kDebug(5320) << "fourth"; + if ( *( scursor - 2 ) != '?' || *( scursor - 1 ) != '=' || + scursor < encodedTextStart + 2 ) { KMIME_WARN_PREMATURE_END_OF( EncodedWord ); return false; } - scursor++; - // check for trailing '=': - if ( scursor == send || *scursor != '=' ) { - // kDebug(5320) << "fifth"; - KMIME_WARN_PREMATURE_END_OF( EncodedWord ); - return false; - } - scursor++; // set end sentinel for encoded-text: const char * const encodedTextEnd = scursor - 2; // // STEP 4: // setup decoders for the transfer encoding and the charset // // try if there's a codec for the encoding found: Codec * codec = Codec::codecForName( maybeEncoding ); if ( !codec ) { KMIME_WARN_UNKNOWN( Encoding, maybeEncoding ); return false; } // get an instance of a corresponding decoder: Decoder * dec = codec->makeDecoder(); assert( dec ); // try if there's a (text)codec for the charset found: bool matchOK = false; QTextCodec *textCodec = 0; if ( forceCS || maybeCharset.isEmpty() ) { textCodec = KGlobal::charsets()->codecForName( defaultCS, matchOK ); usedCS = cachedCharset( defaultCS ); } else { textCodec = KGlobal::charsets()->codecForName( maybeCharset, matchOK ); if ( !matchOK ) { //no suitable codec found => use default charset textCodec = KGlobal::charsets()->codecForName( defaultCS, matchOK ); usedCS = cachedCharset( defaultCS ); } else { usedCS = cachedCharset( maybeCharset ); } } if ( !matchOK || !textCodec ) { KMIME_WARN_UNKNOWN( Charset, maybeCharset ); delete dec; return false; }; // kDebug(5320) << "mimeName(): \"" << textCodec->name() << "\""; // allocate a temporary buffer to store the 8bit text: int encodedTextLength = encodedTextEnd - encodedTextStart; QByteArray buffer; buffer.resize( codec->maxDecodedSizeFor( encodedTextLength ) ); QByteArray::Iterator bit = buffer.begin(); QByteArray::ConstIterator bend = buffer.end(); // // STEP 5: // do the actual decoding // if ( !dec->decode( encodedTextStart, encodedTextEnd, bit, bend ) ) { KMIME_WARN << codec->name() << "codec lies about its maxDecodedSizeFor(" << encodedTextLength << ")\nresult may be truncated"; } result = textCodec->toUnicode( buffer.begin(), bit - buffer.begin() ); // kDebug(5320) << "result now: \"" << result << "\""; // cleanup: delete dec; language = maybeLanguage; return true; } static inline void eatWhiteSpace( const char* &scursor, const char * const send ) { while ( scursor != send && ( *scursor == ' ' || *scursor == '\n' || *scursor == '\t' || *scursor == '\r' ) ) scursor++; } bool parseAtom( const char * &scursor, const char * const send, QString &result, bool allow8Bit ) { QPair maybeResult; if ( parseAtom( scursor, send, maybeResult, allow8Bit ) ) { result += QString::fromLatin1( maybeResult.first, maybeResult.second ); return true; } return false; } bool parseAtom( const char * &scursor, const char * const send, QPair &result, bool allow8Bit ) { bool success = false; const char *start = scursor; while ( scursor != send ) { signed char ch = *scursor++; if ( ch > 0 && isAText( ch ) ) { // AText: OK success = true; } else if ( allow8Bit && ch < 0 ) { // 8bit char: not OK, but be tolerant. KMIME_WARN_8BIT( ch ); success = true; } else { // CTL or special - marking the end of the atom: // re-set sursor to point to the offending // char and return: scursor--; break; } } result.first = start; result.second = scursor - start; return success; } bool parseToken( const char * &scursor, const char * const send, QString &result, bool allow8Bit ) { QPair maybeResult; if ( parseToken( scursor, send, maybeResult, allow8Bit ) ) { result += QString::fromLatin1( maybeResult.first, maybeResult.second ); return true; } return false; } bool parseToken( const char * &scursor, const char * const send, QPair &result, bool allow8Bit ) { bool success = false; const char * start = scursor; while ( scursor != send ) { signed char ch = *scursor++; if ( ch > 0 && isTText( ch ) ) { // TText: OK success = true; } else if ( allow8Bit && ch < 0 ) { // 8bit char: not OK, but be tolerant. KMIME_WARN_8BIT( ch ); success = true; } else { // CTL or tspecial - marking the end of the atom: // re-set sursor to point to the offending // char and return: scursor--; break; } } result.first = start; result.second = scursor - start; return success; } #define READ_ch_OR_FAIL if ( scursor == send ) { \ KMIME_WARN_PREMATURE_END_OF( GenericQuotedString ); \ return false; \ } else { \ ch = *scursor++; \ } // known issues: // // - doesn't handle quoted CRLF bool parseGenericQuotedString( const char* &scursor, const char * const send, QString &result, bool isCRLF, const char openChar, const char closeChar ) { char ch; // We are in a quoted-string or domain-literal or comment and the // cursor points to the first char after the openChar. // We will apply unfolding and quoted-pair removal. // We return when we either encounter the end or unescaped openChar // or closeChar. assert( *(scursor-1) == openChar || *(scursor-1) == closeChar ); while ( scursor != send ) { ch = *scursor++; if ( ch == closeChar || ch == openChar ) { // end of quoted-string or another opening char: // let caller decide what to do. return true; } switch( ch ) { case '\\': // quoted-pair // misses "\" CRLF LWSP-char handling, see rfc822, 3.4.5 READ_ch_OR_FAIL; KMIME_WARN_IF_8BIT( ch ); result += QChar( ch ); break; case '\r': // ### // The case of lonely '\r' is easy to solve, as they're // not part of Unix Line-ending conventions. // But I see a problem if we are given Unix-native // line-ending-mails, where we cannot determine anymore // whether a given '\n' was part of a CRLF or was occurring // on it's own. READ_ch_OR_FAIL; if ( ch != '\n' ) { // CR on it's own... KMIME_WARN_LONE( CR ); result += QChar('\r'); scursor--; // points to after the '\r' again } else { // CRLF encountered. // lookahead: check for folding READ_ch_OR_FAIL; if ( ch == ' ' || ch == '\t' ) { // correct folding; // position cursor behind the CRLF WSP (unfolding) // and add the WSP to the result result += QChar( ch ); } else { // this is the "shouldn't happen"-case. There is a CRLF // inside a quoted-string without it being part of FWS. // We take it verbatim. KMIME_WARN_NON_FOLDING( CRLF ); result += "\r\n"; // the cursor is decremented again, so's we need not // duplicate the whole switch here. "ch" could've been // everything (incl. openChar or closeChar). scursor--; } } break; case '\n': // Note: CRLF has been handled above already! // ### LF needs special treatment, depending on whether isCRLF // is true (we can be sure a lonely '\n' was meant this way) or // false ('\n' alone could have meant LF or CRLF in the original // message. This parser assumes CRLF iff the LF is followed by // either WSP (folding) or NULL (premature end of quoted-string; // Should be fixed, since NULL is allowed as per rfc822). READ_ch_OR_FAIL; if ( !isCRLF && ( ch == ' ' || ch == '\t' ) ) { // folding // correct folding result += QChar( ch ); } else { // non-folding KMIME_WARN_LONE( LF ); result += QChar('\n'); // pos is decremented, so's we need not duplicate the whole // switch here. ch could've been everything (incl. <">, "\"). scursor--; } break; default: KMIME_WARN_IF_8BIT( ch ); result += QChar( ch ); } } return false; } // known issues: // // - doesn't handle encoded-word inside comments. bool parseComment( const char* &scursor, const char * const send, QString &result, bool isCRLF, bool reallySave ) { int commentNestingDepth = 1; const char *afterLastClosingParenPos = 0; QString maybeCmnt; const char *oldscursor = scursor; assert( *(scursor-1) == '(' ); while ( commentNestingDepth ) { QString cmntPart; if ( parseGenericQuotedString( scursor, send, cmntPart, isCRLF, '(', ')' ) ) { assert( *(scursor-1) == ')' || *(scursor-1) == '(' ); // see the kdoc for above function for the possible conditions // we have to check: switch ( *(scursor-1) ) { case ')': if ( reallySave ) { // add the chunk that's now surely inside the comment. result += maybeCmnt; result += cmntPart; if ( commentNestingDepth > 1 ) { // don't add the outermost ')'... result += QChar(')'); } maybeCmnt.clear(); } afterLastClosingParenPos = scursor; --commentNestingDepth; break; case '(': if ( reallySave ) { // don't add to "result" yet, because we might find that we // are already outside the (broken) comment... maybeCmnt += cmntPart; maybeCmnt += QChar('('); } ++commentNestingDepth; break; default: assert( 0 ); } // switch } else { // !parseGenericQuotedString, ie. premature end if ( afterLastClosingParenPos ) { scursor = afterLastClosingParenPos; } else { scursor = oldscursor; } return false; } } // while return true; } // known issues: none. bool parsePhrase( const char* &scursor, const char * const send, QString &result, bool isCRLF ) { enum { None, Phrase, Atom, EncodedWord, QuotedString } found = None; QString tmp; QByteArray lang, charset; const char *successfullyParsed = 0; // only used by the encoded-word branch const char *oldscursor; // used to suppress whitespace between adjacent encoded-words // (rfc2047, 6.2): bool lastWasEncodedWord = false; while ( scursor != send ) { char ch = *scursor++; switch ( ch ) { case '.': // broken, but allow for intorop's sake if ( found == None ) { --scursor; return false; } else { if ( scursor != send && ( *scursor == ' ' || *scursor == '\t' ) ) { result += ". "; } else { result += '.'; } successfullyParsed = scursor; } break; case '"': // quoted-string tmp.clear(); if ( parseGenericQuotedString( scursor, send, tmp, isCRLF, '"', '"' ) ) { successfullyParsed = scursor; assert( *(scursor-1) == '"' ); switch ( found ) { case None: found = QuotedString; break; case Phrase: case Atom: case EncodedWord: case QuotedString: found = Phrase; result += QChar(' '); // rfc822, 3.4.4 break; default: assert( 0 ); } lastWasEncodedWord = false; result += tmp; } else { // premature end of quoted string. // What to do? Return leading '"' as special? Return as quoted-string? // We do the latter if we already found something, else signal failure. if ( found == None ) { return false; } else { result += QChar(' '); // rfc822, 3.4.4 result += tmp; return true; } } break; case '(': // comment // parse it, but ignore content: tmp.clear(); if ( parseComment( scursor, send, tmp, isCRLF, false /*don't bother with the content*/ ) ) { successfullyParsed = scursor; lastWasEncodedWord = false; // strictly interpreting rfc2047, 6.2 } else { if ( found == None ) { return false; } else { scursor = successfullyParsed; return true; } } break; case '=': // encoded-word tmp.clear(); oldscursor = scursor; lang.clear(); charset.clear(); if ( parseEncodedWord( scursor, send, tmp, lang, charset ) ) { successfullyParsed = scursor; switch ( found ) { case None: found = EncodedWord; break; case Phrase: case EncodedWord: case Atom: case QuotedString: if ( !lastWasEncodedWord ) { result += QChar(' '); // rfc822, 3.4.4 } found = Phrase; break; default: assert( 0 ); } lastWasEncodedWord = true; result += tmp; break; } else { // parse as atom: scursor = oldscursor; } // fall though... default: //atom tmp.clear(); scursor--; if ( parseAtom( scursor, send, tmp, true /* allow 8bit */ ) ) { successfullyParsed = scursor; switch ( found ) { case None: found = Atom; break; case Phrase: case Atom: case EncodedWord: case QuotedString: found = Phrase; result += QChar(' '); // rfc822, 3.4.4 break; default: assert( 0 ); } lastWasEncodedWord = false; result += tmp; } else { if ( found == None ) { return false; } else { scursor = successfullyParsed; return true; } } } eatWhiteSpace( scursor, send ); } return found != None; } bool parseDotAtom( const char* &scursor, const char * const send, QString &result, bool isCRLF ) { eatCFWS( scursor, send, isCRLF ); // always points to just after the last atom parsed: const char *successfullyParsed; QString tmp; if ( !parseAtom( scursor, send, tmp, false /* no 8bit */ ) ) { return false; } result += tmp; successfullyParsed = scursor; while ( scursor != send ) { // end of header or no '.' -> return if ( scursor == send || *scursor != '.' ) { return true; } scursor++; // eat '.' if ( scursor == send || !isAText( *scursor ) ) { // end of header or no AText, but this time following a '.'!: // reset cursor to just after last successfully parsed char and // return: scursor = successfullyParsed; return true; } // try to parse the next atom: QString maybeAtom; if ( !parseAtom( scursor, send, maybeAtom, false /*no 8bit*/ ) ) { scursor = successfullyParsed; return true; } result += QChar('.'); result += maybeAtom; successfullyParsed = scursor; } scursor = successfullyParsed; return true; } void eatCFWS( const char* &scursor, const char * const send, bool isCRLF ) { QString dummy; while ( scursor != send ) { const char *oldscursor = scursor; char ch = *scursor++; switch( ch ) { case ' ': case '\t': // whitespace case '\r': case '\n': // folding continue; case '(': // comment if ( parseComment( scursor, send, dummy, isCRLF, false /*don't save*/ ) ) { continue; } scursor = oldscursor; return; default: scursor = oldscursor; return; } } } bool parseDomain( const char* &scursor, const char * const send, QString &result, bool isCRLF ) { eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // domain := dot-atom / domain-literal / atom *("." atom) // // equivalent to: // domain = dot-atom / domain-literal, // since parseDotAtom does allow CFWS between atoms and dots if ( *scursor == '[' ) { // domain-literal: QString maybeDomainLiteral; // eat '[': scursor++; while ( parseGenericQuotedString( scursor, send, maybeDomainLiteral, isCRLF, '[', ']' ) ) { if ( scursor == send ) { // end of header: check for closing ']': if ( *(scursor-1) == ']' ) { // OK, last char was ']': result = maybeDomainLiteral; return true; } else { // not OK, domain-literal wasn't closed: return false; } } // we hit openChar in parseGenericQuotedString. // include it in maybeDomainLiteral and keep on parsing: if ( *(scursor-1) == '[' ) { maybeDomainLiteral += QChar('['); continue; } // OK, real end of domain-literal: result = maybeDomainLiteral; return true; } } else { // dot-atom: QString maybeDotAtom; if ( parseDotAtom( scursor, send, maybeDotAtom, isCRLF ) ) { result = maybeDotAtom; return true; } } return false; } bool parseObsRoute( const char* &scursor, const char* const send, QStringList &result, bool isCRLF, bool save ) { while ( scursor != send ) { eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // empty entry: if ( *scursor == ',' ) { scursor++; if ( save ) { result.append( QString() ); } continue; } // empty entry ending the list: if ( *scursor == ':' ) { scursor++; if ( save ) { result.append( QString() ); } return true; } // each non-empty entry must begin with '@': if ( *scursor != '@' ) { return false; } else { scursor++; } QString maybeDomain; if ( !parseDomain( scursor, send, maybeDomain, isCRLF ) ) { return false; } if ( save ) { result.append( maybeDomain ); } // eat the following (optional) comma: eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } if ( *scursor == ':' ) { scursor++; return true; } if ( *scursor == ',' ) { scursor++; } } return false; } bool parseAddrSpec( const char* &scursor, const char * const send, AddrSpec &result, bool isCRLF ) { // // STEP 1: // local-part := dot-atom / quoted-string / word *("." word) // // this is equivalent to: // local-part := word *("." word) QString maybeLocalPart; QString tmp; while ( scursor != send ) { // first, eat any whitespace eatCFWS( scursor, send, isCRLF ); char ch = *scursor++; switch ( ch ) { case '.': // dot maybeLocalPart += QChar('.'); break; case '@': goto SAW_AT_SIGN; break; case '"': // quoted-string tmp.clear(); if ( parseGenericQuotedString( scursor, send, tmp, isCRLF, '"', '"' ) ) { maybeLocalPart += tmp; } else { return false; } break; default: // atom scursor--; // re-set scursor to point to ch again tmp.clear(); if ( parseAtom( scursor, send, tmp, false /* no 8bit */ ) ) { maybeLocalPart += tmp; } else { return false; // parseAtom can only fail if the first char is non-atext. } break; } } return false; // // STEP 2: // domain // SAW_AT_SIGN: assert( *(scursor-1) == '@' ); QString maybeDomain; if ( !parseDomain( scursor, send, maybeDomain, isCRLF ) ) { return false; } result.localPart = maybeLocalPart; result.domain = maybeDomain; return true; } bool parseAngleAddr( const char* &scursor, const char * const send, AddrSpec &result, bool isCRLF ) { // first, we need an opening angle bracket: eatCFWS( scursor, send, isCRLF ); if ( scursor == send || *scursor != '<' ) { return false; } scursor++; // eat '<' eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } if ( *scursor == '@' || *scursor == ',' ) { // obs-route: parse, but ignore: KMIME_WARN << "obsolete source route found! ignoring."; QStringList dummy; if ( !parseObsRoute( scursor, send, dummy, isCRLF, false /* don't save */ ) ) { return false; } // angle-addr isn't complete until after the '>': if ( scursor == send ) { return false; } } // parse addr-spec: AddrSpec maybeAddrSpec; if ( !parseAddrSpec( scursor, send, maybeAddrSpec, isCRLF ) ) { return false; } eatCFWS( scursor, send, isCRLF ); if ( scursor == send || *scursor != '>' ) { return false; } scursor++; result = maybeAddrSpec; return true; } bool parseMailbox( const char* &scursor, const char * const send, Mailbox &result, bool isCRLF ) { eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } AddrSpec maybeAddrSpec; QString maybeDisplayName; // first, try if it's a vanilla addr-spec: const char * oldscursor = scursor; if ( parseAddrSpec( scursor, send, maybeAddrSpec, isCRLF ) ) { result.setAddress( maybeAddrSpec ); // check for the obsolete form of display-name (as comment): eatWhiteSpace( scursor, send ); if ( scursor != send && *scursor == '(' ) { scursor++; if ( !parseComment( scursor, send, maybeDisplayName, isCRLF, true /*keep*/ ) ) { return false; } } result.setNameFrom7Bit( maybeDisplayName.toLatin1() ); return true; } scursor = oldscursor; // second, see if there's a display-name: if ( !parsePhrase( scursor, send, maybeDisplayName, isCRLF ) ) { // failed: reset cursor, note absent display-name maybeDisplayName.clear(); scursor = oldscursor; } else { // succeeded: eat CFWS eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } } // third, parse the angle-addr: if ( !parseAngleAddr( scursor, send, maybeAddrSpec, isCRLF ) ) { return false; } if ( maybeDisplayName.isNull() ) { // check for the obsolete form of display-name (as comment): eatWhiteSpace( scursor, send ); if ( scursor != send && *scursor == '(' ) { scursor++; if ( !parseComment( scursor, send, maybeDisplayName, isCRLF, true /*keep*/ ) ) { return false; } } } result.setName( maybeDisplayName ); result.setAddress( maybeAddrSpec ); return true; } bool parseGroup( const char* &scursor, const char * const send, Address &result, bool isCRLF ) { // group := display-name ":" [ mailbox-list / CFWS ] ";" [CFWS] // // equivalent to: // group := display-name ":" [ obs-mbox-list ] ";" eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // get display-name: QString maybeDisplayName; if ( !parsePhrase( scursor, send, maybeDisplayName, isCRLF ) ) { return false; } // get ":": eatCFWS( scursor, send, isCRLF ); if ( scursor == send || *scursor != ':' ) { return false; } result.displayName = maybeDisplayName; // get obs-mbox-list (may contain empty entries): scursor++; while ( scursor != send ) { eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // empty entry: if ( *scursor == ',' ) { scursor++; continue; } // empty entry ending the list: if ( *scursor == ';' ) { scursor++; return true; } Mailbox maybeMailbox; if ( !parseMailbox( scursor, send, maybeMailbox, isCRLF ) ) { return false; } result.mailboxList.append( maybeMailbox ); eatCFWS( scursor, send, isCRLF ); // premature end: if ( scursor == send ) { return false; } // regular end of the list: if ( *scursor == ';' ) { scursor++; return true; } // eat regular list entry separator: if ( *scursor == ',' ) { scursor++; } } return false; } bool parseAddress( const char* &scursor, const char * const send, Address &result, bool isCRLF ) { // address := mailbox / group eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // first try if it's a single mailbox: Mailbox maybeMailbox; const char * oldscursor = scursor; if ( parseMailbox( scursor, send, maybeMailbox, isCRLF ) ) { // yes, it is: result.displayName.clear(); result.mailboxList.append( maybeMailbox ); return true; } scursor = oldscursor; Address maybeAddress; // no, it's not a single mailbox. Try if it's a group: if ( !parseGroup( scursor, send, maybeAddress, isCRLF ) ) { return false; } result = maybeAddress; return true; } bool parseAddressList( const char* &scursor, const char * const send, AddressList &result, bool isCRLF ) { while ( scursor != send ) { eatCFWS( scursor, send, isCRLF ); // end of header: this is OK. if ( scursor == send ) { return true; } // empty entry: ignore: if ( *scursor == ',' ) { scursor++; continue; } // broken clients might use ';' as list delimiter, accept that as well if ( *scursor == ';' ) { scursor++; continue; } // parse one entry Address maybeAddress; if ( !parseAddress( scursor, send, maybeAddress, isCRLF ) ) { return false; } result.append( maybeAddress ); eatCFWS( scursor, send, isCRLF ); // end of header: this is OK. if ( scursor == send ) { return true; } // comma separating entries: eat it. if ( *scursor == ',' ) { scursor++; } } return true; } static QString asterisk = QString::fromLatin1( "*0*", 1 ); static QString asteriskZero = QString::fromLatin1( "*0*", 2 ); //static QString asteriskZeroAsterisk = QString::fromLatin1( "*0*", 3 ); bool parseParameter( const char* &scursor, const char * const send, QPair &result, bool isCRLF ) { // parameter = regular-parameter / extended-parameter // regular-parameter = regular-parameter-name "=" value // extended-parameter = // value = token / quoted-string // // note that rfc2231 handling is out of the scope of this function. // Therefore we return the attribute as QString and the value as // (start,length) tupel if we see that the value is encoded // (trailing asterisk), for parseParameterList to decode... eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // // parse the parameter name: // QString maybeAttribute; if ( !parseToken( scursor, send, maybeAttribute, false /* no 8bit */ ) ) { return false; } eatCFWS( scursor, send, isCRLF ); // premature end: not OK (haven't seen '=' yet). if ( scursor == send || *scursor != '=' ) { return false; } scursor++; // eat '=' eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { // don't choke on attribute=, meaning the value was omitted: if ( maybeAttribute.endsWith( asterisk ) ) { KMIME_WARN << "attribute ends with \"*\", but value is empty!" "Chopping away \"*\"."; maybeAttribute.truncate( maybeAttribute.length() - 1 ); } result = qMakePair( maybeAttribute.toLower(), QStringOrQPair() ); return true; } const char * oldscursor = scursor; // // parse the parameter value: // QStringOrQPair maybeValue; if ( *scursor == '"' ) { // value is a quoted-string: scursor++; if ( maybeAttribute.endsWith( asterisk ) ) { // attributes ending with "*" designate extended-parameters, // which cannot have quoted-strings as values. So we remove the // trailing "*" to not confuse upper layers. KMIME_WARN << "attribute ends with \"*\", but value is a quoted-string!" "Chopping away \"*\"."; maybeAttribute.truncate( maybeAttribute.length() - 1 ); } if ( !parseGenericQuotedString( scursor, send, maybeValue.qstring, isCRLF ) ) { scursor = oldscursor; result = qMakePair( maybeAttribute.toLower(), QStringOrQPair() ); return false; // this case needs further processing by upper layers!! } } else { // value is a token: if ( !parseToken( scursor, send, maybeValue.qpair, false /* no 8bit */ ) ) { scursor = oldscursor; result = qMakePair( maybeAttribute.toLower(), QStringOrQPair() ); return false; // this case needs further processing by upper layers!! } } result = qMakePair( maybeAttribute.toLower(), maybeValue ); return true; } bool parseRawParameterList( const char* &scursor, const char * const send, QMap &result, bool isCRLF ) { // we use parseParameter() consecutively to obtain a map of raw // attributes to raw values. "Raw" here means that we don't do // rfc2231 decoding and concatenation. This is left to // parseParameterList(), which will call this function. // // The main reason for making this chunk of code a separate // (private) method is that we can deal with broken parameters // _here_ and leave the rfc2231 handling solely to // parseParameterList(), which will still be enough work. while ( scursor != send ) { eatCFWS( scursor, send, isCRLF ); // empty entry ending the list: OK. if ( scursor == send ) { return true; } // empty list entry: ignore. if ( *scursor == ';' ) { scursor++; continue; } QPair maybeParameter; if ( !parseParameter( scursor, send, maybeParameter, isCRLF ) ) { // we need to do a bit of work if the attribute is not // NULL. These are the cases marked with "needs further // processing" in parseParameter(). Specifically, parsing of the // token or the quoted-string, which should represent the value, // failed. We take the easy way out and simply search for the // next ';' to start parsing again. (Another option would be to // take the text between '=' and ';' as value) if ( maybeParameter.first.isNull() ) { return false; } while ( scursor != send ) { if ( *scursor++ == ';' ) { goto IS_SEMICOLON; } } // scursor == send case: end of list. return true; IS_SEMICOLON: // *scursor == ';' case: parse next entry. continue; } // successful parsing brings us here: result.insert( maybeParameter.first, maybeParameter.second ); eatCFWS( scursor, send, isCRLF ); // end of header: ends list. if ( scursor == send ) { return true; } // regular separator: eat it. if ( *scursor == ';' ) { scursor++; } } return true; } static void decodeRFC2231Value( Codec* &rfc2231Codec, QTextCodec* &textcodec, bool isContinuation, QString &value, QPair &source ) { // // parse the raw value into (charset,language,text): // const char * decBegin = source.first; const char * decCursor = decBegin; const char * decEnd = decCursor + source.second; if ( !isContinuation ) { // find the first single quote while ( decCursor != decEnd ) { if ( *decCursor == '\'' ) { break; } else { decCursor++; } } if ( decCursor == decEnd ) { // there wasn't a single single quote at all! // take the whole value to be in latin-1: KMIME_WARN << "No charset in extended-initial-value." "Assuming \"iso-8859-1\"."; value += QString::fromLatin1( decBegin, source.second ); return; } QByteArray charset( decBegin, decCursor - decBegin ); const char * oldDecCursor = ++decCursor; // find the second single quote (we ignore the language tag): while ( decCursor != decEnd ) { if ( *decCursor == '\'' ) { break; } else { decCursor++; } } if ( decCursor == decEnd ) { KMIME_WARN << "No language in extended-initial-value." "Trying to recover."; decCursor = oldDecCursor; } else { decCursor++; } // decCursor now points to the start of the // "extended-other-values": // // get the decoders: // bool matchOK = false; textcodec = KGlobal::charsets()->codecForName( charset, matchOK ); if ( !matchOK ) { textcodec = 0; KMIME_WARN_UNKNOWN( Charset, charset ); } } if ( !rfc2231Codec ) { rfc2231Codec = Codec::codecForName("x-kmime-rfc2231"); assert( rfc2231Codec ); } if ( !textcodec ) { value += QString::fromLatin1( decCursor, decEnd - decCursor ); return; } Decoder * dec = rfc2231Codec->makeDecoder(); assert( dec ); // // do the decoding: // QByteArray buffer; buffer.resize( rfc2231Codec->maxDecodedSizeFor( decEnd - decCursor ) ); QByteArray::Iterator bit = buffer.begin(); QByteArray::ConstIterator bend = buffer.end(); if ( !dec->decode( decCursor, decEnd, bit, bend ) ) { KMIME_WARN << rfc2231Codec->name() << "codec lies about its maxDecodedSizeFor()" << endl << "result may be truncated"; } value += textcodec->toUnicode( buffer.begin(), bit - buffer.begin() ); // kDebug(5320) << "value now: \"" << value << "\""; // cleanup: delete dec; } // known issues: // - permutes rfc2231 continuations when the total number of parts // exceeds 10 (other-sections then becomes *xy, ie. two digits) bool parseParameterList( const char* &scursor, const char * const send, QMap &result, bool isCRLF ) { // parse the list into raw attribute-value pairs: QMap rawParameterList; if (!parseRawParameterList( scursor, send, rawParameterList, isCRLF ) ) { return false; } if ( rawParameterList.isEmpty() ) { return true; } // decode rfc 2231 continuations and alternate charset encoding: // NOTE: this code assumes that what QMapIterator delivers is sorted // by the key! Codec * rfc2231Codec = 0; QTextCodec * textcodec = 0; QString attribute; QString value; enum Modes { NoMode = 0x0, Continued = 0x1, Encoded = 0x2 } mode; QMap::Iterator it, end = rawParameterList.end(); for ( it = rawParameterList.begin() ; it != end ; ++it ) { if ( attribute.isNull() || !it.key().startsWith( attribute ) ) { // // new attribute: // // store the last attribute/value pair in the result map now: if ( !attribute.isNull() ) { result.insert( attribute, value ); } // and extract the information from the new raw attribute: value.clear(); attribute = it.key(); mode = NoMode; // is the value encoded? if ( attribute.endsWith( asterisk ) ) { attribute.truncate( attribute.length() - 1 ); mode = (Modes) ((int) mode | Encoded); } // is the value continued? if ( attribute.endsWith( asteriskZero ) ) { attribute.truncate( attribute.length() - 2 ); mode = (Modes) ((int) mode | Continued); } // // decode if necessary: // if ( mode & Encoded ) { decodeRFC2231Value( rfc2231Codec, textcodec, false, /* isn't continuation */ value, (*it).qpair ); } else { // not encoded. if ( (*it).qpair.first ) { value += QString::fromLatin1( (*it).qpair.first, (*it).qpair.second ); } else { value += (*it).qstring; } } // // shortcut-processing when the value isn't encoded: // if ( !(mode & Continued) ) { // save result already: result.insert( attribute, value ); // force begin of a new attribute: attribute.clear(); } } else { // it.key().startsWith( attribute ) // // continuation // // ignore the section and trust QMap to have sorted the keys: if ( it.key().endsWith( asterisk ) ) { // encoded decodeRFC2231Value( rfc2231Codec, textcodec, true, /* is continuation */ value, (*it).qpair ); } else { // not encoded if ( (*it).qpair.first ) { value += QString::fromLatin1( (*it).qpair.first, (*it).qpair.second ); } else { value += (*it).qstring; } } } } // write last attr/value pair: if ( !attribute.isNull() ) { result.insert( attribute, value ); } return true; } static const char * const stdDayNames[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; static const int stdDayNamesLen = sizeof stdDayNames / sizeof *stdDayNames; static bool parseDayName( const char* &scursor, const char * const send ) { // check bounds: if ( send - scursor < 3 ) { return false; } for ( int i = 0 ; i < stdDayNamesLen ; ++i ) { if ( qstrnicmp( scursor, stdDayNames[i], 3 ) == 0 ) { scursor += 3; // kDebug(5320) << "found" << stdDayNames[i]; return true; } } return false; } static const char * const stdMonthNames[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; static const int stdMonthNamesLen = sizeof stdMonthNames / sizeof *stdMonthNames; static bool parseMonthName( const char* &scursor, const char * const send, int &result ) { // check bounds: if ( send - scursor < 3 ) { return false; } for ( result = 0 ; result < stdMonthNamesLen ; ++result ) { if ( qstrnicmp( scursor, stdMonthNames[result], 3 ) == 0 ) { scursor += 3; return true; } } // not found: return false; } static const struct { const char * tzName; long int secsEastOfGMT; } timeZones[] = { // rfc 822 timezones: { "GMT", 0 }, { "UT", 0 }, { "EDT", -4*3600 }, { "EST", -5*3600 }, { "MST", -5*3600 }, { "CST", -6*3600 }, { "MDT", -6*3600 }, { "MST", -7*3600 }, { "PDT", -7*3600 }, { "PST", -8*3600 }, // common, non-rfc-822 zones: { "CET", 1*3600 }, { "MET", 1*3600 }, { "UTC", 0 }, { "CEST", 2*3600 }, { "BST", 1*3600 }, // rfc 822 military timezones: { "Z", 0 }, { "A", -1*3600 }, { "B", -2*3600 }, { "C", -3*3600 }, { "D", -4*3600 }, { "E", -5*3600 }, { "F", -6*3600 }, { "G", -7*3600 }, { "H", -8*3600 }, { "I", -9*3600 }, // J is not used! { "K", -10*3600 }, { "L", -11*3600 }, { "M", -12*3600 }, { "N", 1*3600 }, { "O", 2*3600 }, { "P", 3*3600 }, { "Q", 4*3600 }, { "R", 5*3600 }, { "S", 6*3600 }, { "T", 7*3600 }, { "U", 8*3600 }, { "V", 9*3600 }, { "W", 10*3600 }, { "X", 11*3600 }, { "Y", 12*3600 }, }; static const int timeZonesLen = sizeof timeZones / sizeof *timeZones; static bool parseAlphaNumericTimeZone( const char* &scursor, const char * const send, long int &secsEastOfGMT, bool &timeZoneKnown ) { QPair maybeTimeZone( 0, 0 ); if ( !parseToken( scursor, send, maybeTimeZone, false /*no 8bit*/ ) ) { return false; } for ( int i = 0 ; i < timeZonesLen ; ++i ) { if ( qstrnicmp( timeZones[i].tzName, maybeTimeZone.first, maybeTimeZone.second ) == 0 ) { scursor += maybeTimeZone.second; secsEastOfGMT = timeZones[i].secsEastOfGMT; timeZoneKnown = true; return true; } } // don't choke just because we don't happen to know the time zone KMIME_WARN_UNKNOWN( time zone, QByteArray( maybeTimeZone.first, maybeTimeZone.second ) ); secsEastOfGMT = 0; timeZoneKnown = false; return true; } // parse a number and return the number of digits parsed: int parseDigits( const char* &scursor, const char * const send, int &result ) { result = 0; int digits = 0; for ( ; scursor != send && isdigit( *scursor ) ; scursor++, digits++ ) { result *= 10; result += int( *scursor - '0' ); } return digits; } static bool parseTimeOfDay( const char* &scursor, const char * const send, int &hour, int &min, int &sec, bool isCRLF=false ) { // time-of-day := 2DIGIT [CFWS] ":" [CFWS] 2DIGIT [ [CFWS] ":" 2DIGIT ] // // 2DIGIT representing "hour": // if ( !parseDigits( scursor, send, hour ) ) { return false; } eatCFWS( scursor, send, isCRLF ); if ( scursor == send || *scursor != ':' ) { return false; } scursor++; // eat ':' eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // // 2DIGIT representing "minute": // if ( !parseDigits( scursor, send, min ) ) { return false; } eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return true; // seconds are optional } // // let's see if we have a 2DIGIT representing "second": // if ( *scursor == ':' ) { // yepp, there are seconds: scursor++; // eat ':' eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } if ( !parseDigits( scursor, send, sec ) ) { return false; } } else { sec = 0; } return true; } bool parseTime( const char* &scursor, const char * send, int &hour, int &min, int &sec, long int &secsEastOfGMT, bool &timeZoneKnown, bool isCRLF ) { // time := time-of-day CFWS ( zone / obs-zone ) // // obs-zone := "UT" / "GMT" / // "EST" / "EDT" / ; -0500 / -0400 // "CST" / "CDT" / ; -0600 / -0500 // "MST" / "MDT" / ; -0700 / -0600 // "PST" / "PDT" / ; -0800 / -0700 // "A"-"I" / "a"-"i" / // "K"-"Z" / "k"-"z" eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } if ( !parseTimeOfDay( scursor, send, hour, min, sec, isCRLF ) ) { return false; } eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { timeZoneKnown = false; secsEastOfGMT = 0; return true; // allow missing timezone } timeZoneKnown = true; if ( *scursor == '+' || *scursor == '-' ) { // remember and eat '-'/'+': const char sign = *scursor++; // numerical timezone: int maybeTimeZone; if ( parseDigits( scursor, send, maybeTimeZone ) != 4 ) { return false; } secsEastOfGMT = 60 * ( maybeTimeZone / 100 * 60 + maybeTimeZone % 100 ); if ( sign == '-' ) { secsEastOfGMT *= -1; if ( secsEastOfGMT == 0 ) { timeZoneKnown = false; // -0000 means indetermined tz } } } else { // maybe alphanumeric timezone: if ( !parseAlphaNumericTimeZone( scursor, send, secsEastOfGMT, timeZoneKnown ) ) { return false; } } return true; } bool parseDateTime( const char* &scursor, const char * const send, KDateTime &result, bool isCRLF ) { // Parsing date-time; strict mode: // // date-time := [ [CFWS] day-name [CFWS] "," ] ; wday // (expanded) [CFWS] 1*2DIGIT CFWS month-name CFWS 2*DIGIT [CFWS] ; date // time // // day-name := "Mon" / "Tue" / "Wed" / "Thu" / "Fri" / "Sat" / "Sun" // month-name := "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / // "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec" result = KDateTime(); QDateTime maybeDateTime; eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // // let's see if there's a day-of-week: // if ( parseDayName( scursor, send ) ) { eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // day-name should be followed by ',' but we treat it as optional: if ( *scursor == ',' ) { scursor++; // eat ',' eatCFWS( scursor, send, isCRLF ); } } // // 1*2DIGIT representing "day" (of month): // int maybeDay; if ( !parseDigits( scursor, send, maybeDay ) ) { return false; } eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // // month-name: // int maybeMonth = 0; if ( !parseMonthName( scursor, send, maybeMonth ) ) { return false; } if ( scursor == send ) { return false; } assert( maybeMonth >= 0 ); assert( maybeMonth <= 11 ); ++maybeMonth; // 0-11 -> 1-12 eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } // // 2*DIGIT representing "year": // int maybeYear; if ( !parseDigits( scursor, send, maybeYear ) ) { return false; } // RFC 2822 4.3 processing: if ( maybeYear < 50 ) { maybeYear += 2000; } else if ( maybeYear < 1000 ) { maybeYear += 1900; } // else keep as is if ( maybeYear < 1900 ) { return false; // rfc2822, 3.3 } eatCFWS( scursor, send, isCRLF ); if ( scursor == send ) { return false; } maybeDateTime.setDate( QDate( maybeYear, maybeMonth, maybeDay ) ); // // time // int maybeHour, maybeMinute, maybeSecond; long int secsEastOfGMT; bool timeZoneKnown = true; if ( !parseTime( scursor, send, maybeHour, maybeMinute, maybeSecond, secsEastOfGMT, timeZoneKnown, isCRLF ) ) { return false; } maybeDateTime.setTime( QTime( maybeHour, maybeMinute, maybeSecond ) ); if ( !maybeDateTime.isValid() ) return false; result = KDateTime( maybeDateTime, KDateTime::Spec( KDateTime::OffsetFromUTC, secsEastOfGMT ) ); if ( !result.isValid() ) return false; return true; } } // namespace HeaderParsing } // namespace KMime diff --git a/kmime/kmime_warning.h b/kmime/kmime_warning.h index 00c331652..620a4800f 100644 --- a/kmime/kmime_warning.h +++ b/kmime/kmime_warning.h @@ -1,60 +1,60 @@ /* kmime_warning.h KMime, the KDE internet mail/usenet news message library. Copyright (c) 2001-2002 Marc Mutz See file AUTHORS for details 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 KMIME_WARNING_H #define KMIME_WARNING_H #ifndef KMIME_NO_WARNING # include -# define KMIME_WARN kWarning(5100) << "Tokenizer Warning: " +# define KMIME_WARN kWarning(5100) << "Tokenizer Warning:" # define KMIME_WARN_UNKNOWN(x,y) KMIME_WARN << "unknown " #x ": \"" \ << y << "\""; # define KMIME_WARN_UNKNOWN_ENCODING KMIME_WARN << "unknown encoding in " \ "RFC 2047 encoded-word (only know 'q' and 'b')"; # define KMIME_WARN_UNKNOWN_CHARSET(c) KMIME_WARN << "unknown charset \"" \ << c << "\" in RFC 2047 encoded-word"; # define KMIME_WARN_8BIT(ch) KMIME_WARN \ - << "8Bit character '" << QString(QChar(ch)) << "'" << endl + << "8Bit character '" << QString(QChar(ch)) << "'" # define KMIME_WARN_IF_8BIT(ch) if ( (unsigned char)(ch) > 127 ) \ { KMIME_WARN_8BIT(ch); } # define KMIME_WARN_PREMATURE_END_OF(x) KMIME_WARN \ - << "Premature end of " #x << endl -# define KMIME_WARN_LONE(x) KMIME_WARN << "Lonely " #x " character" << endl -# define KMIME_WARN_NON_FOLDING(x) KMIME_WARN << "Non-folding " #x << endl + << "Premature end of " #x +# define KMIME_WARN_LONE(x) KMIME_WARN << "Lonely " #x " character" +# define KMIME_WARN_NON_FOLDING(x) KMIME_WARN << "Non-folding " #x # define KMIME_WARN_CTL_OUTSIDE_QS(x) KMIME_WARN << "Control character " \ - #x " outside quoted-string" << endl + #x " outside quoted-string" # define KMIME_WARN_INVALID_X_IN_Y(X,Y) KMIME_WARN << "Invalid character '" \ QString(QChar(X)) << "' in " #Y; # define KMIME_WARN_TOO_LONG(x) KMIME_WARN << #x \ " too long or missing delimiter"; #else # define KMIME_NOP do {} while (0) # define KMIME_WARN_8BIT(ch) KMIME_NOP # define KMIME_WARN_IF_8BIT(ch) KMIME_NOP # define KMIME_WARN_PREMATURE_END_OF(x) KMIME_NOP # define KMIME_WARN_LONE(x) KMIME_NOP # define KMIME_WARN_NON_FOLDING(x) KMIME_NOP # define KMIME_WARN_CTL_OUTSIDE_QS(x) KMIME_NOP #endif #endif diff --git a/kmime/tests/headertest.cpp b/kmime/tests/headertest.cpp index 515ca1240..099180532 100644 --- a/kmime/tests/headertest.cpp +++ b/kmime/tests/headertest.cpp @@ -1,724 +1,756 @@ /* Copyright (c) 2006 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 "headertest.h" #include #include using namespace KMime; using namespace KMime::Headers; using namespace KMime::Headers::Generics; // the following test cases are taken from KDE mailinglists, bug reports, RFC 2045, // RFC 2183 and RFC 2822, Appendix A QTEST_KDEMAIN( HeaderTest, NoGUI ) void HeaderTest::testIdentHeader() { // empty header Headers::Generics::Ident* h = new Headers::Generics::Ident(); QVERIFY( h->isEmpty() ); // parse single identifier h->from7BitString( QByteArray( "<1162746587.784559.5038.nullmailer@svn.kde.org>" ) ); QCOMPARE( h->identifiers().count(), 1 ); QCOMPARE( h->identifiers().first(), QByteArray( "1162746587.784559.5038.nullmailer@svn.kde.org" ) ); QCOMPARE( h->asUnicodeString(), QString("<1162746587.784559.5038.nullmailer@svn.kde.org>") ); QVERIFY( !h->isEmpty() ); // clearing a header h->clear(); QVERIFY( h->isEmpty() ); QVERIFY( h->identifiers().isEmpty() ); delete h; // parse multiple identifiers h = new Headers::Generics::Ident(); h->from7BitString( QByteArray( "<1234@local.machine.example> <3456@example.net>" ) ); QCOMPARE( h->identifiers().count(), 2 ); QList ids = h->identifiers(); QCOMPARE( ids.takeFirst(), QByteArray( "1234@local.machine.example" ) ); QCOMPARE( ids.first(), QByteArray( "3456@example.net" ) ); delete h; // parse multiple identifiers with folded headers h = new Headers::Generics::Ident(); h->from7BitString( QByteArray( "<1234@local.machine.example>\n <3456@example.net>" ) ); QCOMPARE( h->identifiers().count(), 2 ); ids = h->identifiers(); QCOMPARE( ids.takeFirst(), QByteArray( "1234@local.machine.example" ) ); QCOMPARE( ids.first(), QByteArray( "3456@example.net" ) ); // appending of new identifiers (with and without angle-brackets) h->appendIdentifier( "" ); h->appendIdentifier( "78910@example.net" ); QCOMPARE( h->identifiers().count(), 4 ); // assemble the final header QCOMPARE( h->as7BitString( false ), QByteArray("<1234@local.machine.example> <3456@example.net> <78910@example.net>") ); } void HeaderTest::testAddressListHeader() { // empty header Headers::Generics::AddressList *h = new Headers::Generics::AddressList(); QVERIFY( h->isEmpty() ); // parse single simple address h->from7BitString( "joe@where.test" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->addresses().count(), 1 ); QCOMPARE( h->addresses().first(), QByteArray("joe@where.test") ); QCOMPARE( h->displayNames().count(), 1 ); QCOMPARE( h->displayNames().first(), QString() ); QCOMPARE( h->prettyAddresses().count(), 1 ); QCOMPARE( h->prettyAddresses().first(), QString("joe@where.test") ); // clearing a header h->clear(); QVERIFY( h->isEmpty() ); delete h; // parsing and re-assembling a single address with display name h = new Headers::Generics::AddressList(); h->from7BitString( "Pete " ); QCOMPARE( h->addresses().count(), 1 ); QCOMPARE( h->addresses().first(), QByteArray( "pete@silly.example" ) ); QCOMPARE( h->displayNames().first(), QString("Pete") ); QCOMPARE( h->prettyAddresses().first(), QString("Pete ") ); QCOMPARE( h->as7BitString( false ), QByteArray("Pete ") ); delete h; // parsing a single address with legacy comment style display name h = new Headers::Generics::AddressList(); h->from7BitString( "jdoe@machine.example (John Doe)" ); QCOMPARE( h->addresses().count(), 1 ); QCOMPARE( h->addresses().first(), QByteArray( "jdoe@machine.example" ) ); QCOMPARE( h->displayNames().first(), QString("John Doe") ); QCOMPARE( h->prettyAddresses().first(), QString("John Doe ") ); delete h; // parsing and re-assembling list of diffrent addresses h = new Headers::Generics::AddressList(); h->from7BitString( "Mary Smith , jdoe@example.org, Who? " ); QCOMPARE( h->addresses().count(), 3 ); QStringList names = h->displayNames(); QCOMPARE( names.takeFirst(), QString("Mary Smith") ); QCOMPARE( names.takeFirst(), QString() ); QCOMPARE( names.takeFirst(), QString("Who?") ); QCOMPARE( h->as7BitString( false ), QByteArray("Mary Smith , jdoe@example.org, Who? ") ); delete h; // same again with some interessting quoting h = new Headers::Generics::AddressList(); h->from7BitString( "\"Joe Q. Public\" , , \"Giant; \\\"Big\\\" Box\" " ); QCOMPARE( h->addresses().count(), 3 ); names = h->displayNames(); QCOMPARE( names.takeFirst(), QString("Joe Q. Public") ); QCOMPARE( names.takeFirst(), QString() ); QCOMPARE( names.takeFirst(), QString("Giant; \"Big\" Box") ); QCOMPARE( h->as7BitString( false ), QByteArray("\"Joe Q. Public\" , boss@nil.test, \"Giant; \\\"Big\\\" Box\" ") ); delete h; // a display name with non-latin1 content h = new Headers::Generics::AddressList(); h->from7BitString( "Ingo =?iso-8859-15?q?Kl=F6cker?= " ); QCOMPARE( h->addresses().count(), 1 ); QCOMPARE( h->addresses().first(), QByteArray( "kloecker@kde.org" ) ); QCOMPARE( h->displayNames().first(), QString::fromUtf8("Ingo Klöcker") ); QCOMPARE( h->asUnicodeString(), QString::fromUtf8("Ingo Klöcker ") ); QCOMPARE( h->as7BitString( false ), QByteArray("Ingo =?ISO-8859-1?Q?Kl=F6cker?= ") ); delete h; // again, this time legacy style h = new Headers::Generics::AddressList(); h->from7BitString( "kloecker@kde.org (Ingo =?iso-8859-15?q?Kl=F6cker?=)" ); QCOMPARE( h->addresses().count(), 1 ); QCOMPARE( h->addresses().first(), QByteArray( "kloecker@kde.org" ) ); QCOMPARE( h->displayNames().first(), QString::fromUtf8("Ingo Klöcker") ); delete h; // parsing a empty group h = new Headers::Generics::AddressList(); h->from7BitString( "Undisclosed recipients:;" ); QCOMPARE( h->addresses().count(), 0 ); delete h; // parsing and re-assembling a address list with a group h = new Headers::Generics::AddressList(); h->from7BitString( "A Group:Chris Jones ,joe@where.test,John ;" ); QCOMPARE( h->addresses().count(), 3 ); names = h->displayNames(); QCOMPARE( names.takeFirst(), QString("Chris Jones") ); QCOMPARE( names.takeFirst(), QString() ); QCOMPARE( names.takeFirst(), QString("John") ); QCOMPARE( h->as7BitString( false ), QByteArray("Chris Jones , joe@where.test, John ") ); delete h; // modifying a header h = new Headers::Generics::AddressList(); h->from7BitString( "John " ); h->addAddress( "", QString::fromUtf8("Ingo Klöcker") ); h->addAddress( "c@a.test" ); QCOMPARE( h->addresses().count(), 3 ); QCOMPARE( h->asUnicodeString(), QString::fromUtf8("John , Ingo Klöcker , c@a.test") ); QCOMPARE( h->as7BitString( false ), QByteArray("John , Ingo =?ISO-8859-1?Q?Kl=F6cker?= , c@a.test") ); delete h; // parsing from utf-8 h = new Headers::Generics::AddressList(); h->fromUnicodeString( QString::fromUtf8("Ingo Klöcker "), "utf-8" ); QCOMPARE( h->addresses().count(), 1 ); QCOMPARE( h->addresses().first(), QByteArray( "kloecker@kde.org" ) ); QCOMPARE( h->displayNames().first(), QString::fromUtf8("Ingo Klöcker") ); delete h; // based on bug #137033, a header broken in various ways: ';' as list separator, // unquoted '.' in display name h = new Headers::Generics::AddressList(); h->from7BitString( "Vice@censored.serverkompetenz.net,\n President@mail2.censored.net;\"Int\\\\\\\\\\\\\\\\\\\\'l\" Lotto Commission. " ); QCOMPARE( h->addresses().count(), 3 ); names = h->displayNames(); QCOMPARE( names.takeFirst(), QString() ); QCOMPARE( names.takeFirst(), QString() ); // there is an wrong ' ' after the name, but since the header is completely // broken we can be happy it parses at all... QCOMPARE( names.takeFirst(), QString("Int\\\\\\\\\\'l Lotto Commission. ") ); QList addrs = h->addresses(); QCOMPARE( addrs.takeFirst(), QByteArray("Vice@censored.serverkompetenz.net") ); QCOMPARE( addrs.takeFirst(), QByteArray("President@mail2.censored.net") ); QCOMPARE( addrs.takeFirst(), QByteArray("censored@yahoo.fr") ); delete h; // based on bug #102010, a display name containing '<' h = new Headers::Generics::AddressList( 0, QByteArray("\"|") ); QCOMPARE( h->addresses().count(), 1 ); QCOMPARE( h->addresses().first(), QByteArray("censored@censored.dy") ); QCOMPARE( h->displayNames().first(), QString("|as7BitString( false ), QByteArray("\"|") ); // based on bug #93790 (legacy display name with nested comments) h = new Headers::Generics::AddressList( 0, QByteArray("first.name@domain.tld (first name (nickname))") ); QCOMPARE( h->displayNames().count(), 1 ); QCOMPARE( h->displayNames().first(), QString("first name (nickname)") ); QCOMPARE( h->as7BitString( false ), QByteArray("\"first name (nickname)\" ") ); delete h; // rfc 2047 encoding in quoted name (which is not allowed there) h = new Headers::Generics::AddressList(); h->from7BitString( QByteArray( "\"Ingo =?iso-8859-15?q?Kl=F6cker?=\" " ) ); QCOMPARE( h->mailboxes().count(), 1 ); QCOMPARE( h->asUnicodeString(), QString::fromUtf8( "Ingo =?iso-8859-15?q?Kl=F6cker?= " ) ); delete h; } void HeaderTest::testMailboxListHeader() { // empty header Headers::Generics::MailboxList *h = new Headers::Generics::MailboxList(); QVERIFY( h->isEmpty() ); // parse single simple address h->from7BitString( "joe_smith@where.test" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->mailboxes().count(), 1 ); QCOMPARE( h->addresses().count(), 1 ); QCOMPARE( h->addresses().first(), QByteArray("joe_smith@where.test") ); QCOMPARE( h->displayNames().count(), 1 ); QCOMPARE( h->displayNames().first(), QString() ); QCOMPARE( h->prettyAddresses().count(), 1 ); QCOMPARE( h->prettyAddresses().first(), QString("joe_smith@where.test") ); // https://bugzilla.novell.com/show_bug.cgi?id=421057 (but apparently this was not the cause of the bug) h->from7BitString( "fr...@ce.sco (Francesco)" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->mailboxes().count(), 1 ); QCOMPARE( h->prettyAddresses().first(), QString("Francesco ") ); delete h; } void HeaderTest::testSingleMailboxHeader() { // empty header Headers::Generics::SingleMailbox *h = new Headers::Generics::SingleMailbox(); QVERIFY( h->isEmpty() ); // parse single simple address h->from7BitString( "joe_smith@where.test" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->addresses().count(), 1 ); QCOMPARE( h->addresses().first(), QByteArray("joe_smith@where.test") ); QCOMPARE( h->displayNames().count(), 1 ); QCOMPARE( h->displayNames().first(), QString() ); QCOMPARE( h->prettyAddresses().count(), 1 ); QCOMPARE( h->prettyAddresses().first(), QString("joe_smith@where.test") ); delete h; } void HeaderTest::testMailCopiesToHeader() { Headers::MailCopiesTo *h; // empty header h = new Headers::MailCopiesTo(); QVERIFY( h->isEmpty() ); QVERIFY( !h->alwaysCopy() ); QVERIFY( !h->neverCopy() ); // set to always copy to poster h->setAlwaysCopy(); QVERIFY( !h->isEmpty() ); QVERIFY( h->alwaysCopy() ); QVERIFY( !h->neverCopy() ); QCOMPARE( h->as7BitString(), QByteArray( "Mail-Copies-To: poster" ) ); // set to never copy h->setNeverCopy(); QVERIFY( !h->isEmpty() ); QVERIFY( !h->alwaysCopy() ); QVERIFY( h->neverCopy() ); QCOMPARE( h->as7BitString(), QByteArray( "Mail-Copies-To: nobody" ) ); // clear header h->clear(); QVERIFY( h->isEmpty() ); delete h; // parse copy to poster h = new MailCopiesTo( 0, "always" ); QVERIFY( h->addresses().isEmpty() ); QVERIFY( !h->isEmpty() ); QVERIFY( h->alwaysCopy() ); delete h; // parse never copy h = new MailCopiesTo( 0, "never" ); QVERIFY( h->addresses().isEmpty() ); QVERIFY( !h->isEmpty() ); QVERIFY( h->neverCopy() ); delete h; // parse address h = new MailCopiesTo( 0, "vkrause@kde.org" ); QVERIFY( !h->addresses().isEmpty() ); QVERIFY( h->alwaysCopy() ); QVERIFY( !h->neverCopy() ); QCOMPARE( h->as7BitString(), QByteArray( "Mail-Copies-To: vkrause@kde.org" ) ); delete h; } void HeaderTest::testParametrizedHeader() { Parametrized *h; // empty header h = new Parametrized(); QVERIFY( h->isEmpty() ); // add a parameter h->setParameter( "filename", "bla.jpg" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->parameter( "filename" ), QString( "bla.jpg" ) ); QCOMPARE( h->as7BitString( false ), QByteArray( "filename=\"bla.jpg\"" ) ); // clear again h->clear(); QVERIFY( h->isEmpty() ); delete h; // parse a parameter list h = new Parametrized( 0, "filename=genome.jpeg;\n modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"" ); QCOMPARE( h->parameter( "filename" ), QString( "genome.jpeg" ) ); QCOMPARE( h->parameter( "modification-date" ), QString( "Wed, 12 Feb 1997 16:29:51 -0500" ) ); QCOMPARE( h->as7BitString( false ), QByteArray( "filename=\"genome.jpeg\"; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"" ) ); delete h; // quoting of whitespaces in parameter value h = new Parametrized(); h->setParameter( "boundary", "simple boundary" ); QCOMPARE( h->as7BitString( false ), QByteArray( "boundary=\"simple boundary\"" ) ); delete h; // TODO: test RFC 2047 encoded values // TODO: test case-insensitive key-names } void HeaderTest::testContentDispositionHeader() { ContentDisposition *h; // empty header h = new ContentDisposition(); QVERIFY( h->isEmpty() ); // set some values h->setFilename( "test.jpg" ); QVERIFY( h->isEmpty() ); QVERIFY( h->as7BitString( false ).isEmpty() ); h->setDisposition( CDattachment ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->as7BitString( false ), QByteArray( "attachment; filename=\"test.jpg\"" ) ); delete h; // parse parameter-less header h = new ContentDisposition( 0, "inline" ); QCOMPARE( h->disposition(), CDinline ); QVERIFY( h->filename().isEmpty() ); QCOMPARE( h->as7BitString( true ), QByteArray( "Content-Disposition: inline" ) ); delete h; // parse header with parameter h = new ContentDisposition( 0, "attachment; filename=genome.jpeg;\n modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";"); QCOMPARE( h->disposition(), CDattachment ); QCOMPARE( h->filename(), QString( "genome.jpeg" ) ); delete h; // TODO: test for case-insensitive disposition value } void HeaderTest::testContentTypeHeader() { ContentType* h; // empty header h = new ContentType(); QVERIFY( h->isEmpty() ); // Empty content-type means text/plain (RFC 2045 §5.2) QVERIFY( h->isPlainText() ); QVERIFY( h->isText() ); // set a mimetype h->setMimeType( "text/plain" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->mimeType(), QByteArray( "text/plain" ) ); QCOMPARE( h->mediaType(), QByteArray("text") ); QCOMPARE( h->subType(), QByteArray("plain") ); QVERIFY( h->isText() ); QVERIFY( h->isPlainText() ); QVERIFY( !h->isMultipart() ); QVERIFY( !h->isPartial() ); QVERIFY( h->isMediatype( "text" ) ); QVERIFY( h->isSubtype( "plain" ) ); QCOMPARE( h->as7BitString( true ), QByteArray( "Content-Type: text/plain" ) ); // add some parameters h->setId( "bla" ); h->setCharset( "us-ascii" ); QCOMPARE( h->as7BitString( false ), QByteArray( "text/plain; charset=\"us-ascii\"; id=\"bla\"" ) ); // clear header h->clear(); QVERIFY( h->isEmpty() ); delete h; // parse a complete header h = new ContentType( 0, "text/plain; charset=us-ascii (Plain text)" ); QVERIFY( h->isPlainText() ); QCOMPARE( h->charset(), QByteArray( "us-ascii" ) ); delete h; // bug #136631 (name with rfc 2231 style parameter wrapping) h = new ContentType( 0, "text/plain;\n name*0=\"PIN_Brief_box1@xx.xxx.censored_Konfigkarte.confi\";\n name*1=\"guration.txt\"" ); QVERIFY( h->isPlainText() ); QCOMPARE( h->name(), QString( "PIN_Brief_box1@xx.xxx.censored_Konfigkarte.configuration.txt" ) ); delete h; } void HeaderTest::testTokenHeader() { Token *h; // empty header h = new Token(); QVERIFY( h->isEmpty() ); // set a token h->setToken( "bla" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->as7BitString( false ), QByteArray( "bla" ) ); // clear it again h->clear(); QVERIFY( h->isEmpty() ); delete h; // parse a header h = new Token( 0, "value (comment)" ); QCOMPARE( h->token(), QByteArray("value") ); QCOMPARE( h->as7BitString( false ), QByteArray("value") ); delete h; } void HeaderTest::testContentTransferEncoding() { ContentTransferEncoding *h; // empty header h = new ContentTransferEncoding(); QVERIFY( h->isEmpty() ); // set an encoding h->setEncoding( CEbinary ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->as7BitString( true ), QByteArray("Content-Transfer-Encoding: binary") ); // clear again h->clear(); QVERIFY( h->isEmpty() ); delete h; // parse a header h = new ContentTransferEncoding( 0, "(comment) base64" ); QCOMPARE( h->encoding(), CEbase64 ); QCOMPARE( h->as7BitString( false ), QByteArray("base64") ); delete h; } void HeaderTest::testPhraseListHeader() { PhraseList *h; // empty header h = new PhraseList(); QVERIFY( h->isEmpty() ); delete h; // parse a simple phrase list h = new PhraseList( 0, "foo,\n bar" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->phrases().count(), 2 ); QStringList phrases = h->phrases(); QCOMPARE( phrases.takeFirst(), QString( "foo" ) ); QCOMPARE( phrases.takeFirst(), QString( "bar" ) ); QCOMPARE( h->as7BitString( false ), QByteArray("foo, bar") ); // clear header h->clear(); QVERIFY( h->isEmpty() ); delete h; // TODO: encoded/quoted phrases } void HeaderTest::testDotAtomHeader() { DotAtom *h; // empty header h = new DotAtom; QVERIFY( h->isEmpty() ); // parse a simple dot atom h->from7BitString( "1.0 (mime version)" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->asUnicodeString(), QString( "1.0" ) ); // clear again h->clear(); QVERIFY( h->isEmpty() ); delete h; // TODO: more complex atoms } void HeaderTest::testDateHeader() { Date *h; // empty header h = new Date(); QVERIFY( h->isEmpty() ); // parse a simple date h->from7BitString( "Fri, 21 Nov 1997 09:55:06 -0600" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->dateTime().date(), QDate( 1997, 11, 21 ) ); QCOMPARE( h->dateTime().time(), QTime( 9, 55, 6 ) ); QCOMPARE( h->dateTime().utcOffset(), -6 * 3600 ); QCOMPARE( h->as7BitString(), QByteArray( "Date: Fri, 21 Nov 1997 09:55:06 -0600" ) ); // clear it again h->clear(); QVERIFY( h->isEmpty() ); delete h; // white spaces and comment (from RFC 2822, Appendix A.5) h = new Date( 0, "Thu,\n 13\n Feb\n 1969\n 23:32\n -0330 (Newfoundland Time)" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->dateTime().date(), QDate( 1969, 2, 13 ) ); QCOMPARE( h->dateTime().time(), QTime( 23, 32 ) ); QCOMPARE( h->dateTime().utcOffset(), -12600 ); QCOMPARE( h->as7BitString( false ), QByteArray( "Thu, 13 Feb 1969 23:32 -0330" ) ); delete h; // obsolete date format (from RFC 2822, Appendix A.6.2) h = new Date( 0, "21 Nov 97 09:55:06 GMT" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->dateTime().date(), QDate( 1997, 11, 21 ) ); QCOMPARE( h->dateTime().time(), QTime( 9, 55, 6 ) ); QCOMPARE( h->dateTime().utcOffset(), 0 ); delete h; // obsolete whitespaces and commnets (from RFC 2822, Appendix A.6.3) h = new Date( 0, "Fri, 21 Nov 1997 09(comment): 55 : 06 -0600" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->dateTime().date(), QDate( 1997, 11, 21 ) ); QCOMPARE( h->dateTime().time(), QTime( 9, 55, 6 ) ); QCOMPARE( h->dateTime().utcOffset(), -6 * 3600 ); delete h; } void HeaderTest::testLinesHeader() { Lines *h; // empty header h = new Lines(); QVERIFY( h->isEmpty() ); QVERIFY( h->as7BitString().isEmpty() ); // set some content h->setNumberOfLines( 5 ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->as7BitString(), QByteArray( "Lines: 5" ) ); // clear again h->clear(); QVERIFY( h->isEmpty() ); delete h; // parse header with comment h = new Lines( 0, "(this is a comment) 10 (and yet another comment)" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->numberOfLines(), 10 ); delete h; } void HeaderTest::testNewsgroupsHeader() { Newsgroups *h; // empty header h = new Newsgroups(); QVERIFY( h->isEmpty() ); QVERIFY( h->as7BitString().isEmpty() ); // set newsgroups QList groups; groups << "gmane.comp.kde.devel.core" << "gmane.comp.kde.devel.buildsystem"; h->setGroups( groups ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->as7BitString(), QByteArray( "Newsgroups: gmane.comp.kde.devel.core,gmane.comp.kde.devel.buildsystem" ) ); // and clear again h->clear(); QVERIFY( h->isEmpty() ); delete h; // parse a header h = new Newsgroups( 0, "gmane.comp.kde.devel.core,gmane.comp.kde.devel.buildsystem" ); groups = h->groups(); QCOMPARE( groups.count(), 2 ); QCOMPARE( groups.takeFirst(), QByteArray("gmane.comp.kde.devel.core") ); QCOMPARE( groups.takeFirst(), QByteArray("gmane.comp.kde.devel.buildsystem") ); delete h; // same again, this time with whitespaces and comments h = new Newsgroups(); h->from7BitString( "(comment) gmane.comp.kde.devel.core (second comment),\n gmane.comp.kde.devel.buildsystem (that all)" ); groups = h->groups(); QCOMPARE( groups.count(), 2 ); QCOMPARE( groups.takeFirst(), QByteArray("gmane.comp.kde.devel.core") ); QCOMPARE( groups.takeFirst(), QByteArray("gmane.comp.kde.devel.buildsystem") ); delete h; } void HeaderTest::testControlHeader() { Control *h; // empty header h = new Control(); QVERIFY( h->isEmpty() ); QVERIFY( h->as7BitString().isEmpty() ); // set some content h->setCancel( "" ); QVERIFY( !h->isEmpty() ); QVERIFY( h->isCancel() ); QCOMPARE( h->as7BitString(), QByteArray( "Control: cancel " ) ); // clear again h->clear(); QVERIFY( h->isEmpty() ); delete h; // parse a control header h = new Control( 0, "cancel " ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->parameter(), QByteArray("") ); QVERIFY( h->isCancel() ); QCOMPARE( h->controlType(), QByteArray("cancel") ); delete h; } void HeaderTest::testReturnPath() { ReturnPath *h; h = new ReturnPath(); QVERIFY( h->isEmpty() ); QVERIFY( h->as7BitString().isEmpty() ); h->from7BitString( "" ); QVERIFY( !h->isEmpty() ); QCOMPARE( h->as7BitString( true ), QByteArray( "Return-Path: " ) ); delete h; } void HeaderTest::noAbstractHeaders() { From* h2 = new From(); delete h2; Sender* h3 = new Sender(); delete h3; To* h4 = new To(); delete h4; Cc* h5 = new Cc(); delete h5; Bcc* h6 = new Bcc(); delete h6; ReplyTo* h7 = new ReplyTo(); delete h7; Keywords* h8 = new Keywords(); delete h8; MIMEVersion* h9 = new MIMEVersion(); delete h9; MessageID* h10 = new MessageID(); delete h10; ContentID* h11 = new ContentID(); delete h11; Supersedes* h12 = new Supersedes(); delete h12; InReplyTo* h13 = new InReplyTo(); delete h13; References* h14 = new References(); delete h14; Generic* h15 = new Generic(); delete h15; Subject* h16 = new Subject(); delete h16; Organization* h17 = new Organization(); delete h17; ContentDescription* h18 = new ContentDescription(); delete h18; FollowUpTo* h22 = new FollowUpTo(); delete h22; UserAgent* h24 = new UserAgent(); delete h24; } +void HeaderTest::testInvalidButOkQEncoding() +{ + // A stray '?' should not confuse the parser + Subject subject; + subject.from7BitString( "=?us-ascii?q?Why?_Why_do_some_clients_violate_the_RFC?" "?=" ); + QCOMPARE( subject.as7BitString( false ), QByteArray( "Why? Why do some clients violate the RFC?" ) ); +} + +void HeaderTest::testInvalidQEncoding_data() +{ + QTest::addColumn("encodedWord"); + + // All examples below should not be treated as invalid encoded strings, since the '?=' is missing + QTest::newRow("") << QString( "=?us-ascii?q?Why?_Why_do_some_clients_violate_the_RFC??" ); + QTest::newRow("") << QString( "=?us-ascii?q?Why?_Why_do_some_clients_violate_the_RFC?" ); + QTest::newRow("") << QString( "=?us-ascii?q?Why?_Why_do_some_clients_violate_the_RFC" ); +} + +void HeaderTest::testInvalidQEncoding() +{ + using namespace HeaderParsing; + QFETCH( QString,encodedWord ); + + const char *data = encodedWord.toAscii().data(); + const char *start = data + 1; + const char *end = data + strlen( data ); + QString result; + QByteArray language; + QByteArray usedCS; + QVERIFY( !parseEncodedWord( start, end, result, language, usedCS ) ); +} + #include "headertest.moc" diff --git a/kmime/tests/headertest.h b/kmime/tests/headertest.h index 1ba3e9a24..760fd6932 100644 --- a/kmime/tests/headertest.h +++ b/kmime/tests/headertest.h @@ -1,53 +1,56 @@ /* Copyright (c) 2006 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 KMIME_HEADERTEST_H #define KMIME_HEADERTEST_H #include class HeaderTest : public QObject { Q_OBJECT private Q_SLOTS: void testIdentHeader(); void testAddressListHeader(); void testMailboxListHeader(); void testSingleMailboxHeader(); void testMailCopiesToHeader(); void testParametrizedHeader(); void testContentDispositionHeader(); void testContentTypeHeader(); void testTokenHeader(); void testContentTransferEncoding(); void testPhraseListHeader(); void testDotAtomHeader(); void testDateHeader(); void testLinesHeader(); void testNewsgroupsHeader(); void testControlHeader(); void testReturnPath(); + void testInvalidButOkQEncoding(); + void testInvalidQEncoding(); + void testInvalidQEncoding_data(); // makes sure we don't accidently have an abstract header class that's not // meant to be abstract void noAbstractHeaders(); }; #endif diff --git a/kmime/tests/kmime_charfreq_test.cpp b/kmime/tests/kmime_charfreq_test.cpp index fc6d9299b..9a0297a7d 100644 --- a/kmime/tests/kmime_charfreq_test.cpp +++ b/kmime/tests/kmime_charfreq_test.cpp @@ -1,154 +1,155 @@ /* Copyright (c) 2009 Constantin Berzan 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 "kmime_charfreq_test.h" #include #include #include using namespace KMime; QTEST_KDEMAIN( KMimeCharFreqTest, NoGUI ) void KMimeCharFreqTest::test8bitData() { { // If it has NUL then it's Binary (equivalent to EightBitData in CharFreq). QByteArray data( "123" ); data += char( 0 ); data += "test"; - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::Binary ); } { // If it has lines longer than 998, it's EightBitData. QByteArray data; for( int i = 0; i < 999; i++ ) { data += char( 169 ); } - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::EightBitData ); } { // If #CR != #CRLF then it's EightBitData. QByteArray data( "©line1\r\nline2\r" ); - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::EightBitData ); } { // If #LF != #CRLF then it's EightBitData. QByteArray data( "©line1\r\nline2\n" ); - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::EightBitData ); } { // If it has a lot of control chars, it's EightBitData. QByteArray data( "©test\a\a\a\a\a\a\a" ); - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::EightBitData ); } } void KMimeCharFreqTest::test8bitText() { { // If it has no NULs, few CTLs, no stray CRs or LFs, it's EightBitText. QByteArray data( "©beware the beast but enjoy the feast he offers...\r\n" ); - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::EightBitText ); } } void KMimeCharFreqTest::test7bitData() { { // If it has lines longer than 998, it's SevenBitData. QByteArray data; for( int i = 0; i < 999; i++ ) { data += 'a'; } - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::SevenBitData ); } { // If #CR != #CRLF then it's SevenBitData. QByteArray data( "line1\r\nline2\r" ); - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::SevenBitData ); } { // If #LF != #CRLF then it's SevenBitData. QByteArray data( "line1\r\nline2\n" ); - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::SevenBitData ); } + { + // If the text only contains newlines, then it is SevenBitText + QByteArray data( "line1\nline2\n" ); + CharFreq cf( data ); + QCOMPARE( cf.type(), CharFreq::SevenBitText ); + } + + { + // If the text only contains newlines and some random accented chars, then it is EightBitText + QByteArray data( "asdfasdfasdfasdfasdfasdfäöü\n" ); + CharFreq cf( data ); + QCOMPARE( cf.type(), CharFreq::EightBitText ); + } + { // If it has a lot of control chars, it's SevenBitData. QByteArray data( "test\a\a\a\a\a\a\a" ); - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::SevenBitData ); } } void KMimeCharFreqTest::test7bitText() { { // If it has no NULs, few CTLs, no stray CRs or LFs, it's SevenBitText. QByteArray data( "beware the beast but enjoy the feast he offers...\r\n" ); - kDebug() << data; CharFreq cf( data ); QCOMPARE( cf.type(), CharFreq::SevenBitText ); } } void KMimeCharFreqTest::testTrailingWhitespace() { QByteArray data( "test " ); - kDebug() << data; CharFreq cf( data ); QVERIFY( cf.hasTrailingWhitespace() ); } void KMimeCharFreqTest::testLeadingFrom() { QByteArray data( "From here thither" ); - kDebug() << data; CharFreq cf( data ); QVERIFY( cf.hasLeadingFrom() ); } #include "kmime_charfreq_test.moc" diff --git a/kmime/tests/rfc2047test.cpp b/kmime/tests/rfc2047test.cpp index 27ff7e16f..b49b10d7b 100644 --- a/kmime/tests/rfc2047test.cpp +++ b/kmime/tests/rfc2047test.cpp @@ -1,108 +1,111 @@ /* Copyright (c) 2006 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 version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "rfc2047test.h" #include "rfc2047test.moc" #include using namespace KMime; QTEST_KDEMAIN( RFC2047Test, NoGUI ) void RFC2047Test::testRFC2047decode() { QByteArray encCharset; // empty QCOMPARE( KMime::decodeRFC2047String( QByteArray(), encCharset, "utf-8", false ), QString() ); // identity QCOMPARE( KMime::decodeRFC2047String( "bla", encCharset, "utf-8", false ), QString( "bla" ) ); // utf-8 QCOMPARE( KMime::decodeRFC2047String( "=?utf-8?q?Ingo=20Kl=C3=B6cker?= ", encCharset, "utf-8", false ), QString::fromUtf8( "Ingo Klöcker " ) ); QCOMPARE( KMime::decodeRFC2047String( "=?utf-8?q?Ingo=20Kl=C3=B6cker?= ", encCharset, "iso8859-1", false ), QString::fromUtf8( "Ingo Klöcker " ) ); QCOMPARE( KMime::decodeRFC2047String( "=?utf-8?q?Ingo=20Kl=C3=B6cker?=", encCharset, "utf-8", false ), QString::fromUtf8( "Ingo Klöcker" ) ); QCOMPARE( encCharset, QByteArray( "UTF-8" ) ); // whitespaces between two encoded words QCOMPARE( KMime::decodeRFC2047String( "=?utf-8?q?Ingo=20Kl=C3=B6cker?= =?utf-8?q?Ingo=20Kl=C3=B6cker?=", encCharset, "utf-8", false ), QString::fromUtf8( "Ingo KlöckerIngo Klöcker" ) ); QCOMPARE( decodeRFC2047String( "=?utf-8?q?Ingo=20Kl=C3=B6cker?= foo =?utf-8?q?Ingo=20Kl=C3=B6cker?=", encCharset ), QString::fromUtf8( "Ingo Klöcker foo Ingo Klöcker" ) ); // iso-8859-x QCOMPARE( KMime::decodeRFC2047String( "=?ISO-8859-1?Q?Andr=E9s_Ot=F3n?=", encCharset, "utf-8", false ), QString::fromUtf8( "Andrés Otón" ) ); QCOMPARE( encCharset, QByteArray( "ISO-8859-1" ) ); QCOMPARE( KMime::decodeRFC2047String( "=?iso-8859-2?q?Rafa=B3_Rzepecki?=", encCharset, "utf-8", false ), QString::fromUtf8( "Rafał Rzepecki" ) ); QCOMPARE( encCharset, QByteArray( "ISO-8859-2" ) ); QCOMPARE( KMime::decodeRFC2047String( "=?iso-8859-9?Q?S=2E=C7a=F0lar?= Onur", encCharset, "utf-8", false ), QString::fromUtf8( "S.Çağlar Onur" ) ); QCOMPARE( encCharset, QByteArray( "ISO-8859-9" ) ); QCOMPARE( KMime::decodeRFC2047String( "Rafael =?iso-8859-15?q?Rodr=EDguez?=", encCharset, "utf-8", false ), QString::fromUtf8( "Rafael Rodríguez" ) ); QCOMPARE( encCharset, QByteArray( "ISO-8859-15" ) ); // wrong charset + charset overwrite QCOMPARE( KMime::decodeRFC2047String( "=?iso-8859-1?q?Ingo=20Kl=C3=B6cker?=", encCharset, "utf-8", true ), QString::fromUtf8( "Ingo Klöcker" ) ); // language parameter according to RFC 2231, section 5 QCOMPARE( decodeRFC2047String( "From: =?US-ASCII*EN?Q?Keith_Moore?= ", encCharset ), QString::fromUtf8( "From: Keith Moore " ) ); QCOMPARE( encCharset, QByteArray( "US-ASCII" ) ); // broken qp endoding (using lowercase) QCOMPARE( decodeRFC2047String( "Subject: =?iso-8859-1?Q?Belangrijk=3a=20Verhuizing=20FTP=20server?=", encCharset ), QString::fromUtf8( "Subject: Belangrijk: Verhuizing FTP server" ) ); QCOMPARE( encCharset, QByteArray( "ISO-8859-1" ) ); // mixed charsets, based on bug 125542 but pasted from above instead since I'm unable to enter those asian symbols QCOMPARE( decodeRFC2047String( "Subject: =?utf-8?q?Ingo=20Kl=C3=B6cker?= unencoded words =?iso-8859-9?Q?S=2E=C7a=F0lar?=", encCharset ), QString::fromUtf8( "Subject: Ingo Klöcker unencoded words S.Çağlar" ) ); QCOMPARE( encCharset, QByteArray( "ISO-8859-9" ) ); + + // Small data + QCOMPARE( decodeRFC2047String( "=?iso-8859-1?Q?c?=", encCharset ), QString::fromUtf8("c") ); } void RFC2047Test::testInvalidDecode() { QByteArray encCharset; // invalid / incomplete encoded data QCOMPARE( decodeRFC2047String( "=", encCharset ), QString::fromUtf8("=") ); QCOMPARE( decodeRFC2047String( "=?", encCharset ), QString::fromUtf8("=?") ); QCOMPARE( decodeRFC2047String( "=?a?b?=", encCharset ), QString::fromUtf8("=?a?b?=") ); QCOMPARE( decodeRFC2047String( "=?a?b?c?", encCharset ), QString::fromUtf8("=?a?b?c?") ); QCOMPARE( decodeRFC2047String( "=?a??c?=", encCharset ), QString::fromUtf8("=?a??c?=") ); } void RFC2047Test::testRFC2047encode() { // empty QCOMPARE( KMime::encodeRFC2047String( QString(), "utf-8" ), QByteArray() ); // identity QCOMPARE( KMime::encodeRFC2047String( "bla", "utf-8" ), QByteArray( "bla" ) ); // utf-8 // expected value is probably wrong, libkmime will chose 'B' instead of 'Q' encoding QEXPECT_FAIL( "", "libkmime will chose 'B' instead of 'Q' encoding", Continue ); QCOMPARE( KMime::encodeRFC2047String( QString::fromUtf8( "Ingo Klöcker " ), "utf-8" ).constData(), "=?utf-8?q?Ingo=20Kl=C3=B6cker?= " ); } diff --git a/kpimutils/email.cpp b/kpimutils/email.cpp index 2154820e1..3a8bcbce3 100644 --- a/kpimutils/email.cpp +++ b/kpimutils/email.cpp @@ -1,1063 +1,1064 @@ /* This file is part of the kpimutils library. Copyright (c) 2004 Matt Douhan 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. */ /** @file This file is part of the KDEPIM Utilities library and provides static methods for email address validation. @author Matt Douhan \ */ #include "email.h" #include #include #include #include #include #include using namespace KPIMUtils; //----------------------------------------------------------------------------- QStringList KPIMUtils::splitAddressList( const QString &aStr ) { // Features: // - always ignores quoted characters // - ignores everything (including parentheses and commas) // inside quoted strings // - supports nested comments // - ignores everything (including double quotes and commas) // inside comments QStringList list; if ( aStr.isEmpty() ) { return list; } QString addr; uint addrstart = 0; int commentlevel = 0; bool insidequote = false; for ( int index=0; index 0 ) { commentlevel--; } else { return list; } } break; case '\\' : // quoted character index++; // ignore the quoted character break; case ',' : + case ';' : if ( !insidequote && ( commentlevel == 0 ) ) { addr = aStr.mid( addrstart, index - addrstart ); if ( !addr.isEmpty() ) { list += addr.simplified(); } addrstart = index + 1; } break; } } // append the last address to the list if ( !insidequote && ( commentlevel == 0 ) ) { addr = aStr.mid( addrstart, aStr.length() - addrstart ); if ( !addr.isEmpty() ) { list += addr.simplified(); } } return list; } //----------------------------------------------------------------------------- // Used by KPIMUtils::splitAddress(...) and KPIMUtils::firstEmailAddress(...). KPIMUtils::EmailParseResult splitAddressInternal( const QByteArray address, QByteArray &displayName, QByteArray &addrSpec, QByteArray &comment, bool allowMultipleAddresses ) { // kDebug() << "address"; displayName = ""; addrSpec = ""; comment = ""; if ( address.isEmpty() ) { return AddressEmpty; } // The following is a primitive parser for a mailbox-list (cf. RFC 2822). // The purpose is to extract a displayable string from the mailboxes. // Comments in the addr-spec are not handled. No error checking is done. enum { TopLevel, InComment, InAngleAddress } context = TopLevel; bool inQuotedString = false; int commentLevel = 0; bool stop = false; for ( const char *p = address.data(); *p && !stop; ++p ) { switch ( context ) { case TopLevel : { switch ( *p ) { case '"' : inQuotedString = !inQuotedString; displayName += *p; break; case '(' : if ( !inQuotedString ) { context = InComment; commentLevel = 1; } else { displayName += *p; } break; case '<' : if ( !inQuotedString ) { context = InAngleAddress; } else { displayName += *p; } break; case '\\' : // quoted character displayName += *p; ++p; // skip the '\' if ( *p ) { displayName += *p; } else { return UnexpectedEnd; } break; case ',' : if ( !inQuotedString ) { if ( allowMultipleAddresses ) { stop = true; } else { return UnexpectedComma; } } else { displayName += *p; } break; default : displayName += *p; } break; } case InComment : { switch ( *p ) { case '(' : ++commentLevel; comment += *p; break; case ')' : --commentLevel; if ( commentLevel == 0 ) { context = TopLevel; comment += ' '; // separate the text of several comments } else { comment += *p; } break; case '\\' : // quoted character comment += *p; ++p; // skip the '\' if ( *p ) { comment += *p; } else { return UnexpectedEnd; } break; default : comment += *p; } break; } case InAngleAddress : { switch ( *p ) { case '"' : inQuotedString = !inQuotedString; addrSpec += *p; break; case '>' : if ( !inQuotedString ) { context = TopLevel; } else { addrSpec += *p; } break; case '\\' : // quoted character addrSpec += *p; ++p; // skip the '\' if ( *p ) { addrSpec += *p; } else { return UnexpectedEnd; } break; default : addrSpec += *p; } break; } } // switch ( context ) } // check for errors if ( inQuotedString ) { return UnbalancedQuote; } if ( context == InComment ) { return UnbalancedParens; } if ( context == InAngleAddress ) { return UnclosedAngleAddr; } displayName = displayName.trimmed(); comment = comment.trimmed(); addrSpec = addrSpec.trimmed(); if ( addrSpec.isEmpty() ) { if ( displayName.isEmpty() ) { return NoAddressSpec; } else { addrSpec = displayName; displayName.truncate( 0 ); } } /* kDebug() << "display-name : \"" << displayName << "\""; kDebug() << "comment : \"" << comment << "\""; kDebug() << "addr-spec : \"" << addrSpec << "\""; */ return AddressOk; } //----------------------------------------------------------------------------- EmailParseResult KPIMUtils::splitAddress( const QByteArray &address, QByteArray &displayName, QByteArray &addrSpec, QByteArray &comment ) { return splitAddressInternal( address, displayName, addrSpec, comment, false/* don't allow multiple addresses */ ); } //----------------------------------------------------------------------------- EmailParseResult KPIMUtils::splitAddress( const QString &address, QString &displayName, QString &addrSpec, QString &comment ) { QByteArray d, a, c; EmailParseResult result = splitAddress( address.toUtf8(), d, a, c ); if ( result == AddressOk ) { displayName = QString::fromUtf8( d ); addrSpec = QString::fromUtf8( a ); comment = QString::fromUtf8( c ); } return result; } //----------------------------------------------------------------------------- EmailParseResult KPIMUtils::isValidAddress( const QString &aStr ) { // If we are passed an empty string bail right away no need to process // further and waste resources if ( aStr.isEmpty() ) { return AddressEmpty; } // count how many @'s are in the string that is passed to us // if 0 or > 1 take action // at this point to many @'s cannot bail out right away since // @ is allowed in qoutes, so we use a bool to keep track // and then make a judgment further down in the parser // FIXME count only @ not in double quotes bool tooManyAtsFlag = false; int atCount = aStr.count( '@' ); if ( atCount > 1 ) { tooManyAtsFlag = true; } else if ( atCount == 0 ) { return TooFewAts; } // The main parser, try and catch all weird and wonderful // mistakes users and/or machines can create enum { TopLevel, InComment, InAngleAddress } context = TopLevel; bool inQuotedString = false; int commentLevel = 0; unsigned int strlen = aStr.length(); for ( unsigned int index=0; index < strlen; index++ ) { switch ( context ) { case TopLevel : { switch ( aStr[index].toLatin1() ) { case '"' : inQuotedString = !inQuotedString; break; case '(' : if ( !inQuotedString ) { context = InComment; commentLevel = 1; } break; case '[' : if ( !inQuotedString ) { return InvalidDisplayName; } break; case ']' : if ( !inQuotedString ) { return InvalidDisplayName; } break; case ':' : if ( !inQuotedString ) { return DisallowedChar; } break; case '<' : if ( !inQuotedString ) { context = InAngleAddress; } break; case '\\' : // quoted character ++index; // skip the '\' if ( ( index + 1 ) > strlen ) { return UnexpectedEnd; } break; case ',' : if ( !inQuotedString ) { return UnexpectedComma; } break; case ')' : if ( !inQuotedString ) { return UnbalancedParens; } break; case '>' : if ( !inQuotedString ) { return UnopenedAngleAddr; } break; case '@' : if ( !inQuotedString ) { if ( index == 0 ) { // Missing local part return MissingLocalPart; } else if ( index == strlen-1 ) { return MissingDomainPart; break; } } else if ( inQuotedString ) { --atCount; if ( atCount == 1 ) { tooManyAtsFlag = false; } } break; } break; } case InComment : { switch ( aStr[index].toLatin1() ) { case '(' : ++commentLevel; break; case ')' : --commentLevel; if ( commentLevel == 0 ) { context = TopLevel; } break; case '\\' : // quoted character ++index; // skip the '\' if ( ( index + 1 ) > strlen ) { return UnexpectedEnd; } break; } break; } case InAngleAddress : { switch ( aStr[index].toLatin1() ) { case ',' : if ( !inQuotedString ) { return UnexpectedComma; } break; case '"' : inQuotedString = !inQuotedString; break; case '@' : if ( inQuotedString ) { --atCount; if ( atCount == 1 ) { tooManyAtsFlag = false; } } break; case '>' : if ( !inQuotedString ) { context = TopLevel; break; } break; case '\\' : // quoted character ++index; // skip the '\' if ( ( index + 1 ) > strlen ) { return UnexpectedEnd; } break; } break; } } } if ( atCount == 0 && !inQuotedString ) { return TooFewAts; } if ( inQuotedString ) { return UnbalancedQuote; } if ( context == InComment ) { return UnbalancedParens; } if ( context == InAngleAddress ) { return UnclosedAngleAddr; } if ( tooManyAtsFlag ) { return TooManyAts; } return AddressOk; } //----------------------------------------------------------------------------- KPIMUtils::EmailParseResult KPIMUtils::isValidAddressList( const QString &aStr, QString &badAddr ) { if ( aStr.isEmpty() ) { return AddressEmpty; } const QStringList list = splitAddressList( aStr ); QStringList::const_iterator it = list.begin(); EmailParseResult errorCode = AddressOk; for ( it = list.begin(); it != list.end(); ++it ) { errorCode = isValidAddress( *it ); if ( errorCode != AddressOk ) { badAddr = ( *it ); break; } } return errorCode; } //----------------------------------------------------------------------------- QString KPIMUtils::emailParseResultToString( EmailParseResult errorCode ) { switch ( errorCode ) { case TooManyAts : return i18n( "The email address you entered is not valid because it " "contains more than one @. " "You will not create valid messages if you do not " "change your address." ); case TooFewAts : return i18n( "The email address you entered is not valid because it " "does not contain a @." "You will not create valid messages if you do not " "change your address." ); case AddressEmpty : return i18n( "You have to enter something in the email address field." ); case MissingLocalPart : return i18n( "The email address you entered is not valid because it " "does not contain a local part." ); case MissingDomainPart : return i18n( "The email address you entered is not valid because it " "does not contain a domain part." ); case UnbalancedParens : return i18n( "The email address you entered is not valid because it " "contains unclosed comments/brackets." ); case AddressOk : return i18n( "The email address you entered is valid." ); case UnclosedAngleAddr : return i18n( "The email address you entered is not valid because it " "contains an unclosed angle bracket." ); case UnopenedAngleAddr : return i18n( "The email address you entered is not valid because it " "contains too many closing angle brackets." ); case UnexpectedComma : return i18n( "The email address you have entered is not valid because it " "contains an unexpected comma." ); case UnexpectedEnd : return i18n( "The email address you entered is not valid because it ended " "unexpectedly. This probably means you have used an escaping " "type character like a '\\' as the last character in your " "email address." ); case UnbalancedQuote : return i18n( "The email address you entered is not valid because it " "contains quoted text which does not end." ); case NoAddressSpec : return i18n( "The email address you entered is not valid because it " "does not seem to contain an actual email address, i.e. " "something of the form joe@example.org." ); case DisallowedChar : return i18n( "The email address you entered is not valid because it " "contains an illegal character." ); case InvalidDisplayName : return i18n( "The email address you have entered is not valid because it " "contains an invalid display name." ); } return i18n( "Unknown problem with email address" ); } //----------------------------------------------------------------------------- bool KPIMUtils::isValidSimpleAddress( const QString &aStr ) { // If we are passed an empty string bail right away no need to process further // and waste resources if ( aStr.isEmpty() ) { return false; } int atChar = aStr.lastIndexOf( '@' ); QString domainPart = aStr.mid( atChar + 1 ); QString localPart = aStr.left( atChar ); bool tooManyAtsFlag = false; bool inQuotedString = false; int atCount = localPart.count( '@' ); unsigned int strlen = localPart.length(); for ( unsigned int index=0; index < strlen; index++ ) { switch( localPart[ index ].toLatin1() ) { case '"' : inQuotedString = !inQuotedString; break; case '@' : if ( inQuotedString ) { --atCount; if ( atCount == 0 ) { tooManyAtsFlag = false; } } break; } } QString addrRx = "[a-zA-Z]*[~|{}`\\^?=/+*'&%$#!_\\w.-]*[~|{}`\\^?=/+*'&%$#!_a-zA-Z0-9-]@"; if ( localPart[ 0 ] == '\"' || localPart[ localPart.length()-1 ] == '\"' ) { addrRx = "\"[a-zA-Z@]*[\\w.@-]*[a-zA-Z0-9@]\"@"; } if ( domainPart[ 0 ] == '[' || domainPart[ domainPart.length()-1 ] == ']' ) { addrRx += "\\[[0-9]{,3}(\\.[0-9]{,3}){3}\\]"; } else { addrRx += "[\\w-]+(\\.[\\w-]+)*"; } QRegExp rx( addrRx ); return rx.exactMatch( aStr ) && !tooManyAtsFlag; } //----------------------------------------------------------------------------- QString KPIMUtils::simpleEmailAddressErrorMsg() { return i18n( "The email address you entered is not valid because it " "does not seem to contain an actual email address, i.e. " "something of the form joe@example.org." ); } //----------------------------------------------------------------------------- QByteArray KPIMUtils::extractEmailAddress( const QByteArray &address ) { QByteArray dummy1, dummy2, addrSpec; EmailParseResult result = splitAddressInternal( address, dummy1, addrSpec, dummy2, false/* don't allow multiple addresses */ ); if ( result != AddressOk ) { addrSpec = QByteArray(); if ( result != AddressEmpty ) { kDebug() << "Input: aStr\nError:" << emailParseResultToString( result ); } } return addrSpec; } //----------------------------------------------------------------------------- QString KPIMUtils::extractEmailAddress( const QString &address ) { return QString::fromUtf8( extractEmailAddress( address.toUtf8() ) ); } //----------------------------------------------------------------------------- QByteArray KPIMUtils::firstEmailAddress( const QByteArray &addresses ) { QByteArray dummy1, dummy2, addrSpec; EmailParseResult result = splitAddressInternal( addresses, dummy1, addrSpec, dummy2, true/* allow multiple addresses */ ); if ( result != AddressOk ) { addrSpec = QByteArray(); if ( result != AddressEmpty ) { kDebug() << "Input: aStr\nError:" << emailParseResultToString( result ); } } return addrSpec; } //----------------------------------------------------------------------------- QString KPIMUtils::firstEmailAddress( const QString &addresses ) { return QString::fromUtf8( firstEmailAddress( addresses.toUtf8() ) ); } //----------------------------------------------------------------------------- bool KPIMUtils::extractEmailAddressAndName( const QString &aStr, QString &mail, QString &name ) { name.clear(); mail.clear(); const int len = aStr.length(); const char cQuotes = '"'; bool bInComment = false; bool bInQuotesOutsideOfEmail = false; int i=0, iAd=0, iMailStart=0, iMailEnd=0; QChar c; unsigned int commentstack = 0; // Find the '@' of the email address // skipping all '@' inside "(...)" comments: while ( i < len ) { c = aStr[i]; if ( '(' == c ) { commentstack++; } if ( ')' == c ) { commentstack--; } bInComment = commentstack != 0; if ( '"' == c && !bInComment ) { bInQuotesOutsideOfEmail = !bInQuotesOutsideOfEmail; } if( !bInComment && !bInQuotesOutsideOfEmail ) { if ( '@' == c ) { iAd = i; break; // found it } } ++i; } if ( !iAd ) { // We suppose the user is typing the string manually and just // has not finished typing the mail address part. // So we take everything that's left of the '<' as name and the rest as mail for ( i = 0; len > i; ++i ) { c = aStr[i]; if ( '<' != c ) { name.append( c ); } else { break; } } mail = aStr.mid( i + 1 ); if ( mail.endsWith( '>' ) ) { mail.truncate( mail.length() - 1 ); } } else { // Loop backwards until we find the start of the string // or a ',' that is outside of a comment // and outside of quoted text before the leading '<'. bInComment = false; bInQuotesOutsideOfEmail = false; for ( i = iAd-1; 0 <= i; --i ) { c = aStr[i]; if ( bInComment ) { if ( '(' == c ) { if ( !name.isEmpty() ) { name.prepend( ' ' ); } bInComment = false; } else { name.prepend( c ); // all comment stuff is part of the name } } else if ( bInQuotesOutsideOfEmail ) { if ( cQuotes == c ) { bInQuotesOutsideOfEmail = false; } else { name.prepend( c ); } } else { // found the start of this addressee ? if ( ',' == c ) { break; } // stuff is before the leading '<' ? if ( iMailStart ) { if ( cQuotes == c ) { bInQuotesOutsideOfEmail = true; // end of quoted text found } else { name.prepend( c ); } } else { switch ( c.toLatin1() ) { case '<': iMailStart = i; break; case ')': if ( !name.isEmpty() ) { name.prepend( ' ' ); } bInComment = true; break; default: if ( ' ' != c ) { mail.prepend( c ); } } } } } name = name.simplified(); mail = mail.simplified(); if ( mail.isEmpty() ) { return false; } mail.append( '@' ); // Loop forward until we find the end of the string // or a ',' that is outside of a comment // and outside of quoted text behind the trailing '>'. bInComment = false; bInQuotesOutsideOfEmail = false; int parenthesesNesting = 0; for ( i = iAd+1; len > i; ++i ) { c = aStr[i]; if ( bInComment ) { if ( ')' == c ) { if ( --parenthesesNesting == 0 ) { bInComment = false; if ( !name.isEmpty() ) { name.append( ' ' ); } } else { // nested ")", add it name.append( ')' ); // name can't be empty here } } else { if ( '(' == c ) { // nested "(" ++parenthesesNesting; } name.append( c ); // all comment stuff is part of the name } } else if ( bInQuotesOutsideOfEmail ) { if ( cQuotes == c ) { bInQuotesOutsideOfEmail = false; } else { name.append( c ); } } else { // found the end of this addressee ? if ( ',' == c ) { break; } // stuff is behind the trailing '>' ? if ( iMailEnd ){ if ( cQuotes == c ) { bInQuotesOutsideOfEmail = true; // start of quoted text found } else { name.append( c ); } } else { switch ( c.toLatin1() ) { case '>': iMailEnd = i; break; case '(': if ( !name.isEmpty() ) { name.append( ' ' ); } if ( ++parenthesesNesting > 0 ) { bInComment = true; } break; default: if ( ' ' != c ) { mail.append( c ); } } } } } } name = name.simplified(); mail = mail.simplified(); return ! ( name.isEmpty() || mail.isEmpty() ); } //----------------------------------------------------------------------------- bool KPIMUtils::compareEmail( const QString &email1, const QString &email2, bool matchName ) { QString e1Name, e1Email, e2Name, e2Email; extractEmailAddressAndName( email1, e1Email, e1Name ); extractEmailAddressAndName( email2, e2Email, e2Name ); return e1Email == e2Email && ( !matchName || ( e1Name == e2Name ) ); } //----------------------------------------------------------------------------- QString KPIMUtils::normalizedAddress( const QString &displayName, const QString &addrSpec, const QString &comment ) { if ( displayName.isEmpty() && comment.isEmpty() ) { return addrSpec; } else if ( comment.isEmpty() ) { if ( !displayName.startsWith( '\"' ) ) { return quoteNameIfNecessary( displayName ) + " <" + addrSpec + '>'; } else { return displayName + " <" + addrSpec + '>'; } } else if ( displayName.isEmpty() ) { QString commentStr = comment; return quoteNameIfNecessary( commentStr ) + " <" + addrSpec + '>'; } else { return displayName + " (" + comment + ") <" + addrSpec + '>'; } } //----------------------------------------------------------------------------- QString KPIMUtils::fromIdn( const QString &addrSpec ) { const int atPos = addrSpec.lastIndexOf( '@' ); if ( atPos == -1 ) { return addrSpec; } QString idn = KUrl::fromAce( addrSpec.mid( atPos + 1 ).toLatin1() ); if ( idn.isEmpty() ) { return QString(); } return addrSpec.left( atPos + 1 ) + idn; } //----------------------------------------------------------------------------- QString KPIMUtils::toIdn( const QString &addrSpec ) { const int atPos = addrSpec.lastIndexOf( '@' ); if ( atPos == -1 ) { return addrSpec; } QString idn = KUrl::toAce( addrSpec.mid( atPos + 1 ) ); if ( idn.isEmpty() ) { return addrSpec; } return addrSpec.left( atPos + 1 ) + idn; } //----------------------------------------------------------------------------- QString KPIMUtils::normalizeAddressesAndDecodeIdn( const QString &str ) { // kDebug() << str; if ( str.isEmpty() ) { return str; } const QStringList addressList = splitAddressList( str ); QStringList normalizedAddressList; QByteArray displayName, addrSpec, comment; for ( QStringList::ConstIterator it = addressList.begin(); ( it != addressList.end() ); ++it ) { if ( !(*it).isEmpty() ) { if ( splitAddress( (*it).toUtf8(), displayName, addrSpec, comment ) == AddressOk ) { displayName = KMime::decodeRFC2047String(displayName).toUtf8(); comment = KMime::decodeRFC2047String(comment).toUtf8(); normalizedAddressList << normalizedAddress( QString::fromUtf8( displayName ), fromIdn( QString::fromUtf8( addrSpec ) ), QString::fromUtf8( comment ) ); } } } /* kDebug() << "normalizedAddressList: \"" << normalizedAddressList.join( ", " ) << "\""; */ return normalizedAddressList.join( ", " ); } //----------------------------------------------------------------------------- QString KPIMUtils::normalizeAddressesAndEncodeIdn( const QString &str ) { //kDebug() << str; if ( str.isEmpty() ) { return str; } const QStringList addressList = splitAddressList( str ); QStringList normalizedAddressList; QByteArray displayName, addrSpec, comment; for ( QStringList::ConstIterator it = addressList.begin(); ( it != addressList.end() ); ++it ) { if ( !(*it).isEmpty() ) { if ( splitAddress( (*it).toUtf8(), displayName, addrSpec, comment ) == AddressOk ) { normalizedAddressList << normalizedAddress( QString::fromUtf8( displayName ), toIdn( QString::fromUtf8( addrSpec ) ), QString::fromUtf8( comment ) ); } } } /* kDebug() << "normalizedAddressList: \"" << normalizedAddressList.join( ", " ) << "\""; */ return normalizedAddressList.join( ", " ); } //----------------------------------------------------------------------------- // Escapes unescaped doublequotes in str. static QString escapeQuotes( const QString &str ) { if ( str.isEmpty() ) { return QString(); } QString escaped; // reserve enough memory for the worst case ( """..."" -> \"\"\"...\"\" ) escaped.reserve( 2 * str.length() ); unsigned int len = 0; for ( int i = 0; i < str.length(); ++i, ++len ) { if ( str[i] == '"' ) { // unescaped doublequote escaped[len] = '\\'; ++len; } else if ( str[i] == '\\' ) { // escaped character escaped[len] = '\\'; ++len; ++i; if ( i >= str.length() ) { // handle trailing '\' gracefully break; } } escaped[len] = str[i]; } escaped.truncate( len ); return escaped; } //----------------------------------------------------------------------------- QString KPIMUtils::quoteNameIfNecessary( const QString &str ) { QString quoted = str; QRegExp needQuotes( "[^ 0-9A-Za-z\\x0080-\\xFFFF]" ); // avoid double quoting if ( ( quoted[0] == '"' ) && ( quoted[quoted.length() - 1] == '"' ) ) { quoted = "\"" + escapeQuotes( quoted.mid( 1, quoted.length() - 2 ) ) + "\""; } else if ( quoted.indexOf( needQuotes ) != -1 ) { quoted = "\"" + escapeQuotes( quoted ) + "\""; } return quoted; }