2010-10-26 14:01:59 +02:00
/*
* Copyright 2010 by Marco Martin < mart @ kde . org >
* This program 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 , or
* ( at your option ) any later version .
*
* This program 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 General Public License for more details
*
* You should have received a copy of the GNU Library General Public
* License along with this program ; if not , write to the
* Free Software Foundation , Inc . ,
* 51 Franklin Street , Fifth Floor , Boston , MA 02110 - 1301 , USA .
*/
# include "datamodel.h"
2011-11-01 19:27:55 +01:00
# include "datasource.h"
2010-10-26 14:01:59 +02:00
2011-04-07 22:37:34 +02:00
# include <QTimer>
2010-10-26 14:01:59 +02:00
# include <KDebug>
namespace Plasma
{
2010-10-28 15:10:10 +02:00
SortFilterModel : : SortFilterModel ( QObject * parent )
2010-10-26 23:19:04 +02:00
: QSortFilterProxyModel ( parent )
2010-10-26 14:01:59 +02:00
{
2010-10-28 15:10:10 +02:00
setObjectName ( " SortFilterModel " ) ;
2010-10-26 23:19:04 +02:00
setDynamicSortFilter ( true ) ;
2010-12-05 23:23:13 +01:00
connect ( this , SIGNAL ( rowsInserted ( const QModelIndex & , int , int ) ) ,
this , SIGNAL ( countChanged ( ) ) ) ;
connect ( this , SIGNAL ( rowsRemoved ( const QModelIndex & , int , int ) ) ,
this , SIGNAL ( countChanged ( ) ) ) ;
connect ( this , SIGNAL ( modelReset ( ) ) ,
this , SIGNAL ( countChanged ( ) ) ) ;
2010-10-26 14:01:59 +02:00
}
2010-10-28 15:10:10 +02:00
SortFilterModel : : ~ SortFilterModel ( )
2010-10-26 14:01:59 +02:00
{
}
2010-10-28 15:10:10 +02:00
void SortFilterModel : : syncRoleNames ( )
2010-10-26 23:19:04 +02:00
{
2010-10-28 15:06:40 +02:00
m_roleIds . clear ( ) ;
2010-10-26 23:19:04 +02:00
2010-10-28 15:06:40 +02:00
setRoleNames ( sourceModel ( ) - > roleNames ( ) ) ;
QHash < int , QByteArray > : : const_iterator i ;
for ( i = roleNames ( ) . constBegin ( ) ; i ! = roleNames ( ) . constEnd ( ) ; + + i ) {
m_roleIds [ i . value ( ) ] = i . key ( ) ;
}
setFilterRole ( m_filterRole ) ;
setSortRole ( m_sortRole ) ;
2010-10-26 23:19:04 +02:00
}
2010-10-28 15:10:10 +02:00
int SortFilterModel : : roleNameToId ( const QString & name )
2010-10-26 23:19:04 +02:00
{
2010-10-28 15:06:40 +02:00
if ( ! m_roleIds . contains ( name ) ) {
2011-10-30 00:50:41 +02:00
return Qt : : DisplayRole ;
2010-10-28 15:06:40 +02:00
}
return m_roleIds . value ( name ) ;
2010-10-26 23:19:04 +02:00
}
2010-10-28 15:10:10 +02:00
void SortFilterModel : : setModel ( QObject * source )
2010-10-26 23:19:04 +02:00
{
2010-10-28 15:06:40 +02:00
QAbstractItemModel * model = qobject_cast < QAbstractItemModel * > ( source ) ;
if ( ! model ) {
kWarning ( ) < < " Error: QAbstractItemModel type expected " ;
return ;
}
connect ( model , SIGNAL ( modelReset ( ) ) , this , SLOT ( syncRoleNames ( ) ) ) ;
QSortFilterProxyModel : : setSourceModel ( model ) ;
2010-10-26 23:19:04 +02:00
}
2010-10-28 15:06:40 +02:00
2010-10-28 15:10:10 +02:00
void SortFilterModel : : setFilterRegExp ( const QString & exp )
2010-10-26 23:19:04 +02:00
{
2010-12-06 14:28:45 +01:00
//FIXME: this delaying of the reset signal seems to make the views behave a bit better, i.e. less holes and avoids some crashes, in theory shouldn't be necessary
2011-06-24 23:54:07 +02:00
beginResetModel ( ) ;
2010-12-06 14:28:45 +01:00
blockSignals ( true ) ;
2010-10-29 14:27:38 +02:00
QSortFilterProxyModel : : setFilterRegExp ( QRegExp ( exp , Qt : : CaseInsensitive ) ) ;
2010-12-06 14:28:45 +01:00
blockSignals ( false ) ;
2011-06-24 23:54:07 +02:00
endResetModel ( ) ;
2010-10-26 23:19:04 +02:00
}
2010-10-28 15:10:10 +02:00
QString SortFilterModel : : filterRegExp ( ) const
2010-10-26 23:19:04 +02:00
{
return QSortFilterProxyModel : : filterRegExp ( ) . pattern ( ) ;
}
2010-10-28 15:10:10 +02:00
void SortFilterModel : : setFilterRole ( const QString & role )
2010-10-26 23:19:04 +02:00
{
2010-10-28 15:06:40 +02:00
QSortFilterProxyModel : : setFilterRole ( roleNameToId ( role ) ) ;
2010-10-26 23:19:04 +02:00
m_filterRole = role ;
}
2010-10-28 15:10:10 +02:00
QString SortFilterModel : : filterRole ( ) const
2010-10-26 23:19:04 +02:00
{
return m_filterRole ;
}
2010-10-28 15:10:10 +02:00
void SortFilterModel : : setSortRole ( const QString & role )
2010-10-26 23:19:04 +02:00
{
2010-10-28 15:06:40 +02:00
QSortFilterProxyModel : : setSortRole ( roleNameToId ( role ) ) ;
2010-10-26 23:19:04 +02:00
m_sortRole = role ;
2010-10-27 23:19:03 +02:00
sort ( 0 , sortOrder ( ) ) ;
2010-10-26 23:19:04 +02:00
}
2010-10-28 15:10:10 +02:00
QString SortFilterModel : : sortRole ( ) const
2010-10-26 23:19:04 +02:00
{
return m_sortRole ;
}
2010-10-28 15:10:10 +02:00
void SortFilterModel : : setSortOrder ( const Qt : : SortOrder order )
2010-10-27 23:19:03 +02:00
{
sort ( 0 , order ) ;
}
2011-11-25 18:47:34 +01:00
QVariantHash SortFilterModel : : get ( int row ) const
{
QModelIndex idx = index ( row , 0 ) ;
QVariantHash hash ;
QHash < int , QByteArray > : : const_iterator i ;
for ( i = roleNames ( ) . constBegin ( ) ; i ! = roleNames ( ) . constEnd ( ) ; + + i ) {
hash [ i . value ( ) ] = data ( idx , i . key ( ) ) ;
}
return hash ;
}
2010-10-27 23:19:03 +02:00
2010-10-26 23:19:04 +02:00
2010-10-28 15:10:10 +02:00
DataModel : : DataModel ( QObject * parent )
2010-10-26 23:19:04 +02:00
: QAbstractItemModel ( parent ) ,
2011-01-20 23:03:52 +01:00
m_dataSource ( 0 ) ,
2011-02-19 14:10:59 +01:00
m_maxRoleId ( Qt : : UserRole + 1 )
2010-10-26 23:19:04 +02:00
{
2011-02-19 14:10:59 +01:00
//There is one reserved role name: DataEngineSource
m_roleNames [ m_maxRoleId ] = " DataEngineSource " ;
m_roleIds [ " DataEngineSource " ] = m_maxRoleId ;
+ + m_maxRoleId ;
2010-10-28 15:10:10 +02:00
setObjectName ( " DataModel " ) ;
2010-12-05 23:23:13 +01:00
connect ( this , SIGNAL ( rowsInserted ( const QModelIndex & , int , int ) ) ,
this , SIGNAL ( countChanged ( ) ) ) ;
connect ( this , SIGNAL ( rowsRemoved ( const QModelIndex & , int , int ) ) ,
this , SIGNAL ( countChanged ( ) ) ) ;
connect ( this , SIGNAL ( modelReset ( ) ) ,
this , SIGNAL ( countChanged ( ) ) ) ;
2010-10-26 23:19:04 +02:00
}
2010-10-28 15:06:40 +02:00
DataModel : : ~ DataModel ( )
2010-10-26 23:19:04 +02:00
{
}
2010-11-08 11:27:36 +01:00
void DataModel : : dataUpdated ( const QString & sourceName , const Plasma : : DataEngine : : Data & data )
{
2011-09-22 22:43:32 +02:00
if ( ! m_sourceFilter . isEmpty ( ) & & m_sourceFilterRE . isValid ( ) & & ! m_sourceFilterRE . exactMatch ( sourceName ) ) {
return ;
}
if ( m_keyRoleFilter . isEmpty ( ) ) {
//an item is represented by a source: keys are roles m_roleLevel == FirstLevel
2010-10-26 15:18:19 +02:00
QVariantList list ;
2010-11-08 11:27:36 +01:00
2011-02-19 14:10:59 +01:00
if ( ! m_dataSource - > data ( ) . isEmpty ( ) ) {
2011-09-22 22:43:32 +02:00
QVariantHash : : const_iterator i = m_dataSource - > data ( ) . constBegin ( ) ;
2011-04-09 20:14:22 +02:00
2011-02-19 14:10:59 +01:00
while ( i ! = m_dataSource - > data ( ) . constEnd ( ) ) {
QVariant value = i . value ( ) ;
2011-09-22 22:43:32 +02:00
if ( value . isValid ( ) & & value . canConvert < Plasma : : DataEngine : : Data > ( ) ) {
2011-02-19 14:10:59 +01:00
Plasma : : DataEngine : : Data data = value . value < Plasma : : DataEngine : : Data > ( ) ;
data [ " DataEngineSource " ] = i . key ( ) ;
list . append ( data ) ;
}
+ + i ;
}
2010-10-26 15:18:19 +02:00
}
2010-11-08 11:27:36 +01:00
setItems ( QString ( ) , list ) ;
2011-09-22 22:43:32 +02:00
} else {
//a key that matches the one we want exists and is a list of DataEngine::Data
if ( data . contains ( m_keyRoleFilter ) & &
data . value ( m_keyRoleFilter ) . canConvert < QVariantList > ( ) ) {
setItems ( sourceName , data . value ( m_keyRoleFilter ) . value < QVariantList > ( ) ) ;
} else if ( m_keyRoleFilterRE . isValid ( ) ) {
//try to match the key we want with a regular expression if set
QVariantList list ;
QHash < QString , QVariant > : : const_iterator i ;
for ( i = data . constBegin ( ) ; i ! = data . constEnd ( ) ; + + i ) {
if ( m_keyRoleFilterRE . exactMatch ( i . key ( ) ) ) {
list . append ( i . value ( ) ) ;
}
}
setItems ( sourceName , list ) ;
}
2010-10-26 15:18:19 +02:00
}
}
2010-10-28 15:06:40 +02:00
void DataModel : : setDataSource ( QObject * object )
2010-10-26 15:18:19 +02:00
{
DataSource * source = qobject_cast < DataSource * > ( object ) ;
if ( ! source ) {
kWarning ( ) < < " Error: DataSource type expected " ;
return ;
}
if ( m_dataSource = = source ) {
return ;
}
2011-09-22 22:43:32 +02:00
if ( m_dataSource ) {
disconnect ( m_dataSource , 0 , this , 0 ) ;
}
2010-10-26 15:18:19 +02:00
m_dataSource = source ;
2011-09-15 23:05:44 +02:00
2011-09-22 22:43:32 +02:00
const QHash < QString , QVariant > data = source - > data ( ) ;
QHash < QString , QVariant > : : const_iterator i = data . constBegin ( ) ;
while ( i ! = data . constEnd ( ) ) {
2011-09-15 23:05:44 +02:00
dataUpdated ( i . key ( ) , i . value ( ) . value < Plasma : : DataEngine : : Data > ( ) ) ;
+ + i ;
}
2010-10-26 15:18:19 +02:00
connect ( m_dataSource , SIGNAL ( newData ( const QString & , const Plasma : : DataEngine : : Data & ) ) ,
this , SLOT ( dataUpdated ( const QString & , const Plasma : : DataEngine : : Data & ) ) ) ;
2010-11-09 00:07:25 +01:00
connect ( m_dataSource , SIGNAL ( sourceRemoved ( const QString & ) ) , this , SLOT ( removeSource ( const QString & ) ) ) ;
connect ( m_dataSource , SIGNAL ( sourceDisconnected ( const QString & ) ) , this , SLOT ( removeSource ( const QString & ) ) ) ;
2010-10-26 15:18:19 +02:00
}
2010-10-28 15:06:40 +02:00
QObject * DataModel : : dataSource ( ) const
2010-10-26 15:18:19 +02:00
{
return m_dataSource ;
}
2011-04-03 15:29:54 +02:00
void DataModel : : setKeyRoleFilter ( const QString & key )
2010-10-26 15:18:19 +02:00
{
2011-09-22 22:43:32 +02:00
// the "key role filter" can be used in one of three ways:
//
// 1) empty string -> all data is used, each source is one row in the model
// 2) matches a key in the data exactly -> only that key/value pair is used, and the value is
// treated as a collection where each item in the collection becomes a row in the model
// 3) regular expression -> matches zero or more keys in the data, and each matching key/value
// pair becomes a row in the model
2010-11-08 11:27:36 +01:00
if ( m_keyRoleFilter = = key ) {
2010-10-26 15:18:19 +02:00
return ;
}
2010-11-08 11:27:36 +01:00
m_keyRoleFilter = key ;
2011-09-22 22:43:32 +02:00
m_keyRoleFilterRE = QRegExp ( m_keyRoleFilter ) ;
}
QString DataModel : : keyRoleFilter ( ) const
{
return m_keyRoleFilter ;
2010-10-26 15:18:19 +02:00
}
2011-04-09 20:14:22 +02:00
void DataModel : : setSourceFilter ( const QString & key )
{
if ( m_sourceFilter = = key ) {
return ;
}
m_sourceFilter = key ;
2011-09-22 22:43:32 +02:00
m_sourceFilterRE = QRegExp ( key ) ;
/*
FIXME : if the user changes the source filter , it won ' t immediately be reflected in the
available data
if ( m_sourceFilterRE . isValid ( ) ) {
. . iterate through all items and weed out the ones that don ' t match . .
}
*/
2011-04-09 20:14:22 +02:00
}
QString DataModel : : sourceFilter ( ) const
{
return m_sourceFilter ;
}
2010-11-08 11:27:36 +01:00
void DataModel : : setItems ( const QString & sourceName , const QVariantList & list )
2010-10-26 14:01:59 +02:00
{
2012-02-29 14:37:17 +01:00
const int oldLength = m_items . value ( sourceName ) . count ( ) ;
const int delta = list . length ( ) - oldLength ;
const bool firstRun = m_items . isEmpty ( ) ;
//At what row number the first item associated to this source starts
int sourceIndex = 0 ;
QMap < QString , QVector < QVariant > > : : const_iterator i ;
for ( i = m_items . constBegin ( ) ; i ! = m_items . constEnd ( ) ; + + i ) {
if ( i . key ( ) = = sourceName ) {
break ;
}
sourceIndex + = i . value ( ) . count ( ) ;
}
//signal as inserted the rows at the end, all the other rows will signal a dataupdated.
//better than a model reset because doesn't cause deletion and re-creation of every list item on a qml ListView, repeaters etc.
//the first run it gets reset because otherwise setRoleNames gets broken
if ( firstRun ) {
beginResetModel ( ) ;
} else if ( delta > 0 ) {
beginInsertRows ( QModelIndex ( ) , sourceIndex + oldLength , sourceIndex + list . length ( ) - 1 ) ;
} else if ( delta < 0 ) {
beginRemoveRows ( QModelIndex ( ) , sourceIndex + list . length ( ) , sourceIndex + oldLength - 1 ) ;
}
2010-10-26 14:01:59 +02:00
//convert to vector, so data() will be O(1)
2010-11-08 11:27:36 +01:00
m_items [ sourceName ] = list . toVector ( ) ;
2010-10-26 14:01:59 +02:00
2010-10-26 17:48:39 +02:00
if ( ! list . isEmpty ( ) ) {
2010-10-26 19:32:08 +02:00
if ( list . first ( ) . canConvert < QVariantHash > ( ) ) {
2011-09-12 21:29:00 +02:00
foreach ( const QVariant & item , list ) {
2011-09-22 22:43:32 +02:00
const QVariantHash & vh = item . value < QVariantHash > ( ) ;
QHashIterator < QString , QVariant > it ( vh ) ;
while ( it . hasNext ( ) ) {
it . next ( ) ;
const QString & roleName = it . key ( ) ;
2011-09-12 21:29:00 +02:00
if ( ! m_roleIds . contains ( roleName ) ) {
+ + m_maxRoleId ;
m_roleNames [ m_maxRoleId ] = roleName . toLatin1 ( ) ;
m_roleIds [ roleName ] = m_maxRoleId ;
}
2011-01-20 23:03:52 +01:00
}
2010-10-26 19:32:08 +02:00
}
} else {
2011-09-12 21:29:00 +02:00
foreach ( const QVariant & item , list ) {
2011-09-22 22:43:32 +02:00
const QVariantMap & vh = item . value < QVariantMap > ( ) ;
QMapIterator < QString , QVariant > it ( vh ) ;
while ( it . hasNext ( ) ) {
it . next ( ) ;
const QString & roleName = it . key ( ) ;
2011-09-12 21:29:00 +02:00
if ( ! m_roleIds . contains ( roleName ) ) {
+ + m_maxRoleId ;
m_roleNames [ m_maxRoleId ] = roleName . toLatin1 ( ) ;
m_roleIds [ roleName ] = m_maxRoleId ;
}
2011-01-20 23:03:52 +01:00
}
2010-10-26 19:32:08 +02:00
}
2010-10-26 17:48:39 +02:00
}
2010-10-26 23:19:04 +02:00
2010-10-26 17:48:39 +02:00
setRoleNames ( m_roleNames ) ;
2010-10-26 14:01:59 +02:00
}
2011-04-07 22:37:34 +02:00
setRoleNames ( m_roleNames ) ;
2012-02-29 14:37:17 +01:00
if ( firstRun ) {
endResetModel ( ) ;
} else if ( delta > 0 ) {
endInsertRows ( ) ;
} else if ( delta < 0 ) {
endRemoveRows ( ) ;
}
emit dataChanged ( createIndex ( sourceIndex , 0 ) ,
createIndex ( sourceIndex + qMin ( list . length ( ) , oldLength ) , 0 ) ) ;
2010-10-26 14:01:59 +02:00
}
2010-11-09 00:07:25 +01:00
void DataModel : : removeSource ( const QString & sourceName )
{
//FIXME: this could be way more efficient by not resetting the whole model
2010-11-09 00:10:16 +01:00
//FIXME: find a way to remove only the proper things also in the case where sources are items
2011-07-25 19:11:36 +02:00
2011-09-22 22:43:32 +02:00
if ( m_keyRoleFilter . isEmpty ( ) ) {
//source name in the map, linear scan
2011-06-16 19:29:09 +02:00
for ( int i = 0 ; i < m_items . value ( QString ( ) ) . count ( ) ; + + i ) {
if ( m_items . value ( QString ( ) ) [ i ] . value < QVariantHash > ( ) . value ( " DataEngineSource " ) = = sourceName ) {
2011-07-25 19:11:36 +02:00
beginResetModel ( ) ;
2011-06-16 19:29:09 +02:00
m_items [ QString ( ) ] . remove ( i ) ;
2011-07-25 19:11:36 +02:00
endResetModel ( ) ;
2011-06-16 19:29:09 +02:00
break ;
}
}
2011-09-22 22:43:32 +02:00
} else {
//source name as key of the map
if ( m_items . contains ( sourceName ) ) {
beginResetModel ( ) ;
m_items . remove ( sourceName ) ;
endResetModel ( ) ;
}
2011-06-16 19:29:09 +02:00
}
2010-11-09 00:07:25 +01:00
}
2010-10-28 15:06:40 +02:00
QVariant DataModel : : data ( const QModelIndex & index , int role ) const
2010-10-26 14:01:59 +02:00
{
if ( ! index . isValid ( ) | | index . column ( ) > 0 | |
2010-11-08 11:27:36 +01:00
index . row ( ) < 0 | | index . row ( ) > = countItems ( ) ) {
2010-10-26 14:01:59 +02:00
return QVariant ( ) ;
}
2010-11-08 11:27:36 +01:00
int count = 0 ;
int actualRow = 0 ;
QString source ;
QMap < QString , QVector < QVariant > > : : const_iterator i ;
for ( i = m_items . constBegin ( ) ; i ! = m_items . constEnd ( ) ; + + i ) {
const int oldCount = count ;
count + = i . value ( ) . count ( ) ;
if ( index . row ( ) < count ) {
source = i . key ( ) ;
actualRow = index . row ( ) - oldCount ;
break ;
}
}
2011-02-19 14:10:59 +01:00
//is it the reserved role: DataEngineSource ?
//also, if each source is an item DataEngineSource is a role between all the others, otherwise we know it from the role variable
2011-09-22 22:43:32 +02:00
//finally, sub items are some times QVariantHash some times QVariantMaps
2011-02-19 14:10:59 +01:00
if ( ! m_keyRoleFilter . isEmpty ( ) & & m_roleNames . value ( role ) = = " DataEngineSource " ) {
return source ;
} else if ( m_items . value ( source ) . value ( actualRow ) . canConvert < QVariantHash > ( ) ) {
2010-11-08 11:27:36 +01:00
return m_items . value ( source ) . value ( actualRow ) . value < QVariantHash > ( ) . value ( m_roleNames . value ( role ) ) ;
2010-10-26 19:32:08 +02:00
} else {
2010-11-08 11:27:36 +01:00
return m_items . value ( source ) . value ( actualRow ) . value < QVariantMap > ( ) . value ( m_roleNames . value ( role ) ) ;
2010-10-26 19:32:08 +02:00
}
2010-10-26 14:01:59 +02:00
}
2010-10-28 15:06:40 +02:00
QVariant DataModel : : headerData ( int section , Qt : : Orientation orientation , int role ) const
2010-10-26 14:01:59 +02:00
{
Q_UNUSED ( section )
Q_UNUSED ( orientation )
Q_UNUSED ( role )
return QVariant ( ) ;
}
2010-10-28 15:06:40 +02:00
QModelIndex DataModel : : index ( int row , int column , const QModelIndex & parent ) const
2010-10-26 14:01:59 +02:00
{
2010-11-08 11:27:36 +01:00
if ( parent . isValid ( ) | | column > 0 | | row < 0 | | row > = countItems ( ) ) {
2010-10-26 14:01:59 +02:00
return QModelIndex ( ) ;
}
return createIndex ( row , column , 0 ) ;
}
2010-10-28 15:06:40 +02:00
QModelIndex DataModel : : parent ( const QModelIndex & child ) const
2010-10-26 14:01:59 +02:00
{
Q_UNUSED ( child )
return QModelIndex ( ) ;
}
2010-10-28 15:06:40 +02:00
int DataModel : : rowCount ( const QModelIndex & parent ) const
2010-10-26 14:01:59 +02:00
{
//this is not a tree
//TODO: make it possible some day?
if ( parent . isValid ( ) ) {
return 0 ;
}
2010-11-08 11:27:36 +01:00
return countItems ( ) ;
2010-10-26 14:01:59 +02:00
}
2010-10-28 15:06:40 +02:00
int DataModel : : columnCount ( const QModelIndex & parent ) const
2010-10-26 14:01:59 +02:00
{
if ( parent . isValid ( ) ) {
return 0 ;
}
return 1 ;
}
2011-11-25 18:47:34 +01:00
QVariantHash DataModel : : get ( int row ) const
{
QModelIndex idx = index ( row , 0 ) ;
QVariantHash hash ;
QHash < int , QByteArray > : : const_iterator i ;
for ( i = roleNames ( ) . constBegin ( ) ; i ! = roleNames ( ) . constEnd ( ) ; + + i ) {
hash [ i . value ( ) ] = data ( idx , i . key ( ) ) ;
}
return hash ;
}
2010-10-28 15:06:40 +02:00
int DataModel : : roleNameToId ( const QString & name )
2010-10-26 23:19:04 +02:00
{
if ( ! m_roleIds . contains ( name ) ) {
return - 1 ;
}
return m_roleIds . value ( name ) ;
}
2010-10-26 14:01:59 +02:00
}
2010-10-28 15:10:10 +02:00
2010-10-26 14:01:59 +02:00
# include "datamodel.moc"