diff --git a/akonadi/selftestdialog.cpp b/akonadi/selftestdialog.cpp index 62d697e24..af5281d2d 100644 --- a/akonadi/selftestdialog.cpp +++ b/akonadi/selftestdialog.cpp @@ -1,569 +1,602 @@ /* Copyright (c) 2008 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "selftestdialog_p.h" #include "agentmanager.h" #include "session_p.h" #include "servermanager_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include // @cond PRIVATE #define AKONADI_CONTROL_SERVICE QLatin1String("org.freedesktop.Akonadi.Control") #define AKONADI_SERVER_SERVICE QLatin1String("org.freedesktop.Akonadi") using namespace Akonadi; static QString makeLink( const QString &file ) { return QString::fromLatin1( "%2" ).arg( file, file ); } enum SelfTestRole { ResultTypeRole = Qt::UserRole, FileIncludeRole, ListDirectoryRole, EnvVarRole, SummaryRole, DetailsRole }; SelfTestDialog::SelfTestDialog(QWidget * parent) : KDialog( parent ) { setCaption( i18n( "Akonadi Server Self-Test" ) ); setButtons( Close | User1 | User2 ); setButtonText( User1, i18n( "Save Report..." ) ); setButtonIcon( User1, KIcon( QString::fromLatin1("document-save") ) ); setButtonText( User2, i18n( "Copy Report to Clipboard" ) ); setButtonIcon( User2, KIcon( QString::fromLatin1("edit-copy") ) ); showButtonSeparator( true ); ui.setupUi( mainWidget() ); mTestModel = new QStandardItemModel( this ); ui.testView->setModel( mTestModel ); connect( ui.testView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(selectionChanged(QModelIndex)) ); connect( ui.detailsLabel, SIGNAL(linkActivated(QString)), SLOT(linkActivated(QString)) ); connect( this, SIGNAL(user1Clicked()), SLOT(saveReport()) ); connect( this, SIGNAL(user2Clicked()), SLOT(copyReport()) ); runTests(); } void SelfTestDialog::hideIntroduction() { ui.introductionLabel->hide(); } QStandardItem* SelfTestDialog::report( ResultType type, const KLocalizedString & summary, const KLocalizedString & details) { QStandardItem *item = new QStandardItem( summary.toString() ); switch ( type ) { case Skip: item->setIcon( KIcon( QString::fromLatin1("dialog-ok") ) ); break; case Success: item->setIcon( KIcon( QString::fromLatin1("dialog-ok-apply") ) ); break; case Warning: item->setIcon( KIcon( QString::fromLatin1("dialog-warning") ) ); break; case Error: default: item->setIcon( KIcon( QString::fromLatin1("dialog-error") ) ); } item->setEditable( false ); item->setWhatsThis( details.toString() ); item->setData( type, ResultTypeRole ); item->setData( summary.toString( 0 ), SummaryRole ); item->setData( details.toString( 0 ), DetailsRole ); mTestModel->appendRow( item ); return item; } void SelfTestDialog::selectionChanged(const QModelIndex &index ) { if ( index.isValid() ) { ui.detailsLabel->setText( index.data( Qt::WhatsThisRole ).toString() ); ui.detailsGroup->setEnabled( true ); } else { ui.detailsLabel->setText( QString() ); ui.detailsGroup->setEnabled( false ); } } void SelfTestDialog::runTests() { + const QString driver = serverSetting( QLatin1String("General"), "Driver", QLatin1String("QMYSQL") ).toString(); testSQLDriver(); - testMySQLServer(); - testMySQLServerLog(); - testMySQLServerConfig(); + if (driver == QLatin1String( "QPSQL" )) { + testPSQLServer(); + } + else { + testMySQLServer(); + testMySQLServerLog(); + testMySQLServerConfig(); + } testAkonadiCtl(); testServerStatus(); testProtocolVersion(); testResources(); testServerLog(); testControlLog(); } QVariant SelfTestDialog::serverSetting(const QString & group, const char *key, const QVariant &def ) const { const QString serverConfigFile = XdgBaseDirs::akonadiServerConfigFile( XdgBaseDirs::ReadWrite ); QSettings settings( serverConfigFile, QSettings::IniFormat ); settings.beginGroup( group ); return settings.value( QString::fromLatin1(key), def ); } bool SelfTestDialog::useStandaloneMysqlServer() const { const QString driver = serverSetting( QLatin1String("General"), "Driver", QLatin1String("QMYSQL") ).toString(); if ( driver != QLatin1String( "QMYSQL" ) ) return false; const bool startServer = serverSetting( driver, "StartServer", true ).toBool(); if ( !startServer ) return false; return true; } bool SelfTestDialog::runProcess(const QString & app, const QStringList & args, QString & result) const { QProcess proc; proc.start( app, args ); const bool rv = proc.waitForFinished(); result.clear(); result += QString::fromLocal8Bit( proc.readAllStandardError() ); result += QString::fromLocal8Bit( proc.readAllStandardOutput() ); return rv; } void SelfTestDialog::testSQLDriver() { const QString driver = serverSetting( QLatin1String("General"), "Driver", QLatin1String("QMYSQL") ).toString(); const QStringList availableDrivers = QSqlDatabase::drivers(); const KLocalizedString details = ki18n( "The QtSQL driver '%1' is required by your current Akonadi server configuration.\n" "The following drivers are installed: %2.\n" "Make sure the required driver is installed." ) .subs( driver ) .subs( availableDrivers.join( QLatin1String(", ") ) ); QStandardItem *item = 0; if ( availableDrivers.contains( driver ) ) item = report( Success, ki18n( "Database driver found." ), details ); else item = report( Error, ki18n( "Database driver not found." ), details ); item->setData( XdgBaseDirs::akonadiServerConfigFile( XdgBaseDirs::ReadWrite ), FileIncludeRole ); } void SelfTestDialog::testMySQLServer() { if ( !useStandaloneMysqlServer() ) { report( Skip, ki18n( "MySQL server executable not tested." ), ki18n( "The current configuration does not require an internal MySQL server." ) ); return; } const QString driver = serverSetting( QLatin1String("General"), "Driver", QLatin1String("QMYSQL") ).toString(); const QString serverPath = serverSetting( driver, "ServerPath", QLatin1String("") ).toString(); // ### default? const KLocalizedString details = ki18n( "You currently have configured Akonadi to use the MySQL server '%1'.\n" "Make sure you have the MySQL server installed, set the correct path and ensure you have the " "necessary read and execution rights on the server executable. The server executable is typically " "called 'mysqld', its locations varies depending on the distribution." ).subs( serverPath ); QFileInfo info( serverPath ); if ( !info.exists() ) report( Error, ki18n( "MySQL server not found." ), details ); else if ( !info.isReadable() ) report( Error, ki18n( "MySQL server not readable." ), details ); else if ( !info.isExecutable() ) report( Error, ki18n( "MySQL server not executable." ), details ); else if ( !serverPath.contains( QLatin1String("mysqld") ) ) report( Warning, ki18n( "MySQL found with unexpected name." ), details ); else report( Success, ki18n( "MySQL server found." ), details ); // be extra sure and get the server version while we are at it QString result; if ( runProcess( serverPath, QStringList() << QLatin1String( "--version" ), result ) ) { const KLocalizedString details = ki18n( "MySQL server found: %1" ).subs( result ); report( Success, ki18n( "MySQL server is executable." ), details ); } else { const KLocalizedString details = ki18n( "Executing the MySQL server '%1' failed with the following error message: '%2'" ) .subs( serverPath ).subs( result ); report( Error, ki18n( "Executing the MySQL server failed." ), details ); } } void SelfTestDialog::testMySQLServerLog() { if ( !useStandaloneMysqlServer() ) { report( Skip, ki18n( "MySQL server error log not tested." ), ki18n( "The current configuration does not require an internal MySQL server." ) ); return; } const QString logFileName = XdgBaseDirs::saveDir( "data", QLatin1String( "akonadi/db_data" ) ) + QDir::separator() + QString::fromLatin1( "mysql.err" ); const QFileInfo logFileInfo( logFileName ); if ( !logFileInfo.exists() || logFileInfo.size() == 0 ) { report( Success, ki18n( "No current MySQL error log found." ), ki18n( "The MySQL server did not report any errors during this startup into '%1'." ).subs( logFileName ) ); return; } QFile logFile( logFileName ); if ( !logFile.open( QFile::ReadOnly | QFile::Text ) ) { report( Error, ki18n( "MySQL error log not readable." ), ki18n( "A MySQL server error log file was found but is not readable: %1" ).subs( makeLink( logFileName ) ) ); return; } bool warningsFound = false; QStandardItem *item = 0; while ( !logFile.atEnd() ) { const QString line = QString::fromUtf8( logFile.readLine() ); if ( line.contains( QLatin1String( "error" ), Qt::CaseInsensitive ) ) { item = report( Error, ki18n( "MySQL server log contains errors." ), ki18n( "The MySQL server error log file '%1' contains errors." ).subs( makeLink( logFileName ) ) ); item->setData( logFileName, FileIncludeRole ); return; } if ( !warningsFound && line.contains( QLatin1String( "warn" ), Qt::CaseInsensitive ) ) { warningsFound = true; } } if ( warningsFound ) { item = report( Warning, ki18n( "MySQL server log contains warnings." ), ki18n( "The MySQL server log file '%1' contains warnings." ).subs( makeLink( logFileName ) ) ); } else { item = report( Success, ki18n( "MySQL server log contains no errors." ), ki18n( "The MySQL server log file '%1' does not contain any errors or warnings." ) .subs( makeLink( logFileName ) ) ); } item->setData( logFileName, FileIncludeRole ); logFile.close(); } void SelfTestDialog::testMySQLServerConfig() { if ( !useStandaloneMysqlServer() ) { report( Skip, ki18n( "MySQL server configuration not tested." ), ki18n( "The current configuration does not require an internal MySQL server." ) ); return; } QStandardItem *item = 0; const QString globalConfig = XdgBaseDirs::findResourceFile( "config", QLatin1String( "akonadi/mysql-global.conf" ) ); const QFileInfo globalConfigInfo( globalConfig ); if ( !globalConfig.isEmpty() && globalConfigInfo.exists() && globalConfigInfo.isReadable() ) { item = report( Success, ki18n( "MySQL server default configuration found." ), ki18n( "The default configuration for the MySQL server was found and is readable at %1." ) .subs( makeLink( globalConfig ) ) ); item->setData( globalConfig, FileIncludeRole ); } else { report( Error, ki18n( "MySQL server default configuration not found." ), ki18n( "The default configuration for the MySQL server was not found or was not readable. " "Check your Akonadi installation is complete and you have all required access rights." ) ); } const QString localConfig = XdgBaseDirs::findResourceFile( "config", QLatin1String( "akonadi/mysql-local.conf" ) ); const QFileInfo localConfigInfo( localConfig ); if ( localConfig.isEmpty() || !localConfigInfo.exists() ) { report( Skip, ki18n( "MySQL server custom configuration not available." ), ki18n( "The custom configuration for the MySQL server was not found but is optional." ) ); } else if ( localConfigInfo.exists() && localConfigInfo.isReadable() ) { item = report( Success, ki18n( "MySQL server custom configuration found." ), ki18n( "The custom configuration for the MySQL server was found and is readable at %1" ) .subs( makeLink( localConfig ) ) ); item->setData( localConfig, FileIncludeRole ); } else { report( Error, ki18n( "MySQL server custom configuration not readable." ), ki18n( "The custom configuration for the MySQL server was found at %1 but is not readable. " "Check your access rights." ).subs( makeLink( localConfig ) ) ); } const QString actualConfig = XdgBaseDirs::saveDir( "data", QLatin1String( "akonadi" ) ) + QLatin1String("/mysql.conf"); const QFileInfo actualConfigInfo( actualConfig ); if ( actualConfig.isEmpty() || !actualConfigInfo.exists() || !actualConfigInfo.isReadable() ) { report( Error, ki18n( "MySQL server configuration not found or not readable." ), ki18n( "The MySQL server configuration was not found or is not readable." ) ); } else { item = report( Success, ki18n( "MySQL server configuration is usable." ), ki18n( "The MySQL server configuration was found at %1 and is readable.").subs( makeLink( actualConfig ) ) ); item->setData( actualConfig, FileIncludeRole ); } } +void SelfTestDialog::testPSQLServer() +{ + const QString dbname = serverSetting( QLatin1String( "QPSQL" ), "Name", QLatin1String( "akonadi" )).toString(); + const QString hostname = serverSetting( QLatin1String( "QPSQL" ), "Host", QLatin1String( "localhost" )).toString(); + const QString username = serverSetting( QLatin1String( "QPSQL" ), "User", QLatin1String( "akonadi" )).toString(); + const QString password = serverSetting( QLatin1String( "QPSQL" ), "Password", QLatin1String( "akonadi" )).toString(); + const int port = serverSetting( QLatin1String( "QPSQL" ), "Port", 5432).toInt(); + + QSqlDatabase db = QSqlDatabase::addDatabase( QLatin1String( "QPSQL" ) ); + db.setHostName( hostname ); + db.setDatabaseName( dbname ); + db.setUserName( username ); + db.setPassword( password ); + db.setPort( port ); + + if ( !db.open() ) { + const KLocalizedString details = ki18n( db.lastError().text().toLatin1() ); + report( Error, ki18n( "Cannot connect to PostgreSQL server." ), details); + } + else { + report( Success, ki18n( "PostgreSQL server found." ), + ki18n( "The PostgreSQL server was found and connection is working.")); + } + db.close(); +} + void SelfTestDialog::testAkonadiCtl() { const QString path = KStandardDirs::findExe( QLatin1String("akonadictl") ); if ( path.isEmpty() ) { report( Error, ki18n( "akonadictl not found" ), ki18n( "The program 'akonadictl' needs to be accessible in $PATH. " "Make sure you have the Akonadi server installed." ) ); return; } QString result; if ( runProcess( path, QStringList() << QLatin1String( "--version" ), result ) ) { report( Success, ki18n( "akonadictl found and usable" ), ki18n( "The program '%1' to control the Akonadi server was found " "and could be executed successfully.\nResult:\n%2" ).subs( path ).subs( result ) ); } else { report( Error, ki18n( "akonadictl found but not usable" ), ki18n( "The program '%1' to control the Akonadi server was found " "but could not be executed successfully.\nResult:\n%2\n" "Make sure the Akonadi server is installed correctly." ).subs( path ).subs( result ) ); } } void SelfTestDialog::testServerStatus() { if ( QDBusConnection::sessionBus().interface()->isServiceRegistered( AKONADI_CONTROL_SERVICE ) ) { report( Success, ki18n( "Akonadi control process registered at D-Bus." ), ki18n( "The Akonadi control process is registered at D-Bus which typically indicates it is operational." ) ); } else { report( Error, ki18n( "Akonadi control process not registered at D-Bus." ), ki18n( "The Akonadi control process is not registered at D-Bus which typically means it was not started " "or encountered a fatal error during startup." ) ); } if ( QDBusConnection::sessionBus().interface()->isServiceRegistered( AKONADI_SERVER_SERVICE ) ) { report( Success, ki18n( "Akonadi server process registered at D-Bus." ), ki18n( "The Akonadi server process is registered at D-Bus which typically indicates it is operational." ) ); } else { report( Error, ki18n( "Akonadi server process not registered at D-Bus." ), ki18n( "The Akonadi server process is not registered at D-Bus which typically means it was not started " "or encountered a fatal error during startup." ) ); } } void SelfTestDialog::testProtocolVersion() { if ( Internal::serverProtocolVersion() < 0 ) { report( Skip, ki18n( "Protocol version check not possible." ), ki18n( "Without a connection to the server it is not possible to check if the protocol version meets the requirements." ) ); return; } if ( Internal::serverProtocolVersion() < SessionPrivate::minimumProtocolVersion() ) { report( Error, ki18n( "Server protocol version is too old." ), ki18n( "The server protocol version is %1, but at least version %2 is required. " "Install a newer version of the Akonadi server." ) .subs( Internal::serverProtocolVersion() ) .subs( SessionPrivate::minimumProtocolVersion() ) ); } else { report( Success, ki18n( "Server protocol version is recent enough." ), ki18n( "The server Protocol version is %1, which equal or newer than the required version %2." ) .subs( Internal::serverProtocolVersion() ) .subs( SessionPrivate::minimumProtocolVersion() ) ); } } void SelfTestDialog::testResources() { AgentType::List agentTypes = AgentManager::self()->types(); bool resourceFound = false; foreach ( const AgentType &type, agentTypes ) { if ( type.capabilities().contains( QLatin1String("Resource") ) ) { resourceFound = true; break; } } const QStringList pathList = XdgBaseDirs::findAllResourceDirs( "data", QLatin1String( "akonadi/agents" ) ); QStandardItem *item = 0; if ( resourceFound ) { item = report( Success, ki18n( "Resource agents found." ), ki18n( "At least one resource agent has been found." ) ); } else { item = report( Error, ki18n( "No resource agents found." ), ki18n( "No resource agents have been found, Akonadi is not usable without at least one. " "This usually means that no resource agents are installed or that there is a setup problem. " "The following paths have been searched: '%1'. " "The XDG_DATA_DIRS environment variable is set to '%2', make sure this includes all paths " "where Akonadi agents are installed to." ) .subs( pathList.join( QLatin1String(" ") ) ) .subs( QString::fromLocal8Bit( qgetenv( "XDG_DATA_DIRS" ) ) ) ); } item->setData( pathList, ListDirectoryRole ); item->setData( QByteArray( "XDG_DATA_DIRS" ), EnvVarRole ); } void Akonadi::SelfTestDialog::testServerLog() { QString serverLog = XdgBaseDirs::saveDir( "data", QLatin1String( "akonadi" ) ) + QDir::separator() + QString::fromLatin1( "akonadiserver.error" ); QFileInfo info( serverLog ); if ( !info.exists() || info.size() <= 0 ) { report( Success, ki18n( "No current Akonadi server error log found." ), ki18n( "The Akonadi server did not report any errors during its current startup." ) ); } else { QStandardItem *item = report( Error, ki18n( "Current Akonadi server error log found." ), ki18n( "The Akonadi server did report error during startup into %1." ).subs( makeLink( serverLog ) ) ); item->setData( serverLog, FileIncludeRole ); } serverLog += QLatin1String(".old"); info.setFile( serverLog ); if ( !info.exists() || info.size() <= 0 ) { report( Success, ki18n( "No previous Akonadi server error log found." ), ki18n( "The Akonadi server did not report any errors during its previous startup." ) ); } else { QStandardItem *item = report( Error, ki18n( "Previous Akonadi server error log found." ), ki18n( "The Akonadi server did report error during its previous startup into %1." ).subs( makeLink( serverLog ) ) ); item->setData( serverLog, FileIncludeRole ); } } void SelfTestDialog::testControlLog() { QString controlLog = XdgBaseDirs::saveDir( "data", QLatin1String( "akonadi" ) ) + QDir::separator() + QString::fromLatin1( "akonadi_control.error" ); QFileInfo info( controlLog ); if ( !info.exists() || info.size() <= 0 ) { report( Success, ki18n( "No current Akonadi control error log found." ), ki18n( "The Akonadi control process did not report any errors during its current startup." ) ); } else { QStandardItem *item = report( Error, ki18n( "Current Akonadi control error log found." ), ki18n( "The Akonadi control process did report error during startup into %1." ).subs( makeLink( controlLog ) ) ); item->setData( controlLog, FileIncludeRole ); } controlLog += QLatin1String(".old"); info.setFile( controlLog ); if ( !info.exists() || info.size() <= 0 ) { report( Success, ki18n( "No previous Akonadi control error log found." ), ki18n( "The Akonadi control process did not report any errors during its previous startup." ) ); } else { QStandardItem *item = report( Error, ki18n( "Previous Akonadi control error log found." ), ki18n( "The Akonadi control process did report error during its previous startup into %1." ).subs( makeLink( controlLog ) ) ); item->setData( controlLog, FileIncludeRole ); } } QString SelfTestDialog::createReport() { QString result; QTextStream s( &result ); s << "Akonadi Server Self-Test Report" << endl; s << "===============================" << endl; for ( int i = 0; i < mTestModel->rowCount(); ++i ) { QStandardItem *item = mTestModel->item( i ); s << endl; s << "Test " << (i + 1) << ": "; switch ( item->data( ResultTypeRole ).toInt() ) { case Skip: s << "SKIP"; break; case Success: s << "SUCCESS"; break; case Warning: s << "WARNING"; break; case Error: default: s << "ERROR"; break; } s << endl << "--------" << endl; s << endl; s << item->data( SummaryRole ).toString() << endl; s << "Details: " << item->data( DetailsRole ).toString() << endl; if ( item->data( FileIncludeRole ).isValid() ) { s << endl; const QString fileName = item->data( FileIncludeRole ).toString(); QFile f( fileName ); if ( f.open( QFile::ReadOnly ) ) { s << "File content of '" << fileName << "':" << endl; s << f.readAll() << endl; } else { s << "File '" << fileName << "' could not be opened" << endl; } } if ( item->data( ListDirectoryRole ).isValid() ) { s << endl; const QStringList pathList = item->data( ListDirectoryRole ).toStringList(); if ( pathList.isEmpty() ) s << "Directory list is empty." << endl; foreach ( const QString &path, pathList ) { s << "Directory listing of '" << path << "':" << endl; QDir dir( path ); dir.setFilter( QDir::AllEntries | QDir::NoDotAndDotDot ); foreach ( const QString &entry, dir.entryList() ) s << entry << endl; } } if ( item->data( EnvVarRole ).isValid() ) { s << endl; const QByteArray envVarName = item->data( EnvVarRole ).toByteArray(); const QByteArray envVarValue = qgetenv( envVarName ); s << "Environment variable " << envVarName << " is set to '" << envVarValue << "'" << endl; } } s << endl; s.flush(); return result; } void SelfTestDialog::saveReport() { const QString fileName = KFileDialog::getSaveFileName( KUrl(), QString(), this, i18n("Save Test Report") ); if ( fileName.isEmpty() ) return; QFile file( fileName ); if ( !file.open( QFile::ReadWrite ) ) { KMessageBox::error( this, i18n( "Could not open file '%1'", fileName ) ); return; } file.write( createReport().toUtf8() ); file.close(); } void SelfTestDialog::copyReport() { QApplication::clipboard()->setText( createReport() ); } void SelfTestDialog::linkActivated(const QString & link) { KRun::runUrl( KUrl::fromPath( link ), QLatin1String("text/plain"), this ); } // @endcond #include "selftestdialog_p.moc" diff --git a/akonadi/selftestdialog_p.h b/akonadi/selftestdialog_p.h index f550c9706..cc80a9fa1 100644 --- a/akonadi/selftestdialog_p.h +++ b/akonadi/selftestdialog_p.h @@ -1,98 +1,99 @@ /* Copyright (c) 2008 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_SELFTESTDIALOG_P_H #define AKONADI_SELFTESTDIALOG_P_H #include "akonadiprivate_export.h" #include "ui_selftestdialog.h" #include #include class QStandardItem; class QStandardItemModel; namespace Akonadi { /** * @internal * * @short A dialog that checks the current status of the Akonadi system. * * This dialog checks the current status of the Akonadi system and * displays a summary of the checks. * * @author Volker Krause */ class AKONADI_TESTS_EXPORT SelfTestDialog : public KDialog { Q_OBJECT public: /** * Creates a new self test dialog. * * @param parent The parent widget. */ SelfTestDialog( QWidget *parent = 0 ); /** * Hides the label with the introduction message. */ void hideIntroduction(); private slots: void selectionChanged( const QModelIndex &index ); void saveReport(); void copyReport(); void linkActivated( const QString &link ); private: enum ResultType { Skip, Success, Warning, Error }; QStandardItem* report( ResultType type, const KLocalizedString &summary, const KLocalizedString &details ); void runTests(); QVariant serverSetting( const QString &group, const char *key, const QVariant &def ) const; bool useStandaloneMysqlServer() const; bool runProcess( const QString &app, const QStringList &args, QString &result ) const; void testSQLDriver(); void testMySQLServer(); void testMySQLServerLog(); void testMySQLServerConfig(); + void testPSQLServer(); void testAkonadiCtl(); void testServerStatus(); void testProtocolVersion(); void testResources(); void testServerLog(); void testControlLog(); QString createReport(); Ui::SelfTestDialog ui; QStandardItemModel* mTestModel; }; } #endif diff --git a/akonadi/standardactionmanager.cpp b/akonadi/standardactionmanager.cpp index 9156ac810..f7aaeffe1 100644 --- a/akonadi/standardactionmanager.cpp +++ b/akonadi/standardactionmanager.cpp @@ -1,570 +1,634 @@ /* 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 "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 ) + 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( actionMenu->menu(), + fillFoldersMenu( type, + actionMenu->menu(), collectionSelectionModel->model(), QModelIndex() ); } } } void updatePluralLabel( StandardActionManager::Type type, int count ) { Q_ASSERT( type >= 0 && type < StandardActionManager::LastType ); if ( actions[type] && pluralLabels.contains( type ) && !pluralLabels.value( type ).isEmpty() ) { actions[type]->setText( pluralLabels.value( type ).subs( qMax( count, 1 ) ).toString() ); } } void copy( QItemSelectionModel* selModel ) { Q_ASSERT( selModel ); if ( selModel->selectedRows().count() <= 0 ) return; QMimeData *mimeData = selModel->model()->mimeData( selModel->selectedRows() ); QApplication::clipboard()->setMimeData( mimeData ); } void updateActions() { bool singleColSelected = false; bool multiColSelected = false; int colCount = 0; QModelIndex selectedIndex; if ( collectionSelectionModel ) { colCount = collectionSelectionModel->selectedRows().count(); singleColSelected = colCount == 1; multiColSelected = colCount > 0; if ( singleColSelected ) selectedIndex = collectionSelectionModel->selectedRows().first(); } enableAction( CopyCollections, multiColSelected ); enableAction( CollectionProperties, singleColSelected ); Collection col; if ( singleColSelected && selectedIndex.isValid() ) { col = selectedIndex.data( CollectionModel::CollectionRole ).value(); enableAction( CreateCollection, canCreateCollection( col ) ); enableAction( DeleteCollections, col.rights() & Collection::CanDeleteCollection ); enableAction( CopyCollections, multiColSelected && (col != Collection::root()) ); enableAction( CollectionProperties, singleColSelected && (col != Collection::root()) ); enableAction( SynchronizeCollections, CollectionUtils::isResource( col ) || CollectionUtils::isFolder( col ) ); enableAction( Paste, PasteHelper::canPaste( QApplication::clipboard()->mimeData(), col ) ); //FIXME: remove the reinterpret_cast once FavoriteCollectionsModel is in kdepimlibs/akonadi enableAction( AddToFavoriteCollections, (favoritesModel!=0) && (selectedIndex.model()!=reinterpret_cast(favoritesModel)) && singleColSelected && (col != Collection::root()) ); + //FIXME: better check if the collection is in the model, todo once FavoriteCollectionsModel is in kdepimlibs/akonadi + enableAction( RemoveFromFavoriteCollections, (favoriteSelectionModel!=0) && (selectedIndex.model()!=reinterpret_cast(favoritesModel)) + && singleColSelected && (col != Collection::root()) ); + enableAction( RenameFavoriteCollection, (favoriteSelectionModel!=0) && (selectedIndex.model()!=reinterpret_cast(favoritesModel)) + && 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 ); } 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(); } void slotCreateCollection() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); if ( !canCreateCollection( collection ) ) return; const QString name = KInputDialog::getText( i18nc( "@title:window", "New Folder"), i18nc( "@label:textbox, name of a thing", "Name"), QString(), 0, parentWidget ); if ( name.isEmpty() ) return; Collection::Id parentId = index.data( CollectionModel::CollectionIdRole ).toLongLong(); if ( parentId <= 0 ) return; Collection col; col.setName( name ); col.setParent( parentId ); CollectionCreateJob *job = new CollectionCreateJob( col ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(collectionCreationResult(KJob*)) ); } void slotCopyCollections() { copy( collectionSelectionModel ); } void slotDeleteCollection() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); QString text = i18n( "Do you really want to delete folder '%1' and all its sub-folders?", index.data().toString() ); if ( CollectionUtils::isVirtual( collection ) ) text = i18n( "Do you really want to delete the search view '%1'?", index.data().toString() ); if ( KMessageBox::questionYesNo( parentWidget, text, i18n("Delete folder?"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), QString(), KMessageBox::Dangerous ) != KMessageBox::Yes ) return; const Collection::Id colId = index.data( CollectionModel::CollectionIdRole ).toLongLong(); if ( colId <= 0 ) return; CollectionDeleteJob *job = new CollectionDeleteJob( Collection( colId ), q ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(collectionDeletionResult(KJob*)) ); } void slotSynchronizeCollection() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); AgentManager::self()->synchronizeCollection( col ); } void slotCollectionProperties() { if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); CollectionPropertiesDialog* dlg = new CollectionPropertiesDialog( col, parentWidget ); dlg->show(); } void slotCopyItems() { copy( itemSelectionModel ); } void slotPaste() { Q_ASSERT( collectionSelectionModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection col = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( col.isValid() ); KJob *job = PasteHelper::paste( QApplication::clipboard()->mimeData(), col ); q->connect( job, SIGNAL(result(KJob*)), q, SLOT(pasteResult(KJob*)) ); } void slotDeleteItems() { if ( KMessageBox::questionYesNo( parentWidget, i18n( "Do you really want to delete all selected items?" ), i18n("Delete?"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), QString(), KMessageBox::Dangerous ) != KMessageBox::Yes ) return; Q_ASSERT( itemSelectionModel ); // TODO: fix this once ItemModifyJob can handle item lists foreach ( const QModelIndex &index, itemSelectionModel->selectedRows() ) { bool ok; qlonglong id = index.data( ItemModel::IdRole ).toLongLong(&ok); Q_ASSERT(ok); new ItemDeleteJob( Item( id ), q ); } } void slotLocalSubscription() { SubscriptionDialog* dlg = new SubscriptionDialog( parentWidget ); dlg->show(); } void slotAddToFavorites() { Q_ASSERT( collectionSelectionModel ); Q_ASSERT( favoritesModel ); if ( collectionSelectionModel->selection().indexes().isEmpty() ) return; const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); Q_ASSERT( index.isValid() ); const Collection collection = index.data( CollectionModel::CollectionRole ).value(); Q_ASSERT( collection.isValid() ); //FIXME: remove the reinterpret_cast and invokeMethod once FavoriteCollectionsModel is in kdepimlibs/akonadi QAbstractItemModel *model = reinterpret_cast( favoritesModel ); QMetaObject::invokeMethod( model, "addCollection", Q_ARG(Collection, collection) ); } + void slotRemoveFromFavorites() + { + Q_ASSERT( collectionSelectionModel ); + Q_ASSERT( favoritesModel ); + if ( collectionSelectionModel->selection().indexes().isEmpty() ) + return; + + const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); + Q_ASSERT( index.isValid() ); + const Collection collection = index.data( CollectionModel::CollectionRole ).value(); + Q_ASSERT( collection.isValid() ); + + //FIXME: remove the reinterpret_cast and invokeMethod once FavoriteCollectionsModel is in kdepimlibs/akonadi + QAbstractItemModel *model = reinterpret_cast( favoritesModel ); + QMetaObject::invokeMethod( model, "removeCollection", Q_ARG(Collection, collection) ); + } + + void slotRenameFavorite() + { + Q_ASSERT( collectionSelectionModel ); + Q_ASSERT( favoritesModel ); + if ( collectionSelectionModel->selection().indexes().isEmpty() ) + return; + + const QModelIndex index = collectionSelectionModel->selection().indexes().at( 0 ); + Q_ASSERT( index.isValid() ); + const Collection collection = index.data( CollectionModel::CollectionRole ).value(); + Q_ASSERT( collection.isValid() ); + + bool ok; + QString label = KInputDialog::getText( i18n( "Rename Favorite" ), + i18nc( "@label:textbox New name of the folder.", "Name:" ), + index.data().toString(), &ok, parentWidget ); + if ( !ok ) + return; + + //FIXME: remove the reinterpret_cast and invokeMethod once FavoriteCollectionsModel is in kdepimlibs/akonadi + QAbstractItemModel *model = reinterpret_cast( favoritesModel ); + QMetaObject::invokeMethod( model, "setFavoriteLabel", Q_ARG(Collection, collection), Q_ARG(QString, label) ); + } + 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( QMenu *menu, const QAbstractItemModel *model, QModelIndex parentIndex ) + 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(); - // FIXME: Need a way to skip the search folder + 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( popup, model, index ); - - bool readOnly = false; - //FIXME: If collection is readonly turn this flag to true + 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 ) ); - //FIXME: If collection is readonly disable this action + act->setEnabled( !readOnly ); } } } StandardActionManager *q; KActionCollection *actionCollection; QWidget *parentWidget; QItemSelectionModel *collectionSelectionModel; QItemSelectionModel *itemSelectionModel; + QItemSelectionModel *favoriteSelectionModel; FavoriteCollectionsModel *favoritesModel; 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(updateActions()) ); } void StandardActionManager::setItemSelectionModel(QItemSelectionModel * selectionModel) { d->itemSelectionModel = selectionModel; connect( selectionModel, SIGNAL(selectionChanged( const QItemSelection&, const QItemSelection& )), SLOT(updateActions()) ); } -void Akonadi::StandardActionManager::setFavoriteCollectionsModel( FavoriteCollectionsModel *favoritesModel ) +void StandardActionManager::setFavoriteCollectionsModel( FavoriteCollectionsModel *favoritesModel ) { d->favoritesModel = favoritesModel; } +void StandardActionManager::setFavoriteSelectionModel( QItemSelectionModel *selectionModel ) +{ + d->favoriteSelectionModel = selectionModel; + connect( selectionModel, SIGNAL(selectionChanged( const QItemSelection&, const QItemSelection& )), + SLOT(updateActions()) ); +} + 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/standardactionmanager.h b/akonadi/standardactionmanager.h index e3a9dbb49..6179fc6e6 100644 --- a/akonadi/standardactionmanager.h +++ b/akonadi/standardactionmanager.h @@ -1,217 +1,228 @@ /* Copyright (c) 2008 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_STANDARDACTIONMANAGER_H #define AKONADI_STANDARDACTIONMANAGER_H #include "akonadi_export.h" #include class KAction; class KActionCollection; class KLocalizedString; class QItemSelectionModel; class QWidget; namespace Akonadi { class FavoriteCollectionsModel; /** * @short Manages generic actions for collection and item views. * * Manages generic Akonadi actions common for all types. This covers * creating of the actions with appropriate labels, icons, shortcuts * etc., updating the action state depending on the current selection * as well as default implementations for the actual operations. * * If the default implementation is not appropriate for your application * you can still use the state tracking by disconnecting the triggered() * signal and re-connecting it to your implementation. The actual KAction * objects can be retrieved by calling createAction() or action() for that. * * If the default look and feel (labels, icons, shortcuts) of the actions * is not appropriate for your application, you can access them as noted * above and customize them to your needs. Additionally, you can set a * KLocalizedString which should be used as a action label with correct * plural handling for actions operating on multiple objects with * setActionText(). * * Finally, if you have special needs for the action states, connect to * the actionStateUpdated() signal and adjust the state accordingly. * * The following actions are provided (KAction name in parenthesis): * - Creation of a new collection (@c akonadi_collection_create) * - Copying of selected collections (@c akonadi_collection_copy) * - Deletion of selected collections (@c akonadi_collection_delete) * - Synchronization of selected collections (@c akonadi_collection_sync) * - Showing the collection properties dialog for the current collection (@c akonadi_collection_properties) * - Copying of selected items (@c akonadi_itemcopy) * - Pasting collections, items or raw data (@c akonadi_paste) * - Deleting of selected items (@c akonadi_item_delete) * - Managing local subscriptions (@c akonadi_manage_local_subscriptions) * * The following example shows how to use standard actions in your application: * * @code * * Akonadi::StandardActionManager *actMgr = new Akonadi::StandardActionManager( actionCollection(), this ); * actMgr->setCollectionSelectionModel( collectionView->collectionSelectionModel() ); * actMgr->createAllActions(); * * @endcode * * Additionally you have to add the actions to the KXMLGUI file of your application, * using the names listed above. * * If you only need a subset of the actions provided, you can call createAction() * instead of createAllActions() for the action types you want. * * @todo collection deleting and sync do not support multi-selection yet * * @author Volker Krause */ class AKONADI_EXPORT StandardActionManager : public QObject { Q_OBJECT public: /** * Describes the supported actions. */ enum Type { CreateCollection, ///< Creates an collection CopyCollections, ///< Copies collections DeleteCollections, ///< Deletes collections SynchronizeCollections, ///< Synchronizes collections CollectionProperties, ///< Provides collection properties CopyItems, ///< Copies items Paste, ///< Paste collections or items DeleteItems, ///< Deletes items ManageLocalSubscriptions, ///< Manages local subscriptions AddToFavoriteCollections, ///< Add the collection to the favorite collections model + RemoveFromFavoriteCollections, ///< Remove the collection from the favorite collections model + RenameFavoriteCollection, ///< Rename the collection of the favorite collections model CopyCollectionToMenu, ///< Menu allowing to quickly copy a collection into another collection CopyItemToMenu, ///< Menu allowing to quickly copy an item into a collection LastType ///< Marks last action }; /** * Creates a new standard action manager. * * @param actionCollection The action collection to operate on. * @param parent The parent widget. */ explicit StandardActionManager( KActionCollection *actionCollection, QWidget *parent = 0 ); /** * Destroys the standard action manager. */ ~StandardActionManager(); /** * Sets the collection selection model based on which the collection * related actions should operate. If none is set, all collection actions * will be disabled. */ void setCollectionSelectionModel( QItemSelectionModel *selectionModel ); /** * Sets the item selection model based on which the item related actions * should operate. If none is set, all item actions will be disabled. */ void setItemSelectionModel( QItemSelectionModel* selectionModel ); /** * Sets the favorite collections model based on which the collection * relatedactions should operate. If none is set, the "Add to Favorite Folders" action * will be disabled. */ void setFavoriteCollectionsModel( FavoriteCollectionsModel *favoritesModel ); + /** + * Sets the favorite collection selection model based on which the favorite + * collection related actions should operate. If none is set, all favorite modifications + * actions will be disabled. + */ + void setFavoriteSelectionModel( QItemSelectionModel *selectionModel ); + /** * Creates the action of the given type and adds it to the action collection * specified in the constructor if it does not exist yet. The action is * connected to its default implementation provided by this class. */ KAction* createAction( Type type ); /** * Convenience method to create all standard actions. * @see createAction() */ void createAllActions(); /** * Returns the action of the given type, 0 if it has not been created (yet). */ KAction* action( Type type ) const; /** * Sets the label of the action @p type to @p text, which is used during * updating the action state and substituted according to the number of * selected objects. This is mainly useful to customize the label of actions * that can operate on multiple objects. * * Example: * @code * acctMgr->setActionText( Akonadi::StandardActionManager::CopyItems, * ki18np( "Copy Mail", "Copy %1 Mails" ) ); * @endcode */ void setActionText( Type type, const KLocalizedString &text ); Q_SIGNALS: /** * This signal is emitted whenever the action state has been updated. * In case you have special needs for changing the state of some actions, * connect to this signal and adjust the action state. */ void actionStateUpdated(); private: //@cond PRIVATE class Private; Private* const d; Q_PRIVATE_SLOT( d, void updateActions() ) Q_PRIVATE_SLOT( d, void clipboardChanged(QClipboard::Mode) ) Q_PRIVATE_SLOT( d, void slotCreateCollection() ) Q_PRIVATE_SLOT( d, void slotCopyCollections() ) Q_PRIVATE_SLOT( d, void slotDeleteCollection() ) Q_PRIVATE_SLOT( d, void slotSynchronizeCollection() ) Q_PRIVATE_SLOT( d, void slotCollectionProperties() ) Q_PRIVATE_SLOT( d, void slotCopyItems() ) Q_PRIVATE_SLOT( d, void slotPaste() ) Q_PRIVATE_SLOT( d, void slotDeleteItems() ) Q_PRIVATE_SLOT( d, void slotLocalSubscription() ) Q_PRIVATE_SLOT( d, void slotAddToFavorites() ) + Q_PRIVATE_SLOT( d, void slotRemoveFromFavorites() ) + Q_PRIVATE_SLOT( d, void slotRenameFavorite() ) Q_PRIVATE_SLOT( d, void slotCopyCollectionTo(QAction*) ) Q_PRIVATE_SLOT( d, void slotCopyItemTo(QAction*) ) Q_PRIVATE_SLOT( d, void collectionCreationResult(KJob*) ) Q_PRIVATE_SLOT( d, void collectionDeletionResult(KJob*) ) Q_PRIVATE_SLOT( d, void pasteResult(KJob*) ) Q_PRIVATE_SLOT( d, void copyToResult(KJob*) ) //@endcond }; } #endif diff --git a/kcal/incidenceformatter.cpp b/kcal/incidenceformatter.cpp index f2c46b2d1..6ada4927c 100644 --- a/kcal/incidenceformatter.cpp +++ b/kcal/incidenceformatter.cpp @@ -1,2756 +1,2760 @@ /* 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 ); } 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() ); } 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() ); } 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() ); } 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() ); } 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; } 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 Incidence *existingIncidence = 0; if ( incBase && helper->calendar() ) { existingIncidence = helper->calendar()->incidence( incBase->uid() ); 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() ) { 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() ); rsvpReq = false; } 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 ( 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 ) ) { // 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" ) { html += tdOpen; html += helper->makeLink( "check_calendar", i18nc( "look for scheduling conflicts", "Check my calendar" ) ); html += tdClose; } } } break; } case iTIPCancel: // Remove invitation if ( 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]" ) ); } } 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/kpimidentities/signature.cpp b/kpimidentities/signature.cpp index c3d1618bd..6ce71c6dc 100644 --- a/kpimidentities/signature.cpp +++ b/kpimidentities/signature.cpp @@ -1,372 +1,386 @@ /* Copyright (c) 2002-2004 Marc Mutz Copyright (c) 2007 Tom Albers This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "signature.h" #include #include #include #include #include #include #include #include #include #include using namespace KPIMIdentities; Signature::Signature() : mType( Disabled ), mInlinedHtml( false ) {} Signature::Signature( const QString &text ) : mText( text ), mType( Inlined ), mInlinedHtml( false ) {} Signature::Signature( const QString &url, bool isExecutable ) : mUrl( url ), mType( isExecutable ? FromCommand : FromFile ), mInlinedHtml( false ) {} QString Signature::rawText( bool *ok ) const { switch ( mType ) { case Disabled: if ( ok ) { *ok = true; } return QString(); case Inlined: if ( ok ) { *ok = true; } return mText; case FromFile: return textFromFile( ok ); case FromCommand: return textFromCommand( ok ); }; kFatal(5325) << "Signature::type() returned unknown value!"; return QString(); // make compiler happy } QString Signature::textFromCommand( bool *ok ) const { assert( mType == FromCommand ); // handle pathological cases: if ( mUrl.isEmpty() ) { if ( ok ) { *ok = true; } return QString(); } // create a shell process: KProcess proc; proc.setOutputChannelMode( KProcess::SeparateChannels ); proc.setShellCommand( mUrl ); int rc = proc.execute(); // handle errors, if any: if ( rc != 0 ) { if ( ok ) { *ok = false; } QString wmsg = i18n( "Failed to execute signature script

      %1:

      " "

      %2

      ", mUrl, QString( proc.readAllStandardError() ) ); KMessageBox::error( 0, wmsg ); return QString(); } // no errors: if ( ok ) { *ok = true; } // get output: QByteArray output = proc.readAllStandardOutput(); // TODO: hmm, should we allow other encodings, too? return QString::fromLocal8Bit( output.data(), output.size() ); } QString Signature::textFromFile( bool *ok ) const { assert( mType == FromFile ); // TODO: Use KIO::NetAccess to download non-local files! if ( !KUrl( mUrl ).isLocalFile() && !( QFileInfo( mUrl ).isRelative() && QFileInfo( mUrl ).exists() ) ) { kDebug(5325) << "Signature::textFromFile:" << "non-local URLs are unsupported"; if ( ok ) { *ok = false; } return QString(); } if ( ok ) { *ok = true; } // TODO: hmm, should we allow other encodings, too? const QByteArray ba = KPIMUtils::kFileToByteArray( mUrl, false ); return QString::fromLocal8Bit( ba.data(), ba.size() ); } QString Signature::withSeparator( bool *ok ) const { QString signature = rawText( ok ); if ( ok && (*ok) == false ) return QString(); if ( signature.isEmpty() ) { return signature; // don't add a separator in this case } QString newline = ( isInlinedHtml() && mType == Inlined ) ? "
      " : "\n"; if ( signature.startsWith( QString::fromLatin1( "-- " ) + newline ) || ( signature.indexOf( newline + QString::fromLatin1( "-- " ) + newline ) != -1 ) ) { // already have signature separator at start of sig or inside sig: return signature; } else { // need to prepend one: return QString::fromLatin1( "-- " ) + newline + signature; } } void Signature::setUrl( const QString &url, bool isExecutable ) { mUrl = url; mType = isExecutable ? FromCommand : FromFile; } void Signature::setInlinedHtml( bool isHtml ) { mInlinedHtml = isHtml; } bool Signature::isInlinedHtml() const { return mInlinedHtml; } // config keys and values: static const char sigTypeKey[] = "Signature Type"; static const char sigTypeInlineValue[] = "inline"; static const char sigTypeFileValue[] = "file"; static const char sigTypeCommandValue[] = "command"; static const char sigTypeDisabledValue[] = "disabled"; static const char sigTextKey[] = "Inline Signature"; static const char sigFileKey[] = "Signature File"; static const char sigCommandKey[] = "Signature Command"; static const char sigTypeInlinedHtmlKey[] = "Inlined Html"; void Signature::readConfig( const KConfigGroup &config ) { QString sigType = config.readEntry( sigTypeKey ); if ( sigType == sigTypeInlineValue ) { mType = Inlined; mInlinedHtml = config.readEntry( sigTypeInlinedHtmlKey, false ); } else if ( sigType == sigTypeFileValue ) { mType = FromFile; mUrl = config.readPathEntry( sigFileKey, QString() ); } else if ( sigType == sigTypeCommandValue ) { mType = FromCommand; mUrl = config.readPathEntry( sigCommandKey, QString() ); } else { mType = Disabled; } mText = config.readEntry( sigTextKey ); } void Signature::writeConfig( KConfigGroup &config ) const { switch ( mType ) { case Inlined: config.writeEntry( sigTypeKey, sigTypeInlineValue ); config.writeEntry( sigTypeInlinedHtmlKey, mInlinedHtml ); break; case FromFile: config.writeEntry( sigTypeKey, sigTypeFileValue ); config.writePathEntry( sigFileKey, mUrl ); break; case FromCommand: config.writeEntry( sigTypeKey, sigTypeCommandValue ); config.writePathEntry( sigCommandKey, mUrl ); break; case Disabled: config.writeEntry( sigTypeKey, sigTypeDisabledValue ); default: break; } config.writeEntry( sigTextKey, mText ); } void Signature::insertIntoTextEdit( KRichTextEdit *textEdit, Placement placement, bool addSeparator ) { QString signature; if ( addSeparator ) signature = withSeparator(); else signature = rawText(); insertPlainSignatureIntoTextEdit( signature, textEdit, placement, ( isInlinedHtml() && type() == KPIMIdentities::Signature::Inlined ) ); } void Signature::insertPlainSignatureIntoTextEdit( const QString &signature, KRichTextEdit *textEdit, Signature::Placement placement, bool isHtml ) { if ( !signature.isEmpty() ) { // Save the modified state of the document, as inserting a signature // shouldn't change this. Restore it at the end of this function. bool isModified = textEdit->document()->isModified(); // Move to the desired position, where the signature should be inserted QTextCursor cursor = textEdit->textCursor(); QTextCursor oldCursor = cursor; cursor.beginEditBlock(); if ( placement == End ) cursor.movePosition( QTextCursor::End ); else if ( placement == Start ) cursor.movePosition( QTextCursor::Start ); textEdit->setTextCursor( cursor ); // Insert the signature and newlines depending on where it was inserted. bool hackForCursorsAtEnd = false; int oldCursorPos = -1; if ( placement == End ) { if ( oldCursor.position() == textEdit->toPlainText().length() ) { hackForCursorsAtEnd = true; oldCursorPos = oldCursor.position(); } if ( isHtml ) { textEdit->insertHtml( QLatin1String( "
      " ) + signature ); } else { textEdit->insertPlainText( QLatin1Char( '\n' ) + signature ); } } else if ( placement == Start || placement == AtCursor ) { if ( isHtml ) { textEdit->insertHtml( QLatin1String( "
      " ) + signature + QLatin1String( "
      " ) ); } else { textEdit->insertPlainText( QLatin1Char( '\n' ) + signature + QLatin1Char( '\n' ) ); } } cursor.endEditBlock(); // There is one special case when re-setting the old cursor: The cursor // was at the end. In this case, QTextEdit has no way to know // if the signature was added before or after the cursor, and just decides // that it was added before (and the cursor moves to the end, but it should // not when appending a signature). See bug 167961 if ( hackForCursorsAtEnd ) oldCursor.setPosition( oldCursorPos ); textEdit->setTextCursor( oldCursor ); textEdit->ensureCursorVisible(); textEdit->document()->setModified( isModified ); if ( isHtml ) textEdit->enableRichTextMode(); } } // --------------------- Operators -------------------// QDataStream &KPIMIdentities::operator<< ( QDataStream &stream, const KPIMIdentities::Signature &sig ) { return stream << static_cast( sig.mType ) << sig.mUrl << sig.mText; } QDataStream &KPIMIdentities::operator>> ( QDataStream &stream, KPIMIdentities::Signature &sig ) { quint8 s; stream >> s >> sig.mUrl >> sig.mText; sig.mType = static_cast( s ); return stream; } bool Signature::operator== ( const Signature &other ) const { if ( mType != other.mType ) { return false; } switch ( mType ) { case Inlined: return mText == other.mText; case FromFile: case FromCommand: return mUrl == other.mUrl; default: case Disabled: return true; } } +QString Signature::plainText() const +{ + QString sigText = rawText(); + if ( isInlinedHtml() && type() == Inlined ) { + // Use a QTextDocument as a helper, it does all the work for us and + // strips all HTML tags. + QTextDocument helper; + QTextCursor helperCursor( &helper ); + helperCursor.insertHtml( sigText ); + sigText = helper.toPlainText(); + } + return sigText; +} + // --------------- Getters -----------------------// QString Signature::text() const { return mText; } QString Signature::url() const { return mUrl; } Signature::Type Signature::type() const { return mType; } // --------------- Setters -----------------------// void Signature::setText( const QString &text ) { mText = text; mType = Inlined; } void Signature::setType( Type type ) { mType = type; } diff --git a/kpimidentities/signature.h b/kpimidentities/signature.h index 9a409639a..f6aa795e4 100644 --- a/kpimidentities/signature.h +++ b/kpimidentities/signature.h @@ -1,188 +1,195 @@ /* Copyright (c) 2002-2004 Marc Mutz Copyright (c) 2007 Tom Albers Author: Stefan Taferner This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KPIMIDENTITIES_SIGNATURE_H #define KPIMIDENTITIES_SIGNATURE_H #include "kpimidentities_export.h" #include #include #include #include #include #include namespace KPIMIdentities { class Signature; } class KConfigGroup; class KRichTextEdit; namespace KPIMIdentities { KPIMIDENTITIES_EXPORT QDataStream &operator<< ( QDataStream &stream, const KPIMIdentities::Signature &sig ); KPIMIDENTITIES_EXPORT QDataStream &operator>> ( QDataStream &stream, KPIMIdentities::Signature &sig ); /** * @short abstraction of a signature (aka "footer"). * @author Marc Mutz */ class KPIMIDENTITIES_EXPORT Signature { friend class Identity; friend KPIMIDENTITIES_EXPORT QDataStream &operator<< ( QDataStream &stream, const Signature &sig ); friend KPIMIDENTITIES_EXPORT QDataStream &operator>> ( QDataStream &stream, Signature &sig ); public: /** Type of signature (ie. way to obtain the signature text) */ enum Type { Disabled = 0, Inlined = 1, FromFile = 2, FromCommand = 3 }; /** * Describes the placement of the signature text when it is to be inserted into a * text edit */ enum Placement { Start, ///< The signature is placed at the start of the textedit End, ///< The signature is placed at the end of the textedit AtCursor ///< The signature is placed at the current cursor position }; /** Used for comparison */ bool operator== ( const Signature &other ) const; /** Constructor for disabled signature */ Signature(); /** Constructor for inline text */ Signature( const QString &text ); /** Constructor for text from a file or from output of a command */ Signature( const QString &url, bool isExecutable ); /** @return the raw signature text as entered resp. read from file. */ QString rawText( bool *ok=0 ) const; /** @return the signature text with a "-- \n" separator added, if necessary. A newline will not be appended or prepended. */ QString withSeparator( bool *ok=0 ) const; /** Set the signature text and mark this signature as being of "inline text" type. */ void setText( const QString &text ); QString text() const; + /** + * Returns the text of the signature. If the signature is HTML, the HTML + * tags will be stripped. + * @since 4.4 + */ + QString plainText() const; + /** Set the signature URL and mark this signature as being of "from file" resp. "from output of command" type. */ void setUrl( const QString &url, bool isExecutable=false ); QString url() const; /// @return the type of signature (ie. way to obtain the signature text) Type type() const; void setType( Type type ); /** * Sets the inlined signature to text or html * @param isHtml sets the inlined signature to html * @since 4.1 */ void setInlinedHtml( bool isHtml ); /** * @return boolean whether the inlined signature is html * @since 4.1 */ bool isInlinedHtml() const; /** * Inserts this signature into the given text edit. * The cursor position is preserved. * A leading or trailing newline is also added automatically, depending on * the placement. * For undo/redo, this is treated as one operation. * * Rich text mode of the text edit will be enabled if the signature is in * inlined HTML format. * * @param textEdit the signature will be inserted into this text edit. * @param placement defines where in the text edit the signature should be * inserted. * @param addSeparator if true, the separator '-- \n' will be added in front * of the signature * * @since 4.3 */ void insertIntoTextEdit( KRichTextEdit *textEdit, Placement placement = End, bool addSeparator = true ); /** * Inserts this given signature into the given text edit. * The cursor position is preserved. * A leading or trailing newline is also added automatically, depending on * the placement. * For undo/redo, this is treated as one operation. * A separator is not added. * * Use the insertIntoTextEdit() function if possible, as it has support * for separators and does HTML detection automatically. * * Rich text mode of the text edit will be enabled if @p isHtml is true. * * @param signature the signature, either as plain text or as HTML * @param textEdit the text edit to insert the signature into * @param placement defines where in the textedit the signature should be * inserted. * @param isHtml defines whether the signature should be inserted as text or html * * @since 4.3 */ static void insertPlainSignatureIntoTextEdit( const QString &signature, KRichTextEdit *textEdit, Placement placement = End, bool isHtml = false ); protected: void writeConfig( KConfigGroup &config ) const; void readConfig( const KConfigGroup &config ); private: QString textFromFile( bool *ok ) const; QString textFromCommand( bool *ok ) const; QString mUrl; QString mText; Type mType; bool mInlinedHtml; }; } #endif /*kpim_signature_h*/