// vim: set tabstop=4 shiftwidth=4 noexpandtab /* Gwenview - A simple image viewer for KDE Copyright 2000-2004 Aur�lien G�teau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "imageloader.h" #include // TQt #include #include // KDE #include #include #include // Local #include "cache.h" #include "miscconfig.h" #include "imageutils/imageutils.h" #include "imageutils/jpegcontent.h" #include "imageloader.moc" namespace Gwenview { const unsigned int DECODE_CHUNK_SIZE=4096; /** Interval between image updates, in milli seconds */ const int IMAGE_UPDATE_INTERVAL=100; #undef ENABLE_LOG #undef LOG #undef LOG2 #define ENABLE_LOG 0 #if ENABLE_LOG >= 1 #define LOG(x) kdDebug() << k_funcinfo << x << endl #else #define LOG(x) ; #endif #if ENABLE_LOG >= 2 #define LOG2(x) kdDebug() << k_funcinfo << x << endl #else #define LOG2(x) ; #endif static TQMap< KURL, ImageLoader* > sLoaders; //--------------------------------------------------------------------- // // CancellableBuffer // This class acts like TQBuffer, but will simulates a truncated file if the // TSThread which was passed to its constructor has been asked for cancellation // //--------------------------------------------------------------------- class CancellableBuffer : public TQBuffer { public: CancellableBuffer(TQByteArray buffer, TSThread* thread) : TQBuffer(buffer), mThread(thread) {} bool atEnd() const { if (mThread->testCancel()) { LOG("cancel detected"); return true; } return TQBuffer::atEnd(); } TQ_LONG readBlock(char * data, TQ_ULONG maxlen) { if (mThread->testCancel()) { LOG("cancel detected"); return 0; } return TQBuffer::readBlock(data, maxlen); } TQ_LONG readLine(char * data, TQ_ULONG maxlen) { if (mThread->testCancel()) { LOG("cancel detected"); return 0; } return TQBuffer::readLine(data, maxlen); } TQByteArray readAll() { if (mThread->testCancel()) { LOG("cancel detected"); return TQByteArray(); } return TQBuffer::readAll(); } int getch() { if (mThread->testCancel()) { LOG("cancel detected"); setqStatus(IO_ReadError); return -1; } return TQBuffer::getch(); } private: TSThread* mThread; }; //--------------------------------------------------------------------- // // DecoderThread // //--------------------------------------------------------------------- void DecoderThread::run() { TQMutexLocker locker(&mMutex); LOG(""); // This block makes sure imageIO won't access the image after the signal // has been posted { TQImageIO imageIO; CancellableBuffer buffer(mRawData, this); buffer.open(IO_ReadOnly); imageIO.setIODevice(TQT_TQIODEVICE(&buffer)); bool ok=imageIO.read(); if (testCancel()) { LOG("cancelled"); return; } if (!ok) { LOG("failed"); postSignal( this, TQT_SIGNAL(failed()) ); return; } LOG("succeeded"); mImage=imageIO.image(); } LOG("succeeded, emitting signal"); postSignal( this, TQT_SIGNAL(succeeded()) ); } void DecoderThread::setRawData(const TQByteArray& data) { TQMutexLocker locker(&mMutex); mRawData=data.copy(); } TQImage DecoderThread::popLoadedImage() { TQMutexLocker locker(&mMutex); TQImage img=mImage; mImage=TQImage(); return img; } //--------------------------------------------------------------------- // // ImageLoaderPrivate // //--------------------------------------------------------------------- struct OwnerData { const TQObject* owner; BusyLevel priority; }; enum GetState { GET_PENDING_STAT, // Stat has not been started GET_STATING, // Stat has been started GET_PENDING_GET, // Stat is done, get has not been started GET_GETTING, // Get has been started GET_DONE, // All data has been received }; enum DecodeState { DECODE_WAITING, // No data to decode yet DECODE_PENDING_THREADED_DECODING, // Waiting for all data to start threaded decoding DECODE_THREADED_DECODING, // Threaded decoder is running DECODE_INCREMENTAL_DECODING, // Incremental decoder is running DECODE_INCREMENTAL_DECODING_DONE, // Incremental decoder is done DECODE_CACHED, // Image has been obtained from cache, but raw data was missing. Wait for get to finish. DECODE_DONE, // All done }; class ImageLoaderPrivate { public: ImageLoaderPrivate(ImageLoader* impl) : mDecodedSize(0) , mGetState(GET_PENDING_STAT) , mDecodeState(DECODE_WAITING) , mDecoder(impl) , mSuspended(false) , mNextFrameDelay(0) , mWasFrameData(false) , mOrientation(ImageUtils::NOT_AVAILABLE) , mURLKind(MimeTypeUtils::KIND_UNKNOWN) {} // How many of the raw data we have already decoded unsigned int mDecodedSize; GetState mGetState; DecodeState mDecodeState; KURL mURL; // The file timestamp TQDateTime mTimestamp; // The raw data we get TQByteArray mRawData; // The async decoder and it's waking timer TQImageDecoder mDecoder; TQTimer mDecoderTimer; // The decoder thread DecoderThread mDecoderThread; // A rect of recently loaded pixels that the rest of the application has // not been notified about with the imageChanged() signal TQRect mLoadChangedRect; // The time since we last emitted the imageChanged() signal TQTime mTimeSinceLastUpdate; // Whether the loading should be suspended bool mSuspended; // Delay used for next frame after it's finished decoding. int mNextFrameDelay; bool mWasFrameData; TQImage mProcessedImage; // image frame currently being decoded TQRegion mLoadedRegion; // loaded parts of mProcessedImage ImageFrames mFrames; TQCString mImageFormat; ImageUtils::Orientation mOrientation; TQString mMimeType; MimeTypeUtils::Kind mURLKind; TQValueVector< OwnerData > mOwners; // loaders may be shared void determineImageFormat() { Q_ASSERT(mRawData.size()>0); TQBuffer buffer(mRawData); buffer.open(IO_ReadOnly); mImageFormat = TQImageIO::imageFormat(TQT_TQIODEVICE(&buffer)); } }; //--------------------------------------------------------------------- // // ImageLoader // //--------------------------------------------------------------------- ImageLoader::ImageLoader() { LOG(""); d = new ImageLoaderPrivate(this); connect( BusyLevelManager::instance(), TQT_SIGNAL( busyLevelChanged(BusyLevel)), this, TQT_SLOT( slotBusyLevelChanged(BusyLevel))); } ImageLoader::~ImageLoader() { LOG(""); if (d->mDecoderThread.running()) { d->mDecoderThread.cancel(); d->mDecoderThread.wait(); } delete d; } void ImageLoader::setURL( const KURL& url ) { assert( d->mURL.isEmpty()); d->mURL = url; } void ImageLoader::startLoading() { d->mTimestamp = Cache::instance()->timestamp( d->mURL ); slotBusyLevelChanged( BusyLevelManager::instance()->busyLevel()); connect(&d->mDecoderTimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(decodeChunk()) ); connect(&d->mDecoderThread, TQT_SIGNAL(succeeded()), this, TQT_SLOT(slotDecoderThreadSucceeded()) ); connect(&d->mDecoderThread, TQT_SIGNAL(failed()), this, TQT_SLOT(slotDecoderThreadFailed()) ); checkPendingStat(); } void ImageLoader::checkPendingStat() { if( d->mSuspended || d->mGetState != GET_PENDING_STAT ) return; KIO::Job* job=KIO::stat( d->mURL, false ); job->setWindow(KApplication::kApplication()->mainWidget()); connect(job, TQT_SIGNAL(result(KIO::Job*)), this, TQT_SLOT(slotStatResult(KIO::Job*)) ); d->mGetState = GET_STATING; } void ImageLoader::slotStatResult(KIO::Job* job) { LOG("error code: " << job->error()); // Get modification time of the original file KIO::UDSEntry entry = static_cast(job)->statResult(); KIO::UDSEntry::ConstIterator it= entry.begin(); TQDateTime urlTimestamp; for (; it!=entry.end(); it++) { if ((*it).m_uds == KIO::UDS_MODIFICATION_TIME) { urlTimestamp.setTime_t( (*it).m_long ); break; } } if( d->mTimestamp.isValid() && urlTimestamp == d->mTimestamp ) { // We have the image in cache LOG(d->mURL << ", We have the image in cache"); d->mRawData = Cache::instance()->file( d->mURL ); Cache::instance()->getFrames(d->mURL, &d->mFrames, &d->mImageFormat); if( !d->mFrames.isEmpty()) { LOG("The image in cache can be used"); d->mProcessedImage = d->mFrames[0].image; emit sizeLoaded(d->mProcessedImage.width(), d->mProcessedImage.height()); emit imageChanged(d->mProcessedImage.rect()); if (d->mRawData.isNull() && d->mImageFormat=="JPEG") { // Raw data is needed for JPEG, wait for it to be downloaded LOG("Wait for raw data to be downloaded"); d->mDecodeState = DECODE_CACHED; } else { // We don't care about raw data finish(true); return; } } else { // Image in cache is broken LOG("The image in cache cannot be used"); if( !d->mRawData.isNull()) { LOG("Using cached raw data"); // Raw data is ok, skip get step and decode it d->mGetState = GET_DONE; d->mTimeSinceLastUpdate.start(); d->mDecoderTimer.start(0, false); return; } } } d->mTimestamp = urlTimestamp; d->mRawData.resize(0); d->mGetState = GET_PENDING_GET; checkPendingGet(); } void ImageLoader::checkPendingGet() { if( d->mSuspended || d->mGetState != GET_PENDING_GET ) return; // Start loading the image KIO::Job* getJob=KIO::get( d->mURL, false, false); getJob->setWindow(KApplication::kApplication()->mainWidget()); connect(getJob, TQT_SIGNAL(data(KIO::Job*, const TQByteArray&)), this, TQT_SLOT(slotDataReceived(KIO::Job*, const TQByteArray&)) ); connect(getJob, TQT_SIGNAL(result(KIO::Job*)), this, TQT_SLOT(slotGetResult(KIO::Job*)) ); d->mTimeSinceLastUpdate.start(); d->mGetState = GET_GETTING; } void ImageLoader::slotGetResult(KIO::Job* job) { LOG("error code: " << job->error()); if( job->error() != 0 ) { // failed finish( false ); return; } d->mGetState = GET_DONE; // Store raw data in cache // Note: Cache will give high cost to non-JPEG raw data. Cache::instance()->addFile( d->mURL, d->mRawData, d->mTimestamp ); switch (d->mDecodeState) { case DECODE_CACHED: // image was in cache, but not raw data finish( true ); break; case DECODE_PENDING_THREADED_DECODING: // Start the decoder thread if needed startThread(); break; default: // Finish decoding if needed if (!d->mDecoderTimer.isActive()) d->mDecoderTimer.start(0); } } // There is no way in KImageIO to get the mimeType from the image format. // This function assumes KImageIO::types and KImageIO::mimeTypes return items // in the same order (which they do, according to the source code). static TQString mimeTypeFromFormat(const char* format) { TQStringList formats = KImageIO::types(KImageIO::Reading); TQStringList mimeTypes = KImageIO::mimeTypes(KImageIO::Reading); int pos = formats.findIndex(TQString::fromAscii(format)); Q_ASSERT(pos != -1); return mimeTypes[pos]; } void ImageLoader::slotDataReceived(KIO::Job* job, const TQByteArray& chunk) { LOG2("size: " << chunk.size()); if (chunk.size()<=0) return; int oldSize=d->mRawData.size(); d->mRawData.resize(oldSize + chunk.size()); memcpy(d->mRawData.data()+oldSize, chunk.data(), chunk.size() ); if (oldSize==0) { // Try to determine the data type TQBuffer buffer(d->mRawData); buffer.open(IO_ReadOnly); const char* format = TQImageIO::imageFormat(TQT_TQIODEVICE(&buffer)); if (format) { // This is a raster image, get the mime type now d->mURLKind = MimeTypeUtils::KIND_RASTER_IMAGE; d->mMimeType = mimeTypeFromFormat(format); } else { KMimeType::Ptr ptr = KMimeType::findByContent(d->mRawData); d->mMimeType = ptr->name(); d->mURLKind = MimeTypeUtils::mimeTypeKind(d->mMimeType); } if (d->mURLKind!=MimeTypeUtils::KIND_RASTER_IMAGE) { Q_ASSERT(!d->mDecoderTimer.isActive()); job->kill(true /* quietly */); LOG("emit urlKindDetermined(!raster)"); emit urlKindDetermined(); return; } LOG("emit urlKindDetermined(raster)"); emit urlKindDetermined(); } // Decode the received data if( !d->mDecoderTimer.isActive() && (d->mDecodeState==DECODE_WAITING || d->mDecodeState==DECODE_INCREMENTAL_DECODING) ) { d->mDecoderTimer.start(0); } } void ImageLoader::decodeChunk() { if( d->mSuspended ) { LOG("suspended"); d->mDecoderTimer.stop(); return; } int chunkSize = TQMIN(DECODE_CHUNK_SIZE, int(d->mRawData.size())-d->mDecodedSize); int decodedSize = 0; if (chunkSize>0) { decodedSize = d->mDecoder.decode( (const uchar*)(d->mRawData.data()+d->mDecodedSize), chunkSize); if (decodedSize<0) { // We can't use incremental decoding, switch to threaded decoding d->mDecoderTimer.stop(); if (d->mGetState == GET_DONE) { startThread(); } else { d->mDecodeState = DECODE_PENDING_THREADED_DECODING; } return; } // We just decoded some data if (d->mDecodeState == DECODE_WAITING) { d->mDecodeState = DECODE_INCREMENTAL_DECODING; } d->mDecodedSize+=decodedSize; } if (decodedSize == 0) { // We decoded as much as possible from the buffer, wait to receive // more data before coming again in decodeChunk d->mDecoderTimer.stop(); if (d->mGetState == GET_DONE) { // All available data has been received. if (d->mDecodeState == DECODE_INCREMENTAL_DECODING) { // Decoder is not finished, the image must be truncated, // let's simulate its end kdWarning() << "ImageLoader::decodeChunk(): image '" << d->mURL.prettyURL() << "' is truncated.\n"; if (d->mProcessedImage.isNull()) { d->mProcessedImage = d->mDecoder.image(); } emit imageChanged(d->mProcessedImage.rect()); end(); } } } } void ImageLoader::startThread() { LOG("starting decoder thread"); d->mDecodeState = DECODE_THREADED_DECODING; d->mDecoderThread.setRawData(d->mRawData); d->mDecoderThread.start(); } void ImageLoader::slotDecoderThreadFailed() { LOG(""); // Image can't be loaded finish( false ); } void ImageLoader::slotDecoderThreadSucceeded() { LOG(""); d->mProcessedImage = d->mDecoderThread.popLoadedImage(); d->mFrames.append( ImageFrame( d->mProcessedImage, 0 )); emit sizeLoaded(d->mProcessedImage.width(), d->mProcessedImage.height()); emit imageChanged(d->mProcessedImage.rect()); finish(true); } /** * Cache image and emit imageLoaded */ void ImageLoader::finish( bool ok ) { LOG(""); d->mDecodeState = DECODE_DONE; if (!ok) { d->mFrames.clear(); d->mRawData = TQByteArray(); d->mImageFormat = TQCString(); d->mProcessedImage = TQImage(); emit imageLoaded( false ); return; } if (d->mImageFormat.isEmpty()) { d->determineImageFormat(); } Q_ASSERT(d->mFrames.count() > 0); Cache::instance()->addImage( d->mURL, d->mFrames, d->mImageFormat, d->mTimestamp ); emit imageLoaded( true ); } BusyLevel ImageLoader::priority() const { BusyLevel mylevel = BUSY_NONE; for( TQValueVector< OwnerData >::ConstIterator it = d->mOwners.begin(); it != d->mOwners.end(); ++it ) { mylevel = TQMAX( mylevel, (*it).priority ); } return mylevel; } void ImageLoader::slotBusyLevelChanged( BusyLevel level ) { // this loader may be needed for normal loading (BUSY_LOADING), or // only for prefetching BusyLevel mylevel = priority(); if( level > mylevel ) { suspendLoading(); } else { resumeLoading(); } } void ImageLoader::suspendLoading() { d->mDecoderTimer.stop(); d->mSuspended = true; } void ImageLoader::resumeLoading() { d->mSuspended = false; d->mDecoderTimer.start(0, false); checkPendingGet(); checkPendingStat(); } //--------------------------------------------------------------------- // // TQImageConsumer // //--------------------------------------------------------------------- void ImageLoader::end() { LOG(""); // Notify about the last loaded rectangle LOG("mLoadChangedRect " << d->mLoadChangedRect); if (!d->mLoadChangedRect.isEmpty()) { emit imageChanged( d->mLoadChangedRect ); } d->mDecoderTimer.stop(); d->mDecodeState = DECODE_INCREMENTAL_DECODING_DONE; // We are done if( d->mFrames.count() == 0 ) { d->mFrames.append( ImageFrame( d->mProcessedImage, 0 )); } // The image has been totally decoded, we delay the call to finish because // when we return from this function we will be in decodeChunk(), after the // call to decode(), so we don't want to switch to a new impl yet, since // this means deleting "this". TQTimer::singleShot(0, this, TQT_SLOT(callFinish()) ); } void ImageLoader::callFinish() { finish(true); } void ImageLoader::changed(const TQRect& constRect) { LOG2(""); TQRect rect = constRect; if (d->mLoadedRegion.isEmpty()) { // This is the first time we get called. Init mProcessedImage and emit // sizeLoaded. LOG("mLoadedRegion is empty"); // By default, mProcessedImage should use the image from mDecoder d->mProcessedImage = d->mDecoder.image(); if (d->mImageFormat.isEmpty()) { d->determineImageFormat(); } Q_ASSERT(!d->mImageFormat.isEmpty()); if (d->mImageFormat == "JPEG") { // This is a JPEG, extract orientation and adjust mProcessedImage // if necessary according to misc options ImageUtils::JPEGContent content; if (content.loadFromData(d->mRawData)) { d->mOrientation = content.orientation(); if (MiscConfig::autoRotateImages() && d->mOrientation != ImageUtils::NOT_AVAILABLE && d->mOrientation != ImageUtils::NORMAL) { TQSize size = content.size(); d->mProcessedImage = TQImage(size, d->mDecoder.image().depth()); } d->mProcessedImage.setDotsPerMeterX(content.dotsPerMeterX()); d->mProcessedImage.setDotsPerMeterY(content.dotsPerMeterY()); } else { kdWarning() << "ImageLoader::changed(): JPEGContent could not load '" << d->mURL.prettyURL() << "'\n"; } } LOG("emit sizeLoaded " << d->mProcessedImage.size()); emit sizeLoaded(d->mProcessedImage.width(), d->mProcessedImage.height()); } // Apply orientation if necessary and if wanted by user settings (misc options) if (MiscConfig::autoRotateImages() && d->mOrientation != ImageUtils::NOT_AVAILABLE && d->mOrientation != ImageUtils::NORMAL) { // We can only rotate whole images, so copy the loaded rect in a temp // image, rotate the temp image and copy it to mProcessedImage // Copy loaded rect TQImage temp(rect.size(), d->mProcessedImage.depth()); bitBlt(&temp, 0, 0, &d->mDecoder.image(), rect.left(), rect.top(), rect.width(), rect.height()); // Rotate temp = ImageUtils::transform(temp, d->mOrientation); // Compute destination rect TQWMatrix matrix = ImageUtils::transformMatrix(d->mOrientation); TQRect imageRect = d->mDecoder.image().rect(); imageRect = matrix.mapRect(imageRect); rect = matrix.mapRect(rect); rect.moveBy(-imageRect.left(), -imageRect.top()); // copy temp to mProcessedImage bitBlt(&d->mProcessedImage, rect.left(), rect.top(), &temp, 0, 0, temp.width(), temp.height()); } // Update state tracking vars d->mWasFrameData = true; d->mLoadChangedRect |= rect; d->mLoadedRegion |= rect; if( d->mTimeSinceLastUpdate.elapsed() > IMAGE_UPDATE_INTERVAL ) { LOG("emitting imageChanged " << d->mLoadChangedRect); d->mTimeSinceLastUpdate.start(); emit imageChanged(d->mLoadChangedRect); d->mLoadChangedRect = TQRect(); } } void ImageLoader::frameDone() { frameDone( TQPoint( 0, 0 ), d->mDecoder.image().rect()); } void ImageLoader::frameDone(const TQPoint& offset, const TQRect& rect) { LOG(""); // Another case where the image loading in TQt's is a bit borken. // It's possible to get several notes about a frame being done for one frame (with MNG). if( !d->mWasFrameData ) { // To make it even more fun, with MNG the sequence is actually // setFramePeriod( 0 ) // frameDone() // setFramePeriod( delay ) // frameDone() // Therefore ignore the second frameDone(), but fix the delay that should be // after the frame. if( d->mFrames.count() > 0 ) { d->mFrames.last().delay = d->mNextFrameDelay; d->mNextFrameDelay = 0; } return; } d->mWasFrameData = false; if( !d->mLoadChangedRect.isEmpty()) { emit imageChanged(d->mLoadChangedRect); d->mLoadChangedRect = TQRect(); d->mTimeSinceLastUpdate.start(); } d->mLoadedRegion = TQRegion(); TQImage image; if (d->mProcessedImage.isNull()) { image = d->mDecoder.image().copy(); } else { image = d->mProcessedImage.copy(); } if( offset != TQPoint( 0, 0 ) || rect != image.rect()) { // Blit last frame below 'image' if( !d->mFrames.isEmpty()) { TQImage im = d->mFrames.last().image.copy(); bitBlt( &im, offset.x(), offset.y(), &image, rect.x(), rect.y(), rect.width(), rect.height()); image = im; } } d->mFrames.append( ImageFrame( image, d->mNextFrameDelay )); d->mNextFrameDelay = 0; } void ImageLoader::setLooping(int) { } void ImageLoader::setFramePeriod(int milliseconds) { if( milliseconds < 0 ) milliseconds = 0; // -1 means showing immediately if( d->mNextFrameDelay == 0 || milliseconds != 0 ) { d->mNextFrameDelay = milliseconds; } } void ImageLoader::setSize(int, int) { // Do nothing, size is handled when ::changed() is called for the first // time } TQImage ImageLoader::processedImage() const { return d->mProcessedImage; } ImageFrames ImageLoader::frames() const { return d->mFrames; } TQCString ImageLoader::imageFormat() const { return d->mImageFormat; } TQByteArray ImageLoader::rawData() const { return d->mRawData; } TQString ImageLoader::mimeType() const { return d->mMimeType; } MimeTypeUtils::Kind ImageLoader::urlKind() const { return d->mURLKind; } KURL ImageLoader::url() const { return d->mURL; } TQRegion ImageLoader::loadedRegion() const { return d->mLoadedRegion; } bool ImageLoader::completed() const { return d->mDecodeState == DECODE_DONE; } void ImageLoader::ref( const TQObject* owner, BusyLevel priority ) { OwnerData data; data.owner = owner; data.priority = priority; d->mOwners.append( data ); connect( owner, TQT_SIGNAL( destroyed()), TQT_SLOT( ownerDestroyed())); } void ImageLoader::deref( const TQObject* owner ) { for( TQValueVector< OwnerData >::Iterator it = d->mOwners.begin(); it != d->mOwners.end(); ++it ) { if( (*it).owner == owner ) { d->mOwners.erase( it ); if( d->mOwners.count() == 0 ) { sLoaders.remove( d->mURL ); delete this; } return; } } assert( false ); } void ImageLoader::release( const TQObject* owner ) { disconnect( owner ); deref( owner ); } void ImageLoader::ownerDestroyed() { deref( TQT_TQOBJECT(const_cast(sender()))); } //--------------------------------------------------------------------- // // Managing loaders // //--------------------------------------------------------------------- ImageLoader* ImageLoader::loader( const KURL& url, const TQObject* owner, BusyLevel priority ) { if( sLoaders.contains( url )) { ImageLoader* loader = sLoaders[ url ]; loader->ref( owner, priority ); // resume if this owner has high priority loader->slotBusyLevelChanged( BusyLevelManager::instance()->busyLevel()); return loader; } ImageLoader* loader = new ImageLoader; loader->ref( owner, priority ); sLoaders[ url ] = loader; loader->setURL( url ); // Code using a loader first calls loader() to get ImageLoader* and only after that it can // connect to its signals etc., so don't start loading immediately. // This also helps with preloading jobs, since BUSY_LOADING busy level is not entered immediately // when a new picture is selected, so preloading jobs without this delay could start working // immediately. TQTimer::singleShot( priority >= BUSY_LOADING ? 0 : 10, loader, TQT_SLOT( startLoading())); return loader; } } // namespace