6#include "NewstuffModel.h"
8#include "MarbleDebug.h"
10#include "MarbleZipReader.h"
14#include <QTemporaryFile>
18#include <QFutureWatcher>
19#include <QtConcurrentRun>
20#include <QProcessEnvironment>
21#include <QMutexLocker>
23#include <QNetworkAccessManager>
24#include <QNetworkReply>
25#include <QDomDocument>
45 qint64 m_downloadedSize;
49 QString installedVersion()
const;
50 QString installedReleaseDate()
const;
51 bool isUpgradable()
const;
59class NewstuffModelPrivate
72 typedef QPair<int, UserAction>
Action;
74 NewstuffModel* m_parent;
92 NewstuffModel::IdTag m_idTag;
108 explicit NewstuffModelPrivate( NewstuffModel* parent );
110 QIcon preview(
int index );
111 void setPreview(
int index,
const QIcon &previewIcon );
115 static bool canExecute(
const QString &executable );
123 void uninstall(
int index );
131 static NewstuffItem importNode(
const QDomNode &node );
133 bool isTransitioning(
int index )
const;
140 static void readValue(
const QDomNode &node,
const QString &key, T* target );
146 FetchPreviewJob( NewstuffModelPrivate *modelPrivate,
int index );
151 NewstuffModelPrivate *
const m_modelPrivate;
155NewstuffItem::NewstuffItem() : m_payloadSize( -2 ), m_downloadedSize( 0 )
160QString NewstuffItem::installedVersion()
const
162 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName(
"version" );
163 if ( nodes.
size() == 1 ) {
170QString NewstuffItem::installedReleaseDate()
const
172 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName(
"releasedate" );
173 if ( nodes.
size() == 1 ) {
180bool NewstuffItem::isUpgradable()
const
182 bool installedOk, remoteOk;
183 double const installed = installedVersion().toDouble( &installedOk );
184 double const remote= m_version.toDouble( &remoteOk );
185 return installedOk && remoteOk && remote > installed;
191 QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName(
"installedfile" );
192 for (
int i=0; i<nodes.
count(); ++i ) {
198bool NewstuffItem::deeperThan(
const QString &one,
const QString &two)
203FetchPreviewJob::FetchPreviewJob( NewstuffModelPrivate *modelPrivate,
int index ) :
204 m_modelPrivate( modelPrivate ),
209void FetchPreviewJob::run(
const QByteArray &data )
217 const QIcon previewIcon( pixmap );
218 m_modelPrivate->setPreview( m_index, previewIcon );
221NewstuffModelPrivate::NewstuffModelPrivate( NewstuffModel* parent ) : m_parent( parent ),
222 m_networkAccessManager( nullptr ), m_currentReply( nullptr ), m_currentFile( nullptr ),
223 m_idTag( NewstuffModel::PayloadTag ), m_currentAction( -1, Install ), m_unpackProcess( nullptr )
228QIcon NewstuffModelPrivate::preview(
int index )
230 if ( m_items.at( index ).m_preview.isNull() ) {
231 QPixmap dummyPixmap( 136, 136 );
233 setPreview( index,
QIcon( dummyPixmap ) );
235 m_networkJobs.insert( reply,
new FetchPreviewJob(
this, index ) );
238 Q_ASSERT( !m_items.at( index ).m_preview.isNull() );
240 return m_items.at( index ).m_preview;
243void NewstuffModelPrivate::setPreview(
int index,
const QIcon &previewIcon )
245 NewstuffItem &item = m_items[index];
246 item.m_preview = previewIcon;
247 const QModelIndex affected = m_parent->index( index );
248 emit m_parent->dataChanged( affected, affected );
251void NewstuffModelPrivate::handleProviderData(
QNetworkReply *reply)
255 if ( !redirectionAttribute.
isNull() ) {
256 for (
int i=0; i<m_items.size(); ++i ) {
257 NewstuffItem &item = m_items[i];
258 if ( item.m_payloadUrl == reply->
url() ) {
259 item.m_payloadUrl = redirectionAttribute.
toUrl();
268 qint64 length = size.
value<qint64>();
269 for (
int i=0; i<m_items.size(); ++i ) {
270 NewstuffItem &item = m_items[i];
271 if ( item.m_payloadUrl == reply->
url() ) {
272 item.m_payloadSize = length;
274 emit m_parent->dataChanged( affected, affected );
281 FetchPreviewJob *
const job = m_networkJobs.take( reply );
285 if ( !redirectionAttribute.
isNull() ) {
288 m_networkJobs.insert( redirectReply, job );
301 mDebug() <<
"Cannot parse newstuff xml data ";
309 for (
int i=0 ; i < items.
length(); ++i ) {
310 m_items << importNode( items.
item( i ) );
316bool NewstuffModelPrivate::canExecute(
const QString &executable )
321 if ( application.exists() ) {
329void NewstuffModelPrivate::installMap()
331 if ( m_unpackProcess ) {
332 m_unpackProcess->close();
333 delete m_unpackProcess;
334 m_unpackProcess =
nullptr;
335 }
else if ( m_currentFile->fileName().endsWith(
QLatin1String(
"zip" ) ) ) {
338 else if ( m_currentFile->fileName().endsWith(
QLatin1String(
"tar.gz" ) ) && canExecute(
"tar" ) ) {
341 m_parent, SLOT(contentsListed(
int)) );
343 m_unpackProcess->setWorkingDirectory( m_targetDirectory );
344 m_unpackProcess->start(
"tar", arguments );
346 if ( !m_currentFile->fileName().endsWith(
QLatin1String(
"tar.gz" ) ) ) {
347 mDebug() <<
"Can only handle tar.gz files";
349 mDebug() <<
"Cannot extract archive: tar executable not found in PATH.";
354void NewstuffModelPrivate::unzip()
356 MarbleZipReader zipReader(m_currentFile->fileName());
358 for(
const MarbleZipReader::FileInfo &fileInfo: zipReader.fileInfoList()) {
359 files << fileInfo.filePath;
361 updateRegistry(files);
362 zipReader.extractAll(m_targetDirectory);
363 m_parent->mapInstalled(0);
366void NewstuffModelPrivate::updateModel()
368 QDomNodeList items = m_root.elementsByTagName(
"stuff" );
369 for (
int i=0 ; i < items.
length(); ++i ) {
370 QString const key = m_idTag == NewstuffModel::PayloadTag ?
"payload" :
"name";
372 if ( matches.
size() == 1 ) {
375 for (
int j=0; j<m_items.size() && !found; ++j ) {
376 NewstuffItem &item = m_items[j];
377 if ( m_idTag == NewstuffModel::PayloadTag && item.m_payloadUrl.toString() == value ) {
378 item.m_registryNode = items.
item( i );
381 if ( m_idTag == NewstuffModel::NameTag && item.m_name == value ) {
382 item.m_registryNode = items.
item( i );
389 NewstuffItem item = importNode( items.
item( i ) );
390 if ( m_idTag == NewstuffModel::PayloadTag ) {
391 item.m_registryNode = items.
item( i );
392 }
else if ( m_idTag == NewstuffModel::NameTag ) {
393 item.m_registryNode = items.
item( i );
400 m_parent->beginResetModel();
401 m_parent->endResetModel();
404void NewstuffModelPrivate::saveRegistry()
406 QFile output( m_registryFile );
408 mDebug() <<
"Cannot open " << m_registryFile <<
" for writing";
411 outStream << m_registryDocument.toString( 2 );
417void NewstuffModelPrivate::uninstall(
int index )
422 QStringList const files = m_items[index].installedFiles();
423 for(
const QString &file: files ) {
431 std::sort( directories.
begin(), directories.
end(), NewstuffItem::deeperThan );
432 for(
const QString &dir: directories ) {
436 m_items[index].m_registryNode.parentNode().removeChild( m_items[index].m_registryNode );
437 m_items[index].m_registryNode.clear();
443 if ( action == Append ) {
448 if ( !oldNode.
isNull() ) {
456void NewstuffModelPrivate::readValue(
const QDomNode &node,
const QString &key, T* target )
459 if ( matches.
size() == 1 ) {
462 for (
int i=0; i<matches.
size(); ++i ) {
472NewstuffModel::NewstuffModel(
QObject *parent ) :
475 setTargetDirectory(MarbleDirs::localPath() +
QLatin1String(
"/maps"));
483 roles[Name] =
"name";
484 roles[Author] =
"author";
485 roles[License] =
"license";
486 roles[Summary] =
"summary";
487 roles[Version] =
"version";
488 roles[ReleaseDate] =
"releasedate";
489 roles[Preview] =
"preview";
491 roles[InstalledVersion] =
"installedversion";
492 roles[InstalledReleaseDate] =
"installedreleasedate";
493 roles[InstalledFiles] =
"installedfiles";
494 roles[IsInstalled] =
"installed";
495 roles[IsUpgradable] =
"upgradable";
497 roles[IsTransitioning] =
"transitioning";
498 roles[PayloadSize] =
"size";
499 roles[DownloadedSize] =
"downloaded";
500 d->m_roleNames = roles;
503NewstuffModel::~NewstuffModel()
508int NewstuffModel::rowCount (
const QModelIndex &parent )
const
511 return d->m_items.
size();
519 if ( index.
isValid() && index.
row() >= 0 && index.
row() < d->m_items.size() ) {
523 case Name:
return d->m_items.at( index.
row() ).m_name;
524 case Author:
return d->m_items.at( index.
row() ).m_author;
525 case License:
return d->m_items.at( index.
row() ).m_license;
526 case Summary:
return d->m_items.at( index.
row() ).m_summary;
527 case Version:
return d->m_items.at( index.
row() ).m_version;
528 case ReleaseDate:
return d->m_items.at( index.
row() ).m_releaseDate;
529 case Preview:
return d->m_items.at( index.
row() ).m_previewUrl;
530 case Payload:
return d->m_items.at( index.
row() ).m_payloadUrl;
531 case InstalledVersion:
return d->m_items.at( index.
row() ).installedVersion();
532 case InstalledReleaseDate:
return d->m_items.at( index.
row() ).installedReleaseDate();
533 case InstalledFiles:
return d->m_items.at( index.
row() ).installedFiles();
534 case IsInstalled:
return !d->m_items.at( index.
row() ).m_registryNode.isNull();
535 case IsUpgradable:
return d->m_items.at( index.
row() ).isUpgradable();
536 case Category:
return d->m_items.at( index.
row() ).m_category;
537 case IsTransitioning:
return d->isTransitioning( index.
row() );
539 qint64
const size = d->m_items.at( index.
row() ).m_payloadSize;
540 QUrl const url = d->m_items.at( index.
row() ).m_payloadUrl;
541 if ( size < -1 && !url.
isEmpty() ) {
542 d->m_items[index.
row()].m_payloadSize = -1;
546 return qMax<qint64>( -1, size );
548 case DownloadedSize:
return d->m_items.at( index.
row() ).m_downloadedSize;
557 return d->m_roleNames;
561int NewstuffModel::count()
const
566void NewstuffModel::setProvider(
const QString &downloadUrl )
568 if ( downloadUrl == d->m_provider ) {
572 d->m_provider = downloadUrl;
573 emit providerChanged();
577QString NewstuffModel::provider()
const
579 return d->m_provider;
582void NewstuffModel::setTargetDirectory(
const QString &targetDirectory )
584 if ( targetDirectory != d->m_targetDirectory ) {
586 if ( !targetDir.exists() ) {
588 qDebug() <<
"Failed to create directory " << targetDirectory <<
", newstuff installation might fail.";
592 d->m_targetDirectory = targetDirectory;
593 emit targetDirectoryChanged();
597QString NewstuffModel::targetDirectory()
const
599 return d->m_targetDirectory;
602void NewstuffModel::setRegistryFile(
const QString &filename, IdTag idTag )
604 QString registryFile = filename;
609 if ( d->m_registryFile != registryFile ) {
610 d->m_registryFile = registryFile;
612 emit registryFileChanged();
615 if ( !inputFile.exists() ) {
617 d->m_registryDocument =
QDomDocument(
"khotnewstuff3" );
618 QDomProcessingInstruction header = d->m_registryDocument.createProcessingInstruction(
"xml",
"version=\"1.0\" encoding=\"utf-8\"" );
620 d->m_root = d->m_registryDocument.createElement(
"hotnewstuffregistry" );
623 QFile input( registryFile );
625 mDebug() <<
"Cannot open newstuff registry " << registryFile;
629 if ( !d->m_registryDocument.setContent( &input ) ) {
630 mDebug() <<
"Cannot parse newstuff registry " << registryFile;
634 d->m_root = d->m_registryDocument.documentElement();
641QString NewstuffModel::registryFile()
const
643 return d->m_registryFile;
646void NewstuffModel::install(
int index )
648 if ( index < 0 || index >= d->m_items.size() ) {
652 NewstuffModelPrivate::Action action( index, NewstuffModelPrivate::Install );
655 if ( d->m_actionQueue.contains( action ) ) {
658 d->m_actionQueue << action;
664void NewstuffModel::uninstall(
int idx )
666 if ( idx < 0 || idx >= d->m_items.size() ) {
670 if ( d->m_items[idx].m_registryNode.isNull() ) {
671 emit uninstallationFinished( idx );
674 NewstuffModelPrivate::Action action( idx, NewstuffModelPrivate::Uninstall );
677 if ( d->m_actionQueue.contains( action ) ) {
680 d->m_actionQueue << action;
686void NewstuffModel::cancel(
int index )
688 if ( !d->isTransitioning( index ) ) {
694 if ( d->m_currentAction.first == index ) {
695 if ( d->m_currentAction.second == NewstuffModelPrivate::Install ) {
696 if ( d->m_currentReply ) {
697 d->m_currentReply->abort();
698 d->m_currentReply->deleteLater();
699 d->m_currentReply =
nullptr;
702 if ( d->m_unpackProcess ) {
703 d->m_unpackProcess->terminate();
704 d->m_unpackProcess->deleteLater();
705 d->m_unpackProcess =
nullptr;
708 if ( d->m_currentFile ) {
709 d->m_currentFile->deleteLater();
710 d->m_currentFile =
nullptr;
713 d->m_items[d->m_currentAction.first].m_downloadedSize = 0;
715 emit installationFailed( d->m_currentAction.first, tr(
"Installation aborted by user." ) );
716 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
721 if ( d->m_currentAction.second == NewstuffModelPrivate::Install ) {
722 NewstuffModelPrivate::Action install( index, NewstuffModelPrivate::Install );
723 d->m_actionQueue.removeAll( install );
724 emit installationFailed( index, tr(
"Installation aborted by user." ) );
726 NewstuffModelPrivate::Action uninstall( index, NewstuffModelPrivate::Uninstall );
727 d->m_actionQueue.removeAll( uninstall );
728 emit uninstallationFinished( index );
736void NewstuffModel::updateProgress( qint64 bytesReceived, qint64 bytesTotal )
738 qreal
const progress = qBound<qreal>( 0.0, 0.9 * bytesReceived / qreal( bytesTotal ), 1.0 );
739 emit installationProgressed( d->m_currentAction.first, progress );
740 NewstuffItem &item = d->m_items[d->m_currentAction.first];
741 item.m_payloadSize = bytesTotal;
742 if ( qreal(bytesReceived-item.m_downloadedSize)/bytesTotal >= 0.01 || progress >= 0.9 ) {
744 item.m_downloadedSize = bytesReceived;
745 QModelIndex const affected = index( d->m_currentAction.first );
746 emit dataChanged( affected, affected );
750void NewstuffModel::retrieveData()
752 if ( d->m_currentReply && d->m_currentReply->isReadable() ) {
755 if ( !redirectionAttribute.
isNull() ) {
757 QObject::connect( d->m_currentReply, SIGNAL(readyRead()),
this, SLOT(retrieveData()) );
758 QObject::connect( d->m_currentReply, SIGNAL(readChannelFinished()),
this, SLOT(retrieveData()) );
759 QObject::connect( d->m_currentReply, SIGNAL(downloadProgress(qint64,qint64)),
760 this, SLOT(updateProgress(qint64,qint64)) );
762 d->m_currentFile->write( d->m_currentReply->readAll() );
763 if ( d->m_currentReply->isFinished() ) {
764 d->m_currentReply->deleteLater();
765 d->m_currentReply =
nullptr;
766 d->m_currentFile->flush();
773void NewstuffModel::mapInstalled(
int exitStatus )
775 if ( d->m_unpackProcess ) {
776 d->m_unpackProcess->deleteLater();
777 d->m_unpackProcess =
nullptr;
780 if ( d->m_currentFile ) {
781 d->m_currentFile->deleteLater();
782 d->m_currentFile =
nullptr;
785 emit installationProgressed( d->m_currentAction.first, 1.0 );
786 d->m_items[d->m_currentAction.first].m_downloadedSize = 0;
787 if ( exitStatus == 0 ) {
788 emit installationFinished( d->m_currentAction.first );
790 mDebug() <<
"Process exit status " << exitStatus <<
" indicates an error.";
791 emit installationFailed( d->m_currentAction.first ,
QString(
"Unable to unpack file. Process exited with status code %1." ).arg( exitStatus ) );
793 QModelIndex const affected = index( d->m_currentAction.first );
797 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
799 emit dataChanged( affected, affected );
803void NewstuffModel::mapUninstalled()
805 QModelIndex const affected = index( d->m_currentAction.first );
806 emit uninstallationFinished( d->m_currentAction.first );
810 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
812 emit dataChanged( affected, affected );
816void NewstuffModel::contentsListed(
int exitStatus )
818 if ( exitStatus == 0 ) {
820 d->updateRegistry(files);
823 this, SLOT(contentsListed(
int)) );
825 this, SLOT(mapInstalled(
int)) );
827 d->m_unpackProcess->start(
"tar", arguments );
829 mDebug() <<
"Process exit status " << exitStatus <<
" indicates an error.";
830 emit installationFailed( d->m_currentAction.first ,
QString(
"Unable to list file contents. Process exited with status code %1." ).arg( exitStatus ) );
834 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
840void NewstuffModelPrivate::updateRegistry(
const QStringList &files)
842 emit m_parent->installationProgressed( m_currentAction.first, 0.92 );
843 if ( !m_registryFile.isEmpty() ) {
844 NewstuffItem &item = m_items[m_currentAction.first];
845 QDomNode node = item.m_registryNode;
846 NewstuffModelPrivate::NodeAction action = node.
isNull() ? NewstuffModelPrivate::Append : NewstuffModelPrivate::Replace;
848 node = m_root.
appendChild( m_registryDocument.createElement(
"stuff" ) );
852 changeNode( node, m_registryDocument,
"name", item.m_name, action );
853 changeNode( node, m_registryDocument,
"providerid", m_provider, action );
854 changeNode( node, m_registryDocument,
"author", item.m_author, action );
855 changeNode( node, m_registryDocument,
"homepage",
QString(), action );
856 changeNode( node, m_registryDocument,
"licence", item.m_license, action );
857 changeNode( node, m_registryDocument,
"version", item.m_version, action );
858 QString const itemId = m_idTag == NewstuffModel::PayloadTag ? item.m_payloadUrl.toString() : item.m_name;
859 changeNode( node, m_registryDocument,
"id", itemId, action );
860 changeNode( node, m_registryDocument,
"releasedate", item.m_releaseDate, action );
861 changeNode( node, m_registryDocument,
"summary", item.m_summary, action );
862 changeNode( node, m_registryDocument,
"changelog",
QString(), action );
863 changeNode( node, m_registryDocument,
"preview", item.m_previewUrl.toString(), action );
864 changeNode( node, m_registryDocument,
"previewBig", item.m_previewUrl.toString(), action );
865 changeNode( node, m_registryDocument,
"payload", item.m_payloadUrl.toString(), action );
866 changeNode( node, m_registryDocument,
"status",
"installed", action );
867 m_items[m_currentAction.first].m_registryNode = node;
869 bool hasChildren =
true;
870 while ( hasChildren ) {
873 hasChildren = !fileList.
isEmpty();
874 for (
int i=0; i<fileList.
count(); ++i ) {
879 for(
const QString &file: files ) {
880 QDomNode fileNode = node.
appendChild( m_registryDocument.createElement(
"installedfile" ) );
888void NewstuffModelPrivate::processQueue()
890 if ( m_actionQueue.empty() || m_currentAction.first >= 0 ) {
896 m_currentAction = m_actionQueue.takeFirst();
898 if ( m_currentAction.second == Install ) {
899 if ( !m_currentFile ) {
900 QFileInfo const file = m_items.at( m_currentAction.first ).m_payloadUrl.
path();
904 if ( m_currentFile->open() ) {
905 QUrl const payload = m_items.at( m_currentAction.first ).m_payloadUrl;
906 m_currentReply = m_networkAccessManager.get(
QNetworkRequest( payload ) );
907 QObject::connect( m_currentReply, SIGNAL(readyRead()), m_parent, SLOT(retrieveData()) );
908 QObject::connect( m_currentReply, SIGNAL(readChannelFinished()), m_parent, SLOT(retrieveData()) );
910 m_parent, SLOT(updateProgress(qint64,qint64)) );
913 mDebug() <<
"Failed to write to " << m_currentFile->fileName();
918 QObject::connect( watcher, SIGNAL(finished()), m_parent, SLOT(mapUninstalled()) );
919 QObject::connect( watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()) );
926NewstuffItem NewstuffModelPrivate::importNode(
const QDomNode &node)
930 readValue<QString>( node,
"name", &item.m_name );
931 readValue<QString>( node,
"author", &item.m_author );
932 readValue<QString>( node,
"licence", &item.m_license );
933 readValue<QString>( node,
"summary", &item.m_summary );
934 readValue<QString>( node,
"version", &item.m_version );
935 readValue<QString>( node,
"releasedate", &item.m_releaseDate );
936 readValue<QUrl>( node,
"preview", &item.m_previewUrl );
937 readValue<QUrl>( node,
"payload", &item.m_payloadUrl );
941bool NewstuffModelPrivate::isTransitioning(
int index )
const
943 if ( m_currentAction.first == index ) {
947 for(
const Action &action: m_actionQueue ) {
948 if ( action.first == index ) {
958#include "moc_NewstuffModel.cpp"
KIOCORE_EXPORT MkpathJob * mkpath(const QUrl &url, const QUrl &baseUrl=QUrl(), JobFlags flags=DefaultFlags)
QString path(const QString &relativePath)
Binds a QML item to a specific geodetic location in screen coordinates.
bool mkpath(const QString &dirPath) const const
bool rmdir(const QString &dirName) const const
QString value() const const
QDomElement createElement(const QString &tagName)
QDomText createTextNode(const QString &value)
QDomElement documentElement() const const
ParseResult setContent(QAnyStringView text, ParseOptions options)
QDomNodeList elementsByTagName(const QString &tagname) const const
void setAttribute(const QString &name, const QString &value)
QString text() const const
bool contains(const QString &name) const const
QDomNode namedItem(const QString &name) const const
QDomNode appendChild(const QDomNode &newChild)
QDomNamedNodeMap attributes() const const
QDomNode firstChild() const const
bool isNull() const const
QDomNode namedItem(const QString &name) const const
QDomNode removeChild(const QDomNode &oldChild)
QDomAttr toAttr() const const
QDomElement toElement() const const
QDomNode at(int index) const const
bool isEmpty() const const
QDomNode item(int index) const const
QString fileName() const const
QString path() const const
void setFuture(const QFuture< T > &future)
qsizetype size() const const
QImage fromData(QByteArrayView data, const char *format)
bool isNull() const const
bool isValid() const const
QVariant attribute(QNetworkRequest::Attribute code) const const
QNetworkAccessManager::Operation operation() const const
RedirectionTargetAttribute
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QPixmap fromImage(QImage &&image, Qt::ImageConversionFlags flags)
QProcessEnvironment systemEnvironment()
QString value(const QString &name, const QString &defaultValue) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QFuture< T > run(Function function,...)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isEmpty() const const
bool isNull() const const
bool isValid() const const