diff --git a/Libs/Core/ctkAbstractJob.cpp b/Libs/Core/ctkAbstractJob.cpp index 82187e5e11..fc9f923550 100644 --- a/Libs/Core/ctkAbstractJob.cpp +++ b/Libs/Core/ctkAbstractJob.cpp @@ -45,12 +45,6 @@ ctkAbstractJob::~ctkAbstractJob() { } -//---------------------------------------------------------------------------- -void ctkAbstractJob::setJobUID(const QString &jobUID) -{ - this->JobUID = jobUID; -} - //---------------------------------------------------------------------------- QString ctkAbstractJob::className() const { @@ -67,6 +61,12 @@ QString ctkAbstractJob::jobUID() const return this->JobUID; } +//---------------------------------------------------------------------------- +void ctkAbstractJob::setJobUID(const QString &jobUID) +{ + this->JobUID = jobUID; +} + //---------------------------------------------------------------------------- ctkAbstractJob::JobStatus ctkAbstractJob::status() const { @@ -91,9 +91,13 @@ void ctkAbstractJob::setStatus(JobStatus status) { emit this->started(); } - else if (this->Status == JobStatus::Stopped) + else if (this->Status == JobStatus::UserStopped) + { + emit this->userStopped(); + } + else if (this->Status == JobStatus::AttemptFailed) { - emit this->canceled(); + emit this->attemptFailed(); } else if (this->Status == JobStatus::Failed) { @@ -195,6 +199,35 @@ QDateTime ctkAbstractJob::completionDateTime() const return this->CompletionDateTime; } +//---------------------------------------------------------------------------- +QString ctkAbstractJob::runningThreadID() const +{ + return this->RunningThreadID; +} + +//---------------------------------------------------------------------------- +void ctkAbstractJob::setRunningThreadID(QString runningThreadID) +{ + this->RunningThreadID = runningThreadID; +} + +//---------------------------------------------------------------------------- +QString ctkAbstractJob::loggedText() const +{ + return this->LoggedText; +} + +//---------------------------------------------------------------------------- +void ctkAbstractJob::setLoggedText(QString loggedText) +{ + if (loggedText.isEmpty()) + { + return; + } + + this->LoggedText += loggedText; +} + //---------------------------------------------------------------------------- QVariant ctkAbstractJob::toVariant() { diff --git a/Libs/Core/ctkAbstractJob.h b/Libs/Core/ctkAbstractJob.h index 199d68e111..70cce73b8a 100644 --- a/Libs/Core/ctkAbstractJob.h +++ b/Libs/Core/ctkAbstractJob.h @@ -54,6 +54,8 @@ class CTK_CORE_EXPORT ctkAbstractJob : public QObject Q_PROPERTY(QDateTime creationDateTime READ creationDateTime); Q_PROPERTY(QDateTime startDateTime READ startDateTime); Q_PROPERTY(QDateTime completionDateTime READ completionDateTime); + Q_PROPERTY(QString runningThreadID READ runningThreadID WRITE setRunningThreadID); + Q_PROPERTY(QString loggedText READ loggedText WRITE setLoggedText); public: explicit ctkAbstractJob(); @@ -71,16 +73,20 @@ class CTK_CORE_EXPORT ctkAbstractJob : public QObject ///@{ /// Status /// Initialized: the object has been created and inserted in the JobsQueue map in the ctkJobScheduler - /// Queued: a worker is associated to the job and the worker has been inserted in the queue list of the QThreadPool (object owned by the ctkJobScheduler) with a priority + /// Queued: a worker is associated to the job and the worker has been inserted in the queue list of + /// the QThreadPool (object owned by the ctkJobScheduler) with a priority /// Running: the job is running in another thread by the associated worker. - /// Stopped: the job has been stopped externally (a cancel request from the worker) + /// UserStopped: the job has been stopped externally (a cancel request from the worker) + /// AttemptFailed: the job encountered an internal failure, however, the task will be reattempted + /// by a different job (as the logic returned false). /// Failed: the job failed internally (logic returns false). /// Finished: the job has been run successfully (logic returns true). enum JobStatus { Initialized = 0, Queued, Running, - Stopped, + UserStopped, + AttemptFailed, Failed, Finished, }; @@ -129,20 +135,32 @@ class CTK_CORE_EXPORT ctkAbstractJob : public QObject ///@} ///@{ - /// CreationDateTime + /// Creation Date Time QDateTime creationDateTime() const; ///@} ///@{ - /// StartDateTime + /// Start Date Time QDateTime startDateTime() const; ///@} ///@{ - /// CompletionDateTime + /// Completion Date Time QDateTime completionDateTime() const; ///@} + ///@{ + /// Running ThreadID + QString runningThreadID() const; + void setRunningThreadID(QString runningThreadID); + ///@} + + ///@{ + /// Logged Text + QString loggedText() const; + void setLoggedText(QString loggedText); + ///@} + /// Generate worker for job Q_INVOKABLE virtual ctkAbstractWorker* createWorker() = 0; @@ -161,7 +179,8 @@ class CTK_CORE_EXPORT ctkAbstractJob : public QObject Q_SIGNALS: void started(); - void canceled(); + void userStopped(); + void attemptFailed(); void failed(); void finished(); @@ -177,6 +196,8 @@ class CTK_CORE_EXPORT ctkAbstractJob : public QObject QDateTime CreationDateTime; QDateTime StartDateTime; QDateTime CompletionDateTime; + QString RunningThreadID; + QString LoggedText; private: Q_DISABLE_COPY(ctkAbstractJob) @@ -193,6 +214,8 @@ struct CTK_CORE_EXPORT ctkJobDetail { this->CreationDateTime = job.creationDateTime().toString("HH:mm:ss.zzz ddd dd MMM yyyy"); this->StartDateTime = job.startDateTime().toString("HH:mm:ss.zzz ddd dd MMM yyyy"); this->CompletionDateTime = job.completionDateTime().toString("HH:mm:ss.zzz ddd dd MMM yyyy"); + this->RunningThreadID = job.runningThreadID(); + this->Logging = job.loggedText(); } virtual ~ctkJobDetail() = default; @@ -201,6 +224,8 @@ struct CTK_CORE_EXPORT ctkJobDetail { QString CreationDateTime; QString StartDateTime; QString CompletionDateTime; + QString RunningThreadID; + QString Logging; }; Q_DECLARE_METATYPE(ctkJobDetail); diff --git a/Libs/Core/ctkAbstractWorker.cpp b/Libs/Core/ctkAbstractWorker.cpp index 174c63a5eb..52b1b55ffa 100644 --- a/Libs/Core/ctkAbstractWorker.cpp +++ b/Libs/Core/ctkAbstractWorker.cpp @@ -127,11 +127,15 @@ void ctkAbstractWorker::onJobCanceled(const bool& wasCanceled) timer.start(this->Job->retryDelay()); this->startNextJob(); + this->Job->setStatus(ctkAbstractJob::JobStatus::AttemptFailed); + } + else + { + this->Job->setStatus(ctkAbstractJob::JobStatus::Failed); } - this->Job->setStatus(ctkAbstractJob::JobStatus::Failed); } else { - this->Job->setStatus(ctkAbstractJob::JobStatus::Stopped); + this->Job->setStatus(ctkAbstractJob::JobStatus::UserStopped); } } diff --git a/Libs/Core/ctkJobScheduler.cpp b/Libs/Core/ctkJobScheduler.cpp index 9efde94357..e6ba99488f 100644 --- a/Libs/Core/ctkJobScheduler.cpp +++ b/Libs/Core/ctkJobScheduler.cpp @@ -53,11 +53,17 @@ ctkJobSchedulerPrivate::~ctkJobSchedulerPrivate() = default; //--------------------------------------------------------------------------- void ctkJobSchedulerPrivate::init() { - QObject::connect(this, SIGNAL(queueJobs()), + Q_Q(ctkJobScheduler); + QObject::connect(this, SIGNAL(queueJobsInThreadPool()), this, SLOT(onQueueJobsInThreadPool())); - this->ThreadPool = QSharedPointer(new QThreadPool()); + this->ThreadPool = QSharedPointer(new QThreadPool(this)); this->ThreadPool->setMaxThreadCount(20); + this->ThrottleTimer = QSharedPointer(new QTimer(this)); + this->ThrottleTimer->setSingleShot(true); + + QObject::connect(this->ThrottleTimer.data(), SIGNAL(timeout()), + q, SLOT(emitThrottledSignals())); } //------------------------------------------------------------------------------ @@ -65,12 +71,16 @@ void ctkJobSchedulerPrivate::onQueueJobsInThreadPool() { Q_Q(ctkJobScheduler); + if (this->FreezeJobsScheduling) { - QMutexLocker locker(&this->QueueMutex); - if (this->FreezeJobsScheduling) - { return; - } + } + + { + // The QMutexLocker is enclosed within brackets to restrict its scope and + // prevent conflicts with other QMutexLockers within the scheduler's methods. + QMutexLocker locker(&this->QueueMutex); + foreach (QThread::Priority priority, (QList() << QThread::Priority::HighestPriority @@ -81,6 +91,11 @@ void ctkJobSchedulerPrivate::onQueueJobsInThreadPool() { foreach (QSharedPointer job, this->JobsQueue) { + if (this->FreezeJobsScheduling) + { + return; + } + if (job->priority() != priority) { continue; @@ -125,37 +140,60 @@ bool ctkJobSchedulerPrivate::insertJob(QSharedPointer job) return false; } + if (this->FreezeJobsScheduling) + { + logger.debug(QString("ctkJobScheduler: job object %1 of type %2 in thread %3 " + "not added to the job list since jobs are being stopped.\n") + .arg(job->jobUID()) + .arg(job->className()) + .arg(QString::number(reinterpret_cast(QThread::currentThreadId())), 16)); + return false; + } + logger.debug(QString("ctkJobScheduler: creating job object %1 of type %2 in thread %3.\n") .arg(job->jobUID()) .arg(job->className()) .arg(QString::number(reinterpret_cast(QThread::currentThreadId())), 16)); - QObject::connect(job.data(), SIGNAL(started()), q, SLOT(onJobStarted())); - QObject::connect(job.data(), SIGNAL(canceled()), q, SLOT(onJobCanceled())); - QObject::connect(job.data(), SIGNAL(failed()), q, SLOT(onJobFailed())); - QObject::connect(job.data(), SIGNAL(finished()), q, SLOT(onJobFinished())); - QObject::connect(job.data(), SIGNAL(progressJobDetail(QVariant)), - q, SIGNAL(progressJobDetail(QVariant))); + QMetaObject::Connection startedConnection = QObject::connect(job.data(), &ctkAbstractJob::started, q, [q, job](){ + q->onJobStarted(job.data()); + }); + QMetaObject::Connection userStoppedConnection = QObject::connect(job.data(), &ctkAbstractJob::userStopped, q, [q, job](){ + q->onJobUserStopped(job.data()); + }); + QMetaObject::Connection finishedConnection = QObject::connect(job.data(), &ctkAbstractJob::finished, q, [q, job](){ + q->onJobFinished(job.data()); + }); + QMetaObject::Connection attemptFailedConnection = QObject::connect(job.data(), &ctkAbstractJob::attemptFailed, q, [q, job](){ + q->onJobAttemptFailed(job.data()); + }); + QMetaObject::Connection failedConnection = QObject::connect(job.data(), &ctkAbstractJob::failed, q, [q, job](){ + q->onJobFailed(job.data()); + }); + QMetaObject::Connection progressConnection = + QObject::connect(job.data(), SIGNAL(progressJobDetail(QVariant)), + q, SLOT(onProgressJobDetail(QVariant))); + + QMap connections = + { + {"started", startedConnection}, + {"userStopped", userStoppedConnection}, + {"finished", finishedConnection}, + {"attemptFailed", attemptFailedConnection}, + {"failed", failedConnection}, + {"progress", progressConnection}, + }; { + // The QMutexLocker is enclosed within brackets to restrict its scope and + // prevent conflicts with other QMutexLockers within the scheduler's methods. QMutexLocker locker(&this->QueueMutex); this->JobsQueue.insert(job->jobUID(), job); + this->JobsConnections.insert(job->jobUID(), connections); } emit q->jobInitialized(job->toVariant()); - - if (this->FreezeJobsScheduling) - { - logger.debug(QString("ctkJobScheduler: job object %1 of type %2 in thread %3 " - "not added to the list since jobs are being stopped.\n") - .arg(job->jobUID()) - .arg(job->className()) - .arg(QString::number(reinterpret_cast(QThread::currentThreadId())), 16)); - return false; - } - - emit this->queueJobs(); - + emit this->queueJobsInThreadPool(); return true; } @@ -169,23 +207,28 @@ bool ctkJobSchedulerPrivate::removeJob(const QString& jobUID) .arg(QString::number(reinterpret_cast(QThread::currentThreadId()), 16))); { + // The QMutexLocker is enclosed within brackets to restrict its scope and + // prevent conflicts with other QMutexLockers within the scheduler's methods. QMutexLocker locker(&this->QueueMutex); QSharedPointer job = this->JobsQueue.value(jobUID); - if (!job) + if (!job || !this->JobsConnections.contains(jobUID)) { return false; } - QObject::disconnect(job.data(), SIGNAL(started()), q, SLOT(onJobStarted())); - QObject::disconnect(job.data(), SIGNAL(canceled()), q, SLOT(onJobCanceled())); - QObject::disconnect(job.data(), SIGNAL(failed()), q, SLOT(onJobFailed())); - QObject::disconnect(job.data(), SIGNAL(finished()), q, SLOT(onJobFinished())); - QObject::disconnect(job.data(), SIGNAL(progressJobDetail(QVariant)), q, SIGNAL(progressJobDetail(QVariant))); + QMap connections = this->JobsConnections.value(jobUID); + QObject::disconnect(connections.value("started")); + QObject::disconnect(connections.value("userStopped")); + QObject::disconnect(connections.value("finished")); + QObject::disconnect(connections.value("attemptFailed")); + QObject::disconnect(connections.value("failed")); + QObject::disconnect(connections.value("progress")); + this->JobsConnections.remove(jobUID); this->JobsQueue.remove(jobUID); } - emit this->queueJobs(); + emit this->queueJobsInThreadPool(); return true; } @@ -204,27 +247,27 @@ void ctkJobSchedulerPrivate::removeJobs(const QStringList &jobUIDs) foreach (QString jobUID, jobUIDs) { QSharedPointer job = this->JobsQueue.value(jobUID); - if (!job) + if (!job || !this->JobsConnections.contains(jobUID)) { continue; } datas.append(job->toVariant()); - QObject::disconnect(job.data(), SIGNAL(started()), q, SLOT(onJobStarted())); - QObject::disconnect(job.data(), SIGNAL(canceled()), q, SLOT(onJobCanceled())); - QObject::disconnect(job.data(), SIGNAL(failed()), q, SLOT(onJobFailed())); - QObject::disconnect(job.data(), SIGNAL(finished()), q, SLOT(onJobFinished())); - QObject::disconnect(job.data(), SIGNAL(progressJobDetail(QVariant)), q, SIGNAL(progressJobDetail(QVariant))); + QMap connections = this->JobsConnections.value(jobUID); + QObject::disconnect(connections.value("started")); + QObject::disconnect(connections.value("userStopped")); + QObject::disconnect(connections.value("finished")); + QObject::disconnect(connections.value("attemptFailed")); + QObject::disconnect(connections.value("failed")); + QObject::disconnect(connections.value("progress")); + this->JobsConnections.remove(jobUID); this->JobsQueue.remove(jobUID); } } - foreach (QVariant data, datas) - { - emit q->jobCanceled(data); - } + emit q->jobUserStopped(datas); } //------------------------------------------------------------------------------ @@ -238,13 +281,27 @@ void ctkJobSchedulerPrivate::removeAllJobs() QMutexLocker locker(&this->QueueMutex); foreach (QSharedPointer job, this->JobsQueue) { - QObject::disconnect(job.data(), SIGNAL(started()), q, SLOT(onJobStarted())); - QObject::disconnect(job.data(), SIGNAL(canceled()), q, SLOT(onJobCanceled())); - QObject::disconnect(job.data(), SIGNAL(failed()), q, SLOT(onJobFailed())); - QObject::disconnect(job.data(), SIGNAL(finished()), q, SLOT(onJobFinished())); - QObject::disconnect(job.data(), SIGNAL(progressJobDetail(QVariant)), q, SIGNAL(progressJobDetail(QVariant))); + if (!job) + { + continue; + } + + QString jobUID = job->jobUID(); + if (!this->JobsConnections.contains(jobUID)) + { + continue; + } - this->JobsQueue.remove(job->jobUID()); + QMap connections = this->JobsConnections.value(jobUID); + QObject::disconnect(connections.value("started")); + QObject::disconnect(connections.value("userStopped")); + QObject::disconnect(connections.value("finished")); + QObject::disconnect(connections.value("attemptFailed")); + QObject::disconnect(connections.value("failed")); + QObject::disconnect(connections.value("progress")); + + this->JobsConnections.remove(jobUID); + this->JobsQueue.remove(jobUID); } } } @@ -277,6 +334,17 @@ QString ctkJobSchedulerPrivate::generateUniqueJobUID() return QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces); } +//------------------------------------------------------------------------------ +void ctkJobSchedulerPrivate::clearBactchedJobsLists() +{ + this->BatchedJobsStarted.clear(); + this->BatchedJobsUserStopped.clear(); + this->BatchedJobsFinished.clear(); + this->BatchedJobsAttemptFailed.clear(); + this->BatchedJobsFailed.clear(); + this->BatchedJobsProgress.clear(); +} + //--------------------------------------------------------------------------- // ctkJobScheduler methods @@ -301,7 +369,6 @@ ctkJobScheduler::ctkJobScheduler(ctkJobSchedulerPrivate* pimpl, QObject* parent) // -------------------------------------------------------------------------- ctkJobScheduler::~ctkJobScheduler() { - Q_D(ctkJobScheduler); this->setFreezeJobsScheduling(true); this->stopAllJobs(true); // stopAllJobs is not main thread blocking. Therefore we need actually @@ -367,7 +434,7 @@ int ctkJobScheduler::numberOfRunningJobs() QMutexLocker locker(&d->QueueMutex); foreach (QSharedPointer job, d->JobsQueue) { - if (job->status() == ctkAbstractJob::JobStatus::Running) + if (job->status() <= ctkAbstractJob::JobStatus::Running) { numberOfRunningJobs++; } @@ -440,18 +507,28 @@ ctkAbstractJob* ctkJobScheduler::getJobByUID(const QString& jobUID) } //---------------------------------------------------------------------------- -void ctkJobScheduler::waitForFinish(bool waitForPersistentJobs) +void ctkJobScheduler::waitForFinish(bool waitForPersistentJobs, + bool processEvents) { - Q_D(ctkJobScheduler); + this->waitForDone(500); + if (processEvents) + { + qApp->processEvents(QEventLoop::ExcludeUserInputEvents); + } int numberOfPersistentJobs = this->numberOfPersistentJobs(); if (waitForPersistentJobs) { numberOfPersistentJobs = 0; } + while (this->numberOfRunningJobs() > numberOfPersistentJobs) { this->waitForDone(500); + if (processEvents) + { + qApp->processEvents(QEventLoop::ExcludeUserInputEvents); + } } } @@ -486,36 +563,22 @@ void ctkJobScheduler::stopAllJobs(bool stopPersistentJobs) continue; } + QString jobUID = job->jobUID(); + if (!d->JobsConnections.contains(jobUID)) + { + continue; + } + // For this job, a worker has not beed started yet and the job is still in the main thread. // There is no worry that it will be started in meanwhile, // because only jobs with status Initialized will be started by the scheduler. // Therefore we set the status as stopped, because effettivelty the job has been stopped. // In addition, to speedup the cleaning of jobs, we remove them with one call removeJobs, - // instead of using the signal canceled -> onJobCanceled - QObject::disconnect(job.data(), SIGNAL(canceled()), this, SLOT(onJobCanceled())); - job->setStatus(ctkAbstractJob::JobStatus::Stopped); - initializedStoppedJobsUIDs.append(job->jobUID()); - } - - // Try to stop jobs with a worker, but still not running. - // (in queue in the QThreadPool, still in the main thread) - foreach (QSharedPointer worker, d->Workers) - { - QSharedPointer job = worker->jobShared(); - if (!job) - { - continue; - } - - // trytake stops workers not already running - // these corresponds to jobs with status Queued - if (d->ThreadPool->tryTake(worker.data())) - { - this->deleteWorker(job->jobUID()); - QObject::disconnect(job.data(), SIGNAL(canceled()), this, SLOT(onJobCanceled())); - job->setStatus(ctkAbstractJob::JobStatus::Stopped); - initializedStoppedJobsUIDs.append(job->jobUID()); - } + // instead of using the signal + QMap connections = d->JobsConnections.value(jobUID); + QObject::disconnect(connections.value("userStopped")); + job->setStatus(ctkAbstractJob::JobStatus::UserStopped); + initializedStoppedJobsUIDs.append(jobUID); } } @@ -563,42 +626,23 @@ void ctkJobScheduler::stopJobsByJobUIDs(const QStringList &jobUIDs) continue; } - if ((!job->jobUID().isEmpty() && jobUIDs.contains(job->jobUID()))) - { - // For this job, a worker has not beed started yet and the job is still in the main thread. - // There is no worry that it will be started in meanwhile, - // because only jobs with status Initialized will be started by the scheduler. - // Therefore we set the status as stopped, because effettivelty the job has been stopped. - // In addition, to speedup the cleaning of jobs, we remove them with one call removeJobs, - // instead of using the signal canceled -> onJobCanceled - QObject::disconnect(job.data(), SIGNAL(canceled()), this, SLOT(onJobCanceled())); - job->setStatus(ctkAbstractJob::JobStatus::Stopped); - initializedStoppedJobsUIDs.append(job->jobUID()); - } - } - - // Try to stop jobs with a worker, but still not running. - // (in queue in the QThreadPool, still in the main thread) - foreach (QSharedPointer worker, d->Workers) - { - QSharedPointer job = worker->jobShared(); - if (!job) + QString jobUID = job->jobUID(); + if (jobUID.isEmpty() || !jobUIDs.contains(jobUID) || + !d->JobsConnections.contains(jobUID)) { continue; } - if ((!job->jobUID().isEmpty() && jobUIDs.contains(job->jobUID()))) - { - // trytake stops workers not already running, - // these corresponds to jobs with status Queued - if (d->ThreadPool->tryTake(worker.data())) - { - this->deleteWorker(job->jobUID()); - QObject::disconnect(job.data(), SIGNAL(canceled()), this, SLOT(onJobCanceled())); - job->setStatus(ctkAbstractJob::JobStatus::Stopped); - initializedStoppedJobsUIDs.append(job->jobUID()); - } - } + // For this job, a worker has not beed started yet and the job is still in the main thread. + // There is no worry that it will be started in meanwhile, + // because only jobs with status Initialized will be started by the scheduler. + // Therefore we set the status as stopped, because effettivelty the job has been stopped. + // In addition, to speedup the cleaning of jobs, we remove them with one call removeJobs, + // instead of using the signal + QMap connections = d->JobsConnections.value(jobUID); + QObject::disconnect(connections.value("userStopped")); + job->setStatus(ctkAbstractJob::JobStatus::UserStopped); + initializedStoppedJobsUIDs.append(job->jobUID()); } } @@ -650,70 +694,177 @@ QSharedPointer ctkJobScheduler::threadPoolShared() const } //---------------------------------------------------------------------------- -void ctkJobScheduler::onJobStarted() +void ctkJobScheduler::onJobStarted(ctkAbstractJob* job) { - ctkAbstractJob* job = qobject_cast(this->sender()); + Q_D(ctkJobScheduler); if (!job) { return; } - logger.debug(job->loggerReport("started")); - emit this->jobStarted(job->toVariant()); + logger.debug(job->loggerReport(tr("started"))); + + d->BatchedJobsStarted.append(job->toVariant()); + if (!d->ThrottleTimer->isActive()) + { + d->ThrottleTimer->start(d->ThrottleTimeInterval); + } +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::onJobUserStopped(ctkAbstractJob* job) +{ + Q_D(ctkJobScheduler); + if (!job) + { + return; + } + + logger.debug(job->loggerReport(tr("user stopped"))); + + QVariant data = job->toVariant(); + QString jobUID = job->jobUID(); + this->deleteWorker(jobUID); + this->deleteJob(jobUID); + + d->BatchedJobsUserStopped.append(job->toVariant()); + if (!d->ThrottleTimer->isActive()) + { + d->ThrottleTimer->start(d->ThrottleTimeInterval); + } } //---------------------------------------------------------------------------- -void ctkJobScheduler::onJobCanceled() +void ctkJobScheduler::onJobFinished(ctkAbstractJob* job) { - ctkAbstractJob* job = qobject_cast(this->sender()); + Q_D(ctkJobScheduler); if (!job) { return; } - logger.debug(job->loggerReport("canceled")); + + logger.debug(job->loggerReport(tr("finished"))); QVariant data = job->toVariant(); QString jobUID = job->jobUID(); this->deleteWorker(jobUID); this->deleteJob(jobUID); - emit this->jobCanceled(data); + d->BatchedJobsFinished.append(job->toVariant()); + if (!d->ThrottleTimer->isActive()) + { + d->ThrottleTimer->start(d->ThrottleTimeInterval); + } } //---------------------------------------------------------------------------- -void ctkJobScheduler::onJobFailed() +void ctkJobScheduler::onJobAttemptFailed(ctkAbstractJob* job) { - ctkAbstractJob* job = qobject_cast(this->sender()); + Q_D(ctkJobScheduler); if (!job) { return; } - logger.debug(job->loggerReport("failed")); + logger.debug(job->loggerReport(tr("attempt failed"))); QVariant data = job->toVariant(); QString jobUID = job->jobUID(); this->deleteWorker(jobUID); this->deleteJob(jobUID); - emit this->jobFailed(data); + d->BatchedJobsAttemptFailed.append(job->toVariant()); + if (!d->ThrottleTimer->isActive()) + { + d->ThrottleTimer->start(d->ThrottleTimeInterval); + } } //---------------------------------------------------------------------------- -void ctkJobScheduler::onJobFinished() +void ctkJobScheduler::onJobFailed(ctkAbstractJob* job) { - ctkAbstractJob* job = qobject_cast(this->sender()); + Q_D(ctkJobScheduler); if (!job) { return; } - logger.debug(job->loggerReport("finished")); + logger.debug(job->loggerReport(tr("failed"))); QVariant data = job->toVariant(); QString jobUID = job->jobUID(); this->deleteWorker(jobUID); this->deleteJob(jobUID); - emit this->jobFinished(data); + d->BatchedJobsFailed.append(job->toVariant()); + if (!d->ThrottleTimer->isActive()) + { + d->ThrottleTimer->start(d->ThrottleTimeInterval); + } +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::onProgressJobDetail(QVariant data) +{ + Q_D(ctkJobScheduler); + + d->BatchedJobsProgress.append(data); + if (!d->ThrottleTimer->isActive()) + { + d->ThrottleTimer->start(d->ThrottleTimeInterval); + } +} + +//---------------------------------------------------------------------------- +void ctkJobScheduler::emitThrottledSignals() +{ + Q_D(ctkJobScheduler); + + int totalEmitted = 0; + if (!d->BatchedJobsStarted.isEmpty() && totalEmitted < d->MaximumBatchedSignalsForTimeInterval) + { + int count = qMin(d->MaximumBatchedSignalsForTimeInterval - totalEmitted, d->BatchedJobsStarted.size()); + emit this->jobStarted(d->BatchedJobsStarted.mid(0, count)); + d->BatchedJobsStarted = d->BatchedJobsStarted.mid(count); + totalEmitted += count; + } + if (!d->BatchedJobsUserStopped.isEmpty() && totalEmitted < d->MaximumBatchedSignalsForTimeInterval) + { + int count = qMin(d->MaximumBatchedSignalsForTimeInterval - totalEmitted, d->BatchedJobsUserStopped.size()); + emit this->jobUserStopped(d->BatchedJobsUserStopped.mid(0, count)); + d->BatchedJobsUserStopped = d->BatchedJobsUserStopped.mid(count); + totalEmitted += count; + } + if (!d->BatchedJobsFinished.isEmpty() && totalEmitted < d->MaximumBatchedSignalsForTimeInterval) + { + int count = qMin(d->MaximumBatchedSignalsForTimeInterval - totalEmitted, d->BatchedJobsFinished.size()); + emit this->jobFinished(d->BatchedJobsFinished.mid(0, count)); + d->BatchedJobsFinished = d->BatchedJobsFinished.mid(count); + totalEmitted += count; + } + if (!d->BatchedJobsAttemptFailed.isEmpty() && totalEmitted < d->MaximumBatchedSignalsForTimeInterval) + { + int count = qMin(d->MaximumBatchedSignalsForTimeInterval - totalEmitted, d->BatchedJobsAttemptFailed.size()); + emit this->jobAttemptFailed(d->BatchedJobsAttemptFailed.mid(0, count)); + d->BatchedJobsAttemptFailed = d->BatchedJobsAttemptFailed.mid(count); + totalEmitted += count; + } + if (!d->BatchedJobsFailed.isEmpty() && totalEmitted < d->MaximumBatchedSignalsForTimeInterval) + { + int count = qMin(d->MaximumBatchedSignalsForTimeInterval - totalEmitted, d->BatchedJobsFailed.size()); + emit this->jobFailed(d->BatchedJobsFailed.mid(0, count)); + d->BatchedJobsFailed = d->BatchedJobsFailed.mid(count); + totalEmitted += count; + } + + emit this->progressJobDetail(d->BatchedJobsProgress); + d->BatchedJobsProgress.clear(); + + int numberOfSignalsNotSent = d->BatchedJobsStarted.size() + d->BatchedJobsUserStopped.size() + + d->BatchedJobsFinished.size() + d->BatchedJobsAttemptFailed.size() + + d->BatchedJobsFailed.size(); + if (numberOfSignalsNotSent != 0 && !d->ThrottleTimer->isActive()) + { + d->ThrottleTimer->start(d->ThrottleTimeInterval); + } } diff --git a/Libs/Core/ctkJobScheduler.h b/Libs/Core/ctkJobScheduler.h index 1ac37caf74..4ff945c054 100644 --- a/Libs/Core/ctkJobScheduler.h +++ b/Libs/Core/ctkJobScheduler.h @@ -40,6 +40,9 @@ class ctkJobSchedulerPrivate; class CTK_CORE_EXPORT ctkJobScheduler : public QObject { Q_OBJECT + Q_PROPERTY(int numberOfJobs READ numberOfJobs); + Q_PROPERTY(int numberOfPersistentJobs READ numberOfPersistentJobs); + Q_PROPERTY(int numberOfRunningJobs READ numberOfRunningJobs); Q_PROPERTY(int freezeJobsScheduling READ freezeJobsScheduling WRITE setFreezeJobsScheduling); Q_PROPERTY(int maximumThreadCount READ maximumThreadCount WRITE setMaximumThreadCount); Q_PROPERTY(int maximumNumberOfRetry READ maximumNumberOfRetry WRITE setMaximumNumberOfRetry); @@ -52,21 +55,17 @@ class CTK_CORE_EXPORT ctkJobScheduler : public QObject ///@{ /// Jobs managment - Q_INVOKABLE int numberOfJobs(); - Q_INVOKABLE int numberOfPersistentJobs(); - Q_INVOKABLE int numberOfRunningJobs(); - + int numberOfJobs(); + int numberOfPersistentJobs(); + int numberOfRunningJobs(); Q_INVOKABLE void addJob(ctkAbstractJob* job); - Q_INVOKABLE virtual void deleteJob(const QString& jobUID); Q_INVOKABLE virtual void deleteWorker(const QString& jobUID); - QSharedPointer getJobSharedByUID(const QString& jobUID); Q_INVOKABLE ctkAbstractJob* getJobByUID(const QString& jobUID); - - Q_INVOKABLE void waitForFinish(bool waitForPersistentJobs = false); + Q_INVOKABLE void waitForFinish(bool waitForPersistentJobs = false, + bool processEvents = false); Q_INVOKABLE void waitForDone(int msec = -1); - Q_INVOKABLE void stopAllJobs(bool stopPersistentJobs = false); Q_INVOKABLE void stopJobsByJobUIDs(const QStringList& jobUIDs); ///@} @@ -107,19 +106,23 @@ class CTK_CORE_EXPORT ctkJobScheduler : public QObject QSharedPointer threadPoolShared() const; Q_SIGNALS: - void jobInitialized(QVariant data); - void jobQueued(QVariant data); - void jobStarted(QVariant data); - void jobFinished(QVariant data); - void jobCanceled(QVariant data); - void jobFailed(QVariant data); - void progressJobDetail(QVariant data); + void jobInitialized(QVariant); + void jobQueued(QVariant); + void jobStarted(QList); + void jobUserStopped(QList); + void jobFinished(QList); + void jobAttemptFailed(QList); + void jobFailed(QList); + void progressJobDetail(QList); public Q_SLOTS: - virtual void onJobStarted(); - virtual void onJobFinished(); - virtual void onJobCanceled(); - virtual void onJobFailed(); + virtual void onJobStarted(ctkAbstractJob*); + virtual void onJobUserStopped(ctkAbstractJob*); + virtual void onJobFinished(ctkAbstractJob*); + virtual void onJobAttemptFailed(ctkAbstractJob*); + virtual void onJobFailed(ctkAbstractJob*); + virtual void onProgressJobDetail(QVariant); + virtual void emitThrottledSignals(); protected: QScopedPointer d_ptr; diff --git a/Libs/Core/ctkJobScheduler_p.h b/Libs/Core/ctkJobScheduler_p.h index 1a93e40cbd..f6a792abac 100644 --- a/Libs/Core/ctkJobScheduler_p.h +++ b/Libs/Core/ctkJobScheduler_p.h @@ -24,6 +24,7 @@ // Qt includes #include #include +#include class QThreadPool; // ctkCore includes @@ -44,7 +45,7 @@ class CTK_CORE_EXPORT ctkJobSchedulerPrivate : public QObject ctkJobScheduler* const q_ptr; Q_SIGNALS: - void queueJobs(); + void queueJobsInThreadPool(); public Q_SLOTS: virtual void onQueueJobsInThreadPool(); @@ -62,6 +63,7 @@ public Q_SLOTS: virtual void removeAllJobs(); int getSameTypeJobsInThreadPoolQueueOrRunning(QSharedPointer job); QString generateUniqueJobUID(); + void clearBactchedJobsLists(); QMutex QueueMutex; @@ -71,7 +73,17 @@ public Q_SLOTS: QSharedPointer ThreadPool; QMap> JobsQueue; + QMap> JobsConnections; QMap> Workers; + QList BatchedJobsStarted; + QList BatchedJobsUserStopped; + QList BatchedJobsFinished; + QList BatchedJobsAttemptFailed; + QList BatchedJobsFailed; + QList BatchedJobsProgress; + QSharedPointer ThrottleTimer; + int ThrottleTimeInterval{300}; + int MaximumBatchedSignalsForTimeInterval{20}; }; #endif diff --git a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMSchedulerTest1.cpp b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMSchedulerTest1.cpp index 6e2d0a5a94..e31df5260a 100644 --- a/Libs/DICOM/Core/Testing/Cpp/ctkDICOMSchedulerTest1.cpp +++ b/Libs/DICOM/Core/Testing/Cpp/ctkDICOMSchedulerTest1.cpp @@ -109,8 +109,8 @@ int ctkDICOMSchedulerTest1(int argc, char* argv[]) std::cout << qPrintable(testName) << ": Running queryStudies" << std::endl; QString patientID = "Facial Expression"; - scheduler.queryStudies(patientID); - scheduler.waitForFinish(); + scheduler.queryStudies(patientID, QThread::LowPriority, QStringList("Test")); + scheduler.waitForFinish(false, true); CHECK_INT(database.patients().count(), 1); @@ -120,16 +120,16 @@ int ctkDICOMSchedulerTest1(int argc, char* argv[]) std::cout << qPrintable(testName) << ": Running querySeries" << std::endl; QString studyIstanceUID = studies[0]; - scheduler.querySeries(patientID, studyIstanceUID); - scheduler.waitForFinish(); + scheduler.querySeries(patientID, studyIstanceUID, QThread::LowPriority, QStringList("Test")); + scheduler.waitForFinish(false, true); QStringList series = database.seriesForStudy(studyIstanceUID); CHECK_INT(series.count(), 1); std::cout << qPrintable(testName) << ": Running queryInstances" << std::endl; QString seriesIstanceUID = series[0]; - scheduler.queryInstances(patientID, studyIstanceUID, seriesIstanceUID); - scheduler.waitForFinish(); + scheduler.queryInstances(patientID, studyIstanceUID, seriesIstanceUID, QThread::LowPriority, QStringList("Test")); + scheduler.waitForFinish(false, true); QStringList instances = database.instancesForSeries(seriesIstanceUID); QStringList files = database.filesForSeries(seriesIstanceUID); @@ -147,11 +147,12 @@ int ctkDICOMSchedulerTest1(int argc, char* argv[]) foreach (const QString& sopIstanceUID, instances) { - scheduler.retrieveSOPInstance(patientID, studyIstanceUID, seriesIstanceUID, sopIstanceUID); + scheduler.retrieveSOPInstance(patientID, studyIstanceUID, seriesIstanceUID, + sopIstanceUID, QThread::LowPriority, QStringList("Test")); } CHECK_INT(scheduler.numberOfJobs(), numberOfImages); - scheduler.waitForFinish(); + scheduler.waitForFinish(false, true); instances = database.instancesForSeries(seriesIstanceUID); files = database.filesForSeries(seriesIstanceUID); diff --git a/Libs/DICOM/Core/ctkDICOMAbstractThumbnailGenerator.h b/Libs/DICOM/Core/ctkDICOMAbstractThumbnailGenerator.h index dc41855416..dd3e6d30e3 100644 --- a/Libs/DICOM/Core/ctkDICOMAbstractThumbnailGenerator.h +++ b/Libs/DICOM/Core/ctkDICOMAbstractThumbnailGenerator.h @@ -24,6 +24,7 @@ // Qt includes #include +#include #include "ctkDICOMCoreExport.h" @@ -43,7 +44,10 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMAbstractThumbnailGenerator : public QObject explicit ctkDICOMAbstractThumbnailGenerator(QObject* parent = 0); virtual ~ctkDICOMAbstractThumbnailGenerator(); - virtual bool generateThumbnail(DicomImage* dcmImage, const QString& path ) = 0; + virtual bool generateThumbnail(DicomImage* dcmImage, const QString& path, + QVector color = QVector{169, 169, 169}) = 0; + virtual void generateDocumentThumbnail(const QString &thumbnailPath, + QVector color = QVector{169, 169, 169}) = 0; protected: QScopedPointer d_ptr; diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.cpp b/Libs/DICOM/Core/ctkDICOMDatabase.cpp index 8bcc66bcbf..725a69b066 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.cpp +++ b/Libs/DICOM/Core/ctkDICOMDatabase.cpp @@ -856,34 +856,6 @@ bool ctkDICOMDatabasePrivate::insertPatientStudySeries(const ctkDICOMItem& datas return databaseWasChanged; } -//------------------------------------------------------------------------------ -bool ctkDICOMDatabasePrivate::storeThumbnailFile(const QString& originalFilePath, - const QString& studyInstanceUID, const QString& seriesInstanceUID, const QString& sopInstanceUID) -{ - Q_Q(ctkDICOMDatabase); - if (!this->ThumbnailGenerator) - { - return false; - } - // Create thumbnail here - QString thumbnailPath = q->databaseDirectory() + - "/thumbs/" + this->internalStoragePath(studyInstanceUID, seriesInstanceUID, sopInstanceUID) + ".png"; - QFileInfo thumbnailInfo(thumbnailPath); - if (thumbnailInfo.exists() && (thumbnailInfo.lastModified() > QFileInfo(originalFilePath).lastModified())) - { - // thumbnail already exists and up-to-date - return true; - } - QDir destinationDir(thumbnailInfo.dir()); - if (!destinationDir.exists()) - { - destinationDir.mkpath("."); - } - DicomImage dcmImage(QDir::toNativeSeparators(originalFilePath).toUtf8()); - return this->ThumbnailGenerator->generateThumbnail(&dcmImage, thumbnailPath); -} - - //------------------------------------------------------------------------------ bool ctkDICOMDatabasePrivate::uidsForDataSet(const ctkDICOMItem& dataset, QString& patientsName, QString& patientID, QString& studyInstanceUID, QString& seriesInstanceUID) @@ -1036,7 +1008,7 @@ void ctkDICOMDatabasePrivate::insert(const ctkDICOMItem& dataset, const QString& } if (generateThumbnail) { - this->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); + q->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); } } if (q->isInMemory() && databaseWasChanged) @@ -2375,6 +2347,72 @@ QDateTime ctkDICOMDatabase::insertDateTimeForInstance(QString sopInstanceUID) return result; } +//------------------------------------------------------------------------------ +QString ctkDICOMDatabase::thumbnailPathForInstance(const QString &studyInstanceUID, + const QString &seriesInstanceUID, + const QString &sopInstanceUID) +{ + Q_D(ctkDICOMDatabase); + QString thumbnailPath = this->databaseDirectory() + + "/thumbs/" + d->internalStoragePath(studyInstanceUID, seriesInstanceUID, sopInstanceUID) + ".png"; + + QFileInfo thumbnailInfo(thumbnailPath); + if (thumbnailInfo.exists()) + { + // thumbnail exists + return thumbnailPath; + } + else + { + return ""; + } +} + +//------------------------------------------------------------------------------ +bool ctkDICOMDatabase::storeThumbnailFile(const QString &originalFilePath, + const QString &studyInstanceUID, + const QString &seriesInstanceUID, + const QString &sopInstanceUID, + const QString& modality, + QVector color) +{ + Q_D(ctkDICOMDatabase); + if (!d->ThumbnailGenerator) + { + return false; + } + + QString thumbnailPath = this->databaseDirectory() + + "/thumbs/" + d->internalStoragePath(studyInstanceUID, seriesInstanceUID, sopInstanceUID) + ".png"; + + QFileInfo thumbnailInfo(thumbnailPath); + if (thumbnailInfo.exists() && (thumbnailInfo.lastModified() > QFileInfo(originalFilePath).lastModified())) + { + // thumbnail already exists and it is up-to-date + return true; + } + + QDir destinationDir(thumbnailInfo.dir()); + if (!destinationDir.exists()) + { + destinationDir.mkpath("."); + } + + if (modality == "SEG") + { + // NOTE: currently SEG objects are not fully supported by ctkDICOMThumbnailGenerator, + // The rendering will fail and in addition SEG object can be very large and + // loading the file can be slow. For now, we will just create a blank thumbnail with a document svg. + d->ThumbnailGenerator->generateDocumentThumbnail(thumbnailPath, color); + return true; + } + else + { + DicomImage dcmImage(QDir::toNativeSeparators(originalFilePath).toUtf8()); + return d->ThumbnailGenerator->generateThumbnail(&dcmImage, thumbnailPath, color); + } +} + //------------------------------------------------------------------------------ int ctkDICOMDatabase::patientsCount() { @@ -2848,7 +2886,7 @@ void ctkDICOMDatabase::insert(const QList& ind if (generateThumbnail) { - d->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); + this->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); } } } @@ -3116,7 +3154,7 @@ void ctkDICOMDatabase::insert(QList> jobR } if (generateThumbnail) { - d->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); + this->storeThumbnailFile(storedFilePath, studyInstanceUID, seriesInstanceUID, sopInstanceUID); } } } diff --git a/Libs/DICOM/Core/ctkDICOMDatabase.h b/Libs/DICOM/Core/ctkDICOMDatabase.h index cc680f156c..d074967f3d 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase.h +++ b/Libs/DICOM/Core/ctkDICOMDatabase.h @@ -208,6 +208,15 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject Q_INVOKABLE QString seriesForFile(QString fileName); Q_INVOKABLE QString instanceForFile(const QString fileName); Q_INVOKABLE QDateTime insertDateTimeForInstance(const QString fileName); + Q_INVOKABLE QString thumbnailPathForInstance(const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& sopInstanceUID); + Q_INVOKABLE bool storeThumbnailFile(const QString& originalFilePath, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& sopInstanceUID, + const QString& modality = "", + QVector color = QVector{169, 169, 169}); Q_INVOKABLE int patientsCount(); Q_INVOKABLE int studiesCount(); @@ -263,14 +272,13 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabase : public QObject /// does only make sense if a full object is received. /// @param @generateThumbnail If true, a thumbnail is generated. /// - Q_INVOKABLE void insert( const ctkDICOMItem& ctkDataset, - bool storeFile, bool generateThumbnail); - void insert ( DcmItem *item, - bool storeFile = true, bool generateThumbnail = true); - Q_INVOKABLE void insert ( const QString& filePath, - bool storeFile = true, bool generateThumbnail = true, - bool createHierarchy = true, - const QString& destinationDirectoryName = QString() ); + Q_INVOKABLE void insert(const ctkDICOMItem& ctkDataset, + bool storeFile, bool generateThumbnail); + void insert (DcmItem *item, bool storeFile = true, bool generateThumbnail = true); + Q_INVOKABLE void insert (const QString& filePath, + bool storeFile = true, bool generateThumbnail = true, + bool createHierarchy = true, + const QString& destinationDirectoryName = QString()); Q_INVOKABLE void insert(const QList& indexingResults); Q_INVOKABLE void insert(QList> jobResponseSets); diff --git a/Libs/DICOM/Core/ctkDICOMDatabase_p.h b/Libs/DICOM/Core/ctkDICOMDatabase_p.h index 1efbf2b31c..87ad90bbb5 100644 --- a/Libs/DICOM/Core/ctkDICOMDatabase_p.h +++ b/Libs/DICOM/Core/ctkDICOMDatabase_p.h @@ -72,10 +72,6 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMDatabasePrivate /// Returns false in case of an error bool indexingStatusForFile(const QString& filePath, const QString& sopInstanceUID, bool& datasetInDatabase, bool& datasetUpToDate, QString& databaseFilename); - /// Retrieve thumbnail from file and store in database folder. - bool storeThumbnailFile(const QString& originalFilePath, - const QString& studyInstanceUID, const QString& seriesInstanceUID, const QString& sopInstanceUID); - /// Get basic UIDs for a data set, return true if the data set has all the required tags bool uidsForDataSet(const ctkDICOMItem& dataset, QString& patientsName, QString& patientID, QString& studyInstanceUID, QString& seriesInstanceUID); bool uidsForDataSet(QString& patientsName, QString& patientID, QString& studyInstanceUID); diff --git a/Libs/DICOM/Core/ctkDICOMInserterWorker.cpp b/Libs/DICOM/Core/ctkDICOMInserterWorker.cpp index 9eb5a98a46..c8074d9a7d 100644 --- a/Libs/DICOM/Core/ctkDICOMInserterWorker.cpp +++ b/Libs/DICOM/Core/ctkDICOMInserterWorker.cpp @@ -32,6 +32,9 @@ #include "ctkDICOMInserterWorker_p.h" #include "ctkDICOMJobResponseSet.h" +// DCMTK includes +#include + static ctkLogger logger ("org.commontk.dicom.DICOMInserterWorker"); //------------------------------------------------------------------------------ @@ -100,6 +103,9 @@ void ctkDICOMInserterWorker::run() return; } + QString currentThread = dcmtk::log4cplus::thread::getCurrentThreadName().c_str(); + inserterJob->setRunningThreadID(currentThread); + if (d->Inserter->wasCanceled()) { this->onJobCanceled(d->Inserter->wasCanceled()); @@ -125,7 +131,6 @@ void ctkDICOMInserterWorker::run() { emit inserterJob->progressJobDetail(jobResponseSet->toVariant()); } - inserterJob->setStatus(ctkAbstractJob::JobStatus::Finished); } diff --git a/Libs/DICOM/Core/ctkDICOMJob.h b/Libs/DICOM/Core/ctkDICOMJob.h index c37ad86cdb..47aa796aa9 100644 --- a/Libs/DICOM/Core/ctkDICOMJob.h +++ b/Libs/DICOM/Core/ctkDICOMJob.h @@ -41,6 +41,7 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMJob : public ctkAbstractJob { Q_OBJECT Q_ENUMS(DICOMLevel) + Q_PROPERTY(QString patientID READ patientID WRITE setPatientID); Q_PROPERTY(QString studyInstanceUID READ studyInstanceUID WRITE setStudyInstanceUID); Q_PROPERTY(QString seriesInstanceUID READ seriesInstanceUID WRITE setSeriesInstanceUID); Q_PROPERTY(QString sopInstanceUID READ sopInstanceUID WRITE setSOPInstanceUID); @@ -140,7 +141,6 @@ struct CTK_DICOM_CORE_EXPORT ctkDICOMJobDetail : ctkJobDetail explicit ctkDICOMJobDetail(const ctkDICOMJob& job) : ctkJobDetail(job) { - this->DICOMLevel = job.dicomLevel(); this->JobType = job.getJobType(); this->PatientID = job.patientID(); diff --git a/Libs/DICOM/Core/ctkDICOMJobResponseSet.cpp b/Libs/DICOM/Core/ctkDICOMJobResponseSet.cpp index d0a0b5ab48..554cce0b12 100644 --- a/Libs/DICOM/Core/ctkDICOMJobResponseSet.cpp +++ b/Libs/DICOM/Core/ctkDICOMJobResponseSet.cpp @@ -57,6 +57,7 @@ class ctkDICOMJobResponseSetPrivate : public QObject QString SeriesInstanceUID; QString SOPInstanceUID; QString ConnectionName; + QString RunningThreadID; QMap> Datasets; }; diff --git a/Libs/DICOM/Core/ctkDICOMQueryWorker.cpp b/Libs/DICOM/Core/ctkDICOMQueryWorker.cpp index 62aa68b1f5..9b15a99aa2 100644 --- a/Libs/DICOM/Core/ctkDICOMQueryWorker.cpp +++ b/Libs/DICOM/Core/ctkDICOMQueryWorker.cpp @@ -30,6 +30,9 @@ #include "ctkDICOMScheduler.h" #include "ctkDICOMServer.h" +// DCMTK includes +#include + static ctkLogger logger ("org.commontk.dicom.DICOMQueryWorker"); //------------------------------------------------------------------------------ @@ -109,6 +112,9 @@ void ctkDICOMQueryWorker::run() return; } + QString currentThread = dcmtk::log4cplus::thread::getCurrentThreadName().c_str(); + queryJob->setRunningThreadID(currentThread); + QSharedPointer scheduler = qobject_cast>(this->Scheduler); if (!scheduler || diff --git a/Libs/DICOM/Core/ctkDICOMRetrieveWorker.cpp b/Libs/DICOM/Core/ctkDICOMRetrieveWorker.cpp index 06d80a8148..78898348c8 100644 --- a/Libs/DICOM/Core/ctkDICOMRetrieveWorker.cpp +++ b/Libs/DICOM/Core/ctkDICOMRetrieveWorker.cpp @@ -31,6 +31,9 @@ #include "ctkDICOMScheduler.h" #include "ctkDICOMServer.h" +// DCMTK includes +#include + static ctkLogger logger ("org.commontk.dicom.DICOMRetrieveWorker"); //------------------------------------------------------------------------------ @@ -129,6 +132,9 @@ void ctkDICOMRetrieveWorker::run() return; } + QString currentThread = dcmtk::log4cplus::thread::getCurrentThreadName().c_str(); + retrieveJob->setRunningThreadID(currentThread); + QSharedPointer scheduler = qSharedPointerObjectCast(this->Scheduler); ctkDICOMServer* server = retrieveJob->server(); @@ -244,6 +250,7 @@ void ctkDICOMRetrieveWorker::run() newJob->setRetryCounter(0); newJob->setServer(*proxyServer); scheduler->addJob(newJob); + retrieveJob->setReferenceInserterJobUID("Proxy"); } else if (d->Retrieve->jobResponseSetsShared().count() > 0 && server->retrieveProtocol() == ctkDICOMServer::RetrieveProtocol::CGET) diff --git a/Libs/DICOM/Core/ctkDICOMScheduler.cpp b/Libs/DICOM/Core/ctkDICOMScheduler.cpp index 3e399eea4a..95026d33a3 100644 --- a/Libs/DICOM/Core/ctkDICOMScheduler.cpp +++ b/Libs/DICOM/Core/ctkDICOMScheduler.cpp @@ -40,10 +40,69 @@ // dcmtk includes #include - +#include +#include +#include "dcmtk/oflog/spi/logevent.h" static ctkLogger logger ( "org.commontk.dicom.DICOMScheduler" ); +//------------------------------------------------------------------------------ +// JobAppender class (custom DCMTK Appender) + +//------------------------------------------------------------------------------ +class ctkDICOMJobsAppender : public dcmtk::log4cplus::Appender { +public: + ctkDICOMJobsAppender() {}; + + virtual void close() {}; + + QMap messagesPerThread() const + { + return this->MessagesPerThread; + } + + QString messageByThreadID(QString threadID) + { + if (!this->MessagesPerThread.contains(threadID)) + { + return ""; + } + + return this->MessagesPerThread.take(threadID); + } + + void clearMessagesPerThread() + { + this->MessagesPerThread.clear(); + } + +protected: + void append(const dcmtk::log4cplus::spi::InternalLoggingEvent& event) + { + dcmtk::log4cplus::PatternLayout *layout = + dynamic_cast(this->getLayout()); + if (!layout) + { + return; + } + + dcmtk::log4cplus::tostringstream oss; + layout->formatAndAppend(oss, event); + + QString message = QString::fromLatin1(oss.str().c_str()); + if (message.isEmpty()) + { + return; + } + + QString messageThread = event.getThread().c_str(); + this->MessagesPerThread[messageThread] += message; + } + +private: + QMap MessagesPerThread; +}; + //------------------------------------------------------------------------------ // ctkDICOMSchedulerPrivate methods @@ -52,6 +111,14 @@ ctkDICOMSchedulerPrivate::ctkDICOMSchedulerPrivate(ctkDICOMScheduler& obj) : ctkJobSchedulerPrivate(obj) { ctk::setDICOMLogLevel(ctkErrorLogLevel::Warning); + + OFunique_ptr layout(new dcmtk::log4cplus::PatternLayout("%D{%Y-%m-%d %H:%M:%S.%q} %5p: %m%n")); + this->Appender = (new ctkDICOMJobsAppender()); + this->Appender->setLayout(OFmove(layout)); + this->Appender->setThreshold(ctk::dicomLogLevel()); + + dcmtk::log4cplus::Logger rootLog = dcmtk::log4cplus::Logger::getRoot(); + rootLog.addAppender(this->Appender); } //------------------------------------------------------------------------------ @@ -59,6 +126,12 @@ ctkDICOMSchedulerPrivate::~ctkDICOMSchedulerPrivate() { Q_Q(ctkDICOMScheduler); q->removeAllServers(); + + if (this->Appender) + { + dcmtk::log4cplus::Logger rootLog = dcmtk::log4cplus::Logger::getRoot(); + rootLog.removeAppender(this->Appender); + } } //------------------------------------------------------------------------------ @@ -678,7 +751,7 @@ void ctkDICOMScheduler::waitForFinishByDICOMUIDs(const QStringList& patientIDs, bool wait = true; while (wait) { - QCoreApplication::processEvents(); + qApp->processEvents(QEventLoop::ExcludeUserInputEvents); d->ThreadPool->waitForDone(300); wait = false; @@ -760,7 +833,7 @@ QList> ctkDICOMScheduler::getJobsByDICOMUIDs(cons ctkDICOMJob* dicomJob = qobject_cast(job.data()); if (!dicomJob) { - qCritical() << Q_FUNC_INFO << " failed: unexpected type of job"; + logger.debug("ctkDICOMScheduler::getJobsByDICOMUIDs: unexpected type of job."); continue; } @@ -802,7 +875,7 @@ void ctkDICOMScheduler::stopJobsByDICOMUIDs(const QStringList& patientIDs, if (numberOfInputLists == 0) { - logger.warn("ctkDICOMScheduler::stopJobsByDICOMUIDs failed: all the provided lists with UIDs are empty."); + logger.debug("ctkDICOMScheduler::stopJobsByDICOMUIDs: all the provided lists with UIDs are empty."); return; } @@ -822,11 +895,10 @@ void ctkDICOMScheduler::stopJobsByDICOMUIDs(const QStringList& patientIDs, ctkDICOMJob* dicomJob = qobject_cast(job.data()); if (!dicomJob) { - qCritical() << Q_FUNC_INFO << " failed: unexpected type of job"; + logger.debug("ctkDICOMScheduler::stopJobsByDICOMUIDs: unexpected type of job."); continue; } - ctkDICOMInserterJob* inserterJob = qobject_cast(job.data()); if ((!dicomJob->patientID().isEmpty() && patientIDs.contains(dicomJob->patientID())) || (!dicomJob->studyInstanceUID().isEmpty() && studyInstanceUIDs.contains(dicomJob->studyInstanceUID())) || (!dicomJob->seriesInstanceUID().isEmpty() && seriesInstanceUIDs.contains(dicomJob->seriesInstanceUID())) || @@ -834,10 +906,6 @@ void ctkDICOMScheduler::stopJobsByDICOMUIDs(const QStringList& patientIDs, { jobsUIDs.append(dicomJob->jobUID()); } - else if (inserterJob) - { - jobsUIDs.append(dicomJob->jobUID()); - } } } @@ -845,77 +913,85 @@ void ctkDICOMScheduler::stopJobsByDICOMUIDs(const QStringList& patientIDs, } //---------------------------------------------------------------------------- -void ctkDICOMScheduler::runJobs(const QMap &jobDetails) +void ctkDICOMScheduler::runJob(const ctkDICOMJobDetail& jd, const QStringList& allowedSeversForPatient) { - for(QString jobUID : jobDetails.keys()) + QStringList allowedSevers = QStringList(jd.ConnectionName); + allowedSevers.append(allowedSeversForPatient); + if (jd.JobClass == "ctkDICOMQueryJob") { - ctkDICOMJobDetail jd = jobDetails.value(jobUID); - if (jd.JobClass == "ctkDICOMQueryJob") + switch (jd.DICOMLevel) { - switch (jd.DICOMLevel) - { - case ctkDICOMJob::DICOMLevels::None: - logger.warn("ctkDICOMScheduler : DICOMLevels was not set."); - break; - case ctkDICOMJob::DICOMLevels::Patients: - this->queryPatients(); - break; - case ctkDICOMJob::DICOMLevels::Studies: - this->queryStudies(jd.PatientID, + case ctkDICOMJob::DICOMLevels::None: + logger.warn("ctkDICOMScheduler : DICOMLevels was not set."); + break; + case ctkDICOMJob::DICOMLevels::Patients: + this->queryPatients(); + break; + case ctkDICOMJob::DICOMLevels::Studies: + this->queryStudies(jd.PatientID, + QThread::NormalPriority, + allowedSevers); + break; + case ctkDICOMJob::DICOMLevels::Series: + this->querySeries(jd.PatientID, + jd.StudyInstanceUID, + QThread::NormalPriority, + allowedSevers); + break; + case ctkDICOMJob::DICOMLevels::Instances: + this->queryInstances(jd.PatientID, + jd.StudyInstanceUID, + jd.SeriesInstanceUID, QThread::NormalPriority, - QStringList(jd.ConnectionName)); - break; - case ctkDICOMJob::DICOMLevels::Series: - this->querySeries(jd.PatientID, - jd.StudyInstanceUID, - QThread::NormalPriority, - QStringList(jd.ConnectionName)); - break; - case ctkDICOMJob::DICOMLevels::Instances: - this->queryInstances(jd.PatientID, - jd.StudyInstanceUID, - jd.SeriesInstanceUID, - QThread::NormalPriority, - QStringList(jd.ConnectionName)); - break; - } + allowedSevers); + break; } - else if (jd.JobClass == "ctkDICOMRetrieveJob") + } + else if (jd.JobClass == "ctkDICOMRetrieveJob") + { + switch (jd.DICOMLevel) { - switch (jd.DICOMLevel) - { - case ctkDICOMJob::DICOMLevels::None: - logger.warn("ctkDICOMScheduler : DICOMLevels was not set."); - break; - case ctkDICOMJob::DICOMLevels::Patients: - logger.warn("Retrieve Patient is not implemented"); + case ctkDICOMJob::DICOMLevels::None: + logger.warn("ctkDICOMScheduler : DICOMLevels was not set."); break; - case ctkDICOMJob::DICOMLevels::Studies: - this->retrieveStudy(jd.PatientID, - jd.StudyInstanceUID, - QThread::NormalPriority, - QStringList(jd.ConnectionName)); - break; - case ctkDICOMJob::DICOMLevels::Series: - this->retrieveSeries(jd.PatientID, - jd.StudyInstanceUID, - jd.SeriesInstanceUID, - QThread::NormalPriority, - QStringList(jd.ConnectionName)); - break; - case ctkDICOMJob::DICOMLevels::Instances: - this->retrieveSOPInstance(jd.PatientID, - jd.StudyInstanceUID, - jd.SeriesInstanceUID, - jd.SOPInstanceUID, - QThread::NormalPriority, - QStringList(jd.ConnectionName)); - break; - } + case ctkDICOMJob::DICOMLevels::Patients: + logger.warn("Retrieve Patient is not implemented"); + break; + case ctkDICOMJob::DICOMLevels::Studies: + this->retrieveStudy(jd.PatientID, + jd.StudyInstanceUID, + QThread::NormalPriority, + allowedSevers); + break; + case ctkDICOMJob::DICOMLevels::Series: + this->retrieveSeries(jd.PatientID, + jd.StudyInstanceUID, + jd.SeriesInstanceUID, + QThread::NormalPriority, + allowedSevers); + break; + case ctkDICOMJob::DICOMLevels::Instances: + this->retrieveSOPInstance(jd.PatientID, + jd.StudyInstanceUID, + jd.SeriesInstanceUID, + jd.SOPInstanceUID, + QThread::NormalPriority, + allowedSevers); + break; } } } +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::runJobs(const QMap &jobDetails) +{ + for(QString jobUID : jobDetails.keys()) + { + ctkDICOMJobDetail jd = jobDetails.value(jobUID); + this->runJob(jd); + } +} + //---------------------------------------------------------------------------- void ctkDICOMScheduler::raiseJobsPriorityForSeries(const QStringList& selectedSeriesInstanceUIDs, QThread::Priority priority) @@ -941,7 +1017,7 @@ void ctkDICOMScheduler::raiseJobsPriorityForSeries(const QStringList& selectedSe ctkDICOMJob* dicomJob = qobject_cast(job.data()); if (!dicomJob) { - qCritical() << Q_FUNC_INFO << " failed: unexpected type of job"; + logger.debug("ctkDICOMScheduler::raiseJobsPriorityForSeries: unexpected type of job."); continue; } @@ -1005,3 +1081,98 @@ bool ctkDICOMScheduler::isStorageListenerActive() } return false; } + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::onJobStarted(ctkAbstractJob* job) +{ + Q_D(ctkDICOMScheduler); + if (!job) + { + return; + } + + ctkDICOMJobsAppender* appender = dynamic_cast(d->Appender.get()); + if (appender) + { + QString loggedText = appender->messageByThreadID(job->runningThreadID()); + job->setLoggedText(loggedText); + } + + ctkJobScheduler::onJobStarted(job); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::onJobUserStopped(ctkAbstractJob* job) +{ + Q_D(ctkDICOMScheduler); + if (!job) + { + return; + } + + ctkDICOMJobsAppender* appender = dynamic_cast(d->Appender.get()); + if (appender) + { + QString loggedText = appender->messageByThreadID(job->runningThreadID()); + job->setLoggedText(loggedText); + } + + ctkJobScheduler::onJobUserStopped(job); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::onJobFinished(ctkAbstractJob* job) +{ + Q_D(ctkDICOMScheduler); + if (!job) + { + return; + } + + ctkDICOMJobsAppender* appender = dynamic_cast(d->Appender.get()); + if (appender) + { + QString loggedText = appender->messageByThreadID(job->runningThreadID()); + job->setLoggedText(loggedText); + } + + ctkJobScheduler::onJobFinished(job); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::onJobAttemptFailed(ctkAbstractJob* job) +{ + Q_D(ctkDICOMScheduler); + if (!job) + { + return; + } + + ctkDICOMJobsAppender* appender = dynamic_cast(d->Appender.get()); + if (appender) + { + QString loggedText = appender->messageByThreadID(job->runningThreadID()); + job->setLoggedText(loggedText); + } + + ctkJobScheduler::onJobAttemptFailed(job); +} + +//---------------------------------------------------------------------------- +void ctkDICOMScheduler::onJobFailed(ctkAbstractJob* job) +{ + Q_D(ctkDICOMScheduler); + if (!job) + { + return; + } + + ctkDICOMJobsAppender* appender = dynamic_cast(d->Appender.get()); + if (appender) + { + QString loggedText = appender->messageByThreadID(job->runningThreadID()); + job->setLoggedText(loggedText); + } + + ctkJobScheduler::onJobFailed(job); +} diff --git a/Libs/DICOM/Core/ctkDICOMScheduler.h b/Libs/DICOM/Core/ctkDICOMScheduler.h index 3a98232b6e..3409f86a77 100644 --- a/Libs/DICOM/Core/ctkDICOMScheduler.h +++ b/Libs/DICOM/Core/ctkDICOMScheduler.h @@ -187,6 +187,7 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMScheduler : public ctkJobScheduler const QStringList& seriesInstanceUIDs = {}, const QStringList& sopInstanceUIDs = {}); + Q_INVOKABLE void runJob(const ctkDICOMJobDetail& jobDetails, const QStringList& allowedSeversForPatient = QStringList()); Q_INVOKABLE void runJobs(const QMap& jobDetails); Q_INVOKABLE void raiseJobsPriorityForSeries(const QStringList& selectedSeriesInstanceUIDs, QThread::Priority priority = QThread::HighestPriority); @@ -205,6 +206,13 @@ class CTK_DICOM_CORE_EXPORT ctkDICOMScheduler : public ctkJobScheduler Q_INVOKABLE bool isStorageListenerActive(); ///@} +public Q_SLOTS: + virtual void onJobStarted(ctkAbstractJob* job); + virtual void onJobUserStopped(ctkAbstractJob* job); + virtual void onJobFinished(ctkAbstractJob* job); + virtual void onJobAttemptFailed(ctkAbstractJob* job); + virtual void onJobFailed(ctkAbstractJob* job); + protected: ctkDICOMScheduler(ctkDICOMSchedulerPrivate* pimpl, QObject* parent); diff --git a/Libs/DICOM/Core/ctkDICOMScheduler_p.h b/Libs/DICOM/Core/ctkDICOMScheduler_p.h index 48c90d8608..7762db7cf8 100644 --- a/Libs/DICOM/Core/ctkDICOMScheduler_p.h +++ b/Libs/DICOM/Core/ctkDICOMScheduler_p.h @@ -67,6 +67,8 @@ class ctkDICOMSchedulerPrivate : public ctkJobSchedulerPrivate QMap Filters; int MaximumPatientsQuery{25}; + + dcmtk::log4cplus::SharedAppenderPtr Appender; }; #endif diff --git a/Libs/DICOM/Core/ctkDICOMStorageListener.cpp b/Libs/DICOM/Core/ctkDICOMStorageListener.cpp index e113273443..46b5481aa9 100644 --- a/Libs/DICOM/Core/ctkDICOMStorageListener.cpp +++ b/Libs/DICOM/Core/ctkDICOMStorageListener.cpp @@ -123,6 +123,8 @@ OFCondition ctkDICOMStorageListenerSCUPrivate::handleIncomingCommand(T_DIMSE_Mes reqDataset->findAndGetOFString(DCM_SeriesInstanceUID, seriesUID); OFString studyUID; reqDataset->findAndGetOFString(DCM_StudyInstanceUID, studyUID); + OFString patientID; + reqDataset->findAndGetOFString(DCM_PatientID, patientID); emit this->listener->progress( ctkDICOMStorageListener::tr("Got STORE request for %1").arg(instanceUID.c_str())); emit this->listener->progress(0); @@ -131,6 +133,7 @@ OFCondition ctkDICOMStorageListenerSCUPrivate::handleIncomingCommand(T_DIMSE_Mes QSharedPointer jobResponseSet = QSharedPointer(new ctkDICOMJobResponseSet); jobResponseSet->setJobType(ctkDICOMJobResponseSet::JobType::StoreSOPInstance); + jobResponseSet->setPatientID(patientID.c_str()); jobResponseSet->setStudyInstanceUID(studyUID.c_str()); jobResponseSet->setSeriesInstanceUID(seriesUID.c_str()); jobResponseSet->setSOPInstanceUID(instanceUID.c_str()); diff --git a/Libs/DICOM/Core/ctkDICOMStorageListenerWorker.cpp b/Libs/DICOM/Core/ctkDICOMStorageListenerWorker.cpp index 994861e279..9f0f60924e 100644 --- a/Libs/DICOM/Core/ctkDICOMStorageListenerWorker.cpp +++ b/Libs/DICOM/Core/ctkDICOMStorageListenerWorker.cpp @@ -33,6 +33,9 @@ #include "ctkDICOMStorageListenerJob.h" #include "ctkDICOMStorageListenerWorker_p.h" +// DCMTK includes +#include + static ctkLogger logger ("org.commontk.dicom.DICOMStorageListenerWorker"); //------------------------------------------------------------------------------ @@ -123,6 +126,9 @@ void ctkDICOMStorageListenerWorker::run() return; } + QString currentThread = dcmtk::log4cplus::thread::getCurrentThreadName().c_str(); + storageListenerJob->setRunningThreadID(currentThread); + QSharedPointer scheduler = qSharedPointerObjectCast(this->Scheduler); if (!scheduler diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/error.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/error.svg index 04380d59f7..2a7f3af742 100644 --- a/Libs/DICOM/Widgets/Resources/UI/Icons/error.svg +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/error.svg @@ -1 +1 @@ - + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/error_red.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/error_red.svg index f3095b684c..322ae814d3 100644 --- a/Libs/DICOM/Widgets/Resources/UI/Icons/error_red.svg +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/error_red.svg @@ -1,10 +1,11 @@ + id="defs11659" /> + inkscape:current-layer="svg11655" /> + d="M480-280q17 0 28.5-11.5T520-320q0-17-11.5-28.5T480-360q-17 0-28.5 11.5T440-320q0 17 11.5 28.5T480-280Zm0-160q17 0 28.5-11.5T520-480v-160q0-17-11.5-28.5T480-680q-17 0-28.5 11.5T440-640v160q0 17 11.5 28.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" + id="path11653" + style="fill:#a42a04;fill-opacity:1" /> diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/patient_failed.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/patient_failed.svg new file mode 100644 index 0000000000..75d95bd770 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/patient_failed.svg @@ -0,0 +1,42 @@ + + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/patient_pending.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/patient_pending.svg new file mode 100644 index 0000000000..e24e5cdff2 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/patient_pending.svg @@ -0,0 +1,42 @@ + + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/patient_success.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/patient_success.svg new file mode 100644 index 0000000000..8f97c7aa1c --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/patient_success.svg @@ -0,0 +1,42 @@ + + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/pending.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/pending.svg index 9609f3f746..597674101d 100644 --- a/Libs/DICOM/Widgets/Resources/UI/Icons/pending.svg +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/pending.svg @@ -1 +1 @@ - + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/query.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/query.svg index 2f1acace02..824d73bb5b 100644 --- a/Libs/DICOM/Widgets/Resources/UI/Icons/query.svg +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/query.svg @@ -1 +1,46 @@ - + + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/query_failed.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/query_failed.svg new file mode 100644 index 0000000000..4f117da228 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/query_failed.svg @@ -0,0 +1,50 @@ + + + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/query_success.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/query_success.svg new file mode 100644 index 0000000000..53b21267b4 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/query_success.svg @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/search_local.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/search_local.svg new file mode 100644 index 0000000000..8b6cdffb31 --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/search_local.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/select_none.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/select_none.svg new file mode 100644 index 0000000000..b509c4ec8e --- /dev/null +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/select_none.svg @@ -0,0 +1 @@ + diff --git a/Libs/DICOM/Widgets/Resources/UI/Icons/wait.svg b/Libs/DICOM/Widgets/Resources/UI/Icons/wait.svg index 69e6392135..0a29fbc60f 100644 --- a/Libs/DICOM/Widgets/Resources/UI/Icons/wait.svg +++ b/Libs/DICOM/Widgets/Resources/UI/Icons/wait.svg @@ -1 +1 @@ - + diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMJobListWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMJobListWidget.ui index 74a1e36d59..f64e3c776f 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMJobListWidget.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMJobListWidget.ui @@ -6,7 +6,7 @@ 0 0 - 816 + 878 498 @@ -25,8 +25,8 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> +</style></head><body style=" font-family:'Ubuntu Sans'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu';"><br /></p></body></html> @@ -79,12 +79,6 @@ p, li { white-space: pre-wrap; } - - - 0 - 0 - - Retry selected canceled or failed jobs @@ -99,12 +93,6 @@ p, li { white-space: pre-wrap; } - - - 0 - 0 - - Stop selected in-progress jobs @@ -117,6 +105,20 @@ p, li { white-space: pre-wrap; } + + + + Stop all the in-progress jobs and clear all entries in the table + + + Clear all + + + + :/Icons/delete.svg:/Icons/delete.svg + + + @@ -145,7 +147,7 @@ p, li { white-space: pre-wrap; } - + Reset all filters to default @@ -159,23 +161,20 @@ p, li { white-space: pre-wrap; } - - - - - 0 - 0 - - + + - Clear the completed jobs + Show completed jobs - Clear completed + Show completed - :/Icons/cleaning.svg:/Icons/cleaning.svg + :/Icons/visible_on.svg:/Icons/visible_on.svg + + + true @@ -198,40 +197,17 @@ p, li { white-space: pre-wrap; } - - - - - 0 - 0 - - - - Show completed jobs - - - Show completed - - - - :/Icons/visible_on.svg:/Icons/visible_on.svg - - - true - - - - - + + - Stop all the in-progress jobs and clear all entries in the table + Clear the completed jobs - Clear all + Clear completed - :/Icons/delete.svg:/Icons/delete.svg + :/Icons/cleaning.svg:/Icons/cleaning.svg diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMPatientItemWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMPatientItemWidget.ui index f822c23042..27d61abb6d 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMPatientItemWidget.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMPatientItemWidget.ui @@ -104,6 +104,12 @@ + + + 0 + 0 + + 100 @@ -120,12 +126,6 @@ - - - - - 3 - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse @@ -171,15 +171,6 @@ - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - 3 - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse @@ -187,6 +178,12 @@ + + + 0 + 0 + + 100 @@ -218,15 +215,6 @@ 0 - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - 3 - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse @@ -234,15 +222,6 @@ - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - 3 - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse @@ -298,7 +277,7 @@ 0 0 996 - 257 + 254 diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMStudyItemWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMStudyItemWidget.ui index 20803e1cb7..59b7d6e297 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMStudyItemWidget.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMStudyItemWidget.ui @@ -6,8 +6,8 @@ 0 0 - 378 - 534 + 787 + 612 @@ -29,36 +29,75 @@ 0 - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 20 - 20 - - - - - 0 - 0 - - - - + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + + :/Icons/pending.svg:/Icons/pending.svg + + + + 24 + 24 + + + + true + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + :/Icons/select_all.svg + :/Icons/select_none.svg:/Icons/select_all.svg + + + + 24 + 24 + + + + + + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + + + @@ -67,21 +106,12 @@ 0 - - - 0 - 0 - - - Study ID 1234 --- Date + Series Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - false - true @@ -117,17 +147,6 @@ - - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:12pt;"><br /></p></body></html> - - - @@ -186,9 +205,6 @@ p, li { white-space: pre-wrap; } - - - @@ -210,6 +226,11 @@ p, li { white-space: pre-wrap; } QTextBrowser
ctkFittedTextBrowser.h
+ + ctkPushButton + QPushButton +
ctkPushButton.h
+
diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMWidget.qrc b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMWidget.qrc index 64595fe295..c9b8a3bbb7 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMWidget.qrc +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMWidget.qrc @@ -15,8 +15,6 @@ Icons/add.svg Icons/dns.svg Icons/save.svg - Icons/query.svg - Icons/wait.svg Icons/downloading.svg Icons/visible_off.svg Icons/visible_on.svg @@ -28,5 +26,14 @@ Icons/reset.svg Icons/error_red.svg Icons/select_all.svg + Icons/select_none.svg + Icons/query_failed.svg + Icons/query_success.svg + Icons/search_local.svg + Icons/query.svg + Icons/wait.svg + Icons/patient_failed.svg + Icons/patient_pending.svg + Icons/patient_success.svg diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMPatientItemWidgetTest1.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMPatientItemWidgetTest1.cpp index 159c2e5a35..5e87546d02 100644 --- a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMPatientItemWidgetTest1.cpp +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMPatientItemWidgetTest1.cpp @@ -52,7 +52,7 @@ int ctkDICOMPatientItemWidgetTest1(int argc, char* argv[]) CHECK_QSTRING(widget.filteringStudyDescription(), ""); CHECK_QSTRING(widget.filteringSeriesDescription(), ""); CHECK_INT(widget.filteringDate(), ctkDICOMPatientItemWidget::DateType::Any); - CHECK_INT(widget.numberOfStudiesPerPatient(), 2); + CHECK_INT(widget.numberOfOpenedStudiesPerPatient(), 2); CHECK_INT(widget.thumbnailSize(), ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium); // Test setting and getting @@ -66,8 +66,8 @@ int ctkDICOMPatientItemWidgetTest1(int argc, char* argv[]) CHECK_QSTRING(widget.filteringSeriesDescription(), "series"); widget.setFilteringDate(ctkDICOMPatientItemWidget::DateType::LastYear); CHECK_INT(widget.filteringDate(), ctkDICOMPatientItemWidget::DateType::LastYear); - widget.setNumberOfStudiesPerPatient(6); - CHECK_INT(widget.numberOfStudiesPerPatient(), 6); + widget.setNumberOfOpenedStudiesPerPatient(6); + CHECK_INT(widget.numberOfOpenedStudiesPerPatient(), 6); widget.setThumbnailSize(ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small); CHECK_INT(widget.thumbnailSize(), ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small); diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMStudyItemWidgetTest1.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMStudyItemWidgetTest1.cpp index 157d431c55..221551a0e1 100644 --- a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMStudyItemWidgetTest1.cpp +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMStudyItemWidgetTest1.cpp @@ -50,7 +50,7 @@ int ctkDICOMStudyItemWidgetTest1(int argc, char* argv[]) CHECK_QSTRING(widget.studyItem(), ""); CHECK_QSTRING(widget.patientID(), ""); CHECK_QSTRING(widget.studyInstanceUID(), ""); - CHECK_QSTRING(widget.title(), "Study ID 1234 --- Date"); + CHECK_QSTRING(widget.title(), "Series"); CHECK_QSTRING(widget.description(), ""); CHECK_QSTRING(widget.filteringSeriesDescription(), ""); CHECK_BOOL(widget.collapsed(), false) diff --git a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMVisualBrowserWidgetTest1.cpp b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMVisualBrowserWidgetTest1.cpp index ad58c9c7d8..017256b0d9 100644 --- a/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMVisualBrowserWidgetTest1.cpp +++ b/Libs/DICOM/Widgets/Testing/Cpp/ctkDICOMVisualBrowserWidgetTest1.cpp @@ -63,7 +63,7 @@ int ctkDICOMVisualBrowserWidgetTest1(int argc, char* argv[]) CHECK_QSTRING(browser.filteringSeriesDescription(), ""); CHECK_QSTRING(browser.filteringModalities().at(0), "Any"); CHECK_INT(browser.filteringDate(), ctkDICOMPatientItemWidget::DateType::Any); - CHECK_INT(browser.numberOfStudiesPerPatient(), 2); + CHECK_INT(browser.numberOfOpenedStudiesPerPatient(), 2); CHECK_INT(browser.thumbnailSize(), ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium); CHECK_BOOL(browser.isSendActionVisible(), false); CHECK_BOOL(browser.isDeleteActionVisible(), true); @@ -145,8 +145,8 @@ int ctkDICOMVisualBrowserWidgetTest1(int argc, char* argv[]) CHECK_QSTRING(browser.filteringModalities().at(0), "CT"); browser.setFilteringDate(ctkDICOMPatientItemWidget::DateType::LastYear); CHECK_INT(browser.filteringDate(), ctkDICOMPatientItemWidget::DateType::LastYear); - browser.setNumberOfStudiesPerPatient(6); - CHECK_INT(browser.numberOfStudiesPerPatient(), 6); + browser.setNumberOfOpenedStudiesPerPatient(6); + CHECK_INT(browser.numberOfOpenedStudiesPerPatient(), 6); browser.setThumbnailSize(ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small); CHECK_INT(browser.thumbnailSize(), ctkDICOMStudyItemWidget::ThumbnailSizeOption::Small); browser.setSendActionVisible(true); diff --git a/Libs/DICOM/Widgets/ctkDICOMJobListWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMJobListWidget.cpp index eed6c5fe8e..83a28a343c 100644 --- a/Libs/DICOM/Widgets/ctkDICOMJobListWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMJobListWidget.cpp @@ -86,7 +86,8 @@ class QCenteredItemModel : public QStandardItemModel Initialized, Queued, Running, - Canceled, + UserStopped, + AttemptFailed, Failed, Completed }; Q_ENUM(JobStatus); @@ -107,7 +108,9 @@ class QCenteredItemModel : public QStandardItemModel SOPInstanceUID, Connection, JobUID, - JobClass + JobClass, + JobThread, + JobLogging, }; Q_ENUM(Columns); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; @@ -179,6 +182,10 @@ QString QCenteredItemModel::getJobTypeAsString(QString jobClass, ctkDICOMJob::DI { return QCenteredItemModel::tr("Echo server"); } + else if (jobClass == "ctkDICOMInserterJob") + { + return QCenteredItemModel::tr("Inserter"); + } return QCenteredItemModel::tr(""); } @@ -192,11 +199,6 @@ void QCenteredItemModel::addJob(const ctkDICOMJobDetail &td, return; } - if (td.JobClass == "ctkDICOMInserterJob") - { - return; - } - int row = 0; // add the job to the top this->insertRow(row); @@ -269,76 +271,84 @@ void QCenteredItemModel::addJob(const ctkDICOMJobDetail &td, this->setData(this->index(row, Columns::JobUID), td.JobUID); this->setData(this->index(row, Columns::JobClass), td.JobClass); + this->setData(this->index(row, Columns::JobThread), td.RunningThreadID); + this->setData(this->index(row, Columns::JobLogging), td.Logging); } //---------------------------------------------------------------------------- void QCenteredItemModel::updateJobStatus(const ctkDICOMJobDetail &td, const JobStatus &status) { - if (td.JobClass == "ctkDICOMInserterJob") + QList list = this->findItems(td.JobUID, Qt::MatchExactly, Columns::JobUID); + if (list.empty()) { return; } - QList list = this->findItems(td.JobUID, Qt::MatchExactly, Columns::JobUID); - if (!list.empty()) + int row = list.first()->row(); + QIcon statusIcon; + QString statusText; + if (status == Queued) { - int row = list.first()->row(); - QIcon statusIcon; - QString statusText; - if (status == Queued) - { - statusIcon = QIcon(":/Icons/pending.svg"); - statusText = tr("queued"); - } - else if (status == Running) - { - statusIcon = QIcon(":/Icons/pending.svg"); - statusText = tr("in-progress"); - if (td.JobClass == "ctkDICOMQueryJob") - { - QList data; - data.append(20); - data.append(100); - this->setData(this->index(row, Columns::Progress), data); - } - } - else if (status == Failed) - { - statusIcon = QIcon(":/Icons/error.svg"); - statusText = tr("failed"); - } - else if (status == Canceled) - { - statusIcon = QIcon(":/Icons/error.svg"); - statusText = tr("canceled"); - } - else if (status == Completed) + statusIcon = QIcon(":/Icons/pending.svg"); + statusText = tr("queued"); + } + else if (status == Running) + { + statusIcon = QIcon(":/Icons/pending.svg"); + statusText = tr("in-progress"); + if (td.JobClass == "ctkDICOMQueryJob") { - statusIcon = QIcon(":/Icons/accept.svg"); - statusText = tr("completed"); QList data; - data.append(100); + data.append(20); data.append(100); this->setData(this->index(row, Columns::Progress), data); } + } + else if (status == Failed) + { + statusIcon = QIcon(":/Icons/error.svg"); + statusText = tr("failed"); + } + else if (status == UserStopped) + { + statusIcon = QIcon(":/Icons/error.svg"); + statusText = tr("user-stopped"); + } + else if (status == AttemptFailed) + { + statusIcon = QIcon(":/Icons/error.svg"); + statusText = tr("attempt-failed"); + } + else if (status == Completed) + { + statusIcon = QIcon(":/Icons/accept.svg"); + statusText = tr("completed"); + QList data; + data.append(100); + data.append(100); + this->setData(this->index(row, Columns::Progress), data); + } - QStandardItem *statusItem = new QStandardItem(QString("statusItem")); - statusItem->setIcon(statusIcon); - this->setItem(row, Columns::Status, statusItem); - this->setData(this->index(row, Columns::Status), statusText); - this->setData(this->index(row, Columns::Status), statusText, Qt::ToolTipRole); + QStandardItem *statusItem = new QStandardItem(QString("statusItem")); + statusItem->setIcon(statusIcon); + this->setItem(row, Columns::Status, statusItem); + this->setData(this->index(row, Columns::Status), statusText); + this->setData(this->index(row, Columns::Status), statusText, Qt::ToolTipRole); - if (status == Running) - { - this->setData(this->index(row, Columns::StartDateTime), td.CreationDateTime); - this->setData(this->index(row, Columns::StartDateTime), td.CreationDateTime, Qt::ToolTipRole); - } - else - { - this->setData(this->index(row, Columns::CompletionDateTime), td.CompletionDateTime); - this->setData(this->index(row, Columns::CompletionDateTime), td.CompletionDateTime, Qt::ToolTipRole); - } + if (status == Running) + { + this->setData(this->index(row, Columns::StartDateTime), td.CreationDateTime); + this->setData(this->index(row, Columns::StartDateTime), td.CreationDateTime, Qt::ToolTipRole); + } + else + { + this->setData(this->index(row, Columns::CompletionDateTime), td.CompletionDateTime); + this->setData(this->index(row, Columns::CompletionDateTime), td.CompletionDateTime, Qt::ToolTipRole); } + + this->setData(this->index(row, Columns::JobThread), td.RunningThreadID); + this->setData(this->index(row, Columns::JobThread), td.RunningThreadID, Qt::ToolTipRole); + this->setData(this->index(row, Columns::JobLogging), td.Logging); } //---------------------------------------------------------------------------- @@ -414,12 +424,15 @@ void QCenteredItemModel::setProgressBar(int row, const ctkDICOMJobDetail &td, ct void QCenteredItemModel::clearCompletedJobs() { #if (QT_VERSION >= QT_VERSION_CHECK(5,15,0)) - QList list = this->findItems(tr("completed"), Qt::MatchRegularExpression, Columns::Status); + QList attemptFailedList = this->findItems(tr("attempt-failed"), Qt::MatchRegularExpression, Columns::Status); + QList completedList = this->findItems(tr("completed"), Qt::MatchRegularExpression, Columns::Status); #else - QList list = this->findItems(tr("completed"), Qt::MatchRegExp, Columns::Status); + QList attemptFailedList = this->findItems(tr("attempt-failed"), Qt::MatchRegExp, Columns::Status); + QList completedList = this->findItems(tr("completed"), Qt::MatchRegExp, Columns::Status); #endif - foreach (QStandardItem* item, list) + completedList.append(attemptFailedList); + foreach (QStandardItem* item, completedList) { this->removeRow(item->row()); } @@ -492,6 +505,14 @@ QCenteredItemModel::Columns QCenteredItemModel::getColumnIndexFromString(QString { return Columns::JobClass; } + else if (columnString == QCenteredItemModel::tr("Thread")) + { + return Columns::JobThread; + } + else if (columnString == QCenteredItemModel::tr("Logging")) + { + return Columns::JobLogging; + } else { return Columns::JobClass; @@ -535,6 +556,10 @@ QString QCenteredItemModel::getColumnStringFromIndex(Columns columnIndex) return QCenteredItemModel::tr("Job UID"); case Columns::JobClass: return QCenteredItemModel::tr("Class"); + case Columns::JobThread: + return QCenteredItemModel::tr("Thread"); + case Columns::JobLogging: + return QCenteredItemModel::tr("Logging"); default: return QCenteredItemModel::tr(""); } @@ -617,6 +642,8 @@ void ctkDICOMJobListWidgetPrivate::init() allColumnNames.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::Connection)); allColumnNames.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::JobUID)); allColumnNames.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::JobClass)); + allColumnNames.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::JobThread)); + allColumnNames.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::JobLogging)); this->dataModel = QSharedPointer(new QCenteredItemModel(0, allColumnNames.count(), q)); this->dataModel->setHorizontalHeaderLabels(allColumnNames); @@ -640,6 +667,8 @@ void ctkDICOMJobListWidgetPrivate::init() this->JobsView->setColumnHidden(QCenteredItemModel::Columns::SOPInstanceUID, true); this->JobsView->setColumnHidden(QCenteredItemModel::Columns::JobUID, true); this->JobsView->setColumnHidden(QCenteredItemModel::Columns::JobClass, true); + this->JobsView->setColumnHidden(QCenteredItemModel::Columns::JobThread, true); + this->JobsView->setColumnHidden(QCenteredItemModel::Columns::JobLogging, true); QObject::connect(this->JobsView->selectionModel(), &QItemSelectionModel::selectionChanged, q, &ctkDICOMJobListWidget::onJobsViewSelectionChanged); @@ -685,16 +714,18 @@ void ctkDICOMJobListWidgetPrivate::disconnectScheduler() q, SLOT(onJobInitialized(QVariant))); ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(jobQueued(QVariant)), q, SLOT(onJobQueued(QVariant))); - ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(jobStarted(QVariant)), - q, SLOT(onJobStarted(QVariant))); - ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(jobCanceled(QVariant)), - q, SLOT(onJobCanceled(QVariant))); - ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(jobFinished(QVariant)), - q, SLOT(onJobFinished(QVariant))); - ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), - q, SLOT(onJobFailed(QVariant))); - ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - q, SLOT(onProgressJobDetail(QVariant))); + ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(jobStarted(QList)), + q, SLOT(onJobStarted(QList))); + ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(jobUserStopped(QList)), + q, SLOT(onJobUserStopped(QList))); + ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(jobFinished(QList)), + q, SLOT(onJobFinished(QList))); + ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(jobAttemptFailed(QList)), + q, SLOT(onJobAttemptFailed(QList))); + ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(jobFailed(QList)), + q, SLOT(onJobFailed(QList))); + ctkDICOMJobListWidget::disconnect(this->Scheduler.data(), SIGNAL(progressJobDetail(QList)), + q, SLOT(onProgressJobDetail(QList))); } //---------------------------------------------------------------------------- @@ -710,16 +741,18 @@ void ctkDICOMJobListWidgetPrivate::connectScheduler() q, SLOT(onJobInitialized(QVariant))); ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(jobQueued(QVariant)), q, SLOT(onJobQueued(QVariant))); - ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(jobStarted(QVariant)), - q, SLOT(onJobStarted(QVariant))); - ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(jobCanceled(QVariant)), - q, SLOT(onJobCanceled(QVariant))); - ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(jobFinished(QVariant)), - q, SLOT(onJobFinished(QVariant))); - ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), - q, SLOT(onJobFailed(QVariant))); - ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - q, SLOT(onProgressJobDetail(QVariant))); + ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(jobStarted(QList)), + q, SLOT(onJobStarted(QList))); + ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(jobUserStopped(QList)), + q, SLOT(onJobUserStopped(QList))); + ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(jobFinished(QList)), + q, SLOT(onJobFinished(QList))); + ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(jobAttemptFailed(QList)), + q, SLOT(onJobAttemptFailed(QList))); + ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(jobFailed(QList)), + q, SLOT(onJobFailed(QList))); + ctkDICOMJobListWidget::connect(this->Scheduler.data(), SIGNAL(progressJobDetail(QList)), + q, SLOT(onProgressJobDetail(QList))); } //---------------------------------------------------------------------------- @@ -732,6 +765,8 @@ void ctkDICOMJobListWidgetPrivate::setFilterKeyColumn(QString text) //---------------------------------------------------------------------------- void ctkDICOMJobListWidgetPrivate::updateJobsDetailsWidget() { + Q_Q(ctkDICOMJobListWidget); + this->DetailsTextBrowser->clear(); QItemSelectionModel *select = this->JobsView->selectionModel(); @@ -742,10 +777,14 @@ void ctkDICOMJobListWidgetPrivate::updateJobsDetailsWidget() QString detailsText; QModelIndexList selectedRows = select->selectedRows(); + int count = 0; foreach (QModelIndex rowIndex, selectedRows) { - detailsText.append(QString("\n || --------------------------------------------------------" - "------------------------------------------------------- ||\n\n")); + if (count != 0) + { + detailsText.append(QString("\n =============================================================== \n")); + } + int row = rowIndex.row(); QString jobType = this->showCompletedProxyModel->index (row, QCenteredItemModel::Columns::JobType).data().toString(); @@ -777,85 +816,102 @@ void ctkDICOMJobListWidgetPrivate::updateJobsDetailsWidget() (row, QCenteredItemModel::Columns::JobUID).data().toString(); QString jobClass = this->showCompletedProxyModel->index (row, QCenteredItemModel::Columns::JobClass).data().toString(); + QString thread = this->showCompletedProxyModel->index + (row, QCenteredItemModel::Columns::JobThread).data().toString(); + QString logging = this->showCompletedProxyModel->index + (row, QCenteredItemModel::Columns::JobLogging).data().toString(); if (!jobType.isEmpty()) { detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::JobType)); - detailsText.append(QString(" : ") + jobType + QString(" \n")); + detailsText.append(QString(": ") + jobType + QString("\n")); } if (!jobUID.isEmpty()) { detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::JobUID)); - detailsText.append(QString(" : ") + jobUID + QString(" \n")); + detailsText.append(QString(": ") + jobUID + QString("\n")); } if (!jobClass.isEmpty()) { detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::JobClass)); - detailsText.append(QString(" : ") + jobClass + QString(" \n")); + detailsText.append(QString(": ") + jobClass + QString("\n")); } if (!connection.isEmpty()) { detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::Connection)); - detailsText.append(QString(" : ") + connection + QString(" \n")); + detailsText.append(QString(": ") + connection + QString("\n")); } if (!status.isEmpty()) { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::Status)); - detailsText.append(QString(" : ") + status + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::Status)); + detailsText.append(QString(": ") + status + QString("\n")); } if (!creationDateTime.isEmpty()) { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::CreationDateTime)); - detailsText.append(QString(" : ") + creationDateTime + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::CreationDateTime)); + detailsText.append(QString(": ") + creationDateTime + QString("\n")); } if (!startDateTime.isEmpty()) { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::StartDateTime)); - detailsText.append(QString(" : ") + startDateTime + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::StartDateTime)); + detailsText.append(QString(": ") + startDateTime + QString("\n")); } if (!completionDateTime.isEmpty()) { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::CompletionDateTime)); - detailsText.append(QString(" : ") + completionDateTime + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::CompletionDateTime)); + detailsText.append(QString(": ") + completionDateTime + QString("\n")); } if (!dicomLevel.isEmpty() && dicomLevel != "None") { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::DICOMLevel)); - detailsText.append(QString(" : ") + dicomLevel + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::DICOMLevel)); + detailsText.append(QString(": ") + dicomLevel + QString("\n")); } if (!patientID.isEmpty()) { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::PatientID)); - detailsText.append(QString(" : ") + patientID + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::PatientID)); + detailsText.append(QString(": ") + patientID + QString("\n")); } if (!patientName.isEmpty()) { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::PatientName)); - detailsText.append(QString(" : ") + patientName + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::PatientName)); + detailsText.append(QString(": ") + patientName + QString("\n")); } if (!patientBirthDate.isEmpty()) { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::PatientBirthDate)); - detailsText.append(QString(" : ") + patientBirthDate + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::PatientBirthDate)); + detailsText.append(QString(": ") + patientBirthDate + QString("\n")); } if (!studyInstanceUID.isEmpty()) { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::StudyInstanceUID)); - detailsText.append(QString(" : ") + studyInstanceUID + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::StudyInstanceUID)); + detailsText.append(QString(": ") + studyInstanceUID + QString("\n")); } if (!seriesInstanceUID.isEmpty()) { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::SeriesInstanceUID)); - detailsText.append(QString(" : ") + seriesInstanceUID + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::SeriesInstanceUID)); + detailsText.append(QString(": ") + seriesInstanceUID + QString("\n")); } if (!sopInstanceUID.isEmpty()) { - detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::SOPInstanceUID)); - detailsText.append(QString(" : ") + sopInstanceUID + QString(" \n")); + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::SOPInstanceUID)); + detailsText.append(QString(": ") + sopInstanceUID + QString("\n")); + } + if (!thread.isEmpty()) + { + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::JobThread)); + detailsText.append(QString(": ") + thread + QString("\n")); + } + if (!logging.isEmpty()) + { + detailsText.append(QCenteredItemModel::getColumnStringFromIndex(QCenteredItemModel::Columns::JobLogging)); + detailsText.append(QString(":\n") + logging + QString("\n")); } - //detailsText.append(QString("Logger : ") + QString(" \n")); - // To Do: get DCMTK logging stream per job + if (count == 0) + { + q->patientSelected(patientID, patientName, patientBirthDate); + } + count++; } this->DetailsTextBrowser->setPlainText(detailsText); @@ -876,7 +932,8 @@ void ctkDICOMJobListWidgetPrivate::retryJobs() (row, QCenteredItemModel::Columns::JobClass).data().toString(); QString jobUID = this->showCompletedProxyModel->index (row, QCenteredItemModel::Columns::JobUID).data().toString(); - if (status != ctkDICOMJobListWidget::tr("failed") && status != ctkDICOMJobListWidget::tr("canceled")) + + if (status != ctkDICOMJobListWidget::tr("failed") && status != ctkDICOMJobListWidget::tr("user-stopped")) { continue; } @@ -919,6 +976,8 @@ void ctkDICOMJobListWidgetPrivate::retryJobs() (row, QCenteredItemModel::Columns::SeriesInstanceUID).data().toString(); jobDetail.SOPInstanceUID = this->showCompletedProxyModel->index (row, QCenteredItemModel::Columns::SOPInstanceUID).data().toString(); + jobDetail.ConnectionName = this->showCompletedProxyModel->index + (row, QCenteredItemModel::Columns::Connection).data().toString(); jobsUIDsToRetry.insert(jobUID, jobDetail); } @@ -943,7 +1002,8 @@ void ctkDICOMJobListWidgetPrivate::retryJobs() td.PatientID == filteredTD.PatientID && td.StudyInstanceUID == filteredTD.StudyInstanceUID && td.SeriesInstanceUID == filteredTD.SeriesInstanceUID && - td.SOPInstanceUID == filteredTD.SOPInstanceUID) + td.SOPInstanceUID == filteredTD.SOPInstanceUID && + td.ConnectionName == filteredTD.ConnectionName) { duplicate = true; break; @@ -1042,81 +1102,113 @@ void ctkDICOMJobListWidget::onJobQueued(QVariant data) ctkDICOMJobDetail td = data.value(); if(td.JobClass.isEmpty()) - { + { return; - } + } d->dataModel->updateJobStatus(td, QCenteredItemModel::Queued); } //---------------------------------------------------------------------------- -void ctkDICOMJobListWidget::onJobStarted(QVariant data) +void ctkDICOMJobListWidget::onJobStarted(QList datas) { Q_D(ctkDICOMJobListWidget); - ctkDICOMJobDetail td = data.value(); - - if(td.JobClass.isEmpty()) + foreach (QVariant data, datas) { - return; - } + ctkDICOMJobDetail td = data.value(); - d->dataModel->updateJobStatus(td, QCenteredItemModel::Running); + if(td.JobClass.isEmpty()) + { + continue; + } + + d->dataModel->updateJobStatus(td, QCenteredItemModel::Running); + } } //---------------------------------------------------------------------------- -void ctkDICOMJobListWidget::onJobFinished(QVariant data) +void ctkDICOMJobListWidget::onJobFinished(QList datas) { Q_D(ctkDICOMJobListWidget); - ctkDICOMJobDetail td = data.value(); - - if(td.JobClass.isEmpty()) + foreach (QVariant data, datas) { - return; - } + ctkDICOMJobDetail td = data.value(); + + if(td.JobClass.isEmpty()) + { + continue; + } - d->dataModel->updateJobStatus(td, QCenteredItemModel::Completed); + d->dataModel->updateJobStatus(td, QCenteredItemModel::Completed); + } } //---------------------------------------------------------------------------- -void ctkDICOMJobListWidget::onProgressJobDetail(QVariant data) +void ctkDICOMJobListWidget::onProgressJobDetail(QList datas) { Q_D(ctkDICOMJobListWidget); - ctkDICOMJobDetail td = data.value(); - - if(td.JobType == ctkDICOMJobResponseSet::JobType::None) + foreach (QVariant data, datas) { - return; - } + ctkDICOMJobDetail td = data.value(); + + if(td.JobType == ctkDICOMJobResponseSet::JobType::None) + { + continue; + } - d->dataModel->updateProgressBar(td, d->Scheduler->dicomDatabase()); + d->dataModel->updateProgressBar(td, d->Scheduler->dicomDatabase()); + } } //---------------------------------------------------------------------------- -void ctkDICOMJobListWidget::onJobCanceled(QVariant data) +void ctkDICOMJobListWidget::onJobAttemptFailed(QList datas) { Q_D(ctkDICOMJobListWidget); - ctkDICOMJobDetail td = data.value(); - - if(td.JobClass.isEmpty()) + foreach (QVariant data, datas) { - return; - } + ctkDICOMJobDetail td = data.value(); + + if(td.JobClass.isEmpty()) + { + continue; + } - d->dataModel->updateJobStatus(td, QCenteredItemModel::Canceled); + d->dataModel->updateJobStatus(td, QCenteredItemModel::AttemptFailed); + } } //---------------------------------------------------------------------------- -void ctkDICOMJobListWidget::onJobFailed(QVariant data) +void ctkDICOMJobListWidget::onJobFailed(QList datas) { Q_D(ctkDICOMJobListWidget); - ctkDICOMJobDetail td = data.value(); - - if(td.JobClass.isEmpty()) + foreach (QVariant data, datas) { - return; + ctkDICOMJobDetail td = data.value(); + + if(td.JobClass.isEmpty()) + { + continue; + } + + d->dataModel->updateJobStatus(td, QCenteredItemModel::Failed); } +} - d->dataModel->updateJobStatus(td, QCenteredItemModel::Failed); +//---------------------------------------------------------------------------- +void ctkDICOMJobListWidget::onJobUserStopped(QList datas) +{ + Q_D(ctkDICOMJobListWidget); + foreach (QVariant data, datas) + { + ctkDICOMJobDetail td = data.value(); + + if(td.JobClass.isEmpty()) + { + continue; + } + + d->dataModel->updateJobStatus(td, QCenteredItemModel::UserStopped); + } } //---------------------------------------------------------------------------- @@ -1149,7 +1241,7 @@ void ctkDICOMJobListWidget::onJobsViewSelectionChanged() QString jobClass = d->showCompletedProxyModel->index (row, QCenteredItemModel::Columns::JobClass).data().toString(); - if ((status == tr("failed") || status == tr("canceled")) && + if ((status == tr("failed") || status == tr("user-stopped")) && (jobClass == "ctkDICOMQueryJob" || jobClass == "ctkDICOMRetrieveJob")) { failedJobSelected = true; @@ -1234,7 +1326,7 @@ void ctkDICOMJobListWidget::onResetFiltersButtonClicked() void ctkDICOMJobListWidget::onShowCompletedButtonToggled(bool toggled) { Q_D(ctkDICOMJobListWidget); - QString text = toggled ? "" : tr("initialized|queued|in-progress|canceled|failed"); + QString text = toggled ? "" : tr("^initialized$|^queued$|^in-progress$|^user-stopped$|^failed$"); d->showCompletedProxyModel->setFilterRegExp(text); } diff --git a/Libs/DICOM/Widgets/ctkDICOMJobListWidget.h b/Libs/DICOM/Widgets/ctkDICOMJobListWidget.h index bc783a627f..33cb6cc517 100644 --- a/Libs/DICOM/Widgets/ctkDICOMJobListWidget.h +++ b/Libs/DICOM/Widgets/ctkDICOMJobListWidget.h @@ -57,11 +57,12 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMJobListWidget : public QWidget public Q_SLOTS: void onJobInitialized(QVariant); void onJobQueued(QVariant); - void onJobStarted(QVariant); - void onJobCanceled(QVariant); - void onJobFailed(QVariant); - void onJobFinished(QVariant); - void onProgressJobDetail(QVariant); + void onJobStarted(QList); + void onJobAttemptFailed(QList); + void onJobFailed(QList); + void onJobUserStopped(QList); + void onJobFinished(QList); + void onProgressJobDetail(QList); void onFilterTextChanged(QString); void onFilterColumnChanged(QString); @@ -75,6 +76,10 @@ public Q_SLOTS: void onClearCompletedButtonClicked(); void onClearAllButtonClicked(); +Q_SIGNALS: + /// Emitted when a job row is selected + void patientSelected(const QString&, const QString&, const QString&); + protected: QScopedPointer d_ptr; diff --git a/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.cpp index 0dc4e8a889..55a9eafc7f 100644 --- a/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.cpp @@ -65,7 +65,8 @@ class ctkDICOMPatientItemWidgetPrivate : public Ui_ctkDICOMPatientItemWidget ~ctkDICOMPatientItemWidgetPrivate(); void init(QWidget* parentWidget); - + void connectScheduler(); + void disconnectScheduler(); QString getPatientItemFromPatientID(const QString& patientID); QString formatDate(const QString&); bool isStudyItemAlreadyAdded(const QString& studyItem); @@ -80,12 +81,14 @@ class ctkDICOMPatientItemWidgetPrivate : public Ui_ctkDICOMPatientItemWidget QSharedPointer Scheduler; QSharedPointer VisualDICOMBrowser; - int NumberOfStudiesPerPatient; + int NumberOfOpenedStudiesPerPatient; ctkDICOMStudyItemWidget::ThumbnailSizeOption ThumbnailSize; QString PatientItem; QString PatientID; QString PatientName; + QString PatientBirthDate; + QString PatientSex; QString FilteringStudyDescription; ctkDICOMPatientItemWidget::DateType FilteringDate; @@ -97,7 +100,8 @@ class ctkDICOMPatientItemWidgetPrivate : public Ui_ctkDICOMPatientItemWidget QMap StudyItemWidgetsConnectionMap; QSpacerItem* StudiesListVerticalSpacer; - QStringList AllowedServers = QStringList(); + QStringList AllowedServers; + ctkDICOMPatientItemWidget::OperationStatus Status; bool IsGUIUpdating; bool QueryOn; @@ -112,11 +116,14 @@ ctkDICOMPatientItemWidgetPrivate::ctkDICOMPatientItemWidgetPrivate(ctkDICOMPatie : q_ptr(&obj) { this->FilteringDate = ctkDICOMPatientItemWidget::DateType::Any; - this->NumberOfStudiesPerPatient = 2; + this->NumberOfOpenedStudiesPerPatient = 2; this->ThumbnailSize = ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium; this->PatientItem = ""; this->PatientID = ""; this->PatientName = ""; + this->PatientBirthDate = ""; + this->PatientSex = ""; + this->FilteringStudyDescription = ""; this->FilteringSeriesDescription = ""; @@ -126,6 +133,9 @@ ctkDICOMPatientItemWidgetPrivate::ctkDICOMPatientItemWidgetPrivate(ctkDICOMPatie this->StudiesListVerticalSpacer = new QSpacerItem(0, 5, QSizePolicy::Fixed, QSizePolicy::Expanding); + this->AllowedServers = QStringList(); + this->Status = ctkDICOMPatientItemWidget::NoOperation; + this->IsGUIUpdating = false; this->QueryOn = true; this->RetrieveOn = true; @@ -155,6 +165,48 @@ void ctkDICOMPatientItemWidgetPrivate::init(QWidget* parentWidget) q, SLOT(onPatientServersCheckableComboBoxChanged())); } +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidgetPrivate::connectScheduler() +{ + Q_Q(ctkDICOMPatientItemWidget); + if (!this->Scheduler) + { + return; + } + + QObject::connect(this->Scheduler.data(), SIGNAL(progressJobDetail(QList)), + q, SLOT(updateGUIFromScheduler(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobStarted(QList)), + q, SLOT(onJobStarted(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobUserStopped(QList)), + q, SLOT(onJobUserStopped(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobFailed(QList)), + q, SLOT(onJobFailed(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobFinished(QList)), + q, SLOT(onJobFinished(QList))); +} + +//---------------------------------------------------------------------------- +void ctkDICOMPatientItemWidgetPrivate::disconnectScheduler() +{ + Q_Q(ctkDICOMPatientItemWidget); + if (!this->Scheduler) + { + return; + } + + QObject::disconnect(this->Scheduler.data(), SIGNAL(progressJobDetail(QList)), + q, SLOT(updateGUIFromScheduler(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobStarted(QList)), + q, SLOT(onJobStarted(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobUserStopped(QList)), + q, SLOT(onJobUserStopped(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobFailed(QList)), + q, SLOT(onJobFailed(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobFinished(QList)), + q, SLOT(onJobFinished(QList))); +} + //---------------------------------------------------------------------------- QString ctkDICOMPatientItemWidgetPrivate::getPatientItemFromPatientID(const QString& patientID) { @@ -262,12 +314,10 @@ void ctkDICOMPatientItemWidgetPrivate::createStudies() } else { - QString patientName = this->DicomDatabase->fieldForPatient("PatientsName", this->PatientItem); - patientName.replace(R"(^)", R"( )"); - this->PatientNameValueLabel->setText(patientName); - this->PatientIDValueLabel->setText(this->DicomDatabase->fieldForPatient("PatientID", this->PatientItem)); - this->PatientSexValueLabel->setText(this->DicomDatabase->fieldForPatient("PatientsSex", this->PatientItem)); - this->PatientBirthDateValueLabel->setText(this->formatDate(this->DicomDatabase->fieldForPatient("PatientsBirthDate", this->PatientItem))); + this->PatientNameValueLabel->setText(this->PatientName); + this->PatientIDValueLabel->setText(this->PatientID); + this->PatientSexValueLabel->setText(this->PatientSex); + this->PatientBirthDateValueLabel->setText(this->PatientBirthDate); } QStringList studiesList = this->DicomDatabase->studiesForPatient(this->PatientItem); @@ -309,13 +359,17 @@ void ctkDICOMPatientItemWidgetPrivate::createStudies() QString studyDateString = this->DicomDatabase->fieldForStudy("StudyDate", studyItem); studyDateString.replace(QString("-"), QString("")); - QString studyDescription = this->DicomDatabase->fieldForStudy("StudyDescription", studyItem); - if (studyDateString.isEmpty()) { studyDateString = QDate::currentDate().toString("yyyyMMdd"); } + QString studyDescription = this->DicomDatabase->fieldForStudy("StudyDescription", studyItem); + if (studyDescription.isEmpty()) + { + studyDescription = q->tr("UNDEFINED"); + } + if ((!this->FilteringStudyDescription.isEmpty() && !studyDescription.contains(this->FilteringStudyDescription, Qt::CaseInsensitive))) { @@ -371,7 +425,7 @@ void ctkDICOMPatientItemWidgetPrivate::createStudies() foreach (ctkDICOMStudyItemWidget* studyItemWidget, studiesMap) { studiesListWidgetLayout->addWidget(studyItemWidget); - if (cont < this->NumberOfStudiesPerPatient) + if (cont < this->NumberOfOpenedStudiesPerPatient) { studyItemWidget->setCollapsed(false); studyItemWidget->generateSeries(this->QueryOn, this->RetrieveOn); @@ -573,11 +627,17 @@ ctkDICOMPatientItemWidget::~ctkDICOMPatientItemWidget() //------------------------------------------------------------------------------ CTK_SET_CPP(ctkDICOMPatientItemWidget, const QStringList&, setAllowedServers, AllowedServers); CTK_GET_CPP(ctkDICOMPatientItemWidget, QStringList, allowedServers, AllowedServers); +CTK_SET_CPP(ctkDICOMPatientItemWidget, const OperationStatus&, setOperationStatus, Status); +CTK_GET_CPP(ctkDICOMPatientItemWidget, ctkDICOMPatientItemWidget::OperationStatus, operationStatus, Status); CTK_GET_CPP(ctkDICOMPatientItemWidget, QString, patientItem, PatientItem); CTK_SET_CPP(ctkDICOMPatientItemWidget, const QString&, setPatientID, PatientID); CTK_GET_CPP(ctkDICOMPatientItemWidget, QString, patientID, PatientID); CTK_SET_CPP(ctkDICOMPatientItemWidget, const QString&, setPatientName, PatientName); CTK_GET_CPP(ctkDICOMPatientItemWidget, QString, patientName, PatientName); +CTK_SET_CPP(ctkDICOMPatientItemWidget, const QString&, setPatientBirthDate, PatientBirthDate); +CTK_GET_CPP(ctkDICOMPatientItemWidget, QString, patientBirthDate, PatientBirthDate); +CTK_SET_CPP(ctkDICOMPatientItemWidget, const QString&, setPatientSex, PatientSex); +CTK_GET_CPP(ctkDICOMPatientItemWidget, QString, patientSex, PatientSex); CTK_SET_CPP(ctkDICOMPatientItemWidget, const QString&, setFilteringStudyDescription, FilteringStudyDescription); CTK_GET_CPP(ctkDICOMPatientItemWidget, QString, filteringStudyDescription, FilteringStudyDescription); CTK_SET_CPP(ctkDICOMPatientItemWidget, const ctkDICOMPatientItemWidget::DateType&, setFilteringDate, FilteringDate); @@ -586,8 +646,8 @@ CTK_SET_CPP(ctkDICOMPatientItemWidget, const QString&, setFilteringSeriesDescrip CTK_GET_CPP(ctkDICOMPatientItemWidget, QString, filteringSeriesDescription, FilteringSeriesDescription); CTK_SET_CPP(ctkDICOMPatientItemWidget, const QStringList&, setFilteringModalities, FilteringModalities); CTK_GET_CPP(ctkDICOMPatientItemWidget, QStringList, filteringModalities, FilteringModalities); -CTK_SET_CPP(ctkDICOMPatientItemWidget, int, setNumberOfStudiesPerPatient, NumberOfStudiesPerPatient); -CTK_GET_CPP(ctkDICOMPatientItemWidget, int, numberOfStudiesPerPatient, NumberOfStudiesPerPatient); +CTK_SET_CPP(ctkDICOMPatientItemWidget, int, setNumberOfOpenedStudiesPerPatient, NumberOfOpenedStudiesPerPatient); +CTK_GET_CPP(ctkDICOMPatientItemWidget, int, numberOfOpenedStudiesPerPatient, NumberOfOpenedStudiesPerPatient); CTK_SET_CPP(ctkDICOMPatientItemWidget, const ctkDICOMStudyItemWidget::ThumbnailSizeOption&, setThumbnailSize, ThumbnailSize); CTK_GET_CPP(ctkDICOMPatientItemWidget, ctkDICOMStudyItemWidget::ThumbnailSizeOption, thumbnailSize, ThumbnailSize); @@ -617,38 +677,18 @@ QSharedPointer ctkDICOMPatientItemWidget::schedulerShared() c void ctkDICOMPatientItemWidget::setScheduler(ctkDICOMScheduler& scheduler) { Q_D(ctkDICOMPatientItemWidget); - if (d->Scheduler) - { - QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - this, SLOT(updateGUIFromScheduler(QVariant))); - } - + d->disconnectScheduler(); d->Scheduler = QSharedPointer(&scheduler, skipDelete); - - if (d->Scheduler) - { - QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - this, SLOT(updateGUIFromScheduler(QVariant))); - } + d->connectScheduler(); } //---------------------------------------------------------------------------- void ctkDICOMPatientItemWidget::setScheduler(QSharedPointer scheduler) { Q_D(ctkDICOMPatientItemWidget); - if (d->Scheduler) - { - QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - this, SLOT(updateGUIFromScheduler(QVariant))); - } - + d->disconnectScheduler(); d->Scheduler = scheduler; - - if (d->Scheduler) - { - QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - this, SLOT(updateGUIFromScheduler(QVariant))); - } + d->connectScheduler(); } //---------------------------------------------------------------------------- @@ -731,29 +771,31 @@ void ctkDICOMPatientItemWidget::addStudyItemWidget(const QString& studyItem) QString studyDate = d->DicomDatabase->fieldForStudy("StudyDate", studyItem); QString formattedStudyDate = d->formatDate(studyDate); QString studyDescription = d->DicomDatabase->fieldForStudy("StudyDescription", studyItem); - - ctkDICOMStudyItemWidget* studyItemWidget = new ctkDICOMStudyItemWidget(d->VisualDICOMBrowser.data()); + if (studyDescription.isEmpty()) + { + studyDescription = this->tr("UNDEFINED"); + } + ctkDICOMStudyItemWidget* studyItemWidget = + new ctkDICOMStudyItemWidget(this, d->VisualDICOMBrowser.data()); studyItemWidget->setStudyItem(studyItem); studyItemWidget->setPatientID(d->PatientID); studyItemWidget->setStudyInstanceUID(studyInstanceUID); - if (formattedStudyDate.isEmpty() && studyID.isEmpty()) + + QString fullDescription = tr("Study"); + if (!studyID.isEmpty()) { - studyItemWidget->setTitle(tr("Study")); + fullDescription += tr(" ID %1").arg(studyID); } - else if (formattedStudyDate.isEmpty()) + if (!formattedStudyDate.isEmpty()) { - studyItemWidget->setTitle(tr("Study ID %1").arg(studyID)); + fullDescription += tr(" - %1").arg(formattedStudyDate); } - else if (studyID.isEmpty()) + if (!studyDescription.isEmpty()) { - studyItemWidget->setTitle(tr("Study --- %1").arg(formattedStudyDate)); - } - else - { - studyItemWidget->setTitle(tr("Study ID %1 --- %2").arg(studyID).arg(formattedStudyDate)); + fullDescription += tr(" - %1").arg(studyDescription); } - studyItemWidget->setDescription(studyDescription); + studyItemWidget->setDescription(fullDescription); studyItemWidget->setThumbnailSize(d->ThumbnailSize); studyItemWidget->setFilteringSeriesDescription(d->FilteringSeriesDescription); studyItemWidget->setFilteringModalities(d->FilteringModalities); @@ -849,7 +891,8 @@ void ctkDICOMPatientItemWidget::generateStudies(bool query, bool retrieve) d->QueryOn = query; d->RetrieveOn = retrieve; d->createStudies(); - if (query && d->Scheduler && d->Scheduler->queryRetrieveServersCount() > 0) + if (query && d->Scheduler && + d->Scheduler->queryRetrieveServersCount() > 0) { d->Scheduler->queryStudies(d->PatientID, QThread::NormalPriority, @@ -876,24 +919,142 @@ void ctkDICOMPatientItemWidget::generateSeriesAtToggle(bool toggled, const QStri } //------------------------------------------------------------------------------ -void ctkDICOMPatientItemWidget::updateGUIFromScheduler(const QVariant& data) +void ctkDICOMPatientItemWidget::updateGUIFromScheduler(QList datas) { Q_D(ctkDICOMPatientItemWidget); - ctkDICOMJobDetail td = data.value(); - if (td.JobUID.isEmpty()) + bool updateStudies = false; + foreach (QVariant data, datas) + { + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty()) + { + d->createStudies(); + continue; + } + + if (td.PatientID != d->PatientID) + { + continue; + } + + emit this->progressJobDetail(data); + + if (td.JobType != ctkDICOMJobResponseSet::JobType::QueryStudies) + { + continue; + } + + updateStudies = true; + } + + if (updateStudies) { d->createStudies(); } +} - if (td.JobUID.isEmpty() || - td.JobType != ctkDICOMJobResponseSet::JobType::QueryStudies || - td.PatientID != d->PatientID) +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::onJobStarted(QList datas) +{ + Q_D(ctkDICOMPatientItemWidget); + foreach (QVariant data, datas) { - return; + ctkDICOMJobDetail td = data.value(); + + if (td.JobUID.isEmpty() || + td.PatientID != d->PatientID) + { + continue; + } + + emit this->jobStarted(data); } +} - d->createStudies(); +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::onJobUserStopped(QList datas) +{ + Q_D(ctkDICOMPatientItemWidget); + foreach (QVariant data, datas) + { + ctkDICOMJobDetail td = data.value(); + + if (td.JobUID.isEmpty() || + td.PatientID != d->PatientID) + { + continue; + } + + emit this->jobUserStopped(data); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::onJobFailed(QList datas) +{ + Q_D(ctkDICOMPatientItemWidget); + foreach (QVariant data, datas) + { + ctkDICOMJobDetail td = data.value(); + + if (td.JobUID.isEmpty() || + td.PatientID != d->PatientID) + { + continue; + } + + emit this->jobFailed(data); + } +} + +//------------------------------------------------------------------------------ +void ctkDICOMPatientItemWidget::onJobFinished(QList datas) +{ + Q_D(ctkDICOMPatientItemWidget); + foreach (QVariant data, datas) + { + ctkDICOMJobDetail td = data.value(); + if (td.JobType == ctkDICOMJobResponseSet::JobType::Inserter) + { + foreach (ctkDICOMStudyItemWidget* studyItemWidget, d->StudyItemWidgetsList) + { + if (!studyItemWidget) + { + continue; + } + + QTableWidget* seriesListTableWidget = studyItemWidget->seriesListTableWidget(); + for (int row = 0; row < seriesListTableWidget->rowCount(); row++) + { + for (int column = 0; column < seriesListTableWidget->columnCount(); column++) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = + qobject_cast(seriesListTableWidget->cellWidget(row, column)); + if (!seriesItemWidget) + { + continue; + } + if (seriesItemWidget->referenceSeriesInserterJobUID() == td.JobUID || + seriesItemWidget->referenceInstanceInserterJobUID() == td.JobUID || + seriesItemWidget->referenceSeriesInserterJobUID() == "StorageListener" || + seriesItemWidget->referenceInstanceInserterJobUID() == "StorageListener") + { + seriesItemWidget->onJobFinished(data); + } + } + } + } + } + + if (td.JobUID.isEmpty() || + td.PatientID != d->PatientID) + { + continue; + } + + emit this->jobFinished(data); + } } //------------------------------------------------------------------------------ diff --git a/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.h b/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.h index b57ad49ca7..b95fd308ba 100644 --- a/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.h +++ b/Libs/DICOM/Widgets/ctkDICOMPatientItemWidget.h @@ -47,9 +47,10 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMPatientItemWidget : public QWidget Q_PROPERTY(QString patientItem READ patientItem WRITE setPatientItem); Q_PROPERTY(QString patientID READ patientID WRITE setPatientID); Q_PROPERTY(QString patientName READ patientName WRITE setPatientName); - Q_PROPERTY(int numberOfStudiesPerPatient READ numberOfStudiesPerPatient WRITE setNumberOfStudiesPerPatient); + Q_PROPERTY(int numberOfOpenedStudiesPerPatient READ numberOfOpenedStudiesPerPatient WRITE setNumberOfOpenedStudiesPerPatient); Q_PROPERTY(ctkDICOMStudyItemWidget::ThumbnailSizeOption thumbnailSize READ thumbnailSize WRITE setThumbnailSize); Q_PROPERTY(QStringList allowedServers READ allowedServers WRITE setAllowedServers); + Q_PROPERTY(OperationStatus operationStatus READ operationStatus WRITE setOperationStatus); public: typedef QWidget Superclass; @@ -74,6 +75,18 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMPatientItemWidget : public QWidget QString patientName() const; ///@} + ///@{ + /// Patient Birth Date + void setPatientBirthDate(const QString& patientBirthDate); + QString patientBirthDate() const; + ///@} + + ///@{ + /// Patient Sex + void setPatientSex(const QString& patientSex); + QString patientSex() const; + ///@} + ///@{ /// Query Filters /// Empty by default @@ -120,8 +133,8 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMPatientItemWidget : public QWidget ///@{ /// Number of non collapsed studies per patient /// 2 by default - void setNumberOfStudiesPerPatient(int numberOfStudiesPerPatient); - int numberOfStudiesPerPatient() const; + void setNumberOfOpenedStudiesPerPatient(int numberOfOpenedStudiesPerPatient); + int numberOfOpenedStudiesPerPatient() const; ///@} ///@{ @@ -177,10 +190,29 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMPatientItemWidget : public QWidget Q_INVOKABLE void updateAllowedServersUIFromDB(); ///@} + enum OperationStatus + { + NoOperation = 0, + InProgress, + Completed, + Failed, + }; + + ///@{ + /// Set the operation status + /// NoOperation by default + void setOperationStatus(const OperationStatus& status); + OperationStatus operationStatus() const; + ///@} + public Q_SLOTS: void generateStudies(bool query = true, bool retrieve = true); void generateSeriesAtToggle(bool toggled = true, const QString& studyItem = ""); - void updateGUIFromScheduler(const QVariant& data); + void updateGUIFromScheduler(QList); + void onJobStarted(QList); + void onJobUserStopped(QList); + void onJobFailed(QList); + void onJobFinished(QList); void onSeriesItemClicked(); void raiseSelectedSeriesJobsPriority(); void onPatientServersCheckableComboBoxChanged(); @@ -188,6 +220,12 @@ public Q_SLOTS: Q_SIGNALS: /// Emitted when the GUI finished to update after a studies query. void updateGUIFinished(); + /// Propagate jobs signals to the tree + void jobStarted(QVariant); + void jobUserStopped(QVariant); + void jobFinished(QVariant); + void jobFailed(QVariant); + void progressJobDetail(QVariant); protected: QScopedPointer d_ptr; diff --git a/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.cpp index a0a16c9843..ec3621de7f 100644 --- a/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.cpp @@ -48,6 +48,14 @@ static ctkLogger logger("org.commontk.DICOM.Widgets.DICOMSeriesItemWidget"); +//---------------------------------------------------------------------------- +static void skipDelete(QObject* obj) +{ + Q_UNUSED(obj); + // this deleter does not delete the object from memory + // useful if the pointer is not owned by the smart pointer +} + //---------------------------------------------------------------------------- class ctkDICOMSeriesItemWidgetPrivate : public Ui_ctkDICOMSeriesItemWidget { @@ -60,11 +68,18 @@ class ctkDICOMSeriesItemWidgetPrivate : public Ui_ctkDICOMSeriesItemWidget ctkDICOMSeriesItemWidgetPrivate(ctkDICOMSeriesItemWidget& obj); ~ctkDICOMSeriesItemWidgetPrivate(); - void init(); + void init(QWidget* top); + void connectToTop(); + void disconnectFromTop(); QString getDICOMCenterFrameFromInstances(QStringList instancesList); void createThumbnail(ctkDICOMJobDetail td); - void drawModalityThumbnail(); - void drawThumbnail(const QString& file, int numberOfFrames); + QImage drawModalityThumbnail(); + void drawThumbnail(const QString& dicomFilePath, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& sopInstanceUID, + const QString& modality, + int numberOfFrames); void drawTextWithShadow(QPainter *painter, const QFont &font, int x, @@ -73,14 +88,14 @@ class ctkDICOMSeriesItemWidgetPrivate : public Ui_ctkDICOMSeriesItemWidget const QString &text); void updateThumbnailProgressBar(); void resetOperationProgressBar(); - void updateUIOnStarted(); - void updateUIOnFailed(); - void updateUIOnFinished(); - void connectScheduler(); - void disconnectScheduler(); + void updateRetrieveUIOnStarted(); + void updateRetrieveUIOnFailed(); + void updateRetrieveUIOnFinished(); QSharedPointer DicomDatabase; QSharedPointer Scheduler; + QSharedPointer StudyWidget; + QMap Connections; QStringList AllowedServers; QString PatientID; @@ -90,17 +105,18 @@ class ctkDICOMSeriesItemWidgetPrivate : public Ui_ctkDICOMSeriesItemWidget QString CentralFrameSOPInstanceUID; QString SeriesNumber; QString Modality; - QString ReferenceInserterJobUID; + QString ReferenceSeriesInserterJobUID; + QString ReferenceInstanceInserterJobUID; bool StopJobs; bool RaiseJobsPriority; bool IsCloud; bool RetrieveFailed; + bool RetrieveSeries; bool IsLoaded; bool IsVisible; int ThumbnailSizePixel; int NumberOfDownloads; QImage ThumbnailImage; - bool isThumbnailDocument; bool QueryOn; bool RetrieveOn; @@ -120,15 +136,16 @@ ctkDICOMSeriesItemWidgetPrivate::ctkDICOMSeriesItemWidgetPrivate(ctkDICOMSeriesI this->CentralFrameSOPInstanceUID = ""; this->SeriesNumber = ""; this->Modality = ""; - this->ReferenceInserterJobUID = ""; + this->ReferenceSeriesInserterJobUID = ""; + this->ReferenceInstanceInserterJobUID = ""; this->IsCloud = false; this->RetrieveFailed = false; + this->RetrieveSeries = false; this->IsLoaded = false; this->IsVisible = false; this->StopJobs = false; this->RaiseJobsPriority = false; - this->isThumbnailDocument = false; this->ThumbnailSizePixel = 200; this->NumberOfDownloads = 0; @@ -144,17 +161,74 @@ ctkDICOMSeriesItemWidgetPrivate::ctkDICOMSeriesItemWidgetPrivate(ctkDICOMSeriesI //---------------------------------------------------------------------------- ctkDICOMSeriesItemWidgetPrivate::~ctkDICOMSeriesItemWidgetPrivate() { + this->disconnectFromTop(); } //---------------------------------------------------------------------------- -void ctkDICOMSeriesItemWidgetPrivate::init() +void ctkDICOMSeriesItemWidgetPrivate::init(QWidget* top) { Q_Q(ctkDICOMSeriesItemWidget); this->setupUi(q); + this->StudyWidget = QSharedPointer(top, skipDelete); + this->connectToTop(); + this->SeriesThumbnail->setTransformationMode(Qt::TransformationMode::SmoothTransformation); this->SeriesThumbnail->textPushButton()->setElideMode(Qt::ElideRight); this->SeriesThumbnail->setSelectedColor(QColor::Invalid); + + QObject::connect(this->SeriesThumbnail, SIGNAL(statusPushButtonClicked(bool)), + q, SLOT(onOperationStatusButtonClicked(bool))); +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::connectToTop() +{ + Q_Q(ctkDICOMSeriesItemWidget); + if (!this->StudyWidget) + { + return; + } + + QMetaObject::Connection progressConnection = + QObject::connect(this->StudyWidget.data(), SIGNAL(progressJobDetail(QVariant)), + q, SLOT(updateGUIFromScheduler(QVariant))); + QMetaObject::Connection progressBarConnection = + QObject::connect(this->StudyWidget.data(), SIGNAL(progressJobDetail(QVariant)), + q, SLOT(updateSeriesProgressBar(QVariant))); + QMetaObject::Connection startedConnection = + QObject::connect(this->StudyWidget.data(), SIGNAL(jobStarted(QVariant)), + q, SLOT(onJobStarted(QVariant))); + QMetaObject::Connection userStoppedConnection = + QObject::connect(this->StudyWidget.data(), SIGNAL(jobUserStopped(QVariant)), + q, SLOT(onJobUserStopped(QVariant))); + QMetaObject::Connection failedConnection = + QObject::connect(this->StudyWidget.data(), SIGNAL(jobFailed(QVariant)), + q, SLOT(onJobFailed(QVariant))); + QMetaObject::Connection finishedConnection = + QObject::connect(this->StudyWidget.data(), SIGNAL(jobFinished(QVariant)), + q, SLOT(onJobFinished(QVariant))); + + this->Connections = + { + {"progress", progressConnection}, + {"progressBar", progressBarConnection}, + {"started", startedConnection}, + {"userStopped", userStoppedConnection}, + {"finished", finishedConnection}, + {"failed", failedConnection} + }; +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidgetPrivate::disconnectFromTop() +{ + QObject::disconnect(this->Connections.value("progress")); + QObject::disconnect(this->Connections.value("progressBar")); + QObject::disconnect(this->Connections.value("started")); + QObject::disconnect(this->Connections.value("userStopped")); + QObject::disconnect(this->Connections.value("finished")); + QObject::disconnect(this->Connections.value("failed")); } //---------------------------------------------------------------------------- @@ -305,69 +379,58 @@ void ctkDICOMSeriesItemWidgetPrivate::createThumbnail(ctkDICOMJobDetail td) if (file.isEmpty() && (this->IsCloud || this->RetrieveFailed) && (jobType == ctkDICOMJobResponseSet::JobType::None || - jobType == ctkDICOMJobResponseSet::JobType::QueryInstances)) + jobType == ctkDICOMJobResponseSet::JobType::QueryInstances) && + this->RetrieveOn) { - if (this->RetrieveOn) - { - this->Scheduler->retrieveSOPInstance(this->PatientID, - this->StudyInstanceUID, - this->SeriesInstanceUID, - this->CentralFrameSOPInstanceUID, - this->RaiseJobsPriority ? QThread::HighestPriority : QThread::HighPriority, - this->AllowedServers); - } + this->Scheduler->retrieveSOPInstance(this->PatientID, + this->StudyInstanceUID, + this->SeriesInstanceUID, + this->CentralFrameSOPInstanceUID, + this->RaiseJobsPriority ? QThread::HighPriority : QThread::HighPriority, + this->AllowedServers); + this->RetrieveSeries = true; return; } // Get series if (numberOfFrames > 1 && - (this->IsCloud || this->RetrieveFailed) && - ((jobSopInstanceUID == this->CentralFrameSOPInstanceUID && - (jobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance || - jobType == ctkDICOMJobResponseSet::JobType::StoreSOPInstance)) || - (jobType == ctkDICOMJobResponseSet::JobType::None || - jobType == ctkDICOMJobResponseSet::JobType::QueryInstances))) - { - QList> jobs = - this->Scheduler->getJobsByDICOMUIDs({}, - {}, - {this->SeriesInstanceUID}); - if (jobs.count() == 0 && this->RetrieveOn) + this->IsCloud && + this->RetrieveSeries && + this->RetrieveOn) { - this->Scheduler->retrieveSeries(this->PatientID, - this->StudyInstanceUID, - this->SeriesInstanceUID, - this->RaiseJobsPriority ? QThread::HighestPriority : QThread::LowPriority, - this->AllowedServers); - } + this->RetrieveSeries = false; + this->Scheduler->retrieveSeries(this->PatientID, + this->StudyInstanceUID, + this->SeriesInstanceUID, + this->RaiseJobsPriority ? QThread::HighPriority : QThread::LowPriority, + this->AllowedServers); } } - file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); - if (file.isEmpty()) - { - return; - } - if (jobSopInstanceUID.isEmpty() || ((jobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance || jobType == ctkDICOMJobResponseSet::JobType::StoreSOPInstance) && jobSopInstanceUID == this->CentralFrameSOPInstanceUID) || renderThumbnail) { - this->drawThumbnail(file, numberOfFrames); + this->drawThumbnail(this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID), + this->StudyInstanceUID, + this->SeriesInstanceUID, + this->CentralFrameSOPInstanceUID, + this->Modality, + numberOfFrames); } } //---------------------------------------------------------------------------- -void ctkDICOMSeriesItemWidgetPrivate::drawModalityThumbnail() +QImage ctkDICOMSeriesItemWidgetPrivate::drawModalityThumbnail() { Q_Q(ctkDICOMSeriesItemWidget); if (!this->DicomDatabase) { - logger.error("drawThumbnail failed, no DICOM Database has been set. \n"); - return; + logger.error("drawModalityThumbnail failed, no DICOM Database has been set. \n"); + return QImage(); } qreal scalingFactor = q->devicePixelRatioF(); @@ -377,21 +440,17 @@ void ctkDICOMSeriesItemWidgetPrivate::drawModalityThumbnail() font.setBold(true); font.setPixelSize(textSize); - QPixmap resultPixmap(scaledThumbnailSizePixel, scaledThumbnailSizePixel); - resultPixmap.fill(Qt::transparent); ctkDICOMThumbnailGenerator thumbnailGenerator; thumbnailGenerator.setWidth(scaledThumbnailSizePixel); thumbnailGenerator.setHeight(scaledThumbnailSizePixel); - QImage thumbnailImage; - QPainter painter; - QColor backgroundColor = this->SeriesThumbnail->palette().color(QPalette::Normal, QPalette::Window); thumbnailGenerator.generateBlankThumbnail(thumbnailImage, backgroundColor); - resultPixmap = QPixmap::fromImage(thumbnailImage); + QPixmap resultPixmap = QPixmap::fromImage(thumbnailImage); + QPainter painter; if (painter.begin(&resultPixmap)) { - painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform, true); QRect rect = resultPixmap.rect(); int x = int(rect.width() * 0.5); int y = int(rect.height() * 0.5); @@ -399,11 +458,18 @@ void ctkDICOMSeriesItemWidgetPrivate::drawModalityThumbnail() painter.end(); } + resultPixmap.setDevicePixelRatio(scalingFactor); this->SeriesThumbnail->setPixmap(resultPixmap); + return resultPixmap.toImage(); } //---------------------------------------------------------------------------- -void ctkDICOMSeriesItemWidgetPrivate::drawThumbnail(const QString& file, int numberOfFrames) +void ctkDICOMSeriesItemWidgetPrivate::drawThumbnail(const QString& dicomFilePath, + const QString& studyInstanceUID, + const QString& seriesInstanceUID, + const QString& sopInstanceUID, + const QString& modality, + int numberOfFrames) { Q_Q(ctkDICOMSeriesItemWidget); @@ -413,110 +479,130 @@ void ctkDICOMSeriesItemWidgetPrivate::drawThumbnail(const QString& file, int num return; } + if (dicomFilePath.isEmpty()) + { + logger.debug("drawThumbnail failed, dicomFilePath is empty. \n"); + return; + } + + if (studyInstanceUID.isEmpty()) + { + logger.debug("drawThumbnail failed, studyInstanceUID is empty. \n"); + return; + } + + if (seriesInstanceUID.isEmpty()) + { + logger.debug("drawThumbnail failed, seriesInstanceUID is empty. \n"); + return; + } + + if (sopInstanceUID.isEmpty()) + { + logger.debug("drawThumbnail failed, sopInstanceUID is empty. \n"); + return; + } + + if (modality.isEmpty()) + { + logger.debug("drawThumbnail failed, modality is empty. \n"); + return; + } + qreal scalingFactor = q->devicePixelRatioF(); int scaledThumbnailSizePixel = this->ThumbnailSizePixel * scalingFactor; - int margin = floor(scaledThumbnailSizePixel / 50.); - int iconSize = floor(scaledThumbnailSizePixel / 6.); - int textSize = floor(scaledThumbnailSizePixel / 12.); + int margin = floor(this->ThumbnailSizePixel / 50.); + int iconSize = floor(this->ThumbnailSizePixel / 6.); + int textSize = floor(this->ThumbnailSizePixel / 12.); QFont font = this->SeriesThumbnail->font(); font.setBold(true); font.setPixelSize(textSize); - QPixmap resultPixmap(scaledThumbnailSizePixel, scaledThumbnailSizePixel); - - ctkDICOMThumbnailGenerator thumbnailGenerator; - thumbnailGenerator.setWidth(scaledThumbnailSizePixel); - thumbnailGenerator.setHeight(scaledThumbnailSizePixel); - bool thumbnailGenerated = true; - bool emptyThumbnailGenerated = false; - QPainter painter; - if (this->ThumbnailImage.isNull()) { - if (!thumbnailGenerator.generateThumbnail(file, this->ThumbnailImage)) + ctkDICOMThumbnailGenerator thumbnailGenerator; + thumbnailGenerator.setWidth(scaledThumbnailSizePixel); + thumbnailGenerator.setHeight(scaledThumbnailSizePixel); + + QString thumbnailPath = this->DicomDatabase->thumbnailPathForInstance(studyInstanceUID, seriesInstanceUID, sopInstanceUID); + if (thumbnailPath.isEmpty()) { - thumbnailGenerated = false; - emptyThumbnailGenerated = true; - this->isThumbnailDocument = true; QColor backgroundColor = this->SeriesThumbnail->palette().color(QPalette::Normal, QPalette::Window); - thumbnailGenerator.generateBlankThumbnail(this->ThumbnailImage, backgroundColor); - resultPixmap = QPixmap::fromImage(this->ThumbnailImage); - if (painter.begin(&resultPixmap)) - { - painter.setRenderHint(QPainter::Antialiasing); - QSvgRenderer renderer(QString(":Icons/text_document.svg")); - renderer.render(&painter); - painter.end(); - } + QVector colorVector = {backgroundColor.red(), backgroundColor.green(), backgroundColor.blue()}; + this->DicomDatabase->storeThumbnailFile(dicomFilePath, studyInstanceUID, seriesInstanceUID, + sopInstanceUID, modality, colorVector); + thumbnailPath = this->DicomDatabase->thumbnailPathForInstance(studyInstanceUID, seriesInstanceUID, sopInstanceUID); } - } - if (thumbnailGenerated) - { - QColor firstPixelColor = this->ThumbnailImage.pixelColor(0, 0); - resultPixmap.fill(firstPixelColor); + if (!this->ThumbnailImage.load(thumbnailPath)) + { + logger.error("drawThumbnail failed, could not load png file. \n"); + return; + } } - if (thumbnailGenerated && !this->isThumbnailDocument) + QPixmap resultPixmap(this->ThumbnailSizePixel, this->ThumbnailSizePixel); + QColor firstPixelColor = this->ThumbnailImage.pixelColor(0, 0); + resultPixmap.fill(firstPixelColor); + + QPainter painter; + if (painter.begin(&resultPixmap)) { - if (painter.begin(&resultPixmap)) + painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform, true); + painter.setFont(font); + + QRectF rect = resultPixmap.rect(); + int x = int((rect.width() * 0.5) - (this->ThumbnailImage.rect().width() * 0.5)); + int y = int((rect.height() * 0.5) - (this->ThumbnailImage.rect().height() * 0.5)); + + QPixmap thumbnailPixmap = QPixmap::fromImage(this->ThumbnailImage); + painter.drawPixmap(x, y, thumbnailPixmap); + + QString topLeftString = ctkDICOMSeriesItemWidget::tr("Series: %1\n%2").arg(this->SeriesNumber).arg(this->Modality); + if (modality == "SEG") { - painter.setRenderHint(QPainter::Antialiasing, true); - painter.setRenderHint(QPainter::SmoothPixmapTransform, true); - painter.setFont(font); - - QRectF rect = resultPixmap.rect(); - int x = int((rect.width() * 0.5) - (this->ThumbnailImage.rect().width() * 0.5)); - int y = int((rect.height() * 0.5) - (this->ThumbnailImage.rect().height() * 0.5)); - - QPixmap thumbnailPixmap = QPixmap::fromImage(this->ThumbnailImage); - painter.drawPixmap(x, y, thumbnailPixmap); - - QString topLeftString = ctkDICOMSeriesItemWidget::tr("Series: %1\n%2").arg(this->SeriesNumber).arg(this->Modality); - this->drawTextWithShadow(&painter, font, margin, margin, Qt::AlignTop | Qt::AlignLeft, topLeftString); - QString rows = this->DicomDatabase->instanceValue(this->CentralFrameSOPInstanceUID, "0028,0010"); - QString columns = this->DicomDatabase->instanceValue(this->CentralFrameSOPInstanceUID, "0028,0011"); - QString bottomLeftString = rows + "x" + columns + "x" + QString::number(numberOfFrames); - this->drawTextWithShadow(&painter, font, margin, rect.height() - margin * 2, - Qt::AlignBottom | Qt::AlignLeft, bottomLeftString); - QSvgRenderer renderer; - - if (this->RetrieveFailed) - { - renderer.load(QString(":Icons/error_red.svg")); - } - else if (this->IsCloud) - { - if (this->NumberOfDownloads > 0) - { - renderer.load(QString(":Icons/downloading.svg")); - } - else - { - renderer.load(QString(":Icons/cloud.svg")); - } - } - else if (this->IsVisible) + topLeftString = ctkDICOMSeriesItemWidget::tr("Series: %1").arg(this->SeriesNumber); + } + this->drawTextWithShadow(&painter, font, margin, margin, Qt::AlignTop | Qt::AlignLeft, topLeftString); + QString rows = this->DicomDatabase->instanceValue(this->CentralFrameSOPInstanceUID, "0028,0010"); + QString columns = this->DicomDatabase->instanceValue(this->CentralFrameSOPInstanceUID, "0028,0011"); + QString bottomLeftString = rows + "x" + columns + "x" + QString::number(numberOfFrames); + this->drawTextWithShadow(&painter, font, margin, rect.height() - margin * 2, + Qt::AlignBottom | Qt::AlignLeft, bottomLeftString); + QSvgRenderer renderer; + + if (this->RetrieveFailed) + { + renderer.load(QString(":Icons/error_red.svg")); + } + else if (this->IsCloud) + { + if (this->NumberOfDownloads > 0) { - renderer.load(QString(":Icons/visible.svg")); + renderer.load(QString(":Icons/downloading.svg")); } - else if (this->IsLoaded) + else { - renderer.load(QString(":Icons/loaded.svg")); + renderer.load(QString(":Icons/cloud.svg")); } - - QPointF topRight = rect.topRight(); - QRectF bounds(topRight.x() - iconSize - margin, topRight.y() + margin, iconSize, iconSize); - renderer.render(&painter, bounds); - painter.end(); } - } + else if (this->IsVisible) + { + renderer.load(QString(":Icons/visible.svg")); + } + else if (this->IsLoaded) + { + renderer.load(QString(":Icons/loaded.svg")); + } - if ((thumbnailGenerated && !this->isThumbnailDocument) || emptyThumbnailGenerated) - { - resultPixmap.setDevicePixelRatio(scalingFactor); - this->SeriesThumbnail->setPixmap(resultPixmap); + QPointF topRight = rect.topRight(); + QRectF bounds(topRight.x() - iconSize - margin, topRight.y() + margin, iconSize, iconSize); + renderer.render(&painter, bounds); + painter.end(); } + + resultPixmap.setDevicePixelRatio(scalingFactor); + this->SeriesThumbnail->setPixmap(resultPixmap); } //---------------------------------------------------------------------------- @@ -527,12 +613,14 @@ void ctkDICOMSeriesItemWidgetPrivate::drawTextWithShadow(QPainter *painter, Qt::Alignment alignment, const QString &text) { + Q_Q(ctkDICOMSeriesItemWidget); + QColor textColor(60, 164, 255, 225); QGraphicsDropShadowEffect* dropShadowEffect = new QGraphicsDropShadowEffect; dropShadowEffect->setXOffset(1); dropShadowEffect->setYOffset(1); dropShadowEffect->setBlurRadius(1); - dropShadowEffect->setColor(Qt::gray); + dropShadowEffect->setColor(Qt::darkGray); QLabel textLabel; textLabel.setObjectName("ctkDrawTextWithShadowQLabel"); QPalette palette = textLabel.palette(); @@ -543,7 +631,7 @@ void ctkDICOMSeriesItemWidgetPrivate::drawTextWithShadow(QPainter *painter, textLabel.setText(text); QPixmap textPixMap = textLabel.grab(); QRect rect = textPixMap.rect(); - qreal scalingFactor = textLabel.devicePixelRatioF(); + qreal scalingFactor = q->devicePixelRatioF(); int textWidth = rect.width() / scalingFactor; int textHeight = rect.height() / scalingFactor; @@ -605,10 +693,12 @@ void ctkDICOMSeriesItemWidgetPrivate::updateThumbnailProgressBar() this->SeriesThumbnail->operationProgressBar()->show(); // change icons QString file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); - if (!file.isEmpty()) - { - this->drawThumbnail(file, numberOfFrames); - } + this->drawThumbnail(file, + this->StudyInstanceUID, + this->SeriesInstanceUID, + this->CentralFrameSOPInstanceUID, + this->Modality, + numberOfFrames); } } @@ -620,18 +710,18 @@ void ctkDICOMSeriesItemWidgetPrivate::resetOperationProgressBar() } //---------------------------------------------------------------------------- -void ctkDICOMSeriesItemWidgetPrivate::updateUIOnStarted() +void ctkDICOMSeriesItemWidgetPrivate::updateRetrieveUIOnStarted() { - this->ReferenceInserterJobUID = ""; + this->ReferenceSeriesInserterJobUID = ""; this->RetrieveFailed = false; this->resetOperationProgressBar(); } //---------------------------------------------------------------------------- -void ctkDICOMSeriesItemWidgetPrivate::updateUIOnFailed() +void ctkDICOMSeriesItemWidgetPrivate::updateRetrieveUIOnFailed() { this->RetrieveFailed = true; - this->ReferenceInserterJobUID = ""; + this->ReferenceSeriesInserterJobUID = ""; QString file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); if (file.isEmpty()) @@ -642,11 +732,16 @@ void ctkDICOMSeriesItemWidgetPrivate::updateUIOnFailed() QStringList instancesList = this->DicomDatabase->instancesForSeries(this->SeriesInstanceUID); int numberOfFrames = instancesList.count(); - this->drawThumbnail(file, numberOfFrames); + this->drawThumbnail(file, + this->StudyInstanceUID, + this->SeriesInstanceUID, + this->CentralFrameSOPInstanceUID, + this->Modality, + numberOfFrames); } //---------------------------------------------------------------------------- -void ctkDICOMSeriesItemWidgetPrivate::updateUIOnFinished() +void ctkDICOMSeriesItemWidgetPrivate::updateRetrieveUIOnFinished() { QStringList instancesList = this->DicomDatabase->instancesForSeries(this->SeriesInstanceUID); int numberOfFrames = instancesList.count(); @@ -667,66 +762,35 @@ void ctkDICOMSeriesItemWidgetPrivate::updateUIOnFinished() this->SeriesThumbnail->operationProgressBar()->hide(); } - QString file = this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID); - if (file.isEmpty()) - { - return; - } - - this->drawThumbnail(file, numberOfFrames); -} - -//---------------------------------------------------------------------------- -void ctkDICOMSeriesItemWidgetPrivate::connectScheduler() -{ - Q_Q(ctkDICOMSeriesItemWidget); - QObject::connect(this->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - q, SLOT(updateGUIFromScheduler(QVariant))); - QObject::connect(this->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - q, SLOT(updateSeriesProgressBar(QVariant))); - QObject::connect(this->Scheduler.data(), SIGNAL(jobStarted(QVariant)), - q, SLOT(onJobStarted(QVariant))); - QObject::connect(this->Scheduler.data(), SIGNAL(jobCanceled(QVariant)), - q, SLOT(onJobCanceled(QVariant))); - QObject::connect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), - q, SLOT(onJobFailed(QVariant))); - QObject::connect(this->Scheduler.data(), SIGNAL(jobFinished(QVariant)), - q, SLOT(onJobFinished(QVariant))); -} - -//---------------------------------------------------------------------------- -void ctkDICOMSeriesItemWidgetPrivate::disconnectScheduler() -{ - Q_Q(ctkDICOMSeriesItemWidget); - QObject::disconnect(this->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - q, SLOT(updateGUIFromScheduler(QVariant))); - QObject::disconnect(this->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - q, SLOT(updateSeriesProgressBar(QVariant))); - QObject::disconnect(this->Scheduler.data(), SIGNAL(jobStarted(QVariant)), - q, SLOT(onJobStarted(QVariant))); - QObject::disconnect(this->Scheduler.data(), SIGNAL(jobCanceled(QVariant)), - q, SLOT(onJobCanceled(QVariant))); - QObject::disconnect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), - q, SLOT(onJobFailed(QVariant))); - QObject::disconnect(this->Scheduler.data(), SIGNAL(jobFinished(QVariant)), - q, SLOT(onJobFinished(QVariant))); + this->drawThumbnail(this->DicomDatabase->fileForInstance(this->CentralFrameSOPInstanceUID), + this->StudyInstanceUID, + this->SeriesInstanceUID, + this->CentralFrameSOPInstanceUID, + this->Modality, + numberOfFrames); + this->SeriesThumbnail->setOperationStatus(ctkThumbnailLabel::Completed); + this->SeriesThumbnail->setStatusIcon(QIcon(":/Icons/accept.svg")); } //---------------------------------------------------------------------------- // ctkDICOMSeriesItemWidget methods //---------------------------------------------------------------------------- -ctkDICOMSeriesItemWidget::ctkDICOMSeriesItemWidget(QWidget* parentWidget) - : Superclass(parentWidget) +ctkDICOMSeriesItemWidget::ctkDICOMSeriesItemWidget(QWidget* top, QWidget* parent) + : Superclass(parent) , d_ptr(new ctkDICOMSeriesItemWidgetPrivate(*this)) { Q_D(ctkDICOMSeriesItemWidget); - d->init(); + d->init(top); } //---------------------------------------------------------------------------- ctkDICOMSeriesItemWidget::~ctkDICOMSeriesItemWidget() { + Q_D(ctkDICOMSeriesItemWidget); + + QObject::disconnect(d->SeriesThumbnail, SIGNAL(statusPushButtonClicked(bool)), + this, SLOT(onOperationStatusButtonClicked(bool))); } //------------------------------------------------------------------------------ @@ -753,6 +817,8 @@ CTK_GET_CPP(ctkDICOMSeriesItemWidget, bool, isLoaded, IsLoaded); CTK_GET_CPP(ctkDICOMSeriesItemWidget, bool, isVisible, IsVisible); CTK_SET_CPP(ctkDICOMSeriesItemWidget, bool, setRetrieveFailed, RetrieveFailed); CTK_GET_CPP(ctkDICOMSeriesItemWidget, bool, retrieveFailed, RetrieveFailed); +CTK_GET_CPP(ctkDICOMSeriesItemWidget, QString, referenceSeriesInserterJobUID, ReferenceSeriesInserterJobUID); +CTK_GET_CPP(ctkDICOMSeriesItemWidget, QString, referenceInstanceInserterJobUID, ReferenceInstanceInserterJobUID); CTK_SET_CPP(ctkDICOMSeriesItemWidget, int, setThumbnailSizePixel, ThumbnailSizePixel); CTK_GET_CPP(ctkDICOMSeriesItemWidget, int, thumbnailSizePixel, ThumbnailSizePixel); @@ -776,21 +842,14 @@ void ctkDICOMSeriesItemWidget::forceRetrieve() { Q_D(ctkDICOMSeriesItemWidget); - d->IsCloud = false; + d->IsCloud = true; d->RetrieveFailed = false; d->StopJobs = false; d->DicomDatabase->removeSeries(d->SeriesInstanceUID, false, false); + d->ThumbnailImage = QImage(); this->generateInstances(true); } -//---------------------------------------------------------------------------- -static void skipDelete(QObject* obj) -{ - Q_UNUSED(obj); - // this deleter does not delete the object from memory - // useful if the pointer is not owned by the smart pointer -} - //---------------------------------------------------------------------------- ctkDICOMScheduler* ctkDICOMSeriesItemWidget::scheduler() const { @@ -809,34 +868,14 @@ QSharedPointer ctkDICOMSeriesItemWidget::schedulerShared() co void ctkDICOMSeriesItemWidget::setScheduler(ctkDICOMScheduler& scheduler) { Q_D(ctkDICOMSeriesItemWidget); - if (d->Scheduler) - { - d->disconnectScheduler(); - } - d->Scheduler = QSharedPointer(&scheduler, skipDelete); - - if (d->Scheduler) - { - d->connectScheduler(); - } } //---------------------------------------------------------------------------- void ctkDICOMSeriesItemWidget::setScheduler(QSharedPointer scheduler) { Q_D(ctkDICOMSeriesItemWidget); - if (d->Scheduler) - { - d->disconnectScheduler(); - } - d->Scheduler = scheduler; - - if (d->Scheduler) - { - d->connectScheduler(); - } } //---------------------------------------------------------------------------- @@ -892,7 +931,7 @@ void ctkDICOMSeriesItemWidget::generateInstances(bool query, bool retrieve) d->Scheduler->queryInstances(d->PatientID, d->StudyInstanceUID, d->SeriesInstanceUID, - d->RaiseJobsPriority ? QThread::HighestPriority : QThread::NormalPriority, + d->RaiseJobsPriority ? QThread::HighPriority : QThread::NormalPriority, d->AllowedServers); } } @@ -903,11 +942,14 @@ void ctkDICOMSeriesItemWidget::updateGUIFromScheduler(const QVariant& data) Q_D(ctkDICOMSeriesItemWidget); ctkDICOMJobDetail td = data.value(); - if (td.JobUID.isEmpty() || - (td.JobType != ctkDICOMJobResponseSet::JobType::QueryInstances && - td.JobType != ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance&& - td.JobType != ctkDICOMJobResponseSet::JobType::StoreSOPInstance) || - td.StudyInstanceUID != d->StudyInstanceUID || + if (td.JobUID.isEmpty()) + { + return; + } + + if ((td.JobType != ctkDICOMJobResponseSet::JobType::QueryInstances && + td.JobType != ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance && + td.JobType != ctkDICOMJobResponseSet::JobType::StoreSOPInstance ) || td.SeriesInstanceUID != d->SeriesInstanceUID) { return; @@ -922,22 +964,19 @@ void ctkDICOMSeriesItemWidget::updateSeriesProgressBar(const QVariant& data) Q_D(ctkDICOMSeriesItemWidget); ctkDICOMJobDetail td = data.value(); - if (td.JobUID.isEmpty() || - (td.JobType != ctkDICOMJobResponseSet::JobType::RetrieveSeries && + if (td.JobUID.isEmpty()) + { + return; + } + + if ((td.JobType != ctkDICOMJobResponseSet::JobType::RetrieveSeries && td.JobType != ctkDICOMJobResponseSet::JobType::StoreSOPInstance) || - td.StudyInstanceUID != d->StudyInstanceUID || td.SeriesInstanceUID != d->SeriesInstanceUID) { return; } d->updateThumbnailProgressBar(); - - if (td.JobType == ctkDICOMJobResponseSet::JobType::StoreSOPInstance && - d->ReferenceInserterJobUID == "StorageListener") - { - d->updateUIOnFinished(); - } } //---------------------------------------------------------------------------- @@ -946,33 +985,51 @@ void ctkDICOMSeriesItemWidget::onJobStarted(const QVariant &data) Q_D(ctkDICOMSeriesItemWidget); ctkDICOMJobDetail td = data.value(); - - if (td.JobUID.isEmpty() || - td.JobType != ctkDICOMJobResponseSet::JobType::RetrieveSeries || - td.StudyInstanceUID != d->StudyInstanceUID || - td.SeriesInstanceUID != d->SeriesInstanceUID) + if (!td.JobUID.isEmpty() && + (td.JobType == ctkDICOMJobResponseSet::JobType::QueryInstances || + td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance || + td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries) && + td.SeriesInstanceUID == d->SeriesInstanceUID) { - return; - } + d->SeriesThumbnail->setOperationStatus(ctkThumbnailLabel::InProgress); + d->SeriesThumbnail->setStatusIcon(QIcon(":/Icons/pending.svg")); - d->updateUIOnStarted(); + if (td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance) + { + d->ReferenceInstanceInserterJobUID = ""; + } + else if (td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries) + { + d->updateRetrieveUIOnStarted(); + } + } } //---------------------------------------------------------------------------- -void ctkDICOMSeriesItemWidget::onJobCanceled(const QVariant &data) +void ctkDICOMSeriesItemWidget::onJobUserStopped(const QVariant &data) { Q_D(ctkDICOMSeriesItemWidget); ctkDICOMJobDetail td = data.value(); - if (td.JobUID.isEmpty() || - td.JobType != ctkDICOMJobResponseSet::JobType::RetrieveSeries || - td.StudyInstanceUID != d->StudyInstanceUID || - td.SeriesInstanceUID != d->SeriesInstanceUID) + + if (!td.JobUID.isEmpty() && + (td.JobType == ctkDICOMJobResponseSet::JobType::QueryInstances || + td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance || + td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries) && + td.SeriesInstanceUID == d->SeriesInstanceUID) { - return; - } + d->SeriesThumbnail->setOperationStatus(ctkThumbnailLabel::Failed); + d->SeriesThumbnail->setStatusIcon(QIcon(":/Icons/error_red.svg")); - d->updateUIOnFailed(); + if (td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance) + { + d->ReferenceInstanceInserterJobUID = ""; + } + else if (td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries) + { + d->updateRetrieveUIOnFailed(); + } + } } //---------------------------------------------------------------------------- @@ -981,15 +1038,25 @@ void ctkDICOMSeriesItemWidget::onJobFailed(const QVariant &data) Q_D(ctkDICOMSeriesItemWidget); ctkDICOMJobDetail td = data.value(); - if (td.JobUID.isEmpty() || - td.JobType != ctkDICOMJobResponseSet::JobType::RetrieveSeries || - td.StudyInstanceUID != d->StudyInstanceUID || - td.SeriesInstanceUID != d->SeriesInstanceUID) + + if (!td.JobUID.isEmpty() && + (td.JobType == ctkDICOMJobResponseSet::JobType::QueryInstances || + td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance || + td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries) && + td.SeriesInstanceUID == d->SeriesInstanceUID) { - return; - } + d->SeriesThumbnail->setOperationStatus(ctkThumbnailLabel::Failed); + d->SeriesThumbnail->setStatusIcon(QIcon(":/Icons/error_red.svg")); - d->updateUIOnFailed(); + if (td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance) + { + d->ReferenceInstanceInserterJobUID = ""; + } + else if (td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries) + { + d->updateRetrieveUIOnFailed(); + } + } } //---------------------------------------------------------------------------- @@ -998,31 +1065,84 @@ void ctkDICOMSeriesItemWidget::onJobFinished(const QVariant &data) Q_D(ctkDICOMSeriesItemWidget); ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty()) + { + return; + } - if (!td.JobUID.isEmpty() && - td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries && - td.StudyInstanceUID == d->StudyInstanceUID && - td.SeriesInstanceUID == d->SeriesInstanceUID) + if (td.SeriesInstanceUID == d->SeriesInstanceUID && + (td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries || + td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance)) { - if (td.ReferenceInserterJobUID.isEmpty()) + if (td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSOPInstance) { - d->ReferenceInserterJobUID = "StorageListener"; + if (td.ReferenceInserterJobUID.isEmpty()) + { + d->ReferenceInstanceInserterJobUID = "StorageListener"; + } + else + { + d->ReferenceInstanceInserterJobUID = td.ReferenceInserterJobUID; + } } - else + else if (td.JobType == ctkDICOMJobResponseSet::JobType::RetrieveSeries) { - d->ReferenceInserterJobUID = td.ReferenceInserterJobUID; + if (td.ReferenceInserterJobUID.isEmpty()) + { + d->ReferenceSeriesInserterJobUID = "StorageListener"; + } + else + { + d->ReferenceSeriesInserterJobUID = td.ReferenceInserterJobUID; + } } + return; } - if (td.JobUID.isEmpty() || - d->ReferenceInserterJobUID.isEmpty() || - td.JobType != ctkDICOMJobResponseSet::JobType::Inserter || - td.JobUID != d->ReferenceInserterJobUID) + if (td.JobType != ctkDICOMJobResponseSet::JobType::Inserter) { return; } - d->ReferenceInserterJobUID = ""; - d->updateUIOnFinished(); + QStringList instancesList = d->DicomDatabase->instancesForSeries(d->SeriesInstanceUID); + int numberOfFrames = instancesList.count(); + if ((d->ReferenceInstanceInserterJobUID == td.JobUID || + d->ReferenceInstanceInserterJobUID == "StorageListener") && + numberOfFrames == 1) + { + d->SeriesThumbnail->setOperationStatus(ctkThumbnailLabel::Completed); + d->SeriesThumbnail->setStatusIcon(QIcon(":/Icons/accept.svg")); + d->updateRetrieveUIOnFinished(); + } + else if (d->ReferenceSeriesInserterJobUID == td.JobUID || + d->ReferenceSeriesInserterJobUID == "StorageListener") + { + d->ReferenceSeriesInserterJobUID = ""; + d->updateRetrieveUIOnFinished(); + } +} + +//---------------------------------------------------------------------------- +void ctkDICOMSeriesItemWidget::onOperationStatusButtonClicked(bool) +{ + Q_D(ctkDICOMSeriesItemWidget); + + ctkThumbnailLabel::OperationStatus status = d->SeriesThumbnail->operationStatus(); + if (status == ctkThumbnailLabel::InProgress) + { + d->Scheduler->stopJobsByDICOMUIDs(QStringList(), + QStringList(), + QStringList(d->SeriesInstanceUID)); + } + else if (status > ctkThumbnailLabel::InProgress) + { + ctkDICOMJobDetail queryJobDetail; + queryJobDetail.JobClass = "ctkDICOMQueryJob"; + queryJobDetail.DICOMLevel = ctkDICOMJob::DICOMLevels::Instances; + queryJobDetail.PatientID = d->PatientID; + queryJobDetail.StudyInstanceUID = d->StudyInstanceUID; + queryJobDetail.SeriesInstanceUID = d->SeriesInstanceUID; + d->Scheduler->runJob(queryJobDetail, d->AllowedServers); + } } diff --git a/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.h b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.h index c1f9d0ebee..5931456003 100644 --- a/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.h +++ b/Libs/DICOM/Widgets/ctkDICOMSeriesItemWidget.h @@ -34,7 +34,9 @@ class ctkDICOMScheduler; // ctkDICOMWidgets includes #include "ctkDICOMWidgetsExport.h" + class ctkDICOMSeriesItemWidgetPrivate; +class ctkDICOMStudyItemWidget; /// \ingroup DICOM_Widgets class CTK_DICOM_WIDGETS_EXPORT ctkDICOMSeriesItemWidget : public QWidget @@ -49,6 +51,8 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMSeriesItemWidget : public QWidget Q_PROPERTY(QString seriesDescription READ seriesDescription WRITE setSeriesDescription); Q_PROPERTY(bool isCloud READ isCloud); Q_PROPERTY(bool retrieveFailed READ retrieveFailed WRITE setRetrieveFailed); + Q_PROPERTY(QString referenceSeriesInserterJobUID READ referenceSeriesInserterJobUID); + Q_PROPERTY(QString referenceInstanceInserterJobUID READ referenceInstanceInserterJobUID); Q_PROPERTY(int thumbnailSizePixel READ thumbnailSizePixel WRITE setThumbnailSizePixel); Q_PROPERTY(bool stopJobs READ stopJobs WRITE setStopJobs); Q_PROPERTY(bool raiseJobsPriority READ raiseJobsPriority WRITE setRaiseJobsPriority); @@ -56,7 +60,8 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMSeriesItemWidget : public QWidget public: typedef QWidget Superclass; - explicit ctkDICOMSeriesItemWidget(QWidget* parent = nullptr); + explicit ctkDICOMSeriesItemWidget(QWidget* top = nullptr, + QWidget* parent = nullptr); virtual ~ctkDICOMSeriesItemWidget(); ///@{ @@ -126,6 +131,12 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMSeriesItemWidget : public QWidget bool retrieveFailed() const; ///@} + ///@{ + /// Return the referenceInserterJobUID + QString referenceSeriesInserterJobUID() const; + QString referenceInstanceInserterJobUID() const; + ///@} + /// Series has been loaded by the parent widget bool isLoaded() const; @@ -170,12 +181,13 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMSeriesItemWidget : public QWidget public Q_SLOTS: void generateInstances(bool query = true, bool retrieve = true); - void updateGUIFromScheduler(const QVariant& data); - void updateSeriesProgressBar(const QVariant& data); - void onJobStarted(const QVariant& data); - void onJobCanceled(const QVariant& data); - void onJobFailed(const QVariant& data); - void onJobFinished(const QVariant& data); + void updateGUIFromScheduler(const QVariant&); + void updateSeriesProgressBar(const QVariant&); + void onJobStarted(const QVariant&); + void onJobUserStopped(const QVariant&); + void onJobFailed(const QVariant&); + void onJobFinished(const QVariant&); + void onOperationStatusButtonClicked(bool); protected: QScopedPointer d_ptr; diff --git a/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.cpp b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.cpp index 0b99de0c4a..a9f8992e83 100644 --- a/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.cpp @@ -282,8 +282,8 @@ class ctkDICOMServerNodeWidget2Private : public Ui_ctkDICOMServerNodeWidget2 QPushButton* CancelButton; QPalette DefaultPalette; QPalette ModifiedPalette; - QPalette ServerSuccesPalette; - QPalette ServerProgresPalette; + QPalette ServerSuccessPalette; + QPalette ServerProgressPalette; QPalette ServerFailedPalette; }; @@ -299,10 +299,10 @@ ctkDICOMServerNodeWidget2Private::ctkDICOMServerNodeWidget2Private(ctkDICOMServe this->DefaultPalette.setColor(QPalette::Base, ctkDICOMServerNodeWidget2DefaultColor); this->ModifiedPalette.setColor(QPalette::Button, ctkDICOMServerNodeWidget2ModifiedColor); this->ModifiedPalette.setColor(QPalette::Base, ctkDICOMServerNodeWidget2ModifiedColor); - this->ServerSuccesPalette.setColor(QPalette::Button, ctkDICOMServerNodeWidget2ServerSuccesColor); - this->ServerSuccesPalette.setColor(QPalette::Base, ctkDICOMServerNodeWidget2ServerSuccesColor); - this->ServerProgresPalette.setColor(QPalette::Button, ctkDICOMServerNodeWidget2ServerProgressColor); - this->ServerProgresPalette.setColor(QPalette::Base, ctkDICOMServerNodeWidget2ServerProgressColor); + this->ServerSuccessPalette.setColor(QPalette::Button, ctkDICOMServerNodeWidget2ServerSuccesColor); + this->ServerSuccessPalette.setColor(QPalette::Base, ctkDICOMServerNodeWidget2ServerSuccesColor); + this->ServerProgressPalette.setColor(QPalette::Button, ctkDICOMServerNodeWidget2ServerProgressColor); + this->ServerProgressPalette.setColor(QPalette::Base, ctkDICOMServerNodeWidget2ServerProgressColor); this->ServerFailedPalette.setColor(QPalette::Button, ctkDICOMServerNodeWidget2ServerFailedColor); this->ServerFailedPalette.setColor(QPalette::Base, ctkDICOMServerNodeWidget2ServerFailedColor); } @@ -375,7 +375,7 @@ void ctkDICOMServerNodeWidget2Private::init() QObject::connect(this->RestoreDefaultPushButton, SIGNAL(clicked()), q, SLOT(onRestoreDefaultServers())); this->SaveButton = this->ActionsButtonBox->button(QDialogButtonBox::StandardButton::Save); - this->SaveButton->setText(ctkDICOMServerNodeWidget2::tr("Apply changes")); + this->SaveButton->setText(ctkDICOMServerNodeWidget2::tr("Apply changes ")); this->SaveButton->setIcon(QIcon(":/Icons/save.svg")); this->CancelButton = this->ActionsButtonBox->button(QDialogButtonBox::StandardButton::Discard); this->CancelButton->setText(ctkDICOMServerNodeWidget2::tr("Discard changes")); @@ -397,14 +397,14 @@ void ctkDICOMServerNodeWidget2Private::disconnectScheduler() return; } - ctkDICOMServerNodeWidget2::disconnect(this->Scheduler.data(), SIGNAL(jobStarted(QVariant)), - q, SLOT(onJobStarted(QVariant))); - ctkDICOMServerNodeWidget2::disconnect(this->Scheduler.data(), SIGNAL(jobCanceled(QVariant)), - q, SLOT(onJobCanceled(QVariant))); - ctkDICOMServerNodeWidget2::disconnect(this->Scheduler.data(), SIGNAL(jobFinished(QVariant)), - q, SLOT(onJobFinished(QVariant))); - ctkDICOMServerNodeWidget2::disconnect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), - q, SLOT(onJobFailed(QVariant))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobStarted(QList)), + q, SLOT(onJobStarted(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobUserStopped(QList)), + q, SLOT(onJobUserStopped(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobFinished(QList)), + q, SLOT(onJobFinished(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobFailed(QList)), + q, SLOT(onJobFailed(QList))); } //---------------------------------------------------------------------------- @@ -416,14 +416,14 @@ void ctkDICOMServerNodeWidget2Private::connectScheduler() return; } - ctkDICOMServerNodeWidget2::connect(this->Scheduler.data(), SIGNAL(jobStarted(QVariant)), - q, SLOT(onJobStarted(QVariant))); - ctkDICOMServerNodeWidget2::connect(this->Scheduler.data(), SIGNAL(jobCanceled(QVariant)), - q, SLOT(onJobCanceled(QVariant))); - ctkDICOMServerNodeWidget2::connect(this->Scheduler.data(), SIGNAL(jobFinished(QVariant)), - q, SLOT(onJobFinished(QVariant))); - ctkDICOMServerNodeWidget2::connect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), - q, SLOT(onJobFailed(QVariant))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobStarted(QList)), + q, SLOT(onJobStarted(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobUserStopped(QList)), + q, SLOT(onJobUserStopped(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobFinished(QList)), + q, SLOT(onJobFinished(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobFailed(QList)), + q, SLOT(onJobFailed(QList))); } //---------------------------------------------------------------------------- @@ -900,11 +900,11 @@ void ctkDICOMServerNodeWidget2Private::updateServerVerification(const ctkDICOMJo QPalette palette; if (status == ctkDICOMServerNodeWidget2::tr("success")) { - palette = this->ServerSuccesPalette; + palette = this->ServerSuccessPalette; } - else if (status == ctkDICOMServerNodeWidget2::tr("in progress")) + else if (status == ctkDICOMServerNodeWidget2::tr("in-progress")) { - palette = this->ServerProgresPalette; + palette = this->ServerProgressPalette; } else { @@ -1204,38 +1204,50 @@ void ctkDICOMServerNodeWidget2::onVerifyCurrentServerNode() //---------------------------------------------------------------------------- -void ctkDICOMServerNodeWidget2::onJobStarted(QVariant data) +void ctkDICOMServerNodeWidget2::onJobStarted(QList datas) { Q_D(ctkDICOMServerNodeWidget2); - ctkDICOMJobDetail td = data.value(); - d->updateServerVerification(td, QString(tr("in progress"))); + foreach (QVariant data, datas) + { + ctkDICOMJobDetail td = data.value(); + d->updateServerVerification(td, QString(tr("in-progress"))); + } this->updateGUIState(); } //---------------------------------------------------------------------------- -void ctkDICOMServerNodeWidget2::onJobCanceled(QVariant data) +void ctkDICOMServerNodeWidget2::onJobUserStopped(QList datas) { Q_D(ctkDICOMServerNodeWidget2); - ctkDICOMJobDetail td = data.value(); - d->updateServerVerification(td, QString(tr("canceled"))); + foreach (QVariant data, datas) + { + ctkDICOMJobDetail td = data.value(); + d->updateServerVerification(td, QString(tr("user-stopped"))); + } this->updateGUIState(); } //---------------------------------------------------------------------------- -void ctkDICOMServerNodeWidget2::onJobFailed(QVariant data) +void ctkDICOMServerNodeWidget2::onJobFailed(QList datas) { Q_D(ctkDICOMServerNodeWidget2); - ctkDICOMJobDetail td = data.value(); - d->updateServerVerification(td, QString(tr("failed"))); + foreach (QVariant data, datas) + { + ctkDICOMJobDetail td = data.value(); + d->updateServerVerification(td, QString(tr("failed"))); + } this->updateGUIState(); } //---------------------------------------------------------------------------- -void ctkDICOMServerNodeWidget2::onJobFinished(QVariant data) +void ctkDICOMServerNodeWidget2::onJobFinished(QList datas) { Q_D(ctkDICOMServerNodeWidget2); - ctkDICOMJobDetail td = data.value(); - d->updateServerVerification(td, QString(tr("success"))); + foreach (QVariant data, datas) + { + ctkDICOMJobDetail td = data.value(); + d->updateServerVerification(td, QString(tr("success"))); + } this->updateGUIState(); } diff --git a/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.h b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.h index ae16996cdd..e766b65420 100644 --- a/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.h +++ b/Libs/DICOM/Widgets/ctkDICOMServerNodeWidget2.h @@ -110,10 +110,10 @@ public Q_SLOTS: /// Verify the current selected row (different from the checked rows) void onVerifyCurrentServerNode(); - void onJobStarted(QVariant); - void onJobCanceled(QVariant); - void onJobFailed(QVariant); - void onJobFinished(QVariant); + void onJobStarted(QList); + void onJobUserStopped(QList); + void onJobFailed(QList); + void onJobFinished(QList); void readSettings(); void saveSettings(); diff --git a/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.cpp index 49fab7b97f..f81161e396 100644 --- a/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.cpp @@ -64,7 +64,9 @@ class ctkDICOMStudyItemWidgetPrivate : public Ui_ctkDICOMStudyItemWidget ctkDICOMStudyItemWidgetPrivate(ctkDICOMStudyItemWidget& obj); ~ctkDICOMStudyItemWidgetPrivate(); - void init(QWidget* parentWidget); + void init(QWidget* top, QWidget* parent); + void connectToTop(); + void disconnectFromTop(); void updateColumnsWidths(); void createSeries(); int getScreenWidth(); @@ -77,10 +79,13 @@ class ctkDICOMStudyItemWidgetPrivate : public Ui_ctkDICOMStudyItemWidget QString FilteringSeriesDescription; QStringList FilteringModalities; QStringList AllowedServers; + ctkDICOMStudyItemWidget::OperationStatus Status; QSharedPointer DicomDatabase; QSharedPointer Scheduler; + QSharedPointer PatientWidget; QSharedPointer VisualDICOMBrowser; + QMap Connections; ctkDICOMStudyItemWidget::ThumbnailSizeOption ThumbnailSize; int ThumbnailSizePixel; @@ -109,6 +114,7 @@ ctkDICOMStudyItemWidgetPrivate::ctkDICOMStudyItemWidgetPrivate(ctkDICOMStudyItem this->StudyItem = ""; this->AllowedServers = QStringList(); + this->Status = ctkDICOMStudyItemWidget::NoOperation; this->DicomDatabase = nullptr; this->Scheduler = nullptr; @@ -123,8 +129,7 @@ ctkDICOMStudyItemWidgetPrivate::ctkDICOMStudyItemWidgetPrivate(ctkDICOMStudyItem //---------------------------------------------------------------------------- ctkDICOMStudyItemWidgetPrivate::~ctkDICOMStudyItemWidgetPrivate() { - Q_Q(ctkDICOMStudyItemWidget); - + this->disconnectFromTop(); for (int row = 0; row < this->SeriesListTableWidget->rowCount(); row++) { for (int column = 0; column < this->SeriesListTableWidget->columnCount(); column++) @@ -136,26 +141,77 @@ ctkDICOMStudyItemWidgetPrivate::~ctkDICOMStudyItemWidgetPrivate() continue; } - q->disconnect(seriesItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), - this->VisualDICOMBrowser.data(), SLOT(showSeriesContextMenu(const QPoint&))); + QObject::disconnect(seriesItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), + this->VisualDICOMBrowser.data(), SLOT(showSeriesContextMenu(const QPoint&))); } } } //---------------------------------------------------------------------------- -void ctkDICOMStudyItemWidgetPrivate::init(QWidget* parentWidget) +void ctkDICOMStudyItemWidgetPrivate::init(QWidget* top, QWidget* parent) { Q_Q(ctkDICOMStudyItemWidget); this->setupUi(q); - this->VisualDICOMBrowser = QSharedPointer(parentWidget, skipDelete); + this->PatientWidget = QSharedPointer(top, skipDelete); + this->connectToTop(); + this->VisualDICOMBrowser = QSharedPointer(parent, skipDelete); this->StudyDescriptionTextBrowser->hide(); this->StudyDescriptionTextBrowser->setReadOnly(true); + this->StudyDescriptionTextBrowser->setDisableMouseScroll(true); this->StudyItemCollapsibleGroupBox->setCollapsed(false); - q->connect(this->StudySelectionCheckBox, SIGNAL(clicked(bool)), - q, SLOT(onStudySelectionClicked(bool))); + this->OperationStatusPushButton->hide(); + + QObject::connect(this->StudySelectionCheckBox, SIGNAL(clicked(bool)), + q, SLOT(onStudySelectionClicked(bool))); + QObject::connect(this->OperationStatusPushButton, SIGNAL(clicked(bool)), + q, SLOT(onOperationStatusButtonClicked(bool))); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidgetPrivate::connectToTop() +{ + Q_Q(ctkDICOMStudyItemWidget); + if (!this->PatientWidget) + { + return; + } + + QMetaObject::Connection progressConnection = + QObject::connect(this->PatientWidget.data(), SIGNAL(progressJobDetail(QVariant)), + q, SLOT(updateGUIFromScheduler(QVariant))); + QMetaObject::Connection startedConnection = + QObject::connect(this->PatientWidget.data(), SIGNAL(jobStarted(QVariant)), + q, SLOT(onJobStarted(QVariant))); + QMetaObject::Connection userStoppedConnection = + QObject::connect(this->PatientWidget.data(), SIGNAL(jobUserStopped(QVariant)), + q, SLOT(onJobUserStopped(QVariant))); + QMetaObject::Connection failedConnection = + QObject::connect(this->PatientWidget.data(), SIGNAL(jobFailed(QVariant)), + q, SLOT(onJobFailed(QVariant))); + QMetaObject::Connection finishedConnection = + QObject::connect(this->PatientWidget.data(), SIGNAL(jobFinished(QVariant)), + q, SLOT(onJobFinished(QVariant))); + this->Connections = + { + {"progress", progressConnection}, + {"started", startedConnection}, + {"userStopped", userStoppedConnection}, + {"finished", finishedConnection}, + {"failed", failedConnection} + }; +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidgetPrivate::disconnectFromTop() +{ + QObject::disconnect(this->Connections.value("progress")); + QObject::disconnect(this->Connections.value("started")); + QObject::disconnect(this->Connections.value("userStopped")); + QObject::disconnect(this->Connections.value("finished")); + QObject::disconnect(this->Connections.value("failed")); } //------------------------------------------------------------------------------ @@ -237,7 +293,10 @@ void ctkDICOMStudyItemWidgetPrivate::createSeries() QString modality = this->DicomDatabase->fieldForSeries("Modality", seriesItem); QString seriesDescription = this->DicomDatabase->fieldForSeries("SeriesDescription", seriesItem); - + if (seriesDescription.isEmpty()) + { + seriesDescription = q->tr("UNDEFINED"); + } ctkDICOMSeriesItemWidget* seriesItemWidget = q->addSeriesItemWidget(seriesIndex, seriesItem, seriesInstanceUID, modality, seriesDescription); if (seriesItemWidget) @@ -393,12 +452,12 @@ ctkDICOMSeriesItemWidget* ctkDICOMStudyItemWidgetPrivate::isSeriesItemAlreadyAdd // ctkDICOMStudyItemWidget methods //---------------------------------------------------------------------------- -ctkDICOMStudyItemWidget::ctkDICOMStudyItemWidget(QWidget* parentWidget) - : Superclass(parentWidget) +ctkDICOMStudyItemWidget::ctkDICOMStudyItemWidget(QWidget* top, QWidget* parent) + : Superclass(parent) , d_ptr(new ctkDICOMStudyItemWidgetPrivate(*this)) { Q_D(ctkDICOMStudyItemWidget); - d->init(parentWidget); + d->init(top, parent); } //---------------------------------------------------------------------------- @@ -409,6 +468,8 @@ ctkDICOMStudyItemWidget::~ctkDICOMStudyItemWidget() //------------------------------------------------------------------------------ CTK_SET_CPP(ctkDICOMStudyItemWidget, const QStringList&, setAllowedServers, AllowedServers); CTK_GET_CPP(ctkDICOMStudyItemWidget, QStringList, allowedServers, AllowedServers); +CTK_SET_CPP(ctkDICOMStudyItemWidget, const OperationStatus&, setOperationStatus, Status); +CTK_GET_CPP(ctkDICOMStudyItemWidget, ctkDICOMStudyItemWidget::OperationStatus, operationStatus, Status); CTK_SET_CPP(ctkDICOMStudyItemWidget, const QString&, setStudyItem, StudyItem); CTK_GET_CPP(ctkDICOMStudyItemWidget, QString, studyItem, StudyItem); CTK_SET_CPP(ctkDICOMStudyItemWidget, const QString&, setPatientID, PatientID); @@ -445,12 +506,33 @@ void ctkDICOMStudyItemWidget::setDescription(const QString& description) if (description.isEmpty()) { d->StudyDescriptionTextBrowser->hide(); + return; + } + + QFontMetrics metrics(d->StudyDescriptionTextBrowser->font()); + int textWidth = metrics.horizontalAdvance(description); + int widgetWidth = this->width(); + if (textWidth > widgetWidth) + { + int length = 0; + while (length < description.length() && metrics.horizontalAdvance(description.mid(0, length)) <= widgetWidth) + { + length++; + } + + QString wrappedText = description; + if (length < description.length()) + { + wrappedText.insert(length, "\n"); + } + d->StudyDescriptionTextBrowser->setCollapsibleText(wrappedText); } else { - d->StudyDescriptionTextBrowser->setText(description); - d->StudyDescriptionTextBrowser->show(); + d->StudyDescriptionTextBrowser->setPlainText(description); } + + d->StudyDescriptionTextBrowser->show(); } //------------------------------------------------------------------------------ @@ -532,38 +614,14 @@ QSharedPointer ctkDICOMStudyItemWidget::schedulerShared() con void ctkDICOMStudyItemWidget::setScheduler(ctkDICOMScheduler& scheduler) { Q_D(ctkDICOMStudyItemWidget); - if (d->Scheduler) - { - QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - this, SLOT(updateGUIFromScheduler(QVariant))); - } - d->Scheduler = QSharedPointer(&scheduler, skipDelete); - - if (d->Scheduler) - { - QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - this, SLOT(updateGUIFromScheduler(QVariant))); - } } //---------------------------------------------------------------------------- void ctkDICOMStudyItemWidget::setScheduler(QSharedPointer scheduler) { Q_D(ctkDICOMStudyItemWidget); - if (d->Scheduler) - { - QObject::disconnect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - this, SLOT(updateGUIFromScheduler(QVariant))); - } - d->Scheduler = scheduler; - - if (d->Scheduler) - { - QObject::connect(d->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - this, SLOT(updateGUIFromScheduler(QVariant))); - } } //---------------------------------------------------------------------------- @@ -640,7 +698,7 @@ ctkDICOMSeriesItemWidget* ctkDICOMStudyItemWidget::addSeriesItemWidget(int table } QString seriesNumber = d->DicomDatabase->fieldForSeries("SeriesNumber", seriesItem); - ctkDICOMSeriesItemWidget* seriesItemWidget = new ctkDICOMSeriesItemWidget; + ctkDICOMSeriesItemWidget* seriesItemWidget = new ctkDICOMSeriesItemWidget(this); seriesItemWidget->setSeriesItem(seriesItem); seriesItemWidget->setPatientID(d->PatientID); seriesItemWidget->setStudyInstanceUID(d->StudyInstanceUID); @@ -718,12 +776,13 @@ void ctkDICOMStudyItemWidget::generateSeries(bool query, bool retrieve) d->QueryOn = query; d->RetrieveOn = retrieve; d->createSeries(); - if (query && d->Scheduler && d->Scheduler->queryRetrieveServersCount() > 0) + if (query && d->Scheduler && + d->Scheduler->queryRetrieveServersCount() > 0) { d->Scheduler->querySeries(d->PatientID, - d->StudyInstanceUID, - QThread::NormalPriority, - d->AllowedServers); + d->StudyInstanceUID, + QThread::NormalPriority, + d->AllowedServers); } } @@ -736,17 +795,115 @@ void ctkDICOMStudyItemWidget::updateGUIFromScheduler(const QVariant& data) if (td.JobUID.isEmpty()) { d->createSeries(); + return; + } + + if (td.StudyInstanceUID != d->StudyInstanceUID) + { + return; } + emit this->progressJobDetail(data); + + if (td.JobType != ctkDICOMJobResponseSet::JobType::QuerySeries) + { + return; + } + + d->createSeries(); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::onJobStarted(const QVariant &data) +{ + Q_D(ctkDICOMStudyItemWidget); + + ctkDICOMJobDetail td = data.value(); if (td.JobUID.isEmpty() || - td.JobType != ctkDICOMJobResponseSet::JobType::QuerySeries || - td.PatientID != d->PatientID || td.StudyInstanceUID != d->StudyInstanceUID) { return; } - d->createSeries(); + emit this->jobStarted(data); + + if (td.JobType != ctkDICOMJobResponseSet::JobType::QuerySeries) + { + return; + } + + d->Status = ctkDICOMStudyItemWidget::InProgress; + d->OperationStatusPushButton->show(); + d->OperationStatusPushButton->setIcon(QIcon(":/Icons/pending.svg")); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::onJobUserStopped(const QVariant &data) +{ + Q_D(ctkDICOMStudyItemWidget); + + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty() || + td.StudyInstanceUID != d->StudyInstanceUID) + { + return; + } + + emit this->jobUserStopped(data); + + if (td.JobType != ctkDICOMJobResponseSet::JobType::QuerySeries) + { + return; + } + + d->Status = ctkDICOMStudyItemWidget::Failed; + d->OperationStatusPushButton->setIcon(QIcon(":/Icons/error_red.svg")); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::onJobFailed(const QVariant &data) +{ + Q_D(ctkDICOMStudyItemWidget); + + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty() || + td.StudyInstanceUID != d->StudyInstanceUID) + { + return; + } + + emit this->jobFailed(data); + + if (td.JobType != ctkDICOMJobResponseSet::JobType::QuerySeries) + { + return; + } + + d->Status = ctkDICOMStudyItemWidget::Failed; + d->OperationStatusPushButton->setIcon(QIcon(":/Icons/error_red.svg")); +} + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::onJobFinished(const QVariant &data) +{ + Q_D(ctkDICOMStudyItemWidget); + + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty() || + td.StudyInstanceUID != d->StudyInstanceUID) + { + return; + } + + emit this->jobFinished(data); + + if (td.JobType != ctkDICOMJobResponseSet::JobType::QuerySeries) + { + return; + } + + d->Status = ctkDICOMStudyItemWidget::Completed; + d->OperationStatusPushButton->setIcon(QIcon(":/Icons/accept.svg")); } //------------------------------------------------------------------------------ @@ -754,3 +911,26 @@ void ctkDICOMStudyItemWidget::onStudySelectionClicked(bool toggled) { this->setSelection(toggled); } + +//------------------------------------------------------------------------------ +void ctkDICOMStudyItemWidget::onOperationStatusButtonClicked(bool) +{ + Q_D(ctkDICOMStudyItemWidget); + + ctkDICOMStudyItemWidget::OperationStatus status = d->Status; + if (status == ctkDICOMStudyItemWidget::InProgress) + { + d->Scheduler->stopJobsByDICOMUIDs(QStringList(), + QStringList(d->StudyInstanceUID)); + } + else if (status > ctkDICOMStudyItemWidget::InProgress) + { + ctkDICOMJobDetail queryJobDetail; + queryJobDetail.JobClass = "ctkDICOMQueryJob"; + queryJobDetail.DICOMLevel = ctkDICOMJob::DICOMLevels::Series; + queryJobDetail.PatientID = d->PatientID; + queryJobDetail.StudyInstanceUID = d->StudyInstanceUID; + + d->Scheduler->runJob(queryJobDetail, d->AllowedServers); + } +} diff --git a/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h index 8ef1a6f349..2514f1431b 100644 --- a/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h +++ b/Libs/DICOM/Widgets/ctkDICOMStudyItemWidget.h @@ -40,6 +40,7 @@ class ctkDICOMScheduler; // ctkDICOMWidgets includes #include "ctkDICOMSeriesItemWidget.h" + class ctkDICOMSeriesItemWidget; class ctkDICOMStudyItemWidgetPrivate; @@ -62,10 +63,12 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMStudyItemWidget : public QWidget Q_PROPERTY(QStringList filteringModalities READ filteringModalities WRITE setFilteringModalities); Q_PROPERTY(int filteredSeriesCount READ filteredSeriesCount); Q_PROPERTY(QStringList allowedServers READ allowedServers WRITE setAllowedServers); + Q_PROPERTY(OperationStatus operationStatus READ operationStatus WRITE setOperationStatus); public: typedef QWidget Superclass; - explicit ctkDICOMStudyItemWidget(QWidget* parent = nullptr); + explicit ctkDICOMStudyItemWidget(QWidget* top = nullptr, + QWidget* parent = nullptr); virtual ~ctkDICOMStudyItemWidget(); ///@{ @@ -154,6 +157,21 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMStudyItemWidget : public QWidget QStringList allowedServers() const; ///@} + enum OperationStatus + { + NoOperation = 0, + InProgress, + Completed, + Failed, + }; + + ///@{ + /// Set the operation status + /// NoOperation by default + void setOperationStatus(const OperationStatus& status); + OperationStatus operationStatus() const; + ///@} + /// Return the scheduler. Q_INVOKABLE ctkDICOMScheduler* scheduler() const; /// Return the scheduler as a shared pointer @@ -195,12 +213,23 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMStudyItemWidget : public QWidget public Q_SLOTS: void generateSeries(bool query = true, bool retrieve = true); - void updateGUIFromScheduler(const QVariant& data); + void updateGUIFromScheduler(const QVariant&); + void onJobStarted(const QVariant&); + void onJobUserStopped(const QVariant&); + void onJobFailed(const QVariant&); + void onJobFinished(const QVariant&); void onStudySelectionClicked(bool); + void onOperationStatusButtonClicked(bool); Q_SIGNALS: /// Emitted when the GUI finished to update after a series query. void updateGUIFinished(); + /// Propagate jobs signals to the tree + void jobStarted(QVariant); + void jobUserStopped(QVariant); + void jobFinished(QVariant); + void jobFailed(QVariant); + void progressJobDetail(QVariant); protected: QScopedPointer d_ptr; diff --git a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp index aa084c4cbc..c57819d6c7 100644 --- a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include // DCMTK includes #include "dcmtk/dcmimgle/dcmimage.h" @@ -189,25 +191,27 @@ bool ctkDICOMThumbnailGenerator::generateThumbnail(DicomImage *dcmImage, QImage& } //------------------------------------------------------------------------------ -bool ctkDICOMThumbnailGenerator::generateThumbnail(DicomImage *dcmImage, const QString &path) +bool ctkDICOMThumbnailGenerator::generateThumbnail(DicomImage *dcmImage, const QString &thumbnailPath, QVector color) { QImage image; if (this->generateThumbnail(dcmImage, image)) { - return image.save(path,"PNG"); + return image.save(thumbnailPath, "PNG"); } + + this->generateDocumentThumbnail(thumbnailPath, color); return false; } //------------------------------------------------------------------------------ -bool ctkDICOMThumbnailGenerator::generateThumbnail(const QString dcmImagePath, QImage& image) +bool ctkDICOMThumbnailGenerator::generateThumbnail(const QString& dcmImagePath, QImage& image) { DicomImage dcmImage(QDir::toNativeSeparators(dcmImagePath).toUtf8()); return this->generateThumbnail(&dcmImage, image); } //------------------------------------------------------------------------------ -bool ctkDICOMThumbnailGenerator::generateThumbnail(const QString dcmImagePath, const QString& thumbnailPath) +bool ctkDICOMThumbnailGenerator::generateThumbnail(const QString& dcmImagePath, const QString& thumbnailPath) { DicomImage dcmImage(QDir::toNativeSeparators(dcmImagePath).toUtf8()); return this->generateThumbnail(&dcmImage, thumbnailPath); @@ -223,3 +227,21 @@ void ctkDICOMThumbnailGenerator::generateBlankThumbnail(QImage& image, QColor co } image.fill(color); } + +//------------------------------------------------------------------------------ +void ctkDICOMThumbnailGenerator::generateDocumentThumbnail(const QString &thumbnailPath, QVector color) +{ + QImage image; + this->generateBlankThumbnail(image, QColor(color[0], color[1], color[2])); + QPixmap pixmap = QPixmap::fromImage(image); + QPainter painter; + if (painter.begin(&pixmap)) + { + painter.setRenderHint(QPainter::Antialiasing); + QSvgRenderer renderer(QString(":Icons/text_document.svg")); + renderer.render(&painter); + painter.end(); + } + image = pixmap.toImage(); + image.save(thumbnailPath, "PNG"); +} diff --git a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h index eb8db9a9e8..6881972bfb 100644 --- a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h +++ b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h @@ -49,15 +49,18 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMThumbnailGenerator : public ctkDICOMAbstr explicit ctkDICOMThumbnailGenerator(QObject* parent = 0); virtual ~ctkDICOMThumbnailGenerator(); - virtual bool generateThumbnail(DicomImage* dcmImage, const QString& path); + virtual bool generateThumbnail(DicomImage* dcmImage, const QString& thumbnailPath, + QVector color = QVector{169, 169, 169}); Q_INVOKABLE bool generateThumbnail(DicomImage *dcmImage, QImage& image); - Q_INVOKABLE bool generateThumbnail(const QString dcmImagePath, QImage& image); - Q_INVOKABLE bool generateThumbnail(const QString dcmImagePath, const QString& thumbnailPath); + Q_INVOKABLE bool generateThumbnail(const QString& dcmImagePath, QImage& image); + Q_INVOKABLE bool generateThumbnail(const QString& dcmImagePath, const QString& thumbnailPath); /// Generate a blank thumbnail image (currently a solid gray box of the requested thumbnail size). /// It can be used as a placeholder for invalid images or duringan image is loaded. Q_INVOKABLE void generateBlankThumbnail(QImage& image, QColor color = Qt::darkGray); + Q_INVOKABLE virtual void generateDocumentThumbnail(const QString &thumbnailPath, + QVector color = QVector{169, 169, 169}); /// Set thumbnail width void setWidth(int width); diff --git a/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp index 8c0ae08239..c720752def 100644 --- a/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.cpp @@ -50,6 +50,7 @@ #include "ctkDICOMScheduler.h" #include "ctkDICOMScheduler.h" #include "ctkDICOMServer.h" +#include "ctkDICOMThumbnailGenerator.h" #include "ctkUtils.h" // ctkDICOMWidgets includes @@ -144,6 +145,7 @@ class ctkDICOMVisualBrowserWidgetPrivate : public Ui_ctkDICOMVisualBrowserWidget void importDirectory(QString directory, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode); void importFiles(const QStringList& files, ctkDICOMVisualBrowserWidget::ImportDirectoryMode mode); void importOldSettings(); + void configureSearchIcon(); void showUpdateSchemaDialog(); void updateModalityCheckableComboBox(); void createPatients(bool queryRetrieve = true, @@ -156,6 +158,9 @@ class ctkDICOMVisualBrowserWidgetPrivate : public Ui_ctkDICOMVisualBrowserWidget void retrieveSeries(); bool updateServer(ctkDICOMServer* server); void removeAllPatientItemWidgets(); + QString findPatientItemFromPatientInfo(const QString& patientID = "", + const QString& patientName = "", + const QString& patientBirthDate = ""); int findPatientTabIndexFromPatientItem(const QString& patientItem); void updateSeriesTablesSelection(ctkDICOMSeriesItemWidget* selectedSeriesItemWidget); QStringList getPatientItemsFromWidgets(ctkDICOMModel::IndexType level, @@ -223,6 +228,7 @@ class ctkDICOMVisualBrowserWidgetPrivate : public Ui_ctkDICOMVisualBrowserWidget QString DatabaseDirectory; QSharedPointer DicomDatabase; + QSharedPointer ThumbnailGenerator; QSharedPointer Scheduler; QSharedPointer Indexer; @@ -236,7 +242,7 @@ class ctkDICOMVisualBrowserWidgetPrivate : public Ui_ctkDICOMVisualBrowserWidget QStringList PreviousFilteringModalities; QStringList FilteringModalities; - int NumberOfStudiesPerPatient; + int NumberOfOpenedStudiesPerPatient; ctkDICOMStudyItemWidget::ThumbnailSizeOption ThumbnailSize; bool SendActionVisible; bool DeleteActionVisible; @@ -258,7 +264,10 @@ CTK_SET_CPP(ctkDICOMVisualBrowserWidget, const QString&, setDatabaseDirectoryBas ctkDICOMVisualBrowserWidgetPrivate::ctkDICOMVisualBrowserWidgetPrivate(ctkDICOMVisualBrowserWidget& obj) : q_ptr(&obj) { + this->ThumbnailGenerator = QSharedPointer(new ctkDICOMThumbnailGenerator); + this->DicomDatabase = QSharedPointer(new ctkDICOMDatabase); + this->DicomDatabase->setThumbnailGenerator(ThumbnailGenerator.data()); this->Scheduler = QSharedPointer(new ctkDICOMScheduler); this->Scheduler->setDicomDatabase(this->DicomDatabase); @@ -276,7 +285,7 @@ ctkDICOMVisualBrowserWidgetPrivate::ctkDICOMVisualBrowserWidgetPrivate(ctkDICOMV this->DefaultDatabaseDirectory = ""; this->DatabaseDirectory = ""; - this->NumberOfStudiesPerPatient = 2; + this->NumberOfOpenedStudiesPerPatient = 2; this->ThumbnailSize = ctkDICOMStudyItemWidget::ThumbnailSizeOption::Medium; this->SendActionVisible = false; this->DeleteActionVisible = true; @@ -312,6 +321,8 @@ ctkDICOMVisualBrowserWidgetPrivate::ctkDICOMVisualBrowserWidgetPrivate(ctkDICOMV //---------------------------------------------------------------------------- ctkDICOMVisualBrowserWidgetPrivate::~ctkDICOMVisualBrowserWidgetPrivate() { + this->ImportDialog->deleteLater(); + this->MetadataDialog->deleteLater(); this->removeAllPatientItemWidgets(); } @@ -390,6 +401,7 @@ void ctkDICOMVisualBrowserWidgetPrivate::init() } toggleQueryRetrieveAction->setChecked(settings.value("DICOM/QueryRetrieveEnabled", "").toBool()); queryRetrieveButtonMenu->addAction(toggleQueryRetrieveAction); + this->configureSearchIcon(); QObject::connect(toggleQueryRetrieveAction, SIGNAL(toggled(bool)), q, SLOT(onQueryRetrieveOptionToggled(bool))); @@ -412,6 +424,9 @@ void ctkDICOMVisualBrowserWidgetPrivate::init() QObject::connect(this->PatientsTabWidget, SIGNAL(currentChanged(int)), q, SLOT(onPatientItemChanged(int))); + QObject::connect(this->PatientsTabWidget, SIGNAL(tabBarClicked(int)), + q, SLOT(onOperationStatusTabBarItemClicked(int))); + QObject::connect(this->ServerNodeWidget, SIGNAL(serversSettingsChanged()), q, SLOT(onServersSettingsChanged())); @@ -448,11 +463,11 @@ void ctkDICOMVisualBrowserWidgetPrivate::init() this->ImportDialog->setWindowTitle(ctkDICOMVisualBrowserWidget::tr("Import DICOM files from directory ...")); this->ImportDialog->setWindowModality(Qt::ApplicationModal); - q->connect(this->ImportDialog, SIGNAL(filesSelected(QStringList)), - q, SLOT(onImportDirectoriesSelected(QStringList))); + QObject::connect(this->ImportDialog, SIGNAL(filesSelected(QStringList)), + q, SLOT(onImportDirectoriesSelected(QStringList))); - q->connect(importDirectoryModeComboBox, SIGNAL(currentIndexChanged(int)), - q, SLOT(onImportDirectoryComboBoxCurrentIndexChanged(int))); + QObject::connect(importDirectoryModeComboBox, SIGNAL(currentIndexChanged(int)), + q, SLOT(onImportDirectoryComboBoxCurrentIndexChanged(int))); this->ProgressFrame->hide(); @@ -464,11 +479,14 @@ void ctkDICOMVisualBrowserWidgetPrivate::init() this->JobListWidget->setScheduler(this->Scheduler); this->connectScheduler(); - q->connect(this->ProgressCancelButton, SIGNAL(clicked()), this->Indexer.data(), SLOT(cancel())); - q->connect(this->Indexer.data(), SIGNAL(progress(int)), q, SLOT(onIndexingProgress(int))); - q->connect(this->Indexer.data(), SIGNAL(progressStep(QString)), q, SLOT(onIndexingProgressStep(QString))); - q->connect(this->Indexer.data(), SIGNAL(progressDetail(QString)), q, SLOT(onIndexingProgressDetail(QString))); - q->connect(this->Indexer.data(), SIGNAL(indexingComplete(int, int, int, int)), q, SLOT(onIndexingComplete(int, int, int, int))); + QObject::connect(this->JobListWidget, SIGNAL(patientSelected(QString, QString, QString)), + q, SLOT(patientSelectedOnJobList(QString, QString, QString))); + + QObject::connect(this->ProgressCancelButton, SIGNAL(clicked()), this->Indexer.data(), SLOT(cancel())); + QObject::connect(this->Indexer.data(), SIGNAL(progress(int)), q, SLOT(onIndexingProgress(int))); + QObject::connect(this->Indexer.data(), SIGNAL(progressStep(QString)), q, SLOT(onIndexingProgressStep(QString))); + QObject::connect(this->Indexer.data(), SIGNAL(progressDetail(QString)), q, SLOT(onIndexingProgressDetail(QString))); + QObject::connect(this->Indexer.data(), SIGNAL(indexingComplete(int, int, int, int)), q, SLOT(onIndexingComplete(int, int, int, int))); } //---------------------------------------------------------------------------- @@ -480,10 +498,16 @@ void ctkDICOMVisualBrowserWidgetPrivate::disconnectScheduler() return; } - q->disconnect(this->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - q, SLOT(updateGUIFromScheduler(QVariant))); - q->disconnect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), - q, SLOT(onJobFailed(QVariant))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(progressJobDetail(QList)), + q, SLOT(updateGUIFromScheduler(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobStarted(QList)), + q, SLOT(onJobStarted(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobUserStopped(QList)), + q, SLOT(onJobUserStopped(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobFailed(QList)), + q, SLOT(onJobFailed(QList))); + QObject::disconnect(this->Scheduler.data(), SIGNAL(jobFinished(QList)), + q, SLOT(onJobFinished(QList))); } //---------------------------------------------------------------------------- @@ -495,10 +519,16 @@ void ctkDICOMVisualBrowserWidgetPrivate::connectScheduler() return; } - q->connect(this->Scheduler.data(), SIGNAL(progressJobDetail(QVariant)), - q, SLOT(updateGUIFromScheduler(QVariant))); - q->connect(this->Scheduler.data(), SIGNAL(jobFailed(QVariant)), - q, SLOT(onJobFailed(QVariant))); + QObject::connect(this->Scheduler.data(), SIGNAL(progressJobDetail(QList)), + q, SLOT(updateGUIFromScheduler(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobStarted(QList)), + q, SLOT(onJobStarted(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobUserStopped(QList)), + q, SLOT(onJobUserStopped(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobFailed(QList)), + q, SLOT(onJobFailed(QList))); + QObject::connect(this->Scheduler.data(), SIGNAL(jobFinished(QList)), + q, SLOT(onJobFinished(QList))); } //---------------------------------------------------------------------------- @@ -557,6 +587,21 @@ void ctkDICOMVisualBrowserWidgetPrivate::importOldSettings() settings.remove("MainWindow/DontConfirmCopyOnImport"); } +//---------------------------------------------------------------------------- +void ctkDICOMVisualBrowserWidgetPrivate::configureSearchIcon() +{ + QSettings settings; + bool queryRetrieveEnabled = settings.value("DICOM/QueryRetrieveEnabled", "").toBool(); + if (queryRetrieveEnabled) + { + this->SearchMenuButton->setIcon(QIcon(":/Icons/query.svg")); + } + else + { + this->SearchMenuButton->setIcon(QIcon(":/Icons/search_local.svg")); + } +} + //---------------------------------------------------------------------------- void ctkDICOMVisualBrowserWidgetPrivate::showUpdateSchemaDialog() { @@ -930,6 +975,33 @@ void ctkDICOMVisualBrowserWidgetPrivate::retrieveSeries() this->IsLoading = true; + // Get only the selected series widgets + QList selectedSeriesWidgetsList; + QList studyItemWidgetsList = currentPatientItemWidget->studyItemWidgetsList(); + foreach (ctkDICOMStudyItemWidget* studyItemWidget, studyItemWidgetsList) + { + QTableWidget* seriesListTableWidget = studyItemWidget->seriesListTableWidget(); + QModelIndexList indexList = seriesListTableWidget->selectionModel()->selectedIndexes(); + foreach (QModelIndex index, indexList) + { + ctkDICOMSeriesItemWidget* seriesItemWidget = qobject_cast + (seriesListTableWidget->cellWidget(index.row(), index.column())); + if (!seriesItemWidget) + { + continue; + } + + selectedSeriesWidgetsList.append(seriesItemWidget); + } + } + + if (selectedSeriesWidgetsList.count() == 0) + { + this->IsLoading = false; + return; + } + + // Get all series widgets QList seriesWidgetsList; for (int patientIndex = 0; patientIndex < this->PatientsTabWidget->count(); ++patientIndex) { @@ -961,31 +1033,7 @@ void ctkDICOMVisualBrowserWidgetPrivate::retrieveSeries() } } - QList selectedSeriesWidgetsList; - QList studyItemWidgetsList = currentPatientItemWidget->studyItemWidgetsList(); - foreach (ctkDICOMStudyItemWidget* studyItemWidget, studyItemWidgetsList) - { - QTableWidget* seriesListTableWidget = studyItemWidget->seriesListTableWidget(); - QModelIndexList indexList = seriesListTableWidget->selectionModel()->selectedIndexes(); - foreach (QModelIndex index, indexList) - { - ctkDICOMSeriesItemWidget* seriesItemWidget = qobject_cast - (seriesListTableWidget->cellWidget(index.row(), index.column())); - if (!seriesItemWidget) - { - continue; - } - - selectedSeriesWidgetsList.append(seriesItemWidget); - } - } - - if (selectedSeriesWidgetsList.count() == 0) - { - this->IsLoading = false; - return; - } - + // Update UI QApplication::setOverrideCursor(QCursor(Qt::BusyCursor)); bool deleteActionWasVisible = this->DeleteActionVisible; @@ -994,8 +1042,9 @@ void ctkDICOMVisualBrowserWidgetPrivate::retrieveSeries() bool queryPatientButtonWasEnabled = this->SearchMenuButton->isEnabled(); this->SearchMenuButton->setEnabled(false); + // Set a flag in non selected series widget to stop any jobs creation + // and stop all the jobs connected to the widgets QStringList seriesInstanceUIDsToStop; - QStringList selectedSeriesInstanceUIDs; foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, seriesWidgetsList) { if (!seriesItemWidget) @@ -1008,23 +1057,58 @@ void ctkDICOMVisualBrowserWidgetPrivate::retrieveSeries() seriesItemWidget->setStopJobs(true); seriesInstanceUIDsToStop.append(seriesItemWidget->seriesInstanceUID()); } - else + } + + this->Scheduler->stopJobsByDICOMUIDs({}, {}, seriesInstanceUIDsToStop); + this->Scheduler->waitForDone(300); + + // Check all the selected series widgets. If any widgets is not fully retrieved or the retrieve failed, + // and no jobs are running for the series, force the retrieve. + foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, selectedSeriesWidgetsList) + { + if (!seriesItemWidget) { - selectedSeriesInstanceUIDs.append(seriesItemWidget->seriesInstanceUID()); + continue; + } + + if (!seriesItemWidget->isCloud() && !seriesItemWidget->retrieveFailed()) + { + continue; + } + + if (this->Scheduler->getJobsByDICOMUIDs({}, {}, {seriesItemWidget->seriesInstanceUID()}).count() == 0) + { + seriesItemWidget->forceRetrieve(); } } - this->Scheduler->stopJobsByDICOMUIDs({}, - {}, - seriesInstanceUIDsToStop); + // Create a progress dialog to show the progress of the retrieve + QProgressDialog loadSeriesProgress( + ctkDICOMVisualBrowserWidget::tr("Retrieving and processing selected series..."), + ctkDICOMVisualBrowserWidget::tr("Cancel"), 0, 100, q); + loadSeriesProgress.setWindowModality(Qt::ApplicationModal); + loadSeriesProgress.setMinimumDuration(1000); - this->Scheduler->waitForDone(300); + QProgressBar *bar = new QProgressBar(&loadSeriesProgress); + bar->setTextVisible(false); + loadSeriesProgress.setBar(bar); + loadSeriesProgress.setValue(0); + // Wait for the selected series widgets to be fully retrieved bool wait = true; + int progress = 0; while (wait) { - QCoreApplication::processEvents(); + qApp->processEvents(); this->Scheduler->waitForDone(300); + + progress++; + if (progress == 99) + { + progress = 0; + } + loadSeriesProgress.setValue(progress); + wait = false; foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, selectedSeriesWidgetsList) { @@ -1033,17 +1117,76 @@ void ctkDICOMVisualBrowserWidgetPrivate::retrieveSeries() continue; } + if (loadSeriesProgress.wasCanceled()) + { + break; + } + if (seriesItemWidget->isCloud() && !seriesItemWidget->retrieveFailed()) { wait = true; break; } } + + if (loadSeriesProgress.wasCanceled()) + { + break; + } } + // Update UI this->updateFiltersWarnings(); - this->SearchMenuButton->setIcon(QIcon(":/Icons/query.svg")); + this->configureSearchIcon(); + + this->IsLoading = false; + this->DeleteActionVisible = deleteActionWasVisible; + this->SearchMenuButton->setEnabled(queryPatientButtonWasEnabled); + QApplication::restoreOverrideCursor(); + + // Finalize the retrieve + QStringList selectedSeriesInstanceUIDs; + if (loadSeriesProgress.wasCanceled()) + { + // It was canceled -> stop the jobs connected to the selected series widgets + foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, selectedSeriesWidgetsList) + { + if (!seriesItemWidget) + { + continue; + } + + selectedSeriesInstanceUIDs.append(seriesItemWidget->seriesInstanceUID()); + } + + this->Scheduler->stopJobsByDICOMUIDs({}, {}, selectedSeriesInstanceUIDs); + this->Scheduler->waitForDone(300); + } + else + { + loadSeriesProgress.close(); + foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, selectedSeriesWidgetsList) + { + // If the series was not fully retrieved or the retrieve failed -> skip + if (!seriesItemWidget || seriesItemWidget->isCloud() || seriesItemWidget->retrieveFailed()) + { + continue; + } + + // If the series has only metadata -> skip + if (this->DicomDatabase->instancesForSeries(seriesItemWidget->seriesInstanceUID()).count() == 0) + { + continue; + } + + selectedSeriesInstanceUIDs.append(seriesItemWidget->seriesInstanceUID()); + } + + q->emit seriesRetrieved(selectedSeriesInstanceUIDs); + } + + // Re-allow all seriesItemWidgets to run jobs foreach (ctkDICOMSeriesItemWidget* seriesItemWidget, seriesWidgetsList) { if (!seriesItemWidget) @@ -1053,13 +1196,6 @@ void ctkDICOMVisualBrowserWidgetPrivate::retrieveSeries() seriesItemWidget->setStopJobs(false); } - - q->emit seriesRetrieved(selectedSeriesInstanceUIDs); - - this->IsLoading = false; - this->DeleteActionVisible = deleteActionWasVisible; - this->SearchMenuButton->setEnabled(queryPatientButtonWasEnabled); - QApplication::restoreOverrideCursor(); } //---------------------------------------------------------------------------- @@ -1094,6 +1230,37 @@ void ctkDICOMVisualBrowserWidgetPrivate::removeAllPatientItemWidgets() this->PatientsTabWidget->blockSignals(wasBlocking); } +//---------------------------------------------------------------------------- +QString ctkDICOMVisualBrowserWidgetPrivate::findPatientItemFromPatientInfo(const QString &patientID, + const QString &patientName, + const QString &patientBirthDate) +{ + QString patientItem; + if (patientID.isEmpty() && patientName.isEmpty() && patientBirthDate.isEmpty()) + { + return patientItem; + } + + for (int index = 0; index < this->PatientsTabWidget->count(); ++index) + { + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(this->PatientsTabWidget->widget(index)); + if (!patientItemWidget) + { + continue; + } + + if ((patientID.isEmpty() || patientItemWidget->patientID() == patientID) && + (patientName.isEmpty() || patientItemWidget->patientName() == patientName) && + (patientBirthDate.isEmpty() || patientItemWidget->patientBirthDate() == patientBirthDate)) + { + return this->PatientsTabWidget->tabWhatsThis(index); + } + } + + return patientItem; +} + //---------------------------------------------------------------------------- int ctkDICOMVisualBrowserWidgetPrivate::findPatientTabIndexFromPatientItem(const QString& patientItem) { @@ -1628,13 +1795,7 @@ ctkDICOMVisualBrowserWidget::ctkDICOMVisualBrowserWidget(QWidget* parentWidget) } //---------------------------------------------------------------------------- -ctkDICOMVisualBrowserWidget::~ctkDICOMVisualBrowserWidget() -{ - Q_D(ctkDICOMVisualBrowserWidget); - - d->ImportDialog->deleteLater(); - d->MetadataDialog->deleteLater(); -} +ctkDICOMVisualBrowserWidget::~ctkDICOMVisualBrowserWidget() = default; //---------------------------------------------------------------------------- CTK_GET_CPP(ctkDICOMVisualBrowserWidget, QString, databaseDirectory, DatabaseDirectory); @@ -1645,8 +1806,8 @@ CTK_GET_CPP(ctkDICOMVisualBrowserWidget, QString, filteringStudyDescription, Fil CTK_GET_CPP(ctkDICOMVisualBrowserWidget, ctkDICOMPatientItemWidget::DateType, filteringDate, FilteringDate); CTK_GET_CPP(ctkDICOMVisualBrowserWidget, QString, filteringSeriesDescription, FilteringSeriesDescription); CTK_GET_CPP(ctkDICOMVisualBrowserWidget, QStringList, filteringModalities, FilteringModalities); -CTK_SET_CPP(ctkDICOMVisualBrowserWidget, int, setNumberOfStudiesPerPatient, NumberOfStudiesPerPatient); -CTK_GET_CPP(ctkDICOMVisualBrowserWidget, int, numberOfStudiesPerPatient, NumberOfStudiesPerPatient); +CTK_SET_CPP(ctkDICOMVisualBrowserWidget, int, setNumberOfOpenedStudiesPerPatient, NumberOfOpenedStudiesPerPatient); +CTK_GET_CPP(ctkDICOMVisualBrowserWidget, int, numberOfOpenedStudiesPerPatient, NumberOfOpenedStudiesPerPatient); CTK_SET_CPP(ctkDICOMVisualBrowserWidget, const ctkDICOMStudyItemWidget::ThumbnailSizeOption&, setThumbnailSize, ThumbnailSize); CTK_GET_CPP(ctkDICOMVisualBrowserWidget, ctkDICOMStudyItemWidget::ThumbnailSizeOption, thumbnailSize, ThumbnailSize); CTK_SET_CPP(ctkDICOMVisualBrowserWidget, bool, setSendActionVisible, SendActionVisible); @@ -1936,6 +2097,10 @@ int ctkDICOMVisualBrowserWidget::addPatientItemWidget(const QString& patientItem QString patientName = d->DicomDatabase->fieldForPatient("PatientsName", patientItem); patientName.replace(R"(^)", R"( )"); QString patientID = d->DicomDatabase->fieldForPatient("PatientID", patientItem); + QString date = d->DicomDatabase->fieldForPatient("PatientsBirthDate", patientItem); + date.replace(QString("-"), QString("")); + date = QDate::fromString(date, "yyyyMMdd").toString(); + QString sex = d->DicomDatabase->fieldForPatient("PatientsSex", patientItem); ctkDICOMPatientItemWidget* patientItemWidget = new ctkDICOMPatientItemWidget(this); patientItemWidget->setDicomDatabase(d->DicomDatabase); @@ -1943,12 +2108,14 @@ int ctkDICOMVisualBrowserWidget::addPatientItemWidget(const QString& patientItem patientItemWidget->setPatientItem(patientItem); patientItemWidget->setPatientID(patientID); patientItemWidget->setPatientName(patientName); + patientItemWidget->setPatientBirthDate(date); + patientItemWidget->setPatientSex(sex); patientItemWidget->setFilteringStudyDescription(d->FilteringStudyDescription); patientItemWidget->setFilteringDate(d->FilteringDate); patientItemWidget->setFilteringSeriesDescription(d->FilteringSeriesDescription); patientItemWidget->setFilteringModalities(d->FilteringModalities); patientItemWidget->setThumbnailSize(d->ThumbnailSize); - patientItemWidget->setNumberOfStudiesPerPatient(d->NumberOfStudiesPerPatient); + patientItemWidget->setNumberOfOpenedStudiesPerPatient(d->NumberOfOpenedStudiesPerPatient); patientItemWidget->setContextMenuPolicy(Qt::CustomContextMenu); this->connect(patientItemWidget, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(showPatientContextMenu(const QPoint&))); @@ -2286,7 +2453,7 @@ void ctkDICOMVisualBrowserWidget::onIndexingComplete(int patientsAdded, int stud d->ProgressFrame->hide(); d->ProgressDetailLineEdit->hide(); - d->SearchMenuButton->setIcon(QIcon(":/Icons/query.svg")); + d->configureSearchIcon(); // allow users of this widget to know that the process has finished emit directoryImported(); @@ -2832,71 +2999,195 @@ void ctkDICOMVisualBrowserWidget::onQueryPatients() d->Scheduler->setFilters(parameters); d->Scheduler->queryPatients(QThread::NormalPriority); - - d->SearchMenuButton->setIcon(QIcon(":/Icons/wait.svg")); } } //------------------------------------------------------------------------------ void ctkDICOMVisualBrowserWidget::onQueryRetrieveOptionToggled(bool toggled) { + Q_D(ctkDICOMVisualBrowserWidget); QSettings settings; settings.setValue("DICOM/QueryRetrieveEnabled", toggled); + d->configureSearchIcon(); } //------------------------------------------------------------------------------ -void ctkDICOMVisualBrowserWidget::updateGUIFromScheduler(const QVariant& data) +void ctkDICOMVisualBrowserWidget::updateGUIFromScheduler(QList datas) { Q_D(ctkDICOMVisualBrowserWidget); - d->SearchMenuButton->setIcon(QIcon(":/Icons/query.svg")); - ctkDICOMJobDetail td = data.value(); - if (td.JobUID.isEmpty()) + bool updatePatients = false; + foreach (QVariant data, datas) { + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty()) + { + d->updateFiltersWarnings(); + continue; + } + else if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryStudies || + td.JobType == ctkDICOMJobResponseSet::JobType::QuerySeries) + { + d->updateFiltersWarnings(); + continue; + } + else if (td.JobType != ctkDICOMJobResponseSet::JobType::QueryPatients) + { + continue; + } + d->updateFiltersWarnings(); - return; + if (td.NumberOfDataSets == 0) + { + d->WarningPushButton->setText(tr("The patients query provided no results. Please refine your filters.")); + d->WarningPushButton->show(); + d->SearchMenuButton->setIcon(QIcon(":/Icons/query_failed.svg")); + } + else + { + d->WarningPushButton->hide(); + updatePatients = true; + } } - else if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryStudies || - td.JobType == ctkDICOMJobResponseSet::JobType::QuerySeries) + + if (updatePatients) { - d->updateFiltersWarnings(); - return; + d->createPatients(); } - else if (td.JobType != ctkDICOMJobResponseSet::JobType::QueryPatients) +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onJobStarted(QList datas) +{ + Q_D(ctkDICOMVisualBrowserWidget); + + foreach (QVariant data, datas) { - return; + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty()) + { + continue; + } + + if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryPatients) + { + d->updateFiltersWarnings(); + d->SearchMenuButton->setIcon(QIcon(":/Icons/wait.svg")); + } + else if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryStudies) + { + QString patientItem = d->findPatientItemFromPatientInfo(td.PatientID); + int patientIndex = d->findPatientTabIndexFromPatientItem(patientItem); + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->widget(patientIndex)); + if (patientItemWidget) + { + patientItemWidget->setOperationStatus(ctkDICOMPatientItemWidget::InProgress); + } + d->PatientsTabWidget->setTabIcon(patientIndex, QIcon(":/Icons/patient_pending.svg")); + } } +} - d->updateFiltersWarnings(); - if (td.NumberOfDataSets == 0) +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onJobUserStopped(QList datas) +{ + Q_D(ctkDICOMVisualBrowserWidget); + + foreach (QVariant data, datas) { - d->WarningPushButton->setText(tr("The query provided no results. Please refine your filters.")); - d->WarningPushButton->show(); + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty()) + { + continue; + } + + if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryPatients) + { + d->updateFiltersWarnings(); + d->SearchMenuButton->setIcon(QIcon(":/Icons/query_failed.svg")); + } + else if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryStudies) + { + QString patientItem = d->findPatientItemFromPatientInfo(td.PatientID); + int patientIndex = d->findPatientTabIndexFromPatientItem(patientItem); + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->widget(patientIndex)); + if (patientItemWidget) + { + patientItemWidget->setOperationStatus(ctkDICOMPatientItemWidget::Failed); + } + d->PatientsTabWidget->setTabIcon(patientIndex, QIcon(":/Icons/patient_failed.svg")); + } } - else +} + +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onJobFailed(QList datas) +{ + Q_D(ctkDICOMVisualBrowserWidget); + + foreach (QVariant data, datas) { - d->WarningPushButton->hide(); - } + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty()) + { + continue; + } - d->createPatients(); + if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryPatients) + { + d->updateFiltersWarnings(); + d->SearchMenuButton->setIcon(QIcon(":/Icons/query_failed.svg")); + d->WarningPushButton->setText(tr("The patients query failed. Please check the servers settings.")); + d->WarningPushButton->show(); + } + else if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryStudies) + { + QString patientItem = d->findPatientItemFromPatientInfo(td.PatientID); + int patientIndex = d->findPatientTabIndexFromPatientItem(patientItem); + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->widget(patientIndex)); + if (patientItemWidget) + { + patientItemWidget->setOperationStatus(ctkDICOMPatientItemWidget::Failed); + } + d->PatientsTabWidget->setTabIcon(patientIndex, QIcon(":/Icons/patient_failed.svg")); + } + } } //------------------------------------------------------------------------------ -void ctkDICOMVisualBrowserWidget::onJobFailed(const QVariant& data) +void ctkDICOMVisualBrowserWidget::onJobFinished(QList datas) { Q_D(ctkDICOMVisualBrowserWidget); - ctkDICOMJobDetail td = data.value(); - if (td.JobClass == "ctkDICOMQueryJob") + foreach (QVariant data, datas) { - d->updateFiltersWarnings(); - d->SearchMenuButton->setIcon(QIcon(":/Icons/query.svg")); - } + ctkDICOMJobDetail td = data.value(); + if (td.JobUID.isEmpty()) + { + continue; + } - QString job = td.JobClass.replace("ctkDICOM", "").replace("Job", ""); - d->WarningPushButton->setText(tr("%1 job failed." - "\nFor more information open the Jobs section. \n").arg(job)); - d->WarningPushButton->show(); + if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryPatients) + { + d->updateFiltersWarnings(); + d->SearchMenuButton->setIcon(QIcon(":/Icons/query_success.svg")); + } + else if (td.JobType == ctkDICOMJobResponseSet::JobType::QueryStudies) + { + QString patientItem = d->findPatientItemFromPatientInfo(td.PatientID); + int patientIndex = d->findPatientTabIndexFromPatientItem(patientItem); + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->widget(patientIndex)); + if (patientItemWidget) + { + patientItemWidget->setOperationStatus(ctkDICOMPatientItemWidget::Completed); + } + d->PatientsTabWidget->setTabIcon(patientIndex, QIcon(":/Icons/patient_success.svg")); + } + } } //------------------------------------------------------------------------------ @@ -2913,6 +3204,33 @@ void ctkDICOMVisualBrowserWidget::onPatientItemChanged(int index) patientItem->generateStudies(); } +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::onOperationStatusTabBarItemClicked(int index) +{ + Q_D(ctkDICOMVisualBrowserWidget); + ctkDICOMPatientItemWidget* patientItemWidget = + qobject_cast(d->PatientsTabWidget->widget(index)); + if (!patientItemWidget) + { + return; + } + + ctkDICOMPatientItemWidget::OperationStatus status = patientItemWidget->operationStatus(); + if (status == ctkDICOMPatientItemWidget::InProgress) + { + d->Scheduler->stopJobsByDICOMUIDs(QStringList(patientItemWidget->patientID())); + } + else if (status > ctkDICOMPatientItemWidget::InProgress) + { + ctkDICOMJobDetail queryJobDetail; + queryJobDetail.JobClass = "ctkDICOMQueryJob"; + queryJobDetail.DICOMLevel = ctkDICOMJob::DICOMLevels::Studies; + queryJobDetail.PatientID = patientItemWidget->patientID(); + + d->Scheduler->runJob(queryJobDetail, patientItemWidget->allowedServers()); + } +} + //------------------------------------------------------------------------------ void ctkDICOMVisualBrowserWidget::onServersSettingsChanged() { @@ -2931,6 +3249,20 @@ void ctkDICOMVisualBrowserWidget::onServersSettingsChanged() } } +//------------------------------------------------------------------------------ +void ctkDICOMVisualBrowserWidget::patientSelectedOnJobList(const QString& patientID, + const QString& patientName, + const QString& patientBirthDate) +{ + Q_D(ctkDICOMVisualBrowserWidget); + + QString patientItem = d->findPatientItemFromPatientInfo(patientID, patientName, patientBirthDate); + int selectedIndex = d->findPatientTabIndexFromPatientItem(patientItem); + int wasBlocking = d->PatientsTabWidget->blockSignals(true); + d->PatientsTabWidget->setCurrentIndex(selectedIndex); + d->PatientsTabWidget->blockSignals(wasBlocking); +} + //------------------------------------------------------------------------------ void ctkDICOMVisualBrowserWidget::showPatientContextMenu(const QPoint& point) { @@ -3139,7 +3471,7 @@ void ctkDICOMVisualBrowserWidget::showSeriesContextMenu(const QPoint& point) int row = selectedItem->row(); int column = selectedItem->column(); ctkDICOMSeriesItemWidget* seriesItemWidget = - qobject_cast(seriesListTableWidget->cellWidget(row, column)); + qobject_cast(seriesListTableWidget->cellWidget(row, column)); selectedWidgets.append(seriesItemWidget); } @@ -3516,7 +3848,6 @@ void ctkDICOMVisualBrowserWidget::onStop(bool stopPersistentTasks) d->Scheduler->stopAllJobs(stopPersistentTasks); d->updateFiltersWarnings(); d->ProgressFrame->hide(); - d->SearchMenuButton->setIcon(QIcon(":/Icons/query.svg")); QApplication::restoreOverrideCursor(); } diff --git a/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h index 5af478862b..4e6f32f4f2 100644 --- a/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h +++ b/Libs/DICOM/Widgets/ctkDICOMVisualBrowserWidget.h @@ -79,7 +79,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMVisualBrowserWidget : public QWidget Q_PROPERTY(QString databaseDirectoryBase READ databaseDirectoryBase WRITE setDatabaseDirectoryBase) Q_PROPERTY(QString filteringPatientID READ filteringPatientID WRITE setFilteringPatientID); Q_PROPERTY(QString filteringPatientName READ filteringPatientName WRITE setFilteringPatientName); - Q_PROPERTY(int numberOfStudiesPerPatient READ numberOfStudiesPerPatient WRITE setNumberOfStudiesPerPatient); + Q_PROPERTY(int numberOfOpenedStudiesPerPatient READ numberOfOpenedStudiesPerPatient WRITE setNumberOfOpenedStudiesPerPatient); Q_PROPERTY(ctkDICOMStudyItemWidget::ThumbnailSizeOption thumbnailSize READ thumbnailSize WRITE setThumbnailSize); Q_PROPERTY(ctkDICOMVisualBrowserWidget::ImportDirectoryMode ImportDirectoryMode READ importDirectoryMode WRITE setImportDirectoryMode) Q_PROPERTY(bool sendActionVisible READ isSendActionVisible WRITE setSendActionVisible) @@ -220,8 +220,8 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMVisualBrowserWidget : public QWidget ///@{ /// Number of non collapsed studies per patient /// 2 by default - void setNumberOfStudiesPerPatient(int numberOfStudiesPerPatient); - int numberOfStudiesPerPatient() const; + void setNumberOfOpenedStudiesPerPatient(int numberOfOpenedStudiesPerPatient); + int numberOfOpenedStudiesPerPatient() const; ///@} ///@{ @@ -239,7 +239,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMVisualBrowserWidget : public QWidget ///@} ///@{ - /// Set if cancel action on right click context menu is available + /// Set if delete action on right click context menu is available /// true by default void setDeleteActionVisible(bool visible); bool isDeleteActionVisible() const; @@ -368,8 +368,11 @@ public Q_SLOTS: ///@{ /// update GUI after query/retrieve operations - void updateGUIFromScheduler(const QVariant&); - void onJobFailed(const QVariant&); + void updateGUIFromScheduler(QList); + void onJobStarted(QList); + void onJobUserStopped(QList); + void onJobFailed(QList); + void onJobFinished(QList); ///@} /// stops all the operations @@ -385,9 +388,15 @@ public Q_SLOTS: /// user change patient selection void onPatientItemChanged(int); + /// user clicked on patient tab item + void onOperationStatusTabBarItemClicked(int); + /// server settings have been changed void onServersSettingsChanged(); + /// user selected a job in the job list widget + void patientSelectedOnJobList(const QString&, const QString&, const QString&); + Q_SIGNALS: /// Emitted when directory is changed void databaseDirectoryChanged(const QString&); diff --git a/Libs/Widgets/ctkFittedTextBrowser.cpp b/Libs/Widgets/ctkFittedTextBrowser.cpp index 1ecb6c165b..cabe3f84e4 100644 --- a/Libs/Widgets/ctkFittedTextBrowser.cpp +++ b/Libs/Widgets/ctkFittedTextBrowser.cpp @@ -36,6 +36,7 @@ ctkFittedTextBrowserPrivate::ctkFittedTextBrowserPrivate(ctkFittedTextBrowser& o :q_ptr(&object) { this->Collapsed = true; + this->DisableMouseScroll = false; this->CollapsibleTextSetter = ctkFittedTextBrowserPrivate::Text; this->ShowDetailsText = ctkFittedTextBrowser::tr("Show details..."); this->HideDetailsText = ctkFittedTextBrowser::tr("Hide details."); @@ -153,7 +154,6 @@ ctkFittedTextBrowser::ctkFittedTextBrowser(QWidget* _parent) //----------------------------------------------------------------------------- ctkFittedTextBrowser::~ctkFittedTextBrowser() { - } //----------------------------------------------------------------------------- @@ -223,6 +223,15 @@ void ctkFittedTextBrowser::resizeEvent(QResizeEvent* e) } } +//----------------------------------------------------------------------------- +void ctkFittedTextBrowser::wheelEvent(QWheelEvent *event) +{ + if (!this->disableMouseScroll()) + { + this->QTextBrowser::wheelEvent(event); + } +} + //----------------------------------------------------------------------------- void ctkFittedTextBrowser::setCollapsibleText(const QString &text) { @@ -294,6 +303,20 @@ bool ctkFittedTextBrowser::collapsed() const return d->Collapsed; } +//----------------------------------------------------------------------------- +void ctkFittedTextBrowser::setDisableMouseScroll(bool disableMouseScroll) +{ + Q_D(ctkFittedTextBrowser); + d->DisableMouseScroll = disableMouseScroll; +} + +//----------------------------------------------------------------------------- +bool ctkFittedTextBrowser::disableMouseScroll() const +{ + Q_D(const ctkFittedTextBrowser); + return d->DisableMouseScroll; +} + //----------------------------------------------------------------------------- void ctkFittedTextBrowser::setShowDetailsText(const QString &text) { diff --git a/Libs/Widgets/ctkFittedTextBrowser.h b/Libs/Widgets/ctkFittedTextBrowser.h index 785eb81f29..9c024d530e 100644 --- a/Libs/Widgets/ctkFittedTextBrowser.h +++ b/Libs/Widgets/ctkFittedTextBrowser.h @@ -42,15 +42,21 @@ class ctkFittedTextBrowserPrivate; class CTK_WIDGETS_EXPORT ctkFittedTextBrowser : public QTextBrowser { Q_OBJECT + Q_PROPERTY(bool disableMouseScroll READ disableMouseScroll WRITE setDisableMouseScroll) Q_PROPERTY(bool collapsed READ collapsed WRITE setCollapsed) Q_PROPERTY(QString showDetailsText READ showDetailsText WRITE setShowDetailsText) Q_PROPERTY(QString hideDetailsText READ hideDetailsText WRITE setHideDetailsText) - public: ctkFittedTextBrowser(QWidget* parent = 0); virtual ~ctkFittedTextBrowser(); + /// Disable mouse scroll is false by default. + /// If set to true, the mouse wheel event is ignored. + void setDisableMouseScroll(bool disableMouseScroll); + /// return if mouse scroll is disabled + bool disableMouseScroll() const; + /// Show only first line/the full text. /// Only has effect if collapsible = true. void setCollapsed(bool collapsed); @@ -73,11 +79,11 @@ class CTK_WIDGETS_EXPORT ctkFittedTextBrowser : public QTextBrowser Q_INVOKABLE QString collapsibleText() const; /// Reimplemented for internal reasons - virtual QSize sizeHint() const; + virtual QSize sizeHint() const override; /// Reimplemented for internal reasons - virtual QSize minimumSizeHint() const; + virtual QSize minimumSizeHint() const override; /// Reimplemented for internal reasons - virtual int heightForWidth(int width) const; + virtual int heightForWidth(int width) const override; public Q_SLOTS: @@ -111,7 +117,8 @@ protected Q_SLOTS: protected: QScopedPointer d_ptr; - virtual void resizeEvent(QResizeEvent* e); + virtual void resizeEvent(QResizeEvent* event) override; + void wheelEvent(QWheelEvent *event) override; private: Q_DECLARE_PRIVATE(ctkFittedTextBrowser); diff --git a/Libs/Widgets/ctkFittedTextBrowser_p.h b/Libs/Widgets/ctkFittedTextBrowser_p.h index e11b7ceccf..c90f3bb7f6 100644 --- a/Libs/Widgets/ctkFittedTextBrowser_p.h +++ b/Libs/Widgets/ctkFittedTextBrowser_p.h @@ -49,6 +49,7 @@ class CTK_WIDGETS_EXPORT ctkFittedTextBrowserPrivate QString collapseLinkText() const; bool Collapsed; + bool DisableMouseScroll; QString ShowDetailsText; QString HideDetailsText; diff --git a/Libs/Widgets/ctkThumbnailLabel.cpp b/Libs/Widgets/ctkThumbnailLabel.cpp index af993299b6..c11a805f50 100644 --- a/Libs/Widgets/ctkThumbnailLabel.cpp +++ b/Libs/Widgets/ctkThumbnailLabel.cpp @@ -55,6 +55,7 @@ class ctkThumbnailLabelPrivate: public Ui_ctkThumbnailLabel QModelIndex SourceIndex; QPixmap OriginalThumbnail; Qt::TransformationMode TransformationMode; + ctkThumbnailLabel::OperationStatus Status; // Redraw thumbnail void updateThumbnail(); @@ -73,6 +74,7 @@ ctkThumbnailLabelPrivate::ctkThumbnailLabelPrivate(ctkThumbnailLabel* parent) this->SelectedColor = q->palette().color(QPalette::Highlight); this->TextPosition = Qt::AlignTop | Qt::AlignHCenter; this->TransformationMode = Qt::FastTransformation; + this->Status = ctkThumbnailLabel::OperationStatus::NoOperation; } //---------------------------------------------------------------------------- @@ -89,6 +91,9 @@ void ctkThumbnailLabelPrivate::setupUi(QWidget* widget) // no text by default q->setText(QString()); this->OperationProgressBar->hide(); + + QObject::connect(this->TextPushButton, SIGNAL(clicked(bool)), + q, SIGNAL(statusPushButtonClicked(bool))); } //---------------------------------------------------------------------------- @@ -185,11 +190,9 @@ QProgressBar *ctkThumbnailLabel::operationProgressBar() void ctkThumbnailLabel::setText(const QString &text) { Q_D(ctkThumbnailLabel); - d->TextPushButton->setText(text); - d->TextPushButton->setVisible(!text.isEmpty() && - ! (d->TextPosition & Qt::AlignHCenter && - d->TextPosition & Qt::AlignVCenter) ); + d->TextPushButton->setVisible((!d->TextPushButton->text().isEmpty() || !d->TextPushButton->icon().isNull()) && + ! (d->TextPosition & Qt::AlignHCenter && d->TextPosition & Qt::AlignVCenter)); } //---------------------------------------------------------------------------- @@ -261,20 +264,33 @@ Qt::Alignment ctkThumbnailLabel::textPosition()const } //---------------------------------------------------------------------------- -void ctkThumbnailLabel::setPixmap(const QPixmap &pixmap) +void ctkThumbnailLabel::setOperationStatus(const OperationStatus &status) { Q_D(ctkThumbnailLabel); - - d->OriginalThumbnail = pixmap; - d->updateThumbnail(); + d->Status = status; } //---------------------------------------------------------------------------- -const QPixmap* ctkThumbnailLabel::pixmap()const +ctkThumbnailLabel::OperationStatus ctkThumbnailLabel::operationStatus() const { Q_D(const ctkThumbnailLabel); + return d->Status; +} - return d->OriginalThumbnail.isNull() ? 0 : &(d->OriginalThumbnail); +//---------------------------------------------------------------------------- +void ctkThumbnailLabel::setStatusIcon(const QIcon &icon) +{ + Q_D(ctkThumbnailLabel); + d->TextPushButton->setIcon(icon); + d->TextPushButton->setVisible((!d->TextPushButton->text().isEmpty() || !d->TextPushButton->icon().isNull()) && + ! (d->TextPosition & Qt::AlignHCenter && d->TextPosition & Qt::AlignVCenter)); +} + +//---------------------------------------------------------------------------- +QIcon ctkThumbnailLabel::statusIcon() const +{ + Q_D(const ctkThumbnailLabel); + return d->TextPushButton->icon(); } //---------------------------------------------------------------------------- @@ -291,6 +307,23 @@ void ctkThumbnailLabel::setOperationProgress(const int &progress) d->OperationProgressBar->setValue(progress); } +//---------------------------------------------------------------------------- +void ctkThumbnailLabel::setPixmap(const QPixmap &pixmap) +{ + Q_D(ctkThumbnailLabel); + + d->OriginalThumbnail = pixmap; + d->updateThumbnail(); +} + +//---------------------------------------------------------------------------- +const QPixmap* ctkThumbnailLabel::pixmap()const +{ + Q_D(const ctkThumbnailLabel); + + return d->OriginalThumbnail.isNull() ? 0 : &(d->OriginalThumbnail); +} + //---------------------------------------------------------------------------- Qt::TransformationMode ctkThumbnailLabel::transformationMode()const { diff --git a/Libs/Widgets/ctkThumbnailLabel.h b/Libs/Widgets/ctkThumbnailLabel.h index 584188c8c3..94154adb39 100644 --- a/Libs/Widgets/ctkThumbnailLabel.h +++ b/Libs/Widgets/ctkThumbnailLabel.h @@ -22,6 +22,7 @@ #define __ctkThumbnailLabel_h // Qt includes +#include #include #include @@ -56,6 +57,10 @@ class CTK_WIDGETS_EXPORT ctkThumbnailLabel : public QWidget Q_PROPERTY(QPixmap pixmap READ pixmap WRITE setPixmap) /// Progress bar status. Q_PROPERTY(int operationProgress READ operationProgress WRITE setOperationProgress) + /// Operation status. + Q_PROPERTY(OperationStatus operationStatus READ operationStatus WRITE setOperationStatus) + /// Set status icon. + Q_PROPERTY(QIcon statusIcon READ statusIcon WRITE setStatusIcon) /// Controls the quality of the resizing of the pixmap. /// Qt::FastTransformation by default Q_PROPERTY(Qt::TransformationMode transformationMode READ transformationMode WRITE setTransformationMode) @@ -82,12 +87,25 @@ class CTK_WIDGETS_EXPORT ctkThumbnailLabel : public QWidget void setTextPosition(const Qt::Alignment& alignment); Qt::Alignment textPosition()const; - void setPixmap(const QPixmap& pixmap); - const QPixmap* pixmap()const; + enum OperationStatus{ + NoOperation = 0, + InProgress = 1, + Completed = 2, + Failed = 3 + }; + + void setOperationStatus(const OperationStatus& status); + OperationStatus operationStatus()const; + + void setStatusIcon(const QIcon& icon); + QIcon statusIcon()const; void setOperationProgress(const int& progress); int operationProgress()const; + void setPixmap(const QPixmap& pixmap); + const QPixmap* pixmap()const; + Qt::TransformationMode transformationMode()const; void setTransformationMode(Qt::TransformationMode mode); @@ -117,6 +135,7 @@ class CTK_WIDGETS_EXPORT ctkThumbnailLabel : public QWidget Q_SIGNALS: void selected(const ctkThumbnailLabel& widget); void doubleClicked(const ctkThumbnailLabel& widget); + void statusPushButtonClicked(bool); }; #endif