diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e09232d4..2bff9e08b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,181 +1,182 @@ project(kdepimlibs) # where to look first for cmake modules. This line must be the first one or cmake will use the system's FindFoo.cmake set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules") ############### Build Options ############### option(KDEPIM_ONLY_KLEO "Only build the libraries needed by Kleopatra." FALSE) ############### The kdepimlibs version (used e.g. in KdepimLibsConfig.cmake) ############### set(KDEPIMLIBS_VERSION_MAJOR 4) set(KDEPIMLIBS_VERSION_MINOR 3) -set(KDEPIMLIBS_VERSION_PATCH 60) +set(KDEPIMLIBS_VERSION_PATCH 62) set(KDEPIMLIBS_VERSION ${KDEPIMLIBS_VERSION_MAJOR}.${KDEPIMLIBS_VERSION_MINOR}.${KDEPIMLIBS_VERSION_PATCH}) ############### search packages used by KDE ############### -find_package(KDE4 4.2.90 REQUIRED) +find_package(KDE4 4.3.62 REQUIRED) + include(KDE4Defaults) include(MacroLibrary) ############### Needed commands before building anything ############### add_definitions (${QT_DEFINITIONS} ${KDE4_DEFINITIONS}) include_directories (${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} ${KDE4_INCLUDES}) ############### Find the stuff we need ############### set(Boost_MINIMUM_VERSION "1.33.1") find_package(Boost) macro_log_feature(Boost_FOUND "Boost" "Boost C++ Libraries" "http://www.boost.org" TRUE "" "Required by several critical KDEPIM apps.") #FindGpgme.cmake already handles the log message but we must ensure it is required. find_package(Gpgme REQUIRED) # configure macros if (GPGME_FOUND) include (gpgme++/ConfigureChecks.cmake) endif (GPGME_FOUND) if (NOT KDEPIM_ONLY_KLEO) #FindAkonadi.cmake is only there for compatibility reasons, but we don't want to use that. - find_package(Akonadi 1.1.91 QUIET NO_MODULE) - macro_log_feature(Akonadi_FOUND "Akonadi" "Akonadi server libraries (from kdesupport)" "http://pim.kde.org/akonadi" TRUE "1.1.95" "Akonadi is required to build KdepimLibs.") + find_package(Akonadi 1.2.60 QUIET NO_MODULE) + macro_log_feature(Akonadi_FOUND "Akonadi" "Akonadi server libraries (from kdesupport)" "http://pim.kde.org/akonadi" TRUE "1.2.60" "Akonadi is required to build KdepimLibs.") find_package(Sasl2) macro_log_feature(SASL2_FOUND "cyrus-sasl" "Cyrus SASL API" "http://asg.web.cmu.edu/sasl/sasl-library.html" TRUE "" "Required to support authentication of logins in the IMAP and Sieve kioslaves.") include (ConfigureChecks.cmake) set(SHARED_MIME_INFO_MINIMUM_VERSION "0.30") find_package(SharedMimeInfo) macro_log_feature(SHARED_MIME_INFO_FOUND "SMI" "SharedMimeInfo" "http://freedesktop.org/wiki/Software/shared-mime-info" TRUE "0.30" "SharedMimeInfo is required.") endif (NOT KDEPIM_ONLY_KLEO) ############### Now, we add the KDEPIMLibs components ############### # These targets will always be built add_subdirectory(cmake) add_subdirectory(gpgme++) add_subdirectory(qgpgme) if (NOT KDEPIM_ONLY_KLEO) add_subdirectory(akonadi) add_subdirectory(kabc) add_subdirectory(kblog) add_subdirectory(kcal) add_subdirectory(kholidays) add_subdirectory(kimap) add_subdirectory(kioslave) add_subdirectory(kldap) add_subdirectory(kmime) add_subdirectory(kpimidentities) add_subdirectory(kpimutils) add_subdirectory(kpimtextedit) add_subdirectory(kresources) add_subdirectory(ktnef) add_subdirectory(kxmlrpcclient) add_subdirectory(mailtransport) add_subdirectory(messagecomposer) add_subdirectory(microblog) add_subdirectory(syndication) # Build the CamelCase headers add_subdirectory(includes) endif (NOT KDEPIM_ONLY_KLEO) # doc must be a subdir of kdepimlibs macro_optional_add_subdirectory(doc) # All done, let's display what we found... macro_display_feature_log() ############### Here we install some extra stuff ############### if (NOT KDEPIM_ONLY_KLEO) install(FILES kdepimlibs-mime.xml DESTINATION ${XDG_MIME_INSTALL_DIR}) update_xdg_mimetypes(${XDG_MIME_INSTALL_DIR}) endif (NOT KDEPIM_ONLY_KLEO) # now create the KdepimLibsConfig.cmake file, which will be loaded by # kdelibs/cmake/modules/FindKdepimLibs.cmake and which has to contain all information # about the installed kdepimlibs anybody would like to have. Alex # we need the absolute directories where stuff will be installed too # but since the variables which contain the destinations can be relative # or absolute paths, we need this macro to make them all absoulte, Alex macro(MAKE_INSTALL_PATH_ABSOLUTE out in) if (IS_ABSOLUTE "${in}") # IS_ABSOLUTE is new since cmake 2.4.8 set(${out} "${in}") else (IS_ABSOLUTE "${in}") set(${out} "\${KDEPIMLIBS_INSTALL_DIR}/${in}") endif (IS_ABSOLUTE "${in}") endmacro(MAKE_INSTALL_PATH_ABSOLUTE out in) # all the following variables are put into KdepimLibsConfig.cmake, so # they are usable by projects using kdepimlibs. Alex make_install_path_absolute(KDEPIMLIBS_DATA_DIR ${DATA_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_DBUS_INTERFACES_DIR ${DBUS_INTERFACES_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_DBUS_SERVICES_DIR ${DBUS_SERVICES_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_INCLUDE_DIR ${INCLUDE_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_LIB_DIR ${LIB_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_BIN_DIR ${BIN_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_LIBEXEC_DIR ${LIBEXEC_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_SBIN_DIR ${SBIN_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_HTML_DIR ${HTML_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_CONFIG_DIR ${CONFIG_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_ICON_DIR ${ICON_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_KCFG_DIR ${KCFG_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_LOCALE_DIR ${LOCALE_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_MIME_DIR ${MIME_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_SOUND_DIR ${SOUND_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_TEMPLATES_DIR ${TEMPLATES_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_WALLPAPER_DIR ${WALLPAPER_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_KCONF_UPDATE_DIR ${KCONF_UPDATE_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_AUTOSTART_DIR ${AUTOSTART_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_XDG_APPS_DIR ${XDG_APPS_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_XDG_DIRECTORY_DIR ${XDG_DIRECTORY_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_SYSCONF_DIR ${SYSCONF_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_MAN_DIR ${MAN_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_INFO_DIR ${INFO_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_SERVICES_DIR ${SERVICES_INSTALL_DIR}) make_install_path_absolute(KDEPIMLIBS_SERVICETYPES_DIR ${SERVICETYPES_INSTALL_DIR}) # Used in configure_file() and install(EXPORT) set(KDEPIMLIBS_TARGET_PREFIX KDEPIMLibs__) # this file is installed and contains all necessary information about the installed kdepimlibs, it also loads the file with the exported targets configure_file(KdepimLibsConfig.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/KdepimLibsConfig.cmake" @ONLY) # this file will be installed too and will be used by cmake when searching for the Config.cmake file to check the version of kdepimlibs, Alex macro_write_basic_cmake_version_file(${CMAKE_CURRENT_BINARY_DIR}/KdepimLibsConfigVersion.cmake ${KDEPIMLIBS_VERSION_MAJOR} ${KDEPIMLIBS_VERSION_MINOR} ${KDEPIMLIBS_VERSION_PATCH}) set(_KdepimLibsConfig_INSTALL_DIR ${LIB_INSTALL_DIR}/KdepimLibs/cmake) # places where find_package() looks for FooConfig.cmake files: # CMake >= 2.6.0 looks in lib/Foo*/cmake/, CMake >= 2.6.3 also looks in # lib/cmake/Foo*/, which packagers prefer. So they can set the KDE4_USE_COMMON_CMAKE_PACKAGE_CONFIG_DIR # option to have kdepimlibs install its Config file there. Alex if(KDE4_USE_COMMON_CMAKE_PACKAGE_CONFIG_DIR) set(_KdepimLibsConfig_INSTALL_DIR ${LIB_INSTALL_DIR}/cmake/KdepimLibs) endif(KDE4_USE_COMMON_CMAKE_PACKAGE_CONFIG_DIR) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/KdepimLibsConfigVersion.cmake ${CMAKE_CURRENT_BINARY_DIR}/KdepimLibsConfig.cmake DESTINATION ${_KdepimLibsConfig_INSTALL_DIR} ) # Install the file with the exported targets, use ${KDEPIMLIBS_TARGET_PREFIX} as prefix for the names of these targets, Alex install(EXPORT kdepimlibsLibraryTargets NAMESPACE ${KDEPIMLIBS_TARGET_PREFIX} DESTINATION ${_KdepimLibsConfig_INSTALL_DIR} FILE KDEPimLibsLibraryTargetsWithPrefix.cmake ) # Install a KDEPimLibsDependencies.cmake so people using kdepimlibs 4.2 with kdelibs < 4.2 get a useful error message, Alex file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/KDEPimLibsDependencies.cmake "\n message(FATAL_ERROR \"For using this version of kdepimlibs (${KDEPIMLIBS_VERSION}) you need a newer version of kdelibs, please update.\")\n") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/KDEPimLibsDependencies.cmake DESTINATION ${DATA_INSTALL_DIR}/cmake/modules) diff --git a/akonadi/CMakeLists.txt b/akonadi/CMakeLists.txt index 7c2cb65f8..3b73e988d 100644 --- a/akonadi/CMakeLists.txt +++ b/akonadi/CMakeLists.txt @@ -1,264 +1,266 @@ 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 + collectionfetchscope.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 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 + collectionfetchscope.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 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 6d6aea36b..c960c7dc7 100644 --- a/akonadi/agentbase.cpp +++ b/akonadi/agentbase.cpp @@ -1,602 +1,628 @@ /* 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(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)), + SLOT(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)) ); connect( mMonitor, SIGNAL( collectionRemoved( const Akonadi::Collection& ) ), SLOT( collectionRemoved( const Akonadi::Collection& ) ) ); connect( q, SIGNAL( status( int, const QString& ) ), q, SLOT( slotStatus( int, const QString& ) ) ); connect( q, SIGNAL( percent( int ) ), q, SLOT( slotPercent( int ) ) ); connect( q, SIGNAL( warning( const QString& ) ), q, SLOT( slotWarning( const QString& ) ) ); connect( q, SIGNAL( error( const QString& ) ), q, SLOT( slotError( const QString& ) ) ); // Use reference counting to allow agents to finish internal jobs when the // agent is stopped. KGlobal::ref(); KGlobal::setAllowQuit(true); QTimer::singleShot( 0, q, SLOT( delayedInit() ) ); } void AgentBasePrivate::delayedInit() { Q_Q( AgentBase ); if ( !QDBusConnection::sessionBus().registerService( QLatin1String( "org.freedesktop.Akonadi.Agent." ) + mId ) ) kFatal() << "Unable to register service at dbus:" << QDBusConnection::sessionBus().lastError().message(); q->setOnline( mOnline ); } void AgentBasePrivate::itemAdded( const Akonadi::Item &item, const Akonadi::Collection &collection ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; if ( mObserver != 0 ) mObserver->itemAdded( item, collection ); } void AgentBasePrivate::itemChanged( const Akonadi::Item &item, const QSet &partIdentifiers ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; if ( mObserver != 0 ) mObserver->itemChanged( item, partIdentifiers ); } void AgentBasePrivate::itemMoved( const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &dest ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; 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::collectionMoved( const Akonadi::Collection &collection, 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->collectionRemoved( collection ); + else if ( dest.resource() == q_ptr->identifier() ) // moved to us + mObserver->collectionAdded( collection, dest ); + else // not for us, not sure if we should get here at all + changeProcessed(); + return; + } + } + // either incomplete information or intra-resource move + // ### we cannot just call collectionRemoved here as this will already trigger changeProcessed() + // so, just collectionAdded() is good enough as no resource can have implemented intra-resource moves anyway + // without using Observer2 + mObserver->collectionAdded( collection, dest ); + } +} + void AgentBasePrivate::collectionRemoved( const Akonadi::Collection &collection ) { kDebug() << "mObserver=" << (void*) mObserver << "this=" << (void*) this; if ( mObserver != 0 ) mObserver->collectionRemoved( collection ); } void AgentBasePrivate::changeProcessed() { mMonitor->changeProcessed(); QTimer::singleShot( 0, mMonitor, SLOT( replayNext() ) ); } void AgentBasePrivate::slotStatus( int status, const QString &message ) { mStatusMessage = message; mStatusCode = 0; switch ( status ) { case AgentBase::Idle: if ( mStatusMessage.isEmpty() ) mStatusMessage = defaultReadyMessage(); mStatusCode = 0; break; case AgentBase::Running: if ( mStatusMessage.isEmpty() ) mStatusMessage = defaultSyncingMessage(); mStatusCode = 1; break; case AgentBase::Broken: if ( mStatusMessage.isEmpty() ) mStatusMessage = defaultErrorMessage(); mStatusCode = 2; break; default: Q_ASSERT( !"Unknown status passed" ); break; } } void AgentBasePrivate::slotPercent( int progress ) { mProgress = progress; } void AgentBasePrivate::slotWarning( const QString& message ) { mTracer->warning( QString::fromLatin1( "AgentBase(%1)" ).arg( mId ), message ); } void AgentBasePrivate::slotError( const QString& message ) { mTracer->error( QString::fromLatin1( "AgentBase(%1)" ).arg( mId ), message ); } void AgentBasePrivate::slotNetworkStatusChange( Solid::Networking::Status stat ) { Q_Q( AgentBase ); q->setOnline( stat == Solid::Networking::Connected ); } AgentBase::AgentBase( const QString & id ) : d_ptr( new AgentBasePrivate( this ) ) { sAgentBase = this; d_ptr->mId = id; d_ptr->init(); if ( KApplication::kApplication() ) KApplication::kApplication()->disableSessionManagement(); } AgentBase::AgentBase( AgentBasePrivate* d, const QString &id ) : d_ptr( d ) { sAgentBase = this; d_ptr->mId = id; d_ptr->init(); } AgentBase::~AgentBase() { delete d_ptr; } QString AgentBase::parseArguments( int argc, char **argv ) { QString identifier; if ( argc < 3 ) { kDebug() << "Not enough arguments passed..."; exit( 1 ); } for ( int i = 1; i < argc - 1; ++i ) { if ( QLatin1String( argv[ i ] ) == QLatin1String( "--identifier" ) ) identifier = QLatin1String( argv[ i + 1 ] ); } if ( identifier.isEmpty() ) { kDebug() << "Identifier argument missing"; exit( 1 ); } QByteArray catalog; char *p = strrchr( argv[0], '/' ); if ( p ) catalog = QByteArray( p + 1 ); else catalog = QByteArray( argv[0] ); KCmdLineArgs::init( argc, argv, identifier.toLatin1(), catalog, ki18n("Akonadi Agent"),"0.1" , ki18n("Akonadi Agent") ); KCmdLineOptions options; options.add("identifier ", ki18n("Agent identifier")); KCmdLineArgs::addCmdLineOptions( options ); return identifier; } // @endcond int AgentBase::init( AgentBase *r ) { QApplication::setQuitOnLastWindowClosed( false ); KGlobal::locale()->insertCatalog( QLatin1String("libakonadi") ); int rv = kapp->exec(); delete r; return rv; } int AgentBase::status() const { Q_D( const AgentBase ); return d->mStatusCode; } QString AgentBase::statusMessage() const { Q_D( const AgentBase ); return d->mStatusMessage; } int AgentBase::progress() const { Q_D( const AgentBase ); return d->mProgress; } QString AgentBase::progressMessage() const { Q_D( const AgentBase ); return d->mProgressMessage; } bool AgentBase::isOnline() const { Q_D( const AgentBase ); return d->mOnline; } void AgentBase::setNeedsNetwork( bool needsNetwork ) { Q_D( AgentBase ); d->mNeedsNetwork = needsNetwork; if ( d->mNeedsNetwork ) { connect( Solid::Networking::notifier() , SIGNAL( statusChanged( Solid::Networking::Status ) ) , this, SLOT( slotNetworkStatusChange( Solid::Networking::Status ) ) ); } else { disconnect( Solid::Networking::notifier(), 0, 0, 0 ); setOnline( true ); } } void AgentBase::setOnline( bool state ) { Q_D( AgentBase ); d->mOnline = state; d->mSettings->setValue( QLatin1String( "Agent/Online" ), state ); doSetOnline( state ); emit onlineChanged( state ); } void AgentBase::doSetOnline( bool online ) { Q_UNUSED( online ); } void AgentBase::configure( WId windowId ) { Q_UNUSED( windowId ); } #ifdef Q_OS_WIN //krazy:exclude=cpp void AgentBase::configure( qlonglong windowId ) { configure( reinterpret_cast( windowId ) ); } #endif WId AgentBase::winIdForDialogs() const { bool registered = QDBusConnection::sessionBus().interface()->isServiceRegistered( QLatin1String("org.freedesktop.akonaditray") ); if ( !registered ) return 0; QDBusInterface dbus( QLatin1String("org.freedesktop.akonaditray"), QLatin1String("/Actions"), QLatin1String("org.freedesktop.Akonadi.Tray") ); QDBusMessage reply = dbus.call( QLatin1String("getWinId") ); if ( reply.type() == QDBusMessage::ErrorMessage ) return 0; WId winid = (WId)reply.arguments().at( 0 ).toLongLong(); return winid; } void AgentBase::quit() { Q_D( AgentBase ); aboutToQuit(); if ( d->mSettings ) { d->mMonitor->setConfig( 0 ); d->mSettings->sync(); } KGlobal::deref(); } void AgentBase::aboutToQuit() { } void AgentBase::cleanup() { Q_D( AgentBase ); // prevent the monitor from picking up deletion signals for our own data if we are a resource // and thus avoid that we kill our own data as last act before our own death d->mMonitor->blockSignals( true ); aboutToQuit(); const QString fileName = d->mSettings->fileName(); /* * First destroy the settings object... */ d->mMonitor->setConfig( 0 ); delete d->mSettings; d->mSettings = 0; /* * ... then remove the file from hd. */ QFile::remove( fileName ); /* * ... and also remove the agent configuration file if there is one. */ QString configFile = KStandardDirs::locateLocal( "config", KGlobal::config()->name() ); QFile::remove( configFile ); KGlobal::deref(); } void AgentBase::registerObserver( Observer *observer ) { kDebug() << "observer=" << (void*) observer << "this=" << (void*) this; d_ptr->mObserver = observer; } QString AgentBase::identifier() const { return d_ptr->mId; } void AgentBase::setAgentName( const QString &name ) { Q_D( AgentBase ); if ( name == d->mName ) return; // TODO: rename collection d->mName = name; if ( d->mName.isEmpty() || d->mName == d->mId ) { d->mSettings->remove( QLatin1String( "Resource/Name" ) ); d->mSettings->remove( QLatin1String( "Agent/Name" ) ); } else d->mSettings->setValue( QLatin1String( "Agent/Name" ), d->mName ); d->mSettings->sync(); emit agentNameChanged( d->mName ); } QString AgentBase::agentName() const { Q_D( const AgentBase ); if ( d->mName.isEmpty() ) return d->mId; else return d->mName; } void AgentBase::changeProcessed() { Q_D( AgentBase ); d->changeProcessed(); } ChangeRecorder * AgentBase::changeRecorder() const { return d_ptr->mMonitor; } void AgentBase::abort() { emit abortRequested(); } void AgentBase::reconfigure() { emit reloadConfiguration(); } #include "agentbase.moc" #include "agentbase_p.moc" diff --git a/akonadi/agentbase_p.h b/akonadi/agentbase_p.h index 3d45fd3bc..0070ac35a 100644 --- a/akonadi/agentbase_p.h +++ b/akonadi/agentbase_p.h @@ -1,107 +1,108 @@ /* Copyright (c) 2007 Volker Krause Copyright (c) 2008 Kevin Krammer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_AGENTBASE_P_H #define AKONADI_AGENTBASE_P_H #include "agentbase.h" #include "tracerinterface.h" #include #include class QSettings; namespace Akonadi { /** * @internal */ class AgentBasePrivate : public QObject { Q_OBJECT public: AgentBasePrivate( AgentBase *parent ); virtual ~AgentBasePrivate(); void init(); virtual void delayedInit(); void slotStatus( int status, const QString &message ); void slotPercent( int progress ); void slotWarning( const QString& message ); void slotError( const QString& message ); void slotNetworkStatusChange( Solid::Networking::Status ); virtual void changeProcessed(); QString defaultReadyMessage() const { if ( mOnline ) return i18nc( "@info:status, application ready for work", "Ready" ); return i18nc( "@info:status", "Offline" ); } QString defaultSyncingMessage() const { return i18nc( "@info:status", "Syncing..." ); } QString defaultErrorMessage() const { return i18nc( "@info:status", "Error." ); } AgentBase *q_ptr; Q_DECLARE_PUBLIC( AgentBase ) QString mId; QString mName; int mStatusCode; QString mStatusMessage; uint mProgress; QString mProgressMessage; bool mNeedsNetwork; bool mOnline; QSettings *mSettings; ChangeRecorder *mMonitor; org::freedesktop::Akonadi::Tracer *mTracer; AgentBase::Observer *mObserver; protected Q_SLOTS: void itemAdded( const Akonadi::Item &item, const Akonadi::Collection &collection ); void itemChanged( const Akonadi::Item &item, const QSet &partIdentifiers ); void itemMoved( const Akonadi::Item &, const Akonadi::Collection &source, const Akonadi::Collection &destination ); void itemRemoved( const Akonadi::Item &item ); void collectionAdded( const Akonadi::Collection &collection, const Akonadi::Collection &parent ); void collectionChanged( const Akonadi::Collection &collection ); + void collectionMoved( const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination ); void collectionRemoved( const Akonadi::Collection &collection ); }; } #endif diff --git a/akonadi/collection.cpp b/akonadi/collection.cpp index b907e3e39..a82be3b74 100644 --- a/akonadi/collection.cpp +++ b/akonadi/collection.cpp @@ -1,237 +1,231 @@ /* 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 "collection.h" #include "collection_p.h" #include "attributefactory.h" #include "cachepolicy.h" #include "collectionrightsattribute_p.h" #include "collectionstatistics.h" #include "entity_p.h" #include #include #include #include #include #include using namespace Akonadi; class CollectionRoot : public Collection { public: CollectionRoot() : Collection( 0 ) { QStringList types; types << Collection::mimeType(); setContentMimeTypes( types ); // The root collection is read-only for the users Collection::Rights rights; rights |= Collection::ReadOnly; setRights( rights ); } }; K_GLOBAL_STATIC( CollectionRoot, s_root ) Collection::Collection() : Entity( new CollectionPrivate ) { Q_D( Collection ); static int lastId = -1; d->mId = lastId--; } Collection::Collection( Id id ) : Entity( new CollectionPrivate( id ) ) { } Collection::Collection(const Collection & other) : Entity( other ) { } Collection::~Collection() { } QString Collection::name( ) const { return d_func()->name; } void Collection::setName( const QString & name ) { Q_D( Collection ); d->name = name; } Collection::Rights Collection::rights() const { CollectionRightsAttribute *attr = attribute(); if ( attr ) return attr->rights(); else return AllRights; } void Collection::setRights( Rights rights ) { CollectionRightsAttribute *attr = attribute( AddIfMissing ); attr->setRights( rights ); } QStringList Collection::contentMimeTypes() const { return d_func()->contentTypes; } void Collection::setContentMimeTypes( const QStringList & types ) { Q_D( Collection ); d->contentTypes = types; d->contentTypesChanged = true; } Collection::Id Collection::parent() const { - return d_func()->parentId; + return parentCollection().id(); } void Collection::setParent( Id parent ) { - Q_D( Collection ); - d->parentId = parent; + parentCollection().setId( parent ); } void Collection::setParent(const Collection & collection) { - Q_D( Collection ); - d->parentId = collection.id(); - d->parentRemoteId = collection.remoteId(); + setParentCollection( collection ); } QString Collection::parentRemoteId() const { - return d_func()->parentRemoteId; + return parentCollection().remoteId(); } void Collection::setParentRemoteId(const QString & remoteParent) { - Q_D( Collection ); - d->parentRemoteId = remoteParent; + parentCollection().setRemoteId( remoteParent ); } KUrl Collection::url() const { KUrl url; url.setProtocol( QString::fromLatin1("akonadi") ); url.addQueryItem( QLatin1String("collection"), QString::number( id() ) ); return url; } Collection Collection::fromUrl( const KUrl &url ) { if ( url.protocol() != QLatin1String( "akonadi" ) ) return Collection(); const QString colStr = url.queryItem( QLatin1String( "collection" ) ); bool ok = false; Collection::Id colId = colStr.toLongLong( &ok ); if ( !ok ) return Collection(); if ( colId == 0 ) return Collection::root(); return Collection( colId ); } Collection Collection::root() { return *s_root; } QString Collection::mimeType( ) { return QString::fromLatin1("inode/directory"); } QString Collection::resource() const { return d_func()->resource; } void Collection::setResource(const QString & resource) { Q_D( Collection ); d->resource = resource; } uint qHash( const Akonadi::Collection &collection ) { return qHash( collection.id() ); } QDebug operator <<( QDebug d, const Akonadi::Collection &collection ) { return d << "Collection ID:" << collection.id() << " remote ID:" << collection.remoteId() << endl << " name:" << collection.name() << endl << " url:" << collection.url() << endl - << " parent ID:" << collection.parent() - << " parent remote ID: " << collection.parentRemoteId() << endl + << " parent:" << collection.parentCollection().id() << collection.parentCollection().remoteId() << endl << " resource:" << collection.resource() << endl - << " mime type:" << collection.mimeType() << endl << " rights:" << collection.rights() << endl << " contents mime type:" << collection.contentMimeTypes() << endl << " " << collection.cachePolicy() << endl << " " << collection.statistics(); } CollectionStatistics Collection::statistics() const { return d_func()->statistics; } void Collection::setStatistics(const CollectionStatistics & statistics) { Q_D( Collection ); d->statistics = statistics; } CachePolicy Collection::cachePolicy() const { return d_func()->cachePolicy; } void Collection::setCachePolicy(const CachePolicy & cachePolicy) { Q_D( Collection ); d->cachePolicy = cachePolicy; d->cachePolicyChanged = true; } AKONADI_DEFINE_PRIVATE( Akonadi::Collection ) diff --git a/akonadi/collection.h b/akonadi/collection.h index 7d5bb71fa..433daed24 100644 --- a/akonadi/collection.h +++ b/akonadi/collection.h @@ -1,238 +1,243 @@ /* 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_COLLECTION_H #define AKONADI_COLLECTION_H #include "akonadi_export.h" #include #include #include class KUrl; namespace Akonadi { class CachePolicy; class CollectionPrivate; class CollectionStatistics; /** * @short Represents a collection of PIM items. * * This class represents a collection of PIM items, such as a folder on a mail- or * groupware-server. * * Collections are hierarchical, i.e., they may have a parent collection. * * @code * * using namespace Akonadi; * * // fetching all collections recursive, starting at the root collection * CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); * if ( job->exec() ) { * Collection::List collections = job->collections(); * foreach( const Collection &collection, collections ) { * qDebug() << "Name:" << collection.name(); * } * } * * @endcode * * @author Volker Krause * * @see \ref akonadi_concepts_collections "Akonadi Collection Concept" */ class AKONADI_EXPORT Collection : public Entity { public: /** * Describes a list of collections. */ typedef QList List; /** * Describes rights of a collection. */ enum Right { ReadOnly = 0x0, ///< Can only read items or subcollection of this collection CanChangeItem = 0x1, ///< Can change items in this collection CanCreateItem = 0x2, ///< Can create new items in this collection CanDeleteItem = 0x4, ///< Can delete items in this collection CanChangeCollection = 0x8, ///< Can change subcollections in this collection CanCreateCollection = 0x10, ///< Can create new subcollections in this collection CanDeleteCollection = 0x20, ///< Can delete subcollections in this collection AllRights = (CanChangeItem | CanCreateItem | CanDeleteItem | CanChangeCollection | CanCreateCollection | CanDeleteCollection) ///< Has all rights on this collection }; Q_DECLARE_FLAGS(Rights, Right) /** * Creates an invalid collection. */ Collection(); /** * Create a new collection. * * @param id The unique identifier of the collection. */ explicit Collection( Id id ); /** * Destroys the collection. */ ~Collection(); /** * Creates a collection from an @p other collection. */ Collection( const Collection &other ); /** * Creates a collection from the given @p url. */ static Collection fromUrl( const KUrl &url ); /** * Returns the i18n'ed name of the collection. */ QString name() const; /** * Sets the i18n'ed name of the collection. * * @param name The new collection name. */ void setName( const QString &name ); /** * Returns the rights the user has on the collection. */ Rights rights() const; /** * Sets the @p rights the user has on the collection. */ void setRights( Rights rights ); /** * Returns a list of possible content mimetypes, * e.g. message/rfc822, x-akonadi/collection for a mail folder that * supports sub-folders. */ QStringList contentMimeTypes() const; /** * Sets the list of possible content mime @p types. */ void setContentMimeTypes( const QStringList &types ); /** * Returns the identifier of the parent collection. + * @deprecated Use parentCollection() */ - Id parent() const; + KDE_DEPRECATED Id parent() const; /** * Sets the identifier of the @p parent collection. + * @deprecated Use setParentCollection() */ - void setParent( Id parent ); + KDE_DEPRECATED void setParent( Id parent ); /** * Sets the parent @p collection. + * @deprecated Use setParentCollection() */ - void setParent( const Collection &collection ); + KDE_DEPRECATED void setParent( const Collection &collection ); /** * Returns the parent remote identifier. * @note This usually returns nothing for collections retrieved from the backend. + * @deprecated Use parentCollection() */ - QString parentRemoteId() const; + KDE_DEPRECATED QString parentRemoteId() const; /** * Sets the parent's remote @p identifier. + * @deprecated Use setParentCollection() */ - void setParentRemoteId( const QString &identifier ); + KDE_DEPRECATED void setParentRemoteId( const QString &identifier ); /** * Returns the root collection. */ static Collection root(); /** * Returns the mimetype used for collections. */ static QString mimeType(); /** * Returns the identifier of the resource owning the collection. */ QString resource() const; /** * Sets the @p identifier of the resource owning the collection. */ void setResource( const QString &identifier ); /** * Returns the cache policy of the collection. */ CachePolicy cachePolicy() const; /** * Sets the cache @p policy of the collection. */ void setCachePolicy( const CachePolicy &policy ); /** * Returns the collection statistics of the collection. */ CollectionStatistics statistics() const; /** * Sets the collection @p statistics for the collection. */ void setStatistics( const CollectionStatistics &statistics ); /** * Returns the collection url */ KUrl url() const; private: AKONADI_DECLARE_PRIVATE( Collection ) friend class CollectionFetchJob; friend class CollectionModifyJob; }; } AKONADI_EXPORT uint qHash( const Akonadi::Collection &collection ); /** * Allows to output a collection for debugging purposes. */ AKONADI_EXPORT QDebug operator<<( QDebug d, const Akonadi::Collection &collection ); Q_DECLARE_METATYPE(Akonadi::Collection) Q_DECLARE_METATYPE(Akonadi::Collection::List) #endif diff --git a/akonadi/collection_p.h b/akonadi/collection_p.h index a9255aaa2..1de805e7d 100644 --- a/akonadi/collection_p.h +++ b/akonadi/collection_p.h @@ -1,96 +1,91 @@ /* 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. */ #ifndef AKONADI_COLLECTION_P_H #define AKONADI_COLLECTION_P_H #include "collection.h" #include "cachepolicy.h" #include "collectionstatistics.h" #include "entity_p.h" #include "qstringlist.h" using namespace Akonadi; /** * @internal */ class Akonadi::CollectionPrivate : public EntityPrivate { public: CollectionPrivate( Collection::Id id = -1 ) : EntityPrivate( id ), - parentId( -1 ), contentTypesChanged( false ), cachePolicyChanged( false ) {} CollectionPrivate( const CollectionPrivate &other ) : EntityPrivate( other ) { - parentId = other.parentId; name = other.name; - parentRemoteId = other.parentRemoteId; resource = other.resource; statistics = other.statistics; contentTypes = other.contentTypes; cachePolicy = other.cachePolicy; contentTypesChanged = other.contentTypesChanged; cachePolicyChanged = other.cachePolicyChanged; } ~CollectionPrivate() { } CollectionPrivate* clone() const { return new CollectionPrivate( *this ); } void resetChangeLog() { contentTypesChanged = false; cachePolicyChanged = false; EntityPrivate::resetChangeLog(); } static Collection newRoot() { Collection root( 0 ); QStringList types; types << Collection::mimeType(); root.setContentMimeTypes( types ); return root; } - Collection::Id parentId; QString name; - QString parentRemoteId; QString resource; CollectionStatistics statistics; QStringList contentTypes; static const Collection root; CachePolicy cachePolicy; bool contentTypesChanged; bool cachePolicyChanged; }; #endif diff --git a/akonadi/collectionfetchjob.cpp b/akonadi/collectionfetchjob.cpp index 579997d4a..bf8958ef2 100644 --- a/akonadi/collectionfetchjob.cpp +++ b/akonadi/collectionfetchjob.cpp @@ -1,241 +1,245 @@ /* 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 "collectionfetchscope.h" #include #include #include #include using namespace Akonadi; class Akonadi::CollectionFetchJobPrivate : public JobPrivate { public: CollectionFetchJobPrivate( CollectionFetchJob *parent ) - : JobPrivate( parent ), - mUnsubscribed( false ), - mStatistics( false ) + : JobPrivate( parent ) { } Q_DECLARE_PUBLIC( CollectionFetchJob ) CollectionFetchJob::Type mType; Collection mBase; Collection::List mBaseList; Collection::List mCollections; - QString mResource; - QList mMimeTypes; + CollectionFetchScope mScope; 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 ) + if ( d->mScope.includeUnubscribed() ) 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() ) { + if ( !d->mScope.resource().isEmpty() ) { command += "RESOURCE \""; - command += d->mResource.toUtf8(); + command += d->mScope.resource().toUtf8(); command += '"'; } - if ( !d->mMimeTypes.isEmpty() ) { + if ( !d->mScope.contentMimeTypes().isEmpty() ) { command += " MIMETYPE ("; - command += ImapParser::join( d->mMimeTypes, " " ); + QList mts; + foreach ( const QString &mt, d->mScope.contentMimeTypes() ) + mts.append( mt.toUtf8() ); + command += ImapParser::join( mts, " " ); command += ')'; } - if ( d->mStatistics ) { + if ( d->mScope.includeStatistics() ) { 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() << "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() ); + d->mScope.setResource( resource ); } 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; + d->mScope.setIncludeUnsubscribed( include ); } void CollectionFetchJob::includeStatistics(bool include) { Q_D( CollectionFetchJob ); - d->mStatistics = include; + d->mScope.setIncludeStatistics( include ); +} + +void CollectionFetchJob::setFetchScope( const CollectionFetchScope &scope ) +{ + Q_D( CollectionFetchJob ); + d->mScope = scope; +} + +CollectionFetchScope& CollectionFetchJob::fetchScope() +{ + Q_D( CollectionFetchJob ); + return d->mScope; } #include "collectionfetchjob.moc" diff --git a/akonadi/collectionfetchjob.h b/akonadi/collectionfetchjob.h index 10c140766..9e99635d7 100644 --- a/akonadi/collectionfetchjob.h +++ b/akonadi/collectionfetchjob.h @@ -1,157 +1,180 @@ /* 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_COLLECTIONFETCHJOB_H #define AKONADI_COLLECTIONFETCHJOB_H #include "akonadi_export.h" #include #include namespace Akonadi { +class CollectionFetchScope; class CollectionFetchJobPrivate; /** * @short Job that fetches collections from the Akonadi storage. * * This class can be used to retrieve the complete or partial collection tree * from the Akonadi storage. * * @code * * using namespace Akonadi; * * // fetching all collections recursive, starting at the root collection * CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); * if ( job->exec() ) { * Collection::List collections = job->collections(); * foreach( const Collection &collection, collections ) { * qDebug() << "Name:" << collection.name(); * } * } * * @endcode * * @author Volker Krause */ class AKONADI_EXPORT CollectionFetchJob : public Job { Q_OBJECT public: /** * Describes the type of fetch depth. */ enum Type { Base, ///< Only fetch the base collection. FirstLevel, ///< Only list direct sub-collections of the base collection. Recursive ///< List all sub-collections. }; /** * Creates a new collection fetch job. If the given base collection * has a unique identifier, this is used to identify the collection in the * Akonadi server. If only a remote identifier is avaiable the collection * is identified using that, given a resource search context has been * specified. There two ways of doing that: by calling setResource() * or globally using Akonadi::ResourceSelectJob. * * @param collection The base collection for the listing. * @param type The type of fetch depth. * @param parent The parent object. */ explicit CollectionFetchJob( const Collection &collection, Type type = FirstLevel, QObject *parent = 0 ); /** * Creates a new collection fetch job to retrieve a list of collections. * The same rules for identifiers apply as noted in the constructor above. * * @param collections A list of collections to fetch. Must not be empty. * @param parent The parent object. */ explicit CollectionFetchJob( const Collection::List &collections, QObject *parent = 0 ); /** * Destroys the collection fetch job. */ virtual ~CollectionFetchJob(); /** * Returns the list of fetched collection. */ Collection::List collections() const; /** * Sets a resource identifier to limit collection listing to one resource. * * @param resource The resource identifier. + * @deprecated Use CollectionFetchScope instead. */ - void setResource( const QString &resource ); + KDE_DEPRECATED void setResource( const QString &resource ); /** - * Sets a content mimetype filter, that is include only collections in the result - * that match the given filter. - * @param contentMimeTypes A list of content mimetypes. + * Include also unsubscribed collections. + * @deprecated Use CollectionFetchScope instead. + */ + KDE_DEPRECATED void includeUnsubscribed( bool include = true ); + + /** + * Include also statistics about the collections. * - * @since 4.4 + * @since 4.3 + * @deprecated Use CollectionFetchScope instead. */ - void setContentMimeTypes( const QStringList &contentMimeTypes ); + KDE_DEPRECATED void includeStatistics( bool include = true ); /** - * Include also unsubscribed collections. + * Sets the collection fetch scope. + * + * The CollectionFetchScope controls how much of a collection's data is fetched + * from the server as well as filter to select which collections to fetch. + * + * @param fetchScope The new scope for collection fetch operations. + * + * @see fetchScope() + * @since 4.4 */ - void includeUnsubscribed( bool include = true ); + void setFetchScope( const CollectionFetchScope &fetchScope ); /** - * Include also statistics about the collections. + * Returns the collection fetch scope. * - * @since 4.3 + * 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 CollectionFetchScope documentation + * for an example. + * + * @return a reference to the current collection fetch scope + * + * @see setFetchScope() for replacing the current collection fetch scope + * @since 4.4 */ - void includeStatistics( bool include = true ); + CollectionFetchScope &fetchScope(); Q_SIGNALS: /** * This signal is emitted whenever the job has received collections. * * @param collections The received collections. */ void collectionsReceived( const Akonadi::Collection::List &collections ); protected: virtual void doStart(); virtual void doHandleResponse( const QByteArray &tag, const QByteArray &data ); protected Q_SLOTS: //@cond PRIVATE void slotResult( KJob* job ); //@endcond private: Q_DECLARE_PRIVATE( CollectionFetchJob ) //@cond PRIVATE Q_PRIVATE_SLOT( d_func(), void timeout() ) //@endcond }; } #endif diff --git a/akonadi/collectionmodel.cpp b/akonadi/collectionmodel.cpp index 11274207c..00e571788 100644 --- a/akonadi/collectionmodel.cpp +++ b/akonadi/collectionmodel.cpp @@ -1,316 +1,316 @@ /* 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. */ #include "collectionmodel.h" #include "collectionmodel_p.h" #include "collectionutils_p.h" #include "collectionmodifyjob.h" #include "entitydisplayattribute.h" #include "monitor.h" #include "pastehelper_p.h" #include "session.h" #include #include #include #include #include using namespace Akonadi; CollectionModel::CollectionModel( QObject * parent ) : QAbstractItemModel( parent ), d_ptr( new CollectionModelPrivate( this ) ) { Q_D( CollectionModel ); d->init(); } //@cond PRIVATE CollectionModel::CollectionModel( CollectionModelPrivate *d, QObject *parent ) : QAbstractItemModel( parent ), d_ptr( d ) { d->init(); } //@endcond CollectionModel::~CollectionModel() { Q_D( CollectionModel ); d->childCollections.clear(); d->collections.clear(); delete d->monitor; d->monitor = 0; delete d; } int CollectionModel::columnCount( const QModelIndex & parent ) const { if (parent.isValid() && parent.column() != 0) return 0; return 1; } QVariant CollectionModel::data( const QModelIndex & index, int role ) const { Q_D( const CollectionModel ); if ( !index.isValid() ) return QVariant(); const Collection col = d->collections.value( index.internalId() ); if ( !col.isValid() ) return QVariant(); if ( index.column() == 0 && (role == Qt::DisplayRole || role == Qt::EditRole) ) { if ( col.hasAttribute() && !col.attribute()->displayName().isEmpty() ) return col.attribute()->displayName(); return col.name(); } switch ( role ) { case Qt::DecorationRole: if ( index.column() == 0 ) { if ( col.hasAttribute() && !col.attribute()->iconName().isEmpty() ) return col.attribute()->icon(); return KIcon( CollectionUtils::defaultIconName( col ) ); } break; case OldCollectionIdRole: // fall-through case CollectionIdRole: return col.id(); case OldCollectionRole: // fall-through case CollectionRole: return QVariant::fromValue( col ); } return QVariant(); } QModelIndex CollectionModel::index( int row, int column, const QModelIndex & parent ) const { Q_D( const CollectionModel ); if (column >= columnCount() || column < 0) return QModelIndex(); QList list; if ( !parent.isValid() ) list = d->childCollections.value( Collection::root().id() ); else { if (parent.column() > 0) return QModelIndex(); list = d->childCollections.value( parent.internalId() ); } if ( row < 0 || row >= list.size() ) return QModelIndex(); if ( !d->collections.contains( list.at(row) ) ) return QModelIndex(); return createIndex( row, column, reinterpret_cast( d->collections.value( list.at(row) ).id() ) ); } QModelIndex CollectionModel::parent( const QModelIndex & index ) const { Q_D( const CollectionModel ); if ( !index.isValid() ) return QModelIndex(); Collection col = d->collections.value( index.internalId() ); if ( !col.isValid() ) return QModelIndex(); - Collection parentCol = d->collections.value( col.parent() ); + Collection parentCol = d->collections.value( col.parentCollection().id() ); if ( !parentCol.isValid() ) { return QModelIndex(); } QList list; - list = d->childCollections.value( parentCol.parent() ); + list = d->childCollections.value( parentCol.parentCollection().id() ); int parentRow = list.indexOf( parentCol.id() ); if ( parentRow < 0 ) return QModelIndex(); return createIndex( parentRow, 0, reinterpret_cast( parentCol.id() ) ); } int CollectionModel::rowCount( const QModelIndex & parent ) const { const Q_D( CollectionModel ); QList list; if ( parent.isValid() ) list = d->childCollections.value( parent.internalId() ); else list = d->childCollections.value( Collection::root().id() ); return list.size(); } QVariant CollectionModel::headerData( int section, Qt::Orientation orientation, int role ) const { const Q_D( CollectionModel ); if ( section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole ) return d->headerContent; return QAbstractItemModel::headerData( section, orientation, role ); } bool CollectionModel::setHeaderData( int section, Qt::Orientation orientation, const QVariant &value, int role ) { Q_D( CollectionModel ); if ( section == 0 && orientation == Qt::Horizontal && role == Qt::EditRole ) { d->headerContent = value.toString(); return true; } return false; } bool CollectionModel::setData( const QModelIndex & index, const QVariant & value, int role ) { Q_D( CollectionModel ); if ( index.column() == 0 && role == Qt::EditRole ) { // rename collection Collection col = d->collections.value( index.internalId() ); if ( !col.isValid() || value.toString().isEmpty() ) return false; col.setName( value.toString() ); CollectionModifyJob *job = new CollectionModifyJob( col, d->session ); connect( job, SIGNAL(result(KJob*)), SLOT(editDone(KJob*)) ); return true; } return QAbstractItemModel::setData( index, value, role ); } Qt::ItemFlags CollectionModel::flags( const QModelIndex & index ) const { Q_D( const CollectionModel ); // Pass modeltest. // http://labs.trolltech.com/forums/topic/79 if (!index.isValid()) return 0; Qt::ItemFlags flags = QAbstractItemModel::flags( index ); flags = flags | Qt::ItemIsDragEnabled; Collection col; if ( index.isValid() ) { col = d->collections.value( index.internalId() ); Q_ASSERT( col.isValid() ); } else return flags | Qt::ItemIsDropEnabled; // HACK Workaround for a probable bug in Qt if ( col.isValid() ) { if ( col.rights() & (Collection::CanChangeCollection | Collection::CanCreateCollection | Collection::CanDeleteCollection | Collection::CanCreateItem) ) { if ( index.column() == 0 ) flags = flags | Qt::ItemIsEditable; flags = flags | Qt::ItemIsDropEnabled; } } return flags; } Qt::DropActions CollectionModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList CollectionModel::mimeTypes() const { return QStringList() << QLatin1String( "text/uri-list" ); } QMimeData *CollectionModel::mimeData(const QModelIndexList &indexes) const { QMimeData *data = new QMimeData(); KUrl::List urls; foreach ( const QModelIndex &index, indexes ) { if ( index.column() != 0 ) continue; urls << Collection( index.internalId() ).url(); } urls.populateMimeData( data ); return data; } bool CollectionModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) { Q_D( CollectionModel ); if ( !(action & supportedDropActions()) ) return false; // handle drops onto items as well as drops between items QModelIndex idx; if ( row >= 0 && column >= 0 ) idx = index( row, column, parent ); else idx = parent; if ( !idx.isValid() ) return false; const Collection parentCol = d->collections.value( idx.internalId() ); if (!parentCol.isValid()) return false; KJob *job = PasteHelper::paste( data, parentCol, action != Qt::MoveAction ); connect( job, SIGNAL(result(KJob*)), SLOT(dropResult(KJob*)) ); return true; } Collection CollectionModel::collectionForId(Collection::Id id) const { Q_D( const CollectionModel ); return d->collections.value( id ); } void CollectionModel::fetchCollectionStatistics(bool enable) { Q_D( CollectionModel ); d->fetchStatistics = enable; d->monitor->fetchCollectionStatistics( enable ); } void CollectionModel::includeUnsubscribed(bool include) { Q_D( CollectionModel ); d->unsubscribed = include; } #include "collectionmodel.moc" diff --git a/akonadi/collectionmodel_p.cpp b/akonadi/collectionmodel_p.cpp index 7e7a896f5..095a35bda 100644 --- a/akonadi/collectionmodel_p.cpp +++ b/akonadi/collectionmodel_p.cpp @@ -1,334 +1,335 @@ /* 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 +#include "collectionfetchscope.h" 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(); + Collection::Id oldParentId = collections.value( collection.id() ).parentCollection().id(); + Collection::Id newParentId = collection.parentCollection().id(); 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 ); + job->fetchScope().setIncludeUnsubscribed( unsubscribed ); + job->fetchScope().setIncludeStatistics( 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 ); + job->fetchScope().setIncludeUnsubscribed( unsubscribed ); + job->fetchScope().setIncludeStatistics( 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() << "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() << "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() << "Job error: " << job->errorString() << endl; } emit q->listDone(); } void CollectionModelPrivate::editDone( KJob * job ) { if ( job->error() ) { kWarning() << "Edit failed: " << job->errorString(); } } void CollectionModelPrivate::dropResult(KJob * job) { if ( job->error() ) { 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_newChildCollections[ col.parentCollection().id() ].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(); + Collection::Id parentId = collections.value( id ).parentCollection().id(); // 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() << "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 ); + job->fetchScope().setIncludeUnsubscribed( unsubscribed ); + job->fetchScope().setIncludeStatistics( 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/collectionmodifyjob.cpp b/akonadi/collectionmodifyjob.cpp index 5d31fe331..848076355 100644 --- a/akonadi/collectionmodifyjob.cpp +++ b/akonadi/collectionmodifyjob.cpp @@ -1,92 +1,92 @@ /* 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 "collectionmodifyjob.h" #include "imapparser_p.h" #include "job_p.h" #include "protocolhelper_p.h" #include "collectionstatistics.h" #include "collection_p.h" using namespace Akonadi; class Akonadi::CollectionModifyJobPrivate : public JobPrivate { public: CollectionModifyJobPrivate( CollectionModifyJob *parent ) : JobPrivate( parent ) { } Collection mCollection; }; CollectionModifyJob::CollectionModifyJob( const Collection &collection, QObject * parent ) : Job( new CollectionModifyJobPrivate( this ), parent ) { Q_D( CollectionModifyJob ); d->mCollection = collection; } CollectionModifyJob::~CollectionModifyJob() { } void CollectionModifyJob::doStart() { Q_D( CollectionModifyJob ); if ( !d->mCollection.isValid() && d->mCollection.remoteId().isEmpty() ) { setError( Unknown ); setErrorText( QLatin1String( "Invalid collection" ) ); emitResult(); return; } QByteArray command = d->newTag(); if ( d->mCollection.isValid() ) command += " MODIFY " + QByteArray::number( d->mCollection.id() ); else command += " RID MODIFY " + ImapParser::quote( d->mCollection.remoteId().toUtf8() ); QByteArray changes; if ( d->mCollection.d_func()->contentTypesChanged ) { QList bList; foreach( const QString &s, d->mCollection.contentMimeTypes() ) bList << s.toLatin1(); changes += " MIMETYPE (" + ImapParser::join( bList, " " ) + ')'; } - if ( d->mCollection.parent() >= 0 ) - changes += " PARENT " + QByteArray::number( d->mCollection.parent() ); + if ( d->mCollection.parentCollection().id() >= 0 ) + changes += " PARENT " + QByteArray::number( d->mCollection.parentCollection().id() ); if ( !d->mCollection.name().isEmpty() ) changes += " NAME " + ImapParser::quote( d->mCollection.name().toUtf8() ); if ( !d->mCollection.remoteId().isNull() ) changes += " REMOTEID " + ImapParser::quote( d->mCollection.remoteId().toUtf8() ); if ( d->mCollection.d_func()->cachePolicyChanged ) changes += ' ' + ProtocolHelper::cachePolicyToByteArray( d->mCollection.cachePolicy() ); if ( d->mCollection.attributes().count() > 0 ) changes += ' ' + ProtocolHelper::attributesToByteArray( d->mCollection ); foreach ( const QByteArray &b, d->mCollection.d_func()->mDeletedAttributes ) changes += " -" + b; if ( changes.isEmpty() ) { emitResult(); return; } command += changes + '\n'; d->writeData( command ); } #include "collectionmodifyjob.moc" diff --git a/akonadi/collectionpathresolver.cpp b/akonadi/collectionpathresolver.cpp index 1afeeb4b1..dfce3ac83 100644 --- a/akonadi/collectionpathresolver.cpp +++ b/akonadi/collectionpathresolver.cpp @@ -1,203 +1,203 @@ /* 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 "collectionpathresolver_p.h" #include "collectionfetchjob.h" #include "job_p.h" #include #include using namespace Akonadi; //@cond PRIVATE class Akonadi::CollectionPathResolverPrivate : public JobPrivate { public: CollectionPathResolverPrivate( CollectionPathResolver *parent ) : JobPrivate( parent ) { } void jobResult( KJob* ); QStringList splitPath( const QString &path ) { if ( path.isEmpty() ) // path is normalized, so non-empty means at least one hit return QStringList(); QStringList rv; int begin = 0; for ( int i = 0; i < path.size(); ++i ) { if ( path[i] == QLatin1Char('/') ) { QString pathElement = path.mid( begin, i - begin ); pathElement = pathElement.replace( QLatin1String( "\\/" ), QLatin1String( "/" ) ); rv.append( pathElement ); begin = i + 1; } if ( i < path.size() - 2 && path[i] == QLatin1Char('\\') && path[i + 1] == QLatin1Char('/') ) ++i; } QString pathElement = path.mid( begin ); pathElement = pathElement.replace( QLatin1String( "\\/" ), QLatin1String( "/" ) ); rv.append( pathElement ); return rv; } Q_DECLARE_PUBLIC( CollectionPathResolver ) Collection::Id mColId; QString mPath; bool mPathToId; QStringList mPathParts; Collection mCurrentNode; }; void CollectionPathResolverPrivate::jobResult(KJob *job ) { if ( job->error() ) return; Q_Q( CollectionPathResolver ); CollectionFetchJob *list = static_cast( job ); CollectionFetchJob *nextJob = 0; const Collection::List cols = list->collections(); if ( cols.isEmpty() ) { q->setError( CollectionPathResolver::Unknown ); q->setErrorText( i18n( "No such collection.") ); q->emitResult(); return; } if ( mPathToId ) { const QString currentPart = mPathParts.takeFirst(); bool found = false; foreach ( const Collection &c, cols ) { if ( c.name() == currentPart ) { mCurrentNode = c; found = true; break; } } if ( !found ) { q->setError( CollectionPathResolver::Unknown ); q->setErrorText( i18n( "No such collection.") ); q->emitResult(); return; } if ( mPathParts.isEmpty() ) { mColId = mCurrentNode.id(); q->emitResult(); return; } nextJob = new CollectionFetchJob( mCurrentNode, CollectionFetchJob::FirstLevel, q ); } else { Collection col = list->collections().first(); - mCurrentNode = Collection( col.parent() ); + mCurrentNode = col.parentCollection(); mPathParts.prepend( col.name() ); if ( mCurrentNode == Collection::root() ) { q->emitResult(); return; } nextJob = new CollectionFetchJob( mCurrentNode, CollectionFetchJob::Base, q ); } q->connect( nextJob, SIGNAL(result(KJob*)), q, SLOT(jobResult(KJob*)) ); } CollectionPathResolver::CollectionPathResolver(const QString & path, QObject * parent) : Job( new CollectionPathResolverPrivate( this ), parent ) { Q_D( CollectionPathResolver ); d->mPathToId = true; d->mPath = path; if ( d->mPath.startsWith( pathDelimiter() ) ) d->mPath = d->mPath.right( d->mPath.length() - pathDelimiter().length() ); if ( d->mPath.endsWith( pathDelimiter() ) ) d->mPath = d->mPath.left( d->mPath.length() - pathDelimiter().length() ); d->mPathParts = d->splitPath( d->mPath ); d->mCurrentNode = Collection::root(); } CollectionPathResolver::CollectionPathResolver(const Collection & collection, QObject * parent) : Job( new CollectionPathResolverPrivate( this ), parent ) { Q_D( CollectionPathResolver ); d->mPathToId = false; d->mColId = collection.id(); d->mCurrentNode = collection; } CollectionPathResolver::~CollectionPathResolver() { } Collection::Id CollectionPathResolver::collection() const { Q_D( const CollectionPathResolver ); return d->mColId; } QString CollectionPathResolver::path() const { Q_D( const CollectionPathResolver ); if ( d->mPathToId ) return d->mPath; return d->mPathParts.join( pathDelimiter() ); } QString CollectionPathResolver::pathDelimiter() { return QLatin1String( "/" ); } void CollectionPathResolver::doStart() { Q_D( CollectionPathResolver ); CollectionFetchJob *job = 0; if ( d->mPathToId ) { if ( d->mPath.isEmpty() ) { d->mColId = Collection::root().id(); emitResult(); return; } job = new CollectionFetchJob( d->mCurrentNode, CollectionFetchJob::FirstLevel, this ); } else { if ( d->mColId == 0 ) { d->mColId = Collection::root().id(); emitResult(); return; } job = new CollectionFetchJob( d->mCurrentNode, CollectionFetchJob::Base, this ); } connect( job, SIGNAL(result(KJob*)), SLOT(jobResult(KJob*)) ); } //@endcond #include "collectionpathresolver_p.moc" diff --git a/akonadi/collectionsync.cpp b/akonadi/collectionsync.cpp index 8071eb11b..62a3f05d9 100644 --- a/akonadi/collectionsync.cpp +++ b/akonadi/collectionsync.cpp @@ -1,236 +1,532 @@ /* - Copyright (c) 2007 Volker Krause + Copyright (c) 2007, 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 "collectionsync_p.h" #include "collection.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionfetchjob.h" #include "collectionmodifyjob.h" - +#include "collectionfetchscope.h" #include +#include using namespace Akonadi; +struct RemoteNode; + +/** + LocalNode is used to build a tree structure of all our locally existing collections. +*/ +struct LocalNode +{ + LocalNode( const Collection &col ) : + collection( col ), + parentNode( 0 ), + processed( false ) + {} + + ~LocalNode() + { + qDeleteAll( childNodes ); + qDeleteAll( pendingRemoteNodes ); + } + + Collection collection; + LocalNode *parentNode; // ### really needed? + QList childNodes; + QHash childRidMap; + /** When using hierarchical RIDs we attach a list of not yet processable remote nodes to + the closest already existing local ancestor node. They will be re-evaluated once a new + child node is added. */ + QList pendingRemoteNodes; + bool processed; +}; + +Q_DECLARE_METATYPE( LocalNode* ) +static const char LOCAL_NODE[] = "LocalNode"; + +/** + RemoteNode is used as a container for remote collections which typically don't have a UID set + and thus cannot easily be compared or put into maps etc. +*/ +struct RemoteNode +{ + RemoteNode( const Collection &col ) : + collection( col ) + {} + + Collection collection; +}; + +Q_DECLARE_METATYPE( RemoteNode* ) +static const char REMOTE_NODE[] = "RemoteNode"; + /** * @internal */ class CollectionSync::Private { public: - Private() : + Private( CollectionSync *parent ) : + q( parent ), pendingJobs( 0 ), incremental( false ), - streaming( false ) + streaming( false ), + hierarchicalRIDs( false ), + localListDone( false ), + deliveryDone( false ) { + localRoot = new LocalNode( Collection::root() ); + localUidMap.insert( localRoot->collection.id(), localRoot ); + if ( !hierarchicalRIDs ) + localRidMap.insert( QString(), localRoot ); } - QString resourceId; - - // local: mapped remote id -> collection, id -> collection - QHash localCollections; - QSet unprocessedLocalCollections; + ~Private() + { + delete localRoot; + } - // remote: mapped id -> collection - QHash remoteCollections; + /** Create a local node from the given local collection and integrate it into the local tree structure. */ + LocalNode* createLocalNode( const Collection &col ) + { + LocalNode *node = new LocalNode( col ); + Q_ASSERT( !localUidMap.contains( col.id() ) ); + localUidMap.insert( node->collection.id(), node ); + if ( !hierarchicalRIDs ) + localRidMap.insert( node->collection.remoteId(), node ); + + // add already existing children + if ( localPendingCollections.contains( col.id() ) ) { + QList childIds = localPendingCollections.take( col.id() ); + foreach ( Collection::Id childId, childIds ) { + Q_ASSERT( localUidMap.contains( childId ) ); + LocalNode *childNode = localUidMap.value( childId ); + node->childNodes.append( childNode ); + node->childRidMap.insert( childNode->collection.remoteId(), childNode ); + } + } - // remote collections waiting for a parent - QList orphanRemoteCollections; + // set our parent and add ourselves as child + if ( localUidMap.contains( col.parentCollection().id() ) ) { + node->parentNode = localUidMap.value( col.parentCollection().id() ); + node->parentNode->childNodes.append( node ); + } else { + localPendingCollections[ col.parentCollection().id() ].append( col.id() ); + } - // removed remote collections - Collection::List removedRemoteCollections; + return node; + } - // create counter - int pendingJobs; + /** Same as createLocalNode() for remote collections. */ + void createRemoteNode( const Collection &col ) + { + if ( col.remoteId().isEmpty() ) { + kWarning() << "Collection '" << col.name() << "' does not have a remote identifier - skipping"; + return; + } + RemoteNode *node = new RemoteNode( col ); + localRoot->pendingRemoteNodes.append( node ); + } - bool incremental; - bool streaming; -}; + /** Create local nodes as we receive the local listing from the Akonadi server. */ + void localCollectionsReceived( const Akonadi::Collection::List &localCols ) + { + foreach ( const Collection &c, localCols ) + createLocalNode( c ); + } -CollectionSync::CollectionSync( const QString &resourceId, QObject *parent ) : - TransactionSequence( parent ), - d( new Private ) -{ - d->resourceId = resourceId; -} + /** Once the local collection listing finished we can continue with the interesting stuff. */ + void localCollectionFetchResult( KJob *job ) + { + if ( job->error() ) + return; // handled by the base class + + // safety check: the local tree has to be connected + if ( !localPendingCollections.isEmpty() ) { + q->setError( Unknown ); + q->setErrorText( QLatin1String( "Inconsistent local collection tree detected! OMG, what have you done to my database?!?!" ) ); + q->emitResult(); + return; + } -CollectionSync::~CollectionSync() -{ - delete d; -} + localListDone = true; + execute(); + } -void CollectionSync::setRemoteCollections(const Collection::List & remoteCollections) -{ - foreach ( const Collection &c, remoteCollections ) { - d->remoteCollections.insert( c.id(), c ); - } - if ( !d->streaming ) - retrievalDone(); -} + /** + Find the local node that matches the given remote collection, returns 0 + if that doesn't exist (yet). + */ + LocalNode* findMatchingLocalNode( const Collection &collection ) + { + if ( !hierarchicalRIDs ) { + if ( localRidMap.contains( collection.remoteId() ) ) + return localRidMap.value( collection.remoteId() ); + return 0; + } else { + LocalNode *localParent = 0; + if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { + kWarning() << "Remote collection without valid parent found: " << collection; + return 0; + } + if ( collection.parentCollection() == Collection::root() ) + localParent = localRoot; + else + localParent = findMatchingLocalNode( collection.parentCollection() ); + + if ( localParent && localParent->childRidMap.contains( collection.remoteId() ) ) + return localParent->childRidMap.value( collection.remoteId() ); + return 0; + } + } -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(); -} + /** + Find the local node that is the nearest ancestor of the given remote collection + (when using hierarchical RIDs only, otherwise it's always the local root node). + Never returns 0. + */ + LocalNode* findBestLocalAncestor( const Collection &collection, bool *exactMatch = 0 ) + { + if ( !hierarchicalRIDs ) + return localRoot; + if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { + kWarning() << "Remote collection without valid parent found: " << collection; + return 0; + } + if ( collection.parentCollection() == Collection::root() ) { + if ( exactMatch ) *exactMatch = true; + return localRoot; + } + bool parentIsExact = false; + LocalNode *localParent = findBestLocalAncestor( collection.parentCollection(), &parentIsExact ); + if ( !parentIsExact ) { + if ( exactMatch ) *exactMatch = false; + return localParent; + } + if ( localParent->childRidMap.contains( collection.remoteId() ) ) { + if ( exactMatch ) *exactMatch = true; + return localParent->childRidMap.value( collection.remoteId() ); + } + if ( exactMatch ) *exactMatch = false; + return localParent; + } -void CollectionSync::doStart() -{ -} + /** + Checks the pending remote nodes attached to the given local root node + to see if any of them can be processed by now. If not, they are moved to + the closest ancestor available. + */ + void processPendingRemoteNodes( LocalNode *localRoot ) + { + QList pendingRemoteNodes( localRoot->pendingRemoteNodes ); + localRoot->pendingRemoteNodes.clear(); + QHash > pendingCreations; + foreach ( RemoteNode *remoteNode, pendingRemoteNodes ) { + // step 1: see if we have a matching local node already + LocalNode *localNode = findMatchingLocalNode( remoteNode->collection ); + if ( localNode ) { + Q_ASSERT( !localNode->processed ); + // TODO: moving when using global RIDs + updateLocalCollection( localNode, remoteNode ); + continue; + } + // step 2: check if we have the parent at least, then we can create it + localNode = findMatchingLocalNode( remoteNode->collection.parentCollection() ); + if ( localNode ) { + pendingCreations[localNode].append( remoteNode ); + continue; + } + // step 3: find the best matching ancestor and enqueue it for later processing + localNode = findBestLocalAncestor( remoteNode->collection ); + if ( !localNode ) { + q->setError( Unknown ); + q->setErrorText( QLatin1String( "Remote collection without root-terminated ancestor chain provided, fix your resource dude!" ) ); + q->emitResult(); + return; + } + localNode->pendingRemoteNodes.append( remoteNode ); + } -void CollectionSync::slotLocalListDone(KJob * job) -{ - if ( job->error() ) - return; + // process the now possible collection creations + for ( QHash >::const_iterator it = pendingCreations.constBegin(); + it != pendingCreations.constEnd(); ++it ) + { + createLocalCollections( it.key(), it.value() ); + } + } - Collection::List list = static_cast( job )->collections(); - foreach ( const Collection &c, list ) { - d->localCollections.insert( c.remoteId(), c ); - d->unprocessedLocalCollections.insert( c ); - } + /** + Performs a local update for the given node pair. + */ + void updateLocalCollection( LocalNode *localNode, RemoteNode *remoteNode ) + { + ++pendingJobs; + Collection upd( remoteNode->collection ); + upd.setId( localNode->collection.id() ); + CollectionModifyJob *mod = new CollectionModifyJob( upd, q ); + mod->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); + connect( mod, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*)) ); + localNode->processed = true; + } + void updateLocalCollectionResult( KJob* job ) + { + --pendingJobs; + if ( job->error() ) + return; // handled by the base class + RemoteNode* remoteNode = job->property( REMOTE_NODE ).value(); + delete remoteNode; + checkDone(); + } - // added / updated - foreach ( const Collection &c, d->remoteCollections ) { - if ( c.remoteId().isEmpty() ) { - kWarning() << "Collection '" << c.name() <<"' does not have a remote identifier - skipping"; - continue; + /** + Creates local folders for the given local parent and remote nodes. + @todo group CollectionCreateJobs into a single one once it supports that + */ + void createLocalCollections( LocalNode* localParent, QList remoteNodes ) + { + foreach ( RemoteNode *remoteNode, remoteNodes ) { + ++pendingJobs; + Collection col( remoteNode->collection ); + col.setParentCollection( localParent->collection ); + CollectionCreateJob *create = new CollectionCreateJob( col, q ); + create->setProperty( LOCAL_NODE, QVariant::fromValue( localParent ) ); + create->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); + connect( create, SIGNAL(result(KJob*)), q, SLOT(createLocalCollectionResult(KJob*)) ); + } } - 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() ); + void createLocalCollectionResult( KJob* job ) + { + --pendingJobs; + if ( job->error() ) + return; // handled by the base class - // no local parent found, create later - if ( !localParent.isValid() ) { - d->orphanRemoteCollections << c; - continue; + const Collection newLocal = static_cast( job )->collection(); + LocalNode* localNode = createLocalNode( newLocal ); + localNode->processed = true; + + LocalNode* localParent = job->property( LOCAL_NODE ).value(); + Q_ASSERT( localParent->childNodes.contains( localNode ) ); + RemoteNode* remoteNode = job->property( REMOTE_NODE ).value(); + delete remoteNode; + + processPendingRemoteNodes( localParent ); + if ( !hierarchicalRIDs ) + processPendingRemoteNodes( localRoot ); + + checkDone(); + } + + /** + Find all local nodes that are not marked as processed. + */ + Collection::List findUnprocessedLocalCollections( LocalNode *localNode ) + { + Collection::List rv; + if ( !localNode->processed ) { + rv.append( localNode->collection ); + return rv; } + foreach ( LocalNode *child, localNode->childNodes ) + rv.append( findUnprocessedLocalCollections( child ) ); + return rv; + } - createLocalCollection( c, localParent ); - continue; + /** + Deletes unprocessed local nodes, in non-incremental mode. + */ + void deleteUnprocessedLocalNodes() + { + if ( incremental ) + return; + Collection::List cols = findUnprocessedLocalCollections( localRoot ); } - // 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*)) ); - } + /** + Deletes the given collection list. + @todo optimite delete job to support batch operations + */ + void deleteLocalCollections( const Collection::List &cols ) + { + foreach ( const Collection &col, cols ) { + ++pendingJobs; + CollectionDeleteJob *job = new CollectionDeleteJob( col, q ); + connect( job, SIGNAL(result(KJob*)), q, SLOT(deleteLocalCollectionsResult(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(); + void deleteLocalCollectionsResult( KJob *job ) + { + if ( job->error() ) + return; // handled by the base class + --pendingJobs; + checkDone(); + } - checkDone(); -} + /** + Process what's currently available. + */ + void execute() + { + if ( !localListDone ) + return; + + processPendingRemoteNodes( localRoot ); + + if ( !incremental && deliveryDone ) + deleteUnprocessedLocalNodes(); + + if ( !hierarchicalRIDs ) { + deleteLocalCollections( removedRemoteCollections ); + } else { + Collection::List localCols; + foreach ( const Collection &c, removedRemoteCollections ) { + LocalNode *node = findMatchingLocalNode( c ); + if ( node ) + localCols.append( node->collection ); + } + deleteLocalCollections( localCols ); + } + removedRemoteCollections.clear(); -void CollectionSync::slotLocalCreateDone(KJob * job) -{ - d->pendingJobs--; - if ( job->error() ) - return; + checkDone(); + } - Collection newLocal = static_cast( job )->collection(); -// d->localCollections.insert( newLocal.remoteId(), newLocal ); + /** + Finds pending remote nodes, which at the end of the day should be an empty set. + */ + QList findPendingRemoteNodes( LocalNode *localNode ) + { + QList rv; + rv.append( localNode->pendingRemoteNodes ); + foreach ( LocalNode *child, localNode->childNodes ) + rv.append( findPendingRemoteNodes( child ) ); + return rv; + } - // 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; + /** + Are we there yet?? + @todo progress reporting + */ + void checkDone() + { + // still running jobs or not fully delivered local/remote state + if ( !deliveryDone || pendingJobs > 0 || !localListDone ) + return; + + // safety check: there must be no pending remote nodes anymore + QList orphans = findPendingRemoteNodes( localRoot ); + if ( !orphans.isEmpty() ) { + q->setError( Unknown ); + q->setErrorText( QLatin1String( "Found unresolved orphan collections" ) ); + foreach ( RemoteNode* orphan, orphans ) + kDebug() << "found orphan collection:" << orphan->collection; + } + + q->commit(); } - } - d->orphanRemoteCollections = stillOrphans; - checkDone(); + CollectionSync *q; + + QString resourceId; + + int pendingJobs; + + LocalNode* localRoot; + QHash localUidMap; + QHash localRidMap; + + // temporary during build-up of the local node tree, must be empty afterwards + QHash > localPendingCollections; + + // removed remote collections in incremental mode + Collection::List removedRemoteCollections; + + bool incremental; + bool streaming; + bool hierarchicalRIDs; + + bool localListDone; + bool deliveryDone; +}; + +CollectionSync::CollectionSync( const QString &resourceId, QObject *parent ) : + TransactionSequence( parent ), + d( new Private( this ) ) +{ + d->resourceId = resourceId; +} + +CollectionSync::~CollectionSync() +{ + delete d; } -void CollectionSync::createLocalCollection(const Collection & c, const Collection & parent) +void CollectionSync::setRemoteCollections(const Collection::List & remoteCollections) { - d->pendingJobs++; - Collection col( c ); - col.setParent( parent ); - CollectionCreateJob *create = new CollectionCreateJob( col, this ); - connect( create, SIGNAL(result(KJob*)), SLOT(slotLocalCreateDone(KJob*)) ); + foreach ( const Collection &c, remoteCollections ) + d->createRemoteNode( c ); + + if ( !d->streaming ) + d->deliveryDone = true; + d->execute(); } -void CollectionSync::checkDone() +void CollectionSync::setRemoteCollections(const Collection::List & changedCollections, const Collection::List & removedCollections) { - // 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(); - } + d->incremental = true; + foreach ( const Collection &c, changedCollections ) + d->createRemoteNode( c ); + d->removedRemoteCollections += removedCollections; - commit(); + if ( !d->streaming ) + d->deliveryDone = true; + d->execute(); } -void CollectionSync::slotLocalChangeDone(KJob * job) +void CollectionSync::doStart() { - if ( job->error() ) - return; - d->pendingJobs--; - checkDone(); + CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); + job->fetchScope().setResource( d->resourceId ); + connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(localCollectionsReceived(Akonadi::Collection::List)) ); + connect( job, SIGNAL(result(KJob*)), SLOT(localCollectionFetchResult(KJob*)) ); } 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*)) ); + d->deliveryDone = true; + d->execute(); +} + +void CollectionSync::setHierarchicalRemoteIds( bool hierarchical ) +{ + d->hierarchicalRIDs = hierarchical; } #include "collectionsync_p.moc" diff --git a/akonadi/collectionsync_p.h b/akonadi/collectionsync_p.h index db3885c08..38a6a0073 100644 --- a/akonadi/collectionsync_p.h +++ b/akonadi/collectionsync_p.h @@ -1,101 +1,122 @@ /* - Copyright (c) 2007 Volker Krause + Copyright (c) 2007, 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_COLLECTIONSYNC_P_H #define AKONADI_COLLECTIONSYNC_P_H #include #include namespace Akonadi { /** @internal Syncs remote and local collections. - @todo Optimize the streaming case, so far only the interface supports streaming, - not the actual sync algorithm. + Basic terminology: + - "local": The current state in the Akonadi server + - "remote": The state in the backend, which is also the state the Akonadi + server is supposed to have afterwards. + + There are three options to influence the way syncing is done: + - Streaming vs. complete delivery: If streaming is enabled remote collections + do not need to be delivered in a single batch but can be delivered in multiple + chunks. This improves performance but requires an explicit notification + when delivery has been completed. + - Incremental vs. non-incremental: In the incremental case only remote changes + since the last sync have to be delivered, in the non-incremental mode the full + remote state has to be provided. The first is obviously the preferred way, + but requires support by the backend. + - Hierarchical vs. global RIDs: The first requires RIDs to be unique per parent + collection, the second one requires globally unique RIDs (per resource). Those + have different advantages and disadvantages, esp. regarding moving. Which one + to chose mostly depends on what the backend provides in this regard. + */ class CollectionSync : public TransactionSequence { Q_OBJECT public: /** Creates a new collection synchronzier. @param resourceId The identifier of the resource we are syncing. @param parent The parent object. */ explicit CollectionSync( const QString &resourceId, QObject *parent = 0 ); /** Destroys this job. */ ~CollectionSync(); /** Sets the result of a full remote collection listing. @param remoteCollections A list of collections. Important: All of these need a unique remote identifier and parent remote identifier. */ void setRemoteCollections( const Collection::List &remoteCollections ); /** Sets the result of an incremental remote collection listing. @param changedCollections A list of remotely added or changed collections. @param removedCollections A lost of remotely deleted collections. */ void setRemoteCollections( const Collection::List &changedCollections, const Collection::List &removedCollections ); /** Enables streaming, that is not all collections are delivered at once. Use setRemoteCollections() multiple times when streaming is enabled and call retrievalDone() when all collections have been retrieved. Must be called before the first call to setRemoteCollections(). */ void setStreamingEnabled( bool streaming ); /** Indicate that all collections have been retrieved in streaming mode. */ void retrievalDone(); + /** + Indicate whether the resource supplies collections with hierarchical or + global remote identifiers. @c false by default. + Must be called before the first call to setRemoteCollections(). + */ + void setHierarchicalRemoteIds( bool hierarchical ); + protected: void doStart(); - private: - void createLocalCollection( const Collection &c, const Collection &parent ); - void checkDone(); - - private Q_SLOTS: - void slotLocalListDone( KJob *job ); - void slotLocalCreateDone( KJob *job ); - void slotLocalChangeDone( KJob *job ); - private: class Private; Private* const d; + + Q_PRIVATE_SLOT( d, void localCollectionsReceived( const Akonadi::Collection::List &localCols ) ) + Q_PRIVATE_SLOT( d, void localCollectionFetchResult( KJob* job ) ) + Q_PRIVATE_SLOT( d, void updateLocalCollectionResult(KJob* job) ) + Q_PRIVATE_SLOT( d, void createLocalCollectionResult(KJob* job) ) + Q_PRIVATE_SLOT( d, void deleteLocalCollectionsResult(KJob* job) ) }; } #endif diff --git a/akonadi/collectionutils_p.h b/akonadi/collectionutils_p.h index 53f8e554d..19279de9c 100644 --- a/akonadi/collectionutils_p.h +++ b/akonadi/collectionutils_p.h @@ -1,90 +1,90 @@ /* Copyright (c) 2008 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. */ #ifndef AKONADI_COLLECTIONUTILS_P_H #define AKONADI_COLLECTIONUTILS_P_H #include namespace Akonadi { /** * @internal */ namespace CollectionUtils { inline bool isVirtualParent( const Collection &collection ) { - return (collection.parent() == Collection::root().id() && + return (collection.parentCollection() == Collection::root() && collection.resource() == QLatin1String( "akonadi_search_resource" )); } inline bool isVirtual( const Collection &collection ) { return (collection.resource() == QLatin1String( "akonadi_search_resource" )); } inline bool isResource( const Collection &collection ) { - return (collection.parent() == Collection::root().id()); + return (collection.parentCollection() == Collection::root()); } inline bool isStructural( const Collection &collection ) { return collection.contentMimeTypes().isEmpty(); } inline bool isFolder( const Collection &collection ) { - return (collection.parent() != Collection::root().id() && + return (collection.parentCollection() != Collection::root() && collection.resource() != QLatin1String( "akonadi_search_resource" ) && !collection.contentMimeTypes().isEmpty()); } inline QString defaultIconName( const Collection &col ) { if ( CollectionUtils::isVirtualParent( col ) ) return QLatin1String( "edit-find" ); if ( CollectionUtils::isVirtual( col ) ) return QLatin1String( "document-preview" ); if ( CollectionUtils::isResource( col ) ) return QLatin1String( "network-server" ); if ( CollectionUtils::isStructural( col ) ) return QLatin1String( "folder-grey" ); const QStringList content = col.contentMimeTypes(); if ( content.size() == 1 || (content.size() == 2 && content.contains( Collection::mimeType() )) ) { if ( content.contains( QLatin1String( "text/x-vcard" ) ) || content.contains( QLatin1String( "text/directory" ) ) || content.contains( QLatin1String( "text/vcard" ) ) ) return QLatin1String( "x-office-address-book" ); // TODO: add all other content types and/or fix their mimetypes if ( content.contains( QLatin1String( "akonadi/event" ) ) || content.contains( QLatin1String( "text/ical" ) ) ) return QLatin1String( "view-pim-calendar" ); if ( content.contains( QLatin1String( "akonadi/task" ) ) ) return QLatin1String( "view-pim-tasks" ); } else if ( content.isEmpty() ) { return QLatin1String( "folder-grey" ); } return QLatin1String( "folder" ); } } } #endif diff --git a/akonadi/entity.cpp b/akonadi/entity.cpp index cc8f3353e..7a9aa4d8b 100644 --- a/akonadi/entity.cpp +++ b/akonadi/entity.cpp @@ -1,129 +1,150 @@ /* Copyright (c) 2008 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. */ #include "entity.h" #include "entity_p.h" +#include "collection.h" using namespace Akonadi; Entity::Entity( const Entity &other ) : d_ptr( other.d_ptr ) { } Entity::Entity( EntityPrivate *dd ) : d_ptr( dd ) { } Entity::~Entity() { } void Entity::setId( Id id ) { d_ptr->mId = id; } Entity::Id Entity::id() const { return d_ptr->mId; } void Entity::setRemoteId( const QString& id ) { d_ptr->mRemoteId = id; } QString Entity::remoteId() const { return d_ptr->mRemoteId; } bool Entity::isValid() const { return ( d_ptr->mId >= 0 ); } bool Entity::operator==( const Entity &other ) const { return ( d_ptr->mId == other.d_ptr->mId ); } bool Akonadi::Entity::operator!=(const Entity & other) const { return d_ptr->mId != other.d_ptr->mId; } Entity& Entity ::operator=( const Entity &other ) { if ( this != &other ) d_ptr = other.d_ptr; return *this; } void Entity::addAttribute(Attribute * attr) { if ( d_ptr->mAttributes.contains( attr->type() ) ) delete d_ptr->mAttributes.take( attr->type() ); d_ptr->mAttributes.insert( attr->type(), attr ); d_ptr->mDeletedAttributes.remove( attr->type() ); } void Entity::removeAttribute( const QByteArray &type ) { if ( d_ptr->mAttributes.contains( type ) ) { d_ptr->mDeletedAttributes.insert( type ); delete d_ptr->mAttributes.take( type ); } } bool Entity::hasAttribute(const QByteArray & type) const { return d_ptr->mAttributes.contains( type ); } Attribute::List Entity::attributes() const { return d_ptr->mAttributes.values(); } void Akonadi::Entity::clearAttributes() { foreach ( Attribute *attr, d_ptr->mAttributes ) { d_ptr->mDeletedAttributes.insert( attr->type() ); delete attr; } d_ptr->mAttributes.clear(); } Attribute * Entity::attribute(const QByteArray & type) const { if ( d_ptr->mAttributes.contains( type ) ) return d_ptr->mAttributes.value( type ); return 0; } uint qHash( const Akonadi::Entity &entity ) { return qHash( entity.id() ); } +Collection& Entity::parentCollection() +{ + if ( !d_ptr->mParent ) + d_ptr->mParent = new Collection(); + return *(d_ptr->mParent); +} + +Collection Entity::parentCollection() const +{ + if ( !d_ptr->mParent ) + d_ptr->mParent = new Collection(); + return *(d_ptr->mParent); +} + +void Entity::setParentCollection( const Collection &parent ) +{ + delete d_ptr->mParent; + d_ptr->mParent = new Collection( parent ); +} + AKONADI_DEFINE_PRIVATE( Entity ) diff --git a/akonadi/entity.h b/akonadi/entity.h index 7b6c66e7b..1519c2101 100644 --- a/akonadi/entity.h +++ b/akonadi/entity.h @@ -1,234 +1,262 @@ /* Copyright (c) 2008 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. */ #ifndef AKONADI_ENTITY_H #define AKONADI_ENTITY_H #include "akonadi_export.h" namespace Akonadi { class Entity; } AKONADI_EXPORT uint qHash( const Akonadi::Entity& ); #include #include #include #include #define AKONADI_DECLARE_PRIVATE( Class ) \ Class##Private* d_func(); \ const Class##Private* d_func() const; \ friend class Class##Private; namespace Akonadi { +class Collection; class EntityPrivate; /** * @short The base class for Item and Collection. * * Entity is the common base class for Item and Collection that provides * unique IDs and attributes handling. * * This class is not meant to be used directly, use Item or Collection instead. * * @author Tobias Koenig */ class AKONADI_EXPORT Entity { public: /** * Describes the unique id type. */ typedef qint64 Id; /** * Destroys the entity. */ ~Entity(); /** * Sets the unique @p identifier of the entity. */ void setId( Id identifier ); /** * Returns the unique identifier of the entity. */ Id id() const; /** * Sets the remote @p id of the entity. */ void setRemoteId( const QString& id ); /** * Returns the remote id of the entity. */ QString remoteId() const; /** * Returns whether the entity is valid. */ bool isValid() const; /** * Returns whether the entity's id equals the * id of the @p other entity. */ bool operator==( const Entity &other ) const; /** * Returns whether the entity's id does not equal the id * of the @p other entity. */ bool operator!=( const Entity &other ) const; /** * Assigns the @p other to this entity and returns a reference to this entity. */ Entity& operator=( const Entity &other ); + /** + * Returns the parent collection of this object. + * @note This will of course only return a useful value if it was explictly retrieved + * from the Akonadi server. + * @since 4.4 + */ + Collection parentCollection() const; + + /** + * Returns a reference to the parent collection of this object. + * @note This will of course only return a useful value if it was explictly retrieved + * from the Akonadi server. + * @since 4.4 + */ + Collection& parentCollection(); + + /** + * Set the parent collection of this object. + * @note Calling this method has no immediate effect for the object itself, + * such as being moved to another collection. + * It is mainly relevant to provide a context for RID-based operations + * inside resources. + * @param parent The parent collection. + * @since 4.4 + */ + void setParentCollection( const Collection &parent ); + /** * Adds an attribute to the entity. * * If an attribute of the same type name already exists, it is deleted and * replaced with the new one. * * @param attribute The new attribute. * * @note The entity takes the ownership of the attribute. */ void addAttribute( Attribute *attribute ); /** * Removes and deletes the attribute of the given type @p name. */ void removeAttribute( const QByteArray &name ); /** * Returns @c true if the entity has an attribute of the given type @p name, * false otherwise. */ bool hasAttribute( const QByteArray &name ) const; /** * Returns a list of all attributes of the entity. */ Attribute::List attributes() const; /** * Removes and deletes all attributes of the entity. */ void clearAttributes(); /** * Returns the attribute of the given type @p name if available, 0 otherwise. */ Attribute* attribute( const QByteArray &name ) const; /** * Describes the options that can be passed to access attributes. */ enum CreateOption { AddIfMissing ///< Creates the attribute if it is missing }; /** * Returns the attribute of the requested type. * If the entity has no attribute of that type yet, a new one * is created and added to the entity. * * @param option The create options. */ template inline T* attribute( CreateOption option ) { Q_UNUSED( option ); const T dummy; if ( hasAttribute( dummy.type() ) ) { T* attr = dynamic_cast( attribute( dummy.type() ) ); if ( attr ) return attr; kWarning( 5250 ) << "Found attribute of unknown type" << dummy.type() << ". Did you forget to call AttributeFactory::registerAttribute()?"; } T* attr = new T(); addAttribute( attr ); return attr; } /** * Returns the attribute of the requested type or 0 if it is not available. */ template inline T* attribute() const { const T dummy; if ( hasAttribute( dummy.type() ) ) { T* attr = dynamic_cast( attribute( dummy.type() ) ); if ( attr ) return attr; kWarning( 5250 ) << "Found attribute of unknown type" << dummy.type() << ". Did you forget to call AttributeFactory::registerAttribute()?"; } return 0; } /** * Removes and deletes the attribute of the requested type. */ template inline void removeAttribute() { const T dummy; removeAttribute( dummy.type() ); } /** * Returns whether the entity has an attribute of the requested type. */ template inline bool hasAttribute() const { const T dummy; return hasAttribute( dummy.type() ); } protected: /** * Creates an entity from an @p other entity. */ Entity( const Entity &other ); //@cond PRIVATE Entity( EntityPrivate *dd ); QSharedDataPointer d_ptr; //@endcond AKONADI_DECLARE_PRIVATE( Entity ) }; } #endif diff --git a/akonadi/entity_p.h b/akonadi/entity_p.h index 9340bab04..00ba06ffd 100644 --- a/akonadi/entity_p.h +++ b/akonadi/entity_p.h @@ -1,95 +1,102 @@ /* Copyright (c) 2008 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. */ #ifndef ENTITY_P_H #define ENTITY_P_H #include "entity.h" +#include "collection.h" #include #include #include #define AKONADI_DEFINE_PRIVATE( Class ) \ Class##Private* Class ::d_func() { return reinterpret_cast( d_ptr.data() ); } \ const Class##Private* Class ::d_func() const { return reinterpret_cast( d_ptr.data() ); } namespace Akonadi { /** * @internal */ class EntityPrivate : public QSharedData { public: EntityPrivate( Entity::Id id = -1 ) - : mId( id ) + : mId( id ), + mParent( 0 ) { } virtual ~EntityPrivate() { qDeleteAll( mAttributes ); + delete mParent; } EntityPrivate( const EntityPrivate &other ) - : QSharedData( other ) + : QSharedData( other ), + mParent( 0 ) { mId = other.mId; mRemoteId = other.mRemoteId; foreach ( Attribute* attr, other.mAttributes ) mAttributes.insert( attr->type(), attr->clone() ); mDeletedAttributes = other.mDeletedAttributes; + if ( other.mParent ) + mParent = new Collection( *(other.mParent) ); } virtual void resetChangeLog() { mDeletedAttributes.clear(); } virtual EntityPrivate *clone() const = 0; Entity::Id mId; QString mRemoteId; QHash mAttributes; QSet mDeletedAttributes; + mutable Collection* mParent; }; } /** * @internal * * This template specialization is used to change the detach * behaviour of QSharedDataPointer to match our needs. */ template <> Q_INLINE_TEMPLATE void QSharedDataPointer::detach() { if (d && d->ref != 1) { Akonadi::EntityPrivate *x = d->clone(); x->ref.ref(); if (!d->ref.deref()) delete d; d = x; } } #endif diff --git a/akonadi/entityfilterproxymodel.cpp b/akonadi/entityfilterproxymodel.cpp index 414e7e9d4..5f5d02769 100644 --- a/akonadi/entityfilterproxymodel.cpp +++ b/akonadi/entityfilterproxymodel.cpp @@ -1,248 +1,271 @@ /* 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(); } +bool EntityFilterProxyModel::hasChildren(const QModelIndex &parent) const +{ + if ( !parent.isValid() ) + return sourceModel()->hasChildren(parent); + + Q_D(const EntityFilterProxyModel); + if ( EntityTreeModel::ItemListHeaders == d->m_headerSet) + return false; + + if ( EntityTreeModel::CollectionTreeHeaders == d->m_headerSet ) + { + QModelIndex childIndex = parent.child( 0, 0 ); + while ( childIndex.isValid() ) + { + Collection col = childIndex.data( EntityTreeModel::CollectionRole ).value(); + if (col.isValid()) + return true; + childIndex = childIndex.sibling( childIndex.row() + 1, childIndex.column() ); + } + } + return false; +} + #include "entityfilterproxymodel.moc" diff --git a/akonadi/entityfilterproxymodel.h b/akonadi/entityfilterproxymodel.h index 73af5296f..3416dc422 100644 --- a/akonadi/entityfilterproxymodel.h +++ b/akonadi/entityfilterproxymodel.h @@ -1,158 +1,160 @@ /* 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; + 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; 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 index 03ff881f4..ad8ebe92f 100644 --- a/akonadi/entitytreemodel.cpp +++ b/akonadi/entitytreemodel.cpp @@ -1,914 +1,943 @@ /* 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 ); + if ( parent.column() > 0 ) + { + return QModelIndex(); + } + //TODO: don't use column count here? Use some d-> func. if ( column >= columnCount() || column < 0 ) return QModelIndex(); QList childEntities; const Node *parentNode = reinterpret_cast( parent.internalPointer() ); if ( !parentNode || !parent.isValid() ) { if ( d->m_showRootCollection ) childEntities << d->m_childEntities.value( -1 ); else childEntities = d->m_childEntities.value( d->m_rootCollection.id() ); } else { if ( parentNode->id >= 0 ) childEntities = d->m_childEntities.value( parentNode->id ); } const int size = childEntities.size(); if ( row < 0 || row >= size ) return QModelIndex(); Node *node = childEntities.at( row ); return createIndex( row, column, reinterpret_cast( node ) ); } QModelIndex EntityTreeModel::parent( const QModelIndex & index ) const { Q_D( const EntityTreeModel ); if ( !index.isValid() ) return QModelIndex(); const Node *node = reinterpret_cast( index.internalPointer() ); if ( !node ) return QModelIndex(); const Collection collection = d->m_collections.value( node->parent ); if ( !collection.isValid() ) return QModelIndex(); if ( collection.id() == d->m_rootCollection.id() ) { if ( !d->m_showRootCollection ) return QModelIndex(); else return createIndex( 0, 0, reinterpret_cast( d->m_rootNode ) ); } - const int row = d->indexOf( d->m_childEntities.value( collection.parent()), collection.id() ); + const int row = d->indexOf( d->m_childEntities.value( collection.parentCollection().id()), collection.id() ); - Node *parentNode = d->m_childEntities.value( collection.parent() ).at( row ); + Node *parentNode = d->m_childEntities.value( collection.parentCollection().id() ).at( row ); return createIndex( row, 0, reinterpret_cast( parentNode ) ); } int EntityTreeModel::rowCount( const QModelIndex & parent ) const { Q_D( const EntityTreeModel ); const Node *node = reinterpret_cast( parent.internalPointer() ); qint64 id; if ( !parent.isValid() ) { // If we're showing the root collection then it will be the only child of the root. if ( d->m_showRootCollection ) return d->m_childEntities.value( -1 ).size(); id = d->m_rootCollection.id(); } else { if ( !node ) return 0; if ( Node::Item == node->type ) return 0; id = node->id; } if ( parent.column() <= 0 ) return d->m_childEntities.value( id ).size(); return 0; } int EntityTreeModel::getColumnCount(int headerSet) const { // Not needed in this model. Q_UNUSED(headerSet); return columnCount(); } QVariant EntityTreeModel::getHeaderData( int section, Qt::Orientation orientation, int role, int headerSet) const { // Not needed in this model. Q_UNUSED(headerSet); if ( section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole ) return i18nc( "@title:column, name of a thing", "Name" ); return QAbstractItemModel::headerData( section, orientation, role ); } QVariant EntityTreeModel::headerData( int section, Qt::Orientation orientation, int role ) const { const int headerSet = (role / TerminalUserRole); role %= TerminalUserRole; return getHeaderData( section, orientation, role, headerSet ); } QMimeData *EntityTreeModel::mimeData( const QModelIndexList &indexes ) const { Q_D( const EntityTreeModel ); QMimeData *data = new QMimeData(); KUrl::List urls; foreach( const QModelIndex &index, indexes ) { if ( index.column() != 0 ) continue; if (!index.isValid()) continue; const Node *node = reinterpret_cast( index.internalPointer() ); if ( Node::Collection == node->type ) urls << d->m_collections.value(node->id).url(); else if ( Node::Item == node->type ) urls << d->m_items.value( node->id ).url( Item::UrlWithMimeType ); else // if that happens something went horrible wrong Q_ASSERT(false); } urls.populateMimeData( data ); return data; } // Always return false for actions which take place asyncronously, eg via a Job. bool EntityTreeModel::setData( const QModelIndex &index, const QVariant &value, int role ) { Q_D( EntityTreeModel ); const Node *node = reinterpret_cast( index.internalPointer() ); if ( index.column() == 0 && (role & (Qt::EditRole | ItemRole | CollectionRole)) ) { if ( Node::Collection == node->type ) { Collection collection = d->m_collections.value( node->id ); if ( !collection.isValid() || !value.isValid() ) return false; if ( Qt::EditRole == role ) { collection.setName( value.toString() ); if ( collection.hasAttribute() ) { EntityDisplayAttribute *displayAttribute = collection.attribute(); displayAttribute->setDisplayName( value.toString() ); collection.addAttribute( displayAttribute ); } } if ( CollectionRole == role ) collection = value.value(); CollectionModifyJob *job = new CollectionModifyJob( collection, d->m_session ); connect( job, SIGNAL( result( KJob* ) ), SLOT( updateJobDone( KJob* ) ) ); return false; } else if (Node::Item == node->type) { Item item = d->m_items.value( node->id ); if ( !item.isValid() || !value.isValid() ) return false; if ( Qt::EditRole == role ) { if ( item.hasAttribute() ) { EntityDisplayAttribute *displayAttribute = item.attribute( Entity::AddIfMissing ); displayAttribute->setDisplayName( value.toString() ); item.addAttribute( displayAttribute ); } } if ( ItemRole == role ) item = value.value(); ItemModifyJob *itemModifyJob = new ItemModifyJob( item, d->m_session ); connect( itemModifyJob, SIGNAL( result( KJob* ) ), SLOT( updateJobDone( KJob* ) ) ); return false; } } return QAbstractItemModel::setData( index, value, role ); } bool EntityTreeModel::canFetchMore( const QModelIndex & parent ) const { + Q_D(const EntityTreeModel); const Item item = parent.data( ItemRole ).value(); if ( item.isValid() ) { // items can't have more rows. // TODO: Should I use this for fetching more of an item, ie more payload parts? return false; } else { // but collections can... + const Collection::Id colId = parent.data( CollectionIdRole ).toULongLong(); + + // But the root collection can't... + if ( Collection::root().id() == colId ) + { + return false; + } + + foreach (Node *node, d->m_childEntities.value( colId ) ) + { + if ( Node::Item == node->type ) + { + // Only try to fetch more from a collection if we don't already have items in it. + // Otherwise we'd spend all the time listing items in collections. + // This means that collections which don't contain items get a lot of item fetch jobs started on them. + // Will fix that later. + return false; + } + } return true; } // TODO: It might be possible to get akonadi to tell us if a collection is empty // or not and use that information instead of assuming all collections are not empty. // Using Collection statistics? } void EntityTreeModel::fetchMore( const QModelIndex & parent ) { Q_D( EntityTreeModel ); if ( 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); + return QAbstractItemModel::match(start, role, value, hits, flags); // Try to match names, and email addresses. QModelIndexList list; + + if (role < 0 || !start.isValid() || !value.isValid()) + return list; + const int column = 0; int row = start.row(); QModelIndex parentIdx = start.parent(); int parentRowCount = rowCount(parentIdx); while (row < parentRowCount && (hits == -1 || list.size() < hits)) { QModelIndex idx = index(row, column, parentIdx); Item item = idx.data(ItemRole).value(); if (!item.isValid()) { Collection col = idx.data(CollectionRole).value(); if (!col.isValid()) { continue; } if (match(col, value, flags)) list << idx; } else { if (match(item, value, flags)) { list << idx; } } ++row; } return list; } bool EntityTreeModel::insertRows( int, int, const QModelIndex& ) { return false; } bool EntityTreeModel::insertColumns( int, int, const QModelIndex& ) { return false; } bool EntityTreeModel::removeRows( int start, int end, const QModelIndex &parent ) { /* beginRemoveRows(start, end, parent); // TODO: Implement me. endRemoveRows(start, end, parent); */ return false; } bool EntityTreeModel::removeColumns( int, int, const QModelIndex& ) { return false; } void EntityTreeModel::setRootCollection( const Collection &collection ) { Q_D(EntityTreeModel); Q_ASSERT( collection.isValid() ); d->m_rootCollection = collection; clearAndReset(); } Collection EntityTreeModel::rootCollection() const { Q_D(const EntityTreeModel); return d->m_rootCollection; } QModelIndex EntityTreeModel::indexForCollection( const Collection &collection ) const { Q_D(const EntityTreeModel); // TODO: will this work for collection::root while showing it? - const int row = d->indexOf( d->m_childEntities.value( collection.parent() ), collection.id() ); + const int row = d->indexOf( d->m_childEntities.value( collection.parentCollection().id() ), collection.id() ); if ( row < 0 ) return QModelIndex(); - Node *node = d->m_childEntities.value( collection.parent() ).at( row ); + Node *node = d->m_childEntities.value( collection.parentCollection().id() ).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_p.cpp b/akonadi/entitytreemodel_p.cpp index 8028d5ece..9bfe88ec3 100644 --- a/akonadi/entitytreemodel_p.cpp +++ b/akonadi/entitytreemodel_p.cpp @@ -1,675 +1,765 @@ /* 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 #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() ); + job->fetchScope().setIncludeUnsubscribed( m_includeUnsubscribed ); + job->fetchScope().setIncludeStatistics( m_includeStatistics ); + job->fetchScope().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 parentId = col.parentCollection().id(); 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 ); + 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. + // Persist them until the next time collectionsFetched receives 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 ); + Q_ASSERT( job ); - 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; - } + const Collection::Id collectionId = job->property( ItemFetchCollectionId() ).value(); + Item::List itemsToInsert; + Item::List itemsToUpdate; + + const Collection collection = m_collections.value( collectionId ); + + Q_ASSERT( collection.isValid() ); + + 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(); + } + 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 ); + 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; + Node *node = new Node; + node->id = itemId; + node->parent = collectionId; + node->type = Node::Item; - m_childEntities[ collectionId ].append( node ); - } - q->endInsertRows(); + 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 ); + CollectionFetchJob *job = new CollectionFetchJob( collection.parentCollection(), CollectionFetchJob::Base, m_session ); + job->fetchScope().setIncludeUnsubscribed( m_includeUnsubscribed ); + job->fetchScope().setIncludeStatistics( 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())) + if (m_collections.contains(collection.parentCollection().id())) { - m_ancestors.prepend( m_collections.value(collection.parent()) ); + m_ancestors.prepend( m_collections.value(collection.parentCollection().id()) ); 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 ) { + // If the resource is removed while populating the model with it, we might still + // get some monitor signals. These stale/out-of-order signals can't be completely eliminated + // in the akonadi server due to implementation details, so we also handle such signals in the model silently + // in all the monior slots. + // Stephen Kelly, 28, July 2009 + + // This is currently temporarily blocked by a uninitialized value bug in the server. +// if ( !m_collections.contains( parent.id() ) ) +// { +// kWarning() << "Got a stale notification for a collection whose parent was already removed." << collection.id() << collection.remoteId(); +// return; +// } // 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 ) ) + 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 ); + if ( !m_collections.contains( collection.parent() ) ) + return; + 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())) + if ( !m_collections.contains( collection.id() ) ) + { + kWarning() << "Got a stale notification for a collection which was already removed." << collection.id() << collection.remoteId(); return; + } - const int row = indexOf( m_childEntities.value( collection.parent() ), collection.id() ); - - const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.parent() ) ); + const int row = indexOf( m_childEntities.value( collection.parentCollection().id() ), collection.id() ); + + const QModelIndex parentIndex = q->indexForCollection( m_collections.value( collection.parentCollection().id() ) ); 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 ); + m_childEntities[ collection.parentCollection().id() ].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 ) { + if ( !m_collections.contains( collection.id() ) ) + { + kWarning() << "Got a stale notification for a collection which was already removed." << collection.id() << collection.remoteId(); + return; + } + 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; + if ( !m_collections.contains( collection.id() ) ) + { + kWarning() << "Got a stale notification for a collection which was already removed." << collection.id() << collection.remoteId(); + return; + } + + m_collections[ collection.id() ] = collection; const QModelIndex index = q->indexForCollection( collection ); + Q_ASSERT( index.isValid() ); 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 ) ) + if ( !m_collections.contains( collection.id() ) ) + { + kWarning() << "Got a stale notification for an item whose collection was already removed." << item.id() << item.remoteId(); + return; + } + + Q_ASSERT( m_collections.contains( collection.id() ) ); + + 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; + + if ( !m_items.contains( item.id() ) ) + { + kWarning() << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); + return; + } + + // TODO: Iterate over all (virtual) collections. const Collection collection = parents.first(); + Q_ASSERT( m_collections.contains( collection.id() ) ); + 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 ); + + if ( !m_items.contains( item.id() ) ) + { + kWarning() << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); + return; + } + m_items[ item.id() ] = item; const QModelIndexList indexes = q->indexesForItem( item ); foreach ( const QModelIndex &index, indexes ) - q->dataChanged( index, index ); + { + + if( !index.isValid() ) + { + kWarning() << "item has invalid index:" << item.id() << item.remoteId(); + } else { + q->dataChanged( index, index ); + } + } } void EntityTreeModelPrivate::monitoredItemMoved( const Akonadi::Item& item, - const Akonadi::Collection& sourceItem, - const Akonadi::Collection& destItem ) + const Akonadi::Collection& sourceCollection, + const Akonadi::Collection& destCollection ) { Q_Q( EntityTreeModel ); + if ( !m_items.contains( item.id() ) ) + { + kWarning() << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); + return; + } + + Q_ASSERT( m_collections.contains( sourceCollection.id() ) ); + Q_ASSERT( m_collections.contains( destCollection.id() ) ); + const Item::Id itemId = item.id(); - const int srcRow = indexOf( m_childEntities.value( sourceItem.id() ), itemId ); + const int srcRow = indexOf( m_childEntities.value( sourceCollection.id() ), itemId ); - const QModelIndex srcIndex = q->indexForCollection( sourceItem ); - const QModelIndex destIndex = q->indexForCollection( destItem ); + const QModelIndex srcIndex = q->indexForCollection( sourceCollection ); + const QModelIndex destIndex = q->indexForCollection( destCollection ); // 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 ) ) + if ( !m_items.contains( item.id() ) ) + { + kWarning() << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); + return; + } + Q_ASSERT( m_collections.contains( collection.id() ) ); + + 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 ); + if ( !m_items.contains( item.id() ) ) + { + kWarning() << "Got a stale notification for an item which was already removed." << item.id() << item.remoteId(); + return; + } + Q_ASSERT( m_collections.contains( collection.id() ) ); + 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() ); + return m_collections.value( collection.parentCollection().id() ); } 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/itemmodel.cpp b/akonadi/itemmodel.cpp index 9c4baff59..41e6b0b36 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() ) { + Q_FOREACH( const 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() << "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() << "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(); 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/monitor.h b/akonadi/monitor.h index 7b8f82970..70d7311ca 100644 --- a/akonadi/monitor.h +++ b/akonadi/monitor.h @@ -1,340 +1,348 @@ /* 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_MONITOR_H #define AKONADI_MONITOR_H #include #include #include namespace Akonadi { class CollectionStatistics; class Item; class ItemFetchScope; class MonitorPrivate; class Session; /** * @short Monitors an item or collection for changes. * * The Monitor emits signals if some of these objects are changed or * removed or new ones are added to the Akonadi storage. * * Optionally, the changed objects can be fetched automatically from the server. * To enable this, see fetchCollection(), fetchItemMetaData(), fetchItemData(). * * @todo: distinguish between monitoring collection properties and collection content. * @todo: special case for collection content counts changed * * @author Volker Krause */ class AKONADI_EXPORT Monitor : public QObject { Q_OBJECT public: /** * Creates a new monitor. * * @param parent The parent object. */ explicit Monitor( QObject *parent = 0 ); /** * Destroys the monitor. */ virtual ~Monitor(); /** * Sets whether the specified collection shall be monitored for changes. * * @param collection The collection to monitor. * If this collection is Collection::root(), all collections * in the Akonadi storage will be monitored. */ void setCollectionMonitored( const Collection &collection, bool monitored = true ); /** * Sets whether the specified item shall be monitored for changes. * * @param item The item to monitor. */ void setItemMonitored( const Item &item, bool monitored = true ); /** * Sets whether the specified resource shall be monitored for changes. * * @param resource The resource identifier. */ void setResourceMonitored( const QByteArray &resource, bool monitored = true ); /** * Sets whether objects of the specified mime type shall be monitored for changes. * * @param mimetype The mime type to monitor. */ void setMimeTypeMonitored( const QString &mimetype, bool monitored = true ); /** * Sets whether all items shall be monitored. */ void setAllMonitored( bool monitored = true ); /** * Ignores all change notifications caused by the given session. * * @param session The session you want to ignore. */ void ignoreSession( Session *session ); /** * Enables automatic fetching of changed collections from the Akonadi storage. * * @param enable @c true enables automatic fetching, @c false disables automatic fetching. */ void fetchCollection( bool enable ); /** * Enables automatic fetching of changed collection statistics information from * the Akonadi storage. * * @param enable @c true to enables automatic fetching, @c false disables automatic fetching. */ void fetchCollectionStatistics( bool enable ); /** * Sets the item fetch scope. * * 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 itemFetchScope() */ void setItemFetchScope( const 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 setItemFetchScope() for replacing the current item fetch scope */ ItemFetchScope &itemFetchScope(); /** * Returns the list of collections being monitored. * * @since 4.3 */ Collection::List collectionsMonitored() const; /** * Returns the set of items being monitored. * * @since 4.3 */ QList itemsMonitored() const; /** * Returns the set of mimetypes being monitored. * * @since 4.3 */ QStringList mimeTypesMonitored() const; /** * Returns the set of identifiers for resources being monitored. * * @since 4.3 */ QList resourcesMonitored() const; /** * Returns true if everything is being monitored. * * @since 4.3 */ bool isAllMonitored() const; Q_SIGNALS: /** * This signal is emitted if a monitored item has changed, e.g. item parts have been modified. * * @param item The changed item. * @param partIdentifiers The identifiers of the item parts that has been changed. */ void itemChanged( const Akonadi::Item &item, const QSet &partIdentifiers ); /** * This signal is emitted if a monitored item has been moved between two collections * * @param item The moved item. * @param collectionSource The collection the item has been moved from. * @param collectionDestination The collection the item has been moved to. */ void itemMoved( const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination ); /** * This signal is emitted if an item has been added to a monitored collection in the Akonadi storage. * * @param item The new item. * @param collection The collection the item has been added to. */ void itemAdded( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if * - a monitored item has been removed from the Akonadi storage * or * - a item has been removed from a monitored collection. * * @param item The removed item. */ void itemRemoved( const Akonadi::Item &item ); //TODO remove if we are sure nobody uses it any longer void itemRemoved( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if a reference to an item is added to a virtual collection. * @param item The linked item. * @param collection The collection the item is linked to. * * @since 4.2 */ void itemLinked( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if a reference to an item is removed from a virtual collection. * @param item The unlinked item. * @param collection The collection the item is unlinked from. * * @since 4.2 */ void itemUnlinked( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if a new collection has been added to a monitored collection in the Akonadi storage. * * @param collection The new collection. * @param parent The parent collection. */ void collectionAdded( const Akonadi::Collection &collection, const Akonadi::Collection &parent ); /** - * This signal is emitted if a monitored collection has been changed (properties or content) - * or has been reparented. + * This signal is emitted if a monitored collection has been changed (properties or content). * * @param collection The changed collection. */ void collectionChanged( const Akonadi::Collection &collection ); + /** + * This signals is emitted if a monitored collection has been moved. + * + * @param collection The moved collection. + * @param source The previous parent collection. + * @param distination The new parent collection. + */ + void collectionMoved( const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination ); + /** * This signal is emitted if a monitored collection has been removed from the Akonadi storage. * * @param collection The removed collection. */ void collectionRemoved( const Akonadi::Collection &collection ); /** * This signal is emitted if the statistics information of a monitored collection * has changed. * * @param id The collection identifier of the changed collection. * @param statistics The updated collection statistics, invalid if automatic * fetching of statistics changes is disabled. */ void collectionStatisticsChanged( Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics ); /** * This signal is emitted if the Monitor starts or stops monitoring @p collection explicitly. * @param collection The collection * @param monitored Whether the collection is now being monitored or not. * * @since 4.3 */ void collectionMonitored( const Akonadi::Collection &collection, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring @p item explicitly. * @param item The item * @param monitored Whether the item is now being monitored or not. * * @since 4.3 */ void itemMonitored( const Akonadi::Item &item, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring the resource with the identifier @p identifier explicitly. * @param identifier The identifier of the resource. * @param monitored Whether the resource is now being monitored or not. * * @since 4.3 */ void resourceMonitored( const QByteArray &identifier, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring @p mimeType explicitly. * @param mimeType The mimeType. * @param monitored Whether the mimeType is now being monitored or not. * * @since 4.3 */ void mimeTypeMonitored( const QString &mimeType, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring everything. * @param monitored Whether everything is now being monitored or not. * * @since 4.3 */ void allMonitored( bool monitored ); protected: //@cond PRIVATE MonitorPrivate *d_ptr; explicit Monitor( MonitorPrivate *d, QObject *parent = 0 ); //@endcond private: Q_DECLARE_PRIVATE( Monitor ) //@cond PRIVATE Q_PRIVATE_SLOT( d_ptr, void slotSessionDestroyed( QObject* ) ) Q_PRIVATE_SLOT( d_ptr, void slotStatisticsChangedFinished( KJob* ) ) Q_PRIVATE_SLOT( d_ptr, void slotFlushRecentlyChangedCollections() ) Q_PRIVATE_SLOT( d_ptr, void slotNotify( const Akonadi::NotificationMessage::List& ) ) Q_PRIVATE_SLOT( d_ptr, void slotItemJobFinished( KJob* ) ) Q_PRIVATE_SLOT( d_ptr, void slotCollectionJobFinished( KJob* ) ) //@endcond }; } #endif diff --git a/akonadi/monitor_p.cpp b/akonadi/monitor_p.cpp index cd0ffed2f..232cc7188 100644 --- a/akonadi/monitor_p.cpp +++ b/akonadi/monitor_p.cpp @@ -1,391 +1,394 @@ /* 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() << "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() << "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 ) + if ( msg.operation() == NotificationMessage::Add || msg.operation() == NotificationMessage::Move ) list << Collection( msg.parentCollection() ); + if ( msg.operation() == NotificationMessage::Move ) + list << Collection( msg.parentDestCollection() ); 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() << "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() << "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 ) + const Collection &par, const Collection &dest ) { Q_ASSERT( msg.type() == NotificationMessage::Collection ); Collection collection = col; if ( !collection.isValid() ) { collection = Collection( msg.uid() ); - collection.setParent( msg.parentCollection() ); + collection.setParentCollection( Collection( msg.parentCollection() ) ); collection.setResource( QString::fromUtf8( msg.resource() ) ); collection.setRemoteId( msg.remoteId() ); } Collection parent = par; if ( !parent.isValid() ) parent = Collection( msg.parentCollection() ); + Collection destination = dest; + if ( !destination.isValid() ) + destination = Collection( msg.parentDestCollection() ); switch ( msg.operation() ) { case NotificationMessage::Add: emit q_ptr->collectionAdded( collection, parent ); break; case NotificationMessage::Modify: emit q_ptr->collectionChanged( collection ); break; + case NotificationMessage::Move: + emit q_ptr->collectionMoved( collection, parent, destination ); + break; case NotificationMessage::Remove: emit q_ptr->collectionRemoved( collection ); break; default: - Q_ASSERT_X( false, "MonitorPrivate::emitCollectionNotification", "Invalid enum value" ); + kDebug() << "Unknown operation type" << msg.operation() << "in collection change notification"; } } void MonitorPrivate::slotItemJobFinished( KJob* job ) { if ( !pendingJobs.contains( job ) ) { kWarning() << "Unknown job - wtf is going on here?"; return; } NotificationMessage msg = pendingJobs.take( job ); Item item; Collection col; Collection destCol; if ( job->error() ) { 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() << "Unknown job - wtf is going on here?"; return; } NotificationMessage msg = pendingJobs.take( job ); if ( job->error() ) { 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 ); + QHash cols; + foreach ( const Collection &c, listJob->collections() ) + cols.insert( c.id(), c ); + emitCollectionNotification( msg, cols.value( msg.uid() ), cols.value( msg.parentCollection() ), cols.value( msg.parentDestCollection() ) ); } } 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/monitor_p.h b/akonadi/monitor_p.h index 8f61f9006..9e8a31e23 100644 --- a/akonadi/monitor_p.h +++ b/akonadi/monitor_p.h @@ -1,192 +1,192 @@ /* 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. */ #ifndef AKONADI_MONITOR_P_H #define AKONADI_MONITOR_P_H #include "monitor.h" #include "collection.h" #include "collectionstatisticsjob.h" #include "item.h" #include "itemfetchscope.h" #include "job.h" #include #include "notificationmanagerinterface.h" #include #include #include namespace Akonadi { class Monitor; /** * @internal */ class MonitorPrivate { public: MonitorPrivate( Monitor *parent ); virtual ~MonitorPrivate() {} Monitor *q_ptr; Q_DECLARE_PUBLIC( Monitor ) org::freedesktop::Akonadi::NotificationManager *nm; Collection::List collections; QSet resources; QSet items; QSet mimetypes; bool monitorAll; QList sessions; ItemFetchScope mItemFetchScope; QHash pendingJobs; bool isCollectionMonitored( Collection::Id collection, const QByteArray &resource ) const { if ( monitorAll || isCollectionMonitored( collection ) || resources.contains( resource ) ) return true; return false; } bool isItemMonitored( Item::Id item, Collection::Id collection, Collection::Id collectionDest, const QString &mimetype, const QByteArray &resource ) const { if ( monitorAll || isCollectionMonitored( collection ) || isCollectionMonitored( collectionDest ) ||items.contains( item ) || resources.contains( resource ) || isMimeTypeMonitored( mimetype ) ) return true; return false; } bool isSessionIgnored( const QByteArray &sessionId ) const { return sessions.contains( sessionId ); } bool connectToNotificationManager(); bool acceptNotification( const NotificationMessage &msg ); bool processNotification( const NotificationMessage &msg ); // private slots void slotSessionDestroyed( QObject* ); void slotStatisticsChangedFinished( KJob* ); void slotFlushRecentlyChangedCollections(); virtual void slotNotify( const NotificationMessage::List &msgs ); void slotItemJobFinished( KJob* job ); void slotCollectionJobFinished( KJob *job ); void emitItemNotification( const NotificationMessage &msg, const Item &item = Item(), const Collection &collection = Collection(), const Collection &collectionDest = Collection() ); void emitCollectionNotification( const NotificationMessage &msg, const Collection &col = Collection(), - const Collection &par = Collection() ); + const Collection &par = Collection(), const Collection &dest = Collection() ); bool fetchCollection; bool fetchCollectionStatistics; private: // collections that need a statistics update QSet recentlyChangedCollections; bool isCollectionMonitored( Collection::Id collection ) const { if ( collections.contains( Collection( collection ) ) ) return true; if ( collections.contains( Collection::root() ) ) return true; return false; } bool isMimeTypeMonitored( const QString& mimetype ) const { if ( mimetypes.contains( mimetype ) ) return true; KMimeType::Ptr mimeType = KMimeType::mimeType( mimetype, KMimeType::ResolveAliases ); if ( mimeType.isNull() ) return false; foreach ( const QString &mt, mimetypes ) { if ( mimeType->is( mt ) ) return true; } return false; } void fetchStatistics( Collection::Id colId ) { CollectionStatisticsJob *job = new CollectionStatisticsJob( Collection( colId ), q_ptr ); QObject::connect( job, SIGNAL(result(KJob*)), q_ptr, SLOT(slotStatisticsChangedFinished(KJob*)) ); } void notifyCollectionStatisticsWatchers( Collection::Id collection, const QByteArray &resource ) { if ( isCollectionMonitored( collection, resource ) ) { if (recentlyChangedCollections.empty() ) QTimer::singleShot( 500, q_ptr, SLOT(slotFlushRecentlyChangedCollections()) ); recentlyChangedCollections.insert( collection ); } } }; /** * @internal * * A job which fetches an item and a collection. */ class AKONADI_EXPORT ItemCollectionFetchJob : public Job { Q_OBJECT public: explicit ItemCollectionFetchJob( const Item &item, Collection::Id collectionId, Collection::Id destCollectionId, QObject *parent = 0 ); ~ItemCollectionFetchJob(); Item item() const; Collection collection() const; Collection destCollection() const; void setFetchScope( const ItemFetchScope &fetchScope ); protected: virtual void doStart(); private Q_SLOTS: void collectionJobDone( KJob* job ); void destCollectionJobDone( KJob* job ); void itemJobDone( KJob* job ); private: Item mReferenceItem; Collection::Id mCollectionId; Collection::Id mDestCollectionId; Item mItem; Collection mCollection; Collection mDestCollection; ItemFetchScope mFetchScope; }; } #endif diff --git a/akonadi/preprocessorbase.cpp b/akonadi/preprocessorbase.cpp index e8655717f..793fbbbea 100644 --- a/akonadi/preprocessorbase.cpp +++ b/akonadi/preprocessorbase.cpp @@ -1,107 +1,110 @@ /****************************************************************************** * * 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 "collection.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; }; -} +} // namespace Akonadi 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 ) +void PreprocessorBase::beginProcessItem( qlonglong itemId, qlonglong collectionId, const QString &mimeType ) { Q_D( PreprocessorBase ); - kDebug() << "PreprocessorBase: about to process item " << id; + kDebug() << "PreprocessorBase: about to process item " << itemId << " in collection " << collectionId << " and mimetype " << mimeType; - switch( processItem( Item( id ) ) ) + switch( processItem( static_cast< Item::Id >( itemId ), static_cast< Collection::Id >( collectionId ), mimeType ) ) { case ProcessingFailed: case ProcessingRefused: case ProcessingCompleted: - kDebug() << "PreprocessorBase: item processed, emitting signal (" << id << ")"; + kDebug() << "PreprocessorBase: item processed, emitting signal (" << itemId << ")"; + + // TODO: Handle the different status codes appropriately - emit itemProcessed( id ); + emit itemProcessed( itemId ); - kDebug() << "PreprocessorBase: item processed, signal emitted (" << id << ")"; + kDebug() << "PreprocessorBase: item processed, signal emitted (" << itemId << ")"; break; case ProcessingDelayed: - kDebug() << "PreprocessorBase: item processing delayed (" << id << ")"; + kDebug() << "PreprocessorBase: item processing delayed (" << itemId << ")"; d->mInDelayedProcessing = true; - d->mDelayedProcessingItemId = id; + d->mDelayedProcessingItemId = itemId; break; } } #include "preprocessorbase.moc" diff --git a/akonadi/preprocessorbase.h b/akonadi/preprocessorbase.h index cda0daf4c..b4942c924 100644 --- a/akonadi/preprocessorbase.h +++ b/akonadi/preprocessorbase.h @@ -1,167 +1,183 @@ /****************************************************************************** * * 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 +#include +#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 { Q_OBJECT public: /** * Describes the possible return values of the processItem() method. */ enum ProcessingResult { /** - * Processing completed succesfully for this item. + * Processing completed successfully 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. + * This method must be implemented by every preprocessor subclass. * - * Returns ProcessingCompleted on success, ProcessingDelayed + * It must realize the preprocessing of the item with the specified itemId, + * which is actually in the collection with collectionId and has the specified mimetype. + * + * The Akonadi server will push in for preprocessing any newly created item: + * it's your responsibility to decide if you want to process the item or not. + * + * The method should return ProcessingCompleted on success, ProcessingDelayed * if processing is implemented asynchronously and * ProcessingRefused or ProcessingFailed if the processing * didn't complete. + * + * If your operation is asynchronous then you should also + * connect to the abortRequested() signal and handle it + * appropriately (as the server MAY abort your async job + * if it decides that it's taking too long). */ - virtual ProcessingResult processItem( const Item &item ) = 0; + virtual ProcessingResult processItem( Item::Id itemId, Collection::Id collectionId, const QString &mimeType ) = 0; /** * This method must be called if processing is implemented asynchronously. * + * You should call it when you have completed the processing + * or if an abortRequest() signal arrives (and in this case you + * will probably use ProcessingFailed as result). + * * 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 ); + void beginProcessItem( qlonglong itemId, qlonglong collectionId, const QString &mimeType ); 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 0a81c21f3..99a7b9b3c 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 60844c582..c2900f6ec 100644 --- a/akonadi/resourcebase.cpp +++ b/akonadi/resourcebase.cpp @@ -1,606 +1,607 @@ /* Copyright (c) 2006 Till Adam Copyright (c) 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resourcebase.h" #include "agentbase_p.h" #include "resourceadaptor.h" #include "collectiondeletejob.h" #include "collectionsync_p.h" #include "itemsync.h" #include "resourcescheduler_p.h" #include "tracerinterface.h" #include "xdgbasedirs_p.h" #include "changerecorder.h" #include "collectionfetchjob.h" +#include "collectionfetchscope.h" #include "collectionmodifyjob.h" #include "itemfetchjob.h" #include "itemfetchscope.h" #include "itemmodifyjob.h" #include "itemmodifyjob_p.h" #include "session.h" #include "resourceselectjob_p.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; class Akonadi::ResourceBasePrivate : public AgentBasePrivate { public: ResourceBasePrivate( ResourceBase *parent ) : AgentBasePrivate( parent ), scheduler( 0 ), mItemSyncer( 0 ), mCollectionSyncer( 0 ) { 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() << "Not enough arguments passed..."; exit( 1 ); } for ( int i = 1; i < argc - 1; ++i ) { if ( QLatin1String( argv[ i ] ) == QLatin1String( "--identifier" ) ) identifier = QLatin1String( argv[ i + 1 ] ); } if ( identifier.isEmpty() ) { kDebug() << "Identifier argument missing"; exit( 1 ); } QByteArray catalog; char *p = strrchr( argv[0], '/' ); if ( p ) catalog = QByteArray( p + 1 ); else catalog = QByteArray( argv[0] ); KCmdLineArgs::init( argc, argv, identifier.toLatin1(), catalog, ki18nc("@title, application name", "Akonadi Resource"), "0.1", ki18nc("@title, application description", "Akonadi Resource") ); KCmdLineOptions options; options.add( "identifier ", ki18nc("@label, commandline option", "Resource identifier") ); KCmdLineArgs::addCmdLineOptions( options ); return identifier; } int ResourceBase::init( ResourceBase *r ) { QApplication::setQuitOnLastWindowClosed( false ); int rv = kapp->exec(); delete r; return rv; } void ResourceBase::itemRetrieved( const Item &item ) { Q_D( ResourceBase ); Q_ASSERT( d->scheduler->currentTask().type == ResourceScheduler::FetchItem ); if ( !item.isValid() ) { QDBusMessage reply( d->scheduler->currentTask().dbusMsg ); reply << false; QDBusConnection::sessionBus().send( reply ); d->scheduler->taskDone(); return; } Item i( item ); QSet requestedParts = d->scheduler->currentTask().itemParts; foreach ( const QByteArray &part, requestedParts ) { if ( !item.loadedPayloadParts().contains( part ) ) { kWarning() << "Item does not provide part" << part; } } ItemModifyJob *job = new ItemModifyJob( i ); // FIXME: remove once the item with which we call retrieveItem() has a revision number job->disableRevisionCheck(); connect( job, SIGNAL( result( KJob* ) ), SLOT( slotDeliveryDone( KJob* ) ) ); } void ResourceBasePrivate::slotDeliveryDone(KJob * job) { Q_Q( ResourceBase ); Q_ASSERT( scheduler->currentTask().type == ResourceScheduler::FetchItem ); QDBusMessage reply( scheduler->currentTask().dbusMsg ); if ( job->error() ) { emit q->error( QLatin1String( "Error while creating item: " ) + job->errorString() ); reply << false; } else { reply << true; } QDBusConnection::sessionBus().send( reply ); scheduler->taskDone(); } void ResourceBasePrivate::slotDeleteResourceCollection() { Q_Q( ResourceBase ); CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::FirstLevel ); - job->setResource( q->identifier() ); + job->fetchScope().setResource( q->identifier() ); connect( job, SIGNAL( result( KJob* ) ), q, SLOT( slotDeleteResourceCollectionDone( KJob* ) ) ); } void ResourceBasePrivate::slotDeleteResourceCollectionDone( KJob *job ) { Q_Q( ResourceBase ); if ( job->error() ) { emit q->error( job->errorString() ); scheduler->taskDone(); } else { const CollectionFetchJob *fetchJob = static_cast( job ); if ( !fetchJob->collections().isEmpty() ) { CollectionDeleteJob *job = new CollectionDeleteJob( fetchJob->collections().first() ); connect( job, SIGNAL( result( KJob* ) ), q, SLOT( slotCollectionDeletionDone( KJob* ) ) ); } else { // there is no resource collection, so just ignore the request scheduler->taskDone(); } } } void ResourceBasePrivate::slotCollectionDeletionDone( KJob *job ) { Q_Q( ResourceBase ); if ( job->error() ) { emit q->error( job->errorString() ); } scheduler->taskDone(); } void ResourceBase::changeCommitted( const Item& item ) { Q_D( ResourceBase ); ItemModifyJob *job = new ItemModifyJob( item ); job->d_func()->setClean(); job->disableRevisionCheck(); // TODO: remove, but where/how do we handle the error? job->ignorePayload(); // we only want to reset the dirty flag and update the remote id d->changeProcessed(); } void ResourceBase::changeCommitted( const Collection &collection ) { Q_D( ResourceBase ); CollectionModifyJob *job = new CollectionModifyJob( collection ); Q_UNUSED( job ); //TODO: error checking d->changeProcessed(); } bool ResourceBase::requestItemDelivery( qint64 uid, const QString & remoteId, const QString &mimeType, const QStringList &_parts ) { Q_D( ResourceBase ); if ( !isOnline() ) { emit error( i18nc( "@info", "Cannot fetch item in offline mode." ) ); return false; } setDelayedReply( true ); // FIXME: we need at least the revision number too Item item( uid ); item.setMimeType( mimeType ); item.setRemoteId( remoteId ); QSet parts; Q_FOREACH( const QString &str, _parts ) parts.insert( str.toLatin1() ); d->scheduler->scheduleItemFetch( item, parts, message().createReply() ); return true; } void ResourceBase::collectionsRetrieved( const Collection::List & collections ) { Q_D( ResourceBase ); Q_ASSERT_X( d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, "ResourceBase::collectionsRetrieved()", "Calling collectionsRetrieved() although no collection retrieval is in progress" ); if ( !d->mCollectionSyncer ) { d->mCollectionSyncer = new CollectionSync( identifier() ); 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 ); + list->fetchScope().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() ); + job->fetchScope().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/standardactionmanager.cpp b/akonadi/standardactionmanager.cpp index 4afc68062..3797f2c91 100644 --- a/akonadi/standardactionmanager.cpp +++ b/akonadi/standardactionmanager.cpp @@ -1,709 +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 ) ); enableAction( AddToFavoriteCollections, (favoritesModel!=0) && (!favoritesModel->collections().contains(col)) && singleColSelected && (col != Collection::root()) ); enableAction( RemoveFromFavoriteCollections, (favoritesModel!=0) && (favoritesModel->collections().contains(col)) && singleColSelected && (col != Collection::root()) ); enableAction( RenameFavoriteCollection, (favoritesModel!=0) && (favoritesModel->collections().contains(col)) && singleColSelected && (col != Collection::root()) ); enableAction( CopyCollectionToMenu, multiColSelected && (col != Collection::root()) ); } else { enableAction( CreateCollection, false ); enableAction( DeleteCollections, false ); enableAction( SynchronizeCollections, false ); enableAction( Paste, false ); enableAction( AddToFavoriteCollections, false ); enableAction( RemoveFromFavoriteCollections, false ); enableAction( RenameFavoriteCollection, false ); } bool multiItemSelected = false; 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 ); + col.parentCollection().setId( parentId ); CollectionCreateJob *job = new CollectionCreateJob( col ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(collectionCreationResult(KJob*)) ); } void slotCopyCollections() { copy( collectionSelectionModel ); } void slotDeleteCollection() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); QString text = i18n( "Do you really want to delete folder '%1' and all its sub-folders?", index.data().toString() ); if ( CollectionUtils::isVirtual( collection ) ) text = i18n( "Do you really want to delete the search view '%1'?", index.data().toString() ); if ( KMessageBox::questionYesNo( parentWidget, text, i18n("Delete folder?"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), QString(), KMessageBox::Dangerous ) != KMessageBox::Yes ) return; const Collection::Id colId = index.data( CollectionModel::CollectionIdRole ).toLongLong(); if ( colId <= 0 ) return; CollectionDeleteJob *job = new CollectionDeleteJob( Collection( colId ), q ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(collectionDeletionResult(KJob*)) ); } void slotSynchronizeCollection() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); AgentManager::self()->synchronizeCollection( col ); } void slotCollectionProperties() { if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); CollectionPropertiesDialog* dlg = new CollectionPropertiesDialog( col, parentWidget ); dlg->show(); } void slotCopyItems() { copy( itemSelectionModel ); } void slotPaste() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); KJob *job = PasteHelper::paste( QApplication::clipboard()->mimeData(), col ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(pasteResult(KJob*)) ); } void slotDeleteItems() { if ( KMessageBox::questionYesNo( parentWidget, i18n( "Do you really want to delete all selected items?" ), i18n("Delete?"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), QString(), KMessageBox::Dangerous ) != KMessageBox::Yes ) return; Q_ASSERT( itemSelectionModel ); // TODO: fix this once ItemModifyJob can handle item lists foreach ( const QModelIndex &index, itemSelectionModel->selectedRows() ) { bool ok; qlonglong id = index.data( ItemModel::IdRole ).toLongLong(&ok); Q_ASSERT(ok); new ItemDeleteJob( Item( id ), q ); } } void slotLocalSubscription() { SubscriptionDialog* dlg = new SubscriptionDialog( parentWidget ); dlg->show(); } void slotAddToFavorites() { Q_ASSERT( collectionSelectionModel ); Q_ASSERT( favoritesModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); favoritesModel->addCollection( collection ); } void slotRemoveFromFavorites() { Q_ASSERT( collectionSelectionModel ); Q_ASSERT( favoritesModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); favoritesModel->removeCollection( collection ); } void slotRenameFavorite() { Q_ASSERT( collectionSelectionModel ); Q_ASSERT( favoritesModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); bool ok; QString label = KInputDialog::getText( i18n( "Rename Favorite" ), i18nc( "@label:textbox New name of the folder.", "Name:" ), index.data().toString(), &ok, parentWidget ); if ( !ok ) return; favoritesModel->setFavoriteLabel( collection, label ); } void slotCopyCollectionTo( QAction *action ) { copyTo( collectionSelectionModel, action ); } void slotCopyItemTo( QAction *action ) { copyTo( itemSelectionModel, action ); } void copyTo( QItemSelectionModel *selectionModel, QAction *action ) { Q_ASSERT( selectionModel ); if ( selectionModel->selectedRows().count() <= 0 ) return; QMimeData *mimeData = selectionModel->model()->mimeData( selectionModel->selectedRows() ); Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); KJob *job = PasteHelper::paste( mimeData, col ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(copyToResult(KJob*)) ); } void collectionCreationResult( KJob *job ) { if ( job->error() ) { KMessageBox::error( parentWidget, i18n("Could not create folder: %1", job->errorString()), i18n("Folder creation failed") ); } } void collectionDeletionResult( KJob *job ) { if ( job->error() ) { KMessageBox::error( parentWidget, i18n("Could not delete folder: %1", job->errorString()), i18n("Folder deletion failed") ); } } void pasteResult( KJob *job ) { if ( job->error() ) { KMessageBox::error( parentWidget, i18n("Could not paste data: %1", job->errorString()), i18n("Paste failed") ); } } void copyToResult( KJob *job ) { if ( job->error() ) { KMessageBox::error( parentWidget, i18n("Could not copy data: %1", job->errorString()), i18n("Copy failed") ); } } void fillFoldersMenu( StandardActionManager::Type type, QMenu *menu, const QAbstractItemModel *model, QModelIndex parentIndex ) { int rowCount = model->rowCount( parentIndex ); for ( int row = 0; row < rowCount; row++ ) { QModelIndex index = model->index( row, 0, parentIndex ); Collection collection = model->data( index, CollectionModel::CollectionRole ).value(); if ( CollectionUtils::isVirtual( collection ) ) { continue; } QString label = model->data( index ).toString(); label.replace( QString::fromUtf8( "&" ), QString::fromUtf8( "&&" ) ); QIcon icon = model->data( index, Qt::DecorationRole ).value(); bool readOnly = CollectionUtils::isStructural( collection ) || ( type == CopyItemToMenu && !( collection.rights() & Collection::CanCreateItem ) ) || ( type == CopyCollectionToMenu && !( collection.rights() & Collection::CanCreateCollection ) ); if ( model->rowCount( index ) > 0 ) { // new level QMenu* popup = new QMenu( menu ); popup->setObjectName( QString::fromUtf8( "subMenu" ) ); popup->setTitle( label ); popup->setIcon( icon ); fillFoldersMenu( type, popup, model, index ); if ( !readOnly ) { popup->addSeparator(); QAction *act = popup->addAction( i18n("Copy to This Folder") ); act->setData( QVariant::fromValue( index ) ); } menu->addMenu( popup ); } else { // insert an item QAction* act = menu->addAction( icon, label ); act->setData( QVariant::fromValue( index ) ); act->setEnabled( !readOnly ); } } } void checkModelsConsistency() { if ( favoritesModel==0 || favoriteSelectionModel==0 ) { // No need to check when the favorite collections feature is not used return; } // Check that the collection selection model maps to the same // EntityTreeModel than favoritesModel if ( collectionSelectionModel!=0 ) { const QAbstractItemModel *model = collectionSelectionModel->model(); while ( const QAbstractProxyModel *proxy = qobject_cast( model ) ) { model = proxy->sourceModel(); } Q_ASSERT( model == favoritesModel->sourceModel() ); } // Check that the favorite selection model maps to favoritesModel const QAbstractItemModel *model = favoriteSelectionModel->model(); while ( const QAbstractProxyModel *proxy = qobject_cast( model ) ) { model = proxy->sourceModel(); } Q_ASSERT( model == favoritesModel->sourceModel() ); } StandardActionManager *q; KActionCollection *actionCollection; QWidget *parentWidget; QItemSelectionModel *collectionSelectionModel; QItemSelectionModel *itemSelectionModel; FavoriteCollectionsModel *favoritesModel; QItemSelectionModel *favoriteSelectionModel; QVector actions; AgentManager *agentManager; QHash pluralLabels; }; //@endcond StandardActionManager::StandardActionManager( KActionCollection * actionCollection, QWidget * parent) : QObject( parent ), d( new Private( this ) ) { d->parentWidget = parent; d->actionCollection = actionCollection; connect( QApplication::clipboard(), SIGNAL(changed(QClipboard::Mode)), SLOT(clipboardChanged(QClipboard::Mode)) ); } StandardActionManager::~ StandardActionManager() { delete d; } void StandardActionManager::setCollectionSelectionModel( QItemSelectionModel * selectionModel ) { d->collectionSelectionModel = selectionModel; connect( selectionModel, SIGNAL(selectionChanged( const QItemSelection&, const QItemSelection& )), SLOT(collectionSelectionChanged()) ); d->checkModelsConsistency(); } void StandardActionManager::setItemSelectionModel( QItemSelectionModel * selectionModel ) { d->itemSelectionModel = selectionModel; connect( selectionModel, SIGNAL(selectionChanged( const QItemSelection&, const QItemSelection& )), SLOT(updateActions()) ); } void StandardActionManager::setFavoriteCollectionsModel( FavoriteCollectionsModel *favoritesModel ) { d->favoritesModel = favoritesModel; d->checkModelsConsistency(); } void StandardActionManager::setFavoriteSelectionModel( QItemSelectionModel *selectionModel ) { d->favoriteSelectionModel = selectionModel; connect( selectionModel, SIGNAL(selectionChanged( const QItemSelection&, const QItemSelection& )), SLOT(favoriteSelectionChanged()) ); d->checkModelsConsistency(); } KAction* StandardActionManager::createAction( Type type ) { Q_ASSERT( type >= 0 && type < LastType ); Q_ASSERT( actionData[type].name ); if ( d->actions[type] ) return d->actions[type]; KAction *action; if ( !actionData[type].isActionMenu ) { action = new KAction( d->parentWidget ); } else { action = new KActionMenu( d->parentWidget ); } if ( d->pluralLabels.contains( type ) && !d->pluralLabels.value( type ).isEmpty() ) action->setText( d->pluralLabels.value( type ).subs( 1 ).toString() ); else if ( actionData[type].label ) action->setText( i18n( actionData[type].label ) ); if ( actionData[type].icon ) action->setIcon( KIcon( QString::fromLatin1( actionData[type].icon ) ) ); action->setShortcut( actionData[type].shortcut ); if ( actionData[type].slot && !actionData[type].isActionMenu ) { connect( action, SIGNAL(triggered()), actionData[type].slot ); } else if ( actionData[type].slot ) { KActionMenu *actionMenu = qobject_cast( action ); connect( actionMenu->menu(), SIGNAL(triggered(QAction*)), actionData[type].slot ); } d->actionCollection->addAction( QString::fromLatin1(actionData[type].name), action ); d->actions[type] = action; d->updateActions(); return action; } void StandardActionManager::createAllActions() { for ( int i = 0; i < LastType; ++i ) createAction( (Type)i ); } KAction * StandardActionManager::action( Type type ) const { Q_ASSERT( type >= 0 && type < LastType ); return d->actions[type]; } void StandardActionManager::setActionText( Type type, const KLocalizedString & text ) { Q_ASSERT( type >= 0 && type < LastType ); d->pluralLabels.insert( type, text ); d->updateActions(); } #include "standardactionmanager.moc" diff --git a/akonadi/tests/benchmarker/maildir/maildir20percentread.cpp b/akonadi/tests/benchmarker/maildir/maildir20percentread.cpp index 6dd9b5c5a..cd718e856 100644 --- a/akonadi/tests/benchmarker/maildir/maildir20percentread.cpp +++ b/akonadi/tests/benchmarker/maildir/maildir20percentread.cpp @@ -1,55 +1,56 @@ /* Copyright (c) 2009 Igor Trindade Oliveira based on kdepimlibs/akonadi/tests/benchmarker.cpp wrote by 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 "maildir20percentread.h" #include "maildir.h" #include #include #include +#include #include #include using namespace Akonadi; MailDir20PercentAsRead::MailDir20PercentAsRead():MailDir(){} void MailDir20PercentAsRead::runTest() { timer.start(); qDebug() << " Marking 20% of messages as read."; CollectionFetchJob *clj2 = new CollectionFetchJob( Collection::root() , CollectionFetchJob::Recursive ); - clj2->setResource( currentInstance.identifier() ); + clj2->fetchScope().setResource( currentInstance.identifier() ); clj2->exec(); Collection::List list2 = clj2->collections(); foreach ( const Collection &collection, list2 ) { ItemFetchJob *ifj = new ItemFetchJob( collection, this ); ifj->exec(); Item::List itemlist = ifj->items(); for ( int i = ifj->items().count() - 1; i >= 0; i -= 5) { Item item = itemlist[i]; item.setFlag( "\\Seen" ); ItemModifyJob *isj = new ItemModifyJob( item, this ); isj->exec(); } } outputStats( "mark20percentread" ); } diff --git a/akonadi/tests/benchmarker/maildir/maildirfetchallheaders.cpp b/akonadi/tests/benchmarker/maildir/maildirfetchallheaders.cpp index c31277e21..47d151972 100644 --- a/akonadi/tests/benchmarker/maildir/maildirfetchallheaders.cpp +++ b/akonadi/tests/benchmarker/maildir/maildirfetchallheaders.cpp @@ -1,60 +1,61 @@ /* Copyright (c) 2009 Igor Trindade Oliveira based on kdepimlibs/akonadi/tests/benchmarker.cpp wrote by 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 "maildirfetchallheaders.h" #include "maildir.h" #include #include #include +#include #include #include #include #include "kmime/messageparts.h" #include typedef boost::shared_ptr MessagePtr; using namespace Akonadi; MailDirFetchAllHeaders::MailDirFetchAllHeaders():MailDir(){} void MailDirFetchAllHeaders::runTest() { timer.start(); qDebug() << " Listing all headers of every folder."; CollectionFetchJob *clj = new CollectionFetchJob( Collection::root() , CollectionFetchJob::Recursive ); - clj->setResource( currentInstance.identifier() ); + clj->fetchScope().setResource( currentInstance.identifier() ); clj->exec(); Collection::List list = clj->collections(); foreach ( const Collection &collection, list ) { ItemFetchJob *ifj = new ItemFetchJob( collection, this ); ifj->fetchScope().fetchPayloadPart( MessagePart::Envelope ); ifj->exec(); QString a; foreach ( const Item &item, ifj->items() ) { a = item.payload()->subject()->asUnicodeString(); } } outputStats( "fullheaderlist" ); } diff --git a/akonadi/tests/benchmarker/maildir/maildirfetchunreadheaders.cpp b/akonadi/tests/benchmarker/maildir/maildirfetchunreadheaders.cpp index a1b13c846..7ce241897 100644 --- a/akonadi/tests/benchmarker/maildir/maildirfetchunreadheaders.cpp +++ b/akonadi/tests/benchmarker/maildir/maildirfetchunreadheaders.cpp @@ -1,63 +1,64 @@ /* Copyright (c) 2009 Igor Trindade Oliveira based on kdepimlibs/akonadi/tests/benchmarker.cpp wrote by 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 "maildirfetchunreadheaders.h" #include "maildir.h" #include #include #include +#include #include #include #include #include "kmime/messageparts.h" #include typedef boost::shared_ptr MessagePtr; using namespace Akonadi; MailDirFetchUnreadHeaders::MailDirFetchUnreadHeaders():MailDir(){} void MailDirFetchUnreadHeaders::runTest() { timer.start(); qDebug() << " Listing headers of unread messages of every folder."; CollectionFetchJob *clj3 = new CollectionFetchJob( Collection::root() , CollectionFetchJob::Recursive ); - clj3->setResource( currentInstance.identifier() ); + clj3->fetchScope().setResource( currentInstance.identifier() ); clj3->exec(); Collection::List list3 = clj3->collections(); foreach ( const Collection &collection, list3 ) { ItemFetchJob *ifj = new ItemFetchJob( collection, this ); ifj->fetchScope().fetchPayloadPart( MessagePart::Envelope ); ifj->exec(); QString a; foreach ( const Item &item, ifj->items() ) { // filter read messages if( !item.hasFlag( "\\Seen" ) ) { a = item.payload()->subject()->asUnicodeString(); } } } outputStats( "unreadheaderlist" ); } diff --git a/akonadi/tests/benchmarker/maildir/maildirremovereadmessages.cpp b/akonadi/tests/benchmarker/maildir/maildirremovereadmessages.cpp index 94ac98b63..6ed1f33ef 100644 --- a/akonadi/tests/benchmarker/maildir/maildirremovereadmessages.cpp +++ b/akonadi/tests/benchmarker/maildir/maildirremovereadmessages.cpp @@ -1,55 +1,56 @@ /* Copyright (c) 2009 Igor Trindade Oliveira based on kdepimlibs/akonadi/tests/benchmarker.cpp wrote by 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 "maildirremovereadmessages.h" #include "maildir.h" #include #include #include +#include #include #include using namespace Akonadi; MailDirRemoveReadMessages::MailDirRemoveReadMessages():MailDir(){} void MailDirRemoveReadMessages::runTest() { timer.start(); qDebug() << " Removing read messages from every folder."; CollectionFetchJob *clj4 = new CollectionFetchJob( Collection::root() , CollectionFetchJob::Recursive ); - clj4->setResource( currentInstance.identifier() ); + clj4->fetchScope().setResource( currentInstance.identifier() ); clj4->exec(); Collection::List list4 = clj4->collections(); foreach ( const Collection &collection, list4 ) { ItemFetchJob *ifj = new ItemFetchJob( collection, this ); ifj->exec(); foreach ( const Item &item, ifj->items() ) { // delete read messages if( item.hasFlag( "\\Seen" ) ) { ItemDeleteJob *idj = new ItemDeleteJob( item, this); idj->exec(); } } } outputStats( "removereaditems" ); } diff --git a/akonadi/tests/benchmarker/maketest.cpp b/akonadi/tests/benchmarker/maketest.cpp index 56e03c3d4..e1dacfb84 100644 --- a/akonadi/tests/benchmarker/maketest.cpp +++ b/akonadi/tests/benchmarker/maketest.cpp @@ -1,126 +1,127 @@ /* Copyright (c) 2009 Igor Trindade Oliveira based on kdepimlibs/akonadi/tests/benchmarker.cpp wrote by 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 "test.h" #include #include #include +#include #include #include #include using namespace Akonadi; MakeTest::MakeTest() { connect( AgentManager::self(), SIGNAL( instanceRemoved( const Akonadi::AgentInstance& ) ), this, SLOT( instanceRemoved( const Akonadi::AgentInstance& ) ) ); connect( AgentManager::self(), SIGNAL( instanceStatusChanged( const Akonadi::AgentInstance& ) ), this, SLOT( instanceStatusChanged( const Akonadi::AgentInstance& ) ) ); } void MakeTest::createAgent(const QString &name) { const AgentType type = AgentManager::self()->type( name ); AgentInstanceCreateJob *job = new AgentInstanceCreateJob( type ); job->exec(); currentInstance = job->instance(); if( job->error() || !currentInstance.isValid() ) { qDebug() << " Unable to create resource" << name; exit( -1 ); } else qDebug() << " Created resource instance" << currentInstance.identifier(); QTest::qWait(100); //fix this hack } void MakeTest::configureDBusIface(const QString &name,const QString &dir) { QDBusInterface *configIface = new QDBusInterface( "org.freedesktop.Akonadi.Resource." + currentInstance.identifier(), "/Settings", "org.kde.Akonadi." + name + ".Settings", QDBusConnection::sessionBus(), this ); configIface->call( "setPath", dir ); configIface->call( "setReadOnly", true ); if( !configIface->isValid()) qFatal( "Could not configure instance %s.", qPrintable( currentInstance.identifier() ) ); } void MakeTest::instanceRemoved( const AgentInstance &instance ) { Q_UNUSED( instance ); done = true; // qDebug() << "agent removed:" << instance; } void MakeTest::instanceStatusChanged( const AgentInstance &instance ) { //qDebug() << "agent status changed:" << agentIdentifier << status << message ; if ( instance == currentInstance ) { if ( instance.status() == AgentInstance::Running ) { //qDebug() << " " << message; } if ( instance.status() == AgentInstance::Idle ) { done = true; } } } void MakeTest::outputStats( const QString &description ) { output( description + "\t\t" + currentAccount + "\t\t" + QByteArray::number( timer.elapsed() ) + '\n' ); } void MakeTest::output( const QString &message ) { QTextStream out( stdout ); out << message; } void MakeTest::removeCollections() { timer.restart(); qDebug() << " Removing every folder sequentially."; CollectionFetchJob *clj5 = new CollectionFetchJob( Collection::root() , CollectionFetchJob::Recursive ); - clj5->setResource( currentInstance.identifier() ); + clj5->fetchScope().setResource( currentInstance.identifier() ); clj5->exec(); Collection::List list5 = clj5->collections(); foreach ( const Collection &collection, list5 ) { CollectionDeleteJob *cdj = new CollectionDeleteJob( collection, this ); cdj->exec(); } outputStats( "removeallcollections" ); } void MakeTest::removeResource() { qDebug() << " Removing resource."; AgentManager::self()->removeInstance( currentInstance ); } void MakeTest::start() { runTest(); } diff --git a/akonadi/tests/collectionattributetest.cpp b/akonadi/tests/collectionattributetest.cpp index 12439127d..7457e7530 100644 --- a/akonadi/tests/collectionattributetest.cpp +++ b/akonadi/tests/collectionattributetest.cpp @@ -1,183 +1,183 @@ /* 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 "collectionattributetest.h" #include "collectionattributetest.moc" #include "collectionpathresolver_p.h" #include #include #include #include #include #include #include #include #include using namespace Akonadi; QTEST_AKONADIMAIN( CollectionAttributeTest, NoGUI ) class TestAttribute : public Attribute { public: TestAttribute() : Attribute() {} TestAttribute( const QByteArray &data ) : mData( data ) {}; TestAttribute* clone() const { return new TestAttribute( mData ); } QByteArray type() const { return "TESTATTRIBUTE"; } QByteArray serialized() const { return mData; } void deserialize( const QByteArray &data ) { mData = data; } private: QByteArray mData; }; static int parentColId = -1; void CollectionAttributeTest::initTestCase() { Control::start(); AttributeFactory::registerAttribute(); CollectionPathResolver *resolver = new CollectionPathResolver( "res3", this ); QVERIFY( resolver->exec() ); parentColId = resolver->collection(); QVERIFY( parentColId > 0 ); } void CollectionAttributeTest::testAttributes_data() { QTest::addColumn("attr1"); QTest::addColumn("attr2"); QTest::newRow("basic") << QByteArray("foo") << QByteArray("bar"); QTest::newRow("empty1") << QByteArray("") << QByteArray("non-empty"); QTest::newRow("empty2") << QByteArray("non-empty") << QByteArray(""); QTest::newRow("space") << QByteArray("foo bar") << QByteArray("bar foo"); QTest::newRow("newline") << QByteArray("\n") << QByteArray("no newline"); QTest::newRow("newline2") << QByteArray(" \\\n\\\nnn") << QByteArray("no newline"); QTest::newRow("cr") << QByteArray("\r") << QByteArray("\\\r\n"); QTest::newRow("quotes") << QByteArray("\"quoted \\ test\"") << QByteArray("single \" quote \\"); QTest::newRow("parenthesis") << QByteArray(")") << QByteArray("("); QTest::newRow("binary") << QByteArray("\000") << QByteArray("\001"); } void CollectionAttributeTest::testAttributes() { QFETCH( QByteArray, attr1 ); QFETCH( QByteArray, attr2 ); // add a custom attribute TestAttribute *attr = new TestAttribute(); attr->deserialize( attr1 ); Collection col; col.setName( "attribute test" ); - col.setParent( parentColId ); + col.setParentCollection( Collection( parentColId ) ); col.addAttribute( attr ); CollectionCreateJob *create = new CollectionCreateJob( col, this ); QVERIFY( create->exec() ); col = create->collection(); QVERIFY( col.isValid() ); attr = col.attribute(); QVERIFY( attr != 0 ); QCOMPARE( attr->serialized(), QByteArray( attr1 ) ); CollectionFetchJob *list = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); QVERIFY( list->exec() ); QCOMPARE( list->collections().count(), 1 ); col = list->collections().first(); QVERIFY( col.isValid() ); attr = col.attribute(); QVERIFY( attr != 0 ); QCOMPARE( attr->serialized(), QByteArray( attr1 ) ); // modify a custom attribute col.attribute( Collection::AddIfMissing )->deserialize( attr2 ); CollectionModifyJob *modify = new CollectionModifyJob( col, this ); QVERIFY( modify->exec() ); list = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); QVERIFY( list->exec() ); QCOMPARE( list->collections().count(), 1 ); col = list->collections().first(); QVERIFY( col.isValid() ); attr = col.attribute(); QVERIFY( attr != 0 ); QCOMPARE( attr->serialized(), QByteArray( attr2 ) ); // delete a custom attribute col.removeAttribute(); modify = new CollectionModifyJob( col, this ); QVERIFY( modify->exec() ); list = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); QVERIFY( list->exec() ); QCOMPARE( list->collections().count(), 1 ); col = list->collections().first(); QVERIFY( col.isValid() ); attr = col.attribute(); QVERIFY( attr == 0 ); // cleanup CollectionDeleteJob *del = new CollectionDeleteJob( col, this ); QVERIFY( del->exec() ); } void CollectionAttributeTest::testDefaultAttributes() { Collection col; QCOMPARE( col.attributes().count(), 0 ); Attribute* attr = AttributeFactory::createAttribute( "TYPE" ); QVERIFY( attr ); attr->deserialize( "VALUE" ); col.addAttribute( attr ); QCOMPARE( col.attributes().count(), 1 ); QVERIFY( col.hasAttribute( "TYPE" ) ); QCOMPARE( col.attribute( "TYPE" )->serialized(), QByteArray("VALUE") ); } void CollectionAttributeTest::testCollectionRightsAttribute() { CollectionRightsAttribute attribute; Collection::Rights rights; QCOMPARE( attribute.rights(), rights ); for ( int mask = 0; mask <= Collection::AllRights; ++mask ) { rights = Collection::AllRights; rights &= mask; QCOMPARE( rights, mask ); attribute.setRights( rights ); QCOMPARE( attribute.rights(), rights ); QByteArray data = attribute.serialized(); attribute.deserialize( data ); QCOMPARE( attribute.rights(), rights ); } } diff --git a/akonadi/tests/collectioncreator.cpp b/akonadi/tests/collectioncreator.cpp index 68096b517..90dc11425 100644 --- a/akonadi/tests/collectioncreator.cpp +++ b/akonadi/tests/collectioncreator.cpp @@ -1,88 +1,88 @@ /* Copyright (c) 2006, 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 "agentinstance.h" #include "agentmanager.h" #include "collectioncreatejob.h" #include "collectionpathresolver_p.h" #include "transactionjobs.h" #include "qtest_akonadi.h" #include "test_utils.h" #include using namespace Akonadi; class CollectionCreator : public QObject { Q_OBJECT private slots: void initTestCase() { // switch all resources offline to reduce interference from them foreach ( Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances() ) agent.setIsOnline( false ); } void createCollections_data() { QTest::addColumn( "count" ); QTest::addColumn( "useTransaction" ); QList counts = QList() << 1 << 10 << 100 << 1000; QList transactions = QList() << false << true; foreach( int count, counts ) foreach( bool transaction, transactions ) QTest::newRow( QString::fromLatin1( "%1-%2" ).arg( count ).arg( transaction ? "trans" : "notrans" ).toLatin1().constData() ) << count << transaction; } void createCollections() { QFETCH( int, count ); QFETCH( bool, useTransaction ); const Collection parent( collectionIdFromPath( "res3" ) ); QVERIFY( parent.isValid() ); static int index = 0; Job *lastJob = 0; #if (QT_VERSION >= QT_VERSION_CHECK(4, 5, 0)) QBENCHMARK #endif { if ( useTransaction ) lastJob = new TransactionBeginJob( this ); for ( int i = 0; i < count; ++i ) { Collection col; - col.setParent( parent ); + col.setParentCollection( parent ); col.setName( QLatin1String("col") + QString::number( ++index ) ); lastJob = new CollectionCreateJob( col, this ); } if ( useTransaction ) lastJob = new TransactionCommitJob( this ); QTest::kWaitForSignal( lastJob, SIGNAL(result(KJob*)) ); } } }; QTEST_AKONADIMAIN( CollectionCreator, NoGUI ) #include "collectioncreator.moc" diff --git a/akonadi/tests/collectionjobtest.cpp b/akonadi/tests/collectionjobtest.cpp index d50f7ac6c..2ccac4526 100644 --- a/akonadi/tests/collectionjobtest.cpp +++ b/akonadi/tests/collectionjobtest.cpp @@ -1,625 +1,626 @@ /* 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 #include "collectionjobtest.h" #include #include "test_utils.h" #include "testattribute.h" #include "agentmanager.h" #include "agentinstance.h" #include "attributefactory.h" #include "cachepolicy.h" #include "collection.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionfetchjob.h" #include "collectionmodifyjob.h" #include "collectionselectjob_p.h" #include "collectionstatistics.h" #include "collectionstatisticsjob.h" #include "collectionpathresolver_p.h" #include "collectionutils_p.h" #include "control.h" #include "item.h" #include "kmime/messageparts.h" #include "resourceselectjob_p.h" +#include "collectionfetchscope.h" #include #include using namespace Akonadi; QTEST_AKONADIMAIN( CollectionJobTest, NoGUI ) void CollectionJobTest::initTestCase() { qRegisterMetaType(); AttributeFactory::registerAttribute(); Control::start(); // switch all resources offline to reduce interference from them foreach ( Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances() ) agent.setIsOnline( false ); } static Collection findCol( const Collection::List &list, const QString &name ) { foreach ( const Collection &col, list ) if ( col.name() == name ) return col; return Collection(); } // list compare which ignores the order template static void compareLists( const QList &l1, const QList &l2 ) { QCOMPARE( l1.count(), l2.count() ); foreach ( const T entry, l1 ) { QVERIFY( l2.contains( entry ) ); } } template static T* extractAttribute( QList attrs ) { T dummy; foreach ( Attribute* attr, attrs ) { if ( attr->type() == dummy.type() ) return dynamic_cast( attr ); } return 0; } static Collection::Id res1ColId = 6; // -1; static Collection::Id res2ColId = 7; //-1; static Collection::Id res3ColId = -1; static Collection::Id searchColId = -1; void CollectionJobTest::testTopLevelList( ) { // non-recursive top-level list CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::FirstLevel ); QVERIFY( job->exec() ); Collection::List list = job->collections(); // check if everything is there and has the correct types and attributes QCOMPARE( list.count(), 4 ); Collection col; col = findCol( list, "res1" ); QVERIFY( col.isValid() ); res1ColId = col.id(); // for the next test QVERIFY( res1ColId > 0 ); QVERIFY( CollectionUtils::isResource( col ) ); - QCOMPARE( col.parent(), Collection::root().id() ); + QCOMPARE( col.parentCollection(), Collection::root() ); QCOMPARE( col.resource(), QLatin1String("akonadi_knut_resource_0") ); QVERIFY( findCol( list, "res2" ).isValid() ); res2ColId = findCol( list, "res2" ).id(); QVERIFY( res2ColId > 0 ); QVERIFY( findCol( list, "res3" ).isValid() ); res3ColId = findCol( list, "res3" ).id(); QVERIFY( res3ColId > 0 ); col = findCol( list, "Search" ); searchColId = col.id(); QVERIFY( col.isValid() ); QVERIFY( CollectionUtils::isVirtualParent( col ) ); QCOMPARE( col.resource(), QLatin1String("akonadi_search_resource") ); } void CollectionJobTest::testFolderList( ) { // recursive list of physical folders CollectionFetchJob *job = new CollectionFetchJob( Collection( res1ColId ), CollectionFetchJob::Recursive ); QSignalSpy spy( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)) ); QVERIFY( spy.isValid() ); QVERIFY( job->exec() ); Collection::List list = job->collections(); int count = 0; for ( int i = 0; i < spy.count(); ++i ) { Collection::List l = spy[i][0].value(); for ( int j = 0; j < l.count(); ++j ) { QVERIFY( list.count() > count + j ); QCOMPARE( list[count + j].id(), l[j].id() ); } count += l.count(); } QCOMPARE( count, list.count() ); // check if everything is there QCOMPARE( list.count(), 4 ); Collection col; QStringList contentTypes; col = findCol( list, "foo" ); QVERIFY( col.isValid() ); - QCOMPARE( col.parent(), res1ColId ); + QCOMPARE( col.parentCollection().id(), res1ColId ); QVERIFY( CollectionUtils::isFolder( col ) ); contentTypes << "message/rfc822" << "text/calendar" << "text/directory" << "application/octet-stream" << "inode/directory"; compareLists( col.contentMimeTypes(), contentTypes ); QVERIFY( findCol( list, "bar" ).isValid() ); - QCOMPARE( findCol( list, "bar" ).parent(), col.id() ); + QCOMPARE( findCol( list, "bar" ).parentCollection(), col ); QVERIFY( findCol( list, "bla" ).isValid() ); } void CollectionJobTest::testNonRecursiveFolderList( ) { CollectionFetchJob *job = new CollectionFetchJob( Collection( res1ColId ), CollectionFetchJob::Base ); QVERIFY( job->exec() ); Collection::List list = job->collections(); QCOMPARE( list.count(), 1 ); QVERIFY( findCol( list, "res1" ).isValid() ); } void CollectionJobTest::testEmptyFolderList( ) { CollectionFetchJob *job = new CollectionFetchJob( Collection( res3ColId ), CollectionFetchJob::FirstLevel ); QVERIFY( job->exec() ); Collection::List list = job->collections(); QCOMPARE( list.count(), 0 ); } void CollectionJobTest::testSearchFolderList( ) { CollectionFetchJob *job = new CollectionFetchJob( Collection( searchColId ), CollectionFetchJob::FirstLevel ); QVERIFY( job->exec() ); Collection::List list = job->collections(); QCOMPARE( list.count(), 0 ); } void CollectionJobTest::testResourceFolderList() { // non-existing resource CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::FirstLevel ); - job->setResource( "i_dont_exist" ); + job->fetchScope().setResource( "i_dont_exist" ); QVERIFY( !job->exec() ); // recursive listing of all collections of an existing resource job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); - job->setResource( "akonadi_knut_resource_0" ); + job->fetchScope().setResource( "akonadi_knut_resource_0" ); QVERIFY( job->exec() ); Collection::List list = job->collections(); QCOMPARE( list.count(), 5 ); QVERIFY( findCol( list, "res1" ).isValid() ); QVERIFY( findCol( list, "foo" ).isValid() ); QVERIFY( findCol( list, "bar" ).isValid() ); QVERIFY( findCol( list, "bla" ).isValid() ); int fooId = findCol( list, "foo" ).id(); // limited listing of a resource job = new CollectionFetchJob( Collection( fooId ), CollectionFetchJob::Recursive ); - job->setResource( "akonadi_knut_resource_0" ); + job->fetchScope().setResource( "akonadi_knut_resource_0" ); QVERIFY( job->exec() ); list = job->collections(); QCOMPARE( list.count(), 3 ); QVERIFY( findCol( list, "bar" ).isValid() ); QVERIFY( findCol( list, "bla" ).isValid() ); } void CollectionJobTest::testMimeTypeFilter() { CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); - job->setContentMimeTypes( QStringList() << "message/rfc822" ); + job->fetchScope().setContentMimeTypes( QStringList() << "message/rfc822" ); AKVERIFYEXEC( job ); Collection::List list = job->collections(); QCOMPARE( list.count(), 2 ); QVERIFY( findCol( list, "res1" ).isValid() ); QVERIFY( findCol( list, "foo" ).isValid() ); int fooId = findCol( list, "foo" ).id(); // limited listing of a resource job = new CollectionFetchJob( Collection( fooId ), CollectionFetchJob::Recursive ); - job->setContentMimeTypes( QStringList() << "message/rfc822" ); + job->fetchScope().setContentMimeTypes( QStringList() << "message/rfc822" ); AKVERIFYEXEC( job ); list = job->collections(); QCOMPARE( list.count(), 0 ); } void CollectionJobTest::testCreateDeleteFolder_data() { QTest::addColumn("collection"); QTest::addColumn("creatable"); Collection col; QTest::newRow("empty") << col << false; col.setName( "new folder" ); - col.setParent( res3ColId ); + col.parentCollection().setId( res3ColId ); QTest::newRow("simple") << col << true; - col.setParent( res3ColId ); + col.parentCollection().setId( res3ColId ); col.setName( "foo" ); QTest::newRow( "existing in different resource" ) << col << true; col.setName( "mail folder" ); QStringList mimeTypes; mimeTypes << "inode/directory" << "message/rfc822"; col.setContentMimeTypes( mimeTypes ); col.setRemoteId( "remote id" ); CachePolicy policy; policy.setInheritFromParent( false ); policy.setIntervalCheckTime( 60 ); policy.setLocalParts( QStringList( MessagePart::Envelope ) ); policy.setSyncOnDemand( true ); policy.setCacheTimeout( 120 ); col.setCachePolicy( policy ); QTest::newRow( "complex" ) << col << true; col = Collection(); col.setName( "New Folder" ); - col.setParent( searchColId ); + col.parentCollection().setId( searchColId ); QTest::newRow( "search folder" ) << col << false; - col.setParent( res2ColId ); + col.parentCollection().setId( res2ColId ); col.setName( "foo2" ); QTest::newRow( "already existing" ) << col << false; col.setName( "Bla" ); - col.setParent( 2 ); + col.parentCollection().setId( 2 ); QTest::newRow( "already existing with different case" ) << col << true; CollectionPathResolver *resolver = new CollectionPathResolver( "res2/foo2", this ); QVERIFY( resolver->exec() ); - col.setParent( resolver->collection() ); + col.parentCollection().setId( resolver->collection() ); col.setName( "new folder" ); QTest::newRow( "parent noinferior" ) << col << false; - col.setParent( INT_MAX ); + col.parentCollection().setId( INT_MAX ); QTest::newRow( "missing parent" ) << col << false; col = Collection(); col.setName( "rid parent" ); - col.setParentRemoteId( "8" ); + col.parentCollection().setRemoteId( "8" ); QTest::newRow( "rid parent" ) << col << false; // missing resource context } void CollectionJobTest::testCreateDeleteFolder() { QFETCH( Collection, collection ); QFETCH( bool, creatable ); CollectionCreateJob *createJob = new CollectionCreateJob( collection, this ); QCOMPARE( createJob->exec(), creatable ); if ( !creatable ) return; Collection createdCol = createJob->collection(); QVERIFY( createdCol.isValid() ); QCOMPARE( createdCol.name(), collection.name() ); - QCOMPARE( createdCol.parent(), collection.parent() ); + QCOMPARE( createdCol.parentCollection(), collection.parentCollection() ); QCOMPARE( createdCol.remoteId(), collection.remoteId() ); QCOMPARE( createdCol.cachePolicy(), collection.cachePolicy() ); - CollectionFetchJob *listJob = new CollectionFetchJob( Collection( collection.parent() ), CollectionFetchJob::FirstLevel, this ); + CollectionFetchJob *listJob = new CollectionFetchJob( collection.parentCollection(), CollectionFetchJob::FirstLevel, this ); AKVERIFYEXEC( listJob ); Collection listedCol = findCol( listJob->collections(), collection.name() ); QCOMPARE( listedCol, createdCol ); QCOMPARE( listedCol.remoteId(), collection.remoteId() ); QCOMPARE( listedCol.cachePolicy(), collection.cachePolicy() ); // fetch parent to compare inherited collection properties Collection parentCol = Collection::root(); - if ( collection.parent() > 0 ) { - CollectionFetchJob *listJob = new CollectionFetchJob( Collection( collection.parent() ), CollectionFetchJob::Base, this ); + if ( collection.parentCollection().isValid() ) { + CollectionFetchJob *listJob = new CollectionFetchJob( collection.parentCollection(), CollectionFetchJob::Base, this ); AKVERIFYEXEC( listJob ); QCOMPARE( listJob->collections().count(), 1 ); parentCol = listJob->collections().first(); } if ( collection.contentMimeTypes().isEmpty() ) compareLists( listedCol.contentMimeTypes(), parentCol.contentMimeTypes() ); else compareLists( listedCol.contentMimeTypes(), collection.contentMimeTypes() ); if ( collection.resource().isEmpty() ) QCOMPARE( listedCol.resource(), parentCol.resource() ); else QCOMPARE( listedCol.resource(), collection.resource() ); CollectionDeleteJob *delJob = new CollectionDeleteJob( createdCol, this ); AKVERIFYEXEC( delJob ); - listJob = new CollectionFetchJob( Collection( collection.parent() ), CollectionFetchJob::FirstLevel, this ); + listJob = new CollectionFetchJob( collection.parentCollection(), CollectionFetchJob::FirstLevel, this ); AKVERIFYEXEC( listJob ); QVERIFY( !findCol( listJob->collections(), collection.name() ).isValid() ); } void CollectionJobTest::testIllegalDeleteFolder() { // non-existing folder CollectionDeleteJob *del = new CollectionDeleteJob( Collection( INT_MAX ), this ); QVERIFY( !del->exec() ); // root del = new CollectionDeleteJob( Collection::root(), this ); QVERIFY( !del->exec() ); } void CollectionJobTest::testStatistics() { // empty folder CollectionStatisticsJob *statistics = new CollectionStatisticsJob( Collection( res1ColId ), this ); QVERIFY( statistics->exec() ); CollectionStatistics s = statistics->statistics(); QCOMPARE( s.count(), 0ll ); QCOMPARE( s.unreadCount(), 0ll ); // folder with attributes and content CollectionPathResolver *resolver = new CollectionPathResolver( "res1/foo", this );; QVERIFY( resolver->exec() ); statistics = new CollectionStatisticsJob( Collection( resolver->collection() ), this ); QVERIFY( statistics->exec() ); s = statistics->statistics(); QCOMPARE( s.count(), 15ll ); QCOMPARE( s.unreadCount(), 14ll ); } void CollectionJobTest::testModify_data() { QTest::addColumn( "uid" ); QTest::addColumn( "rid" ); QTest::newRow( "uid" ) << collectionIdFromPath( "res1/foo" ) << QString(); QTest::newRow( "rid" ) << -1ll << QString( "10" ); } #define RESET_COLLECTION_ID \ col.setId( uid ); \ if ( !rid.isEmpty() ) col.setRemoteId( rid ) void CollectionJobTest::testModify() { QFETCH( qint64, uid ); QFETCH( QString, rid ); if ( !rid.isEmpty() ) { ResourceSelectJob *rjob = new ResourceSelectJob( "akonadi_knut_resource_0" ); AKVERIFYEXEC( rjob ); } QStringList reference; reference << "text/calendar" << "text/directory" << "message/rfc822" << "application/octet-stream" << "inode/directory"; Collection col; RESET_COLLECTION_ID; // test noop modify CollectionModifyJob *mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); CollectionFetchJob* ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QCOMPARE( ljob->collections().count(), 1 ); col = ljob->collections().first(); compareLists( col.contentMimeTypes(), reference ); // test clearing content types RESET_COLLECTION_ID; col.setContentMimeTypes( QStringList() ); mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QCOMPARE( ljob->collections().count(), 1 ); col = ljob->collections().first(); QVERIFY( col.contentMimeTypes().isEmpty() ); // test setting contnet types RESET_COLLECTION_ID; col.setContentMimeTypes( reference ); mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QCOMPARE( ljob->collections().count(), 1 ); col = ljob->collections().first(); compareLists( col.contentMimeTypes(), reference ); // add attribute RESET_COLLECTION_ID; col.attribute( Collection::AddIfMissing )->data = "new"; mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QVERIFY( ljob->collections().first().hasAttribute() ); QCOMPARE( ljob->collections().first().attribute()->data, QByteArray( "new" ) ); // modify existing attribute RESET_COLLECTION_ID; col.attribute()->data = "modified"; mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QVERIFY( ljob->collections().first().hasAttribute() ); QCOMPARE( ljob->collections().first().attribute()->data, QByteArray( "modified" ) ); // renaming RESET_COLLECTION_ID; col.setName( "foo (renamed)" ); mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); ljob = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); AKVERIFYEXEC( ljob ); QCOMPARE( ljob->collections().count(), 1 ); col = ljob->collections().first(); QCOMPARE( col.name(), QString( "foo (renamed)" ) ); RESET_COLLECTION_ID; col.setName( "foo" ); mod = new CollectionModifyJob( col, this ); AKVERIFYEXEC( mod ); } #undef RESET_COLLECTION_ID void CollectionJobTest::testIllegalModify() { // non-existing collection Collection col( INT_MAX ); - col.setParent( res1ColId ); + col.parentCollection().setId( res1ColId ); CollectionModifyJob *mod = new CollectionModifyJob( col, this ); QVERIFY( !mod->exec() ); // rename to already existing name col = Collection( res1ColId ); col.setName( "res2" ); mod = new CollectionModifyJob( col, this ); QVERIFY( !mod->exec() ); } void CollectionJobTest::testUtf8CollectionName() { QString folderName = QString::fromUtf8( "ä" ); // create collection Collection col; - col.setParent( res3ColId ); + col.parentCollection().setId( res3ColId ); col.setName( folderName ); CollectionCreateJob *create = new CollectionCreateJob( col, this ); QVERIFY( create->exec() ); col = create->collection(); QVERIFY( col.isValid() ); // list parent CollectionFetchJob *list = new CollectionFetchJob( Collection( res3ColId ), CollectionFetchJob::Recursive, this ); QVERIFY( list->exec() ); QCOMPARE( list->collections().count(), 1 ); QCOMPARE( col, list->collections().first() ); QCOMPARE( col.name(), folderName ); // modify collection col.setContentMimeTypes( QStringList( "message/rfc822'" ) ); CollectionModifyJob *modify = new CollectionModifyJob( col, this ); QVERIFY( modify->exec() ); // collection statistics CollectionStatisticsJob *statistics = new CollectionStatisticsJob( col, this ); QVERIFY( statistics->exec() ); CollectionStatistics s = statistics->statistics(); QCOMPARE( s.count(), 0ll ); QCOMPARE( s.unreadCount(), 0ll ); // delete collection CollectionDeleteJob *del = new CollectionDeleteJob( col, this ); QVERIFY( del->exec() ); } void CollectionJobTest::testMultiList() { Collection::List req; req << Collection( res1ColId ) << Collection( res2ColId ); CollectionFetchJob* job = new CollectionFetchJob( req, this ); QVERIFY( job->exec() ); Collection::List res; res = job->collections(); compareLists( res, req ); } void CollectionJobTest::testSelect() { CollectionPathResolver *resolver = new CollectionPathResolver( "res1/foo", this );; QVERIFY( resolver->exec() ); Collection col( resolver->collection() ); CollectionSelectJob *job = new CollectionSelectJob( col, this ); QVERIFY( job->exec() ); QCOMPARE( job->unseen(), -1 ); job = new CollectionSelectJob( col, this ); job->setRetrieveStatus( true ); QVERIFY( job->exec() ); QVERIFY( job->unseen() > -1 ); job = new CollectionSelectJob( Collection::root(), this ); QVERIFY( job->exec() ); job = new CollectionSelectJob( Collection( INT_MAX ), this ); QVERIFY( !job->exec() ); } void CollectionJobTest::testRidFetch() { Collection col; col.setRemoteId( "10" ); CollectionFetchJob *job = new CollectionFetchJob( col, CollectionFetchJob::Base, this ); - job->setResource( "akonadi_knut_resource_0" ); + job->fetchScope().setResource( "akonadi_knut_resource_0" ); QVERIFY( job->exec() ); QCOMPARE( job->collections().count(), 1 ); col = job->collections().first(); QVERIFY( col.isValid() ); QCOMPARE( col.remoteId(), QString::fromLatin1( "10" ) ); } void CollectionJobTest::testRidCreateDelete() { Collection collection; collection.setName( "rid create" ); - collection.setParentRemoteId( "8" ); + collection.parentCollection().setRemoteId( "8" ); collection.setRemoteId( "MY REMOTE ID" ); ResourceSelectJob *resSel = new ResourceSelectJob( "akonadi_knut_resource_2" ); AKVERIFYEXEC( resSel ); CollectionCreateJob *createJob = new CollectionCreateJob( collection, this ); AKVERIFYEXEC( createJob ); Collection createdCol = createJob->collection(); QVERIFY( createdCol.isValid() ); QCOMPARE( createdCol.name(), collection.name() ); CollectionFetchJob *listJob = new CollectionFetchJob( Collection( res3ColId ), CollectionFetchJob::FirstLevel, this ); AKVERIFYEXEC( listJob ); Collection listedCol = findCol( listJob->collections(), collection.name() ); QCOMPARE( listedCol, createdCol ); QCOMPARE( listedCol.name(), collection.name() ); QVERIFY( !collection.isValid() ); CollectionDeleteJob *delJob = new CollectionDeleteJob( collection, this ); AKVERIFYEXEC( delJob ); listJob = new CollectionFetchJob( Collection( res3ColId ), CollectionFetchJob::FirstLevel, this ); AKVERIFYEXEC( listJob ); QVERIFY( !findCol( listJob->collections(), collection.name() ).isValid() ); } #include "collectionjobtest.moc" diff --git a/akonadi/tests/collectionmovetest.cpp b/akonadi/tests/collectionmovetest.cpp index be21e5f46..9bb0eb81e 100644 --- a/akonadi/tests/collectionmovetest.cpp +++ b/akonadi/tests/collectionmovetest.cpp @@ -1,147 +1,147 @@ /* Copyright (c) 2006, 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 "test_utils.h" #include "collection.h" #include "collectionfetchjob.h" #include "collectionmovejob.h" #include "item.h" #include "itemfetchjob.h" #include "itemfetchscope.h" #include using namespace Akonadi; class CollectionMoveTest : public QObject { Q_OBJECT private slots: void testIllegalMove_data() { QTest::addColumn( "source" ); QTest::addColumn( "destination" ); const Collection res1( collectionIdFromPath( "res1" ) ); const Collection res1foo( collectionIdFromPath( "res1/foo" ) ); const Collection res1bla( collectionIdFromPath( "res1/foo/bar/bla" ) ); QTest::newRow( "non-existing-target" ) << res1 << Collection( INT_MAX ); QTest::newRow( "root" ) << Collection::root() << res1; QTest::newRow( "move-into-child" ) << res1 << res1foo; QTest::newRow( "same-name-in-target" ) << res1bla << res1foo; QTest::newRow( "non-existing-source" ) << Collection( INT_MAX ) << res1; } void testIllegalMove() { QFETCH( Collection, source ); QFETCH( Collection, destination ); QVERIFY( source.isValid() ); QVERIFY( destination.isValid() ); CollectionMoveJob* mod = new CollectionMoveJob( source, destination, this ); QVERIFY( !mod->exec() ); } void testMove_data() { QTest::addColumn( "source" ); QTest::addColumn( "destination" ); QTest::addColumn( "crossResource" ); QTest::newRow( "inter-resource" ) << Collection( collectionIdFromPath( "res1" ) ) << Collection( collectionIdFromPath( "res2" ) ) << true; QTest::newRow( "intra-resource" ) << Collection( collectionIdFromPath( "res1/foo/bla" ) ) << Collection( collectionIdFromPath( "res1/foo/bar/bla" ) ) << false; } // TODO: test signals void testMove() { QFETCH( Collection, source ); QFETCH( Collection, destination ); QFETCH( bool, crossResource ); QVERIFY( source.isValid() ); QVERIFY( destination.isValid() ); CollectionFetchJob *fetch = new CollectionFetchJob( source, CollectionFetchJob::Base, this ); AKVERIFYEXEC( fetch ); QCOMPARE( fetch->collections().count(), 1 ); source = fetch->collections().first(); // obtain reference listing fetch = new CollectionFetchJob( source, CollectionFetchJob::Recursive ); AKVERIFYEXEC( fetch ); QHash referenceData; foreach ( const Collection c, fetch->collections() ) { ItemFetchJob *job = new ItemFetchJob( c, this ); AKVERIFYEXEC( job ); referenceData.insert( c, job->items() ); } // move collection CollectionMoveJob *mod = new CollectionMoveJob( source, destination, this ); AKVERIFYEXEC( mod ); // check if source was modified correctly CollectionFetchJob *ljob = new CollectionFetchJob( source, CollectionFetchJob::Base ); AKVERIFYEXEC( ljob ); Collection::List list = ljob->collections(); QCOMPARE( list.count(), 1 ); Collection col = list.first(); QCOMPARE( col.name(), source.name() ); - QCOMPARE( col.parent(), destination.id() ); + QCOMPARE( col.parentCollection(), destination ); // list destination and check if everything is still there ljob = new CollectionFetchJob( destination, CollectionFetchJob::Recursive ); AKVERIFYEXEC( ljob ); list = ljob->collections(); QVERIFY( list.count() >= referenceData.count() ); for ( QHash::ConstIterator it = referenceData.constBegin(); it != referenceData.constEnd(); ++it ) { QVERIFY( list.contains( it.key() ) ); if ( crossResource ) { QVERIFY( list[ list.indexOf( it.key() ) ].resource() != it.key().resource() ); } else { QCOMPARE( list[ list.indexOf( it.key() ) ].resource(), it.key().resource() ); } ItemFetchJob *job = new ItemFetchJob( it.key(), this ); job->fetchScope().fetchFullPayload(); AKVERIFYEXEC( job ); QCOMPARE( job->items().count(), it.value().count() ); foreach ( const Item item, job->items() ) { QVERIFY( it.value().contains( item ) ); QVERIFY( item.hasPayload() ); } } // cleanup - mod = new CollectionMoveJob( col, Collection( source.parent() ), this ); + mod = new CollectionMoveJob( col, source.parentCollection(), this ); AKVERIFYEXEC( mod ); } }; QTEST_AKONADIMAIN( CollectionMoveTest, NoGUI ) #include "collectionmovetest.moc" diff --git a/akonadi/tests/collectionsynctest.cpp b/akonadi/tests/collectionsynctest.cpp index f30523013..74851db97 100644 --- a/akonadi/tests/collectionsynctest.cpp +++ b/akonadi/tests/collectionsynctest.cpp @@ -1,196 +1,260 @@ /* 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 "test_utils.h" #include #include #include #include #include +#include #include "../akonadi/collectionsync.cpp" #include #include #include #include using namespace Akonadi; Q_DECLARE_METATYPE( KJob* ) class CollectionSyncTest : public QObject { Q_OBJECT private: Collection::List fetchCollections( const QString &res ) { CollectionFetchJob *fetch = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); - fetch->setResource( res ); + fetch->fetchScope().setResource( res ); Q_ASSERT( fetch->exec() ); Q_ASSERT( !fetch->collections().isEmpty() ); return fetch->collections(); } + void makeTestData() + { + QTest::addColumn( "hierarchicalRIDs" ); + QTest::addColumn( "resource" ); + + QTest::newRow( "akonadi_knut_resource_0 global RID" ) << false << "akonadi_knut_resource_0"; + QTest::newRow( "akonadi_knut_resource_1 global RID" ) << false << "akonadi_knut_resource_1"; + QTest::newRow( "akonadi_knut_resource_2 global RID" ) << false << "akonadi_knut_resource_2"; + +/* QTest::newRow( "akonadi_knut_resource_0 hierarchical RID" ) << true << "akonadi_knut_resource_0"; + QTest::newRow( "akonadi_knut_resource_1 hierarchical RID" ) << true << "akonadi_knut_resource_1"; + QTest::newRow( "akonadi_knut_resource_2 hierarchical RID" ) << true << "akonadi_knut_resource_2";*/ + } + private slots: void initTestCase() { Control::start(); qRegisterMetaType(); // switch all resources offline to reduce interference from them foreach ( Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances() ) agent.setIsOnline( false ); } + void testFullSync_data() + { + makeTestData(); + } + void testFullSync() { - Collection::List origCols = fetchCollections( "akonadi_knut_resource_0" ); + QFETCH( bool, hierarchicalRIDs ); + QFETCH( QString, resource ); + + Collection::List origCols = fetchCollections( resource ); - CollectionSync* syncer = new CollectionSync( "akonadi_knut_resource_0" ); + CollectionSync* syncer = new CollectionSync( resource, this ); + syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setRemoteCollections( origCols ); AKVERIFYEXEC( syncer ); - Collection::List resultCols = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); } + void testFullStreamingSync_data() + { + makeTestData(); + } + void testFullStreamingSync() { - Collection::List origCols = fetchCollections( "akonadi_knut_resource_0" ); + QFETCH( bool, hierarchicalRIDs ); + QFETCH( QString, resource ); + + Collection::List origCols = fetchCollections( resource ); - CollectionSync* syncer = new CollectionSync( "akonadi_knut_resource_0" ); + CollectionSync* syncer = new CollectionSync( resource, this ); + syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setAutoDelete( false ); QSignalSpy spy( syncer, SIGNAL(result(KJob*)) ); QVERIFY( spy.isValid() ); syncer->setStreamingEnabled( true ); QTest::qWait( 10 ); QCOMPARE( spy.count(), 0 ); for ( int i = 0; i < origCols.count(); ++i ) { Collection::List l; l << origCols[i]; syncer->setRemoteCollections( l ); if ( i < origCols.count() - 1 ) QTest::qWait( 10 ); // enter the event loop so itemsync actually can do something QCOMPARE( spy.count(), 0 ); } syncer->retrievalDone(); QTest::qWait( 1000 ); // let it finish its job QCOMPARE( spy.count(), 1 ); KJob *job = spy.at( 0 ).at( 0 ).value(); QCOMPARE( job, syncer ); + QCOMPARE( job->errorText(), QString() ); QCOMPARE( job->error(), 0 ); - Collection::List resultCols = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); delete syncer; } + void testIncrementalSync_data() + { + makeTestData(); + } + void testIncrementalSync() { - Collection::List origCols = fetchCollections( "akonadi_knut_resource_0" ); + QFETCH( bool, hierarchicalRIDs ); + QFETCH( QString, resource ); + if ( resource == QLatin1String( "akonadi_knut_resource_2" ) ) + QSKIP( "test requires more than one collection", SkipSingle ); - CollectionSync* syncer = new CollectionSync( "akonadi_knut_resource_0" ); + Collection::List origCols = fetchCollections( resource ); + + CollectionSync* syncer = new CollectionSync( resource, this ); + syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setRemoteCollections( origCols, Collection::List() ); AKVERIFYEXEC( syncer ); - Collection::List resultCols = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); Collection::List delCols; delCols << resultCols.front(); resultCols.pop_front(); // ### not implemented yet I guess #if 0 Collection colWithOnlyRemoteId; colWithOnlyRemoteId.setRemoteId( resultCols.front().remoteId() ); delCols << colWithOnlyRemoteId; resultCols.pop_front(); #endif #if 0 // ### should this work? Collection colWithRandomRemoteId; colWithRandomRemoteId.setRemoteId( KRandom::randomString( 100 ) ); delCols << colWithRandomRemoteId; #endif - syncer = new CollectionSync( "akonadi_knut_resource_0" ); + syncer = new CollectionSync( resource, this ); syncer->setRemoteCollections( resultCols, delCols ); AKVERIFYEXEC( syncer ); - Collection::List resultCols2 = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols2 = fetchCollections( resource ); QCOMPARE( resultCols2.count(), resultCols.count() ); } + void testIncrementalStreamingSync_data() + { + makeTestData(); + } + void testIncrementalStreamingSync() { - Collection::List origCols = fetchCollections( "akonadi_knut_resource_0" ); + QFETCH( bool, hierarchicalRIDs ); + QFETCH( QString, resource ); + + Collection::List origCols = fetchCollections( resource ); - CollectionSync* syncer = new CollectionSync( "akonadi_knut_resource_0" ); + CollectionSync* syncer = new CollectionSync( resource, this ); + syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setAutoDelete( false ); QSignalSpy spy( syncer, SIGNAL(result(KJob*)) ); QVERIFY( spy.isValid() ); syncer->setStreamingEnabled( true ); QTest::qWait( 10 ); QCOMPARE( spy.count(), 0 ); for ( int i = 0; i < origCols.count(); ++i ) { Collection::List l; l << origCols[i]; syncer->setRemoteCollections( l, Collection::List() ); if ( i < origCols.count() - 1 ) QTest::qWait( 10 ); // enter the event loop so itemsync actually can do something QCOMPARE( spy.count(), 0 ); } syncer->retrievalDone(); QTest::qWait( 1000 ); // let it finish its job QCOMPARE( spy.count(), 1 ); KJob *job = spy.at( 0 ).at( 0 ).value(); QCOMPARE( job, syncer ); + QCOMPARE( job->errorText(), QString() ); QCOMPARE( job->error(), 0 ); - Collection::List resultCols = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); delete syncer; } + void testEmptyIncrementalSync_data() + { + makeTestData(); + } + void testEmptyIncrementalSync() { - Collection::List origCols = fetchCollections( "akonadi_knut_resource_0" ); + QFETCH( bool, hierarchicalRIDs ); + QFETCH( QString, resource ); + + Collection::List origCols = fetchCollections( resource ); - CollectionSync* syncer = new CollectionSync( "akonadi_knut_resource_0" ); + CollectionSync* syncer = new CollectionSync( resource, this ); + syncer->setHierarchicalRemoteIds( hierarchicalRIDs ); syncer->setRemoteCollections( Collection::List(), Collection::List() ); AKVERIFYEXEC( syncer ); - Collection::List resultCols = fetchCollections( "akonadi_knut_resource_0" ); + Collection::List resultCols = fetchCollections( resource ); QCOMPARE( resultCols.count(), origCols.count() ); } }; QTEST_AKONADIMAIN( CollectionSyncTest, NoGUI ) #include "collectionsynctest.moc" diff --git a/akonadi/tests/itemtest.cpp b/akonadi/tests/itemtest.cpp index d1256e58d..2e5218da8 100644 --- a/akonadi/tests/itemtest.cpp +++ b/akonadi/tests/itemtest.cpp @@ -1,63 +1,93 @@ /* 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 "itemtest.h" #include "itemtest.moc" #include "testattribute.h" #include #include +#include QTEST_KDEMAIN( ItemTest, NoGUI ) using namespace Akonadi; void ItemTest::testMultipart() { Item item; item.setMimeType( "application/octet-stream" ); QSet parts; QCOMPARE( item.loadedPayloadParts(), parts ); QByteArray bodyData = "bodydata"; item.setPayload( bodyData ); parts << Item::FullPayload; QCOMPARE( item.loadedPayloadParts(), parts ); QCOMPARE( item.payload(), bodyData ); QByteArray myData = "mypartdata"; item.attribute( Item::AddIfMissing )->data = myData; QCOMPARE( item.loadedPayloadParts(), parts ); QCOMPARE( item.attributes().count(), 1 ); QVERIFY( item.hasAttribute() ); QCOMPARE( item.attribute()->data, myData ); } void ItemTest::testInheritance() { Item a; a.setRemoteId( "Hello World" ); Item b( a ); b.setFlag( "\\send" ); } + +void ItemTest::testParentCollection () +{ + Item a; + QVERIFY( !a.parentCollection().isValid() ); + + a.setParentCollection( Collection::root() ); + QCOMPARE( a.parentCollection(), Collection::root() ); + Item b = a; + QCOMPARE( b.parentCollection(), Collection::root() ); + + Item c; + c.parentCollection().setRemoteId( "foo" ); + QCOMPARE( c.parentCollection().remoteId(), QString( "foo" ) ); + const Item d = c; + QCOMPARE( d.parentCollection().remoteId(), QString( "foo" ) ); + + const Item e; + QVERIFY( !e.parentCollection().isValid() ); + + Collection col( 5 ); + Item f; + f.setParentCollection( col ); + QCOMPARE( f.parentCollection(), col ); + Item g = f; + QCOMPARE( g.parentCollection(), col ); + b = g; + QCOMPARE( b.parentCollection(), col ); +} diff --git a/akonadi/tests/itemtest.h b/akonadi/tests/itemtest.h index b8be110a3..cd6112e31 100644 --- a/akonadi/tests/itemtest.h +++ b/akonadi/tests/itemtest.h @@ -1,34 +1,35 @@ /* Copyright (c) 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_ITEMTEST_H #define AKONADI_ITEMTEST_H #include class ItemTest : public QObject { Q_OBJECT private slots: void testMultipart(); void testInheritance(); + void testParentCollection(); }; #endif diff --git a/akonadi/tests/monitortest.cpp b/akonadi/tests/monitortest.cpp index 6222de4e9..da918d044 100644 --- a/akonadi/tests/monitortest.cpp +++ b/akonadi/tests/monitortest.cpp @@ -1,320 +1,358 @@ /* 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 "monitortest.h" #include "test_utils.h" #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; QTEST_AKONADIMAIN( MonitorTest, NoGUI ) static Collection res3; Q_DECLARE_METATYPE(Akonadi::Collection::Id) Q_DECLARE_METATYPE(QSet) void MonitorTest::initTestCase() { Control::start(); res3 = Collection( collectionIdFromPath( "res3" ) ); // switch all resources offline to reduce interference from them foreach ( Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances() ) agent.setIsOnline( false ); } void MonitorTest::testMonitor_data() { QTest::addColumn( "fetchCol" ); QTest::newRow( "with collection fetching" ) << true; QTest::newRow( "without collection fetching" ) << false; } void MonitorTest::testMonitor() { QFETCH( bool, fetchCol ); Monitor *monitor = new Monitor( this ); monitor->setCollectionMonitored( Collection::root() ); monitor->fetchCollection( fetchCol ); monitor->itemFetchScope().fetchFullPayload(); // monitor signals qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType >(); QSignalSpy caddspy( monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)) ); QSignalSpy cmodspy( monitor, SIGNAL(collectionChanged(const Akonadi::Collection&)) ); + QSignalSpy cmvspy( monitor, SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)) ); QSignalSpy crmspy( monitor, SIGNAL(collectionRemoved(const Akonadi::Collection&)) ); QSignalSpy cstatspy( monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)) ); QSignalSpy iaddspy( monitor, SIGNAL(itemAdded(const Akonadi::Item&, const Akonadi::Collection&)) ); QSignalSpy imodspy( monitor, SIGNAL(itemChanged(const Akonadi::Item&, const QSet&)) ); QSignalSpy imvspy( monitor, SIGNAL(itemMoved(const Akonadi::Item&, const Akonadi::Collection&, const Akonadi::Collection&)) ); QSignalSpy irmspy( monitor, SIGNAL(itemRemoved(const Akonadi::Item&)) ); QVERIFY( caddspy.isValid() ); QVERIFY( cmodspy.isValid() ); + QVERIFY( cmvspy.isValid() ); QVERIFY( crmspy.isValid() ); QVERIFY( cstatspy.isValid() ); QVERIFY( iaddspy.isValid() ); QVERIFY( imodspy.isValid() ); QVERIFY( imvspy.isValid() ); QVERIFY( irmspy.isValid() ); // create a collection Collection monitorCol; - monitorCol.setParent( res3 ); + monitorCol.setParentCollection( res3 ); monitorCol.setName( "monitor" ); CollectionCreateJob *create = new CollectionCreateJob( monitorCol, this ); QVERIFY( create->exec() ); monitorCol = create->collection(); QVERIFY( monitorCol.isValid() ); QTest::qWait(1000); // make sure the DBus signal has been processed QCOMPARE( caddspy.count(), 1 ); QList arg = caddspy.takeFirst(); Collection col = arg.at(0).value(); QCOMPARE( col, monitorCol ); if ( fetchCol ) QCOMPARE( col.name(), QString("monitor") ); Collection parent = arg.at(1).value(); QCOMPARE( parent, res3 ); QVERIFY( cmodspy.isEmpty() ); + QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( cstatspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); // add an item Item newItem; newItem.setMimeType( "application/octet-stream" ); ItemCreateJob *append = new ItemCreateJob( newItem, monitorCol, this ); QVERIFY( append->exec() ); Item monitorRef = append->item(); QVERIFY( monitorRef.isValid() ); QTest::qWait(1000); QCOMPARE( cstatspy.count(), 1 ); arg = cstatspy.takeFirst(); QEXPECT_FAIL( "", "Don't know how to handle 'Akonadi::Collection::Id', use qRegisterMetaType to register it. <-- I did this, but it still doesn't work!", Continue ); QCOMPARE( arg.at(0).value(), monitorCol.id() ); /* qRegisterMetaType() registers the type with a name of "qlonglong". Doing qRegisterMetaType( "Akonadi::Collection::Id" ) doesn't help. The problem here is that Akonadi::Collection::Id is a typedef to qlonglong, and qlonglong is already a registered meta type. So the signal spy will give us a QVariant of type Akonadi::Collection::Id, but calling .value() on that variant will in fact end up calling qvariant_cast. From the point of view of QMetaType, Akonadi::Collection::Id and qlonglong are different types, so QVariant can't convert, and returns a default-constructed qlonglong, zero. When connecting to a real slot (without QSignalSpy), this problem is avoided, because the casting is done differently (via a lot of void pointers). The docs say nothing about qRegisterMetaType -ing a typedef, so I'm not sure if this is a bug or not. (cberzan) */ QCOMPARE( iaddspy.count(), 1 ); arg = iaddspy.takeFirst(); Item item = arg.at( 0 ).value(); QCOMPARE( item, monitorRef ); QCOMPARE( item.mimeType(), QString::fromLatin1( "application/octet-stream" ) ); Collection collection = arg.at( 1 ).value(); QCOMPARE( collection.id(), monitorCol.id() ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmodspy.isEmpty() ); + QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); // modify an item item.setPayload( "some new content" ); ItemModifyJob *store = new ItemModifyJob( item, this ); QVERIFY( store->exec() ); QTest::qWait(1000); QCOMPARE( cstatspy.count(), 1 ); arg = cstatspy.takeFirst(); QEXPECT_FAIL( "", "Don't know how to handle 'Akonadi::Collection::Id', use qRegisterMetaType to register it. <-- I did this, but it still doesn't work!", Continue ); QCOMPARE( arg.at(0).value(), monitorCol.id() ); QCOMPARE( imodspy.count(), 1 ); arg = imodspy.takeFirst(); item = arg.at( 0 ).value(); QCOMPARE( monitorRef, item ); QCOMPARE( item.payload(), QByteArray( "some new content" ) ); QSet parts = arg.at( 1 ).value >(); QCOMPARE( parts, QSet() << "PLD:RFC822" ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmodspy.isEmpty() ); + QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); // move an item ItemMoveJob *move = new ItemMoveJob( item, res3 ); QVERIFY( move->exec() ); QTest::qWait( 1000 ); QCOMPARE( cstatspy.count(), 1 ); arg = cstatspy.takeFirst(); QEXPECT_FAIL( "", "Don't know how to handle 'Akonadi::Collection::Id', use qRegisterMetaType to register it. <-- I did this, but it still doesn't work!", Continue ); QCOMPARE( arg.at(0).value(), monitorCol.id() ); QCOMPARE( imvspy.count(), 1 ); arg = imvspy.takeFirst(); item = arg.at( 0 ).value(); // the item QCOMPARE( monitorRef, item ); col = arg.at( 1 ).value(); // the source collection QCOMPARE( col.id(), monitorCol.id() ); col = arg.at( 2 ).value(); // the destination collection QCOMPARE( col.id(), res3.id() ); QCOMPARE( cmodspy.count(), 2 ); arg = cmodspy.takeFirst(); Collection col1 = arg.at( 0 ).value(); arg = cmodspy.takeFirst(); Collection col2 = arg.at( 0 ).value(); // source and dest collections, in any order QVERIFY( ( col1.id() == monitorCol.id() && col2.id() == res3.id() ) || ( col2.id() == monitorCol.id() && col1.id() == res3.id() ) ); QVERIFY( caddspy.isEmpty() ); + QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); // delete an item ItemDeleteJob *del = new ItemDeleteJob( monitorRef, this ); QVERIFY( del->exec() ); QTest::qWait(1000); QCOMPARE( cstatspy.count(), 1 ); arg = cstatspy.takeFirst(); QEXPECT_FAIL( "", "Don't know how to handle 'Akonadi::Collection::Id', use qRegisterMetaType to register it. <-- I did this, but it still doesn't work!", Continue ); QCOMPARE( arg.at(0).value(), monitorCol.id() ); cmodspy.clear(); QCOMPARE( irmspy.count(), 1 ); arg = irmspy.takeFirst(); Item ref = qvariant_cast( arg.at(0) ); QCOMPARE( monitorRef, ref ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmodspy.isEmpty() ); + QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); imvspy.clear(); // modify a collection monitorCol.setName( "changed name" ); CollectionModifyJob *mod = new CollectionModifyJob( monitorCol, this ); AKVERIFYEXEC( mod ); QTest::qWait(1000); QCOMPARE( cmodspy.count(), 1 ); arg = cmodspy.takeFirst(); col = arg.at(0).value(); QCOMPARE( col, monitorCol ); if ( fetchCol ) QCOMPARE( col.name(), QString("changed name") ); QVERIFY( caddspy.isEmpty() ); + QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( cstatspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); + + // move a collection + Collection dest = Collection( collectionIdFromPath( "res1/foo" ) ); + CollectionMoveJob *cmove = new CollectionMoveJob( monitorCol, dest, this ); + AKVERIFYEXEC( cmove ); + QTest::qWait( 1000 ); + + QCOMPARE( cmvspy.count(), 1 ); + arg = cmvspy.takeFirst(); + col = arg.at( 0 ).value(); + QCOMPARE( col, monitorCol ); + if ( fetchCol ) + QCOMPARE( col.name(), monitorCol.name() ); + col = arg.at( 1 ).value(); + QCOMPARE( col, res3 ); + col = arg.at( 2 ).value(); + QCOMPARE( col, dest ); + + QVERIFY( caddspy.isEmpty() ); + QVERIFY( cmodspy.isEmpty() ); + QVERIFY( crmspy.isEmpty() ); + QVERIFY( cstatspy.isEmpty() ); + QVERIFY( iaddspy.isEmpty() ); + QVERIFY( imodspy.isEmpty() ); + QVERIFY( imvspy.isEmpty() ); + QVERIFY( irmspy.isEmpty() ); + + // delete a collection CollectionDeleteJob *cdel = new CollectionDeleteJob( monitorCol, this ); QVERIFY( cdel->exec() ); QTest::qWait(1000); QCOMPARE( crmspy.count(), 1 ); arg = crmspy.takeFirst(); QCOMPARE( arg.at(0).value().id(), monitorCol.id() ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmodspy.isEmpty() ); + QVERIFY( cmvspy.isEmpty() ); QVERIFY( cstatspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); } void MonitorTest::testVirtualCollectionsMonitoring() { Monitor *monitor = new Monitor( this ); monitor->setCollectionMonitored( Collection( 1 ) ); // top-level 'Search' collection QSignalSpy caddspy( monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)) ); QVERIFY( caddspy.isValid() ); SearchCreateJob *job = new SearchCreateJob( "Test search collection", "test-search-query" ); AKVERIFYEXEC( job ); QTest::qWait( 1000 ); QCOMPARE( caddspy.count(), 1 ); } #include "monitortest.moc" diff --git a/akonadi/tests/searchjobtest.cpp b/akonadi/tests/searchjobtest.cpp index 1fbe3a8cd..8af2c9498 100644 --- a/akonadi/tests/searchjobtest.cpp +++ b/akonadi/tests/searchjobtest.cpp @@ -1,55 +1,55 @@ /* 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 "searchjobtest.h" #include "searchjobtest.moc" #include #include #include #include #include "collectionutils_p.h" #include QTEST_AKONADIMAIN( SearchJobTest, NoGUI ) using namespace Akonadi; void SearchJobTest::testCreateDeleteSearch() { SearchCreateJob *create = new SearchCreateJob( "search123456", "Akonadi", this ); QVERIFY( create->exec() ); CollectionFetchJob *list = new CollectionFetchJob( Collection( 1 ), CollectionFetchJob::Recursive, this ); QVERIFY( list->exec() ); Collection::List cols = list->collections(); Collection col; foreach ( const Collection &c, cols ) { if ( c.name() == "search123456" ) col = c; } QVERIFY( col.isValid() ); - QCOMPARE( col.parent(), 1LL ); + QCOMPARE( col.parentCollection().id(), 1LL ); QVERIFY( CollectionUtils::isVirtual( col ) ); CollectionDeleteJob *delJob = new CollectionDeleteJob( col, this ); QVERIFY( delJob->exec() ); } diff --git a/akonadi/tests/subscriptiontest.cpp b/akonadi/tests/subscriptiontest.cpp index a7a6ea984..5d5b6041a 100644 --- a/akonadi/tests/subscriptiontest.cpp +++ b/akonadi/tests/subscriptiontest.cpp @@ -1,92 +1,93 @@ /* 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 "test_utils.h" #include #include #include +#include #include #include #include using namespace Akonadi; class SubscriptionTest : public QObject { Q_OBJECT private slots: void initTestCase() { Control::start(); } void testSubscribe() { Collection::List l; l << Collection( collectionIdFromPath( "res2/foo2" ) ); QVERIFY( l.first().isValid() ); SubscriptionJob *sjob = new SubscriptionJob( this ); sjob->unsubscribe( l ); QVERIFY( sjob->exec() ); const Collection res2Col = Collection( collectionIdFromPath( "res2" ) ); QVERIFY( res2Col.isValid() ); CollectionFetchJob *ljob = new CollectionFetchJob( res2Col, CollectionFetchJob::FirstLevel, this ); QVERIFY( ljob->exec() ); QCOMPARE( ljob->collections().count(), 1 ); ljob = new CollectionFetchJob( res2Col, CollectionFetchJob::FirstLevel, this ); - ljob->includeUnsubscribed(); + ljob->fetchScope().setIncludeUnsubscribed( true ); QVERIFY( ljob->exec() ); QCOMPARE( ljob->collections().count(), 2 ); sjob = new SubscriptionJob( this ); sjob->subscribe( l ); QVERIFY( sjob->exec() ); ljob = new CollectionFetchJob( res2Col, CollectionFetchJob::FirstLevel, this ); QVERIFY( ljob->exec() ); QCOMPARE( ljob->collections().count(), 2 ); } void testEmptySubscribe() { Collection::List l; SubscriptionJob *sjob = new SubscriptionJob( this ); QVERIFY( sjob->exec() ); } void testInvalidSubscribe() { Collection::List l; l << Collection( 1 ); SubscriptionJob *sjob = new SubscriptionJob( this ); sjob->subscribe( l ); l << Collection( INT_MAX ); sjob->unsubscribe( l ); QVERIFY( !sjob->exec() ); } }; QTEST_AKONADIMAIN( SubscriptionTest, NoGUI ) #include "subscriptiontest.moc" diff --git a/akonadi/tests/transactiontest.cpp b/akonadi/tests/transactiontest.cpp index 589b9ce39..5d1a29a39 100644 --- a/akonadi/tests/transactiontest.cpp +++ b/akonadi/tests/transactiontest.cpp @@ -1,102 +1,102 @@ /* Copyright (c) 2008 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. */ #include "transactiontest.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; QTEST_AKONADIMAIN( TransactionTest, NoGUI ) void TransactionTest::initTestCase() { Control::start(); } void TransactionTest::testTransaction() { Collection basisCollection; CollectionFetchJob *listJob = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); QVERIFY( listJob->exec() ); Collection::List list = listJob->collections(); foreach ( const Collection &col, list ) if ( col.name() == "res3" ) basisCollection = col; Collection testCollection; - testCollection.setParent( basisCollection ); + testCollection.setParentCollection( basisCollection ); testCollection.setName( "transactionTest" ); testCollection.setRemoteId( "transactionTestRemoteId" ); CollectionCreateJob *job = new CollectionCreateJob( testCollection, Session::defaultSession() ); QVERIFY( job->exec() ); testCollection = job->collection(); TransactionBeginJob *beginTransaction1 = new TransactionBeginJob( Session::defaultSession() ); QVERIFY( beginTransaction1->exec() ); TransactionBeginJob *beginTransaction2 = new TransactionBeginJob( Session::defaultSession() ); QVERIFY( beginTransaction2->exec() ); TransactionCommitJob *commitTransaction2 = new TransactionCommitJob( Session::defaultSession() ); QVERIFY( commitTransaction2->exec() ); TransactionCommitJob *commitTransaction1 = new TransactionCommitJob( Session::defaultSession() ); QVERIFY( commitTransaction1->exec() ); TransactionCommitJob *commitTransactionX = new TransactionCommitJob( Session::defaultSession() ); QVERIFY( commitTransactionX->exec() == false ); TransactionBeginJob *beginTransaction3 = new TransactionBeginJob( Session::defaultSession() ); QVERIFY( beginTransaction3->exec() ); Item item; item.setMimeType( "application/octet-stream" ); item.setPayload( "body data" ); ItemCreateJob *appendJob = new ItemCreateJob( item, testCollection, Session::defaultSession() ); QVERIFY( appendJob->exec() ); TransactionRollbackJob *rollbackTransaction3 = new TransactionRollbackJob( Session::defaultSession() ); QVERIFY( rollbackTransaction3->exec() ); ItemFetchJob *fetchJob = new ItemFetchJob( testCollection, Session::defaultSession() ); QVERIFY( fetchJob->exec() ); QVERIFY( fetchJob->items().isEmpty() ); CollectionDeleteJob *deleteJob = new CollectionDeleteJob( testCollection, Session::defaultSession() ); QVERIFY( deleteJob->exec() ); } #include "transactiontest.moc" diff --git a/gpgme++/key.cpp b/gpgme++/key.cpp index c2c4f22aa..414bede84 100644 --- a/gpgme++/key.cpp +++ b/gpgme++/key.cpp @@ -1,682 +1,692 @@ /* key.cpp - wraps a gpgme key Copyright (C) 2003, 2005 Klarälvdalens Datakonsult AB This file is part of GPGME++. GPGME++ 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. GPGME++ 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 GPGME++; 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 #include "util.h" #include #include GpgME::Key::Null GpgME::Key::null; namespace GpgME { Key::Key() : key() {} Key::Key( const Null & ) : key() {} Key::Key( const shared_gpgme_key_t & k ) : key( k ) {} Key::Key( gpgme_key_t k, bool ref ) : key( k ? shared_gpgme_key_t( k, &gpgme_key_unref ) : shared_gpgme_key_t() ) { if ( ref && impl() ) gpgme_key_ref( impl() ); } UserID Key::userID( unsigned int index ) const { return UserID( key, index ); } Subkey Key::subkey( unsigned int index ) const { return Subkey( key, index ); } unsigned int Key::numUserIDs() const { if ( !key ) return 0; unsigned int count = 0; for ( gpgme_user_id_t uid = key->uids ; uid ; uid = uid->next ) ++count; return count; } unsigned int Key::numSubkeys() const { if ( !key ) return 0; unsigned int count = 0; for ( gpgme_sub_key_t subkey = key->subkeys ; subkey ; subkey = subkey->next ) ++count; return count; } std::vector Key::userIDs() const { if ( !key ) return std::vector(); std::vector v; v.reserve( numUserIDs() ); for ( gpgme_user_id_t uid = key->uids ; uid ; uid = uid->next ) v.push_back( UserID( key, uid ) ); return v; } std::vector Key::subkeys() const { if ( !key ) return std::vector(); std::vector v; v.reserve( numSubkeys() ); for ( gpgme_sub_key_t subkey = key->subkeys ; subkey ; subkey = subkey->next ) v.push_back( Subkey( key, subkey ) ); return v; } Key::OwnerTrust Key::ownerTrust() const { if ( !key ) return Unknown; switch ( key->owner_trust ) { default: case GPGME_VALIDITY_UNKNOWN: return Unknown; case GPGME_VALIDITY_UNDEFINED: return Undefined; case GPGME_VALIDITY_NEVER: return Never; case GPGME_VALIDITY_MARGINAL: return Marginal; case GPGME_VALIDITY_FULL: return Full; case GPGME_VALIDITY_ULTIMATE: return Ultimate; } } char Key::ownerTrustAsString() const { if ( !key ) return '?'; switch ( key->owner_trust ) { default: case GPGME_VALIDITY_UNKNOWN: return '?'; case GPGME_VALIDITY_UNDEFINED: return 'q'; case GPGME_VALIDITY_NEVER: return 'n'; case GPGME_VALIDITY_MARGINAL: return 'm'; case GPGME_VALIDITY_FULL: return 'f'; case GPGME_VALIDITY_ULTIMATE: return 'u'; } } Protocol Key::protocol() const { if ( !key ) return UnknownProtocol; switch ( key->protocol ) { case GPGME_PROTOCOL_CMS: return CMS; case GPGME_PROTOCOL_OpenPGP: return OpenPGP; default: return UnknownProtocol; } } const char * Key::protocolAsString() const { return key ? gpgme_get_protocol_name( key->protocol ) : 0 ; } bool Key::isRevoked() const { return key && key->revoked; } bool Key::isExpired() const { return key && key->expired; } bool Key::isDisabled() const { return key && key->disabled; } bool Key::isInvalid() const { return key && key->invalid; } bool Key::hasSecret() const { return key && key->secret; } bool Key::isRoot() const { return key && key->subkeys && key->subkeys->fpr && key->chain_id && strcasecmp( key->subkeys->fpr, key->chain_id ) == 0; } bool Key::canEncrypt() const { return key && key->can_encrypt; } bool Key::canSign() const { #ifndef GPGME_CAN_SIGN_ON_SECRET_OPENPGP_KEYLISTING_NOT_BROKEN if ( key && key->protocol == GPGME_PROTOCOL_OpenPGP ) return true; #endif return key && key->can_sign; } bool Key::canCertify() const { return key && key->can_certify; } bool Key::canAuthenticate() const { return key && key->can_authenticate; } bool Key::isQualified() const { #ifdef HAVE_GPGME_KEY_T_IS_QUALIFIED return key && key->is_qualified; #else return false; #endif } const char * Key::issuerSerial() const { return key ? key->issuer_serial : 0 ; } const char * Key::issuerName() const { return key ? key->issuer_name : 0 ; } const char * Key::chainID() const { return key ? key->chain_id : 0 ; } const char * Key::keyID() const { return key && key->subkeys ? key->subkeys->keyid : 0 ; } const char * Key::shortKeyID() const { if ( !key || !key->subkeys || !key->subkeys->keyid ) return 0; const int len = strlen( key->subkeys->keyid ); if ( len > 8 ) return key->subkeys->keyid + len - 8; // return the last 8 bytes (in hex notation) else return key->subkeys->keyid ; } const char * Key::primaryFingerprint() const { const char * fpr = key && key->subkeys ? key->subkeys->fpr : 0 ; if ( fpr ) return fpr; else return keyID(); } unsigned int Key::keyListMode() const { return key ? convert_from_gpgme_keylist_mode_t( key->keylist_mode ) : 0 ; } const Key & Key::mergeWith( const Key & other ) { // ### incomplete. Just merges has* and can*, nothing else atm // ### detach also missing if ( !this->primaryFingerprint() || !other.primaryFingerprint() || strcasecmp( this->primaryFingerprint(), other.primaryFingerprint() ) != 0 ) return *this; // only merge the Key object which describe the same key const gpgme_key_t me = impl(); const gpgme_key_t him = other.impl(); if ( !me || !him ) return *this; me->revoked |= him->revoked; me->expired |= him->expired; me->disabled |= him->disabled; me->invalid |= him->invalid; me->can_encrypt |= him->can_encrypt; me->can_sign |= him->can_sign; me->can_certify |= him->can_certify; me->secret |= him->secret; me->can_authenticate |= him->can_authenticate; #ifdef HAVE_GPGME_KEY_T_IS_QUALIFIED me->is_qualified |= him->is_qualified; #endif me->keylist_mode |= him->keylist_mode; +#ifdef HAVE_GPGME_SUBKEY_T_IS_CARDKEY + // make sure the gpgme_sub_key_t::is_cardkey flag isn't lost: + for ( gpgme_sub_key_t mysk = me->subkeys ; mysk ; mysk = mysk->next ) + for ( gpgme_sub_key_t hissk = him->subkeys ; hissk ; hissk = hissk->next ) + if ( strcmp( mysk->fpr, hissk->fpr ) == 0 ) { + mysk->is_cardkey |= hissk->is_cardkey; + break; + } +#endif + return *this; } // // // class Subkey // // gpgme_sub_key_t find_subkey( const shared_gpgme_key_t & key, unsigned int idx ) { if ( key ) for ( gpgme_sub_key_t s = key->subkeys ; s ; s = s->next, --idx ) if ( idx == 0 ) return s; return 0; } gpgme_sub_key_t verify_subkey( const shared_gpgme_key_t & key, gpgme_sub_key_t subkey ) { if ( key ) for ( gpgme_sub_key_t s = key->subkeys ; s ; s = s->next ) if ( s == subkey ) return subkey; return 0; } Subkey::Subkey() : key(), subkey( 0 ) {} Subkey::Subkey( const shared_gpgme_key_t & k, unsigned int idx ) : key( k ), subkey( find_subkey( k, idx ) ) { } Subkey::Subkey( const shared_gpgme_key_t & k, gpgme_sub_key_t sk ) : key( k ), subkey( verify_subkey( k, sk ) ) { } Key Subkey::parent() const { return Key( key ); } const char * Subkey::keyID() const { return subkey ? subkey->keyid : 0 ; } const char * Subkey::fingerprint() const { return subkey ? subkey->fpr : 0 ; } unsigned int Subkey::publicKeyAlgorithm() const { return subkey ? subkey->pubkey_algo : 0 ; } const char * Subkey::publicKeyAlgorithmAsString() const { return gpgme_pubkey_algo_name( subkey ? subkey->pubkey_algo : (gpgme_pubkey_algo_t)0 ); } bool Subkey::canEncrypt() const { return subkey && subkey->can_encrypt; } bool Subkey::canSign() const { return subkey && subkey->can_sign; } bool Subkey::canCertify() const { return subkey && subkey->can_certify; } bool Subkey::canAuthenticate() const { return subkey && subkey->can_authenticate; } bool Subkey::isQualified() const { #ifdef HAVE_GPGME_SUBKEY_T_IS_QUALIFIED return subkey && subkey->is_qualified; #else return false; #endif } bool Subkey::isCardKey() const { #ifdef HAVE_GPGME_SUBKEY_T_IS_CARDKEY return subkey && subkey->is_cardkey; #else return false; #endif } const char * Subkey::cardSerialNumber() const { #ifdef HAVE_GPGME_SUBKEY_T_IS_CARDKEY return subkey ? subkey->card_number : 0 ; #else return 0; #endif } bool Subkey::isSecret() const { return subkey && subkey->secret; } unsigned int Subkey::length() const { return subkey ? subkey->length : 0 ; } time_t Subkey::creationTime() const { return static_cast( subkey ? subkey->timestamp : 0 ); } time_t Subkey::expirationTime() const { return static_cast( subkey ? subkey->expires : 0 ); } bool Subkey::neverExpires() const { return expirationTime() == time_t(0); } bool Subkey::isRevoked() const { return subkey && subkey->revoked; } bool Subkey::isInvalid() const { return subkey && subkey->invalid; } bool Subkey::isExpired() const { return subkey && subkey->expired; } bool Subkey::isDisabled() const { return subkey && subkey->disabled; } // // // class UserID // // gpgme_user_id_t find_uid( const shared_gpgme_key_t & key, unsigned int idx ) { if ( key ) for ( gpgme_user_id_t u = key->uids ; u ; u = u->next, --idx ) if ( idx == 0 ) return u; return 0; } gpgme_user_id_t verify_uid( const shared_gpgme_key_t & key, gpgme_user_id_t uid ) { if ( key ) for ( gpgme_user_id_t u = key->uids ; u ; u = u->next ) if ( u == uid ) return uid; return 0; } UserID::UserID() : key(), uid( 0 ) {} UserID::UserID( const shared_gpgme_key_t & k, gpgme_user_id_t u ) : key( k ), uid( verify_uid( k, u ) ) { } UserID::UserID( const shared_gpgme_key_t & k, unsigned int idx ) : key( k ), uid( find_uid( k, idx ) ) { } Key UserID::parent() const { return Key( key ); } UserID::Signature UserID::signature( unsigned int index ) const { return Signature( key, uid, index ); } unsigned int UserID::numSignatures() const { if ( !uid ) return 0; unsigned int count = 0; for ( gpgme_key_sig_t sig = uid->signatures ; sig ; sig = sig->next ) ++count; return count; } std::vector UserID::signatures() const { if ( !uid ) return std::vector(); std::vector v; v.reserve( numSignatures() ); for ( gpgme_key_sig_t sig = uid->signatures ; sig ; sig = sig->next ) v.push_back( Signature( key, uid, sig ) ); return v; } const char * UserID::id() const { return uid ? uid->uid : 0 ; } const char * UserID::name() const { return uid ? uid->name : 0 ; } const char * UserID::email() const { return uid ? uid->email : 0 ; } const char * UserID::comment() const { return uid ? uid->comment : 0 ; } UserID::Validity UserID::validity() const { if ( !uid ) return Unknown; switch ( uid->validity ) { default: case GPGME_VALIDITY_UNKNOWN: return Unknown; case GPGME_VALIDITY_UNDEFINED: return Undefined; case GPGME_VALIDITY_NEVER: return Never; case GPGME_VALIDITY_MARGINAL: return Marginal; case GPGME_VALIDITY_FULL: return Full; case GPGME_VALIDITY_ULTIMATE: return Ultimate; } } char UserID::validityAsString() const { if ( !uid ) return '?'; switch ( uid->validity ) { default: case GPGME_VALIDITY_UNKNOWN: return '?'; case GPGME_VALIDITY_UNDEFINED: return 'q'; case GPGME_VALIDITY_NEVER: return 'n'; case GPGME_VALIDITY_MARGINAL: return 'm'; case GPGME_VALIDITY_FULL: return 'f'; case GPGME_VALIDITY_ULTIMATE: return 'u'; } } bool UserID::isRevoked() const { return uid && uid->revoked; } bool UserID::isInvalid() const { return uid && uid->invalid; } // // // class Signature // // gpgme_key_sig_t find_signature( gpgme_user_id_t uid, unsigned int idx ) { if ( uid ) for ( gpgme_key_sig_t s = uid->signatures ; s ; s = s->next, --idx ) if ( idx == 0 ) return s; return 0; } gpgme_key_sig_t verify_signature( gpgme_user_id_t uid, gpgme_key_sig_t sig ) { if ( uid ) for ( gpgme_key_sig_t s = uid->signatures ; s ; s = s->next ) if ( s == sig ) return sig; return 0; } UserID::Signature::Signature() : key(), uid( 0 ), sig( 0 ) {} UserID::Signature::Signature( const shared_gpgme_key_t & k, gpgme_user_id_t u, unsigned int idx ) : key( k ), uid( verify_uid( k, u ) ), sig( find_signature( uid, idx ) ) { } UserID::Signature::Signature( const shared_gpgme_key_t & k, gpgme_user_id_t u, gpgme_key_sig_t s ) : key( k ), uid( verify_uid( k, u ) ), sig( verify_signature( uid, s ) ) { } UserID UserID::Signature::parent() const { return UserID( key, uid ); } const char * UserID::Signature::signerKeyID() const { return sig ? sig->keyid : 0 ; } const char * UserID::Signature::algorithmAsString() const { return gpgme_pubkey_algo_name( sig ? sig->pubkey_algo : (gpgme_pubkey_algo_t)0 ); } unsigned int UserID::Signature::algorithm() const { return sig ? sig->pubkey_algo : 0 ; } time_t UserID::Signature::creationTime() const { return static_cast( sig ? sig->timestamp : 0 ); } time_t UserID::Signature::expirationTime() const { return static_cast( sig ? sig->expires : 0 ); } bool UserID::Signature::neverExpires() const { return expirationTime() == time_t(0); } bool UserID::Signature::isRevokation() const { return sig && sig->revoked; } bool UserID::Signature::isInvalid() const { return sig && sig->invalid; } bool UserID::Signature::isExpired() const { return sig && sig->expired; } bool UserID::Signature::isExportable() const { return sig && sig->exportable; } const char * UserID::Signature::signerUserID() const { return sig ? sig->uid : 0 ; } const char * UserID::Signature::signerName() const { return sig ? sig->name : 0 ; } const char * UserID::Signature::signerEmail() const { return sig ? sig->email : 0 ; } const char * UserID::Signature::signerComment() const { return sig ? sig->comment : 0 ; } unsigned int UserID::Signature::certClass() const { return sig ? sig->sig_class : 0 ; } UserID::Signature::Status UserID::Signature::status() const { if ( !sig ) return GeneralError; switch ( gpgme_err_code(sig->status) ) { case GPG_ERR_NO_ERROR: return NoError; case GPG_ERR_SIG_EXPIRED: return SigExpired; case GPG_ERR_KEY_EXPIRED: return KeyExpired; case GPG_ERR_BAD_SIGNATURE: return BadSignature; case GPG_ERR_NO_PUBKEY: return NoPublicKey; default: case GPG_ERR_GENERAL: return GeneralError; } } std::string UserID::Signature::statusAsString() const { if ( !sig ) return std::string(); char buf[ 1024 ]; gpgme_strerror_r( sig->status, buf, sizeof buf ); buf[ sizeof buf - 1 ] = '\0'; return std::string( buf ); } GpgME::Notation UserID::Signature::notation( unsigned int idx ) const { if ( !sig ) return GpgME::Notation(); #ifdef HAVE_GPGME_KEY_SIG_NOTATIONS for ( gpgme_sig_notation_t nota = sig->notations ; nota ; nota = nota->next ) if ( nota->name ) if ( idx-- == 0 ) return GpgME::Notation( nota ); #endif return GpgME::Notation(); } unsigned int UserID::Signature::numNotations() const { if ( !sig ) return 0; unsigned int count = 0; #ifdef HAVE_GPGME_KEY_SIG_NOTATIONS for ( gpgme_sig_notation_t nota = sig->notations ; nota ; nota = nota->next ) if ( nota->name ) ++count; // others are policy URLs... #endif return count; } std::vector UserID::Signature::notations() const { if ( !sig ) return std::vector(); std::vector v; #ifdef HAVE_GPGME_KEY_SIG_NOTATIONS v.reserve( numNotations() ); for ( gpgme_sig_notation_t nota = sig->notations ; nota ; nota = nota->next ) if ( nota->name ) v.push_back( GpgME::Notation( nota ) ); #endif return v; } const char * UserID::Signature::policyURL() const { #ifdef HAVE_GPGME_KEY_SIG_NOTATIONS if ( !sig ) return 0; for ( gpgme_sig_notation_t nota = sig->notations ; nota ; nota = nota->next ) if ( !nota->name ) return nota->value; #endif return 0; } } // namespace GpgME diff --git a/gpgme++/util.h b/gpgme++/util.h index 29c01e4f0..4829fd24b 100644 --- a/gpgme++/util.h +++ b/gpgme++/util.h @@ -1,130 +1,130 @@ /* util.h - some inline helper functions Copyright (C) 2004 Klarälvdalens Datakonsult AB This file is part of GPGME++. GPGME++ 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. GPGME++ 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 GPGME++; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // -*- c++ -*- #ifndef __GPGMEPP_UTIL_H__ #define __GPGMEPP_UTIL_H__ #include #include #include #ifndef NDEBUG #include #endif static inline const char * protect( const char * s ) { return s ? s : "" ; } static inline gpgme_keylist_mode_t add_to_gpgme_keylist_mode_t( unsigned int oldmode, unsigned int newmodes ) { if ( newmodes & GpgME::Local ) oldmode |= GPGME_KEYLIST_MODE_LOCAL; if ( newmodes & GpgME::Extern ) oldmode |= GPGME_KEYLIST_MODE_EXTERN; if ( newmodes & GpgME::Signatures ) oldmode |= GPGME_KEYLIST_MODE_SIGS; if ( newmodes & GpgME::SignatureNotations ) { #ifdef HAVE_GPGME_KEYLIST_MODE_SIG_NOTATIONS oldmode |= GPGME_KEYLIST_MODE_SIG_NOTATIONS; #elif !defined(NDEBUG) std::cerr << "GpgME: ignoring SignatureNotations keylist flag (gpgme too old)." << std::endl; #endif } if ( newmodes & GpgME::Ephemeral ) { #ifdef HAVE_GPGME_KEYLIST_MODE_EPHEMERAL oldmode |= GPGME_KEYLIST_MODE_EPHEMERAL; #elif !defined(NDEBUG) std::cerr << "GpgME: ignoring Ephemeral keylist flag (gpgme too old)." << std::endl; #endif } if ( newmodes & GpgME::Validate ) oldmode |= GPGME_KEYLIST_MODE_VALIDATE; #ifndef NDEBUG - if ( newmodes & ~(GpgME::Local|GpgME::Extern|GpgME::Signatures|GpgME::SignatureNotations|GpgME::Validate) ) + if ( newmodes & ~(GpgME::Local|GpgME::Extern|GpgME::Signatures|GpgME::SignatureNotations|GpgME::Ephemeral|GpgME::Validate) ) std::cerr << "GpgME::Context: keylist mode must be one of Local, " "Extern, Signatures, SignatureNotations, or Validate, or a combination thereof!" << std::endl; #endif return static_cast( oldmode ); } static inline unsigned int convert_from_gpgme_keylist_mode_t( unsigned int mode ) { unsigned int result = 0; if ( mode & GPGME_KEYLIST_MODE_LOCAL ) result |= GpgME::Local; if ( mode & GPGME_KEYLIST_MODE_EXTERN ) result |= GpgME::Extern; if ( mode & GPGME_KEYLIST_MODE_SIGS ) result |= GpgME::Signatures; #ifdef HAVE_GPGME_KEYLIST_MODE_SIG_NOTATIONS if ( mode & GPGME_KEYLIST_MODE_SIG_NOTATIONS ) result |= GpgME::SignatureNotations; #endif #ifdef HAVE_GPGME_KEYLIST_MODE_EPHEMERAL if ( mode & GPGME_KEYLIST_MODE_EPHEMERAL ) result |= GpgME::Ephemeral; #endif if ( mode & GPGME_KEYLIST_MODE_VALIDATE ) result |= GpgME::Validate; #ifndef NDEBUG if ( mode & ~(GPGME_KEYLIST_MODE_LOCAL| GPGME_KEYLIST_MODE_EXTERN| #ifdef HAVE_GPGME_KEYLIST_MODE_SIG_NOTATIONS GPGME_KEYLIST_MODE_SIG_NOTATIONS| #endif #ifdef HAVE_GPGME_KEYLIST_MODE_EPHEMERAL GPGME_KEYLIST_MODE_EPHEMERAL| #endif GPGME_KEYLIST_MODE_VALIDATE| GPGME_KEYLIST_MODE_SIGS) ) std::cerr << "GpgME: WARNING: gpgme_get_keylist_mode() returned an unknown flag!" << std::endl; #endif // NDEBUG return result; } static inline GpgME::Notation::Flags convert_from_gpgme_sig_notation_flags_t( unsigned int flags ) { #ifdef HAVE_GPGME_SIG_NOTATION_FLAGS_T unsigned int result = 0; #ifdef HAVE_GPGME_SIG_NOTATION_HUMAN_READABLE if ( flags & GPGME_SIG_NOTATION_HUMAN_READABLE ) result |= GpgME::Notation::HumanReadable ; #endif #ifdef HAVE_GPGME_SIG_NOTATION_CRITICAL if ( flags & GPGME_SIG_NOTATION_CRITICAL ) result |= GpgME::Notation::Critical ; #endif return static_cast( result ); #else return GpgME::Notation::NoFlags; #endif } #ifdef HAVE_GPGME_SIG_NOTATION_FLAGS_T static inline gpgme_sig_notation_flags_t add_to_gpgme_sig_notation_flags_t( unsigned int oldflags, unsigned int newflags ) { unsigned int result = oldflags; if ( newflags & GpgME::Notation::HumanReadable ) { #ifdef HAVE_GPGME_SIG_NOTATION_HUMAN_READABLE result |= GPGME_SIG_NOTATION_HUMAN_READABLE; #elif !defined(NDEBUG) std::cerr << "GpgME::Context: ignoring HumanReadable signature notation flag (gpgme too old)" << std::endl; #endif } if ( newflags & GpgME::Notation::Critical ) { #ifdef HAVE_GPGME_SIG_NOTATION_CRITICAL result |= GPGME_SIG_NOTATION_CRITICAL; #elif !defined(NDEBUG) std::cerr << "GpgME::Context: ignoring Critical signature notation flag (gpgme too old)" << std::endl; #endif } return static_cast( result ); } #endif #endif // __GPGMEPP_UTIL_H__ diff --git a/kabc/plugins/dir/dir.desktop b/kabc/plugins/dir/dir.desktop index 324501d07..c4d2e6244 100644 --- a/kabc/plugins/dir/dir.desktop +++ b/kabc/plugins/dir/dir.desktop @@ -1,59 +1,63 @@ [Desktop Entry] Name=Folder Name[ca]=Carpeta Name[da]=Mappe Name[de]=Ordner Name[el]=Φάκελος Name[es]=Carpeta Name[et]=Kataloog Name[fr]=Dossier Name[ga]=Fillteán Name[gl]=Cartafol Name[hu]=Mappa Name[it]=Cartella Name[km]=ážáž Name[lv]=Mape Name[nb]=Mappe Name[nds]=Orner Name[nn]=Mappe Name[pt]=Pasta Name[pt_BR]=Pasta +Name[ro]=Dosar +Name[sl]=Mapa Name[sr]=ФаÑцикла Name[sr@latin]=Fascikla Name[sv]=Katalog Name[tr]=Dizin Name[uk]=Тека Name[x-test]=xxFolderxx Name[zh_CN]=文件夹 Name[zh_TW]=資料夾 Comment=Provides access to contacts, each stored in a single file, in a given folder. Supports standard VCard file and other formats depending on availability of plugins. Comment[ca]=Proporciona l'accés als contactes, cada un emmagatzemat en un fitxer individual, en una carpeta donada. Accepta els fitxers estàndard VCard i altres formats depenent de la disponibilitat dels connectors. Comment[da]=Giver adgang til kontakter, hver lagret i en enkelt fil, i en given mappe. Understøtter standard VCard-fil og andre formater afhængigt af tilgængeligheden af plugins. Comment[de]=Ermöglicht Zugriff auf Kontakte, die in einzelnen Dateien in einem vorgegebenen Ordner gespeichert sind. Unterstützt Standard-VCard-Dateien und andere Formate abhängig von den verfügbaren Modulen. Comment[el]=ΠÏοσφέÏει Ï€Ïόσβαση σε επαφές, αποθηκευμένες σε ξεχωÏιστά αÏχεία, σε ένα 
δοσμένο φάκελο. ΥποστηÏίζει τυπικά αÏχεία VCard και άλλες μοÏφές αÏχείων ανάλογα με τη διαθεσιμότητα των Ï€Ïόσθετων. Comment[es]=Provee acceso a los contactos, cada uno almacenado en un archivo diferente, dentro de un directorio determinado. Soporta archivos VCard estándar y otros formatos dependiendo de la disponibilidad de los complementos Comment[et]=Võimaldab kasutada eraldi failidesse salvestatud kontakte määratud kataloogis. Toetab standardseid VCard-faile ja teisi vorminguid sõltuvalt pluginate olemasolust. Comment[fr]=Fourni l'accès aux contacts stockés chacun dans un fichier dans le dossier indiqué. Le format VCard et d'autres formats sont pris en charge en fonction des modules externes disponibles Comment[ga]=Soláthraíonn sé seo rochtain ar theagmhálacha, gach ceann stóráilte i gcomhad aonair, i bhfillteán sonraithe. Tacaítear le comhaid v-Chárta agus le formáidí eile, ag brath ar na breiseáin atá ar fáil. Comment[gl]=Dá acceso aos contactos, cada un gardado nun ficheiro nun cartafol dado. Admite ficheiros vCard estándar e outros formatos, en función da dispoñibilidade de extensións. Comment[hu]=Névjegyek elérését biztosítja. Minden névjegy külön fájlban található, egy adott mappában. Támogatja a standard vCard formátumot, és bÅ‘vítmények segítségével más formátumok is kezelhetÅ‘k. Comment[it]=Fornisce accesso a contatti, ciascuno dei quali memorizzato in un singolo file in una cartella data. Gestisce file in standard VCard e altri formati in base ai plugin disponibili. Comment[km]=ផ្ដល់​ការ​ចូលដំណើរការ​ទៅ​កាន់​ទំនាក់ទំនង ដែល​ទំនាក់ទំនង​នីមួយៗ​ážáŸ’រូវ​បានផ្ទុកនៅ​ក្នុង​ឯកសារ​ážáŸ‚​មួយ នៅ​ក្នុង​ážážâ€‹ážŠáŸ‚ល​បានផ្ដល់​ឲ្យ ។ គាំទ្រ​ឯកសារ VCard និង​ទ្រង់ទ្រាយ​ផ្សáŸáž„ៗ​ ដោយ​អាស្រáŸáž™â€‹áž›áž¾â€‹áž€áž˜áŸ’មវិធី​ជំនួយ​ដែល​មាន ។ Comment[lv]=NodroÅ¡ina pieeju kontaktiem, kas katrs saglabÄts individuÄlÄ failÄ norÄdÄ«tajÄ mapÄ“. Atbalsta standarta VCard failus un citus formÄtus, atkarÄ«bÄ no pieejamajiem spraudņiem. Comment[nb]=Gir tilgang til kontakter, lagret hver for seg i en enkelt fil, i en gitt mappe. Støtter standard vCard og andre formater avhengig av tilgjengelige programtillegg. Comment[nds]=Stellt Togriep op Kontakten praat, elkeen binnen een Datei binnen en angeven Orner. Ãœnnerstütt VCard-Dateien un anner Formaten na de verföögboren Modulen. Comment[nn]=Gjev tilgang til kontaktar i ei viss mappe, der dei er lagra i kvar sine filer. Standard vCard-filer er støtta, i tillegg til andre format, avhengig av kva programtillegg som er i bruk. Comment[pt]=Oferece o acesso aos contactos, estando cada um guardado num único ficheiro de uma dada pasta. Suporta os ficheiros VCard normais, assim como outros formatos, dependendo da disponibilidade dos 'plugins'. Comment[pt_BR]=Fornece acesso aos contatos, cada um armazenado em um único arquivo, em uma pasta informada. Suporta o formato de arquivos VCard padrão e outros formatos, dependendo da disponibilidade de plug-ins. +Comment[ro]=Oferă acces la contacte, fiecare stocat într-un singur fiÈ™ier, într-un dosar dat. SusÈ›ine fiÈ™iere VCard standard precum È™i alte formate în dependență de disponibilitatea modulelor. +Comment[sl]=Nudi dostop do stikov, ki so vsak posebej shranjeni v eni datoteki, v danem imeniku. Podpira obiÄajne datoteke VCard in druge formate, odvisno od dosegljivosti vstavkov. Comment[sr]=Пружа приÑтуп контактима Ñкладиштеним у појединачним фајловима у датој фаÑцикли. Подржава Ñтандардне в‑кард фајлове и друге формате, према раÑположивим прикључцима. Comment[sr@latin]=Pruža pristup kontaktima skladiÅ¡tenim u pojedinaÄnim fajlovima u datoj fascikli. Podržava standardne vCard fajlove i druge formate, prema raspoloživim prikljuÄcima. Comment[sv]=Ger tillgÃ¥ng till kontakter, var och en lagrad i en enstaka fil, i en given katalog. Stöder vCard-standardfiler och andra format, beroende pÃ¥ tillgängliga insticksprogram. Comment[uk]=Ðадає доÑтуп до контактів, кожен з Ñких зберігаєтьÑÑ Ñƒ окремому файлі у вказаній теці. Підтримує Ñтандартні файли VCard та файли у інших форматах, залежно від наÑвноÑÑ‚Ñ– відповідних додатків. Comment[x-test]=xxProvides access to contacts, each stored in a single file, in a given folder. Supports standard VCard file and other formats depending on availability of plugins.xx Comment[zh_CN]=æ供对被存储在指定目录下的å•ç‹¬ä¸€ä¸ªæ–‡ä»¶ä¸­çš„è”系人的访问支æŒã€‚支æŒæ ‡å‡† VCard 文件和其它æ’件所å…许的格å¼ã€‚ Comment[zh_TW]=æ供存å–特定目錄下,個別儲存在單一檔案中的è¯çµ¡äººçš„功能。支æ´æ¨™æº–çš„ VCard 檔,以åŠå¤–掛程å¼æ‰€æ供的其它格å¼æª”案。 X-KDE-Library=kabc_directory Type=Service X-KDE-ServiceTypes=KResources/Plugin X-KDE-ResourceFamily=contact X-KDE-ResourceType=dir diff --git a/kabc/plugins/file/file.desktop b/kabc/plugins/file/file.desktop index a2c8158e8..c106cae34 100644 --- a/kabc/plugins/file/file.desktop +++ b/kabc/plugins/file/file.desktop @@ -1,74 +1,76 @@ [Desktop Entry] Name=File Name[be]=Файл Name[ca]=Fitxer Name[cs]=Soubor Name[da]=Fil Name[de]=Datei Name[el]=ΑÏχείο Name[es]=Archivo Name[et]=Fail Name[fr]=Fichier Name[ga]=Comhad Name[gl]=Ficheiro Name[hne]=फाइल Name[hu]=Fájl Name[ja]=ファイル Name[km]=ឯកសារ Name[lt]=Failas Name[lv]=Fails Name[nb]=Fil Name[nds]=Datei Name[nl]=Bestand Name[nn]=Fil Name[oc]=Fichièr Name[pa]=ਫਾਇਲ Name[pl]=Plik Name[pt]=Ficheiro Name[pt_BR]=Arquivo Name[ro]=FiÈ™ier Name[ru]=Файл Name[se]=Fiila Name[sk]=Súbor Name[sl]=Datoteka Name[sr]=Фајл Name[sr@latin]=Fajl Name[sv]=Fil Name[th]=à¹à¸Ÿà¹‰à¸¡ Name[tr]=Dosya Name[uk]=Файл Name[x-test]=xxFilexx Name[zh_CN]=文件 Name[zh_TW]=檔案 Comment=Provides access to contacts stored in a single local file. Supports standard VCard files and other formats depending on available plugins. Comment[ca]=Proporciona l'accés als contactes emmagatzemats en un fitxer individual. Accepta els fitxers estàndard VCard i altres formats, depenent dels connectors disponibles. Comment[da]=Giver adgang til kontakter, hver lagret i en enkelt fil. Understøtter standard VCard-fil og andre formater afhængigt af tilgængelige af plugins. Comment[de]=Ermöglicht Zugriff auf Kontakte, die in einer einzigen Dateien lokal gespeichert sind. Unterstützt Standard-VCard-Dateien und andere Formate abhängig von den verfügbaren Modulen. Comment[el]=ΠÏοσφέÏει Ï€Ïόσβαση σε επαφές, αποθηκευμένες σε ένα τοπικό αÏχείο. ΥποστηÏίζει τυπικά αÏχεία VCard και άλλες μοÏφές αÏχείων ανάλογα με τη διαθεσιμότητα των Ï€Ïόσθετων. Comment[es]=Provee acceso a los contactos almacenados en un único archivo local. Soporta archivos VCard estándar y otros formatos dependiendo de la disponibilidad de los componentes. Comment[et]=Võimaldab kasutada kohalikku faili salvestatud kontakte. Toetab standardseid VCard-faile ja teisi vorminguid sõltuvalt pluginate olemasolust. Comment[fr]=Fourni l'accès aux contacts stockés dans un seul fichier local. Le format VCard et d'autres formats sont pris en charge en fonction des modules externes disponibles. Comment[ga]=Soláthraíonn sé seo rochtain ar theagmhálacha, stóráilte i gcomhad aonair. Tacaítear le comhaid v-Chárta agus formáidí eile, ag brath ar na breiseáin atá ar fáil. Comment[gl]=Dá acceso a contactos gardados nun único ficheiro local. Admite ficheiros vCard estándar e outros formatos en función das extensións dispoñíbeis. Comment[hu]=Névjegyek elérését biztosítja. Minden névjegy egy közös helyi fájlban található. Támogatja a standard vCard formátumot, és bÅ‘vítmények segítségével más formátumok is kezelhetÅ‘k. Comment[it]=Fornisce accesso a contatti memorizzati in un singolo file locale. Gestisce file in standard VCard e altri formati in base ai plugin disponibili. Comment[ja]=å˜ä¸€ã®ãƒ­ãƒ¼ã‚«ãƒ«ãƒ•ã‚¡ã‚¤ãƒ«ã«ä¿å­˜ã•ã‚Œã¦ã„る連絡先ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’æä¾›ã—ã¾ã™ã€‚標準㮠VCard ファイルã¨ã€åˆ©ç”¨å¯èƒ½ãªãƒ—ラグインã«å¿œã˜ãŸãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã‚’サãƒãƒ¼ãƒˆã—ã¾ã™ã€‚ Comment[km]=ផ្ដល់​កា​រចូលដំណើរការ​ទៅ​ទំនាក់ទំនង​​នៅ​ក្នុង​ឯកសារ​មូលដ្ឋាន​ážáŸ‚​មួយ ។ គាំទ្រ​ឯកសារ VCard ស្ážáž„់ដារ និង​ទ្រង់ទ្រាយ​ផ្សáŸáž„ៗ​ ដោយ​អាស្រáŸáž™â€‹áž›áž¾â€‹áž€áž˜áŸ’មវិធី​ជំនួយ​ដែល​មាន ។ Comment[lv]=NodroÅ¡ina piekļuvi kontaktiem, kas glabÄjas vienÄ lokÄlÄ failÄ. Atbalsta standarta VCard failus un citus formÄtus, atkarÄ«bÄ no pieejamajiem spraudņiem. Comment[nb]=Gir tilgang til kontakter, lagret i en enkelt lokal fil. Støtter standard vCard og andre formater avhengig av tilgjengelige programtillegg. Comment[nds]=Stellt Togriep op Kontakten praat, de in een enkel lokaal Datei wohrt warrt. Ãœnnerstütt VCard-Dateien un anner Formaten na de 'verföögboren Modulen. Comment[nn]=Gjev tilgang til kontaktar lagra i ei einskild lokal fil. Standard vCard-filer er støtta, i tillegg til andre format, avhengig av kva programtillegg som er i bruk. Comment[pt]=Oferece o acesso aos contactos, estando todos guardados num único ficheiro local. Suporta os ficheiros VCard normais, assim como outros formatos, dependendo da disponibilidade dos 'plugins'. Comment[pt_BR]=Fornece acesso aos contatos armazenados em um único arquivo local. Suporta o formato de arquivos VCard padrão e outros formatos, dependendo da disponibilidade de plug-ins. +Comment[ro]=Oferă acces la contactele stocate într-un singur fiÈ™ier. SusÈ›ine fiÈ™iere VCard standard precum È™i alte formate în dependență de modulele disponibile. +Comment[sl]=Nudi dostop do stikov, ki so shranjeni v eni sami krajevni datoteki. Podpira obiÄajne datoteke VCard in druge formate, odvisno od dosegljivosti vstavkov. Comment[sr]=Пружа приÑтуп контактима Ñкладиштеним у једном локалном фајлу. Подржава Ñтандардне в‑кард фајлове и друге формате, према раÑположивим прикључцима. Comment[sr@latin]=Pruža pristup kontaktima skladiÅ¡tenim u jednom lokalnom fajlu. Podržava standardne vCard fajlove i druge formate, prema raspoloživim prikljuÄcima. Comment[sv]=Ger tillgÃ¥ng till kontakter lagrade i en enda lokal fil. Stöder vCard-standardfiler och andra format, beroende pÃ¥ tillgängliga insticksprogram. Comment[uk]=Ðадає доÑтуп до контактів, Ñкі зберігаютьÑÑ Ñƒ окремому локальному файлі. Підтримує Ñтандартні файли VCard та файли у інших форматах, залежно від наÑвноÑÑ‚Ñ– відповідних додатків. Comment[x-test]=xxProvides access to contacts stored in a single local file. Supports standard VCard files and other formats depending on available plugins.xx Comment[zh_CN]=æ供对被存储在å•ç‹¬çš„本地文件中的è”系人的访问支æŒã€‚支æŒæ ‡å‡† VCard 文件和其它æ’件所å…许的格å¼ã€‚ Comment[zh_TW]=æ供存å–儲存在單一檔案中的è¯çµ¡äººçš„功能。支æ´æ¨™æº–çš„ VCard 檔,以åŠå¤–掛程å¼æ‰€æ供的其它格å¼æª”案。 X-KDE-Library=kabc_file Type=Service X-KDE-ServiceTypes=KResources/Plugin X-KDE-ResourceFamily=contact X-KDE-ResourceType=file diff --git a/kabc/plugins/net/net.desktop b/kabc/plugins/net/net.desktop index 92e5290da..5a9f92099 100644 --- a/kabc/plugins/net/net.desktop +++ b/kabc/plugins/net/net.desktop @@ -1,76 +1,77 @@ [Desktop Entry] Name=Network Name[be]=Сетка Name[ca]=Xarxa Name[cs]=Síť Name[da]=Netværk Name[de]=Netzwerk Name[el]=Δίκτυο Name[es]=Red Name[et]=Võrk Name[fr]=Réseau Name[ga]=Líonra Name[gl]=Rede Name[hne]=नेटवरà¥à¤• Name[hu]=Hálózat Name[it]=Rete Name[ja]=ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ Name[km]=បណ្ážáž¶áž‰ Name[lt]=Tinklas Name[lv]=TÄ«kls Name[nb]=Nettverk Name[nds]=Nettwark Name[nl]=Netwerk Name[nn]=Nettverk Name[oc]=Ret Name[pa]=ਨੈੱਟਵਰਕ Name[pl]=Sieć Name[pt]=Rede Name[pt_BR]=Rede Name[ro]=ReÈ›ea Name[ru]=Сетевой файл Name[se]=Fierbmi Name[sk]=SieÅ¥ Name[sl]=Omrežje Name[sr]=Мрежа Name[sr@latin]=Mreža Name[sv]=Nätverk Name[th]=ระบบเครือข่าย Name[tr]=AÄŸ Name[uk]=Мережа Name[x-test]=xxNetworkxx Name[zh_CN]=网络 Name[zh_TW]=網路 Comment=Provides access to contacts in remote files using KDE's network framework KIO. Supports standard VCard files and other formats depending on available plugins. Comment[ca]=Proporciona l'accés als contactes en fitxers remots usant la infraestructura KIO de xarxa del KDE. Accepta els fitxers estàndard VCard i altres formats, depenent dels connectors disponibles. Comment[da]=Giver adgang til kontakter i eksterne filer med brug af KDE's netværks-framework KIO. Understøtter standard VCard-filer og andre formater afhængigt af tilgængelige plugins. Comment[de]=Ermöglicht Zugriff auf Kontakte in entfernten Dateien durch das KIO-Netzwerksystem von KDE. Unterstützt Standard-VCard-Dateien und andere Formate abhängig von den verfügbaren Modulen. Comment[el]=ΠÏοσφέÏει Ï€Ïόσβαση σε επαφές σε απομακÏυσμένα αÏχεία με τη χÏήση του συστήματος KIO του KDE. ΥποστηÏίζει τυπικά αÏχεία VCard και άλλες μοÏφές αÏχείων ανάλογα με τη διαθεσιμότητα των Ï€Ïόσθετων. Comment[es]=Provee acceso a los contactos en un archivo remoto utilizando la infraestructura de red KIO de KDE. Soporta archivos VCard estándar y otros formatos dependiendo en la disponibilidad de los complementos. Comment[et]=Võimaldab kasutada võrgufaile KDE võrguraamistiku KIO abil. Toetab standardseid VCard-faile ja teisi vorminguid sõltuvalt pluginate olemasolust. Comment[fr]=Fourni l'accès aux contacts stockés dans des fichiers distants en utilisant le mécanisme réseau KIO de KDE. Le format VCard et d'autres formats sont pris en charge en fonction des modules externes disponibles. Comment[ga]=Soláthraíonn sé seo rochtain ar theagmhálacha i gcianchomhaid tríd an gcreatlach líonra KIO atá cuid de KDE. Tacaítear le comhaid v-Chárta agus formáidí eile, ag brath ar na breiseáin atá ar fáil. Comment[gl]=Dá acceso a contactos gardados en ficheiros remotos mediante a infraestrutura de rede KIO, de KDE. Admite ficheiros vCard estándar e outros formatos en función das extensións dispoñíbeis. Comment[hu]=Távoli fájlokban található névjegyek elérését biztosítja a KDE KIO keretrendszeren keresztül. Támogatja a standard vCard formátumot, és bÅ‘vítmények segítségével más formátumok is kezelhetÅ‘k. Comment[it]=Fornisce accesso a contatti su file remoti usando KIO, l'infrastruttura di rete di KDE. Gestisce file in standard VCard e altri formati in base ai plugin disponibili. Comment[ja]=KDE ã®ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ãƒ•ãƒ¬ãƒ¼ãƒ ãƒ¯ãƒ¼ã‚¯ KIO を使ã£ã¦ã€ãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚¡ã‚¤ãƒ«ã«ä¿å­˜ã•ã‚Œã¦ã„る連絡先ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’æä¾›ã—ã¾ã™ã€‚標準㮠VCard ファイルã¨ã€åˆ©ç”¨å¯èƒ½ãªãƒ—ラグインã«å¿œã˜ãŸãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã‚’サãƒãƒ¼ãƒˆã—ã¾ã™ã€‚ Comment[km]=ផ្ដល់​កា​រចូលដំណើរការ​ទៅកាន់​ទំនាក់ទំនង​ក្នុង​​ឯកសារ​ពី​ចម្ងាយ ដោយ​ប្រើ KIO ​គ្រោងការណáŸâ€‹áž”ណ្ដាញ​របស់ KDE ។ គាំទ្រ​ឯកសារ VCard ស្ážáž„់ដារ និង​ទ្រង់ទ្រាយ​ផ្សáŸáž„ៗ​ទៀហដោយ​អាស្រáŸáž™â€‹áž›áž¾â€‹áž€áž˜áŸ’មវិធី​ជំនួយ​ដែល​លមាន ។ Comment[lv]=NodroÅ¡ina piekļuvi kontaktiem attÄlinÄtos failus, izmantojot KDE tÄ«kloÅ¡anas ietvaru KIO. Atbalsta standarta VCard failus un citus formÄtus, atkarÄ«bÄ no pieejamajiem spraudņiem. Comment[nb]=Gir tilgang til kontakter i nettverksfiler, ved bruk av KDEs rammeverk KIO for nettverk. Støtter standard vCard og andre formater avhengig av tilgjengelige programtillegg. Comment[nds]=Stellt Togriep op Kontakten binnen feern Dateien praat, bruukt KDE sien Nettwark-Rahmenwark KIO. Ãœnnerstütt VCard-Dateien un anner Formaten na de verföögboren Modulen. Comment[nn]=Gjev tilgang til kontaktar i nettverksfiler ved Ã¥ bruka KIO, nettverksrammeverket frÃ¥ KDE. Standard vCard-filer er støtta, i tillegg til andre format, avhengig av kva programtillegg som er i bruk. Comment[pt]=Oferece o acesso aos contactos em ficheiros remotos, disponíveis através de um KIO da plataforma de rede do KDE. Suporta os ficheiros VCard normais, assim como outros formatos, dependendo da disponibilidade dos 'plugins'. Comment[pt_BR]=Fornece acesso aos contatos em arquivos remotos usando o KIO framework da rede do KDE. O suporte à arquivos VCard padrão e outros formatos dependem da disponibilidade de plug-ins. +Comment[sl]=Nudi dostop do stikov v oddaljenih datotekah z uporabo KDE-jevega ogrodja KIO. Podpira obiÄajne datoteke VCard in druge formate, odvisno od dosegljivosti vstavkov. Comment[sr]=Пружа приÑтуп контактима у удаљеним фајловима преко КДЕ‑овог мрежног радног оквира К‑У/И. Подржава Ñтандардне в‑кард фајлове и друге формате, према раÑположивим прикључцима. Comment[sr@latin]=Pruža pristup kontaktima u udaljenim fajlovima preko KDE‑ovog mrežnog radnog okvira K‑U/I. Podržava standardne vCard fajlove i druge formate, prema raspoloživim prikljuÄcima. Comment[sv]=Ger tillgÃ¥ng till kontakter i fjärrfiler med användning av KDE:s I/O-ramverk. Stöder vCard-standardfiler och andra format, beroende pÃ¥ tillgängliga insticksprogram. Comment[tr]=KDE'nin aÄŸ iskeleti KIO'yu kullanarak uzak dosyalardaki kiÅŸilere eriÅŸim saÄŸlar. Mevcut eklentilere baÄŸlı olarak standart VCard dosyalarını ve diÄŸer dosya biçimleri destekler. Comment[uk]=Ðадає доÑтуп до контактів, Ñкі зберігаютьÑÑ Ñƒ віддалених файлах, за допомогою мережевих заÑобів KIO KDE. Підтримує Ñтандартні файли VCard та файли у інших форматах, залежно від наÑвноÑÑ‚Ñ– відповідних додатків. Comment[x-test]=xxProvides access to contacts in remote files using KDE's network framework KIO. Supports standard VCard files and other formats depending on available plugins.xx Comment[zh_CN]=通过 KDE 的网络é€æ˜Žæ¡†æž¶ KIO,æ供对被存储在远程文件中的è”系人的访问支æŒã€‚支æŒæ ‡å‡† VCard 文件和其它æ’件所å…许的格å¼ã€‚ Comment[zh_TW]=æ供存å–使用 KDE 網路架構 KIO 儲存在é ç«¯æª”案中的è¯çµ¡äººã€‚支æ´æ¨™æº–çš„ VCard 檔,以åŠå¤–掛程å¼æ‰€æ供的其它格å¼æª”案。 X-KDE-Library=kabc_net Type=Service X-KDE-ServiceTypes=KResources/Plugin X-KDE-ResourceFamily=contact X-KDE-ResourceType=net diff --git a/kcal/attendee.cpp b/kcal/attendee.cpp index d341fa710..34a20ad83 100644 --- a/kcal/attendee.cpp +++ b/kcal/attendee.cpp @@ -1,246 +1,249 @@ /* This file is part of the kcal library. Copyright (c) 2001 Cornelius Schumacher 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 Attendee class. @brief Represents information related to an attendee of an Calendar Incidence. @author Cornelius Schumacher \ */ #include "attendee.h" #include #include #include using namespace KCal; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KCal::Attendee::Private { public: bool mRSVP; Role mRole; PartStat mStatus; QString mUid; QString mDelegate; QString mDelegator; }; //@endcond Attendee::Attendee( const QString &name, const QString &email, bool rsvp, Attendee::PartStat status, Attendee::Role role, const QString &uid ) : d( new Attendee::Private ) { setName( name ); setEmail( email ); d->mRSVP = rsvp; d->mStatus = status; d->mRole = role; d->mUid = uid; } Attendee::Attendee( const Attendee &attendee ) : Person( attendee ), d( new Attendee::Private( *attendee.d ) ) { } Attendee::~Attendee() { delete d; } bool KCal::Attendee::operator==( const Attendee &attendee ) { return ( Person & )*this == ( const Person & )attendee && d->mRSVP == attendee.d->mRSVP && d->mRole == attendee.d->mRole && d->mStatus == attendee.d->mStatus && d->mUid == attendee.d->mUid && d->mDelegate == attendee.d->mDelegate && d->mDelegator == attendee.d->mDelegator; } Attendee &KCal::Attendee::operator=( const Attendee &attendee ) { // check for self assignment if ( &attendee == this ) { return *this; } *d = *attendee.d; setName( attendee.name() ); setEmail( attendee.email() ); return *this; } void Attendee::setRSVP( bool r ) { d->mRSVP = r; } bool Attendee::RSVP() const { return d->mRSVP; } void Attendee::setStatus( Attendee::PartStat status ) { d->mStatus = status; } Attendee::PartStat Attendee::status() const { return d->mStatus; } QString Attendee::statusStr() const { return statusName( d->mStatus ); } QString Attendee::statusName( Attendee::PartStat status ) { switch ( status ) { default: case NeedsAction: return i18nc( "@item event, to-do or journal needs action", "Needs Action" ); break; case Accepted: return i18nc( "@item event, to-do or journal accepted", "Accepted" ); break; case Declined: return i18nc( "@item event, to-do or journal declined", "Declined" ); break; case Tentative: return i18nc( "@item event or to-do tentatively accepted", "Tentative" ); break; case Delegated: return i18nc( "@item event or to-do delegated", "Delegated" ); break; case Completed: return i18nc( "@item to-do completed", "Completed" ); break; case InProcess: return i18nc( "@item to-do in process of being completed", "In Process" ); break; + case None: + return i18nc( "@item event or to-do status unknown", "Unknown" ); + break; } } QStringList Attendee::statusList() { QStringList list; list << statusName( NeedsAction ); list << statusName( Accepted ); list << statusName( Declined ); list << statusName( Tentative ); list << statusName( Delegated ); list << statusName( Completed ); list << statusName( InProcess ); return list; } void Attendee::setRole( Attendee::Role role ) { d->mRole = role; } Attendee::Role Attendee::role() const { return d->mRole; } QString Attendee::roleStr() const { return roleName( d->mRole ); } void Attendee::setUid( const QString &uid ) { d->mUid = uid; } QString Attendee::uid() const { return d->mUid; } QString Attendee::roleName( Attendee::Role role ) { switch ( role ) { case Chair: return i18nc( "@item chairperson", "Chair" ); break; default: case ReqParticipant: return i18nc( "@item participation is required", "Participant" ); break; case OptParticipant: return i18nc( "@item participation is optional", "Optional Participant" ); break; case NonParticipant: return i18nc( "@item non-participant copied for information", "Observer" ); break; } } QStringList Attendee::roleList() { QStringList list; list << roleName( ReqParticipant ); list << roleName( OptParticipant ); list << roleName( NonParticipant ); list << roleName( Chair ); return list; } void Attendee::setDelegate( const QString &delegate ) { d->mDelegate = delegate; } QString Attendee::delegate() const { return d->mDelegate; } void Attendee::setDelegator( const QString &delegator ) { d->mDelegator = delegator; } QString Attendee::delegator() const { return d->mDelegator; } diff --git a/kcal/htmlexport.cpp b/kcal/htmlexport.cpp index db7f1fe79..e1d6d486a 100644 --- a/kcal/htmlexport.cpp +++ b/kcal/htmlexport.cpp @@ -1,781 +1,782 @@ /* This file is part of the kcal library. Copyright (c) 2000,2001 Cornelius Schumacher Copyright (C) 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. */ #include "htmlexport.h" #include "htmlexportsettings.h" #include "incidenceformatter.h" #include "calendar.h" #include "event.h" #include "todo.h" #ifndef KORG_NOKABC #include "kabc/stdaddressbook.h" #endif #include #include #include #include #include #include #include #include #include #include using namespace KCal; static QString cleanChars( const QString &txt ); //@cond PRIVATE class KCal::HtmlExport::Private { public: Private( Calendar *calendar, HTMLExportSettings *settings ) : mCalendar( calendar ), mSettings( settings ) {} Calendar *mCalendar; HTMLExportSettings *mSettings; QMap mHolidayMap; }; //@endcond HtmlExport::HtmlExport( Calendar *calendar, HTMLExportSettings *settings ) : d( new Private( calendar, settings ) ) { } HtmlExport::~HtmlExport() { delete d; } bool HtmlExport::save( const QString &fileName ) { QString fn( fileName ); if ( fn.isEmpty() && d->mSettings ) { fn = d->mSettings->outputFile(); } if ( !d->mSettings || fn.isEmpty() ) { return false; } QFile f( fileName ); if ( !f.open( QIODevice::WriteOnly ) ) { return false; } QTextStream ts( &f ); bool success = save( &ts ); f.close(); return success; } bool HtmlExport::save( QTextStream *ts ) { if ( !d->mSettings ) { return false; } ts->setCodec( "UTF-8" ); // Write HTML header *ts << ""<" << endl; *ts << "" << endl; *ts << " "<" << endl; if ( !d->mSettings->pageTitle().isEmpty() ) { - *ts << " " << d->mSettings->pageTitle() << ""<" << d->mSettings->pageTitle() << "" << endl; } - *ts << " "<"<" << endl; + *ts << "" << endl; // FIXME: Write header // (Heading, Calendar-Owner, Calendar-Date, ...) if ( d->mSettings->eventView() || d->mSettings->monthView() || d->mSettings->weekView() ) { if ( !d->mSettings->eventTitle().isEmpty() ) { - *ts << "

" << d->mSettings->eventTitle() << "

"<" << d->mSettings->eventTitle() << "" << endl; } // Write Week View if ( d->mSettings->weekView() ) { createWeekView( ts ); } // Write Month View if ( d->mSettings->monthView() ) { createMonthView( ts ); } // Write Event List if ( d->mSettings->eventView() ) { createEventList( ts ); } } // Write Todo List if ( d->mSettings->todoView() ) { if ( !d->mSettings->todoListTitle().isEmpty() ) { - *ts << "

" << d->mSettings->todoListTitle() << "

"<" << d->mSettings->todoListTitle() << "" << endl; } createTodoList( ts ); } // Write Journals if ( d->mSettings->journalView() ) { if ( !d->mSettings->journalTitle().isEmpty() ) { - *ts << "

" << d->mSettings->journalTitle() << "

"<" << d->mSettings->journalTitle() << "" << endl; } createJournalView( ts ); } // Write Free/Busy if ( d->mSettings->freeBusyView() ) { if ( !d->mSettings->freeBusyTitle().isEmpty() ) { - *ts << "

" << d->mSettings->freeBusyTitle() << "

"<" << d->mSettings->freeBusyTitle() << "" << endl; } createFreeBusyView( ts ); } createFooter( ts ); // Write HTML trailer - *ts << ""<" << endl; return true; } void HtmlExport::createMonthView( QTextStream *ts ) { QDate start = fromDate(); start.setYMD( start.year(), start.month(), 1 ); // go back to first day in month QDate end( start.year(), start.month(), start.daysInMonth() ); int startmonth = start.month(); int startyear = start.year(); while ( start < toDate() ) { // Write header QDate hDate( start.year(), start.month(), 1 ); QString hMon = hDate.toString( "MMMM" ); QString hYear = hDate.toString( "yyyy" ); *ts << "

" << i18nc( "@title month and year", "%1 %2", hMon, hYear ) - << "

"<" << endl; if ( KGlobal::locale()->weekStartDay() == 1 ) { start = start.addDays( 1 - start.dayOfWeek() ); } else { if ( start.dayOfWeek() != 7 ) { start = start.addDays( -start.dayOfWeek() ); } } - *ts << ""<" << endl; // Write table header *ts << " "; for ( int i=0; i < 7; ++i ) { *ts << ""; } - *ts << ""<" << endl; // Write days while ( start <= end ) { - *ts << " "<" << endl; for ( int i=0; i < 7; ++i ) { *ts << " "<
" << KGlobal::locale()->calendar()->weekDayName( start.addDays(i) ) << "
"; *ts << "
mHolidayMap.contains( start ) || start.dayOfWeek() == 7 ) { *ts << "class=\"dateholiday\""; } else { *ts << "class=\"date\""; } *ts << ">" << QString::number( start.day() ); if ( d->mHolidayMap.contains( start ) ) { *ts << " " << d->mHolidayMap[start] << ""; } *ts << "
"; Event::List events = d->mCalendar->events( start, d->mCalendar->timeSpec(), EventSortStartDate, SortDirectionAscending ); if ( events.count() ) { *ts << ""; Event::List::ConstIterator it; for ( it = events.constBegin(); it != events.constEnd(); ++it ) { if ( checkSecrecy( *it ) ) { createEvent( ts, *it, start, false ); } } *ts << "
"; } else { *ts << " "; } - *ts << "
" << endl; start = start.addDays( 1 ); } - *ts << " "<" << endl; } - *ts << ""<" << endl; startmonth += 1; if ( startmonth > 12 ) { startyear += 1; startmonth = 1; } start.setYMD( startyear, startmonth, 1 ); end.setYMD( start.year(), start.month(), start.daysInMonth() ); } } void HtmlExport::createEventList( QTextStream *ts ) { int columns = 3; - *ts << ""<"<" << endl; + *ts << " " << endl; *ts << " "<" << endl; *ts << " "<" << endl; *ts << " "<" << endl; if ( d->mSettings->eventLocation() ) { *ts << " "<" << endl; ++columns; } if ( d->mSettings->eventCategories() ) { *ts << " "<" << endl; ++columns; } if ( d->mSettings->eventAttendees() ) { *ts << " "<" << endl; ++columns; } - *ts << " "<" << endl; for ( QDate dt = fromDate(); dt <= toDate(); dt = dt.addDays(1) ) { kDebug() << "Getting events for" << dt.toString(); Event::List events = d->mCalendar->events( dt, d->mCalendar->timeSpec(), EventSortStartDate, SortDirectionAscending ); if ( events.count() ) { *ts << " "<" << endl; Event::List::ConstIterator it; for ( it = events.constBegin(); it != events.constEnd(); ++it ) { if ( checkSecrecy( *it ) ) { createEvent( ts, *it, dt ); } } } } - *ts << "
" << i18nc( "@title:column event start time", - "Start Time" ) << "" << i18nc( "@title:column event end time", - "End Time" ) << "" << i18nc( "@title:column event description", - "Event" ) << "" << i18nc( "@title:column event locatin", - "Location" ) << "" << i18nc( "@title:column event categories", - "Categories" ) << "" << i18nc( "@title:column event attendees", - "Attendees" ) << "
" << KGlobal::locale()->formatDate( dt ) - << "
"<" << endl; } void HtmlExport::createEvent ( QTextStream *ts, Event *event, QDate date, bool withDescription ) { kDebug() << event->summary(); - *ts << " "<" << endl; if ( !event->allDay() ) { if ( event->isMultiDay( d->mCalendar->timeSpec() ) && ( event->dtStart().date() != date ) ) { - *ts << "  "< " << endl; } else { *ts << " " << IncidenceFormatter::timeToString( event->dtStart(), true, d->mCalendar->timeSpec() ) - << ""<" << endl; } if ( event->isMultiDay( d->mCalendar->timeSpec() ) && ( event->dtEnd().date() != date ) ) { - *ts << "  "< " << endl; } else { *ts << " " << IncidenceFormatter::timeToString( event->dtEnd(), true, d->mCalendar->timeSpec() ) - << ""<" << endl; } } else { - *ts << "   "<  " << endl; } - *ts << " "<" << cleanChars( event->summary() ) << ""<" << endl; + *ts << " " << cleanChars( event->summary() ) << "" << endl; if ( withDescription && !event->description().isEmpty() ) { - *ts << "

" << breakString( cleanChars( event->description() ) ) << "

"<" << breakString( cleanChars( event->description() ) ) << "

" << endl; } - *ts << " "<" << endl; if ( d->mSettings->eventLocation() ) { - *ts << " "<" << endl; formatLocation( ts, event ); - *ts << " "<" << endl; } if ( d->mSettings->eventCategories() ) { - *ts << " "<" << endl; formatCategories( ts, event ); - *ts << " "<" << endl; } if ( d->mSettings->eventAttendees() ) { - *ts << " "<" << endl; formatAttendees( ts, event ); - *ts << " "<" << endl; } - *ts << " "<" << endl; } void HtmlExport::createTodoList ( QTextStream *ts ) { Todo::List rawTodoList = d->mCalendar->todos(); int index = 0; while ( index < rawTodoList.count() ) { Todo *ev = rawTodoList[ index ]; Todo *subev = ev; if ( ev->relatedTo() ) { if ( ev->relatedTo()->type() == "Todo" ) { if ( !rawTodoList.contains( static_cast( ev->relatedTo() ) ) ) { rawTodoList.append( static_cast( ev->relatedTo() ) ); } } } index = rawTodoList.indexOf( subev ); ++index; } // FIXME: Sort list by priorities. This is brute force and should be // replaced by a real sorting algorithm. Todo::List todoList; Todo::List::ConstIterator it; for ( int i = 1; i <= 9; ++i ) { for ( it = rawTodoList.constBegin(); it != rawTodoList.constEnd(); ++it ) { if ( (*it)->priority() == i && checkSecrecy( *it ) ) { todoList.append( *it ); } } } for ( it = rawTodoList.constBegin(); it != rawTodoList.constEnd(); ++it ) { if ( (*it)->priority() == 0 && checkSecrecy( *it ) ) { todoList.append( *it ); } } int columns = 3; - *ts << ""<"<" << i18nc( "@title:column", "To-do" ) << ""<" << i18nc( "@title:column to-do priority", "Priority" ) << ""<" << i18nc( "@title:column to-do percent completed", "Completed" ) << ""<" << endl; + *ts << " " << endl; + *ts << " " << endl; + *ts << " " << endl; + *ts << " " << endl; if ( d->mSettings->taskDueDate() ) { - *ts << " "<" << i18nc( "@title:column to-do due date", "Due Date" ) << "" << endl; ++columns; } if ( d->mSettings->taskLocation() ) { - *ts << " "<" << i18nc( "@title:column to-do location", "Location" ) << "" << endl; ++columns; } if ( d->mSettings->taskCategories() ) { - *ts << " "<" << i18nc( "@title:column to-do categories", "Categories" ) << "" << endl; ++columns; } if ( d->mSettings->taskAttendees() ) { - *ts << " "<" << i18nc( "@title:column to-do attendees", "Attendees" ) << "" << endl; ++columns; } - *ts << " "<" << endl; // Create top-level list. for ( it = todoList.constBegin(); it != todoList.constEnd(); ++it ) { if ( !(*it)->relatedTo() ) { createTodo( ts, *it ); } } // Create sub-level lists for ( it = todoList.constBegin(); it != todoList.constEnd(); ++it ) { Incidence::List relations = (*it)->relations(); if ( relations.count() ) { // Generate sub-to-do list - *ts << " "<" << endl; *ts << " "<"<" << endl; + *ts << " " << endl; Todo::List sortedList; // FIXME: Sort list by priorities. This is brute force and should be // replaced by a real sorting algorithm. for ( int i = 1; i <= 9; ++i ) { Incidence::List::ConstIterator it2; for ( it2 = relations.constBegin(); it2 != relations.constEnd(); ++it2 ) { Todo *ev3 = dynamic_cast( *it2 ); if ( ev3 && ev3->priority() == i ) { sortedList.append( ev3 ); } } } Incidence::List::ConstIterator it2; for ( it2 = relations.constBegin(); it2 != relations.constEnd(); ++it2 ) { Todo *ev3 = dynamic_cast( *it2 ); if ( ev3 && ev3->priority() == 0 ) { sortedList.append( ev3 ); } } Todo::List::ConstIterator it3; for ( it3 = sortedList.constBegin(); it3 != sortedList.constEnd(); ++it3 ) { createTodo( ts, *it3 ); } } } - *ts << "
" << i18nc( "@title:column", "To-do" ) << "" << i18nc( "@title:column to-do priority", "Priority" ) << "" << i18nc( "@title:column to-do percent completed", + "Completed" ) << "" << i18nc( "@title:column to-do due date", "Due Date" ) << "" << i18nc( "@title:column to-do location", "Location" ) << "" << i18nc( "@title:column to-do categories", "Categories" ) << "" << i18nc( "@title:column to-do attendees", "Attendees" ) << "
uid() << "\">" << i18nc( "@title:column sub-to-dos of the parent to-do", "Sub-To-dos of: " ) << "uid() << "\">" << cleanChars( (*it)->summary() ) - << "
"<" << endl; } void HtmlExport::createTodo( QTextStream *ts, Todo *todo ) { kDebug(); bool completed = todo->isCompleted(); Incidence::List relations = todo->relations(); - *ts << ""<" << endl; *ts << " "<uid() << "\">"<" << cleanChars( todo->summary() ) << ""<" << endl; + *ts << " uid() << "\">" << endl; + *ts << " " << cleanChars( todo->summary() ) << "" << endl; if ( !todo->description().isEmpty() ) { - *ts << "

" << breakString( cleanChars( todo->description() ) ) << "

"<" << breakString( cleanChars( todo->description() ) ) << "

" << endl; } if ( relations.count() ) { *ts << " "<" << endl; } - *ts << " "<" << endl; *ts << " "<priority() <"<" << endl; + *ts << " " << todo->priority() << endl; + *ts << " " << endl; *ts << " "<" << endl; *ts << " " << i18nc( "@info/plain to-do percent complete", - "%1 %", todo->percentComplete() ) <"<percentComplete() ) << endl; + *ts << " " << endl; if ( d->mSettings->taskDueDate() ) { *ts << " "<" << endl; if ( todo->hasDueDate() ) { - *ts << " " << IncidenceFormatter::dateToString(todo->dtDue(true)) <dtDue( true ) ) << endl; } else { - *ts << "  "<"<" << endl; } if ( d->mSettings->taskLocation() ) { *ts << " "<" << endl; formatLocation( ts, todo ); - *ts << " "<" << endl; } if ( d->mSettings->taskCategories() ) { *ts << " "<" << endl; formatCategories( ts, todo ); - *ts << " "<" << endl; } if ( d->mSettings->taskAttendees() ) { *ts << " "<" << endl; formatAttendees( ts, todo ); - *ts << " "<" << endl; } - *ts << ""<" << endl; } void HtmlExport::createWeekView( QTextStream *ts ) { Q_UNUSED( ts ); // FIXME: Implement this! } void HtmlExport::createJournalView( QTextStream *ts ) { Q_UNUSED( ts ); // Journal::List rawJournalList = d->mCalendar->journals(); // FIXME: Implement this! } void HtmlExport::createFreeBusyView( QTextStream *ts ) { Q_UNUSED( ts ); // FIXME: Implement this! } bool HtmlExport::checkSecrecy( Incidence *incidence ) { int secrecy = incidence->secrecy(); if ( secrecy == Incidence::SecrecyPublic ) { return true; } if ( secrecy == Incidence::SecrecyPrivate && !d->mSettings->excludePrivate() ) { return true; } if ( secrecy == Incidence::SecrecyConfidential && !d->mSettings->excludeConfidential() ) { return true; } return false; } void HtmlExport::formatLocation( QTextStream *ts, Incidence *incidence ) { if ( !incidence->location().isEmpty() ) { - *ts << " " << cleanChars( incidence->location() ) <location() ) << endl; } else { - *ts << "  "<categoriesStr().isEmpty() ) { - *ts << " " << cleanChars( incidence->categoriesStr() ) <categoriesStr() ) << endl; } else { - *ts << "  "<attendees(); if ( attendees.count() ) { *ts << ""; #ifndef KORG_NOKABC KABC::AddressBook *add_book = KABC::StdAddressBook::self( true ); KABC::Addressee::List addressList; addressList = add_book->findByEmail( incidence->organizer().email() ); if ( !addressList.isEmpty() ) { KABC::Addressee o = addressList.first(); if ( !o.isEmpty() && addressList.size() < 2 ) { *ts << "organizer().email() << "\">"; - *ts << cleanChars( o.formattedName() ) << ""<" << endl; } else { *ts << incidence->organizer().fullName(); } } #else *ts << incidence->organizer().fullName(); #endif *ts << "
"; Attendee::List::ConstIterator it; for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { Attendee *a = *it; if ( !a->email().isEmpty() ) { *ts << "email(); *ts << "\">" << cleanChars( a->name() ) << ""; } else { *ts << " " << cleanChars( a->name() ); } - *ts << "
" <" << endl; } } else { - *ts << "  "<"; } return out; } } void HtmlExport::createFooter( QTextStream *ts ) { // FIXME: Implement this in a translatable way! QString trailer = i18nc( "@info/plain", "This page was created " ); /* bool hasPerson = false; bool hasCredit = false; bool hasCreditURL = false; QString mail, name, credit, creditURL;*/ if ( !d->mSettings->eMail().isEmpty() ) { if ( !d->mSettings->name().isEmpty() ) { trailer += i18nc( "@info/plain page creator email link with name", "by %2", d->mSettings->eMail(), d->mSettings->name() ); } else { trailer += i18nc( "@info/plain page creator email link", "by %2", d->mSettings->eMail(), d->mSettings->eMail() ); } } else { if ( !d->mSettings->name().isEmpty() ) { trailer += i18nc( "@info/plain page creator name only", "by %1 ", d->mSettings->name() ); } } if ( !d->mSettings->creditName().isEmpty() ) { if ( !d->mSettings->creditURL().isEmpty() ) { trailer += i18nc( "@info/plain page credit with name and link", "with %2", d->mSettings->creditURL(), d->mSettings->creditName() ); } else { trailer += i18nc( "@info/plain page credit name only", "with %1", d->mSettings->creditName() ); } } - *ts << "

" << trailer << "

"<" << trailer << "

" << endl; } QString cleanChars( const QString &text ) { QString txt = text; txt = txt.replace( '&', "&" ); txt = txt.replace( '<', "<" ); txt = txt.replace( '>', ">" ); txt = txt.replace( '\"', """ ); txt = txt.replace( QString::fromUtf8( "ä" ), "ä" ); txt = txt.replace( QString::fromUtf8( "Ä" ), "Ä" ); txt = txt.replace( QString::fromUtf8( "ö" ), "ö" ); txt = txt.replace( QString::fromUtf8( "Ö" ), "Ö" ); txt = txt.replace( QString::fromUtf8( "ü" ), "ü" ); txt = txt.replace( QString::fromUtf8( "Ãœ" ), "Ü" ); txt = txt.replace( QString::fromUtf8( "ß" ), "ß" ); txt = txt.replace( QString::fromUtf8( "€" ), "€" ); txt = txt.replace( QString::fromUtf8( "é" ), "é" ); return txt; } QString HtmlExport::styleSheet() const { if ( !d->mSettings->styleSheet().isEmpty() ) { return d->mSettings->styleSheet(); } QString css; if ( QApplication::isRightToLeft() ) { css += " body { background-color:white; color:black; direction: rtl }\n"; css += " td { text-align:center; background-color:#eee }\n"; css += " th { text-align:center; background-color:#228; color:white }\n"; css += " td.sumdone { background-color:#ccc }\n"; css += " td.done { background-color:#ccc }\n"; css += " td.subhead { text-align:center; background-color:#ccf }\n"; css += " td.datehead { text-align:center; background-color:#ccf }\n"; css += " td.space { background-color:white }\n"; css += " td.dateholiday { color:red }\n"; } else { css += " body { background-color:white; color:black }\n"; css += " td { text-align:center; background-color:#eee }\n"; css += " th { text-align:center; background-color:#228; color:white }\n"; css += " td.sum { text-align:left }\n"; css += " td.sumdone { text-align:left; background-color:#ccc }\n"; css += " td.done { background-color:#ccc }\n"; css += " td.subhead { text-align:center; background-color:#ccf }\n"; css += " td.datehead { text-align:center; background-color:#ccf }\n"; css += " td.space { background-color:white }\n"; css += " td.date { text-align:left }\n"; css += " td.dateholiday { text-align:left; color:red }\n"; } return css; } void HtmlExport::addHoliday( const QDate &date, const QString &name ) { if ( d->mHolidayMap[date].isEmpty() ) { d->mHolidayMap[date] = name; } else { d->mHolidayMap[date] = i18nc( "@info/plain holiday by date and name", "%1, %2", d->mHolidayMap[date], name ); } } QDate HtmlExport::fromDate() const { return d->mSettings->dateStart().date(); } QDate HtmlExport::toDate() const { return d->mSettings->dateEnd().date(); } diff --git a/kcal/incidenceformatter.cpp b/kcal/incidenceformatter.cpp index 6ada4927c..f4c423d05 100644 --- a/kcal/incidenceformatter.cpp +++ b/kcal/incidenceformatter.cpp @@ -1,2760 +1,2796 @@ /* This file is part of the kcal library. Copyright (c) 2001 Cornelius Schumacher Copyright (c) 2004 Reinhold Kainhofer Copyright (c) 2005 Rafal Rzepecki Copyright (c) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company 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 provides static functions for formatting Incidences for various purposes. @brief Provides methods to format Incidences in various ways for display purposes. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "incidenceformatter.h" #include "attachment.h" #include "event.h" #include "todo.h" #include "journal.h" #include "calendar.h" #include "calendarlocal.h" #include "icalformat.h" #include "freebusy.h" #include "calendarresources.h" #include "kpimutils/email.h" #include "kabc/phonenumber.h" #include "kabc/vcardconverter.h" #include "kabc/stdaddressbook.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCal; /******************************************************************* * Helper functions for the extensive display (event viewer) *******************************************************************/ //@cond PRIVATE static QString eventViewerAddLink( const QString &ref, const QString &text, bool newline = true ) { QString tmpStr( "" + text + "" ); if ( newline ) { tmpStr += '\n'; } return tmpStr; } static QString eventViewerAddTag( const QString &tag, const QString &text ) { int numLineBreaks = text.count( "\n" ); QString str = '<' + tag + '>'; QString tmpText = text; QString tmpStr = str; if( numLineBreaks >= 0 ) { if ( numLineBreaks > 0 ) { int pos = 0; QString tmp; for ( int i = 0; i <= numLineBreaks; ++i ) { pos = tmpText.indexOf( "\n" ); tmp = tmpText.left( pos ); tmpText = tmpText.right( tmpText.length() - pos - 1 ); tmpStr += tmp + "
"; } } else { tmpStr += tmpText; } } tmpStr += "'; return tmpStr; } static QString eventViewerFormatCategories( Incidence *event ) { QString tmpStr; if ( !event->categoriesStr().isEmpty() ) { if ( event->categories().count() == 1 ) { tmpStr = eventViewerAddTag( "h3", i18n( "Category" ) ); } else { tmpStr = eventViewerAddTag( "h3", i18n( "Categories" ) ); } tmpStr += eventViewerAddTag( "p", event->categoriesStr() ); } return tmpStr; } static QString linkPerson( const QString &email, QString name, QString uid, const QString &iconPath ) { // Make the search, if there is an email address to search on, // and either name or uid is missing if ( !email.isEmpty() && ( name.isEmpty() || uid.isEmpty() ) ) { KABC::AddressBook *add_book = KABC::StdAddressBook::self( true ); KABC::Addressee::List addressList = add_book->findByEmail( email ); KABC::Addressee o = ( !addressList.isEmpty() ? addressList.first() : KABC::Addressee() ); if ( !o.isEmpty() && addressList.size() < 2 ) { if ( name.isEmpty() ) { // No name set, so use the one from the addressbook name = o.formattedName(); } uid = o.uid(); } else { // Email not found in the addressbook. Don't make a link uid.clear(); } } // Show the attendee QString tmpString = "
  • "; if ( !uid.isEmpty() ) { // There is a UID, so make a link to the addressbook if ( name.isEmpty() ) { // Use the email address for text tmpString += eventViewerAddLink( "uid:" + uid, email ); } else { tmpString += eventViewerAddLink( "uid:" + uid, name ); } } else { // No UID, just show some text tmpString += ( name.isEmpty() ? email : name ); } tmpString += '\n'; // Make the mailto link if ( !email.isEmpty() && !iconPath.isNull() ) { KUrl mailto; mailto.setProtocol( "mailto" ); mailto.setPath( email ); tmpString += eventViewerAddLink( mailto.url(), "" ); } tmpString += "
  • \n"; return tmpString; } static QString eventViewerFormatAttendees( Incidence *event ) { QString tmpStr; Attendee::List attendees = event->attendees(); if ( attendees.count() ) { KIconLoader *iconLoader = KIconLoader::global(); const QString iconPath = iconLoader->iconPath( "mail-message-new", KIconLoader::Small ); // Add organizer link tmpStr += eventViewerAddTag( "h4", i18n( "Organizer" ) ); tmpStr += "
      "; tmpStr += linkPerson( event->organizer().email(), event->organizer().name(), QString(), iconPath ); tmpStr += "
    "; // Add attendees links tmpStr += eventViewerAddTag( "h4", i18n( "Attendees" ) ); tmpStr += "
      "; Attendee::List::ConstIterator it; for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { Attendee *a = *it; tmpStr += linkPerson( a->email(), a->name(), a->uid(), iconPath ); if ( !a->delegator().isEmpty() ) { tmpStr += i18n( " (delegated by %1)", a->delegator() ); } if ( !a->delegate().isEmpty() ) { tmpStr += i18n( " (delegated to %1)", a->delegate() ); } } tmpStr += "
    "; } return tmpStr; } static QString eventViewerFormatAttachments( Incidence *i ) { QString tmpStr; Attachment::List as = i->attachments(); if ( as.count() > 0 ) { Attachment::List::ConstIterator it; for ( it = as.constBegin(); it != as.constEnd(); ++it ) { if ( (*it)->isUri() ) { tmpStr += eventViewerAddLink( (*it)->uri(), (*it)->label() ); tmpStr += "
    "; } } } return tmpStr; } /* FIXME:This function depends of kaddressbook. Is necessary a new type of event? */ static QString eventViewerFormatBirthday( Event *event ) { if ( !event ) { return QString(); } if ( event->customProperty( "KABC", "BIRTHDAY" ) != "YES" ) { return QString(); } QString uid_1 = event->customProperty( "KABC", "UID-1" ); QString name_1 = event->customProperty( "KABC", "NAME-1" ); QString email_1= event->customProperty( "KABC", "EMAIL-1" ); KIconLoader *iconLoader = KIconLoader::global(); const QString iconPath = iconLoader->iconPath( "mail-message-new", KIconLoader::Small ); //TODO: add a tart icon QString tmpString = "
      "; tmpString += linkPerson( email_1, name_1, uid_1, iconPath ); if ( event->customProperty( "KABC", "ANNIVERSARY" ) == "YES" ) { QString uid_2 = event->customProperty( "KABC", "UID-2" ); QString name_2 = event->customProperty( "KABC", "NAME-2" ); QString email_2= event->customProperty( "KABC", "EMAIL-2" ); tmpString += linkPerson( email_2, name_2, uid_2, iconPath ); } tmpString += "
    "; return tmpString; } static QString eventViewerFormatHeader( Incidence *incidence ) { QString tmpStr = ""; // show icons KIconLoader *iconLoader = KIconLoader::global(); tmpStr += ""; tmpStr += ""; tmpStr += "
    "; // TODO: KDE5. Make the function QString Incidence::getPixmap() so we don't // need downcasting. if ( incidence->type() == "Todo" ) { tmpStr += "( incidence ); if ( !todo->isCompleted() ) { tmpStr += iconLoader->iconPath( "view-calendar-tasks", KIconLoader::Small ); } else { tmpStr += iconLoader->iconPath( "task-complete", KIconLoader::Small ); } tmpStr += "\">"; } if ( incidence->type() == "Event" ) { tmpStr += "iconPath( "view-calendar-day", KIconLoader::Small ) + "\">"; } if ( incidence->type() == "Journal" ) { tmpStr += "iconPath( "view-pim-journal", KIconLoader::Small ) + "\">"; } if ( incidence->isAlarmEnabled() ) { tmpStr += "iconPath( "preferences-desktop-notification-bell", KIconLoader::Small ) + "\">"; } if ( incidence->recurs() ) { tmpStr += "iconPath( "edit-redo", KIconLoader::Small ) + "\">"; } if ( incidence->isReadOnly() ) { tmpStr += "iconPath( "object-locked", KIconLoader::Small ) + "\">"; } tmpStr += "" + eventViewerAddTag( "h2", incidence->richSummary() ) + "
    "; return tmpStr; } static QString eventViewerFormatEvent( Event *event, KDateTime::Spec spec ) { if ( !event ) { return QString(); } QString tmpStr = eventViewerFormatHeader( event ); tmpStr += ""; if ( !event->location().isEmpty() ) { tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += ""; } tmpStr += ""; if ( event->allDay() ) { if ( event->isMultiDay() ) { tmpStr += ""; tmpStr += ""; } else { tmpStr += ""; tmpStr += ""; } } else { if ( event->isMultiDay() ) { tmpStr += ""; tmpStr += ""; } else { tmpStr += ""; if ( event->hasEndDate() && event->dtStart() != event->dtEnd() ) { tmpStr += ""; } else { tmpStr += ""; } tmpStr += ""; tmpStr += ""; tmpStr += ""; } } tmpStr += ""; if ( event->customProperty( "KABC", "BIRTHDAY" ) == "YES" ) { tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += "
    " + i18n( "Location" ) + "" + event->richLocation() + "
    " + i18n( "Time" ) + "" + i18nc( " - ","%1 - %2", IncidenceFormatter::dateToString( event->dtStart(), true, spec ), IncidenceFormatter::dateToString( event->dtEnd(), true, spec ) ) + "" + i18n( "Date" ) + "" + i18nc( "date as string","%1", IncidenceFormatter::dateToString( event->dtStart(), true, spec ) ) + "" + i18n( "Time" ) + "" + i18nc( " - ","%1 - %2", IncidenceFormatter::dateToString( event->dtStart(), true, spec ), IncidenceFormatter::dateToString( event->dtEnd(), true, spec ) ) + "" + i18n( "Time" ) + "" + i18nc( " - ","%1 - %2", IncidenceFormatter::timeToString( event->dtStart(), true, spec ), IncidenceFormatter::timeToString( event->dtEnd(), true, spec ) ) + "" + IncidenceFormatter::timeToString( event->dtStart(), true, spec ) + "
    " + i18n( "Date" ) + "" + i18nc( "date as string","%1", IncidenceFormatter::dateToString( event->dtStart(), true, spec ) ) + "
    " + i18n( "Birthday" ) + "" + eventViewerFormatBirthday( event ) + "
    "; return tmpStr; } if ( !event->description().isEmpty() ) { tmpStr += ""; tmpStr += ""; tmpStr += "" + eventViewerAddTag( "p", event->richDescription() ) + ""; tmpStr += ""; } if ( event->categories().count() > 0 ) { tmpStr += ""; tmpStr += ""; tmpStr += i18np( "1 category", "%1 categories", event->categories().count() ) + ""; tmpStr += "" + event->categoriesStr() + ""; tmpStr += ""; } if ( event->recurs() ) { KDateTime dt = event->recurrence()->getNextDateTime( KDateTime::currentUtcDateTime() ); tmpStr += ""; tmpStr += "" + i18n( "Next Occurrence" )+ ""; tmpStr += "" + ( dt.isValid() ? KGlobal::locale()->formatDateTime( dt.dateTime(), KLocale::ShortDate ) : i18nc( "no date", "none" ) ) + ""; tmpStr += ""; } tmpStr += ""; tmpStr += eventViewerFormatAttendees( event ); tmpStr += ""; int attachmentCount = event->attachments().count(); if ( attachmentCount > 0 ) { tmpStr += ""; tmpStr += ""; tmpStr += i18np( "1 attachment", "%1 attachments", attachmentCount )+ ""; tmpStr += "" + eventViewerFormatAttachments( event ) + ""; tmpStr += ""; } KDateTime kdt = event->created().toTimeSpec( spec ); tmpStr += ""; tmpStr += "

    " + i18n( "Creation date: %1", KGlobal::locale()->formatDateTime( kdt.dateTime(), KLocale::ShortDate ) ) + ""; return tmpStr; } static QString eventViewerFormatTodo( Todo *todo, KDateTime::Spec spec ) { if ( !todo ) { return QString(); } QString tmpStr = eventViewerFormatHeader( todo ); if ( !todo->location().isEmpty() ) { tmpStr += eventViewerAddTag( "b", i18n(" Location: %1", todo->richLocation() ) ); tmpStr += "
    "; } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { tmpStr += i18n( "Due on: %1", IncidenceFormatter::dateTimeToString( todo->dtDue(), todo->allDay(), true, spec ) ); } if ( !todo->description().isEmpty() ) { tmpStr += eventViewerAddTag( "p", todo->richDescription() ); } tmpStr += eventViewerFormatCategories( todo ); if ( todo->priority() > 0 ) { tmpStr += i18n( "

    Priority: %1

    ", todo->priority() ); } else { tmpStr += i18n( "

    Priority: %1

    ", i18n( "Unspecified" ) ); } tmpStr += i18n( "

    %1 % completed

    ", todo->percentComplete() ); if ( todo->recurs() ) { KDateTime dt = todo->recurrence()->getNextDateTime( KDateTime::currentUtcDateTime() ); tmpStr += eventViewerAddTag( "p", "" + i18n( "This is a recurring to-do. The next occurrence will be on %1.", KGlobal::locale()->formatDateTime( dt.dateTime(), KLocale::ShortDate ) ) + "" ); } tmpStr += eventViewerFormatAttendees( todo ); tmpStr += eventViewerFormatAttachments( todo ); KDateTime kdt = todo->created().toTimeSpec( spec ); tmpStr += "

    " + i18n( "Creation date: %1", KGlobal::locale()->formatDateTime( kdt.dateTime(), KLocale::ShortDate ) ) + ""; return tmpStr; } static QString eventViewerFormatJournal( Journal *journal, KDateTime::Spec spec ) { if ( !journal ) { return QString(); } QString tmpStr; if ( !journal->summary().isEmpty() ) { tmpStr+= eventViewerAddTag( "h2", journal->richSummary() ); } tmpStr += eventViewerAddTag( "h3", i18n( "Journal for %1", IncidenceFormatter::dateToString( journal->dtStart(), false, spec ) ) ); if ( !journal->description().isEmpty() ) { tmpStr += eventViewerAddTag( "p", journal->richDescription() ); } return tmpStr; } static QString eventViewerFormatFreeBusy( FreeBusy *fb, KDateTime::Spec spec ) { Q_UNUSED( spec ); if ( !fb ) { return QString(); } QString tmpStr( eventViewerAddTag( "h2", i18n( "Free/Busy information for %1", fb->organizer().fullName() ) ) ); tmpStr += eventViewerAddTag( "h4", i18n( "Busy times in date range %1 - %2:", KGlobal::locale()->formatDate( fb->dtStart().date(), KLocale::ShortDate ), KGlobal::locale()->formatDate( fb->dtEnd().date(), KLocale::ShortDate ) ) ); QList periods = fb->busyPeriods(); QString text = eventViewerAddTag( "em", eventViewerAddTag( "b", i18nc( "tag for busy periods list", "Busy:" ) ) ); QList::iterator it; for ( it = periods.begin(); it != periods.end(); ++it ) { Period per = *it; if ( per.hasDuration() ) { int dur = per.duration().asSeconds(); QString cont; if ( dur >= 3600 ) { cont += i18ncp( "hours part of duration", "1 hour ", "%1 hours ", dur / 3600 ); dur %= 3600; } if ( dur >= 60 ) { cont += i18ncp( "minutes part duration", "1 minute ", "%1 minutes ", dur / 60 ); dur %= 60; } if ( dur > 0 ) { cont += i18ncp( "seconds part of duration", "1 second", "%1 seconds", dur ); } text += i18nc( "startDate for duration", "%1 for %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), cont ); text += "
    "; } else { if ( per.start().date() == per.end().date() ) { text += i18nc( "date, fromTime - toTime ", "%1, %2 - %3", KGlobal::locale()->formatDate( per.start().date() ), KGlobal::locale()->formatTime( per.start().time() ), KGlobal::locale()->formatTime( per.end().time() ) ); } else { text += i18nc( "fromDateTime - toDateTime", "%1 - %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), KGlobal::locale()->formatDateTime( per.end().dateTime(), KLocale::LongDate ) ); } text += "
    "; } } tmpStr += eventViewerAddTag( "p", text ); return tmpStr; } //@endcond //@cond PRIVATE class KCal::IncidenceFormatter::EventViewerVisitor : public IncidenceBase::Visitor { public: EventViewerVisitor() : mSpec( KDateTime::Spec() ), mResult( "" ) {} bool act( IncidenceBase *incidence, KDateTime::Spec spec=KDateTime::Spec() ) { mSpec = spec; mResult = ""; return incidence->accept( *this ); } QString result() const { return mResult; } protected: bool visit( Event *event ) { mResult = eventViewerFormatEvent( event, mSpec ); return !mResult.isEmpty(); } bool visit( Todo *todo ) { mResult = eventViewerFormatTodo( todo, mSpec ); return !mResult.isEmpty(); } bool visit( Journal *journal ) { mResult = eventViewerFormatJournal( journal, mSpec ); return !mResult.isEmpty(); } bool visit( FreeBusy *fb ) { mResult = eventViewerFormatFreeBusy( fb, mSpec ); return !mResult.isEmpty(); } protected: KDateTime::Spec mSpec; QString mResult; }; //@endcond QString IncidenceFormatter::extensiveDisplayString( IncidenceBase *incidence ) { return extensiveDisplayStr( incidence, KDateTime::Spec() ); } QString IncidenceFormatter::extensiveDisplayStr( IncidenceBase *incidence, KDateTime::Spec spec ) { if ( !incidence ) { return QString(); } EventViewerVisitor v; if ( v.act( incidence, spec ) ) { return v.result(); } else { return QString(); } } /******************************************************************* * Helper functions for the body part formatter of kmail *******************************************************************/ //@cond PRIVATE static QString string2HTML( const QString &str ) { - return Qt::escape( str ); + return Qt::convertFromPlainText( str, Qt::WhiteSpaceNormal ); } static QString cleanHtml( const QString &html ) { QRegExp rx( "]*>(.*)", Qt::CaseInsensitive ); rx.indexIn( html ); QString body = rx.cap( 1 ); return Qt::escape( body.remove( QRegExp( "<[^>]*>" ) ).trimmed() ); } static QString eventStartTimeStr( Event *event ) { QString tmp; if ( !event->allDay() ) { tmp = i18nc( "%1: Start Date, %2: Start Time", "%1 %2", IncidenceFormatter::dateToString( event->dtStart(), true, KSystemTimeZones::local() ), IncidenceFormatter::timeToString( event->dtStart(), true, KSystemTimeZones::local() ) ); } else { tmp = i18nc( "%1: Start Date", "%1 (all day)", IncidenceFormatter::dateToString( event->dtStart(), true, KSystemTimeZones::local() ) ); } return tmp; } static QString eventEndTimeStr( Event *event ) { QString tmp; if ( event->hasEndDate() && event->dtEnd().isValid() ) { if ( !event->allDay() ) { tmp = i18nc( "%1: End Date, %2: End Time", "%1 %2", IncidenceFormatter::dateToString( event->dtEnd(), true, KSystemTimeZones::local() ), IncidenceFormatter::timeToString( event->dtEnd(), true, KSystemTimeZones::local() ) ); } else { tmp = i18nc( "%1: End Date", "%1 (all day)", IncidenceFormatter::dateToString( event->dtEnd(), true, KSystemTimeZones::local() ) ); } } return tmp; } static QString invitationRow( const QString &cell1, const QString &cell2 ) { return "" + cell1 + "" + cell2 + "\n"; } static bool iamOrganizer( Incidence *incidence ) { // Check if I'm the organizer for this incidence if ( !incidence ) { return false; } bool iam = false; KEMailSettings settings; QStringList profiles = settings.profiles(); for ( QStringList::Iterator it=profiles.begin(); it != profiles.end(); ++it ) { settings.setProfile( *it ); if ( settings.getSetting( KEMailSettings::EmailAddress ) == incidence->organizer().email() ) { iam = true; break; } } return iam; } static bool iamAttendee( Attendee *attendee ) { // Check if I'm this attendee bool iam = false; KEMailSettings settings; QStringList profiles = settings.profiles(); for ( QStringList::Iterator it=profiles.begin(); it != profiles.end(); ++it ) { settings.setProfile( *it ); if ( settings.getSetting( KEMailSettings::EmailAddress ) == attendee->email() ) { iam = true; break; } } return iam; } static Attendee *findMyAttendee( Incidence *incidence ) { // Return the attendee for the incidence that is probably me Attendee *attendee = 0; if ( !incidence ) { return attendee; } KEMailSettings settings; QStringList profiles = settings.profiles(); for ( QStringList::Iterator it=profiles.begin(); it != profiles.end(); ++it ) { settings.setProfile( *it ); Attendee::List attendees = incidence->attendees(); Attendee::List::ConstIterator it2; for ( it2 = attendees.constBegin(); it2 != attendees.constEnd(); ++it2 ) { Attendee *a = *it2; if ( settings.getSetting( KEMailSettings::EmailAddress ) == a->email() ) { attendee = a; break; } } } return attendee; } static Attendee *findAttendee( Incidence *incidence, const QString &email ) { // Search for an attendee by email address Attendee *attendee = 0; if ( !incidence ) { return attendee; } Attendee::List attendees = incidence->attendees(); Attendee::List::ConstIterator it; for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { Attendee *a = *it; if ( email == a->email() ) { attendee = a; break; } } return attendee; } static bool rsvpRequested( Incidence *incidence ) { if ( !incidence ) { return false; } //use a heuristic to determine if a response is requested. bool rsvp = true; // better send superfluously than not at all Attendee::List attendees = incidence->attendees(); Attendee::List::ConstIterator it; for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { if ( it == attendees.constBegin() ) { rsvp = (*it)->RSVP(); // use what the first one has } else { if ( (*it)->RSVP() != rsvp ) { rsvp = true; // they differ, default break; } } } return rsvp; } static QString rsvpRequestedStr( bool rsvpRequested ) { if ( rsvpRequested ) { return i18n( "Your response is requested" ); } else { return i18n( "A response is not necessary" ); } } static QString invitationPerson( const QString &email, QString name, QString uid ) { // Make the search, if there is an email address to search on, // and either name or uid is missing if ( !email.isEmpty() && ( name.isEmpty() || uid.isEmpty() ) ) { KABC::AddressBook *add_book = KABC::StdAddressBook::self( true ); KABC::Addressee::List addressList = add_book->findByEmail( email ); if ( !addressList.isEmpty() ) { KABC::Addressee o = addressList.first(); if ( !o.isEmpty() && addressList.size() < 2 ) { if ( name.isEmpty() ) { // No name set, so use the one from the addressbook name = o.formattedName(); } uid = o.uid(); } else { // Email not found in the addressbook. Don't make a link uid.clear(); } } } // Show the attendee QString tmpString; if ( !uid.isEmpty() ) { // There is a UID, so make a link to the addressbook if ( name.isEmpty() ) { // Use the email address for text tmpString += eventViewerAddLink( "uid:" + uid, email ); } else { tmpString += eventViewerAddLink( "uid:" + uid, name ); } } else { // No UID, just show some text tmpString += ( name.isEmpty() ? email : name ); } tmpString += '\n'; // Make the mailto link if ( !email.isEmpty() ) { KCal::Person person( name, email ); KUrl mailto; mailto.setProtocol( "mailto" ); mailto.setPath( person.fullName() ); const QString iconPath = KIconLoader::global()->iconPath( "mail-message-new", KIconLoader::Small ); tmpString += eventViewerAddLink( mailto.url(), "" ); } tmpString += '\n'; return tmpString; } static QString invitationsDetailsIncidence( Incidence *incidence, bool noHtmlMode ) { // if description and comment -> use both // if description, but no comment -> use the desc as the comment (and no desc) // if comment, but no description -> use the comment and no description QString html; QString descr; QStringList comments; if ( incidence->comments().isEmpty() ) { if ( !incidence->description().isEmpty() ) { // use description as comments if ( !incidence->descriptionIsRich() ) { comments << string2HTML( incidence->description() ); } else { comments << incidence->richDescription(); if ( noHtmlMode ) { comments[0] = cleanHtml( comments[0] ); } comments[0] = eventViewerAddTag( "p", comments[0] ); } } //else desc and comments are empty } else { // non-empty comments foreach ( const QString &c, incidence->comments() ) { if ( !c.isEmpty() ) { comments += string2HTML( c ); } } if ( !incidence->description().isEmpty() ) { // use description too if ( !incidence->descriptionIsRich() ) { descr = string2HTML( incidence->description() ); } else { descr = incidence->richDescription(); if ( noHtmlMode ) { descr = cleanHtml( descr ); } descr = eventViewerAddTag( "p", descr ); } } } if( !descr.isEmpty() ) { html += "

    "; html += ""; html += ""; html += ""; html += "
    " + eventViewerAddTag( "u", i18n( "Description:" ) ) + "
    " + descr + "
    "; } if ( !comments.isEmpty() ) { html += "

    "; html += ""; html += ""; html += ""; html += "
    " + eventViewerAddTag( "u", i18n( "Comments:" ) ) + "
    "; if ( comments.count() > 1 ) { html += "
      "; for ( int i=0; i < comments.count(); ++i ) { html += "
    • " + comments[i] + "
    • "; } html += "
    "; } else { html += comments[0]; } html += "
    "; } return html; } static QString invitationDetailsEvent( Event *event, bool noHtmlMode, KDateTime::Spec spec ) { // Invitation details are formatted into an HTML table if ( !event ) { return QString(); } QString sSummary = i18n( "Summary unspecified" ); if ( !event->summary().isEmpty() ) { if ( !event->summaryIsRich() ) { - sSummary = string2HTML( event->summary() ); + sSummary = Qt::escape( event->summary() ); } else { sSummary = event->richSummary(); if ( noHtmlMode ) { sSummary = cleanHtml( sSummary ); } } } QString sLocation = i18n( "Location unspecified" ); if ( !event->location().isEmpty() ) { if ( !event->locationIsRich() ) { - sLocation = string2HTML( event->location() ); + sLocation = Qt::escape( event->location() ); } else { sLocation = event->richLocation(); if ( noHtmlMode ) { sLocation = cleanHtml( sLocation ); } } } QString dir = ( QApplication::isRightToLeft() ? "rtl" : "ltr" ); QString html = QString( "

    \n" ).arg( dir ); html += ""; // Invitation summary & location rows html += invitationRow( i18n( "What:" ), sSummary ); html += invitationRow( i18n( "Where:" ), sLocation ); // If a 1 day event if ( event->dtStart().date() == event->dtEnd().date() ) { html += invitationRow( i18n( "Date:" ), IncidenceFormatter::dateToString( event->dtStart(), false, spec ) ); if ( !event->allDay() ) { html += invitationRow( i18n( "Time:" ), IncidenceFormatter::timeToString( event->dtStart(), false, spec ) + " - " + IncidenceFormatter::timeToString( event->dtEnd(), false, spec ) ); } } else { html += invitationRow( i18nc( "starting date", "From:" ), IncidenceFormatter::dateToString( event->dtStart(), false, spec ) ); if ( !event->allDay() ) { html += invitationRow( i18nc( "starting time", "At:" ), IncidenceFormatter::timeToString( event->dtStart(), false, spec ) ); } if ( event->hasEndDate() ) { html += invitationRow( i18nc( "ending date", "To:" ), IncidenceFormatter::dateToString( event->dtEnd(), false, spec ) ); if ( !event->allDay() ) { html += invitationRow( i18nc( "ending time", "At:" ), IncidenceFormatter::timeToString( event->dtEnd(), false, spec ) ); } } else { html += invitationRow( i18nc( "ending date", "To:" ), i18n( "no end date specified" ) ); } } // Invitation Duration Row if ( !event->allDay() && event->hasEndDate() && event->dtEnd().isValid() ) { QString tmp; QTime sDuration( 0, 0, 0 ), t; int secs = event->dtStart().secsTo( event->dtEnd() ); t = sDuration.addSecs( secs ); if ( t.hour() > 0 ) { tmp += i18np( "1 hour ", "%1 hours ", t.hour() ); } if ( t.minute() > 0 ) { tmp += i18np( "1 minute ", "%1 minutes ", t.minute() ); } html += invitationRow( i18n( "Duration:" ), tmp ); } if ( event->recurs() ) { html += invitationRow( i18n( "Recurrence:" ), IncidenceFormatter::recurrenceString( event ) ); } html += "
    \n"; html += invitationsDetailsIncidence( event, noHtmlMode ); return html; } static QString invitationDetailsTodo( Todo *todo, bool noHtmlMode, KDateTime::Spec spec ) { // To-do details are formatted into an HTML table if ( !todo ) { return QString(); } QString sSummary = i18n( "Summary unspecified" ); if ( !todo->summary().isEmpty() ) { if ( !todo->summaryIsRich() ) { - sSummary = string2HTML( todo->summary() ); + sSummary = Qt::escape( todo->summary() ); } else { sSummary = todo->richSummary(); if ( noHtmlMode ) { sSummary = cleanHtml( sSummary ); } } } QString sLocation = i18n( "Location unspecified" ); if ( !todo->location().isEmpty() ) { if ( !todo->locationIsRich() ) { - sLocation = string2HTML( todo->location() ); + sLocation = Qt::escape( todo->location() ); } else { sLocation = todo->richLocation(); if ( noHtmlMode ) { sLocation = cleanHtml( sLocation ); } } } QString dir = ( QApplication::isRightToLeft() ? "rtl" : "ltr" ); QString html = QString( "
    \n" ).arg( dir ); html += ""; // Invitation summary & location rows html += invitationRow( i18n( "What:" ), sSummary ); html += invitationRow( i18n( "Where:" ), sLocation ); if ( todo->hasStartDate() && todo->dtStart().isValid() ) { html += invitationRow( i18n( "Start Date:" ), IncidenceFormatter::dateToString( todo->dtStart(), false, spec ) ); } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { html += invitationRow( i18n( "Due Date:" ), IncidenceFormatter::dateToString( todo->dtDue(), false, spec ) ); } else { html += invitationRow( i18n( "Due Date:" ), i18nc( "no to-do due date", "None" ) ); } html += "
    \n"; html += invitationsDetailsIncidence( todo, noHtmlMode ); return html; } static QString invitationDetailsJournal( Journal *journal, bool noHtmlMode, KDateTime::Spec spec ) { if ( !journal ) { return QString(); } QString sSummary = i18n( "Summary unspecified" ); QString sDescr = i18n( "Description unspecified" ); if ( ! journal->summary().isEmpty() ) { sSummary = journal->richSummary(); if ( noHtmlMode ) { sSummary = cleanHtml( sSummary ); } } if ( ! journal->description().isEmpty() ) { sDescr = journal->richDescription(); if ( noHtmlMode ) { sDescr = cleanHtml( sDescr ); } } QString html( "\n" ); html += invitationRow( i18n( "Summary:" ), sSummary ); html += invitationRow( i18n( "Date:" ), IncidenceFormatter::dateToString( journal->dtStart(), false, spec ) ); html += invitationRow( i18n( "Description:" ), sDescr ); html += "
    \n"; html += invitationsDetailsIncidence( journal, noHtmlMode ); return html; } static QString invitationDetailsFreeBusy( FreeBusy *fb, bool noHtmlMode, KDateTime::Spec spec ) { Q_UNUSED( noHtmlMode ); if ( !fb ) { return QString(); } QString html( "\n" ); html += invitationRow( i18n( "Person:" ), fb->organizer().fullName() ); html += invitationRow( i18n( "Start date:" ), IncidenceFormatter::dateToString( fb->dtStart(), true, spec ) ); html += invitationRow( i18n( "End date:" ), IncidenceFormatter::dateToString( fb->dtEnd(), true, spec ) ); html += "\n"; html += "\n"; QList periods = fb->busyPeriods(); QList::iterator it; for ( it = periods.begin(); it != periods.end(); ++it ) { Period per = *it; if ( per.hasDuration() ) { int dur = per.duration().asSeconds(); QString cont; if ( dur >= 3600 ) { cont += i18ncp( "hours part of duration", "1 hour ", "%1 hours ", dur / 3600 ); dur %= 3600; } if ( dur >= 60 ) { cont += i18ncp( "minutes part of duration", "1 minute", "%1 minutes ", dur / 60 ); dur %= 60; } if ( dur > 0 ) { cont += i18ncp( "seconds part of duration", "1 second", "%1 seconds", dur ); } html += invitationRow( QString(), i18nc( "startDate for duration", "%1 for %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), cont ) ); } else { QString cont; if ( per.start().date() == per.end().date() ) { cont = i18nc( "date, fromTime - toTime ", "%1, %2 - %3", KGlobal::locale()->formatDate( per.start().date() ), KGlobal::locale()->formatTime( per.start().time() ), KGlobal::locale()->formatTime( per.end().time() ) ); } else { cont = i18nc( "fromDateTime - toDateTime", "%1 - %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), KGlobal::locale()->formatDateTime( per.end().dateTime(), KLocale::LongDate ) ); } html += invitationRow( QString(), cont ); } } html += "

    Busy periods given in this free/busy object:
    \n"; return html; } static QString invitationHeaderEvent( Event *event, ScheduleMessage *msg ) { if ( !msg || !event ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This event has been published" ); case iTIPRequest: if ( event->revision() > 0 ) { return i18n( "This invitation has been updated" ); } if ( iamOrganizer( event ) ) { return i18n( "I sent this invitation" ); } else { if ( !event->organizer().fullName().isEmpty() ) { return i18n( "You received an invitation from %1", event->organizer().fullName() ); } else { return i18n( "You received an invitation" ); } } case iTIPRefresh: return i18n( "This invitation was refreshed" ); case iTIPCancel: return i18n( "This invitation has been canceled" ); case iTIPAdd: return i18n( "Addition to the invitation" ); case iTIPReply: { Attendee::List attendees = event->attendees(); if( attendees.count() == 0 ) { kDebug() << "No attendees in the iCal reply!"; return QString(); } if ( attendees.count() != 1 ) { kDebug() << "Warning: attendeecount in the reply should be 1" << "but is" << attendees.count(); } Attendee *attendee = *attendees.begin(); QString attendeeName = attendee->name(); if ( attendeeName.isEmpty() ) { attendeeName = attendee->email(); } if ( attendeeName.isEmpty() ) { attendeeName = i18n( "Sender" ); } QString delegatorName, dummy; KPIMUtils::extractEmailAddressAndName( attendee->delegator(), dummy, delegatorName ); if ( delegatorName.isEmpty() ) { delegatorName = attendee->delegator(); } switch( attendee->status() ) { case Attendee::NeedsAction: return i18n( "%1 indicates this invitation still needs some action", attendeeName ); case Attendee::Accepted: if ( delegatorName.isEmpty() ) { return i18n( "%1 accepts this invitation", attendeeName ); } else { return i18n( "%1 accepts this invitation on behalf of %2", attendeeName, delegatorName ); } case Attendee::Tentative: if ( delegatorName.isEmpty() ) { return i18n( "%1 tentatively accepts this invitation", attendeeName ); } else { return i18n( "%1 tentatively accepts this invitation on behalf of %2", attendeeName, delegatorName ); } case Attendee::Declined: if ( delegatorName.isEmpty() ) { return i18n( "%1 declines this invitation", attendeeName ); } else { return i18n( "%1 declines this invitation on behalf of %2", attendeeName, delegatorName ); } case Attendee::Delegated: { QString delegate, dummy; KPIMUtils::extractEmailAddressAndName( attendee->delegate(), dummy, delegate ); if ( delegate.isEmpty() ) { delegate = attendee->delegate(); } if ( !delegate.isEmpty() ) { return i18n( "%1 has delegated this invitation to %2", attendeeName, delegate ); } else { return i18n( "%1 has delegated this invitation", attendeeName ); } } case Attendee::Completed: return i18n( "This invitation is now completed" ); case Attendee::InProcess: return i18n( "%1 is still processing the invitation", attendeeName ); default: return i18n( "Unknown response to this invitation" ); } break; } case iTIPCounter: return i18n( "Sender makes this counter proposal" ); case iTIPDeclineCounter: return i18n( "Sender declines the counter proposal" ); case iTIPNoMethod: return i18n( "Error: iMIP message with unknown method: '%1'", msg->method() ); } return QString(); } static QString invitationHeaderTodo( Todo *todo, ScheduleMessage *msg ) { if ( !msg || !todo ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This to-do has been published" ); case iTIPRequest: if ( todo->revision() > 0 ) { return i18n( "This to-do has been updated" ); } else { return i18n( "You have been assigned this to-do" ); } case iTIPRefresh: return i18n( "This to-do was refreshed" ); case iTIPCancel: return i18n( "This to-do was canceled" ); case iTIPAdd: return i18n( "Addition to the to-do" ); case iTIPReply: { Attendee::List attendees = todo->attendees(); if ( attendees.count() == 0 ) { kDebug() << "No attendees in the iCal reply!"; return QString(); } if ( attendees.count() != 1 ) { kDebug() << "Warning: attendeecount in the reply should be 1" << "but is" << attendees.count(); } Attendee *attendee = *attendees.begin(); switch( attendee->status() ) { case Attendee::NeedsAction: return i18n( "Sender indicates this to-do assignment still needs some action" ); case Attendee::Accepted: return i18n( "Sender accepts this to-do" ); case Attendee::Tentative: return i18n( "Sender tentatively accepts this to-do" ); case Attendee::Declined: return i18n( "Sender declines this to-do" ); case Attendee::Delegated: { QString delegate, dummy; KPIMUtils::extractEmailAddressAndName( attendee->delegate(), dummy, delegate ); if ( delegate.isEmpty() ) { delegate = attendee->delegate(); } if ( !delegate.isEmpty() ) { return i18n( "Sender has delegated this request for the to-do to %1", delegate ); } else { return i18n( "Sender has delegated this request for the to-do " ); } } case Attendee::Completed: return i18n( "The request for this to-do is now completed" ); case Attendee::InProcess: return i18n( "Sender is still processing the invitation" ); default: return i18n( "Unknown response to this to-do" ); } break; } case iTIPCounter: return i18n( "Sender makes this counter proposal" ); case iTIPDeclineCounter: return i18n( "Sender declines the counter proposal" ); case iTIPNoMethod: return i18n( "Error: iMIP message with unknown method: '%1'", msg->method() ); } return QString(); } static QString invitationHeaderJournal( Journal *journal, ScheduleMessage *msg ) { // TODO: Several of the methods are not allowed for journals, so remove them. if ( !msg || !journal ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This journal has been published" ); case iTIPRequest: return i18n( "You have been assigned this journal" ); case iTIPRefresh: return i18n( "This journal was refreshed" ); case iTIPCancel: return i18n( "This journal was canceled" ); case iTIPAdd: return i18n( "Addition to the journal" ); case iTIPReply: { Attendee::List attendees = journal->attendees(); if ( attendees.count() == 0 ) { kDebug() << "No attendees in the iCal reply!"; return QString(); } if( attendees.count() != 1 ) { kDebug() << "Warning: attendeecount in the reply should be 1 " << "but is " << attendees.count(); } Attendee *attendee = *attendees.begin(); switch( attendee->status() ) { case Attendee::NeedsAction: return i18n( "Sender indicates this journal assignment still needs some action" ); case Attendee::Accepted: return i18n( "Sender accepts this journal" ); case Attendee::Tentative: return i18n( "Sender tentatively accepts this journal" ); case Attendee::Declined: return i18n( "Sender declines this journal" ); case Attendee::Delegated: return i18n( "Sender has delegated this request for the journal" ); case Attendee::Completed: return i18n( "The request for this journal is now completed" ); case Attendee::InProcess: return i18n( "Sender is still processing the invitation" ); default: return i18n( "Unknown response to this journal" ); } break; } case iTIPCounter: return i18n( "Sender makes this counter proposal" ); case iTIPDeclineCounter: return i18n( "Sender declines the counter proposal" ); case iTIPNoMethod: return i18n( "Error: iMIP message with unknown method: '%1'", msg->method() ); } return QString(); } static QString invitationHeaderFreeBusy( FreeBusy *fb, ScheduleMessage *msg ) { if ( !msg || !fb ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This free/busy list has been published" ); case iTIPRequest: return i18n( "The free/busy list has been requested" ); case iTIPRefresh: return i18n( "This free/busy list was refreshed" ); case iTIPCancel: return i18n( "This free/busy list was canceled" ); case iTIPAdd: return i18n( "Addition to the free/busy list" ); case iTIPNoMethod: default: return i18n( "Error: Free/Busy iMIP message with unknown method: '%1'", msg->method() ); } } //@endcond static QString invitationAttendees( Incidence *incidence ) { QString tmpStr; if ( !incidence ) { return tmpStr; } tmpStr += i18n( "Invitation List" ); int count=0; Attendee::List attendees = incidence->attendees(); if ( !attendees.isEmpty() ) { Attendee::List::ConstIterator it; for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { Attendee *a = *it; if ( !iamAttendee( a ) ) { count++; if ( count == 1 ) { tmpStr += ""; } tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += ""; } } } if ( count ) { tmpStr += "
    "; tmpStr += invitationPerson( a->email(), a->name(), QString() ); if ( !a->delegator().isEmpty() ) { tmpStr += i18n( " (delegated by %1)", a->delegator() ); } if ( !a->delegate().isEmpty() ) { tmpStr += i18n( " (delegated to %1)", a->delegate() ); } tmpStr += "" + a->statusStr() + "
    "; } else { tmpStr += "" + i18nc( "no attendees", "None" ) + ""; } return tmpStr; } //@cond PRIVATE class KCal::IncidenceFormatter::ScheduleMessageVisitor : public IncidenceBase::Visitor { public: ScheduleMessageVisitor() : mMessage(0) { mResult = ""; } bool act( IncidenceBase *incidence, ScheduleMessage *msg ) { mMessage = msg; return incidence->accept( *this ); } QString result() const { return mResult; } protected: QString mResult; ScheduleMessage *mMessage; }; class KCal::IncidenceFormatter::InvitationHeaderVisitor : public IncidenceFormatter::ScheduleMessageVisitor { protected: bool visit( Event *event ) { mResult = invitationHeaderEvent( event, mMessage ); return !mResult.isEmpty(); } bool visit( Todo *todo ) { mResult = invitationHeaderTodo( todo, mMessage ); return !mResult.isEmpty(); } bool visit( Journal *journal ) { mResult = invitationHeaderJournal( journal, mMessage ); return !mResult.isEmpty(); } bool visit( FreeBusy *fb ) { mResult = invitationHeaderFreeBusy( fb, mMessage ); return !mResult.isEmpty(); } }; class KCal::IncidenceFormatter::InvitationBodyVisitor : public IncidenceFormatter::ScheduleMessageVisitor { public: InvitationBodyVisitor( bool noHtmlMode, KDateTime::Spec spec ) : ScheduleMessageVisitor(), mNoHtmlMode( noHtmlMode ), mSpec( spec ) {} protected: bool visit( Event *event ) { mResult = invitationDetailsEvent( event, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } bool visit( Todo *todo ) { mResult = invitationDetailsTodo( todo, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } bool visit( Journal *journal ) { mResult = invitationDetailsJournal( journal, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } bool visit( FreeBusy *fb ) { mResult = invitationDetailsFreeBusy( fb, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } private: bool mNoHtmlMode; KDateTime::Spec mSpec; }; //@endcond QString InvitationFormatterHelper::generateLinkURL( const QString &id ) { return id; } //@cond PRIVATE class IncidenceFormatter::IncidenceCompareVisitor : public IncidenceBase::Visitor { public: IncidenceCompareVisitor() : mExistingIncidence( 0 ) {} bool act( IncidenceBase *incidence, Incidence *existingIncidence ) { if ( !existingIncidence ) { return false; } Incidence *inc = dynamic_cast( incidence ); if ( !inc || !existingIncidence || inc->revision() <= existingIncidence->revision() ) { return false; } mExistingIncidence = existingIncidence; return incidence->accept( *this ); } QString result() const { if ( mChanges.isEmpty() ) { return QString(); } QString html = "
    • "; html += mChanges.join( "
    • " ); html += "
      "; return html; } protected: bool visit( Event *event ) { compareEvents( event, dynamic_cast( mExistingIncidence ) ); compareIncidences( event, mExistingIncidence ); return !mChanges.isEmpty(); } bool visit( Todo *todo ) { compareIncidences( todo, mExistingIncidence ); return !mChanges.isEmpty(); } bool visit( Journal *journal ) { compareIncidences( journal, mExistingIncidence ); return !mChanges.isEmpty(); } bool visit( FreeBusy *fb ) { Q_UNUSED( fb ); return !mChanges.isEmpty(); } private: void compareEvents( Event *newEvent, Event *oldEvent ) { if ( !oldEvent || !newEvent ) { return; } if ( oldEvent->dtStart() != newEvent->dtStart() || oldEvent->allDay() != newEvent->allDay() ) { mChanges += i18n( "The invitation starting time has been changed from %1 to %2", eventStartTimeStr( oldEvent ), eventStartTimeStr( newEvent ) ); } if ( oldEvent->dtEnd() != newEvent->dtEnd() || oldEvent->allDay() != newEvent->allDay() ) { mChanges += i18n( "The invitation ending time has been changed from %1 to %2", eventEndTimeStr( oldEvent ), eventEndTimeStr( newEvent ) ); } } void compareIncidences( Incidence *newInc, Incidence *oldInc ) { if ( !oldInc || !newInc ) { return; } if ( oldInc->summary() != newInc->summary() ) { mChanges += i18n( "The summary has been changed to: \"%1\"", newInc->richSummary() ); } if ( oldInc->location() != newInc->location() ) { mChanges += i18n( "The location has been changed to: \"%1\"", newInc->richLocation() ); } if ( oldInc->description() != newInc->description() ) { mChanges += i18n( "The description has been changed to: \"%1\"", newInc->richDescription() ); } Attendee::List oldAttendees = oldInc->attendees(); Attendee::List newAttendees = newInc->attendees(); for ( Attendee::List::ConstIterator it = newAttendees.constBegin(); it != newAttendees.constEnd(); ++it ) { Attendee *oldAtt = oldInc->attendeeByMail( (*it)->email() ); if ( !oldAtt ) { mChanges += i18n( "Attendee %1 has been added", (*it)->fullName() ); } else { if ( oldAtt->status() != (*it)->status() ) { mChanges += i18n( "The status of attendee %1 has been changed to: %2", (*it)->fullName(), (*it)->statusStr() ); } } } for ( Attendee::List::ConstIterator it = oldAttendees.constBegin(); it != oldAttendees.constEnd(); ++it ) { Attendee *newAtt = newInc->attendeeByMail( (*it)->email() ); if ( !newAtt ) { mChanges += i18n( "Attendee %1 has been removed", (*it)->fullName() ); } } } private: Incidence *mExistingIncidence; QStringList mChanges; }; //@endcond QString InvitationFormatterHelper::makeLink( const QString &id, const QString &text ) { QString res( "%2" ); return res.arg( generateLinkURL( id ) ).arg( text ); return res; } +// Check if the given incidence is likely one that we own instead one from +// a shared calendar (Kolab-specific) +static bool incidenceOwnedByMe( Calendar *calendar, Incidence *incidence ) +{ + CalendarResources *cal = dynamic_cast( calendar ); + if ( !cal || !incidence ) { + return true; + } + ResourceCalendar *res = cal->resource( incidence ); + if ( !res ) { + return true; + } + const QString subRes = res->subresourceIdentifier( incidence ); + if ( !subRes.contains( "/.INBOX.directory/" ) ) { + return false; + } + return true; +} + Calendar *InvitationFormatterHelper::calendar() const { return 0; } static QString formatICalInvitationHelper( QString invitation, Calendar *mCalendar, InvitationFormatterHelper *helper, bool noHtmlMode, KDateTime::Spec spec ) { if ( invitation.isEmpty() ) { return QString(); } ICalFormat format; // parseScheduleMessage takes the tz from the calendar, // no need to set it manually here for the format! ScheduleMessage *msg = format.parseScheduleMessage( mCalendar, invitation ); if( !msg ) { kDebug() << "Failed to parse the scheduling message"; Q_ASSERT( format.exception() ); kDebug() << format.exception()->message(); return QString(); } IncidenceBase *incBase = msg->event(); incBase->shiftTimes( mCalendar->timeSpec(), KDateTime::Spec::LocalZone() ); - // Determine if this incidence is in my calendar + // Determine if this incidence is in my calendar (and owned by me) Incidence *existingIncidence = 0; if ( incBase && helper->calendar() ) { existingIncidence = helper->calendar()->incidence( incBase->uid() ); + if ( !incidenceOwnedByMe( helper->calendar(), existingIncidence ) ) { + existingIncidence = 0; + } if ( !existingIncidence ) { const Incidence::List list = helper->calendar()->incidences(); for ( Incidence::List::ConstIterator it = list.begin(), end = list.end(); it != end; ++it ) { - if ( (*it)->schedulingID() == incBase->uid() ) { + if ( (*it)->schedulingID() == incBase->uid() && + incidenceOwnedByMe( helper->calendar(), *it ) ) { existingIncidence = *it; break; } } } } // First make the text of the message QString html; html += "
      "; IncidenceFormatter::InvitationHeaderVisitor headerVisitor; // The InvitationHeaderVisitor returns false if the incidence is somehow invalid, or not handled if ( !headerVisitor.act( incBase, msg ) ) { return QString(); } html += eventViewerAddTag( "h3", headerVisitor.result() ); IncidenceFormatter::InvitationBodyVisitor bodyVisitor( noHtmlMode, spec ); if ( !bodyVisitor.act( incBase, msg ) ) { return QString(); } html += bodyVisitor.result(); if ( msg->method() == iTIPRequest ) { // ### Scheduler::Publish/Refresh/Add as well? IncidenceFormatter::IncidenceCompareVisitor compareVisitor; if ( compareVisitor.act( incBase, existingIncidence ) ) { html += i18n( "

      The following changes have been made by the organizer:

      " ); html += compareVisitor.result(); } } Incidence *inc = dynamic_cast( incBase ); // determine if I am the organizer for this invitation bool myInc = iamOrganizer( inc ); // determine if the invitation response has already been recorded bool rsvpRec = false; Attendee *ea = 0; if ( !myInc ) { if ( existingIncidence ) { ea = findMyAttendee( existingIncidence ); } if ( ea && ( ea->status() == Attendee::Accepted || ea->status() == Attendee::Declined ) ) { rsvpRec = true; } } // Print if RSVP needed, not-needed, or response already recorded bool rsvpReq = rsvpRequested( inc ); if ( !myInc ) { html += "
      "; html += ""; - if ( rsvpRec && ( inc && inc->revision() == 0 ) ) { - html += i18n( "Your response has already been recorded [%1]", ea->statusStr() ); + if ( rsvpRec ) { + if ( inc && inc->revision() == 0 ) { + html += i18n( "Your response has already been recorded [%1]", + ea->statusStr() ); + } + if ( inc && inc->revision() > 0 ) { + html += i18n( "Your response to the update has already been recorded [%1]", + ea->statusStr() ); + } rsvpReq = false; + } else if ( msg->method() == iTIPCancel ) { + html += i18n( "Declined the invitation" ); + } else if ( msg->method() == iTIPAdd ) { + html += i18n( "Accepted the invitation" ); } else { html += rsvpRequestedStr( rsvpReq ); } html += "
      "; } // Add groupware links html += "

      "; html += ""; const QString tdOpen = ""; switch ( msg->method() ) { case iTIPPublish: case iTIPRequest: case iTIPRefresh: case iTIPAdd: { - if ( inc && inc->revision() > 0 && existingIncidence ) { + if ( !existingIncidence && !rsvpReq ) { if ( inc->type() == "Todo" ) { html += helper->makeLink( "reply", i18n( "[Record invitation into my to-do list]" ) ); } else { html += helper->makeLink( "reply", i18n( "[Record invitation into my calendar]" ) ); } } if ( !myInc ) { if ( rsvpReq ) { // Accept html += tdOpen; html += helper->makeLink( "accept", i18nc( "accept invitation", "Accept" ) ); html += tdClose; // Accept conditionally html += tdOpen; html += helper->makeLink( "accept_conditionally", i18nc( "Accept invitation conditionally", "Accept cond." ) ); html += tdClose; } if ( rsvpReq ) { // Counter proposal html += tdOpen; html += helper->makeLink( "counter", i18nc( "invitation counter proposal", "Counter proposal" ) ); html += tdClose; } if ( rsvpReq ) { // Decline html += tdOpen; html += helper->makeLink( "decline", i18nc( "decline invitation", "Decline" ) ); html += tdClose; } - if ( !rsvpRec || ( inc && inc->revision() > 0 ) ) { + if ( !existingIncidence && !rsvpReq ) { // Delegate html += tdOpen; html += helper->makeLink( "delegate", i18nc( "delegate inviation to another", "Delegate" ) ); html += tdClose; // Forward html += tdOpen; html += helper->makeLink( "forward", i18nc( "forward request to another", "Forward" ) ); html += tdClose; // Check calendar - if ( incBase->type() == "Event" ) { + if ( incBase && incBase->type() == "Event" ) { html += tdOpen; html += helper->makeLink( "check_calendar", i18nc( "look for scheduling conflicts", "Check my calendar" ) ); html += tdClose; } } } break; } case iTIPCancel: // Remove invitation - if ( existingIncidence ) { + if ( inc && existingIncidence ) { html += tdOpen; if ( inc->type() == "Todo" ) { html += helper->makeLink( "cancel", i18n( "Remove invitation from my task list" ) ); } else { html += helper->makeLink( "cancel", i18n( "Remove invitation from my calendar" ) ); } html += tdClose; } break; case iTIPReply: { // Record invitation response Attendee *a = 0; Attendee *ea = 0; if ( inc ) { a = inc->attendees().first(); if ( a && helper->calendar() ) { ea = findAttendee( existingIncidence, a->email() ); } } if ( ea && ( ea->status() != Attendee::NeedsAction ) && ( ea->status() == a->status() ) ) { html += tdOpen; html += eventViewerAddTag( "i", i18n( "The response has already been recorded" ) ); html += tdClose; } else { - if ( inc->type() == "Todo" ) { - html += helper->makeLink( "reply", i18n( "[Record response into my to-do list]" ) ); - } else { - html += helper->makeLink( "reply", i18n( "[Record response into my calendar]" ) ); + if ( inc ) { + if ( inc->type() == "Todo" ) { + html += helper->makeLink( "reply", i18n( "[Record response into my to-do list]" ) ); + } else { + html += helper->makeLink( "reply", i18n( "[Record response into my calendar]" ) ); + } } } break; } case iTIPCounter: // Counter proposal html += tdOpen; html += helper->makeLink( "accept_counter", i18n( "Accept" ) ); html += tdClose; html += tdOpen; html += helper->makeLink( "decline_counter", i18n( "Decline" ) ); html += tdClose; html += tdOpen; html += helper->makeLink( "check_calendar", i18n( "Check my calendar" ) ); html += tdClose; break; case iTIPDeclineCounter: case iTIPNoMethod: break; } // close the groupware table html += "
      "; const QString tdClose = "
      "; // Add the attendee list if I am the organizer if ( myInc && helper->calendar() ) { html += invitationAttendees( helper->calendar()->incidence( inc->uid() ) ); } // close the top-level html += "

      "; return html; } //@endcond QString IncidenceFormatter::formatICalInvitation( QString invitation, Calendar *mCalendar, InvitationFormatterHelper *helper ) { return formatICalInvitationHelper( invitation, mCalendar, helper, false, KSystemTimeZones::local() ); } QString IncidenceFormatter::formatICalInvitationNoHtml( QString invitation, Calendar *mCalendar, InvitationFormatterHelper *helper ) { return formatICalInvitationHelper( invitation, mCalendar, helper, true, KSystemTimeZones::local() ); } /******************************************************************* * Helper functions for the Incidence tooltips *******************************************************************/ //@cond PRIVATE class KCal::IncidenceFormatter::ToolTipVisitor : public IncidenceBase::Visitor { public: ToolTipVisitor() : mRichText( true ), mSpec( KDateTime::Spec() ), mResult( "" ) {} bool act( IncidenceBase *incidence, bool richText=true, KDateTime::Spec spec=KDateTime::Spec() ) { mRichText = richText; mSpec = spec; mResult = ""; return incidence ? incidence->accept( *this ) : false; } QString result() const { return mResult; } protected: bool visit( Event *event ); bool visit( Todo *todo ); bool visit( Journal *journal ); bool visit( FreeBusy *fb ); QString dateRangeText( Event *event ); QString dateRangeText( Todo *todo ); QString dateRangeText( Journal *journal ); QString dateRangeText( FreeBusy *fb ); QString generateToolTip( Incidence *incidence, QString dtRangeText ); protected: bool mRichText; KDateTime::Spec mSpec; QString mResult; }; QString IncidenceFormatter::ToolTipVisitor::dateRangeText( Event *event ) { //FIXME: support mRichText==false QString ret; QString tmp; if ( event->isMultiDay() ) { tmp = IncidenceFormatter::dateToString( event->dtStart(), true, mSpec ); ret += "
      " + i18nc( "Event start", "From: %1", tmp ); tmp = IncidenceFormatter::dateToString( event->dtEnd(), true, mSpec ); ret += "
      " + i18nc( "Event end","To: %1", tmp ); } else { ret += "
      " + i18n( "Date: %1", IncidenceFormatter::dateToString( event->dtStart(), true, mSpec ) ); if ( !event->allDay() ) { const QString dtStartTime = IncidenceFormatter::timeToString( event->dtStart(), true, mSpec ); const QString dtEndTime = IncidenceFormatter::timeToString( event->dtEnd(), true, mSpec ); if ( dtStartTime == dtEndTime ) { // to prevent 'Time: 17:00 - 17:00' tmp = "
      " + i18nc( "time for event", "Time: %1", dtStartTime ); } else { tmp = "
      " + i18nc( "time range for event", "Time: %1 - %2", dtStartTime, dtEndTime ); } ret += tmp; } } return ret.replace( ' ', " " ); } QString IncidenceFormatter::ToolTipVisitor::dateRangeText( Todo *todo ) { //FIXME: support mRichText==false QString ret; if ( todo->hasStartDate() && todo->dtStart().isValid() ) { // No need to add here. This is separated issue and each line // is very visible on its own. On the other hand... Yes, I like it // italics here :) ret += "
      " + i18n( "Start: %1", IncidenceFormatter::dateToString( todo->dtStart( false ), true, mSpec ) ); } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { ret += "
      " + i18n( "Due: %1", IncidenceFormatter::dateTimeToString( todo->dtDue(), todo->allDay(), true, mSpec ) ); } if ( todo->isCompleted() ) { ret += "
      " + i18n( "Completed: %1", todo->completedStr() ); } else { ret += "
      " + i18nc( "percent complete", "%1 % completed", todo->percentComplete() ); } return ret.replace( ' ', " " ); } QString IncidenceFormatter::ToolTipVisitor::dateRangeText( Journal *journal ) { //FIXME: support mRichText==false QString ret; if ( journal->dtStart().isValid() ) { ret += "
      " + i18n( "Date: %1", IncidenceFormatter::dateToString( journal->dtStart(), false, mSpec ) ); } return ret.replace( ' ', " " ); } QString IncidenceFormatter::ToolTipVisitor::dateRangeText( FreeBusy *fb ) { //FIXME: support mRichText==false QString ret; ret = "
      " + i18n( "Period start: %1", KGlobal::locale()->formatDateTime( fb->dtStart().dateTime() ) ); ret += "
      " + i18n( "Period start: %1", KGlobal::locale()->formatDateTime( fb->dtEnd().dateTime() ) ); return ret.replace( ' ', " " ); } bool IncidenceFormatter::ToolTipVisitor::visit( Event *event ) { mResult = generateToolTip( event, dateRangeText( event ) ); return !mResult.isEmpty(); } bool IncidenceFormatter::ToolTipVisitor::visit( Todo *todo ) { mResult = generateToolTip( todo, dateRangeText( todo ) ); return !mResult.isEmpty(); } bool IncidenceFormatter::ToolTipVisitor::visit( Journal *journal ) { mResult = generateToolTip( journal, dateRangeText( journal ) ); return !mResult.isEmpty(); } bool IncidenceFormatter::ToolTipVisitor::visit( FreeBusy *fb ) { //FIXME: support mRichText==false mResult = "" + i18n( "Free/Busy information for %1", fb->organizer().fullName() ) + ""; mResult += dateRangeText( fb ); mResult += ""; return !mResult.isEmpty(); } QString IncidenceFormatter::ToolTipVisitor::generateToolTip( Incidence *incidence, QString dtRangeText ) { //FIXME: support mRichText==false if ( !incidence ) { return QString(); } QString tmp = ""+ incidence->richSummary() + ""; tmp += dtRangeText; if ( !incidence->location().isEmpty() ) { // Put Location: in italics tmp += "
      " + i18n( "Location: %1", incidence->richLocation() ); } if ( !incidence->description().isEmpty() ) { QString desc( incidence->description() ); if ( !incidence->descriptionIsRich() ) { if ( desc.length() > 120 ) { desc = desc.left( 120 ) + "..."; } desc = Qt::escape( desc ).replace( '\n', "
      " ); } else { // TODO: truncate the description when it's rich text } tmp += "
      ----------
      " + i18n( "Description:" ) + "
      " + desc; } tmp += "
      "; return tmp; } //@endcond QString IncidenceFormatter::toolTipString( IncidenceBase *incidence, bool richText ) { return toolTipStr( incidence, richText, KDateTime::Spec() ); } QString IncidenceFormatter::toolTipStr( IncidenceBase *incidence, bool richText, KDateTime::Spec spec ) { ToolTipVisitor v; if ( v.act( incidence, richText, spec ) ) { return v.result(); } else { return QString(); } } /******************************************************************* * Helper functions for the Incidence tooltips *******************************************************************/ //@cond PRIVATE static QString mailBodyIncidence( Incidence *incidence ) { QString body; if ( !incidence->summary().isEmpty() ) { body += i18n( "Summary: %1\n", incidence->richSummary() ); } if ( !incidence->organizer().isEmpty() ) { body += i18n( "Organizer: %1\n", incidence->organizer().fullName() ); } if ( !incidence->location().isEmpty() ) { body += i18n( "Location: %1\n", incidence->richLocation() ); } return body; } //@endcond //@cond PRIVATE class KCal::IncidenceFormatter::MailBodyVisitor : public IncidenceBase::Visitor { public: MailBodyVisitor() : mSpec( KDateTime::Spec() ), mResult( "" ) {} bool act( IncidenceBase *incidence, KDateTime::Spec spec=KDateTime::Spec() ) { mSpec = spec; mResult = ""; return incidence ? incidence->accept( *this ) : false; } QString result() const { return mResult; } protected: bool visit( Event *event ); bool visit( Todo *todo ); bool visit( Journal *journal ); bool visit( FreeBusy * ) { mResult = i18n( "This is a Free Busy Object" ); return !mResult.isEmpty(); } protected: KDateTime::Spec mSpec; QString mResult; }; bool IncidenceFormatter::MailBodyVisitor::visit( Event *event ) { QString recurrence[]= { i18nc( "no recurrence", "None" ), i18nc( "event recurs by minutes", "Minutely" ), i18nc( "event recurs by hours", "Hourly" ), i18nc( "event recurs by days", "Daily" ), i18nc( "event recurs by weeks", "Weekly" ), i18nc( "event recurs same position (e.g. first monday) each month", "Monthly Same Position" ), i18nc( "event recurs same day each month", "Monthly Same Day" ), i18nc( "event recurs same month each year", "Yearly Same Month" ), i18nc( "event recurs same day each year", "Yearly Same Day" ), i18nc( "event recurs same position (e.g. first monday) each year", "Yearly Same Position" ) }; mResult = mailBodyIncidence( event ); mResult += i18n( "Start Date: %1\n", IncidenceFormatter::dateToString( event->dtStart(), true, mSpec ) ); if ( !event->allDay() ) { mResult += i18n( "Start Time: %1\n", IncidenceFormatter::timeToString( event->dtStart(), true, mSpec ) ); } if ( event->dtStart() != event->dtEnd() ) { mResult += i18n( "End Date: %1\n", IncidenceFormatter::dateToString( event->dtEnd(), true, mSpec ) ); } if ( !event->allDay() ) { mResult += i18n( "End Time: %1\n", IncidenceFormatter::timeToString( event->dtEnd(), true, mSpec ) ); } if ( event->recurs() ) { Recurrence *recur = event->recurrence(); // TODO: Merge these two to one of the form "Recurs every 3 days" mResult += i18n( "Recurs: %1\n", recurrence[ recur->recurrenceType() ] ); mResult += i18n( "Frequency: %1\n", event->recurrence()->frequency() ); if ( recur->duration() > 0 ) { mResult += i18np( "Repeats once", "Repeats %1 times", recur->duration() ); mResult += '\n'; } else { if ( recur->duration() != -1 ) { // TODO_Recurrence: What to do with all-day QString endstr; if ( event->allDay() ) { endstr = KGlobal::locale()->formatDate( recur->endDate() ); } else { endstr = KGlobal::locale()->formatDateTime( recur->endDateTime().dateTime() ); } mResult += i18n( "Repeat until: %1\n", endstr ); } else { mResult += i18n( "Repeats forever\n" ); } } } QString details = event->richDescription(); if ( !details.isEmpty() ) { mResult += i18n( "Details:\n%1\n", details ); } return !mResult.isEmpty(); } bool IncidenceFormatter::MailBodyVisitor::visit( Todo *todo ) { mResult = mailBodyIncidence( todo ); if ( todo->hasStartDate() && todo->dtStart().isValid() ) { mResult += i18n( "Start Date: %1\n", IncidenceFormatter::dateToString( todo->dtStart(false), true, mSpec ) ); if ( !todo->allDay() ) { mResult += i18n( "Start Time: %1\n", IncidenceFormatter::timeToString( todo->dtStart(false), true, mSpec ) ); } } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { mResult += i18n( "Due Date: %1\n", IncidenceFormatter::dateToString( todo->dtDue(), true, mSpec ) ); if ( !todo->allDay() ) { mResult += i18n( "Due Time: %1\n", IncidenceFormatter::timeToString( todo->dtDue(), true, mSpec ) ); } } QString details = todo->richDescription(); if ( !details.isEmpty() ) { mResult += i18n( "Details:\n%1\n", details ); } return !mResult.isEmpty(); } bool IncidenceFormatter::MailBodyVisitor::visit( Journal *journal ) { mResult = mailBodyIncidence( journal ); mResult += i18n( "Date: %1\n", IncidenceFormatter::dateToString( journal->dtStart(), true, mSpec ) ); if ( !journal->allDay() ) { mResult += i18n( "Time: %1\n", IncidenceFormatter::timeToString( journal->dtStart(), true, mSpec ) ); } if ( !journal->description().isEmpty() ) { mResult += i18n( "Text of the journal:\n%1\n", journal->richDescription() ); } return !mResult.isEmpty(); } //@endcond QString IncidenceFormatter::mailBodyString( IncidenceBase *incidence ) { return mailBodyStr( incidence, KDateTime::Spec() ); } QString IncidenceFormatter::mailBodyStr( IncidenceBase *incidence, KDateTime::Spec spec ) { if ( !incidence ) { return QString(); } MailBodyVisitor v; if ( v.act( incidence, spec ) ) { return v.result(); } return QString(); } //@cond PRIVATE static QString recurEnd( Incidence *incidence ) { QString endstr; if ( incidence->allDay() ) { endstr = KGlobal::locale()->formatDate( incidence->recurrence()->endDate() ); } else { endstr = KGlobal::locale()->formatDateTime( incidence->recurrence()->endDateTime() ); } return endstr; } //@endcond QString IncidenceFormatter::recurrenceString( Incidence *incidence ) { if ( !incidence->recurs() ) { return i18n( "No recurrence" ); } QStringList dayList; dayList.append( i18n( "31st Last" ) ); dayList.append( i18n( "30th Last" ) ); dayList.append( i18n( "29th Last" ) ); dayList.append( i18n( "28th Last" ) ); dayList.append( i18n( "27th Last" ) ); dayList.append( i18n( "26th Last" ) ); dayList.append( i18n( "25th Last" ) ); dayList.append( i18n( "24th Last" ) ); dayList.append( i18n( "23rd Last" ) ); dayList.append( i18n( "22nd Last" ) ); dayList.append( i18n( "21st Last" ) ); dayList.append( i18n( "20th Last" ) ); dayList.append( i18n( "19th Last" ) ); dayList.append( i18n( "18th Last" ) ); dayList.append( i18n( "17th Last" ) ); dayList.append( i18n( "16th Last" ) ); dayList.append( i18n( "15th Last" ) ); dayList.append( i18n( "14th Last" ) ); dayList.append( i18n( "13th Last" ) ); dayList.append( i18n( "12th Last" ) ); dayList.append( i18n( "11th Last" ) ); dayList.append( i18n( "10th Last" ) ); dayList.append( i18n( "9th Last" ) ); dayList.append( i18n( "8th Last" ) ); dayList.append( i18n( "7th Last" ) ); dayList.append( i18n( "6th Last" ) ); dayList.append( i18n( "5th Last" ) ); dayList.append( i18n( "4th Last" ) ); dayList.append( i18n( "3rd Last" ) ); dayList.append( i18n( "2nd Last" ) ); dayList.append( i18nc( "last day of the month", "Last" ) ); dayList.append( i18nc( "unknown day of the month", "unknown" ) ); //#31 - zero offset from UI dayList.append( i18n( "1st" ) ); dayList.append( i18n( "2nd" ) ); dayList.append( i18n( "3rd" ) ); dayList.append( i18n( "4th" ) ); dayList.append( i18n( "5th" ) ); dayList.append( i18n( "6th" ) ); dayList.append( i18n( "7th" ) ); dayList.append( i18n( "8th" ) ); dayList.append( i18n( "9th" ) ); dayList.append( i18n( "10th" ) ); dayList.append( i18n( "11th" ) ); dayList.append( i18n( "12th" ) ); dayList.append( i18n( "13th" ) ); dayList.append( i18n( "14th" ) ); dayList.append( i18n( "15th" ) ); dayList.append( i18n( "16th" ) ); dayList.append( i18n( "17th" ) ); dayList.append( i18n( "18th" ) ); dayList.append( i18n( "19th" ) ); dayList.append( i18n( "20th" ) ); dayList.append( i18n( "21st" ) ); dayList.append( i18n( "22nd" ) ); dayList.append( i18n( "23rd" ) ); dayList.append( i18n( "24th" ) ); dayList.append( i18n( "25th" ) ); dayList.append( i18n( "26th" ) ); dayList.append( i18n( "27th" ) ); dayList.append( i18n( "28th" ) ); dayList.append( i18n( "29th" ) ); dayList.append( i18n( "30th" ) ); dayList.append( i18n( "31st" ) ); int weekStart = KGlobal::locale()->weekStartDay(); QString dayNames; QString txt; const KCalendarSystem *calSys = KGlobal::locale()->calendar(); Recurrence *recur = incidence->recurrence(); switch ( recur->recurrenceType() ) { case Recurrence::rNone: return i18n( "No recurrence" ); case Recurrence::rMinutely: if ( recur->duration() != -1 ) { txt = i18np( "Recurs every minute until %2", "Recurs every %1 minutes until %2", recur->frequency(), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18np( "Recurs every minute", "Recurs every %1 minutes", recur->frequency() ); case Recurrence::rHourly: if ( recur->duration() != -1 ) { txt = i18np( "Recurs hourly until %2", "Recurs every %1 hours until %2", recur->frequency(), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18np( "Recurs hourly", "Recurs every %1 hours", recur->frequency() ); case Recurrence::rDaily: if ( recur->duration() != -1 ) { txt = i18np( "Recurs daily until %2", "Recurs every %1 days until %2", recur->frequency(), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18np( "Recurs daily", "Recurs every %1 days", recur->frequency() ); case Recurrence::rWeekly: { bool addSpace = false; for ( int i = 0; i < 7; ++i ) { if ( recur->days().testBit( ( i + weekStart + 6 ) % 7 ) ) { if ( addSpace ) { dayNames.append( i18nc( "separator for list of days", ", " ) ); } dayNames.append( calSys->weekDayName( ( ( i + weekStart + 6 ) % 7 ) + 1, KCalendarSystem::ShortDayName ) ); addSpace = true; } } if ( dayNames.isEmpty() ) { dayNames = i18nc( "Recurs weekly on no days", "no days" ); } if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs weekly on [list of days] until end-date", "Recurs weekly on %2 until %3", "Recurs every %1 weeks on %2 until %3", recur->frequency(), dayNames, recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs weekly on [list of days]", "Recurs weekly on %2", "Recurs every %1 weeks on %2", recur->frequency(), dayNames ); } case Recurrence::rMonthlyPos: { KCal::RecurrenceRule::WDayPos rule = recur->monthPositions()[0]; if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs every N months on the [2nd|3rd|...]" " weekdayname until end-date", "Recurs every month on the %2 %3 until %4", "Recurs every %1 months on the %2 %3 until %4", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(),KCalendarSystem::LongDayName ), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs every N months on the [2nd|3rd|...] weekdayname", "Recurs every month on the %2 %3", "Recurs every %1 months on the %2 %3", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(), KCalendarSystem::LongDayName ) ); } case Recurrence::rMonthlyDay: { int days = recur->monthDays()[0]; if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs monthly on the [1st|2nd|...] day until end-date", "Recurs monthly on the %2 day until %3", "Recurs every %1 months on the %2 day until %3", recur->frequency(), dayList[days + 31], recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs monthly on the [1st|2nd|...] day", "Recurs monthly on the %2 day", "Recurs every %1 month on the %2 day", recur->frequency(), dayList[days + 31] ); } case Recurrence::rYearlyMonth: { if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs Every N years on month-name [1st|2nd|...]" " until end-date", "Recurs yearly on %2 %3 until %4", "Recurs every %1 years on %2 %3 until %4", recur->frequency(), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), dayList[ recur->yearDates()[0] + 31 ], recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } if ( !recur->yearDates().isEmpty() ) { return i18ncp( "Recurs Every N years on month-name [1st|2nd|...]", "Recurs yearly on %2 %3", "Recurs every %1 years on %2 %3", recur->frequency(), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), dayList[ recur->yearDates()[0] + 31 ] ); } else { if (!recur->yearMonths().isEmpty() ) { return i18nc( "Recurs Every year on month-name [1st|2nd|...]", "Recurs yearly on %1 %2", calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), dayList[ recur->startDate().day() + 31 ] ); } else { return i18nc( "Recurs Every year on month-name [1st|2nd|...]", "Recurs yearly on %1 %2", calSys->monthName( recur->startDate().month(), recur->startDate().year() ), dayList[ recur->startDate().day() + 31 ] ); } } } case Recurrence::rYearlyDay: if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs every N years on day N until end-date", "Recurs every year on day %2 until %3", "Recurs every %1 years" " on day %2 until %3", recur->frequency(), recur->yearDays()[0], recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs every N YEAR[S] on day N", "Recurs every year on day %2", "Recurs every %1 years" " on day %2", recur->frequency(), recur->yearDays()[0] ); case Recurrence::rYearlyPos: { KCal::RecurrenceRule::WDayPos rule = recur->yearPositions()[0]; if ( recur->duration() != -1 ) { txt = i18ncp( "Every N years on the [2nd|3rd|...] weekdayname " "of monthname until end-date", "Every year on the %2 %3 of %4 until %5", "Every %1 years on the %2 %3 of %4" " until %5", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(), KCalendarSystem::LongDayName ), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Every N years on the [2nd|3rd|...] weekdayname " "of monthname", "Every year on the %2 %3 of %4", "Every %1 years on the %2 %3 of %4", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(), KCalendarSystem::LongDayName ), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ) ); } default: return i18n( "Incidence recurs" ); } } QString IncidenceFormatter::timeToString( const KDateTime &date, bool shortfmt, const KDateTime::Spec &spec ) { if ( spec.isValid() ) { QString timeZone; if ( spec.timeZone() != KSystemTimeZones::local() ) { timeZone = ' ' + spec.timeZone().name(); } return KGlobal::locale()->formatTime( date.toTimeSpec( spec ).time(), !shortfmt ) + timeZone; } else { return KGlobal::locale()->formatTime( date.time(), !shortfmt ); } } QString IncidenceFormatter::dateToString( const KDateTime &date, bool shortfmt, const KDateTime::Spec &spec ) { if ( spec.isValid() ) { QString timeZone; if ( spec.timeZone() != KSystemTimeZones::local() ) { timeZone = ' ' + spec.timeZone().name(); } return KGlobal::locale()->formatDate( date.toTimeSpec( spec ).date(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone; } else { return KGlobal::locale()->formatDate( date.date(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ); } } QString IncidenceFormatter::dateTimeToString( const KDateTime &date, bool allDay, bool shortfmt, const KDateTime::Spec &spec ) { if ( allDay ) { return dateToString( date, shortfmt, spec ); } if ( spec.isValid() ) { QString timeZone; if ( spec.timeZone() != KSystemTimeZones::local() ) { timeZone = ' ' + spec.timeZone().name(); } return KGlobal::locale()->formatDateTime( date.toTimeSpec( spec ).dateTime(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone; } else { return KGlobal::locale()->formatDateTime( date.dateTime(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ); } } diff --git a/kcal/scheduler.cpp b/kcal/scheduler.cpp index b5c53d8b3..39f81048c 100644 --- a/kcal/scheduler.cpp +++ b/kcal/scheduler.cpp @@ -1,515 +1,597 @@ /* This file is part of the kcal library. Copyright (c) 2001,2004 Cornelius Schumacher Copyright (C) 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. */ #include "scheduler.h" #include "calendar.h" #include "event.h" #include "todo.h" #include "freebusy.h" #include "freebusycache.h" #include "icalformat.h" #include #include #include #include using namespace KCal; //@cond PRIVATE class KCal::ScheduleMessage::Private { public: Private() {} IncidenceBase *mIncidence; iTIPMethod mMethod; Status mStatus; QString mError; }; //@endcond ScheduleMessage::ScheduleMessage( IncidenceBase *incidence, iTIPMethod method, ScheduleMessage::Status status ) : d( new KCal::ScheduleMessage::Private ) { d->mIncidence = incidence; d->mMethod = method; d->mStatus = status; } ScheduleMessage::~ScheduleMessage() { delete d; } IncidenceBase *ScheduleMessage::event() { return d->mIncidence; } iTIPMethod ScheduleMessage::method() { return d->mMethod; } ScheduleMessage::Status ScheduleMessage::status() { return d->mStatus; } QString ScheduleMessage::statusName( ScheduleMessage::Status status ) { switch( status ) { case PublishNew: return i18nc( "@item new message posting", "New Message Publish" ); case PublishUpdate: return i18nc( "@item updated message", "Updated Message Published" ); case Obsolete: return i18nc( "@item obsolete status", "Obsolete" ); case RequestNew: return i18nc( "@item request new message posting", "Request New Message" ); case RequestUpdate: return i18nc( "@item request updated posting", "Request Updated Message" ); default: return i18nc( "@item unknown status", "Unknown Status: %1", status ); } } QString ScheduleMessage::error() { return d->mError; } //@cond PRIVATE struct KCal::Scheduler::Private { Private() : mFreeBusyCache( 0 ) { } FreeBusyCache *mFreeBusyCache; }; //@endcond Scheduler::Scheduler( Calendar *calendar ) : d( new KCal::Scheduler::Private ) { mCalendar = calendar; mFormat = new ICalFormat(); mFormat->setTimeSpec( calendar->timeSpec() ); } Scheduler::~Scheduler() { delete mFormat; delete d; } void Scheduler::setFreeBusyCache( FreeBusyCache *c ) { d->mFreeBusyCache = c; } FreeBusyCache *Scheduler::freeBusyCache() const { return d->mFreeBusyCache; } -bool Scheduler::acceptTransaction( IncidenceBase *incidence, iTIPMethod method, +bool Scheduler::acceptTransaction( IncidenceBase *incidence, + iTIPMethod method, ScheduleMessage::Status status ) +{ + return acceptTransaction( incidence, method, status, QString() ); +} + +bool Scheduler::acceptTransaction( IncidenceBase *incidence, + iTIPMethod method, + ScheduleMessage::Status status, + const QString &email ) { kDebug() << "method=" << methodName( method ); switch ( method ) { case iTIPPublish: return acceptPublish( incidence, status, method ); case iTIPRequest: - return acceptRequest( incidence, status ); + return acceptRequest( incidence, status, email ); case iTIPAdd: return acceptAdd( incidence, status ); case iTIPCancel: return acceptCancel( incidence, status ); case iTIPDeclineCounter: return acceptDeclineCounter( incidence, status ); case iTIPReply: return acceptReply( incidence, status, method ); case iTIPRefresh: return acceptRefresh( incidence, status ); case iTIPCounter: return acceptCounter( incidence, status ); default: break; } deleteTransaction( incidence ); return false; } QString Scheduler::methodName( iTIPMethod method ) { switch ( method ) { case iTIPPublish: return QLatin1String( "Publish" ); case iTIPRequest: return QLatin1String( "Request" ); case iTIPRefresh: return QLatin1String( "Refresh" ); case iTIPCancel: return QLatin1String( "Cancel" ); case iTIPAdd: return QLatin1String( "Add" ); case iTIPReply: return QLatin1String( "Reply" ); case iTIPCounter: return QLatin1String( "Counter" ); case iTIPDeclineCounter: return QLatin1String( "Decline Counter" ); default: return QLatin1String( "Unknown" ); } } QString Scheduler::translatedMethodName( iTIPMethod method ) { switch ( method ) { case iTIPPublish: return i18nc( "@item event, to-do, journal or freebusy posting", "Publish" ); case iTIPRequest: return i18nc( "@item event, to-do or freebusy scheduling requests", "Request" ); case iTIPReply: return i18nc( "@item event, to-do or freebusy reply to request", "Reply" ); case iTIPAdd: return i18nc( "@item event, to-do or journal additional property request", "Add" ); case iTIPCancel: return i18nc( "@item event, to-do or journal cancellation notice", "Cancel" ); case iTIPRefresh: return i18nc( "@item event or to-do description update request", "Refresh" ); case iTIPCounter: return i18nc( "@item event or to-do submit counter proposal", "Counter" ); case iTIPDeclineCounter: return i18nc( "@item event or to-do decline a counter proposal", "Decline Counter" ); default: return i18nc( "@item no method", "Unknown" ); } } bool Scheduler::deleteTransaction(IncidenceBase *) { return true; } bool Scheduler::acceptPublish( IncidenceBase *newIncBase, ScheduleMessage::Status status, iTIPMethod method ) { if( newIncBase->type() == "FreeBusy" ) { return acceptFreeBusy( newIncBase, method ); } bool res = false; kDebug() << "status=" << ScheduleMessage::statusName( status ); Incidence *newInc = static_cast( newIncBase ); Incidence *calInc = mCalendar->incidence( newIncBase->uid() ); switch ( status ) { case ScheduleMessage::Unknown: case ScheduleMessage::PublishNew: case ScheduleMessage::PublishUpdate: res = true; if ( calInc ) { if ( ( newInc->revision() > calInc->revision() ) || ( newInc->revision() == calInc->revision() && newInc->lastModified() > calInc->lastModified() ) ) { mCalendar->deleteIncidence( calInc ); } else { res = false; } } if ( res ) { mCalendar->addIncidence( newInc ); } break; case ScheduleMessage::Obsolete: res = true; break; default: break; } deleteTransaction( newIncBase ); return res; } -bool Scheduler::acceptRequest( IncidenceBase *newIncBase, ScheduleMessage::Status /* status */) +bool Scheduler::acceptRequest( IncidenceBase *incidence, + ScheduleMessage::Status status ) +{ + return acceptRequest( incidence, status, QString() ); +} + +bool Scheduler::acceptRequest( IncidenceBase *incidence, + ScheduleMessage::Status status, + const QString &email ) { - if ( newIncBase->type() == "FreeBusy" ) { + Incidence *inc = static_cast( incidence ); + if ( !inc ) { + return false; + } + if ( inc->type() == "FreeBusy" ) { // reply to this request is handled in korganizer's incomingdialog return true; } - Incidence *newInc = dynamic_cast( newIncBase ); - if ( newInc ) { - bool res = true; - Incidence *exInc = mCalendar->incidenceFromSchedulingID( newIncBase->uid() ); - if ( exInc ) { - res = false; - if ( ( newInc->revision() > exInc->revision() ) || - ( newInc->revision() == exInc->revision() && - newInc->lastModified()>exInc->lastModified() ) ) { - mCalendar->deleteIncidence( exInc ); - res = true; + + const Incidence::List existingIncidences = mCalendar->incidencesFromSchedulingID( inc->uid() ); + kDebug() << "status=" << ScheduleMessage::statusName( status ) + << ": found " << existingIncidences.count() + << " incidences with schedulingID " << inc->schedulingID(); + Incidence::List::ConstIterator incit = existingIncidences.begin(); + for ( ; incit != existingIncidences.end() ; ++incit ) { + Incidence *i = *incit; + kDebug() << "Considering this found event (" + << ( i->isReadOnly() ? "readonly" : "readwrite" ) + << ") :" << mFormat->toString( i ); + // If it's readonly, we can't possible update it. + if ( i->isReadOnly() ) { + continue; + } + if ( i->revision() <= inc->revision() ) { + // The new incidence might be an update for the found one + bool isUpdate = true; + // Code for new invitations: + // If you think we could check the value of "status" to be RequestNew: we can't. + // It comes from a similar check inside libical, where the event is compared to + // other events in the calendar. But if we have another version of the event around + // (e.g. shared folder for a group), the status could be RequestNew, Obsolete or Updated. + kDebug() << "looking in " << i->uid() << "'s attendees"; + // This is supposed to be a new request, not an update - however we want to update + // the existing one to handle the "clicking more than once on the invitation" case. + // So check the attendee status of the attendee. + const KCal::Attendee::List attendees = i->attendees(); + KCal::Attendee::List::ConstIterator ait; + for ( ait = attendees.begin(); ait != attendees.end(); ++ait ) { + if( (*ait)->email() == email && (*ait)->status() == Attendee::NeedsAction ) { + // This incidence wasn't created by me - it's probably in a shared folder + // and meant for someone else, ignore it. + kDebug() << "ignoring " << i->uid() << " since I'm still NeedsAction there"; + isUpdate = false; + break; + } } + if ( isUpdate ) { + if ( i->revision() == inc->revision() && + i->lastModified() > inc->lastModified() ) { + // This isn't an update - the found incidence was modified more recently + kDebug() << "This isn't an update - the found incidence was modified more recently"; + deleteTransaction( i ); + return false; + } + kDebug() << "replacing existing incidence " << i->uid(); + mCalendar->deleteIncidence( i ); + break; // replacing one is enough + } + } else { + // This isn't an update - the found incidence has a bigger revision number + kDebug() << "This isn't an update - the found incidence has a bigger revision number"; + deleteTransaction(incidence); + return false; } - if ( res ) { - // Move the uid to be the schedulingID and make a unique UID - newInc->setSchedulingID( newInc->uid() ); - newInc->setUid( CalFormat::createUniqueId() ); + } - mCalendar->addIncidence( newInc ); - } - deleteTransaction( newIncBase ); - return res; + // Move the uid to be the schedulingID and make a unique UID + inc->setSchedulingID( inc->uid() ); + inc->setUid( CalFormat::createUniqueId() ); + // in case this is an update and we didn't find the to-be-updated incidence, + // ask whether we should create a new one, or drop the update + if ( existingIncidences.count() > 0 || inc->revision() == 0 || + KMessageBox::warningYesNo( + 0, + i18nc( "@info", + "The event, to-do or journal to be updated could not be found. " + "Maybe it has already been deleted, or the calendar that " + "contains it is disabled. Press 'Store' to create a new " + "one or 'Throw away' to discard this update." ), + i18nc( "@title", "Discard this update?" ), + KGuiItem( i18nc( "@option", "Store" ) ), + KGuiItem( i18nc( "@option", "Throw away" ) ) ) == KMessageBox::Yes ) { + kDebug() << "Storing new incidence with scheduling uid=" << inc->schedulingID() + << " and uid=" << inc->uid(); + mCalendar->addIncidence( inc ); } - return false; + deleteTransaction(incidence); + return true; } bool Scheduler::acceptAdd( IncidenceBase *incidence, ScheduleMessage::Status /* status */) { deleteTransaction(incidence); return false; } bool Scheduler::acceptCancel( IncidenceBase *incidence, ScheduleMessage::Status /* status */) { - bool ret = false; const IncidenceBase *toDelete = mCalendar->incidenceFromSchedulingID( incidence->uid() ); + + bool ret = true; if ( toDelete ) { - Event *event = mCalendar->event( toDelete->uid() ); - if ( event ) { - mCalendar->deleteEvent( event ); - ret = true; - } else { + if ( toDelete->type() == "Event" ) { + Event *event = mCalendar->event( toDelete->uid() ); + ret = ( event && mCalendar->deleteEvent( event ) ); + } else if ( toDelete->type() == "Todo" ) { Todo *todo = mCalendar->todo( toDelete->uid() ); - if ( todo ) { - mCalendar->deleteTodo( todo ); - ret = true; - } + ret = ( todo && mCalendar->deleteTodo( todo ) ); } } - deleteTransaction( incidence ); + + if ( !ret ) { + KMessageBox::error( + 0, + i18n( "The event or task to be canceled could not be removed from your calendar. " + "Maybe it has already been deleted, or the calendar that " + "contains it is disabled." ) ); + } + deleteTransaction(incidence); return ret; } bool Scheduler::acceptDeclineCounter( IncidenceBase *incidence, ScheduleMessage::Status status ) { Q_UNUSED( status ); deleteTransaction( incidence ); return false; } bool Scheduler::acceptReply( IncidenceBase *incidence, ScheduleMessage::Status status, iTIPMethod method ) { Q_UNUSED( status ); if ( incidence->type() == "FreeBusy" ) { return acceptFreeBusy( incidence, method ); } bool ret = false; Event *ev = mCalendar->event( incidence->uid() ); Todo *to = mCalendar->todo( incidence->uid() ); // try harder to find the correct incidence if ( !ev && !to ) { const Incidence::List list = mCalendar->incidences(); for ( Incidence::List::ConstIterator it=list.constBegin(), end=list.constEnd(); it != end; ++it ) { if ( (*it)->schedulingID() == incidence->uid() ) { ev = dynamic_cast( *it ); to = dynamic_cast( *it ); break; } } } if ( ev || to ) { //get matching attendee in calendar kDebug() << "match found!"; Attendee::List attendeesIn = incidence->attendees(); Attendee::List attendeesEv; Attendee::List attendeesNew; if ( ev ) { attendeesEv = ev->attendees(); } if ( to ) { attendeesEv = to->attendees(); } Attendee::List::ConstIterator inIt; Attendee::List::ConstIterator evIt; for ( inIt = attendeesIn.constBegin(); inIt != attendeesIn.constEnd(); ++inIt ) { Attendee *attIn = *inIt; bool found = false; for ( evIt = attendeesEv.constBegin(); evIt != attendeesEv.constEnd(); ++evIt ) { Attendee *attEv = *evIt; if ( attIn->email().toLower() == attEv->email().toLower() ) { //update attendee-info kDebug() << "update attendee"; attEv->setStatus( attIn->status() ); attEv->setDelegate( attIn->delegate() ); attEv->setDelegator( attIn->delegator() ); ret = true; found = true; } } if ( !found && attIn->status() != Attendee::Declined ) { attendeesNew.append( attIn ); } } bool attendeeAdded = false; for ( Attendee::List::ConstIterator it = attendeesNew.constBegin(); it != attendeesNew.constEnd(); ++it ) { Attendee *attNew = *it; QString msg = i18nc( "@info", "%1 wants to attend %2 but was not invited.", attNew->fullName(), ( ev ? ev->summary() : to->summary() ) ); if ( !attNew->delegator().isEmpty() ) { msg = i18nc( "@info", "%1 wants to attend %2 on behalf of %3.", attNew->fullName(), ( ev ? ev->summary() : to->summary() ), attNew->delegator() ); } if ( KMessageBox::questionYesNo( 0, msg, i18nc( "@title", "Uninvited attendee" ), KGuiItem( i18nc( "@option", "Accept Attendance" ) ), KGuiItem( i18nc( "@option", "Reject Attendance" ) ) ) != KMessageBox::Yes ) { KCal::Incidence *cancel = dynamic_cast( incidence ); if ( cancel ) { cancel->addComment( i18nc( "@info", "The organizer rejected your attendance at this meeting." ) ); } performTransaction( cancel ? cancel : incidence, iTIPCancel, attNew->fullName() ); // ### can't delete cancel here because it is aliased to incidence which // is accessed in the next loop iteration (CID 4232) // delete cancel; continue; } Attendee *a = new Attendee( attNew->name(), attNew->email(), attNew->RSVP(), attNew->status(), attNew->role(), attNew->uid() ); a->setDelegate( attNew->delegate() ); a->setDelegator( attNew->delegator() ); if ( ev ) { ev->addAttendee( a ); } else if ( to ) { to->addAttendee( a ); } ret = true; attendeeAdded = true; } // send update about new participants if ( attendeeAdded ) { if ( ev ) { ev->setRevision( ev->revision() + 1 ); performTransaction( ev, iTIPRequest ); } if ( to ) { to->setRevision( to->revision() + 1 ); performTransaction( to, iTIPRequest ); } } if ( ret ) { // We set at least one of the attendees, so the incidence changed // Note: This should not result in a sequence number bump if ( ev ) { ev->updated(); } else if ( to ) { to->updated(); } } if ( to ) { // for VTODO a REPLY can be used to update the completion status of // a to-do. see RFC2446 3.4.3 Todo *update = dynamic_cast ( incidence ); Q_ASSERT( update ); if ( update && ( to->percentComplete() != update->percentComplete() ) ) { to->setPercentComplete( update->percentComplete() ); to->updated(); } } } else { kError(5800) << "No incidence for scheduling\n"; } if ( ret ) { deleteTransaction( incidence ); } return ret; } bool Scheduler::acceptRefresh( IncidenceBase *incidence, ScheduleMessage::Status status ) { Q_UNUSED( status ); // handled in korganizer's IncomingDialog deleteTransaction( incidence ); return false; } bool Scheduler::acceptCounter( IncidenceBase *incidence, ScheduleMessage::Status status ) { Q_UNUSED( status ); deleteTransaction( incidence ); return false; } bool Scheduler::acceptFreeBusy( IncidenceBase *incidence, iTIPMethod method ) { if ( !d->mFreeBusyCache ) { kError() << "KCal::Scheduler: no FreeBusyCache."; return false; } FreeBusy *freebusy = static_cast(incidence); kDebug() << "freeBusyDirName:" << freeBusyDir(); Person from; if( method == iTIPPublish ) { from = freebusy->organizer(); } if ( ( method == iTIPReply ) && ( freebusy->attendeeCount() == 1 ) ) { Attendee *attendee = freebusy->attendees().first(); from.setName( attendee->name() ); from.setEmail( attendee->email() ); } if ( !d->mFreeBusyCache->saveFreeBusy( freebusy, from ) ) { return false; } deleteTransaction( incidence ); return true; } diff --git a/kcal/scheduler.h b/kcal/scheduler.h index f00030f0e..b688807f3 100644 --- a/kcal/scheduler.h +++ b/kcal/scheduler.h @@ -1,224 +1,241 @@ /* This file is part of the kcal library. Copyright (c) 2001-2003 Cornelius Schumacher 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 KCAL_SCHEDULER_H #define KCAL_SCHEDULER_H #include "kcal_export.h" #include #include namespace KCal { /** iTIP methods. */ enum iTIPMethod { iTIPPublish, /**< Event, to-do, journal or freebusy posting */ iTIPRequest, /**< Event, to-do or freebusy scheduling request */ iTIPReply, /**< Event, to-do or freebusy reply to request */ iTIPAdd, /**< Event, to-do or journal additional property request */ iTIPCancel, /**< Event, to-do or journal cancellation notice */ iTIPRefresh, /**< Event or to-do description update request */ iTIPCounter, /**< Event or to-do submit counter proposal */ iTIPDeclineCounter,/**< Event or to-do decline a counter proposal */ iTIPNoMethod /**< No method */ }; class IncidenceBase; class Calendar; class ICalFormat; class FreeBusyCache; /** @brief A Scheduling message class. This class provides an encapsulation of a scheduling message. It associates an incidence with an iTIPMethod and status information. */ class KCAL_EXPORT ScheduleMessage { public: /** Message status. */ enum Status { PublishNew, /**< New message posting */ PublishUpdate, /**< Updated message */ Obsolete, /**< obsolete */ RequestNew, /**< Request new message posting */ RequestUpdate, /**< Request updated message */ Unknown /**< No status */ }; /** Creates a scheduling message with method as defined in iTIPMethod and a status. */ ScheduleMessage( IncidenceBase *incidence, iTIPMethod method, Status status ); /** Destructor. */ ~ScheduleMessage(); /** Returns the event associated with this message. */ IncidenceBase *event(); /** Returns the iTIP method associated with this message. */ iTIPMethod method(); /** Returns the status of this message. */ Status status(); /** Returns a human-readable name for an iTIP message status. */ static QString statusName( Status status ); /** Returns the error message if there is any. */ QString error(); private: Q_DISABLE_COPY( ScheduleMessage ) class Private; Private *const d; }; /** This class provides an encapsulation of iTIP transactions (RFC 2446). It is an abstract base class for inheritance by implementations of the iTIP scheme like iMIP or iRIP. */ class KCAL_EXPORT Scheduler { public: /** Creates a scheduler for calendar specified as argument. */ explicit Scheduler( Calendar *calendar ); virtual ~Scheduler(); /** iTIP publish action */ virtual bool publish( IncidenceBase *incidence, const QString &recipients ) = 0; /** Performs iTIP transaction on incidence. The method is specified as the method argument and can be any valid iTIP method. @param incidence the incidence for the transaction. @param method the iTIP transaction method to use. */ virtual bool performTransaction( IncidenceBase *incidence, iTIPMethod method ) = 0; /** Performs iTIP transaction on incidence to specified recipient(s). The method is specified as the method argumanet and can be any valid iTIP method. @param incidence the incidence for the transaction. @param method the iTIP transaction method to use. @param recipients the receipients of the transaction. */ virtual bool performTransaction( IncidenceBase *incidence, iTIPMethod method, const QString &recipients ) = 0; /** Retrieves incoming iTIP transactions. */ virtual QList retrieveTransactions() = 0; + /** + @deprecated: Use the other acceptTransaction() instead + KDE5: Remove me, make email an optional argument in the other overload + */ + bool KDE_DEPRECATED acceptTransaction( IncidenceBase *incidence, iTIPMethod method, + ScheduleMessage::Status status ); + /** Accepts the transaction. The incidence argument specifies the iCal component on which the transaction acts. The status is the result of processing a iTIP message with the current calendar and specifies the action to be taken for this incidence. @param incidence the incidence for the transaction. @param method iTIP transaction method to check. @param status scheduling status. + @param email the email address of the person for whom this + transaction is to be performed. */ - bool acceptTransaction( IncidenceBase *incidence, iTIPMethod method, - ScheduleMessage::Status status ); + bool acceptTransaction( IncidenceBase *incidence, + iTIPMethod method, + ScheduleMessage::Status status, + const QString &email ); /** Returns a machine-readable name for a iTIP method. */ static QString methodName( iTIPMethod method ); /** Returns a translated human-readable name for a iTIP method. */ static QString translatedMethodName( iTIPMethod method ); virtual bool deleteTransaction( IncidenceBase *incidence ); /** Returns the directory where the free-busy information is stored. */ virtual QString freeBusyDir() = 0; /** Sets the free/busy cache used to store free/busy information. */ void setFreeBusyCache( FreeBusyCache * ); /** Returns the free/busy cache. */ FreeBusyCache *freeBusyCache() const; protected: bool acceptPublish( IncidenceBase *, ScheduleMessage::Status status, iTIPMethod method ); - bool acceptRequest( IncidenceBase *, ScheduleMessage::Status status ); + /** + @deprecated: Use the other overload instead + KDE5: remove me + */ + bool KDE_DEPRECATED acceptRequest( IncidenceBase *, ScheduleMessage::Status status ); + bool acceptRequest( IncidenceBase *, ScheduleMessage::Status status, + const QString &email ); bool acceptAdd( IncidenceBase *, ScheduleMessage::Status status ); bool acceptCancel( IncidenceBase *, ScheduleMessage::Status status ); bool acceptDeclineCounter( IncidenceBase *, ScheduleMessage::Status status ); bool acceptReply( IncidenceBase *, ScheduleMessage::Status status, iTIPMethod method ); bool acceptRefresh( IncidenceBase *, ScheduleMessage::Status status ); bool acceptCounter( IncidenceBase *, ScheduleMessage::Status status ); bool acceptFreeBusy( IncidenceBase *, iTIPMethod method ); Calendar *mCalendar; ICalFormat *mFormat; private: Q_DISABLE_COPY( Scheduler ) struct Private; Private *const d; }; } #endif diff --git a/kimap/CMakeLists.txt b/kimap/CMakeLists.txt index 6af69bf06..b8f13d91b 100644 --- a/kimap/CMakeLists.txt +++ b/kimap/CMakeLists.txt @@ -1,64 +1,65 @@ project(kimap) add_definitions( -DKDE_DEFAULT_DEBUG_AREA=5327 ) add_subdirectory( tests ) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}") set(kimap_LIB_SRCS acl.cpp imapset.cpp imapstreamparser.cpp job.cpp appendjob.cpp capabilitiesjob.cpp fetchjob.cpp + idlejob.cpp listjob.cpp loginjob.cpp logoutjob.cpp rfccodecs.cpp selectjob.cpp session.cpp sessionthread.cpp closejob.cpp expungejob.cpp deletejob.cpp createjob.cpp subscribejob.cpp unsubscribejob.cpp renamejob.cpp storejob.cpp copyjob.cpp searchjob.cpp acljobbase.cpp setacljob.cpp getacljob.cpp deleteacljob.cpp myrightsjob.cpp listrightsjob.cpp quotajobbase.cpp setquotajob.cpp getquotajob.cpp getquotarootjob.cpp metadatajobbase.cpp setmetadatajob.cpp getmetadatajob.cpp ) kde4_add_library(kimap SHARED ${kimap_LIB_SRCS}) target_link_libraries(kimap ${KDE4_KDECORE_LIBS} ${QT_QTNETWORK_LIBRARY} kmime ${SASL2_LIBRARIES}) include_directories( ${CMAKE_SOURCE_DIR}/kioslave ${SASL2_INCLUDE_DIR} ${Boost_INCLUDE_DIR} ) set_target_properties(kimap PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION} ) install(TARGETS kimap EXPORT kdepimlibsLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) ########### install files ############### -install( FILES kimap_export.h acl.h imapset.h job.h appendjob.h capabilitiesjob.h fetchjob.h listjob.h loginjob.h logoutjob.h rfccodecs.h +install( FILES kimap_export.h acl.h imapset.h job.h appendjob.h capabilitiesjob.h fetchjob.h idlejob.h listjob.h loginjob.h logoutjob.h rfccodecs.h selectjob.h closejob.h expungejob.h deletejob.h createjob.h subscribejob.h unsubscribejob.h renamejob.h session.h sessionuiproxy.h storejob.h copyjob.h searchjob.h acljobbase.h setacljob.h getacljob.h deleteacljob.h myrightsjob.h listrightsjob.h quotajobbase.h setquotajob.h getquotajob.h getquotarootjob.h metadatajobbase.h setmetadatajob.h getmetadatajob.h DESTINATION ${INCLUDE_INSTALL_DIR}/kimap COMPONENT Devel) diff --git a/kimap/job.cpp b/kimap/job.cpp index 4ca76bb98..61186b49c 100644 --- a/kimap/job.cpp +++ b/kimap/job.cpp @@ -1,86 +1,92 @@ /* 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 "job.h" #include "job_p.h" #include "message_p.h" #include "session_p.h" #include #include using namespace KIMAP; Job::Job( Session *session ) : d_ptr(new JobPrivate(session, i18n("Job"))) { } Job::Job( JobPrivate &dd ) : d_ptr(&dd) { } Job::~Job() { delete d_ptr; } +Session *Job::session() const +{ + Q_D(const Job); + return d->m_session; +} + void Job::start() { Q_D(Job); d->sessionInternal()->addJob(this); } void Job::handleResponse(const Message &response) { handleErrorReplies(response); } void Job::connectionLost() { setError( KJob::UserDefinedError ); setErrorText( i18n("Connection to server lost.") ); emitResult(); } Job::HandlerResponse Job::handleErrorReplies(const Message &response) { Q_D(Job); // kDebug() << response.toString(); if ( !response.content.isEmpty() && response.content.first().toString() == d->tag ) { if ( response.content.size() < 2 ) { setErrorText( i18n("%1 failed, malformed reply from the server.", d->m_name) ); } else if ( response.content[1].toString() != "OK" ) { setError( UserDefinedError ); setErrorText( i18n("%1 failed, server replied: %2", d->m_name, response.toString().constData()) ); } emitResult(); return Handled; } return NotHandled; } #include "job.moc" diff --git a/kimap/job.h b/kimap/job.h index cf3679016..4656049f1 100644 --- a/kimap/job.h +++ b/kimap/job.h @@ -1,67 +1,69 @@ /* 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 KIMAP_JOB_H #define KIMAP_JOB_H #include "kimap_export.h" #include namespace KIMAP { class Session; class SessionPrivate; class JobPrivate; struct Message; class KIMAP_EXPORT Job : public KJob { Q_OBJECT Q_DECLARE_PRIVATE(Job) friend class SessionPrivate; public: virtual ~Job(); + Session *session() const; + virtual void start(); private: virtual void doStart() = 0; virtual void handleResponse(const Message &response); virtual void connectionLost(); protected: enum HandlerResponse { Handled = 0, NotHandled }; HandlerResponse handleErrorReplies(const Message &response); explicit Job( Session *session ); explicit Job( JobPrivate &dd ); JobPrivate *const d_ptr; }; } #endif diff --git a/kimap/job_p.h b/kimap/job_p.h index cff4535bd..04ebb94ea 100644 --- a/kimap/job_p.h +++ b/kimap/job_p.h @@ -1,49 +1,53 @@ /* 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 KIMAP_JOB_P_H #define KIMAP_JOB_P_H #include "session.h" #include namespace KIMAP { class SessionPrivate; class JobPrivate { public: JobPrivate( Session *session, const QString& name ) : m_session(session) { m_name = name; } virtual ~JobPrivate() { } inline SessionPrivate *sessionInternal() { return m_session->d; } + inline const SessionPrivate *sessionInternal() const { + return m_session->d; + } + QByteArray tag; Session *m_session; QString m_name; }; } #endif diff --git a/kimap/tests/CMakeLists.txt b/kimap/tests/CMakeLists.txt index 9f69663c8..c561580f0 100644 --- a/kimap/tests/CMakeLists.txt +++ b/kimap/tests/CMakeLists.txt @@ -1,55 +1,56 @@ include_directories( ${CMAKE_SOURCE_DIR}/kimap ${Boost_INCLUDE_DIR}) set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) MACRO(KIMAP_UNIT_TESTS) FOREACH(_testname ${ARGN}) kde4_add_unit_test(${_testname} TESTNAME kimap-${_testname} NOGUI ${_testname}.cpp) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}") target_link_libraries(${_testname} ${KDE4_KDECORE_LIBS} ${QT_QTTEST_LIBRARY} kimap kmime) ENDFOREACH(_testname) ENDMACRO(KIMAP_UNIT_TESTS) MACRO(KIMAP_EXECUTABLE_TESTS) FOREACH(_testname ${ARGN}) kde4_add_executable(${_testname} NOGUI TEST ${_testname}.cpp) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}") target_link_libraries(${_testname} ${KDE4_KDECORE_LIBS} ${KDE4_KIO_LIBS} kimap kmime) ENDFOREACH(_testname) ENDMACRO(KIMAP_EXECUTABLE_TESTS) ### convenience macro MACRO(ADD_IMAPLIB_TEST _source) set(_test ${_source} fakeserver.cpp ) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}") get_filename_component(_name ${_source} NAME_WE) kde4_add_unit_test(${_name} TESTNAME kimap-${_name} ${_test}) target_link_libraries(${_name} kimap ${QT_QTTEST_LIBRARY} ${KDE4_KDECORE_LIBS} ${QT_QTGUI_LIBRARY} ${QT_QTNETWORK_LIBRARY} ) ENDMACRO(ADD_IMAPLIB_TEST) ### tests ADD_IMAPLIB_TEST(loginjobtest.cpp) ADD_IMAPLIB_TEST(logoutjobtest.cpp) ADD_IMAPLIB_TEST(capabilitiesjobtest.cpp) ADD_IMAPLIB_TEST(selectjobtest.cpp) ADD_IMAPLIB_TEST(createjobtest.cpp) ADD_IMAPLIB_TEST(deletejobtest.cpp) ADD_IMAPLIB_TEST(renamejobtest.cpp) ADD_IMAPLIB_TEST(subscribejobtest.cpp) ADD_IMAPLIB_TEST(unsubscribejobtest.cpp) ADD_IMAPLIB_TEST(listjobtest.cpp) ADD_IMAPLIB_TEST(storejobtest.cpp) ########### automated tests ############### KIMAP_UNIT_TESTS( testrfccodecs testsession ) ########### manual tests ############### KIMAP_EXECUTABLE_TESTS( + testimapidle testimapserver ) diff --git a/kimap/tests/testimapserver.cpp b/kimap/tests/testimapserver.cpp index ed1d502de..ef74d7d2a 100644 --- a/kimap/tests/testimapserver.cpp +++ b/kimap/tests/testimapserver.cpp @@ -1,558 +1,579 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2009 Kevin Ottens + * Copyright (C) 2009 Andras Mantia + * + * 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 #include #include #include #include #include #include #include #include "kimap/acl.h" #include "kimap/session.h" #include "kimap/appendjob.h" #include "kimap/capabilitiesjob.h" #include "kimap/fetchjob.h" #include "kimap/listjob.h" #include "kimap/loginjob.h" #include "kimap/logoutjob.h" #include "kimap/selectjob.h" #include "kimap/closejob.h" #include "kimap/expungejob.h" #include "kimap/createjob.h" #include "kimap/deletejob.h" #include "kimap/subscribejob.h" #include "kimap/unsubscribejob.h" #include "kimap/renamejob.h" #include "kimap/storejob.h" #include "kimap/sessionuiproxy.h" #include "kimap/setacljob.h" #include "kimap/getacljob.h" #include "kimap/deleteacljob.h" #include "kimap/myrightsjob.h" #include "kimap/listrightsjob.h" #include "kimap/setmetadatajob.h" #include "kimap/getmetadatajob.h" using namespace KIMAP; class UiProxy: public SessionUiProxy { public: bool ignoreSslError(const KSslErrorUiData& errorData) { if (KIO::SslUi::askIgnoreSslErrors(errorData, KIO::SslUi::StoreRules)) { return true; } else { return false; } } }; void dumpContentHelper(KMime::Content *part, const QString &partId = QString()) { if (partId.isEmpty()) { kDebug() << "** Message root **"; } else { kDebug() << "** Part" << partId << "**"; } kDebug() << part->head(); KMime::Content::List children = part->contents(); for (int i=0; isetIncludeUnsubscribed(includeUnsubscribed); list->exec(); Q_ASSERT_X(list->error()==0, "ListJob", list->errorString().toLocal8Bit()); int count = list->mailBoxes().size(); for (int i=0; imailBoxes()[i]; if (descriptor.name.endsWith(nameFilter)) kDebug() << descriptor.separator << descriptor.name; } } void testMetaData(Session *session) { kDebug() << "TESTING: METADATA commands"; CreateJob *create = new CreateJob(session); create->setMailBox("INBOX/TestFolder"); create->exec(); SetMetaDataJob *setmetadata = new SetMetaDataJob(session); setmetadata->setMailBox("INBOX/TestFolder"); setmetadata->setServerCapability(SetMetaDataJob::Annotatemore); setmetadata->setEntry("/comment"); setmetadata->addMetaData("value.priv", "My new comment"); setmetadata->exec(); setmetadata = new SetMetaDataJob(session); setmetadata->setMailBox("INBOX/TestFolder"); setmetadata->setServerCapability(SetMetaDataJob::Annotatemore); setmetadata->setEntry("/check"); setmetadata->addMetaData("value.priv", "true"); setmetadata->exec(); GetMetaDataJob *getmetadata = new GetMetaDataJob(session); getmetadata->setMailBox("INBOX/TestFolder"); getmetadata->setServerCapability(SetMetaDataJob::Annotatemore); getmetadata->addEntry("/*","value.priv"); getmetadata->exec(); Q_ASSERT_X(getmetadata->metaData("INBOX/TestFolder", "/check", "value.priv") == "true", "", "/check metadata should be true"); Q_ASSERT_X(getmetadata->metaData("INBOX/TestFolder", "/comment", "value.priv") == "My new comment", "", "/check metadata should be My new comment"); //cleanup DeleteJob *deletejob = new DeleteJob(session); deletejob->setMailBox("INBOX/TestFolder"); deletejob->exec(); } void testAcl(Session *session, const QString &user) { kDebug() << "TESTING: ACL commands"; CreateJob *create = new CreateJob(session); create->setMailBox("INBOX/TestFolder"); create->exec(); ListRightsJob *listRights = new ListRightsJob(session); listRights->setMailBox("INBOX/TestFolder"); listRights->setIdentifier(user.toLatin1()); listRights->exec(); kDebug() << "Default rights on INBOX/TestFolder: " << Acl::rightsToString(listRights->defaultRights()); QList possible = listRights->possibleRights(); QStringList strList; Q_FOREACH(Acl::Rights r, possible) { strList << Acl::rightsToString(r); } kDebug() << "Possible rights on INBOX/TestFolder: " << strList; MyRightsJob *myRights = new MyRightsJob(session); myRights->setMailBox("INBOX/TestFolder"); myRights->exec(); Acl::Rights mine = myRights->rights(); kDebug() << "My rights on INBOX/TestFolder: " << Acl::rightsToString(mine); kDebug() << "Reading INBOX/TestFolder is possible: " << myRights->hasRightEnabled(Acl::Read); Q_ASSERT_X(myRights->hasRightEnabled(Acl::Read), "Reading INBOX is NOT possible", ""); GetAclJob *getAcl= new GetAclJob(session); getAcl->setMailBox("INBOX/TestFolder"); getAcl->exec(); kDebug() << "Anyone rights on INBOX/TestFolder: " << getAcl->rights("anyone"); Acl::Rights users = getAcl->rights(user.toLatin1()); kDebug() << user << " rights on INBOX/TestFolder: " << Acl::rightsToString(users); Q_ASSERT_X(mine == users, "GETACL returns different rights for the same user", ""); kDebug() << "Removing Delete right "; mine = Acl::Delete; SetAclJob *setAcl= new SetAclJob(session); setAcl->setMailBox("INBOX/TestFolder"); setAcl->setIdentifier(user.toLatin1()); setAcl->setRights(AclJobBase::Remove, mine); setAcl->exec(); getAcl= new GetAclJob(session); getAcl->setMailBox("INBOX/TestFolder"); getAcl->exec(); users = getAcl->rights(user.toLatin1()); kDebug() << user << " rights on INBOX/TestFolder: " << Acl::rightsToString(users); kDebug() << "Adding back Delete right "; mine = Acl::Delete; setAcl= new SetAclJob(session); setAcl->setMailBox("INBOX/TestFolder"); setAcl->setIdentifier(user.toLatin1()); setAcl->setRights(AclJobBase::Add, mine); setAcl->exec(); getAcl= new GetAclJob(session); getAcl->setMailBox("INBOX/TestFolder"); getAcl->exec(); users = getAcl->rights(user.toLatin1()); kDebug() << user << " rights on INBOX/TestFolder: " << Acl::rightsToString(users); //cleanup DeleteJob *deletejob = new DeleteJob(session); deletejob->setMailBox("INBOX/TestFolder"); deletejob->exec(); } void testAppendAndStore(Session *session) { kDebug() << "TESTING: APPEND and STORE"; //setup CreateJob *create = new CreateJob(session); create->setMailBox("INBOX/TestFolder"); create->exec(); QByteArray testMailContent = "Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)\r\n" "From: Fred Foobar \r\n" "Subject: afternoon meeting\r\n" "To: mooch@owatagu.siam.edu\r\n" "Message-Id: \r\n" "MIME-Version: 1.0\r\n" "Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n" "\r\n" "Hello Joe, do you think we can meet at 3:30 tomorrow?\r\n"; kDebug() << "Append a message in INBOX/TestFolder..."; AppendJob *append = new AppendJob(session); append->setMailBox("INBOX/TestFolder"); append->setContent(testMailContent); append->exec(); Q_ASSERT_X(append->error()==0, "AppendJob", append->errorString().toLocal8Bit()); kDebug() << "Read the message back and compare..."; SelectJob *select = new SelectJob(session); select->setMailBox("INBOX/TestFolder"); select->exec(); FetchJob *fetch = new FetchJob(session); FetchJob::FetchScope scope; fetch->setSequenceSet(ImapSet(1)); scope.parts.clear(); scope.mode = FetchJob::FetchScope::Content; fetch->setScope(scope); fetch->exec(); MessagePtr message = fetch->messages()[1]; Q_ASSERT_X(fetch->error()==0, "FetchJob", fetch->errorString().toLocal8Bit()); testMailContent.replace( "\r\n", "\n" ); Q_ASSERT_X(testMailContent==message->head()+"\n"+message->body(), "Message differs from reference", message->head()+"\n"+message->body()); fetch = new FetchJob(session); fetch->setSequenceSet(ImapSet(1)); scope.parts.clear(); scope.mode = FetchJob::FetchScope::Flags; fetch->setScope(scope); fetch->exec(); MessageFlags expectedFlags = fetch->flags()[1]; kDebug() << "Read the message flags:" << expectedFlags; kDebug() << "Add the \\Deleted flag..."; expectedFlags << "\\Deleted"; qSort(expectedFlags); StoreJob *store = new StoreJob(session); store->setSequenceSet(ImapSet(1)); store->setMode(StoreJob::AppendFlags); store->setFlags(QList() << "\\Deleted"); store->exec(); Q_ASSERT_X(store->error()==0, "StoreJob", store->errorString().toLocal8Bit()); QList resultingFlags = store->resultingFlags()[1]; qSort(resultingFlags); if (expectedFlags!=resultingFlags) kDebug() << resultingFlags; Q_ASSERT(expectedFlags==resultingFlags); select = new SelectJob(session); select->setMailBox("INBOX"); select->exec(); //cleanup DeleteJob *deletejob = new DeleteJob(session); deletejob->setMailBox("INBOX/TestFolder"); deletejob->exec(); deletejob = new DeleteJob(session); deletejob->setMailBox("INBOX/RenamedTestFolder"); deletejob->exec(); } void testRename(Session *session) { kDebug() << "TESTING: RENAME"; //setup CreateJob *create = new CreateJob(session); create->setMailBox("INBOX/TestFolder"); create->exec(); kDebug() << "Listing mailboxes with name TestFolder:"; listFolders(session, true, "TestFolder"); //actual tests kDebug() << "Renaming to RenamedTestFolder"; RenameJob *rename = new RenameJob(session); rename->setSourceMailBox("INBOX/TestFolder"); rename->setDestinationMailBox("INBOX/RenamedTestFolder"); rename->exec(); kDebug() << "Listing mailboxes with name TestFolder:"; listFolders(session, true, "TestFolder"); kDebug() << "Listing mailboxes with name RenamedTestFolder:"; listFolders(session, true, "RenamedTestFolder"); //cleanup DeleteJob *deletejob = new DeleteJob(session); deletejob->setMailBox("INBOX/TestFolder"); deletejob->exec(); deletejob = new DeleteJob(session); deletejob->setMailBox("INBOX/RenamedTestFolder"); deletejob->exec(); } void testSubscribe(Session *session) { kDebug() << "TESTING: SUBSCRIBE/UNSUBSCRIBE"; //setup CreateJob *create = new CreateJob(session); create->setMailBox("INBOX/TestFolder"); create->exec(); kDebug() << "Listing subscribed mailboxes with name TestFolder:"; listFolders(session, false, "TestFolder"); //actual tests kDebug() << "Subscribing to INBOX/TestFolder"; SubscribeJob *subscribe = new SubscribeJob(session); subscribe->setMailBox("INBOX/TestFolder"); subscribe->exec(); kDebug() << "Listing subscribed mailboxes with name TestFolder:"; listFolders(session, false, "TestFolder"); kDebug() << "Unsubscribing from INBOX/TestFolder"; UnsubscribeJob *unsubscribe = new UnsubscribeJob(session); unsubscribe->setMailBox("INBOX/TestFolder"); unsubscribe->exec(); kDebug() << "Listing subscribed mailboxes with name TestFolder:"; listFolders(session, false, "TestFolder"); //cleanup DeleteJob *deletejob = new DeleteJob(session); deletejob->setMailBox("INBOX/TestFolder"); deletejob->exec(); } void testDelete(Session *session) { kDebug() << "TESTING: DELETE"; kDebug() << "Creating INBOX/TestFolder:"; CreateJob *create = new CreateJob(session); create->setMailBox("INBOX/TestFolder"); create->exec(); kDebug() << "Listing with name TestFolder before DELETE:"; listFolders(session, true, "TestFolder"); kDebug() << "Deleting INBOX/TestFolder"; DeleteJob *deletejob = new DeleteJob(session); deletejob->setMailBox("INBOX/TestFolder"); deletejob->exec(); kDebug() << "Listing with name TestFolder after DELETE:"; listFolders(session, true, "TestFolder"); } int main( int argc, char **argv ) { KAboutData about("TestImapServer", 0, ki18n("TestImapServer"), "version"); KComponentData cData(&about); if (argc < 4) { kError() << "Not enough parameters, expecting: "; } QString server = QString::fromLocal8Bit(argv[1]); int port = 143; if ( server.count( ':' ) == 1 ) { port = server.split( ':' ).last().toInt(); server = server.split( ':' ).first(); } QString user = QString::fromLocal8Bit(argv[2]); QString password = QString::fromLocal8Bit(argv[3]); kDebug() << "Querying:" << server << port << user << password; qDebug(); QApplication app(argc, argv); Session session(server, port); UiProxy *proxy = new UiProxy(); session.setUiProxy(proxy); kDebug() << "Logging in..."; LoginJob *login = new LoginJob(&session); login->setEncryptionMode(LoginJob::TlsV1); login->setAuthenticationMode(LoginJob::Plain); login->setUserName(user); login->setPassword(password); login->exec(); qDebug(); if (login->encryptionMode() == LoginJob::Unencrypted) { kDebug() << "Encrypted login not possible, try to log in without encryption"; login = new LoginJob(&session); login->setUserName(user); login->setPassword(password); login->exec(); Q_ASSERT_X(login->error()==0, "LoginJob", login->errorString().toLocal8Bit()); Q_ASSERT(session.state()==Session::Authenticated); qDebug(); } kDebug() << "Asking for capabilities:"; CapabilitiesJob *capabilities = new CapabilitiesJob(&session); capabilities->exec(); Q_ASSERT_X(capabilities->error()==0, "CapabilitiesJob", capabilities->errorString().toLocal8Bit()); Q_ASSERT(session.state()==Session::Authenticated); kDebug() << capabilities->capabilities(); qDebug(); kDebug() << "Listing mailboxes:"; listFolders(&session); Q_ASSERT(session.state()==Session::Authenticated); kDebug() << "Selecting INBOX:"; SelectJob *select = new SelectJob(&session); select->setMailBox("INBOX"); select->exec(); Q_ASSERT_X(select->error()==0, "SelectJob", select->errorString().toLocal8Bit()); Q_ASSERT(session.state()==Session::Selected); kDebug() << "Flags:" << select->flags(); kDebug() << "Permanent flags:" << select->permanentFlags(); kDebug() << "Total Number of Messages:" << select->messageCount(); kDebug() << "Number of recent Messages:" << select->recentCount(); kDebug() << "First Unseen Message Index:" << select->firstUnseenIndex(); kDebug() << "UID validity:" << select->uidValidity(); kDebug() << "Next UID:" << select->nextUid(); qDebug(); kDebug() << "Fetching first 3 messages headers:"; FetchJob *fetch = new FetchJob(&session); FetchJob::FetchScope scope; fetch->setSequenceSet(ImapSet(1, 3)); scope.parts.clear(); scope.mode = FetchJob::FetchScope::Headers; fetch->setScope(scope); fetch->exec(); Q_ASSERT_X(fetch->error()==0, "FetchJob", fetch->errorString().toLocal8Bit()); Q_ASSERT(session.state()==Session::Selected); QMap messages = fetch->messages(); foreach (qint64 id, messages.keys()) { kDebug() << "* Message" << id << "(" << fetch->sizes()[id] << "bytes )"; kDebug() << " From :" << messages[id]->from()->asUnicodeString(); kDebug() << " To :" << messages[id]->to()->asUnicodeString(); kDebug() << " Date :" << messages[id]->date()->asUnicodeString(); kDebug() << " Subject :" << messages[id]->subject()->asUnicodeString(); kDebug() << " Message-ID:" << messages[id]->messageID()->asUnicodeString(); } qDebug(); kDebug() << "Fetching first 3 messages flags:"; fetch = new FetchJob(&session); fetch->setSequenceSet(ImapSet(1, 3)); scope.parts.clear(); scope.mode = FetchJob::FetchScope::Flags; fetch->setScope(scope); fetch->exec(); Q_ASSERT_X(fetch->error()==0, "FetchJob", fetch->errorString().toLocal8Bit()); Q_ASSERT(session.state()==Session::Selected); QMap flags = fetch->flags(); foreach (qint64 id, flags.keys()) { kDebug() << "* Message" << id << "flags:" << flags[id]; } qDebug(); kDebug() << "Fetching first message structure:"; fetch = new FetchJob(&session); fetch->setSequenceSet(ImapSet(1)); scope.parts.clear(); scope.mode = FetchJob::FetchScope::Structure; fetch->setScope(scope); fetch->exec(); Q_ASSERT_X(fetch->error()==0, "FetchJob", fetch->errorString().toLocal8Bit()); Q_ASSERT(session.state()==Session::Selected); MessagePtr message = fetch->messages()[1]; dumpContentHelper(message.get()); qDebug(); kDebug() << "Fetching first message second part headers:"; fetch = new FetchJob(&session); fetch->setSequenceSet(ImapSet(1)); scope.parts.clear(); scope.parts << "2"; scope.mode = FetchJob::FetchScope::Headers; fetch->setScope(scope); fetch->exec(); Q_ASSERT_X(fetch->error()==0, "FetchJob", fetch->errorString().toLocal8Bit()); Q_ASSERT(session.state()==Session::Selected); QMap allParts = fetch->parts(); foreach (qint64 id, allParts.keys()) { kDebug() << "* Message" << id << "parts headers"; MessageParts parts = allParts[id]; foreach (const QByteArray &partId, parts.keys()) { kDebug() << " ** Part" << partId; kDebug() << " Name :" << parts[partId]->contentType()->name(); kDebug() << " Mimetype :" << parts[partId]->contentType()->mimeType(); kDebug() << " Description:" << parts[partId]->contentDescription()->asUnicodeString().simplified(); } } qDebug(); kDebug() << "Fetching first message second part content:"; fetch = new FetchJob(&session); fetch->setSequenceSet(ImapSet(1)); scope.parts.clear(); scope.parts << "2"; scope.mode = FetchJob::FetchScope::Content; fetch->setScope(scope); fetch->exec(); Q_ASSERT_X(fetch->error()==0, "FetchJob", fetch->errorString().toLocal8Bit()); Q_ASSERT(session.state()==Session::Selected); allParts = fetch->parts(); foreach (int id, allParts.keys()) { MessageParts parts = allParts[id]; foreach (const QByteArray &partId, parts.keys()) { kDebug() << "* Message" << id << "part" << partId << "content:"; kDebug() << parts[partId]->body(); } } qDebug(); testDelete(&session); testSubscribe(&session); testRename(&session); testAppendAndStore(&session); testAcl(&session, user); testMetaData(&session); kDebug() << "Expunge INBOX:"; ExpungeJob *expunge = new ExpungeJob(&session); expunge->exec(); kDebug() << "Closing INBOX:"; CloseJob *close = new CloseJob(&session); close->exec(); Q_ASSERT(session.state()==Session::Authenticated); qDebug(); kDebug() << "Logging out..."; LogoutJob *logout = new LogoutJob(&session); logout->exec(); Q_ASSERT_X(logout->error()==0, "LogoutJob", logout->errorString().toLocal8Bit()); Q_ASSERT(session.state()==Session::Disconnected); return 0; } diff --git a/kpimutils/linklocator.cpp b/kpimutils/linklocator.cpp index 22cb4fb73..86496ee27 100644 --- a/kpimutils/linklocator.cpp +++ b/kpimutils/linklocator.cpp @@ -1,430 +1,434 @@ /* Copyright (c) 2002 Dave Corrie 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 the LinkLocator class. @brief Identifies URLs and email addresses embedded in plaintext. @author Dave Corrie \ */ #include "linklocator.h" #include #include #include #include #include #if KDE_IS_VERSION( 4, 0, 95 ) #include #endif #include #include #include #include #include using namespace KPIMUtils; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KPIMUtils::LinkLocator::Private { public: int mMaxUrlLen; int mMaxAddressLen; }; //@endcond #if KDE_IS_VERSION( 4, 0, 95 ) // Use a static for this as calls to the KEmoticons constructor are expensive. K_GLOBAL_STATIC( KEmoticons, sEmoticons ) #endif LinkLocator::LinkLocator( const QString &text, int pos ) : mText( text ), mPos( pos ), d( new KPIMUtils::LinkLocator::Private ) { d->mMaxUrlLen = 4096; d->mMaxAddressLen = 255; // If you change either of the above values for maxUrlLen or // maxAddressLen, then please also update the documentation for // setMaxUrlLen()/setMaxAddressLen() in the header file AND the // default values used for the maxUrlLen/maxAddressLen parameters // of convertToHtml(). } LinkLocator::~LinkLocator() { delete d; } void LinkLocator::setMaxUrlLen( int length ) { d->mMaxUrlLen = length; } int LinkLocator::maxUrlLen() const { return d->mMaxUrlLen; } void LinkLocator::setMaxAddressLen( int length ) { d->mMaxAddressLen = length; } int LinkLocator::maxAddressLen() const { return d->mMaxAddressLen; } QString LinkLocator::getUrl() { QString url; if ( atUrl() ) { - // handle cases like this: http://foobar.org/ + // for reference: rfc1738: + // Thus, only alphanumerics, the special characters "$-_.+!*'(),", and + // reserved characters used for their reserved purposes may be used + // unencoded within a URL. + // NOTE: this implementation is not RFC conforming int start = mPos; while ( mPos < (int)mText.length() && mText[mPos] > ' ' && mText[mPos] != '"' && - QString( "<>()[]" ).indexOf( mText[mPos] ) == -1 ) { + QString( "<>[]" ).indexOf( mText[mPos] ) == -1 ) { ++mPos; } - /* some URLs really end with: # / & - _ */ + // some URLs really end with: # / & - _ const QString allowedSpecialChars = QString( "#/&-_" ); while ( mPos > start && mText[mPos-1].isPunct() && allowedSpecialChars.indexOf( mText[mPos-1] ) == -1 ) { --mPos; } url = mText.mid( start, mPos - start ); if ( isEmptyUrl(url) || mPos - start > maxUrlLen() ) { mPos = start; url = ""; } else { --mPos; } } return url; } // keep this in sync with KMMainWin::slotUrlClicked() bool LinkLocator::atUrl() const { // the following characters are allowed in a dot-atom (RFC 2822): // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ const QString allowedSpecialChars = QString( ".!#$%&'*+-/=?^_`{|}~" ); // the character directly before the URL must not be a letter, a number or // any other character allowed in a dot-atom (RFC 2822). if ( ( mPos > 0 ) && ( mText[mPos-1].isLetterOrNumber() || ( allowedSpecialChars.indexOf( mText[mPos-1] ) != -1 ) ) ) { return false; } QChar ch = mText[mPos]; return ( ch == 'h' && ( mText.mid( mPos, 7 ) == "http://" || mText.mid( mPos, 8 ) == "https://" ) ) || ( ch == 'v' && mText.mid( mPos, 6 ) == "vnc://" ) || ( ch == 'f' && ( mText.mid( mPos, 7 ) == "fish://" || mText.mid( mPos, 6 ) == "ftp://" || mText.mid( mPos, 7 ) == "ftps://" ) ) || ( ch == 's' && ( mText.mid( mPos, 7 ) == "sftp://" || mText.mid( mPos, 6 ) == "smb://" ) ) || ( ch == 'm' && mText.mid( mPos, 7 ) == "mailto:" ) || ( ch == 'w' && mText.mid( mPos, 4 ) == "www." ) || ( ch == 'f' && ( mText.mid( mPos, 4 ) == "ftp." || mText.mid( mPos, 7 ) == "file://" ) ) || ( ch == 'n' && mText.mid( mPos, 5 ) == "news:" ); } bool LinkLocator::isEmptyUrl( const QString &url ) const { return url.isEmpty() || url == "http://" || url == "https://" || url == "fish://" || url == "ftp://" || url == "ftps://" || url == "sftp://" || url == "smb://" || url == "vnc://" || url == "mailto" || url == "www" || url == "ftp" || url == "news" || url == "news://"; } QString LinkLocator::getEmailAddress() { QString address; if ( mText[mPos] == '@' ) { // the following characters are allowed in a dot-atom (RFC 2822): // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ const QString allowedSpecialChars = QString( ".!#$%&'*+-/=?^_`{|}~" ); // determine the local part of the email address int start = mPos - 1; while ( start >= 0 && mText[start].unicode() < 128 && ( mText[start].isLetterOrNumber() || mText[start] == '@' || // allow @ to find invalid email addresses allowedSpecialChars.indexOf( mText[start] ) != -1 ) ) { if ( mText[start] == '@' ) { return QString(); // local part contains '@' -> no email address } --start; } ++start; // we assume that an email address starts with a letter or a digit while ( ( start < mPos ) && !mText[start].isLetterOrNumber() ) { ++start; } if ( start == mPos ) { return QString(); // local part is empty -> no email address } // determine the domain part of the email address int dotPos = INT_MAX; int end = mPos + 1; while ( end < (int)mText.length() && ( mText[end].isLetterOrNumber() || mText[end] == '@' || // allow @ to find invalid email addresses mText[end] == '.' || mText[end] == '-' ) ) { if ( mText[end] == '@' ) { return QString(); // domain part contains '@' -> no email address } if ( mText[end] == '.' ) { dotPos = qMin( dotPos, end ); // remember index of first dot in domain } ++end; } // we assume that an email address ends with a letter or a digit while ( ( end > mPos ) && !mText[end - 1].isLetterOrNumber() ) { --end; } if ( end == mPos ) { return QString(); // domain part is empty -> no email address } if ( dotPos >= end ) { return QString(); // domain part doesn't contain a dot } if ( end - start > maxAddressLen() ) { return QString(); // too long -> most likely no email address } address = mText.mid( start, end - start ); mPos = end - 1; } return address; } QString LinkLocator::convertToHtml( const QString &plainText, int flags, int maxUrlLen, int maxAddressLen ) { LinkLocator locator( plainText ); locator.setMaxUrlLen( maxUrlLen ); locator.setMaxAddressLen( maxAddressLen ); QString str; QString result( (QChar*)0, (int)locator.mText.length() * 2 ); QChar ch; int x; bool startOfLine = true; QString emoticon; for ( locator.mPos = 0, x = 0; locator.mPos < (int)locator.mText.length(); locator.mPos++, x++ ) { ch = locator.mText[locator.mPos]; if ( flags & PreserveSpaces ) { if ( ch == ' ' ) { if ( startOfLine ) { result += " "; locator.mPos++, x++; startOfLine = false; } while ( locator.mText[locator.mPos] == ' ' ) { result += ' '; locator.mPos++, x++; if ( locator.mText[locator.mPos] == ' ' ) { result += " "; locator.mPos++, x++; } } locator.mPos--, x--; continue; } else if ( ch == '\t' ) { do { result += " "; x++; } while ( ( x & 7 ) != 0 ); x--; startOfLine = false; continue; } } if ( ch == '\n' ) { result += "
      \n"; // Keep the \n, so apps can figure out the quoting levels correctly. startOfLine = true; x = -1; continue; } startOfLine = false; if ( ch == '&' ) { result += "&"; } else if ( ch == '"' ) { result += """; } else if ( ch == '<' ) { result += "<"; } else if ( ch == '>' ) { result += ">"; } else { const int start = locator.mPos; if ( !( flags & IgnoreUrls ) ) { str = locator.getUrl(); if ( !str.isEmpty() ) { QString hyperlink; if ( str.left( 4 ) == "www." ) { hyperlink = "http://" + str; } else if ( str.left( 4 ) == "ftp." ) { hyperlink = "ftp://" + str; } else { hyperlink = str; } str = str.replace( '&', "&" ); result += "" + str + ""; x += locator.mPos - start; continue; } str = locator.getEmailAddress(); if ( !str.isEmpty() ) { // len is the length of the local part int len = str.indexOf( '@' ); QString localPart = str.left( len ); // remove the local part from the result (as '&'s have been expanded to // & we have to take care of the 4 additional characters per '&') result.truncate( result.length() - len - ( localPart.count( '&' ) * 4 ) ); x -= len; result += "" + str + ""; x += str.length() - 1; continue; } } if ( flags & HighlightText ) { str = locator.highlightedText(); if ( !str.isEmpty() ) { result += str; x += locator.mPos - start; continue; } } result += ch; } } #if KDE_IS_VERSION( 4, 0, 95 ) if ( flags & ReplaceSmileys ) { QStringList exclude; exclude << "(c)" << "(C)" << ">:-(" << ">:(" << "(B)" << "(b)" << "(P)" << "(p)"; exclude << "(O)" << "(o)" << "(D)" << "(d)" << "(E)" << "(e)" << "(K)" << "(k)"; exclude << "(I)" << "(i)" << "(L)" << "(l)" << "(8)" << "(T)" << "(t)" << "(G)"; exclude << "(g)" << "(F)" << "(f)" << "(H)"; exclude << "8)" << "(N)" << "(n)" << "(Y)" << "(y)" << "(U)" << "(u)" << "(W)" << "(w)"; static QString cachedEmoticonsThemeName; if ( cachedEmoticonsThemeName.isEmpty() ) { cachedEmoticonsThemeName = KEmoticons::currentThemeName(); } result = sEmoticons->theme( cachedEmoticonsThemeName ).parseEmoticons( result, KEmoticonsTheme::StrictParse | KEmoticonsTheme::SkipHTML, exclude ); } #endif return result; } QString LinkLocator::pngToDataUrl( const QString &iconPath ) { if ( iconPath.isEmpty() ) { return QString(); } QFile pngFile( iconPath ); if ( !pngFile.open( QIODevice::ReadOnly | QIODevice::Unbuffered ) ) { return QString(); } QByteArray ba = pngFile.readAll(); pngFile.close(); return QString::fromLatin1( "data:image/png;base64,%1" ).arg( ba.toBase64().constData() ); } QString LinkLocator::highlightedText() { // formating symbols must be prepended with a whitespace if ( ( mPos > 0 ) && !mText[mPos-1].isSpace() ) { return QString(); } const QChar ch = mText[mPos]; if ( ch != '/' && ch != '*' && ch != '_' ) { return QString(); } QRegExp re = QRegExp( QString( "\\%1([0-9A-Za-z]+)\\%2" ).arg( ch ).arg( ch ) ); if ( re.indexIn( mText, mPos ) == mPos ) { int length = re.matchedLength(); // there must be a whitespace after the closing formating symbol if ( mPos + length < mText.length() && !mText[mPos + length].isSpace() ) { return QString(); } mPos += length - 1; switch ( ch.toLatin1() ) { case '*': return "" + re.cap( 1 ) + ""; case '_': return "" + re.cap( 1 ) + ""; case '/': return "" + re.cap( 1 ) + ""; } } return QString(); } diff --git a/kpimutils/tests/testlinklocator.cpp b/kpimutils/tests/testlinklocator.cpp index 46489f69d..258a85fb2 100644 --- a/kpimutils/tests/testlinklocator.cpp +++ b/kpimutils/tests/testlinklocator.cpp @@ -1,106 +1,196 @@ /* This file is part of the kpimutils library. Copyright (C) 2005 Ingo Kloecker Copyright (C) 2007 Allen Winter 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 #include "testlinklocator.h" #include "testlinklocator.moc" QTEST_KDEMAIN( LinkLocatorTest, NoGUI ) #include "kpimutils/linklocator.h" using namespace KPIMUtils; void LinkLocatorTest::testGetEmailAddress() { // empty input const QString emptyQString; LinkLocator ll1( emptyQString, 0 ); QVERIFY( ll1.getEmailAddress().isEmpty() ); // no '@' at scan position LinkLocator ll2( "foo@bar.baz", 0 ); QVERIFY( ll2.getEmailAddress().isEmpty() ); // '@' in local part LinkLocator ll3( "foo@bar@bar.baz", 7 ); QVERIFY( ll3.getEmailAddress().isEmpty() ); // empty local part LinkLocator ll4( "@bar.baz", 0 ); QVERIFY( ll4.getEmailAddress().isEmpty() ); LinkLocator ll5( ".@bar.baz", 1 ); QVERIFY( ll5.getEmailAddress().isEmpty() ); LinkLocator ll6( " @bar.baz", 1 ); QVERIFY( ll6.getEmailAddress().isEmpty() ); LinkLocator ll7( ".!#$%&'*+-/=?^_`{|}~@bar.baz", strlen( ".!#$%&'*+-/=?^_`{|}~" ) ); QVERIFY( ll7.getEmailAddress().isEmpty() ); // allowed special chars in local part of address LinkLocator ll8( "a.!#$%&'*+-/=?^_`{|}~@bar.baz", strlen( "a.!#$%&'*+-/=?^_`{|}~" ) ); QVERIFY( ll8.getEmailAddress() == "a.!#$%&'*+-/=?^_`{|}~@bar.baz" ); // '@' in domain part LinkLocator ll9 ( "foo@bar@bar.baz", 3 ); QVERIFY( ll9.getEmailAddress().isEmpty() ); // domain part without dot LinkLocator lla( "foo@bar", 3 ); QVERIFY( lla.getEmailAddress().isEmpty() ); LinkLocator llb( "foo@bar.", 3 ); QVERIFY( llb.getEmailAddress().isEmpty() ); LinkLocator llc( ".foo@bar", 4 ); QVERIFY( llc.getEmailAddress().isEmpty() ); LinkLocator lld( "foo@bar ", 3 ); QVERIFY( lld.getEmailAddress().isEmpty() ); LinkLocator lle( " foo@bar", 4 ); QVERIFY( lle.getEmailAddress().isEmpty() ); LinkLocator llf( "foo@bar-bar", 3 ); QVERIFY( llf.getEmailAddress().isEmpty() ); // empty domain part LinkLocator llg( "foo@", 3 ); QVERIFY( llg.getEmailAddress().isEmpty() ); LinkLocator llh( "foo@.", 3 ); QVERIFY( llh.getEmailAddress().isEmpty() ); LinkLocator lli( "foo@-", 3 ); QVERIFY( lli.getEmailAddress().isEmpty() ); // simple address LinkLocator llj( "foo@bar.baz", 3 ); QVERIFY( llj.getEmailAddress() == "foo@bar.baz" ); LinkLocator llk( "foo@bar.baz.", 3 ); QVERIFY( llk.getEmailAddress() == "foo@bar.baz" ); LinkLocator lll( ".foo@bar.baz", 4 ); QVERIFY( lll.getEmailAddress() == "foo@bar.baz" ); LinkLocator llm( "foo@bar.baz-", 3 ); QVERIFY( llm.getEmailAddress() == "foo@bar.baz" ); LinkLocator lln( "-foo@bar.baz", 4 ); QVERIFY( lln.getEmailAddress() == "foo@bar.baz" ); LinkLocator llo( "foo@bar.baz ", 3 ); QVERIFY( llo.getEmailAddress() == "foo@bar.baz" ); LinkLocator llp( " foo@bar.baz", 4 ); QVERIFY( llp.getEmailAddress() == "foo@bar.baz" ); LinkLocator llq( "foo@bar-bar.baz", 3 ); QVERIFY( llq.getEmailAddress() == "foo@bar-bar.baz" ); } +void LinkLocatorTest::testGetUrl() +{ + QStringList brackets; + brackets << "" << ""; // no brackets + brackets << "(" << ")"; + brackets << "<" << ">"; + brackets << "[" << "]"; + brackets << "" << ""; + + for (int i = 0; i < brackets.count(); i += 2) + testGetUrl2(brackets[i], brackets[i+1]); +} + +void LinkLocatorTest::testGetUrl2(const QString &left, const QString &right) +{ + QStringList schemas; + schemas << "http://"; + schemas << "https://"; + schemas << "vnc://"; + schemas << "fish://"; + schemas << "ftp://"; + schemas << "ftps://"; + schemas << "sftp://"; + schemas << "smb://"; + schemas << "file://"; + + QStringList urls; + urls << "www.kde.org"; + urls << "user@www.kde.org"; + urls << "user:pass@www.kde.org"; + urls << "user:pass@www.kde.org:1234"; + urls << "user:pass@www.kde.org:1234/sub/path"; + urls << "user:pass@www.kde.org:1234/sub/path?a=1"; + urls << "user:pass@www.kde.org:1234/sub/path?a=1#anchor"; + urls << "user:pass@www.kde.org:1234/sub/path/special(123)?a=1#anchor"; + urls << "user:pass@www.kde.org:1234/sub/path:with:colon/special(123)?a=1#anchor"; + + foreach (QString schema, schemas) + { + foreach (QString url, urls) + { + QString test(left + schema + url + right); + LinkLocator ll(test, left.length()); + QString gotUrl = ll.getUrl(); + + bool ok = ( gotUrl == (schema + url) ); + qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << (schema + url); + QVERIFY2( ok, qPrintable(test) ); + } + } + + QStringList urlsWithoutSchema; + urlsWithoutSchema << ".kde.org"; + urlsWithoutSchema << ".kde.org:1234/sub/path"; + urlsWithoutSchema << ".kde.org:1234/sub/path?a=1"; + urlsWithoutSchema << ".kde.org:1234/sub/path?a=1#anchor"; + urlsWithoutSchema << ".kde.org:1234/sub/path/special(123)?a=1#anchor"; + urlsWithoutSchema << ".kde.org:1234/sub/path:with:colon/special(123)?a=1#anchor"; + + QStringList starts; + starts << "www" << "ftp" << "news:www"; + + foreach (QString start, starts) + { + foreach (QString url, urlsWithoutSchema) + { + QString test(left + start + url + right); + LinkLocator ll(test, left.length()); + QString gotUrl = ll.getUrl(); + + bool ok = ( gotUrl == (start + url) ); + qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << (start + url); + QVERIFY2( ok, qPrintable(test) ); + } + } + + // mailto + { + QString addr = "mailto:test@kde.org"; + QString test(left + addr + right); + LinkLocator ll(test, left.length()); + + QString gotUrl = ll.getUrl(); + + bool ok = ( gotUrl == addr ); + qDebug() << "check:" << (ok ? "OK" : "NOK") << test << "=>" << addr; + QVERIFY2( ok, qPrintable(test) ); + } +} diff --git a/kpimutils/tests/testlinklocator.h b/kpimutils/tests/testlinklocator.h index fa68bfd87..65544eeab 100644 --- a/kpimutils/tests/testlinklocator.h +++ b/kpimutils/tests/testlinklocator.h @@ -1,34 +1,38 @@ /* This file is part of the kpimutils library. Copyright (c) 2007 Allen Winter 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 TESTLINKLOCATOR_H #define TESTLINKLOCATOR_H #include class LinkLocatorTest : public QObject { Q_OBJECT private Q_SLOTS: void testGetEmailAddress(); + void testGetUrl(); + + private: + void testGetUrl2(const QString &left, const QString &right); }; #endif diff --git a/kresources/factory.cpp b/kresources/factory.cpp index bb1b9a219..aee1250f8 100644 --- a/kresources/factory.cpp +++ b/kresources/factory.cpp @@ -1,254 +1,253 @@ /* This file is part of libkresources. Copyright (c) 2002 Tobias Koenig Copyright (c) 2002 Jan-Pascal van Best Copyright (c) 2003 Cornelius Schumacher 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 KDE resource framework and defines the Factory class. @brief A class for loading resource plugins. @author Tobias Koenig @author Jan-Pascal van Best @author Cornelius Schumacher */ #include "factory.h" #include #include #include #include #include #include #include #include #include "resource.h" using namespace KRES; class Factory::Private { public: Resource *resourceInternal ( const QString &type, const KConfigGroup *group ); QString mResourceFamily; QMap mTypeMap; }; class FactoryMap : public QMap { -public: - + public: ~FactoryMap() { qDeleteAll(*this); } }; K_GLOBAL_STATIC( FactoryMap, mSelves ) Factory *Factory::self( const QString &resourceFamily ) { kDebug(); Factory *factory = 0; factory = mSelves->value( resourceFamily, 0 ); if ( !factory ) { factory = new Factory( resourceFamily ); mSelves->insert( resourceFamily, factory ); // Akonadi migration KConfig config( "kres-migratorrc" ); KConfigGroup migrationCfg( &config, "Migration" ); const bool enabled = migrationCfg.readEntry( "Enabled", false ); const bool setupClientBrige = migrationCfg.readEntry( "SetupClientBridge", true ); const int currentVersion = migrationCfg.readEntry( "Version-" + resourceFamily, 0 ); const int targetVersion = migrationCfg.readEntry( "TargetVersion", 0 ); if ( enabled && currentVersion < targetVersion ) { kDebug() << "Performing Akonadi migration. Good luck!"; KProcess proc; QStringList args = QStringList() << "--interactive-on-change" << "--type" << resourceFamily; if ( !setupClientBrige ) { args << "--omit-client-bridge"; } proc.setProgram( "kres-migrator", args ); proc.start(); bool result = proc.waitForStarted(); if ( result ) { result = proc.waitForFinished(); } if ( result && proc.exitCode() == 0 ) { kDebug() << "Akonadi migration has been successful"; migrationCfg.writeEntry( "Version-" + resourceFamily, targetVersion ); migrationCfg.sync(); } else if ( !result || proc.exitCode() != 1 ) { // exit code 1 means it is already running, so we are probably called by a migrator instance kError() << "Akonadi migration failed!"; kError() << "command was: " << proc.program(); kError() << "exit code: " << proc.exitCode(); kError() << "stdout: " << proc.readAllStandardOutput(); kError() << "stderr: " << proc.readAllStandardError(); } } } return factory; } Factory::Factory( const QString &resourceFamily ) : d( new KRES::Factory::Private ) { d->mResourceFamily = resourceFamily; reloadConfig(); } void Factory::reloadConfig() { d->mTypeMap.clear(); const KService::List plugins = KServiceTypeTrader::self()->query( "KResources/Plugin", QString( "[X-KDE-ResourceFamily] == '%1'" ).arg( d->mResourceFamily ) ); KService::List::ConstIterator it; for ( it = plugins.begin(); it != plugins.end(); ++it ) { const QVariant type = (*it)->property( "X-KDE-ResourceType" ); if ( !type.toString().isEmpty() ) { d->mTypeMap.insert( type.toString(), *it ); } } } Factory::~Factory() { delete d; } QStringList Factory::typeNames() const { return d->mTypeMap.keys(); } ConfigWidget *Factory::configWidget( const QString &type, QWidget *parent ) { if ( type.isEmpty() || !d->mTypeMap.contains( type ) ) { return 0; } KService::Ptr ptr = d->mTypeMap[ type ]; KPluginLoader loader( ptr->library() ); KPluginFactory *factory = loader.factory(); if ( !factory ) { kDebug() << "Factory creation failed: " << loader.errorString(); return 0; } PluginFactoryBase *pluginFactory = static_cast( factory ); if ( !pluginFactory ) { kDebug() << "no plugin factory."; return 0; } ConfigWidget *wdg = pluginFactory->configWidget( parent ); if ( !wdg ) { kDebug() << "'" << ptr->library() << "' doesn't provide a ConfigWidget"; return 0; } return wdg; } QString Factory::typeName( const QString &type ) const { if ( type.isEmpty() || !d->mTypeMap.contains( type ) ) { return QString(); } KService::Ptr ptr = d->mTypeMap[ type ]; return ptr->name(); } QString Factory::typeDescription( const QString &type ) const { if ( type.isEmpty() || !d->mTypeMap.contains( type ) ) { return QString(); } KService::Ptr ptr = d->mTypeMap[ type ]; return ptr->comment(); } Resource *Factory::Private::resourceInternal( const QString &type, const KConfigGroup *group ) { kDebug() << "(" << type << ", config )"; if ( type.isEmpty() || !mTypeMap.contains( type ) ) { kDebug() << "no such type" << type; return 0; } KService::Ptr ptr = mTypeMap[ type ]; KPluginLoader loader( ptr->library() ); KPluginFactory *factory = loader.factory(); if ( !factory ) { kDebug() << "Factory creation failed" << loader.errorString(); return 0; } PluginFactoryBase *pluginFactory = static_cast( factory ); if ( !pluginFactory ) { kDebug() << "no plugin factory."; return 0; } Resource *resource; if ( group ) { resource = pluginFactory->resource( *group ); } else { resource = pluginFactory->resource(); } if ( !resource ) { kDebug() << "'" << ptr->library() << "' is not a" << mResourceFamily << "plugin."; return 0; } resource->setType( type ); return resource; } Resource *Factory::resource( const QString &type, const KConfigGroup &group ) { return d->resourceInternal( type, &group ); } Resource *Factory::resource( const QString &type ) { return d->resourceInternal( type, 0 ); }