Development/Tutorials/Akonadi/Application: Difference between revisions
(Tutorial about usage of Akonadi in applications) |
Gditchfield (talk | contribs) No edit summary |
||
(29 intermediate revisions by 7 users not shown) | |||
Line 1: | Line 1: | ||
{{TutorialBrowser| | {{TutorialBrowser| | ||
series=Akonadi Tutorial| | series=[[Development/Tutorials#Personal_Information_Management_.28Akonadi.29|Akonadi Tutorial]]| | ||
name= | name=Creating an Akonadi application| | ||
pre=[http://mindview.net/Books/TICPP/ThinkingInCPP2e.html C++], [http://www.trolltech.com/products/qt/ Qt], [[Getting_Started/Build | pre=[http://mindview.net/Books/TICPP/ThinkingInCPP2e.html C++], [http://www.trolltech.com/products/qt/ Qt], [[Getting_Started/Build|KDE development environment]]| | ||
next=| | next=| | ||
reading=[[Development/Tutorials/CMake|CMake]] | reading=[[Development/Tutorials/CMake|CMake]], [[Projects/PIM/Akonadi/Development_Tools|Akonadi Development Tools]] | ||
}} | }} | ||
Line 31: | Line 33: | ||
We can kick-start the application by using '''KAppTemplate''', which can be found as '''KDE template generator''' in the development section of the K-menu, or by running '''kapptemplate''' in a terminal window. | We can kick-start the application by using '''KAppTemplate''', which can be found as '''KDE template generator''' in the development section of the K-menu, or by running '''kapptemplate''' in a terminal window. | ||
First, we select the ''' | First, we select the '''KDE Framework C++''' in the <menuchoice>Qt</menuchoice> → <menuchoice>Graphical</menuchoice> section of the program, give our project a name and continue through the following pages to complete the template creation. | ||
[[ | [[File:TutorialAkonadiDetacherTemplate.png|500px|center]] | ||
A look at the generated project top level directory shows us the following files: | A look at the generated project top level directory shows us the following files: | ||
< | <syntaxhighlight lang="bash"> | ||
CMakeLists.txt | CMakeLists.txt | ||
COPYING | |||
COPYING.DOC | |||
Messages.sh | |||
README | |||
doc/ | doc/ | ||
icons/ | icons/ | ||
src/ | src/ | ||
</ | </syntaxhighlight> | ||
and the following files in sub directory '''src''': | and the following files in sub directory '''src''': | ||
< | <syntaxhighlight lang="bash"> | ||
CMakeLists.txt | CMakeLists.txt | ||
DetacherSettings.kcfg | |||
DetacherSettings.kcfgc | |||
detacherui.rc | detacherui.rc | ||
detacherview.cpp | detacherview.cpp | ||
detacherview.h | detacherview.h | ||
detacherview.ui | |||
detacherwindow.cpp | |||
detacherwindow.h | |||
main.cpp | main.cpp | ||
org.example.detacher.appdata.xml | |||
org.example.detacher.desktop | |||
settings.ui | |||
</ | </syntaxhighlight> | ||
At this stage it is already possible to compile the application, so we can already check if our development environment is setup correctly by creating the build directory and having CMake either generate Makefiles or | At this stage it is already possible to compile the application, so we can already check if our development environment is setup correctly by creating the build directory and having CMake either generate Makefiles or import the project in KDevelop. | ||
=== Generating Makefiles === | === Generating Makefiles === | ||
From within the generated top level directory: | From within the generated top level directory: | ||
< | <syntaxhighlight lang="bash"> | ||
mkdir build | mkdir build | ||
cd build | cd build | ||
cmake -DCMAKE_BUILD_TYPE=debugfull .. | cmake -DCMAKE_BUILD_TYPE=debugfull .. | ||
</ | </syntaxhighlight> | ||
and run the build using make as usual. | and run the build using make as usual. | ||
=== Generating a KDevelop project file === | <!--=== Generating a KDevelop project file === | ||
From within the generated top level directory: | From within the generated top level directory: | ||
< | <syntaxhighlight lang="bash"> | ||
mkdir build | mkdir build | ||
cd build | cd build | ||
cmake -DCMAKE_BUILD_TYPE=debugfull -G KDevelop3 .. | cmake -DCMAKE_BUILD_TYPE=debugfull -G KDevelop3 .. | ||
</ | </syntaxhighlight> | ||
and open the generated project with KDevelop and run the build process from there. | and open the generated project with KDevelop and run the build process from there.--> | ||
=== Adjusting the main Qt Designer file === | === Adjusting the main Qt Designer file === | ||
Open the file ''' | Open the file '''detacherview.ui''' in Qt Designer and remove the example label. | ||
Remove the widget's main layout by clicking on the now empty widget and use the '''Break Layout''' menu entry in the ''' | Remove the widget's main layout by clicking on the now empty widget and use the '''Break Layout''' menu entry in the '''Lay out''' menu. | ||
Now, from left to right, place two '''Tree Views''' and one '''List Widget''' side-by-side. | Now, from left to right, place two '''Tree Views''' and one '''List Widget''' side-by-side. | ||
Select all three boxes by holding SHIFT and clicking each box with the left mouse button. | Select all three boxes in the object inspector docker by holding <keycap>SHIFT</keycap> and clicking each box with the left mouse button. | ||
Use '''Layout Horizontally in a Splitter''' in the ''' | Use '''Layout Horizontally in a Splitter''' in the '''Lay out''' menu and then create a main layout by clicking on their parent widget and using '''Layout Vertically''' in the '''Lay out''' menu. | ||
A preview ( | A preview ({{Menu|Form|Preview}}) should now look like this: | ||
[[ | |||
[[File:TutorialAkonadiDetacherPreview.png|450px|center]] | |||
Finally, change the object names for the three widgets by right clicking it and choosing '''Change objectName'''. The left widget should be named '''folderView''', the middle one '''messageView''' and the right one '''attachmentList'''. | Finally, change the object names for the three widgets by right clicking it and choosing '''Change objectName'''. The left widget should be named '''folderView''', the middle one '''messageView''' and the right one '''attachmentList'''. | ||
Line 104: | Line 109: | ||
== Promoting Views == | == Promoting Views == | ||
The [ | The [https://api.kde.org/kdepim/akonadi/html/index.html KDE client library for Akonadi] has a couple of convenience classes which make our life as application developers more pleasant. | ||
Two of these classes are specialized view widgets, one for displaying collections and one for displaying items. | Two of these classes are specialized view widgets, one for displaying collections and one for displaying items. | ||
Line 110: | Line 115: | ||
Right click the left widget and choose '''Promote to'''. Then fill the form like shown in the next screenshot. | Right click the left widget and choose '''Promote to'''. Then fill the form like shown in the next screenshot. | ||
[[ | [[File:TutorialAkonadiDetacherPromote.png|450px|center]] | ||
Click add and promote. Repeat the same for the middle widget, this time using '''Akonadi::ItemView''' as the class name and | Click add and promote. Repeat the same for the middle widget, this time using '''Akonadi::ItemView''' as the class name and {{Path|itemview.h}} for the header file. | ||
This change also requires a change in the | This change also requires a change in the {{Path|CMakeLists.txt}} file in the top level directory and in the one from the source directory. | ||
In the file from the top level directory the following line | In the file from the top level directory add the following line | ||
< | <syntaxhighlight lang="cmake"> | ||
set(LIBKDEPIM_VERSION "5.11.0") | |||
# Find KdepimLibs Package | |||
find_package(KF5Akonadi ${LIBKDEPIM_VERSION} CONFIG REQUIRED) | |||
find_package(KF5Libkdepim ${LIBKDEPIM_VERSION} CONFIG REQUIRED) | |||
</ | </syntaxhighlight> | ||
and add new target library in {{Path|src/CMakeLists.txt}} | |||
< | <syntaxhighlight lang="cmake"> | ||
target_link_libraries(detacher | target_link_libraries(detacher | ||
</ | ... | ||
KF5::AkonadiCore | |||
KF5::AkonadiAgentBase | |||
KF5::AkonadiWidgets | |||
KF5::AkonadiXml | |||
) | |||
</syntaxhighlight> | |||
== Initialization == | == Initialization == | ||
Since the application will depend on Akonadi running, we can ensure this by starting it if it is not. | Since the application will depend on Akonadi running, we can ensure this by starting it if it is not. | ||
This is handled by the [ | This is handled by the [https://api.kde.org/kdepim/akonadi/html/classAkonadi_1_1Control.html Akonadi::Control] class. | ||
In | In {{Path|detacherwindow.h}} we add another slot called '''delayedInit()''' which will perform this initialization. | ||
A slot so we can delay its executing using a single shot timer, a technique called "Delayed Initialization", i.e. letting the application create and show its GUI as fast as possible and do any probably time consuming initialization after that. | A slot so we can delay its executing using a single shot timer, a technique called "Delayed Initialization", i.e. letting the application create and show its GUI as fast as possible and do any probably time consuming initialization after that. | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
private | private Q_SLOTS: | ||
void fileNew(); | void fileNew(); | ||
void | void settingsConfigure(); | ||
void delayedInit(); | void delayedInit(); | ||
</ | </syntaxhighlight> | ||
In | In {{Path|detacherwindow.cpp}} we need two new include directives: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include <QtCore/QTimer> | #include <QtCore/QTimer> | ||
#include < | #include <control.h> | ||
</ | </syntaxhighlight> | ||
and the slot's implementation | and the slot's implementation | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void | void DetacherWindow::delayedInit() | ||
{ | { | ||
if ( !Akonadi::Control::start( | if (!Akonadi::Control::start() ) { | ||
qApp->exit( -1 ); | qApp->exit(-1); | ||
return; | return; | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
If the application fails to start Akonadi, it simply quits. A real application should probably tell the user about that though. | If the application fails to start Akonadi, it simply quits. A real application should probably tell the user about that though. | ||
Line 169: | Line 179: | ||
Since we want the slot to be executed delayed, add the following line at the end of the class' constructor | Since we want the slot to be executed delayed, add the following line at the end of the class' constructor | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
QTimer::singleShot( 0, this, | QTimer::singleShot(0, this, &DetacherWindow::delayedInit); | ||
</ | </syntaxhighlight> | ||
Lets add a new public method to the view class. In | Lets add a new public method to the view class. In {{Path|detacherview.h}} add | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void createModels(); | void createModels(); | ||
</ | </syntaxhighlight> | ||
and for now with an empty body in | and for now with an empty body in {{Path|detacherview.cpp}} (we will get to the implementation shortly) | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void DetacherView::createModels() | void DetacherView::createModels() | ||
{ | { | ||
} | } | ||
</ | </syntaxhighlight> | ||
and call it from ''' | and call it from '''DetacherWindow::delayedInit()''' after the Akonadi start succeeded | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void | void DetacherWindow::delayedInit() | ||
{ | { | ||
if ( !Akonadi::Control::start( | if (!Akonadi::Control::start() ) { | ||
qApp->exit( -1 ); | qApp->exit(-1); | ||
return; | return; | ||
} | } | ||
m_detacherView->createModels(); | |||
} | } | ||
</ | </syntaxhighlight> | ||
== Connecting Views to Akonadi == | == Connecting Views to Akonadi == | ||
Line 206: | Line 215: | ||
Actually, the data type the application will be working on, MIME messages, has an even further specialized model in a type specific sub library. | Actually, the data type the application will be working on, MIME messages, has an even further specialized model in a type specific sub library. | ||
To properly link this additional | First we need to add two new KDE PIM library, in the {{Path|CMakeLists.txt}}. | ||
< | <syntaxhighlight lang="cmake"> | ||
target_link_libraries(detacher | find_package(KF5AkonadiMime ${LIBKDEPIM_VERSION} CONFIG REQUIRED) | ||
</ | find_package(KF5Mime ${LIBKDEPIM_VERSION} CONFIG REQUIRED) | ||
</syntaxhighlight> | |||
To properly link this additional libraries change the source directory's {{Path|src/CMakeLists.txt}} to this | |||
<syntaxhighlight lang="cmake"> | |||
target_link_libraries(detacher | |||
... | |||
KF5::Mime | |||
KF5::AkonadiMime | |||
) | |||
</syntaxhighlight> | |||
In | In {{Path|detacherview.cpp}} add the following include directives | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include < | #include <CollectionFilterProxyModel> | ||
#include < | #include <MessageModel> | ||
#include < | #include <Monitor> | ||
</ | #include <EntityTreeModel> | ||
</syntaxhighlight> | |||
With that we can now properly implement the '''createModels()''' method: | With that we can now properly implement the '''createModels()''' method: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void DetacherView::createModels() | void DetacherView::createModels() | ||
{ | { | ||
Akonadi:: | Akonadi::Monitor *monitor = new Akonadi::Monitor(this); | ||
monitor->setObjectName(QStringLiteral("CollectionWidgetMonitor")); | |||
monitor->fetchCollection(true); | |||
monitor->setAllMonitored(true); | |||
Akonadi::EntityTreeModel *treeModel = new Akonadi::EntityTreeModel(monitor, this); | |||
Akonadi::CollectionFilterProxyModel *filterModel = new Akonadi::CollectionFilterProxyModel( this ); | Akonadi::CollectionFilterProxyModel *filterModel = new Akonadi::CollectionFilterProxyModel(this); | ||
filterModel->setSourceModel( | filterModel->setSourceModel(treeModel); | ||
filterModel->addMimeTypeFilter( QLatin1String( "message/rfc822" ) ); | filterModel->addMimeTypeFilter(QLatin1String("message/rfc822")); | ||
Akonadi::ItemModel *itemModel = new Akonadi::MessageModel( this ); | Akonadi::ItemModel *itemModel = new Akonadi::MessageModel(this); | ||
m_ui.folderView->setModel(filterModel); | |||
m_ui.messageView->setModel(itemModel); | |||
connect( | connect(m_ui.folderView, SIGNAL(currentChanged(Akonadi::Collection)), | ||
itemModel, SLOT( setCollection( Akonadi::Collection ) ) ); | itemModel, SLOT(setCollection(Akonadi::Collection))); | ||
} | } | ||
</ | </syntaxhighlight> | ||
TODO Update this paragraph | |||
The first line creates a [http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1CollectionModel.html CollectionModel] which will get all "folders" from Akonadi and keep this data updated as long as the application is running. | The first line creates a [http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1CollectionModel.html CollectionModel] which will get all "folders" from Akonadi and keep this data updated as long as the application is running. | ||
However, since this includes collections for other data types as well, we need to filter for the data type we are interested in, MIME messages or in terms of MIME type '''message/rfc822'''. | However, since this includes collections for other data types as well, we need to filter for the data type we are interested in, MIME messages or in terms of MIME type '''message/rfc822'''. | ||
This kind of filtering is conveniently supplied in the form of a proxy model called [ | This kind of filtering is conveniently supplied in the form of a proxy model called [https://api.kde.org/kdepim/akonadi/html/classAkonadi_1_1CollectionFilterProxyModel.html CollectionFilterProxyModel]. | ||
The next model, [ | The next model, [https://api.kde.org/kdepim/akonadi-mime/html/classAkonadi_1_1MessageModel.html MessageModel] is an [https://api.kde.org/kdepim/akonadi/html/classAkonadi_1_1ItemModel.html ItemModel] specialized in dealing with our data type, messages. | ||
Setting the models on the respective view almost completes the setup process, the only thing left is to connect the [ | Setting the models on the respective view almost completes the setup process, the only thing left is to connect the [https://api.kde.org/kdepim/akonadi/html/classAkonadi_1_1CollectionView.html CollectionView] to the MessageModel so it changes its data depending on which folder gets selected. | ||
At this stage the application is already capable of showing all your mail folders and headers of all your e-mails! | At this stage the application is already capable of showing all your mail folders and headers of all your e-mails! | ||
[[File:TutorialAkonadiDetacherConnectingView.png|450px|center]] | |||
== Getting at the Attachments == | == Getting at the Attachments == | ||
Line 266: | Line 294: | ||
Using this is quite simple. First we add a new include and a class forward declaration for '''detacherview.h''' | Using this is quite simple. First we add a new include and a class forward declaration for '''detacherview.h''' | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include <akonadi/item.h> | #include <akonadi/item.h> | ||
class KJob; | class KJob; | ||
</ | </syntaxhighlight> | ||
In the private member section add an [http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1Item.html Item] member: | In the private member section add an [http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1Item.html Item] member: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
private: | private: | ||
Ui::detacherview_base ui_detacherview_base; | Ui::detacherview_base ui_detacherview_base; | ||
Akonadi::Item mItem; | Akonadi::Item mItem; | ||
</ | </syntaxhighlight> | ||
and two new slots in the private slots section: | and two new slots in the private slots section: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
private slots: | private slots: | ||
void switchColors(); | void switchColors(); | ||
Line 288: | Line 316: | ||
void itemChanged( const Akonadi::Item &item ); | void itemChanged( const Akonadi::Item &item ); | ||
void itemFetchDone( KJob *job ); | void itemFetchDone( KJob *job ); | ||
</ | </syntaxhighlight> | ||
In the source file '''detacherview.cpp''' two new includes are required | In the source file '''detacherview.cpp''' two new includes are required | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include <akonadi/itemfetchjob.h> | #include <akonadi/itemfetchjob.h> | ||
#include <akonadi/itemfetchscope.h> | #include <akonadi/itemfetchscope.h> | ||
</ | </syntaxhighlight> | ||
for the implementation of the two new slots | for the implementation of the two new slots | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void DetacherView::itemChanged( const Akonadi::Item &item ) | void DetacherView::itemChanged( const Akonadi::Item &item ) | ||
{ | { | ||
Line 332: | Line 360: | ||
} | } | ||
} | } | ||
</ | </syntaxhighlight> | ||
To trigger the item fetching we connect the first new slot to a signal of the MessageView. In '''DetacherView::createModels()''' add another '''connect''' statement: | To trigger the item fetching we connect the first new slot to a signal of the MessageView. In '''DetacherView::createModels()''' add another '''connect''' statement: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
connect( ui_detacherview_base.messageView, SIGNAL( currentChanged( Akonadi::Item ) ), | connect( ui_detacherview_base.messageView, SIGNAL( currentChanged( Akonadi::Item ) ), | ||
SLOT( itemChanged( Akonadi::Item ) ) ); | SLOT( itemChanged( Akonadi::Item ) ) ); | ||
</ | </syntaxhighlight> | ||
=== Getting the attachments from the message === | === Getting the attachments from the message === | ||
Line 349: | Line 377: | ||
For the application this means two more includes and a typedef: | For the application this means two more includes and a typedef: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include <kmime/kmime_message.h> | #include <kmime/kmime_message.h> | ||
#include <boost/shared_ptr.hpp> | #include <boost/shared_ptr.hpp> | ||
typedef boost::shared_ptr<KMime::Message> MessagePtr; | typedef boost::shared_ptr<KMime::Message> MessagePtr; | ||
</ | </syntaxhighlight> | ||
[http://api.kde.org/4.x-api/kdepimlibs-apidocs/kmime/html/classKMime_1_1Message.html KMime::Message] is the data type and we need the [http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm boost::shared_ptr] to provide us with the value based behavior required by the item's payload methods. | [http://api.kde.org/4.x-api/kdepimlibs-apidocs/kmime/html/classKMime_1_1Message.html KMime::Message] is the data type and we need the [http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm boost::shared_ptr] to provide us with the value based behavior required by the item's payload methods. | ||
Line 359: | Line 387: | ||
Equipped with this new tools we can extend '''DetacherView::itemFetchDone()''' by adding the following code at its end: | Equipped with this new tools we can extend '''DetacherView::itemFetchDone()''' by adding the following code at its end: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
if ( !mItem.hasPayload<MessagePtr>() ) { | if ( !mItem.hasPayload<MessagePtr>() ) { | ||
kWarning() << "Item does not have message payload"; | kWarning() << "Item does not have message payload"; | ||
Line 375: | Line 403: | ||
ui_detacherview_base.attachmentList->addItem( fileName ); | ui_detacherview_base.attachmentList->addItem( fileName ); | ||
} | } | ||
</ | </syntaxhighlight> | ||
{{tip|'''Exercise:''' Instead of using a QListView and simple strings, a model working on the single item would make the application a lot cleaner.}} | {{tip|'''Exercise:''' Instead of using a QListView and simple strings, a model working on the single item would make the application a lot cleaner.}} | ||
Line 389: | Line 417: | ||
First we need a new slot in the private slots section of '''detacherview.h''' | First we need a new slot in the private slots section of '''detacherview.h''' | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void detachAttachment(); | void detachAttachment(); | ||
</ | </syntaxhighlight> | ||
To implement the first sub task, saving the selected attachment into a file, the following new includes are needed in '''detacherview.cpp''' | To implement the first sub task, saving the selected attachment into a file, the following new includes are needed in '''detacherview.cpp''' | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include <kaction.h> | #include <kaction.h> | ||
#include <kfiledialog.h> | #include <kfiledialog.h> | ||
#include <kstandardaction.h> | #include <kstandardaction.h> | ||
</ | </syntaxhighlight> | ||
In the class' constructor create and connect an action and make it available as the attachment list widget's context menu: | In the class' constructor create and connect an action and make it available as the attachment list widget's context menu: | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
KAction *detachAction = KStandardAction::cut( this, SLOT( detachAttachment() ), this ); | KAction *detachAction = KStandardAction::cut( this, SLOT( detachAttachment() ), this ); | ||
detachAction->setText( i18nc( "@action:button remove an attachment from an email", | detachAction->setText( i18nc( "@action:button remove an attachment from an email", | ||
Line 408: | Line 436: | ||
ui_detacherview_base.attachmentList->addAction( detachAction ); | ui_detacherview_base.attachmentList->addAction( detachAction ); | ||
ui_detacherview_base.attachmentList->setContextMenuPolicy( Qt::ActionsContextMenu ); | ui_detacherview_base.attachmentList->setContextMenuPolicy( Qt::ActionsContextMenu ); | ||
</ | </syntaxhighlight> | ||
{{note|Abusing a KDE standard action like this is not recommendable for real applications. For the scope of this tutorial we overlook this as it provides a quick way to setup an action.}} | {{note|Abusing a KDE standard action like this is not recommendable for real applications. For the scope of this tutorial we overlook this as it provides a quick way to setup an action.}} | ||
To properly link the additional library for file dialog, change the source directory's '''CMakeLists.txt''' to this | |||
<syntaxhighlight lang="cmake"> | |||
target_link_libraries(detacher ${KDE4_KDEUI_LIBS} ${KDEPIMLIBS_AKONADI_LIBS} ${KDEPIMLIBS_AKONADI_KMIME_LIBS} ${KDEPIMLIBS_KMIME_LIBS} ${KDE4_KIO_LIBS}) | |||
</syntaxhighlight> | |||
In case the [http://api.kde.org/4.x-api/kdelibs-apidocs/kdecore/html/klocalizedstring_8h.html#c3e33b6ced4f356d040e165094a5c3d5 i18nc] function or the text used in it are unexpected, see the [[Development/Tutorials/Localization/i18n_Semantics|Semantic Markup Tutorial]]. | |||
Saving the attachment means we need to ask for a saving location, probably including a filename override and get the data from the message. | Saving the attachment means we need to ask for a saving location, probably including a filename override and get the data from the message. | ||
This can be implemented like this | This can be implemented like this | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
void DetacherView::detachAttachment() | void DetacherView::detachAttachment() | ||
{ | { | ||
Line 480: | Line 513: | ||
file.close(); | file.close(); | ||
} | } | ||
</ | </syntaxhighlight> | ||
{{note|Ideally we would have a direct mapping of file name to attachment pointer, e.g. when using a model working on the Akonadi item.} | {{note|Ideally we would have a direct mapping of file name to attachment pointer, e.g. when using a model working on the Akonadi item.}} | ||
=== Removing the Attachment === | === Removing the Attachment === | ||
Line 489: | Line 522: | ||
For the last step another job class is needed: [http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1ItemModifyJob.html ItemModifyJob] | For the last step another job class is needed: [http://api.kde.org/4.x-api/kdepimlibs-apidocs/akonadi/html/classAkonadi_1_1ItemModifyJob.html ItemModifyJob] | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
#include <akonadi/itemmodifyjob.h> | #include <akonadi/itemmodifyjob.h> | ||
</ | </syntaxhighlight> | ||
and can be implemented by appending the following code to '''DetacherView::detachAttachment()''' | and can be implemented by appending the following code to '''DetacherView::detachAttachment()''' | ||
< | <syntaxhighlight lang="cpp-qt"> | ||
// remove attachment from message | // remove attachment from message | ||
message->removeContent( selectedAttachment, true ); | message->removeContent( selectedAttachment, true ); | ||
Line 511: | Line 544: | ||
return; | return; | ||
} | } | ||
</ | </syntaxhighlight> | ||
[[Category:Tutorial]] | [[Category:Tutorial]] | ||
[[Category:C++]] | [[Category:C++]] | ||
[[Category: | [[Category:KF5]] | ||
[[Category:Akonadi]] | [[Category:Akonadi]] | ||
[[Category:PIM]] | [[Category:PIM]] |
Latest revision as of 17:47, 28 February 2021
Tutorial Series | Akonadi Tutorial |
Previous | C++, Qt, KDE development environment |
What's Next | |
Further Reading | CMake, Akonadi Development Tools |
This tutorial will guide you through the steps of creating an Akonadi application, an end user program which displays and manipulates PIM data provided by Akonadi.
If you are looking for a tutorial on how to provide data for Akonadi see the Akonadi Resource Tutorial
Prerequisites
cleanup confusing sections and fix sections which contain a todo
Describe required versions and build setup
Tutorial Example
The goal of the tutorial is to create a simple application allowing a user to "detach" attachments from e-mails, i.e. save them to disk and remove them from the message.
To stay concentrated on Akonadi related steps, all general application or GUI related steps will done in a minimal fashion.
Preparation
We can kick-start the application by using KAppTemplate, which can be found as KDE template generator in the development section of the K-menu, or by running kapptemplate in a terminal window.
First, we select the KDE Framework C++ in the
→ section of the program, give our project a name and continue through the following pages to complete the template creation.A look at the generated project top level directory shows us the following files:
CMakeLists.txt
COPYING
COPYING.DOC
Messages.sh
README
doc/
icons/
src/
and the following files in sub directory src:
CMakeLists.txt
DetacherSettings.kcfg
DetacherSettings.kcfgc
detacherui.rc
detacherview.cpp
detacherview.h
detacherview.ui
detacherwindow.cpp
detacherwindow.h
main.cpp
org.example.detacher.appdata.xml
org.example.detacher.desktop
settings.ui
At this stage it is already possible to compile the application, so we can already check if our development environment is setup correctly by creating the build directory and having CMake either generate Makefiles or import the project in KDevelop.
Generating Makefiles
From within the generated top level directory:
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=debugfull ..
and run the build using make as usual.
Adjusting the main Qt Designer file
Open the file detacherview.ui in Qt Designer and remove the example label. Remove the widget's main layout by clicking on the now empty widget and use the Break Layout menu entry in the Lay out menu.
Now, from left to right, place two Tree Views and one List Widget side-by-side. Select all three boxes in the object inspector docker by holding SHIFT and clicking each box with the left mouse button. Use Layout Horizontally in a Splitter in the Lay out menu and then create a main layout by clicking on their parent widget and using Layout Vertically in the Lay out menu.
A preview (
→ ) should now look like this:Finally, change the object names for the three widgets by right clicking it and choosing Change objectName. The left widget should be named folderView, the middle one messageView and the right one attachmentList.
In order to make it build again, edit the file detacherview.cpp and remove the code from the settingsChanged method. The application should now build again and be able to run.
Promoting Views
The KDE client library for Akonadi has a couple of convenience classes which make our life as application developers more pleasant. Two of these classes are specialized view widgets, one for displaying collections and one for displaying items.
In order to use these widgets instead of ones from Qt we need to use a Qt Designer feature called "promoting". Right click the left widget and choose Promote to. Then fill the form like shown in the next screenshot.
Click add and promote. Repeat the same for the middle widget, this time using Akonadi::ItemView as the class name and itemview.h for the header file.
This change also requires a change in the CMakeLists.txt file in the top level directory and in the one from the source directory.
In the file from the top level directory add the following line
set(LIBKDEPIM_VERSION "5.11.0")
# Find KdepimLibs Package
find_package(KF5Akonadi ${LIBKDEPIM_VERSION} CONFIG REQUIRED)
find_package(KF5Libkdepim ${LIBKDEPIM_VERSION} CONFIG REQUIRED)
and add new target library in src/CMakeLists.txt
target_link_libraries(detacher
...
KF5::AkonadiCore
KF5::AkonadiAgentBase
KF5::AkonadiWidgets
KF5::AkonadiXml
)
Initialization
Since the application will depend on Akonadi running, we can ensure this by starting it if it is not. This is handled by the Akonadi::Control class.
In detacherwindow.h we add another slot called delayedInit() which will perform this initialization. A slot so we can delay its executing using a single shot timer, a technique called "Delayed Initialization", i.e. letting the application create and show its GUI as fast as possible and do any probably time consuming initialization after that.
private Q_SLOTS:
void fileNew();
void settingsConfigure();
void delayedInit();
In detacherwindow.cpp we need two new include directives:
#include <QtCore/QTimer>
#include <control.h>
and the slot's implementation
void DetacherWindow::delayedInit()
{
if (!Akonadi::Control::start() ) {
qApp->exit(-1);
return;
}
}
If the application fails to start Akonadi, it simply quits. A real application should probably tell the user about that though.
Since we want the slot to be executed delayed, add the following line at the end of the class' constructor
QTimer::singleShot(0, this, &DetacherWindow::delayedInit);
Lets add a new public method to the view class. In detacherview.h add
void createModels();
and for now with an empty body in detacherview.cpp (we will get to the implementation shortly)
void DetacherView::createModels()
{
}
and call it from DetacherWindow::delayedInit() after the Akonadi start succeeded
void DetacherWindow::delayedInit()
{
if (!Akonadi::Control::start() ) {
qApp->exit(-1);
return;
}
m_detacherView->createModels();
}
Connecting Views to Akonadi
The actual data connection between our views and Akonadi is conveniently handled by specialized models which are also provided by the KDE client library for Akonadi.
Actually, the data type the application will be working on, MIME messages, has an even further specialized model in a type specific sub library.
First we need to add two new KDE PIM library, in the CMakeLists.txt.
find_package(KF5AkonadiMime ${LIBKDEPIM_VERSION} CONFIG REQUIRED)
find_package(KF5Mime ${LIBKDEPIM_VERSION} CONFIG REQUIRED)
To properly link this additional libraries change the source directory's src/CMakeLists.txt to this
target_link_libraries(detacher
...
KF5::Mime
KF5::AkonadiMime
)
In detacherview.cpp add the following include directives
#include <CollectionFilterProxyModel>
#include <MessageModel>
#include <Monitor>
#include <EntityTreeModel>
With that we can now properly implement the createModels() method:
void DetacherView::createModels()
{
Akonadi::Monitor *monitor = new Akonadi::Monitor(this);
monitor->setObjectName(QStringLiteral("CollectionWidgetMonitor"));
monitor->fetchCollection(true);
monitor->setAllMonitored(true);
Akonadi::EntityTreeModel *treeModel = new Akonadi::EntityTreeModel(monitor, this);
Akonadi::CollectionFilterProxyModel *filterModel = new Akonadi::CollectionFilterProxyModel(this);
filterModel->setSourceModel(treeModel);
filterModel->addMimeTypeFilter(QLatin1String("message/rfc822"));
Akonadi::ItemModel *itemModel = new Akonadi::MessageModel(this);
m_ui.folderView->setModel(filterModel);
m_ui.messageView->setModel(itemModel);
connect(m_ui.folderView, SIGNAL(currentChanged(Akonadi::Collection)),
itemModel, SLOT(setCollection(Akonadi::Collection)));
}
TODO Update this paragraph The first line creates a CollectionModel which will get all "folders" from Akonadi and keep this data updated as long as the application is running.
However, since this includes collections for other data types as well, we need to filter for the data type we are interested in, MIME messages or in terms of MIME type message/rfc822. This kind of filtering is conveniently supplied in the form of a proxy model called CollectionFilterProxyModel.
The next model, MessageModel is an ItemModel specialized in dealing with our data type, messages.
Setting the models on the respective view almost completes the setup process, the only thing left is to connect the CollectionView to the MessageModel so it changes its data depending on which folder gets selected.
At this stage the application is already capable of showing all your mail folders and headers of all your e-mails!
Getting at the Attachments
This task can be split into two steps:
- Getting the message from Akonadi
- Getting the attachments from the message
Getting the message from Akonadi
While we could have instructed the MessageModel to get all data for each of its entries, the proper way is to retrieve it only for the items that get selected. Moreover we want to do this asynchronously because we don't want to block the application even is a message is really huge.
The KDE client library for Akonadi offers this kind of functionality through a job-based API, in this case ItemFetchJob.
Using this is quite simple. First we add a new include and a class forward declaration for detacherview.h
#include <akonadi/item.h>
class KJob;
In the private member section add an Item member:
private:
Ui::detacherview_base ui_detacherview_base;
Akonadi::Item mItem;
and two new slots in the private slots section:
private slots:
void switchColors();
void settingsChanged();
void itemChanged( const Akonadi::Item &item );
void itemFetchDone( KJob *job );
In the source file detacherview.cpp two new includes are required
#include <akonadi/itemfetchjob.h>
#include <akonadi/itemfetchscope.h>
for the implementation of the two new slots
void DetacherView::itemChanged( const Akonadi::Item &item )
{
// clear attachment list
ui_detacherview_base.attachmentList->clear();
// re-initialize the member we use for referencing the current item
mItem = Akonadi::Item();
// create fetch job and let it get the whole message
Akonadi::ItemFetchJob *fetchJob = new Akonadi::ItemFetchJob( item, this );
fetchJob->fetchScope().fetchFullPayload();
connect( fetchJob, SIGNAL( result( KJob* ) ), SLOT( itemFetchDone( KJob* ) ) );
}
void DetacherView::itemFetchDone( KJob *job )
{
Akonadi::ItemFetchJob *fetchJob = static_cast<Akonadi::ItemFetchJob*>( job );
if ( job->error() ) {
kError() << job->errorString();
return;
}
if ( fetchJob->items().isEmpty() ) {
kWarning() << "Job did not retrieve any items";
return;
}
mItem = fetchJob->items().first();
if ( !mItem.isValid() ) {
kWarning() << "Item not valid";
return;
}
}
To trigger the item fetching we connect the first new slot to a signal of the MessageView. In DetacherView::createModels() add another connect statement:
connect( ui_detacherview_base.messageView, SIGNAL( currentChanged( Akonadi::Item ) ),
SLOT( itemChanged( Akonadi::Item ) ) );
Getting the attachments from the message
With the item now fully available, we can proceed to check whether the message has any attachments and if it has display them in the right most widget of our GUI.
MIME messages in KDE are handled by the kmime library.
For the application this means two more includes and a typedef:
#include <kmime/kmime_message.h>
#include <boost/shared_ptr.hpp>
typedef boost::shared_ptr<KMime::Message> MessagePtr;
KMime::Message is the data type and we need the boost::shared_ptr to provide us with the value based behavior required by the item's payload methods.
Equipped with this new tools we can extend DetacherView::itemFetchDone() by adding the following code at its end:
if ( !mItem.hasPayload<MessagePtr>() ) {
kWarning() << "Item does not have message payload";
return;
}
const MessagePtr message = mItem.payload<MessagePtr>();
const KMime::Content::List attachments = message->attachments();
foreach ( KMime::Content *attachment, attachments ) {
const QString fileName = attachment->contentDisposition()->filename();
if ( fileName.isEmpty() )
continue;
ui_detacherview_base.attachmentList->addItem( fileName );
}
Detaching an Attachment
This task can again be split into sub tasks:
- Saving the selected attachment into a file
- Removing the selected attachment from the message
Saving Attachment into File
First we need a new slot in the private slots section of detacherview.h
void detachAttachment();
To implement the first sub task, saving the selected attachment into a file, the following new includes are needed in detacherview.cpp
#include <kaction.h>
#include <kfiledialog.h>
#include <kstandardaction.h>
In the class' constructor create and connect an action and make it available as the attachment list widget's context menu:
KAction *detachAction = KStandardAction::cut( this, SLOT( detachAttachment() ), this );
detachAction->setText( i18nc( "@action:button remove an attachment from an email",
"Detach..." ) );
ui_detacherview_base.attachmentList->addAction( detachAction );
ui_detacherview_base.attachmentList->setContextMenuPolicy( Qt::ActionsContextMenu );
To properly link the additional library for file dialog, change the source directory's CMakeLists.txt to this
target_link_libraries(detacher ${KDE4_KDEUI_LIBS} ${KDEPIMLIBS_AKONADI_LIBS} ${KDEPIMLIBS_AKONADI_KMIME_LIBS} ${KDEPIMLIBS_KMIME_LIBS} ${KDE4_KIO_LIBS})
In case the i18nc function or the text used in it are unexpected, see the Semantic Markup Tutorial.
Saving the attachment means we need to ask for a saving location, probably including a filename override and get the data from the message.
This can be implemented like this
void DetacherView::detachAttachment()
{
const QList<QListWidgetItem*> items = ui_detacherview_base.attachmentList->selectedItems();
if ( items.isEmpty() ) {
kDebug() << "No attachment selected";
return;
}
if ( !mItem.hasPayload<MessagePtr>() ) {
kWarning() << "Item no longer has a payload";
return;
}
// get the selected list item's text. it is the attachment's filename
const QString fileName = items.first()->text();
// ask for a saving location, using the attachment's filename as a
// suggestion
KFileDialog dialog( KUrl(), QString(), this );
dialog.setMode( KFile::Files | KFile::LocalOnly );
dialog.setOperationMode( KFileDialog::Saving );
dialog.setConfirmOverwrite( true );
dialog.setSelection( fileName );
if ( dialog.exec() != QDialog::Accepted ) {
kDebug() << "Saving cancelled. Aborting detaching";
return;
}
const QString saveFileName = dialog.selectedFile();
if ( saveFileName.isEmpty() ) {
kDebug() << "Empty target file name. Aborting detaching";
return;
}
// find the corresponding attachment data structure
const MessagePtr message = mItem.payload<MessagePtr>();
const KMime::Content::List attachments = message->attachments();
KMime::Content *selectedAttachment = 0;
foreach ( KMime::Content *attachment, attachments ) {
if ( fileName == attachment->contentDisposition()->filename() ) {
selectedAttachment = attachment;
break;
}
}
if ( selectedAttachment == 0 ) {
kWarning() << "Selected attachment file name no longer available in message. Aborting detaching";
return;
}
QFile file( saveFileName );
if ( !file.open( QIODevice::WriteOnly ) ) {
kError() << "Cannot open target file for writing. Aborting detaching.";
return;
}
file.write( selectedAttachment->decodedContent() );
file.close();
}
Removing the Attachment
After successfully saving the attachment we can proceed with removing the attachment from the message data and finally update the item in Akonadi to make it a permanent change.
For the last step another job class is needed: ItemModifyJob
#include <akonadi/itemmodifyjob.h>
and can be implemented by appending the following code to DetacherView::detachAttachment()
// remove attachment from message
message->removeContent( selectedAttachment, true );
// and from the listwidget
delete items.first();
// prepare Akonadi update
Akonadi::Item item( mItem );
item.setPayload<MessagePtr>( message );
Akonadi::ItemModifyJob *modifyJob = new Akonadi::ItemModifyJob( item, this );
if ( !modifyJob->exec() ) {
kError() << modifyJob->errorString();
return;
}