diff options
Diffstat (limited to 'src/gvcore')
118 files changed, 22922 insertions, 0 deletions
diff --git a/src/gvcore/.vimrc b/src/gvcore/.vimrc new file mode 100644 index 0000000..a37475c --- /dev/null +++ b/src/gvcore/.vimrc @@ -0,0 +1,4 @@ +set tabstop=4 +set shiftwidth=4 +set noexpandtab +set makeprg=unsermake diff --git a/src/gvcore/Makefile.am b/src/gvcore/Makefile.am new file mode 100644 index 0000000..1bec46b --- /dev/null +++ b/src/gvcore/Makefile.am @@ -0,0 +1,85 @@ +AM_CPPFLAGS = -I$(srcdir)/.. $(all_includes) -D_LARGEFILE64_SOURCE + +lib_LTLIBRARIES = libgwenviewcore.la + +libgwenviewcore_la_LDFLAGS = $(all_libraries) -version-info 1:0:0 -no-undefined + +libgwenviewcore_la_LIBADD = \ + $(LIB_KFILE) $(LIB_KDEUI) $(LIB_KDECORE) $(LIB_KDEPRINT) $(LIB_QT) \ + $(LIBJPEG) $(LIBPNG) $(LIBMNG) $(GV_LIB_XCURSOR) $(LIB_EXIV2)\ + -lkmediaplayer ../imageutils/libgvimageutils.la \ + ../tsthread/libtsthread.la + +libgwenviewcore_la_METASOURCES = AUTO + +noinst_HEADERS = libgwenview_export.h + +# Be sure to keep pngformattype.cpp first, to avoid troubles with --enable-final +# See bug #134919 +libgwenviewcore_la_SOURCES = \ + pngformattype.cpp \ + printdialog.cpp \ + printdialogpagebase.ui \ + thumbnailloadjob.cpp \ + imageview.cpp \ + imageviewcontroller.cpp \ + document.cpp \ + externaltoolmanager.cpp \ + externaltoolcontext.cpp \ + externaltoolaction.cpp \ + externaltooldialogbase.ui \ + externaltooldialog.cpp \ + fileviewcontroller.cpp \ + filethumbnailview.cpp \ + fileoperation.cpp \ + fileopobject.cpp \ + filethumbnailviewitem.cpp \ + filterbar.ui \ + qxcfi.cpp \ + archive.cpp \ + slideshow.cpp \ + filedetailview.cpp \ + filedetailviewitem.cpp \ + imagesavedialog.cpp \ + jpegformattype.cpp \ + mngformattype.cpp \ + xpm.cpp \ + documentimpl.cpp \ + documentloadingimpl.cpp \ + documentloadedimpl.cpp \ + documentjpegloadedimpl.cpp \ + documentanimatedloadedimpl.cpp \ + documentotherloadedimpl.cpp \ + busylevelmanager.cpp \ + cache.cpp \ + threadgate.cpp \ + imageviewtools.cpp \ + fullscreenbar.cpp \ + imageloader.cpp \ + cursortracker.cpp \ + captionformatter.cpp \ + thumbnaildetailsdialogbase.ui \ + thumbnaildetailsdialog.cpp \ + xcursor.cpp \ + mimetypeutils.cpp \ + bcgdialog.cpp \ + bcgdialogbase.ui \ + timeutils.cpp \ + clicklineedit.cpp \ + inputdialog.cpp \ + deletedialog.cpp \ + deletedialogbase.ui \ + miscconfig.kcfgc \ + slideshowconfig.kcfgc \ + fileoperationconfig.kcfgc \ + fullscreenconfig.kcfgc \ + imageviewconfig.kcfgc \ + fileviewconfig.kcfgc + +kde_kcfg_DATA = \ + miscconfig.kcfg \ + slideshowconfig.kcfg \ + fileoperationconfig.kcfg \ + fullscreenconfig.kcfg \ + imageviewconfig.kcfg \ + fileviewconfig.kcfg diff --git a/src/gvcore/archive.cpp b/src/gvcore/archive.cpp new file mode 100644 index 0000000..d31d1a7 --- /dev/null +++ b/src/gvcore/archive.cpp @@ -0,0 +1,87 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +// KDE includes +#include <kfileitem.h> + +// Our includes +#include "archive.h" +namespace Gwenview { + + +namespace Archive { + +typedef QMap<QString,QString> MimeTypeProtocols; + +static const char* KDE_PROTOCOL = "X-KDE-LocalProtocol"; + +static const MimeTypeProtocols& mimeTypeProtocols() { + static MimeTypeProtocols map; + if (map.isEmpty()) { + KMimeType::List list = KMimeType::allMimeTypes(); + KMimeType::List::Iterator it=list.begin(), end=list.end(); + for (; it!=end; ++it) { + if ( (*it)->propertyNames().findIndex(KDE_PROTOCOL)!= -1 ) { + QString protocol = (*it)->property(KDE_PROTOCOL).toString(); + map[(*it)->name()] = protocol; + } + } + } + return map; +} + + +bool fileItemIsArchive(const KFileItem* item) { + return mimeTypeProtocols().contains(item->mimetype()); +} + +bool fileItemIsDirOrArchive(const KFileItem* item) { + return item->isDir() || Archive::fileItemIsArchive(item); +} + +bool protocolIsArchive(const QString& protocol) { + const MimeTypeProtocols& map=mimeTypeProtocols(); + MimeTypeProtocols::ConstIterator it; + for (it=map.begin();it!=map.end();++it) { + if (it.data()==protocol) return true; + } + return false; +} + +QStringList mimeTypes() { + const MimeTypeProtocols& map=mimeTypeProtocols(); + MimeTypeProtocols::ConstIterator it; + QStringList strlist; + for (it=map.begin();it!=map.end();++it) { + strlist+=it.key(); + } + return strlist; + //return mimeTypeProtocols().keys(); // keys() does not exist in Qt 3.0 +} + + +QString protocolForMimeType(const QString& mimeType) { + return mimeTypeProtocols()[mimeType]; +} + +} + +} // namespace diff --git a/src/gvcore/archive.h b/src/gvcore/archive.h new file mode 100644 index 0000000..cc98ced --- /dev/null +++ b/src/gvcore/archive.h @@ -0,0 +1,46 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#ifndef ARCHIVE_H +#define ARCHIVE_H + +// Qt includes +#include <qstringlist.h> +#include "libgwenview_export.h" +class KFileItem; +namespace Gwenview { + +/** + * Helper functions to deal with archives + */ +namespace Archive { + +LIBGWENVIEW_EXPORT bool fileItemIsArchive(const KFileItem*); +LIBGWENVIEW_EXPORT bool fileItemIsDirOrArchive(const KFileItem*); +bool protocolIsArchive(const QString&); +QStringList mimeTypes(); +QString protocolForMimeType(const QString&); + +} + +} // namespace +#endif + diff --git a/src/gvcore/bcgdialog.cpp b/src/gvcore/bcgdialog.cpp new file mode 100644 index 0000000..18b7822 --- /dev/null +++ b/src/gvcore/bcgdialog.cpp @@ -0,0 +1,84 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2006 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Self +#include "bcgdialog.moc" + +// Qt +#include <qslider.h> +#include <qvbox.h> + +// KDE +#include <klocale.h> + +// Local +#include "imageview.h" +#include "bcgdialogbase.h" + +namespace Gwenview { + + +struct BCGDialog::Private { + ImageView* mView; + BCGDialogBase* mContent; +}; + + +BCGDialog::BCGDialog(ImageView* view) +: KDialogBase(view, "bcg_dialog", false /* modal */, + i18n("Adjust Brightness/Contrast/Gamma"), KDialogBase::Close | KDialogBase::Default) +{ + d=new Private; + d->mView=view; + d->mContent=new BCGDialogBase(this); + setMainWidget(d->mContent); + connect(d->mContent->mBSlider, SIGNAL(valueChanged(int)), + view, SLOT(setBrightness(int)) ); + connect(d->mContent->mCSlider, SIGNAL(valueChanged(int)), + view, SLOT(setContrast(int)) ); + connect(d->mContent->mGSlider, SIGNAL(valueChanged(int)), + view, SLOT(setGamma(int)) ); + + connect(view, SIGNAL(bcgChanged()), + this, SLOT(updateFromImageView()) ); +} + + +BCGDialog::~BCGDialog() { + delete d; +} + + +void BCGDialog::slotDefault() { + d->mView->setBrightness(0); + d->mView->setContrast(0); + d->mView->setGamma(0); + updateFromImageView(); +} + + +void BCGDialog::updateFromImageView() { + d->mContent->mBSlider->setValue(d->mView->brightness()); + d->mContent->mCSlider->setValue(d->mView->contrast()); + d->mContent->mGSlider->setValue(d->mView->gamma()); +} + + +} // namespace diff --git a/src/gvcore/bcgdialog.h b/src/gvcore/bcgdialog.h new file mode 100644 index 0000000..4093320 --- /dev/null +++ b/src/gvcore/bcgdialog.h @@ -0,0 +1,53 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2006 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef BCGDIALOG_H +#define BCGDIALOG_H + +// KDE +#include <kdialogbase.h> + +class QShowEvent; + +namespace Gwenview { + +class ImageView; + +class BCGDialog : public KDialogBase { +Q_OBJECT +public: + BCGDialog(ImageView*); + ~BCGDialog(); + +protected: + virtual void slotDefault(); + +private slots: + void updateFromImageView(); + +private: + struct Private; + Private* d; +}; + + +} // namespace + +#endif /* BCGDIALOG_H */ diff --git a/src/gvcore/bcgdialogbase.ui b/src/gvcore/bcgdialogbase.ui new file mode 100644 index 0000000..81a79a3 --- /dev/null +++ b/src/gvcore/bcgdialogbase.ui @@ -0,0 +1,179 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>BCGDialogBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>BCGDialogBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>377</width> + <height>140</height> + </rect> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="QLabel" row="1" column="0"> + <property name="name"> + <cstring>textLabel2</cstring> + </property> + <property name="text"> + <string>&Contrast:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>mCSlider</cstring> + </property> + </widget> + <widget class="QLabel" row="2" column="0"> + <property name="name"> + <cstring>textLabel2_2</cstring> + </property> + <property name="text"> + <string>&Gamma:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>mGSlider</cstring> + </property> + </widget> + <widget class="QSlider" row="0" column="1"> + <property name="name"> + <cstring>mBSlider</cstring> + </property> + <property name="minValue"> + <number>-100</number> + </property> + <property name="maxValue"> + <number>100</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + </widget> + <widget class="QSlider" row="1" column="1"> + <property name="name"> + <cstring>mCSlider</cstring> + </property> + <property name="minValue"> + <number>-100</number> + </property> + <property name="maxValue"> + <number>100</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + </widget> + <widget class="QSlider" row="2" column="1"> + <property name="name"> + <cstring>mGSlider</cstring> + </property> + <property name="minValue"> + <number>-100</number> + </property> + <property name="maxValue"> + <number>100</number> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + </widget> + <widget class="QSpinBox" row="0" column="2"> + <property name="name"> + <cstring>mBSpinBox</cstring> + </property> + <property name="maxValue"> + <number>100</number> + </property> + <property name="minValue"> + <number>-100</number> + </property> + </widget> + <widget class="QSpinBox" row="1" column="2"> + <property name="name"> + <cstring>mCSpinBox</cstring> + </property> + <property name="maxValue"> + <number>100</number> + </property> + <property name="minValue"> + <number>-100</number> + </property> + </widget> + <widget class="QSpinBox" row="2" column="2"> + <property name="name"> + <cstring>mGSpinBox</cstring> + </property> + <property name="maxValue"> + <number>100</number> + </property> + <property name="minValue"> + <number>-100</number> + </property> + </widget> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>textLabel1</cstring> + </property> + <property name="text"> + <string>&Brightness:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>mBSlider</cstring> + </property> + </widget> + </grid> +</widget> +<connections> + <connection> + <sender>mBSlider</sender> + <signal>valueChanged(int)</signal> + <receiver>mBSpinBox</receiver> + <slot>setValue(int)</slot> + </connection> + <connection> + <sender>mCSlider</sender> + <signal>valueChanged(int)</signal> + <receiver>mCSpinBox</receiver> + <slot>setValue(int)</slot> + </connection> + <connection> + <sender>mGSlider</sender> + <signal>valueChanged(int)</signal> + <receiver>mGSpinBox</receiver> + <slot>setValue(int)</slot> + </connection> + <connection> + <sender>mBSpinBox</sender> + <signal>valueChanged(int)</signal> + <receiver>mBSlider</receiver> + <slot>setValue(int)</slot> + </connection> + <connection> + <sender>mCSpinBox</sender> + <signal>valueChanged(int)</signal> + <receiver>mCSlider</receiver> + <slot>setValue(int)</slot> + </connection> + <connection> + <sender>mGSpinBox</sender> + <signal>valueChanged(int)</signal> + <receiver>mGSlider</receiver> + <slot>setValue(int)</slot> + </connection> +</connections> +<tabstops> + <tabstop>mBSlider</tabstop> + <tabstop>mBSpinBox</tabstop> + <tabstop>mCSlider</tabstop> + <tabstop>mCSpinBox</tabstop> + <tabstop>mGSlider</tabstop> + <tabstop>mGSpinBox</tabstop> +</tabstops> +<layoutdefaults spacing="6" margin="11"/> +</UI> diff --git a/src/gvcore/busylevelmanager.cpp b/src/gvcore/busylevelmanager.cpp new file mode 100644 index 0000000..adfdf55 --- /dev/null +++ b/src/gvcore/busylevelmanager.cpp @@ -0,0 +1,108 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +// Qt +#include <qtimer.h> + +// KDE +#include <kdebug.h> + +// Local +#include "busylevelmanager.moc" +namespace Gwenview { + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + +BusyLevelManager::BusyLevelManager() +: mCurrentBusyLevel( BUSY_NONE ) +{ + connect( &mDelayedBusyLevelTimer, SIGNAL( timeout()), + this, SLOT( delayedBusyLevelChanged())); +} + +BusyLevelManager* BusyLevelManager::instance() { + static BusyLevelManager manager; + return &manager; +} + +// How the busy level stuff works: +// This system allows suspending less important tasks while more important +// task are active, i.e. no thumbnails are generated when the viewed +// image is being loaded and painted. +// All objects responsible for operations set their busy level +// to the matching value when the operation starts and reset their busy +// level when the operation is done. They all connect to busyLevelChanged() +// signal and suspend their operation if the current busy level is higher +// than the busy level of their operation. If a new operation is started, +// it needs to be immediatelly suspended if the current busy level is higher! +// Note that there can be only one level per object, +// so if one object is responsible for more operations, +// it needs to use helper objects for setBusyLevel(). + +void BusyLevelManager::setBusyLevel( QObject* obj, BusyLevel level ) { + LOG("BUSY:" << level << ":" << obj << ":" << obj->className() ); + if( level > BUSY_NONE ) { + if( mBusyLevels.contains( obj ) && mBusyLevels[ obj ] == level ) return; + if( !mBusyLevels.contains( obj )) { + connect( obj, SIGNAL( destroyed( QObject* )), this, SLOT( objectDestroyed( QObject* ))); + } + mBusyLevels[ obj ] = level; + } else { + mBusyLevels.remove( obj ); + disconnect( obj, SIGNAL( destroyed( QObject* )), this, SLOT( objectDestroyed( QObject* ))); + } + mDelayedBusyLevelTimer.start( 0, true ); +} + +void BusyLevelManager::objectDestroyed( QObject* obj ) { + LOG("DESTROYED:" << obj ); + mBusyLevels.remove( obj ); + mDelayedBusyLevelTimer.start( 0, true ); +} + +void BusyLevelManager::delayedBusyLevelChanged() { + BusyLevel newLevel = BUSY_NONE; + for( QMap< QObject*, BusyLevel >::ConstIterator it = mBusyLevels.begin(); + it != mBusyLevels.end(); + ++it ) { + newLevel = QMAX( newLevel, *it ); + } + + if( newLevel != mCurrentBusyLevel ) { + LOG("CHANGED BUSY:" << newLevel); + mCurrentBusyLevel = newLevel; + emit busyLevelChanged( newLevel ); + } +} + +BusyLevel BusyLevelManager::busyLevel() const { + return mCurrentBusyLevel; +} + + +} // namespace diff --git a/src/gvcore/busylevelmanager.h b/src/gvcore/busylevelmanager.h new file mode 100644 index 0000000..21bee6d --- /dev/null +++ b/src/gvcore/busylevelmanager.h @@ -0,0 +1,113 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef BUSYLEVELMANAGER_H +#define BUSYLEVELMANAGER_H + +// Qt +#include <qtimer.h> +namespace Gwenview { + +// KDE +#include "libgwenview_export.h" + +/* + Busy level of the application. + Sorted by increasing priority. +*/ +enum BusyLevel { + BUSY_NONE, + BUSY_THUMBNAILS, + BUSY_PRELOADING, + BUSY_LOADING, + BUSY_SMOOTHING, + BUSY_PAINTING, + BUSY_CHECKING_NEW_IMAGE +}; + +class LIBGWENVIEW_EXPORT BusyLevelManager : public QObject { +Q_OBJECT +public: + static BusyLevelManager* instance(); + + /** + * Announces that the given object is busy. + */ + void setBusyLevel( QObject* obj, BusyLevel level ); + + /** + * Returns the busy level of the whole application (i.e. maximum). + */ + BusyLevel busyLevel() const; + +signals: + /** + * When emitted, operations that are less important than current level + * should be suspended until the level decreases to their level. + * E.g. when loading a picture thumbnail generation should get suspended. + */ + void busyLevelChanged( BusyLevel level ); + +private slots: + void delayedBusyLevelChanged(); + void objectDestroyed( QObject* obj ); + +private: + BusyLevelManager(); + QMap< QObject*, BusyLevel > mBusyLevels; + BusyLevel mCurrentBusyLevel; + QTimer mDelayedBusyLevelTimer; +}; + + +/** + Helper class. Constructor sets its busy level to the given level, + destructor resets the busy level to none. + */ +class BusyLevelHelper : public QObject { +Q_OBJECT +public: + BusyLevelHelper( BusyLevel level ); + ~BusyLevelHelper(); + void reset(); +}; + +inline +BusyLevelHelper::BusyLevelHelper( BusyLevel level ) +{ + BusyLevelManager::instance()->setBusyLevel( this, level ); +} + +inline +void BusyLevelHelper::reset() +{ + BusyLevelManager::instance()->setBusyLevel( this, BUSY_NONE ); +} + +inline +BusyLevelHelper::~BusyLevelHelper() +{ + reset(); +} + + +} // namespace +#endif + diff --git a/src/gvcore/cache.cpp b/src/gvcore/cache.cpp new file mode 100644 index 0000000..77b0211 --- /dev/null +++ b/src/gvcore/cache.cpp @@ -0,0 +1,402 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "cache.h" + +// Qt + +// KDE +#include <kconfig.h> +#include <kdebug.h> +#include <kdeversion.h> +#include <ksharedptr.h> +#include <kstaticdeleter.h> +#include <kio/global.h> + +#include "cache.moc" + +namespace Gwenview { + +// Local + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + +//#define DEBUG_CACHE + +const char CONFIG_CACHE_MAXSIZE[]="maxSize"; + + +struct ImageData : public KShared { + ImageData( const KURL& url, const QDateTime& _timestamp ) + : timestamp(_timestamp) + , age(0) + , fast_url( url.isLocalFile() && !KIO::probably_slow_mounted( url.path())) + , priority( false ) { + } + + void addFile( const QByteArray& file ); + void addImage( const ImageFrames& frames, const QCString& format ); + void addThumbnail( const QPixmap& thumbnail, QSize imagesize ); + long long cost() const; + int size() const; + QByteArray file; + ImageFrames frames; + QPixmap thumbnail; + QSize imagesize; + QCString format; + QDateTime timestamp; + mutable int age; + bool fast_url; + bool priority; + int fileSize() const; + int imageSize() const; + int thumbnailSize() const; + bool reduceSize(); + bool isEmpty() const; + + typedef KSharedPtr<ImageData> Ptr; +}; + +typedef QMap<KURL, ImageData::Ptr> ImageMap; + +struct Cache::Private { + ImageMap mImages; + int mMaxSize; + int mThumbnailSize; + QValueList< KURL > mPriorityURLs; + + + /** + * This function tries to returns a valid ImageData for url and timestamp. + * If it can't find one, it will create a new one and return it. + */ + ImageData::Ptr getOrCreateImageData(const KURL& url, const QDateTime& timestamp) { + if (mImages.contains(url)) { + ImageData::Ptr data = mImages[url]; + if (data->timestamp == timestamp) return data; + } + + ImageData::Ptr data = new ImageData(url, timestamp); + mImages[url] = data; + if (mPriorityURLs.contains(url)) data->priority = true; + return data; + } +}; + + +Cache::Cache() +{ + d = new Private; + d->mMaxSize = DEFAULT_MAXSIZE; + // don't remember size for every thumbnail, but have one global and dump all if needed + d->mThumbnailSize = 0; +} + + +Cache::~Cache() { + d->mImages.clear(); + delete d; +} + + +static Cache* sCache; +static KStaticDeleter<Cache> sCacheDeleter; + + +Cache* Cache::instance() { + if (!sCache) { + sCacheDeleter.setObject(sCache, new Cache()); + } + return sCache; +} + +// Priority URLs are used e.g. when prefetching for the slideshow - after an image is prefetched, +// the loader tries to put the image in the cache. When the slideshow advances, the next loader +// just gets the image from the cache. However, the prefetching may be useless if the image +// actually doesn't stay long enough in the cache, e.g. because of being too big for the cache. +// Marking an URL as a priority one will make sure it stays in the cache and that the cache +// will be even enlarged as necessary if needed. +void Cache::setPriorityURL( const KURL& url, bool set ) { + if( set ) { + d->mPriorityURLs.append( url ); + if( d->mImages.contains( url )) { + d->mImages[ url ]->priority = true; + } + } else { + d->mPriorityURLs.remove( url ); + if( d->mImages.contains( url )) { + d->mImages[ url ]->priority = false; + } + checkMaxSize(); + } +} + + +void Cache::addFile( const KURL& url, const QByteArray& file, const QDateTime& timestamp ) { + LOG(url.prettyURL()); + updateAge(); + d->getOrCreateImageData(url, timestamp)->addFile(file); + checkMaxSize(); +} + +void Cache::addImage( const KURL& url, const ImageFrames& frames, const QCString& format, const QDateTime& timestamp ) { + LOG(url.prettyURL()); + updateAge(); + d->getOrCreateImageData(url, timestamp)->addImage(frames, format); + checkMaxSize(); +} + +void Cache::addThumbnail( const KURL& url, const QPixmap& thumbnail, QSize imagesize, const QDateTime& timestamp ) { +// Thumbnails are many and often - things would age too quickly. Therefore +// when adding thumbnails updateAge() is called from the outside only once for all of them. +// updateAge(); + d->getOrCreateImageData(url, timestamp)->addThumbnail(thumbnail, imagesize); + checkMaxSize(); +} + +void Cache::invalidate( const KURL& url ) { + d->mImages.remove( url ); +} + +QDateTime Cache::timestamp( const KURL& url ) const { + LOG(url.prettyURL()); + if( d->mImages.contains( url )) return d->mImages[ url ]->timestamp; + return QDateTime(); +} + +QByteArray Cache::file( const KURL& url ) const { + LOG(url.prettyURL()); + if( d->mImages.contains( url )) { + const ImageData::Ptr data = d->mImages[ url ]; + if( data->file.isNull()) return QByteArray(); + data->age = 0; + return data->file; + } + return QByteArray(); +} + +void Cache::getFrames( const KURL& url, ImageFrames* frames, QCString* format ) const { + LOG(url.prettyURL()); + Q_ASSERT(frames); + Q_ASSERT(format); + frames->clear(); + *format = QCString(); + if( d->mImages.contains( url )) { + const ImageData::Ptr data = d->mImages[ url ]; + if( data->frames.isEmpty()) return; + *frames = data->frames; + *format = data->format; + data->age = 0; + } +} + +QPixmap Cache::thumbnail( const KURL& url, QSize& imagesize ) const { + if( d->mImages.contains( url )) { + const ImageData::Ptr data = d->mImages[ url ]; + if( data->thumbnail.isNull()) return QPixmap(); + imagesize = data->imagesize; +// data.age = 0; + return data->thumbnail; + } + return QPixmap(); +} + +void Cache::updateAge() { + for( ImageMap::Iterator it = d->mImages.begin(); + it != d->mImages.end(); + ++it ) { + (*it)->age++; + } +} + +void Cache::checkThumbnailSize( int size ) { + if( size != d->mThumbnailSize ) { + // simply remove all thumbnails, should happen rarely + for( ImageMap::Iterator it = d->mImages.begin(); + it != d->mImages.end(); + ) { + if( !(*it)->thumbnail.isNull()) { + ImageMap::Iterator it2 = it; + ++it; + d->mImages.remove( it2 ); + } else { + ++it; + } + } + d->mThumbnailSize = size; + } +} + +#ifdef DEBUG_CACHE +static KURL _cache_url; // hack only for debugging for item to show also its key +#endif + +void Cache::checkMaxSize() { + for(;;) { + int size = 0; + ImageMap::Iterator max; + long long max_cost = -1; +#ifdef DEBUG_CACHE + int with_file = 0; + int with_thumb = 0; + int with_image = 0; +#endif + for( ImageMap::Iterator it = d->mImages.begin(); + it != d->mImages.end(); + ++it ) { + size += (*it)->size(); +#ifdef DEBUG_CACHE + if( !(*it).file.isNull()) ++with_file; + if( !(*it).thumbnail.isNull()) ++with_thumb; + if( !(*it).frames.isEmpty()) ++with_image; +#endif + long long cost = (*it)->cost(); + if( cost > max_cost && ! (*it)->priority ) { + max_cost = cost; + max = it; + } + } + if( size <= d->mMaxSize || max_cost == -1 ) { +#if 0 +#ifdef DEBUG_CACHE + kdDebug() << "Cache: Statistics (" << d->mImages.size() << "/" << with_file << "/" + << with_thumb << "/" << with_image << ")" << endl; +#endif +#endif + break; + } +#ifdef DEBUG_CACHE + _cache_url = max.key(); +#endif + + if( !(*max)->reduceSize() || (*max)->isEmpty()) d->mImages.remove( max ); + } +} + +void Cache::readConfig(KConfig* config,const QString& group) { + KConfigGroupSaver saver( config, group ); + d->mMaxSize = config->readNumEntry( CONFIG_CACHE_MAXSIZE, d->mMaxSize ); + checkMaxSize(); +} + + +void ImageData::addFile( const QByteArray& f ) { + file = f; + file.detach(); // explicit sharing + age = 0; +} + +void ImageData::addImage( const ImageFrames& fs, const QCString& f ) { + frames = fs; + format = f; + age = 0; +} + +void ImageData::addThumbnail( const QPixmap& thumb, QSize imgsize ) { + thumbnail = thumb; + imagesize = imgsize; +// age = 0; +} + +int ImageData::size() const { + return QMAX( fileSize() + imageSize() + thumbnailSize(), 100 ); // some minimal size per item +} + +int ImageData::fileSize() const { + return !file.isNull() ? file.size() : 0; +} + +int ImageData::thumbnailSize() const { + return !thumbnail.isNull() ? thumbnail.height() * thumbnail.width() * thumbnail.depth() / 8 : 0; +} + +int ImageData::imageSize() const { + int ret = 0; + for( ImageFrames::ConstIterator it = frames.begin(); it != frames.end(); ++it ) { + ret += (*it).image.height() * (*it).image.width() * (*it).image.depth() / 8; + } + return ret; +} + +bool ImageData::reduceSize() { + if( !file.isNull() && fast_url && !frames.isEmpty()) { + file = QByteArray(); +#ifdef DEBUG_CACHE + kdDebug() << "Cache: Dumping fast file: " << _cache_url.prettyURL() << ":" << cost() << endl; +#endif + return true; + } + if( !thumbnail.isNull()) { +#ifdef DEBUG_CACHE + kdDebug() << "Cache: Dumping thumbnail: " << _cache_url.prettyURL() << ":" << cost() << endl; +#endif + thumbnail = QPixmap(); + return true; + } + if( !file.isNull() && !frames.isEmpty()) { + // possibly slow file to fetch - dump the image data unless the image + // is JPEG (which needs raw data anyway) or the raw data much larger than the image + if( format == "JPEG" || fileSize() < imageSize() / 10 ) { + frames.clear(); +#ifdef DEBUG_CACHE + kdDebug() << "Cache: Dumping images: " << _cache_url.prettyURL() << ":" << cost() << endl; +#endif + } else { + file = QByteArray(); +#ifdef DEBUG_CACHE + kdDebug() << "Cache: Dumping file: " << _cache_url.prettyURL() << ":" << cost() << endl; +#endif + } + return true; + } +#ifdef DEBUG_CACHE + kdDebug() << "Cache: Dumping completely: " << _cache_url.prettyURL() << ":" << cost() << endl; +#endif + return false; // reducing here would mean clearing everything +} + +bool ImageData::isEmpty() const { + return file.isNull() && frames.isEmpty() && thumbnail.isNull(); +} + +long long ImageData::cost() const { + long long s = size(); + if( fast_url && !file.isNull()) { + s *= ( format == "JPEG" ? 10 : 100 ); // heavy penalty for storing local files + } else if( !thumbnail.isNull()) { + s *= 10 * 10; // thumbnails are small, and try to get rid of them soon + } + static const int mod[] = { 50, 30, 20, 16, 12, 10 }; + if( age <= 5 ) { + return s * 10 / mod[ age ]; + } else { + return s * ( age - 5 ); + } +} + +} // namespace diff --git a/src/gvcore/cache.h b/src/gvcore/cache.h new file mode 100644 index 0000000..bf81b91 --- /dev/null +++ b/src/gvcore/cache.h @@ -0,0 +1,67 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef CACHE_H +#define CACHE_H + +// Qt +#include <qcstring.h> +#include <qdatetime.h> +#include <qimage.h> +#include <qobject.h> +#include <qtimer.h> +#include <qvaluelist.h> + +// KDE +#include <kurl.h> + +// Local +#include "imageframe.h" +#include "libgwenview_export.h" +class KConfig; + +namespace Gwenview { +class LIBGWENVIEW_EXPORT Cache : public QObject { +Q_OBJECT +public: + static Cache* instance(); + ~Cache(); + void addImage( const KURL& url, const ImageFrames& frames, const QCString& format, const QDateTime& timestamp ); + void addFile( const KURL& url, const QByteArray& file, const QDateTime& timestamp ); + void addThumbnail( const KURL& url, const QPixmap& thumbnail, QSize imagesize, const QDateTime& timestamp ); + QDateTime timestamp( const KURL& url ) const; + QByteArray file( const KURL& url ) const; + void getFrames( const KURL& url, ImageFrames* frames, QCString* format ) const; + QPixmap thumbnail( const KURL& url, QSize& imagesize ) const; + void setPriorityURL( const KURL& url, bool set ); + void invalidate( const KURL& url ); + void checkThumbnailSize( int size ); + void readConfig(KConfig*,const QString& group); + void updateAge(); + enum { DEFAULT_MAXSIZE = 16 * 1024 * 1024 }; // 16MiB +private: + Cache(); + void checkMaxSize(); + class Private; + Private* d; +}; + +} // namespace +#endif diff --git a/src/gvcore/captionformatter.cpp b/src/gvcore/captionformatter.cpp new file mode 100644 index 0000000..4bfac6b --- /dev/null +++ b/src/gvcore/captionformatter.cpp @@ -0,0 +1,57 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2005 Aurelien Gateau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#include "captionformatter.h" + +// KDE +#include <klocale.h> + + +namespace Gwenview { + + +QString CaptionFormatter::format(const QString& format) { + QString comment=mComment; + if (comment.isNull()) { + comment=i18n("(No comment)"); + } + + QString resolution; + if (mImageSize.isValid()) { + resolution = QString( "%1x%2" ).arg( mImageSize.width()).arg( mImageSize.height()); + } + + QString str=format; + str.replace("%f", mFileName); + str.replace("%p", mPath); + str.replace("%c", comment); + str.replace("%r", resolution); + str.replace("%n", QString::number(mPosition)); + str.replace("%N", QString::number(mCount)); + str.replace("%a", mAperture); + str.replace("%t", mExposureTime); + str.replace("%i", mIso); + str.replace("%l", mFocalLength); + + return str; +} + + +} // namespace diff --git a/src/gvcore/captionformatter.h b/src/gvcore/captionformatter.h new file mode 100644 index 0000000..edce26a --- /dev/null +++ b/src/gvcore/captionformatter.h @@ -0,0 +1,57 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2005 Aurelien Gateau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef CAPTIONFORMATTER_H +#define CAPTIONFORMATTER_H + +// Qt +#include <qsize.h> +#include <qstring.h> + +// Local +#include "libgwenview_export.h" + +namespace Gwenview { + + +/** + * A class to format image captions. Used for example in fullscreen mode. + * All attributes of the class are public because it's just a "record" with a + * format() function. + */ +class LIBGWENVIEW_EXPORT CaptionFormatter { +public: + QString mPath; + QString mFileName; + QString mComment; + QString mAperture; + QString mFocalLength; + QString mExposureTime; + QString mIso; + + QSize mImageSize; + int mPosition; + int mCount; + QString format(const QString& format); +}; + +} // namespace + +#endif /* CAPTIONFORMATTER_H */ diff --git a/src/gvcore/clicklineedit.cpp b/src/gvcore/clicklineedit.cpp new file mode 100644 index 0000000..a1728a0 --- /dev/null +++ b/src/gvcore/clicklineedit.cpp @@ -0,0 +1,106 @@ +/* + This file is part of libkdepim. + Copyright (c) 2004 Daniel Molkentin <molkentin@kde.org> + based on code by Cornelius Schumacher <schumacher@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Steet, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + + +#include "clicklineedit.h" + +#include "qpainter.h" + +namespace Gwenview { + +ClickLineEdit::ClickLineEdit(QWidget *parent, const char* name ) : + KLineEdit( parent, name ) +{ + mDrawClickMsg = true; +} + + +///////////////////////////////////////////////////////////////////////////////////// +// PUBLIC +///////////////////////////////////////////////////////////////////////////////////// + +void ClickLineEdit::setClickMessage( const QString &msg ) +{ + mClickMessage = msg; + repaint(); +} + + +void ClickLineEdit::setText( const QString &txt ) +{ + mDrawClickMsg = txt.isEmpty(); + repaint(); + KLineEdit::setText( txt ); +} + + +///////////////////////////////////////////////////////////////////////////////////// +// PROTECTED +///////////////////////////////////////////////////////////////////////////////////// + +//#include <kiconloader.h> +void ClickLineEdit::drawContents( QPainter *p ) +{ + KLineEdit::drawContents( p ); + + if ( mDrawClickMsg == true && !hasFocus() ) { + QPen tmp = p->pen(); + p->setPen( palette().color( QPalette::Disabled, QColorGroup::Text ) ); + QRect cr = contentsRect(); + + //p->drawPixmap( 3, 3, SmallIcon("filter") ); + + // Add two pixel margin on the left side + cr.rLeft() += 3; + p->drawText( cr, AlignAuto | AlignVCenter, mClickMessage ); + p->setPen( tmp ); + } +} + +void ClickLineEdit::dropEvent( QDropEvent *ev ) +{ + mDrawClickMsg = false; + KLineEdit::dropEvent( ev ); +} + + +void ClickLineEdit::focusInEvent( QFocusEvent *ev ) +{ + if ( mDrawClickMsg == true ) { + mDrawClickMsg = false; + repaint(); + } + QLineEdit::focusInEvent( ev ); +} + + +void ClickLineEdit::focusOutEvent( QFocusEvent *ev ) +{ + if ( text().isEmpty() ) { + mDrawClickMsg = true; + repaint(); + } + QLineEdit::focusOutEvent( ev ); +} +} // namespace + +#include "clicklineedit.moc" + diff --git a/src/gvcore/clicklineedit.h b/src/gvcore/clicklineedit.h new file mode 100644 index 0000000..05624eb --- /dev/null +++ b/src/gvcore/clicklineedit.h @@ -0,0 +1,63 @@ +/* + This file is part of libkdepim. + Copyright (c) 2004 Daniel Molkentin <molkentin@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Steet, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef CLICKLINEEDIT_H +#define CLICKLINEEDIT_H + +#include <klineedit.h> + +namespace Gwenview { + +/** + This class provides a KLineEdit which contains a greyed-out hinting + text as long as the user didn't enter any text + + @short LineEdit with customizable "Click here" text + @author Daniel Molkentin +*/ +class ClickLineEdit : public KLineEdit +{ + Q_OBJECT + Q_PROPERTY( QString clickMessage READ clickMessage WRITE setClickMessage ) + public: + ClickLineEdit(QWidget *parent, const char* name = 0 ); + + void setClickMessage( const QString &msg ); + QString clickMessage() const { return mClickMessage; } + + virtual void setText( const QString& txt ); + + protected: + virtual void drawContents( QPainter *p ); + virtual void dropEvent( QDropEvent *ev ); + virtual void focusInEvent( QFocusEvent *ev ); + virtual void focusOutEvent( QFocusEvent *ev ); + + private: + QString mClickMessage; + bool mDrawClickMsg; + +}; + +} + +#endif // CLICKLINEEDIT_H + + diff --git a/src/gvcore/cursortracker.cpp b/src/gvcore/cursortracker.cpp new file mode 100644 index 0000000..cc7ff89 --- /dev/null +++ b/src/gvcore/cursortracker.cpp @@ -0,0 +1,86 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2004 Aur�ien G�eau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "cursortracker.h" + +// Qt +#include <qevent.h> +#include <qtooltip.h> + +namespace Gwenview { + +CursorTracker::CursorTracker(const QString& txt, QWidget* reference) +: QLabel(txt, 0, "", WX11BypassWM) { + reference->setMouseTracking(true); + reference->installEventFilter(this); +} + + +/** + * Overload to make sure the widget size is correct + */ +void CursorTracker::setText(const QString& txt) { + QLabel::setText(txt); + adjustSize(); +} + + +bool CursorTracker::eventFilter(QObject* object, QEvent* _event) { + QWidget* widget=static_cast<QWidget*>(object); + + switch (_event->type()) { + case QEvent::MouseMove: { + QMouseEvent* event=static_cast<QMouseEvent*>(_event); + if (widget->rect().contains(event->pos()) || (event->stateAfter() & LeftButton)) { + show(); + move(event->globalPos().x() + 15, event->globalPos().y() + 15); + } else { + hide(); + } + break; + } + + case QEvent::MouseButtonRelease: { + QMouseEvent* event=static_cast<QMouseEvent*>(_event); + if ( !widget->rect().contains(event->pos()) ) { + hide(); + } + break; + } + + default: + break; + } + + return false; +} + + +TipTracker::TipTracker(const QString& txt, QWidget* reference) +: CursorTracker(txt, reference) { + setPalette(QToolTip::palette()); + setFrameStyle(QFrame::Plain | QFrame::Box); + setLineWidth(1); + setAlignment(AlignAuto | AlignTop); +} + + +} // namespace diff --git a/src/gvcore/cursortracker.h b/src/gvcore/cursortracker.h new file mode 100644 index 0000000..1dc4003 --- /dev/null +++ b/src/gvcore/cursortracker.h @@ -0,0 +1,56 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2004 Aur�ien G�eau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#ifndef CURSORTRACKER_H +#define CURSORTRACKER_H + +// Qt +#include <qlabel.h> + +namespace Gwenview { + +/** + * This class implements a decoration-less window which will follow the cursor + * when it's over a specified widget. + */ +class CursorTracker : public QLabel { +public: + CursorTracker(const QString& txt, QWidget* reference); + + void setText(const QString& txt); + +protected: + bool eventFilter(QObject*, QEvent*); +}; + + +/** + * A specialized CursorTracker class, which looks like a tool tip. + */ +class TipTracker : public CursorTracker { +public: + TipTracker(const QString& txt, QWidget* reference); +}; + + +} // namespace + +#endif /* CURSORTRACKER_H */ diff --git a/src/gvcore/deletedialog.cpp b/src/gvcore/deletedialog.cpp new file mode 100644 index 0000000..19c34ba --- /dev/null +++ b/src/gvcore/deletedialog.cpp @@ -0,0 +1,133 @@ +/*************************************************************************** + begin : Tue Aug 31 21:59:58 EST 2004 + copyright : (C) 2004 by Michael Pyne <michael.pyne@kdemail.net> + (C) 2006 by Ian Monroe <ian@monroe.nu> + (C) 2006 by Aurelien Gateau <aurelien.gateau@free.fr> +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kconfig.h> +#include <kdeversion.h> +#include <kdialogbase.h> +#include <kglobal.h> +#include <kiconloader.h> +#include <kio/job.h> +#include <klocale.h> +#include <kstdguiitem.h> +#include <kurl.h> + +#include <qstringlist.h> +#include <qcheckbox.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qtimer.h> +#include <qvbox.h> +#include <qhbox.h> +#include <qpushbutton.h> + +#include "fileoperationconfig.h" +#include "deletedialog.h" +#include "deletedialogbase.h" + +namespace Gwenview { + +DeleteDialog::DeleteDialog(QWidget *parent, const char *name) : + KDialogBase(Swallow, WStyle_DialogBorder, parent, name, + true /* modal */, i18n("About to delete selected files"), + Ok | Cancel, Cancel /* Default */, true /* separator */), + m_trashGuiItem(i18n("&Send to Trash"), "trashcan_full") +{ + m_widget = new DeleteDialogBase(this, "delete_dialog_widget"); + setMainWidget(m_widget); + + m_widget->setMinimumSize(400, 300); + + actionButton(Ok)->setFocus(); + + bool deleteInstead = ! FileOperationConfig::deleteToTrash(); + m_widget->ddShouldDelete->setChecked(deleteInstead); + + connect(m_widget->ddShouldDelete, SIGNAL(toggled(bool)), SLOT(updateUI())); +} + +void DeleteDialog::setURLList(const KURL::List &files) +{ + m_widget->ddFileList->clear(); + for( KURL::List::ConstIterator it = files.begin(); it != files.end(); it++) { + m_widget->ddFileList->insertItem( (*it).pathOrURL() ); + } + m_widget->ddNumFiles->setText(i18n("<b>1</b> item selected.", "<b>%n</b> items selected.", files.count())); + updateUI(); +} + +void DeleteDialog::accept() +{ + FileOperationConfig::setDeleteToTrash( ! shouldDelete() ); + FileOperationConfig::writeConfig(); + + KDialogBase::accept(); +} + + +void DeleteDialog::updateUI() +{ + QString msg, iconName; + + int fileCount = m_widget->ddFileList->count(); + bool reallyDelete = m_widget->ddShouldDelete->isChecked(); + + if(reallyDelete) { + msg = i18n( + "<qt>This item will be <b>permanently deleted</b> from your hard disk.</qt>", + "<qt>These items will be <b>permanently deleted</b> from your hard disk.</qt>", + fileCount); + iconName = "messagebox_warning"; + } + else { + msg = i18n( + "<qt>This item will be moved to the trash bin.</qt>", + "<qt>These items will be moved to the trash bin.</qt>", + fileCount); + iconName = "trashcan_full"; + } + QPixmap icon = KGlobal::iconLoader()->loadIcon(iconName, KIcon::NoGroup, KIcon::SizeMedium); + + m_widget->ddDeleteText->setText(msg); + m_widget->ddWarningIcon->setPixmap(icon); + + setButtonGuiItem(Ok, reallyDelete ? KStdGuiItem::del() : m_trashGuiItem); + adjustSize(); +} + + +bool DeleteDialog::shouldDelete() const { + return m_widget->ddShouldDelete->isChecked(); +} + + +QSize DeleteDialog::sizeHint() const { + m_widget->adjustSize(); + QSize hint = m_widget->minimumSize(); + hint = calculateSize(hint.width(), hint.height()); + + // For some reason calculateSize does not return a correct height. As I'm + // fed up fighting with it, let's just add a few more pixels. + hint.rheight() += 50; + return hint; +} + + + +} // namespace + +#include "deletedialog.moc" + +// vim: set et ts=4 sw=4: diff --git a/src/gvcore/deletedialog.h b/src/gvcore/deletedialog.h new file mode 100644 index 0000000..0340e09 --- /dev/null +++ b/src/gvcore/deletedialog.h @@ -0,0 +1,55 @@ +/*************************************************************************** + begin : Tue Aug 31 21:54:20 EST 2004 + copyright : (C) 2004 by Michael Pyne <michael.pyne@kdemail.net> + (C) 2006 by Ian Monroe <ian@monroe.nu> + (C) 2006 by Aurelien Gateau <aurelien.gateau@free.fr> +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef _DELETEDIALOG_H +#define _DELETEDIALOG_H + + +#include <kdialogbase.h> + +class DeleteDialogBase; +class KGuiItem; + +namespace Gwenview { + +class DeleteDialog : public KDialogBase +{ + Q_OBJECT + +public: + DeleteDialog(QWidget *parent, const char *name = "delete_dialog"); + + void setURLList(const KURL::List &files); + bool shouldDelete() const; + + QSize sizeHint() const; + +protected slots: + virtual void accept(); + +private slots: + void updateUI(); + +private: + DeleteDialogBase *m_widget; + KGuiItem m_trashGuiItem; +}; + +} // namespace + +#endif + +// vim: set et ts=4 sw=4: diff --git a/src/gvcore/deletedialogbase.ui b/src/gvcore/deletedialogbase.ui new file mode 100644 index 0000000..dd338ea --- /dev/null +++ b/src/gvcore/deletedialogbase.ui @@ -0,0 +1,111 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>DeleteDialogBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>DeleteDialogBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>542</width> + <height>374</height> + </rect> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout3</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>ddWarningIcon</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>4</hsizetype> + <vsizetype>4</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Icon Placeholder, not in GUI</string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>ddDeleteText</cstring> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>60</height> + </size> + </property> + <property name="text"> + <string>Deletion method placeholder, not in GUI</string> + </property> + <property name="alignment"> + <set>WordBreak|AlignCenter</set> + </property> + </widget> + </hbox> + </widget> + <widget class="KListBox"> + <property name="name"> + <cstring>ddFileList</cstring> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="selectionMode"> + <enum>NoSelection</enum> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>ddNumFiles</cstring> + </property> + <property name="text"> + <string>Placeholder for number of files, not in GUI</string> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignRight</set> + </property> + </widget> + <widget class="QCheckBox"> + <property name="name"> + <cstring>ddShouldDelete</cstring> + </property> + <property name="text"> + <string>&Delete items instead of moving them to the trash</string> + </property> + <property name="toolTip" stdset="0"> + <string>If checked, items will be permanently removed instead of being placed in the trash bin</string> + </property> + <property name="whatsThis" stdset="0"> + <string><qt><p>If this box is checked, items will be <b>permanently removed</b> instead of being placed in the trash bin.</p> + +<p><em>Use this option with caution</em>: Most filesystems are unable to reliably undelete deleted files.</p></qt></string> + </property> + </widget> + </vbox> +</widget> +<customwidgets> +</customwidgets> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>klistbox.h</includehint> +</includehints> +</UI> diff --git a/src/gvcore/document.cpp b/src/gvcore/document.cpp new file mode 100644 index 0000000..058efe2 --- /dev/null +++ b/src/gvcore/document.cpp @@ -0,0 +1,618 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2006 Aurelien Gateau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include <sys/stat.h> // For S_ISDIR + +// Qt +#include <qfileinfo.h> +#include <qguardedptr.h> +#include <qpaintdevicemetrics.h> +#include <qpainter.h> +#include <qwmatrix.h> + +// KDE +#include <kapplication.h> +#include <kdebug.h> +#include <kfilemetainfo.h> +#include <kglobalsettings.h> +#include <kimageio.h> +#include <kio/job.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kprinter.h> +#include <kstringhandler.h> + +// Local +#include "archive.h" +#include "busylevelmanager.h" +#include "cache.h" +#include "documentloadingimpl.h" +#include "documentimpl.h" +#include "imagesavedialog.h" +#include "imageutils/imageutils.h" +#include "jpegformattype.h" +#include "pngformattype.h" +#include "mngformattype.h" +#include "printdialog.h" +#include "qxcfi.h" +#include "xpm.h" +#include "xcursor.h" + +#include "document.moc" +namespace Gwenview { + + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + +const char* CONFIG_SAVE_AUTOMATICALLY="save automatically"; + + +/** + * Returns a widget suitable to use as a dialog parent + */ +static QWidget* dialogParentWidget() { + return KApplication::kApplication()->mainWidget(); +} + +//------------------------------------------------------------------- +// +// DocumentPrivate +// +//------------------------------------------------------------------- +class DocumentPrivate { +public: + KURL mURL; + bool mModified; + QImage mImage; + QString mMimeType; + QCString mImageFormat; + DocumentImpl* mImpl; + QGuardedPtr<KIO::StatJob> mStatJob; + int mFileSize; +}; + + +//------------------------------------------------------------------- +// +// Document +// +//------------------------------------------------------------------- +Document::Document(QObject* parent) +: QObject(parent) { + d=new DocumentPrivate; + d->mModified=false; + d->mImpl=new DocumentEmptyImpl(this); + d->mStatJob=0L; + d->mFileSize=-1; + + // Register formats here to make sure they are always enabled + KImageIO::registerFormats(); + XCFImageFormat::registerFormat(); + + // First load Qt's plugins, so that Gwenview's decoders that + // override some of them are installed later and thus come first. + QImageIO::inputFormats(); + { + static Gwenview::JPEGFormatType sJPEGFormatType; + static Gwenview::PNGFormatType sPNGFormatType; + static Gwenview::XPM sXPM; + static Gwenview::MNG sMNG; + static Gwenview::XCursorFormatType sXCursorFormatType; + } + + connect( this, SIGNAL( loading()), + this, SLOT( slotLoading())); + connect( this, SIGNAL( loaded(const KURL&)), + this, SLOT( slotLoaded())); +} + + +Document::~Document() { + delete d->mImpl; + delete d; +} + + +//--------------------------------------------------------------------- +// +// Properties +// +//--------------------------------------------------------------------- +QString Document::mimeType() const { + return d->mMimeType; +} + +void Document::setMimeType(const QString& mimeType) { + d->mMimeType = mimeType; +} + +MimeTypeUtils::Kind Document::urlKind() const { + return d->mImpl->urlKind(); +} + + +KURL Document::url() const { + return d->mURL; +} + + +void Document::setURL(const KURL& paramURL) { + if (paramURL==url()) return; + // Make a copy, we might have to fix the protocol + KURL localURL(paramURL); + LOG("url: " << paramURL.prettyURL()); + + // Be sure we are not waiting for another stat result + if (!d->mStatJob.isNull()) { + d->mStatJob->kill(); + } + BusyLevelManager::instance()->setBusyLevel(this, BUSY_NONE); + + // Ask to save if necessary. + saveBeforeClosing(); + + if (localURL.isEmpty()) { + reset(); + return; + } + + // Set high busy level, so that operations like smoothing are suspended. + // Otherwise the stat() below done using KIO can take quite long. + BusyLevelManager::instance()->setBusyLevel( this, BUSY_CHECKING_NEW_IMAGE ); + + + // Fix wrong protocol + if (Archive::protocolIsArchive(localURL.protocol())) { + QFileInfo info(localURL.path()); + if (info.exists()) { + localURL.setProtocol("file"); + } + } + + d->mURL = localURL; // this may be fixed after stat() is complete, but set at least something + d->mStatJob = KIO::stat( localURL, !localURL.isLocalFile() ); + d->mStatJob->setWindow(KApplication::kApplication()->mainWidget()); + connect( d->mStatJob, SIGNAL( result (KIO::Job *) ), + this, SLOT( slotStatResult (KIO::Job *) ) ); +} + + +void Document::slotStatResult(KIO::Job* job) { + LOG(""); + Q_ASSERT(d->mStatJob==job); + if (d->mStatJob!=job) { + kdWarning() << k_funcinfo << "We did not get the right job!\n"; + return; + } + BusyLevelManager::instance()->setBusyLevel( this, BUSY_NONE ); + if (d->mStatJob->error()) return; + + bool isDir=false; + KIO::UDSEntry entry = d->mStatJob->statResult(); + d->mURL=d->mStatJob->url(); + + KIO::UDSEntry::ConstIterator it; + for(it=entry.begin();it!=entry.end();++it) { + if ((*it).m_uds==KIO::UDS_FILE_TYPE) { + isDir=S_ISDIR( (*it).m_long ); + break; + } + } + + if (isDir) { + d->mURL.adjustPath( +1 ); // add trailing / + reset(); + return; + } + + load(); +} + + +void Document::setDirURL(const KURL& paramURL) { + saveBeforeClosing(); + d->mURL=paramURL; + d->mURL.adjustPath( +1 ); // add trailing / + reset(); +} + + +const QImage& Document::image() const { + return d->mImage; +} + +void Document::setImage(QImage img) { + bool sizechange = d->mImage.size() != img.size(); + d->mImage = img; + if( sizechange ) emit sizeUpdated(); +} + + +KURL Document::dirURL() const { + if (filename().isEmpty()) { + return d->mURL; + } else { + KURL url=d->mURL.upURL(); + url.adjustPath(1); + return url; + } +} + +QString Document::filename() const { + return d->mURL.filename(false); +} + +const QCString& Document::imageFormat() const { + return d->mImageFormat; +} + +void Document::setImageFormat(const QCString& format) { + d->mImageFormat=format; +} + +void Document::setFileSize(int size) { + d->mFileSize=size; +} + +QString Document::comment() const { + return d->mImpl->comment(); +} + +QString Document::aperture() const { + return d->mImpl->aperture(); +} + +QString Document::exposureTime() const { + return d->mImpl->exposureTime(); +} + +QString Document::iso() const { + return d->mImpl->iso(); +} + +QString Document::focalLength() const { + return d->mImpl->focalLength(); +} + +void Document::setComment(const QString& comment) { + d->mImpl->setComment(comment); + d->mModified=true; + emit modified(); +} + +Document::CommentState Document::commentState() const { + return d->mImpl->commentState(); +} + +/** + * Returns the duration of the document in seconds, or 0 if there is no + * duration + */ +int Document::duration() const { + return d->mImpl->duration(); +} + +int Document::fileSize() const { + return d->mFileSize; +} + +bool Document::canBeSaved() const { + return d->mImpl->canBeSaved(); +} + +bool Document::isModified() const { + return d->mModified; +} + +void Document::slotLoading() { + BusyLevelManager::instance()->setBusyLevel( this, BUSY_LOADING ); +} + +void Document::slotLoaded() { + BusyLevelManager::instance()->setBusyLevel( this, BUSY_NONE ); +} + +//--------------------------------------------------------------------- +// +// Operations +// +//--------------------------------------------------------------------- +void Document::reload() { + Cache::instance()->invalidate( url()); + load(); + emit reloaded(url()); +} + + +void Document::print(KPrinter *pPrinter) { + QPainter printPainter; + printPainter.begin(pPrinter); + doPaint(pPrinter, &printPainter); + printPainter.end(); +} + + +void Document::doPaint(KPrinter *printer, QPainter *painter) { + // will contain the final image to print + QImage image = d->mImage; + image.detach(); + + // We use a QPaintDeviceMetrics to know the actual page size in pixel, + // this gives the real painting area + QPaintDeviceMetrics pdMetrics(painter->device()); + const int margin = pdMetrics.logicalDpiY() / 2; // half-inch margin + + painter->setFont( KGlobalSettings::generalFont() ); + QFontMetrics fMetrics = painter->fontMetrics(); + + int x = 0; + int y = 0; + int pdWidth = pdMetrics.width(); + int pdHeight = pdMetrics.height(); + + QString t = "true"; + QString f = "false"; + + int alignment = (printer->option("app-gwenview-position").isEmpty() ? + Qt::AlignCenter : printer->option("app-gwenview-position").toInt()); + + // Compute filename offset + int filenameOffset = 0; + bool printFilename = printer->option( "app-gwenview-printFilename" ) != f; + if ( printFilename ) { + filenameOffset = fMetrics.lineSpacing() + 14; + pdHeight -= filenameOffset; // filename goes into one line! + } + + // Compute comment offset + int commentOffset = 0; + bool printComment = printer->option( "app-gwenview-printComment" ) != f; + if ( commentOffset ) { + commentOffset = fMetrics.lineSpacing() + 14;// #### TODO check if it's correct + pdHeight -= commentOffset; // #### TODO check if it's correct + } + if (commentOffset || printFilename) { + pdHeight -= margin; + } + + // Apply scaling + int scaling = printer->option( "app-gwenview-scale" ).toInt(); + + QSize size = image.size(); + if (scaling==GV_FITTOPAGE /* Fit to page */) { + bool enlargeToFit = printer->option( "app-gwenview-enlargeToFit" ) != f; + if ((image.width() > pdWidth || image.height() > pdHeight) || enlargeToFit) { + size.scale( pdWidth, pdHeight, QSize::ScaleMin ); + } + } else { + if (scaling==GV_SCALE /* Scale To */) { + int unit = (printer->option("app-gwenview-scaleUnit").isEmpty() ? + GV_INCHES : printer->option("app-gwenview-scaleUnit").toInt()); + double inches = 1; + if (unit == GV_MILLIMETERS) { + inches = 1/25.4; + } else if (unit == GV_CENTIMETERS) { + inches = 1/2.54; + } + double wImg = (printer->option("app-gwenview-scaleWidth").isEmpty() ? + 1 : printer->option("app-gwenview-scaleWidth").toDouble()) * inches; + double hImg = (printer->option("app-gwenview-scaleHeight").isEmpty() ? + 1 : printer->option("app-gwenview-scaleHeight").toDouble()) * inches; + size.setWidth( int(wImg * printer->resolution()) ); + size.setHeight( int(hImg * printer->resolution()) ); + } else { + /* GV_NOSCALE: no scaling */ + // try to get the density info so that we can print using original size + // known if it is am image from scanner for instance + const float INCHESPERMETER = (100. / 2.54); + if (image.dotsPerMeterX()) + { + double wImg = double(size.width()) / double(image.dotsPerMeterX()) * INCHESPERMETER; + size.setWidth( int(wImg *printer->resolution()) ); + } + if (image.dotsPerMeterY()) + { + double hImg = double(size.height()) / double(image.dotsPerMeterY()) * INCHESPERMETER; + size.setHeight( int(hImg *printer->resolution()) ); + } + } + + if (size.width() > pdWidth || size.height() > pdHeight) { + int resp = KMessageBox::warningYesNoCancel(dialogParentWidget(), + i18n("The image will not fit on the page, what do you want to do?"), + QString::null,KStdGuiItem::cont(), + i18n("Shrink") ); + + if (resp==KMessageBox::Cancel) { + printer->abort(); + return; + } else if (resp == KMessageBox::No) { // Shrink + size.scale(pdWidth, pdHeight, QSize::ScaleMin); + } + } + } + + // Compute x and y + if ( alignment & Qt::AlignHCenter ) + x = (pdWidth - size.width())/2; + else if ( alignment & Qt::AlignLeft ) + x = 0; + else if ( alignment & Qt::AlignRight ) + x = pdWidth - size.width(); + + if ( alignment & Qt::AlignVCenter ) + y = (pdHeight - size.height())/2; + else if ( alignment & Qt::AlignTop ) + y = 0; + else if ( alignment & Qt::AlignBottom ) + y = pdHeight - size.height(); + + // Draw, the image will be scaled to fit the given area if necessary + painter->drawImage( QRect( x, y, size.width(), size.height()), image ); + + if ( printFilename ) { + QString fname = KStringHandler::cPixelSqueeze( filename(), fMetrics, pdWidth ); + if ( !fname.isEmpty() ) { + int fw = fMetrics.width( fname ); + int x = (pdWidth - fw)/2; + int y = pdMetrics.height() - filenameOffset/2 -commentOffset/2 - margin; + painter->drawText( x, y, fname ); + } + } + if ( printComment ) { + QString comm = comment(); + if ( !comm.isEmpty() ) { + int fw = fMetrics.width( comm ); + int x = (pdWidth - fw)/2; + int y = pdMetrics.height() - commentOffset/2 - margin; + painter->drawText( x, y, comm ); + } + } +} + + +void Document::transform(ImageUtils::Orientation orientation) { + d->mImpl->transform(orientation); + d->mModified=true; + emit modified(); +} + + +void Document::save() { + QString msg=saveInternal(url(), d->mImageFormat); + if (!msg.isNull()) { + KMessageBox::error(dialogParentWidget(), msg); + // If it can't be saved we leave it as modified, because user + // could choose to save it to another path with saveAs + } +} + + +void Document::saveAs() { + KURL saveURL; + + ImageSaveDialog dialog(saveURL, d->mImageFormat, dialogParentWidget()); + dialog.setSelection(url().fileName()); + if (!dialog.exec()) return; + + QString msg=saveInternal(saveURL, dialog.imageFormat() ); + if (!msg.isNull()) { + // If it can't be saved we leave it as modified, because user + // could choose a wrong readonly path from dialog and retry to + KMessageBox::error(dialogParentWidget(), msg); + } +} + +void Document::saveBeforeClosing() { + if (!d->mModified) return; + + QString msg=i18n("<qt>The image <b>%1</b> has been modified, do you want to save the changes?</qt>") + .arg(url().prettyURL()); + + int result=KMessageBox::questionYesNo(dialogParentWidget(), msg, QString::null, + KStdGuiItem::save(), KStdGuiItem::discard(), CONFIG_SAVE_AUTOMATICALLY); + + if (result == KMessageBox::Yes) { + saveInternal(url(), d->mImageFormat); + // If it can't be saved it's useless to leave it as modified + // since user is closing this image and changing to another one + d->mModified=false; + //FIXME it should be nice to tell the user it failed + } else { + d->mModified=false; + } +} + + +//--------------------------------------------------------------------- +// +// Private stuff +// +//--------------------------------------------------------------------- +void Document::switchToImpl(DocumentImpl* impl) { + // There should always be an implementation defined + Q_ASSERT(d->mImpl); + Q_ASSERT(impl); + delete d->mImpl; + d->mImpl=impl; + + connect(d->mImpl, SIGNAL(finished(bool)), + this, SLOT(slotFinished(bool)) ); + connect(d->mImpl, SIGNAL(sizeUpdated()), + this, SIGNAL(sizeUpdated()) ); + connect(d->mImpl, SIGNAL(rectUpdated(const QRect&)), + this, SIGNAL(rectUpdated(const QRect&)) ); + d->mImpl->init(); +} + + +void Document::load() { + KURL pixURL=url(); + Q_ASSERT(!pixURL.isEmpty()); + LOG("url: " << pixURL.prettyURL()); + + // DocumentLoadingImpl might emit "finished()" in its "init()" method, so + // make sure we emit "loading()" before switching + emit loading(); + switchToImpl(new DocumentLoadingImpl(this)); +} + + +void Document::slotFinished(bool success) { + LOG(""); + if (success) { + emit loaded(d->mURL); + } else { + // FIXME: Emit a failed signal instead + emit loaded(d->mURL); + } +} + + +QString Document::saveInternal(const KURL& url, const QCString& format) { + QString msg=d->mImpl->save(url, format); + + if (msg.isNull()) { + emit saved(url); + d->mModified=false; + return QString::null; + } + + LOG("Save failed: " << msg); + return QString("<qt><b>%1</b><br/>") + .arg(i18n("Could not save the image to %1.").arg(url.prettyURL())) + + msg + "</qt>"; +} + + +void Document::reset() { + switchToImpl(new DocumentEmptyImpl(this)); + emit loaded(d->mURL); +} + +} // namespace diff --git a/src/gvcore/document.h b/src/gvcore/document.h new file mode 100644 index 0000000..98b14ca --- /dev/null +++ b/src/gvcore/document.h @@ -0,0 +1,194 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2006 Aurelien Gateau + + 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef DOCUMENT_H +#define DOCUMENT_H + +// Qt +#include <qcstring.h> +#include <qobject.h> +#include <qimage.h> + +// KDE +#include <kurl.h> +#include <kprinter.h> + +// Local +#include "imageutils/orientation.h" +#include "mimetypeutils.h" +#include "libgwenview_export.h" +namespace KIO { class Job; } + +namespace Gwenview { +class DocumentPrivate; +class DocumentImpl; + +/** + * The application document. + * It knows what the current url is and will emit signals when + * loading/loaded/modified... + * + * The ordering of loading() and loaded() signals is: + * - setURL() is called + * - URL is stated + * - loading() is emitted (may be skipped if no loading is needed, e.g. wrong URL) + * - image is being loaded + * - loaded() is emitted + */ +class LIBGWENVIEW_EXPORT Document : public QObject { +Q_OBJECT +public: + enum CommentState { NONE=0, READ_ONLY=1, WRITABLE=2 }; + + Document(QObject*); + ~Document(); + + // Properties + const QImage& image() const; + KURL url() const; + KURL dirURL() const; + QString filename() const; + const QCString& imageFormat() const; + int fileSize() const; + QString mimeType() const; + MimeTypeUtils::Kind urlKind() const; + bool isModified() const; + + /** + * Returns true if Gwenview knows how to save such an image + */ + bool canBeSaved() const; + + // Convenience methods + bool isNull() const { return image().isNull(); } + int width() const { return image().width(); } + int height() const { return image().height(); } + + Document::CommentState commentState() const; + QString comment() const; + void setComment(const QString&); + QString aperture() const; + QString exposureTime() const; + QString iso() const; + QString focalLength() const; + + int duration() const; + +public slots: + void setURL(const KURL&); + void setDirURL(const KURL&); + void reload(); + + /** + * Save to the current file. + */ + void save(); + void saveAs(); + + /** print the selected file */ + void print(KPrinter *pPrinter); + + /** + * If the image has been modified, prompt the user to save the changes. + */ + void saveBeforeClosing(); + + // "Image manipulation" + void transform(ImageUtils::Orientation); + +signals: + /** + * Emitted when the class starts to load the image. + */ + void loading(); + + /** + * Emitted when the class has finished loading the image. + * Also emitted if the image could not be loaded. + */ + void loaded(const KURL& url); + + /** + * Emitted when the image has been modified. + */ + void modified(); + + /** + * Emitted when the image has been saved on disk. + */ + void saved(const KURL& url); + + /** + * Emitted when the image has been reloaded. + */ + void reloaded(const KURL& url); + + /** + * Emitted to show a part of the image must be refreshed + */ + void rectUpdated(const QRect& rect); + + /** + * Emitted when the size is known + */ + void sizeUpdated(); + + /** + * Emitted when something goes wrong, like when save fails + */ + void errorHappened(const QString& message); + +private slots: + void slotStatResult(KIO::Job*); + void slotFinished(bool success); + void slotLoading(); + void slotLoaded(); + +private: + friend class DocumentImpl; + friend class DocumentPrivate; + + DocumentPrivate* d; + + // These methods are used by DocumentImpl and derived + void switchToImpl(DocumentImpl*); + void setImage(QImage); + void setImageFormat(const QCString&); + void setMimeType(const QString&); + void setFileSize(int); + + void reset(); + void load(); + void doPaint(KPrinter *pPrinter, QPainter *p); + + /** + * The returned string is null if the image was successfully saved, + * otherwise it's the translated error message. + */ + QString saveInternal(const KURL& url, const QCString& format); + + Document(const Document&); + Document &operator=(const Document&); +}; + + +} // namespace +#endif + diff --git a/src/gvcore/documentanimatedloadedimpl.cpp b/src/gvcore/documentanimatedloadedimpl.cpp new file mode 100644 index 0000000..a6cabb1 --- /dev/null +++ b/src/gvcore/documentanimatedloadedimpl.cpp @@ -0,0 +1,97 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "documentanimatedloadedimpl.moc" + +// Qt +#include <qstring.h> +#include <qtimer.h> + +// KDE +#include <kdebug.h> +#include <klocale.h> +namespace Gwenview { + +// Local + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + +class DocumentAnimatedLoadedImplPrivate { +public: + ImageFrames mFrames; + int mCurrentFrame; + QTimer mFrameTimer; +}; + + +DocumentAnimatedLoadedImpl::DocumentAnimatedLoadedImpl(Document* document, const ImageFrames& frames) +: DocumentLoadedImpl(document) { + LOG("" << mDocument->url().prettyURL() << ", frames: " << frames.count() ); + d=new DocumentAnimatedLoadedImplPrivate; + d->mFrames = frames; + d->mCurrentFrame = -1; + connect( &d->mFrameTimer, SIGNAL( timeout()), SLOT( nextFrame())); +} + +void DocumentAnimatedLoadedImpl::init() { + DocumentLoadedImpl::init(); + nextFrame(); +} + +void DocumentAnimatedLoadedImpl::nextFrame() { + ++d->mCurrentFrame; + if( d->mCurrentFrame == int( d->mFrames.count())) d->mCurrentFrame = 0; + d->mFrameTimer.start( QMAX( 10, d->mFrames[ d->mCurrentFrame ].delay )); +// NOTE! If this ever gets changed to already animate the picture while it's still +// loading, with MNG the frame delay gets announced only after the frame is ready. +// See ImageLoader::frameDone() . + LOG("" << d->mCurrentFrame ); + + setImage(d->mFrames[ d->mCurrentFrame ].image); + emitImageRectUpdated(); +} + +DocumentAnimatedLoadedImpl::~DocumentAnimatedLoadedImpl() { + delete d; +} + + +void DocumentAnimatedLoadedImpl::transform(ImageUtils::Orientation orientation) { + for( ImageFrames::Iterator it = d->mFrames.begin(); it != d->mFrames.end(); ++it ) { + (*it).image = ImageUtils::transform( (*it).image, orientation ); + } + setImage( d->mFrames[ d->mCurrentFrame ].image); + emitImageRectUpdated(); +} + + +QString DocumentAnimatedLoadedImpl::localSave(QFile* /*file*/, const QCString& /*format*/) const { + return i18n("Sorry, cannot save animated images."); +} + +} // namespace diff --git a/src/gvcore/documentanimatedloadedimpl.h b/src/gvcore/documentanimatedloadedimpl.h new file mode 100644 index 0000000..b9266b3 --- /dev/null +++ b/src/gvcore/documentanimatedloadedimpl.h @@ -0,0 +1,62 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef DOCUMENTANIMATEDIMPL_H +#define DOCUMENTANIMATEDIMPL_H + +// Qt +#include <qimage.h> +#include <qvaluevector.h> + +// Local +#include "documentloadedimpl.h" +#include "imageutils/imageutils.h" +#include "imageframe.h" + +class QFile; +class QCString; + +namespace Gwenview { +class Document; + +class DocumentAnimatedLoadedImplPrivate; + +class DocumentAnimatedLoadedImpl : public DocumentLoadedImpl { +Q_OBJECT +public: + DocumentAnimatedLoadedImpl(Document* document, const ImageFrames& frames); + ~DocumentAnimatedLoadedImpl(); + void init(); + + void transform(ImageUtils::Orientation); + virtual bool canBeSaved() const { return false; } + +protected: + QString localSave(QFile*, const QCString& format) const; + +private slots: + void nextFrame(); +private: + DocumentAnimatedLoadedImplPrivate* d; +}; + +} // namespace +#endif /* DOCUMENTANIMATEDIMPL_H */ + diff --git a/src/gvcore/documentimpl.cpp b/src/gvcore/documentimpl.cpp new file mode 100644 index 0000000..3221229 --- /dev/null +++ b/src/gvcore/documentimpl.cpp @@ -0,0 +1,103 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// KDE +#include <klocale.h> + +// Local +#include "document.h" +#include "documentimpl.moc" +namespace Gwenview { + +DocumentImpl::DocumentImpl(Document* document) +: mDocument(document) {} + +void DocumentImpl::init() {} + +DocumentImpl::~DocumentImpl() {} + +void DocumentImpl::switchToImpl(DocumentImpl* impl) { + mDocument->switchToImpl(impl); +} + +void DocumentImpl::setImage(QImage img) { + if (img.depth() == 1) { + // 1 bit depth images are difficult to scale. Let's convert to 8 bit + // depth. See bug #155518. + img = img.convertDepth(8); + } + mDocument->setImage(img); +} + +void DocumentImpl::emitImageRectUpdated() { + emit rectUpdated(mDocument->image().rect()); +} + +void DocumentImpl::setImageFormat(const QCString& format) { + mDocument->setImageFormat(format); +} + +void DocumentImpl::setMimeType(const QString& mimeType) { + mDocument->setMimeType(mimeType); +} + +void DocumentImpl::setFileSize(int size) const { + mDocument->setFileSize(size); +} + +QString DocumentImpl::aperture() const { + return QString::null; +} + +QString DocumentImpl::exposureTime() const { + return QString::null; +} + +QString DocumentImpl::iso() const { + return QString::null; +} + +QString DocumentImpl::focalLength() const { + return QString::null; +} + +QString DocumentImpl::comment() const { + return QString::null; +} + +Document::CommentState DocumentImpl::commentState() const { + return Document::NONE; +} + +void DocumentImpl::setComment(const QString&) { +} + +int DocumentImpl::duration() const { + return 0; +} + +void DocumentImpl::transform(ImageUtils::Orientation) { +} + +QString DocumentImpl::save(const KURL&, const QCString&) const { + return i18n("No document to save"); +} + +} // namespace diff --git a/src/gvcore/documentimpl.h b/src/gvcore/documentimpl.h new file mode 100644 index 0000000..d5ca875 --- /dev/null +++ b/src/gvcore/documentimpl.h @@ -0,0 +1,103 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef DOCUMENTIMPL_H +#define DOCUMENTIMPL_H + +// Qt +#include <qobject.h> +#include <qrect.h> + +// Local +#include "document.h" +#include "imageutils/orientation.h" +namespace Gwenview { + + +class DocumentImpl : public QObject { +Q_OBJECT +public: + DocumentImpl(Document* document); + virtual ~DocumentImpl(); + /** + * This method is called by Document::switchToImpl after it has connect + * signals to the object + */ + virtual void init(); + + void switchToImpl(DocumentImpl*); + void setImage(QImage); + void setMimeType(const QString&); + void setImageFormat(const QCString&); + void setFileSize(int) const; + + /** + * Convenience method to emit rectUpdated with the whole image rect + */ + void emitImageRectUpdated(); + + virtual QString aperture() const; + virtual QString exposureTime() const; + virtual QString iso() const; + virtual QString focalLength() const; + + virtual QString comment() const; + virtual Document::CommentState commentState() const; + virtual void setComment(const QString&); + virtual int duration() const; + + virtual void transform(ImageUtils::Orientation); + virtual QString save(const KURL&, const QCString& format) const; + + virtual MimeTypeUtils::Kind urlKind() const=0; + + virtual bool canBeSaved() const=0; + + +signals: + void finished(bool success); + void sizeUpdated(); + void rectUpdated(const QRect&); + +protected: + Document* mDocument; +}; + +class DocumentEmptyImpl : public DocumentImpl { +public: + DocumentEmptyImpl(Document* document) + : DocumentImpl(document) { + setImage(QImage()); + setImageFormat(0); + setMimeType("application/x-zerosize"); + } + + MimeTypeUtils::Kind urlKind() const { + return MimeTypeUtils::KIND_UNKNOWN; + } + + bool canBeSaved() const { + return false; + } +}; + +} // namespace +#endif /* DOCUMENTIMPL_H */ + diff --git a/src/gvcore/documentjpegloadedimpl.cpp b/src/gvcore/documentjpegloadedimpl.cpp new file mode 100644 index 0000000..2f3250b --- /dev/null +++ b/src/gvcore/documentjpegloadedimpl.cpp @@ -0,0 +1,143 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Qt +#include <qcstring.h> +#include <qfile.h> +#include <qfileinfo.h> +#include <qtimer.h> + +// KDE +#include <kdebug.h> +#include <kio/netaccess.h> +#include <klocale.h> + +// Local +#include "miscconfig.h" +#include "imageutils/jpegcontent.h" +#include "imageutils/imageutils.h" +#include "documentjpegloadedimpl.moc" +namespace Gwenview { + + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + + +class DocumentJPEGLoadedImplPrivate { +public: + ImageUtils::JPEGContent mJPEGContent; + +}; + + +DocumentJPEGLoadedImpl::DocumentJPEGLoadedImpl(Document* document, const QByteArray& rawData) +: DocumentLoadedImpl(document) { + LOG("" << mDocument->url().prettyURL() << ", data size: " << rawData.size() ); + d=new DocumentJPEGLoadedImplPrivate; + d->mJPEGContent.loadFromData(rawData); +} + + +void DocumentJPEGLoadedImpl::init() { + LOG(""); + ImageUtils::Orientation orientation=d->mJPEGContent.orientation(); + + if (MiscConfig::autoRotateImages() + && orientation!=ImageUtils::NOT_AVAILABLE + && orientation!=ImageUtils::NORMAL) + { + d->mJPEGContent.transform(orientation); + } + + DocumentLoadedImpl::init(); +} + + +DocumentJPEGLoadedImpl::~DocumentJPEGLoadedImpl() { + delete d; +} + + +void DocumentJPEGLoadedImpl::transform(ImageUtils::Orientation orientation) { + d->mJPEGContent.transform(orientation); + setImage(ImageUtils::transform(mDocument->image(), orientation)); + emitImageRectUpdated(); +} + + +QString DocumentJPEGLoadedImpl::localSave(QFile* file, const QCString& format) const { + if (qstrcmp(format, "JPEG")==0) { + LOG("JPEG Reset orientation"); + d->mJPEGContent.resetOrientation(); + if (!d->mJPEGContent.thumbnail().isNull()) { + d->mJPEGContent.setThumbnail( ImageUtils::scale( + mDocument->image(), 128, 128, ImageUtils::SMOOTH_FAST, QImage::ScaleMin)); + } + + LOG("JPEG Lossless save"); + if (!d->mJPEGContent.save(file)) { + return i18n("Could not save this JPEG file."); + } + } else { + QString msg=DocumentLoadedImpl::localSave(file, format); + if (!msg.isNull()) return msg; + } + + return QString::null; +} + + +QString DocumentJPEGLoadedImpl::comment() const { + return d->mJPEGContent.comment(); +} + +void DocumentJPEGLoadedImpl::setComment(const QString& comment) { + d->mJPEGContent.setComment(comment); +} + +QString DocumentJPEGLoadedImpl::aperture() const { + return d->mJPEGContent.aperture(); +} + +QString DocumentJPEGLoadedImpl::exposureTime() const { + return d->mJPEGContent.exposureTime(); +} + +QString DocumentJPEGLoadedImpl::iso() const { + return d->mJPEGContent.iso(); +} + +QString DocumentJPEGLoadedImpl::focalLength() const { + return d->mJPEGContent.focalLength(); +} + +Document::CommentState DocumentJPEGLoadedImpl::commentState() const { + return Document::WRITABLE; +} + + +} // namespace diff --git a/src/gvcore/documentjpegloadedimpl.h b/src/gvcore/documentjpegloadedimpl.h new file mode 100644 index 0000000..abb1e81 --- /dev/null +++ b/src/gvcore/documentjpegloadedimpl.h @@ -0,0 +1,62 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef DOCUMENTJPEGLOADEDIMPL_H +#define DOCUMENTJPEGLOADEDIMPL_H + +// Qt +#include <qimage.h> + +// Local +#include "documentloadedimpl.h" +namespace Gwenview { + +class Document; + +class DocumentJPEGLoadedImplPrivate; + +class DocumentJPEGLoadedImpl : public DocumentLoadedImpl { +Q_OBJECT +public: + DocumentJPEGLoadedImpl(Document* document, const QByteArray& rawData); + ~DocumentJPEGLoadedImpl(); + void init(); + + QString comment() const; + void setComment(const QString&); + Document::CommentState commentState() const; + + QString aperture() const; + QString exposureTime() const; + QString iso() const; + QString focalLength() const; + + void transform(ImageUtils::Orientation); + +protected: + QString localSave(QFile*, const QCString& format) const; + +private: + DocumentJPEGLoadedImplPrivate* d; +}; + +} // namespace +#endif /* DOCUMENTJPEGLOADEDIMPL_H */ + diff --git a/src/gvcore/documentloadedimpl.cpp b/src/gvcore/documentloadedimpl.cpp new file mode 100644 index 0000000..1d9456a --- /dev/null +++ b/src/gvcore/documentloadedimpl.cpp @@ -0,0 +1,198 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#include "documentloadedimpl.moc" + +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <sys/stat.h> + +// Qt +#include <qdir.h> +#include <qfileinfo.h> +#include <qtimer.h> + +// KDE +#include <kapplication.h> +#include <kdebug.h> +#include <kio/netaccess.h> +#include <klargefile.h> +#include <klocale.h> +#include <ktempfile.h> + +// Local +#include "imageutils/imageutils.h" +namespace Gwenview { + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + + +class DocumentLoadedImplPrivate { + int mSize; + QDateTime mModified; +}; + +DocumentLoadedImpl::DocumentLoadedImpl(Document* document) +: DocumentImpl(document) { + LOG(""); +} + + +void DocumentLoadedImpl::init() { + emit finished(true); +} + + +DocumentLoadedImpl::~DocumentLoadedImpl() { +} + + +void DocumentLoadedImpl::transform(ImageUtils::Orientation orientation) { + setImage(ImageUtils::transform(mDocument->image(), orientation)); + emitImageRectUpdated(); +} + + +QString DocumentLoadedImpl::save(const KURL& _url, const QCString& format) const { + if (!QImageIO::outputFormats().contains(format)) { + return i18n("Gwenview cannot write files in this format."); + } + + QString msg; + KURL url(_url); + + // Use the umask to determine default mode (will be used if the dest file + // does not exist) + int _umask=umask(0); + umask(_umask); + mode_t mode=0666 & ~_umask; + + if (url.isLocalFile()) { + // If the file is a link, dereference it but take care of circular + // links + QFileInfo info(url.path()); + if (info.isSymLink()) { + QStringList links; + while (info.isSymLink()) { + links.append(info.filePath()); + QString path=info.readLink(); + if (path[0]!='/') { + path=info.dirPath(true) + '/' + path; + } + path=QDir::cleanDirPath(path); + if (links.contains(path)) { + return i18n("This is a circular link."); + } + info.setFile(path); + } + url.setPath(info.filePath()); + } + + + // Make some quick tests on the file if it is local + if (info.exists() && ! info.isWritable()) { + return i18n("This file is read-only."); + } + + if (info.exists()) { + // Get current file mode + KDE_struct_stat st; + if (KDE_stat(QFile::encodeName(info.filePath()), &st)==0) { + mode=st.st_mode & 07777; + } else { + // This should not happen + kdWarning() << "Could not stat " << info.filePath() << endl; + } + + } else { + QFileInfo parent=QFileInfo(info.dirPath()); + if (!parent.isWritable()) { + return + i18n("The %1 folder is read-only.") + .arg(parent.filePath()); + } + } + } + + // Save the file to a tmp file + QString prefix; + if (url.isLocalFile()) { + // We set the prefix to url.path() so that the temp file is on the + // same partition as the destination file. If we don't do this, rename + // will fail + prefix=url.path(); + } + KTempFile tmp(prefix, "gwenview", mode); + tmp.setAutoDelete(true); + if (tmp.status()!=0) { + QString reason( strerror(tmp.status()) ); + return i18n("Could not create a temporary file.\nReason: %1.") + .arg(reason); + } + QFile* file=tmp.file(); + msg=localSave(file, format); + if (!msg.isNull()) return msg; + file->close(); + + if (tmp.status()!=0) { + QString reason( strerror(tmp.status()) ); + return i18n("Saving image to a temporary file failed.\nReason: %1.") + .arg(reason); + } + + QString tmpName=tmp.name(); + int tmpSize=QFileInfo(tmpName).size(); + setFileSize(tmpSize); + + // Move the tmp file to the final dest + if (url.isLocalFile()) { + if( ::rename( QFile::encodeName(tmpName), QFile::encodeName( url.path())) < 0 ) { + return i18n("Could not write to %1.").arg(url.path()); + } + } else { + if (!KIO::NetAccess::upload(tmp.name(), url, KApplication::kApplication()->mainWidget() )) { + return i18n("Could not upload the file to %1.").arg(url.prettyURL()); + } + } + + return QString::null; +} + + +QString DocumentLoadedImpl::localSave(QFile* file, const QCString& format) const { + QImageIO iio(file, format); + iio.setImage(mDocument->image()); + if (!iio.write()) { + return + i18n("An error happened while saving."); + } + return QString::null; +} + + +} // namespace diff --git a/src/gvcore/documentloadedimpl.h b/src/gvcore/documentloadedimpl.h new file mode 100644 index 0000000..27a8ea7 --- /dev/null +++ b/src/gvcore/documentloadedimpl.h @@ -0,0 +1,54 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef DOCUMENTLOADEDIMPL_H +#define DOCUMENTLOADEDIMPL_H + +// Qt +#include <qimage.h> + +// Local +#include "documentimpl.h" + +class QFile; + +namespace Gwenview { +class Document; + +class DocumentLoadedImpl : public DocumentImpl { +Q_OBJECT +public: + DocumentLoadedImpl(Document* document); + void init(); + ~DocumentLoadedImpl(); + + void transform(ImageUtils::Orientation); + QString save(const KURL&, const QCString& format) const; + + virtual MimeTypeUtils::Kind urlKind() const { return MimeTypeUtils::KIND_RASTER_IMAGE; } + virtual bool canBeSaved() const { return true; } + +protected: + virtual QString localSave(QFile* file, const QCString& format) const; +}; + +} // namespace +#endif /* DOCUMENTLOADEDIMPL_H */ + diff --git a/src/gvcore/documentloadingimpl.cpp b/src/gvcore/documentloadingimpl.cpp new file mode 100644 index 0000000..e148543 --- /dev/null +++ b/src/gvcore/documentloadingimpl.cpp @@ -0,0 +1,165 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "documentloadingimpl.moc" + +// Qt + +// KDE +#include <kmimetype.h> + +// Local +#include "imageloader.h" +#include "documentotherloadedimpl.h" +#include "documentanimatedloadedimpl.h" +#include "documentloadedimpl.h" +#include "documentjpegloadedimpl.h" +#include "mimetypeutils.h" + +namespace Gwenview { + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + +//--------------------------------------------------------------------- +// +// DocumentLoadingImplPrivate +// +//--------------------------------------------------------------------- + +class DocumentLoadingImplPrivate { +public: + DocumentLoadingImplPrivate() + : mLoader( NULL ) + {} + + ImageLoader* mLoader; +}; + +//--------------------------------------------------------------------- +// +// DocumentLoadingImpl +// +//--------------------------------------------------------------------- +DocumentLoadingImpl::DocumentLoadingImpl(Document* document) +: DocumentImpl(document) { + LOG(""); + d=new DocumentLoadingImplPrivate; +} + + +DocumentLoadingImpl::~DocumentLoadingImpl() { + LOG(""); + delete d; +} + + +void DocumentLoadingImpl::init() { + LOG(""); + d->mLoader = ImageLoader::loader( mDocument->url(), this, BUSY_LOADING ); + if (d->mLoader->urlKind()==MimeTypeUtils::KIND_FILE) { + LOG("urlKind already determined"); + switchToImpl(new DocumentOtherLoadedImpl(mDocument)); + return; + } + connect( d->mLoader, SIGNAL( urlKindDetermined()), SLOT( slotURLKindDetermined() )); + connect( d->mLoader, SIGNAL( sizeLoaded( int, int )), SLOT( sizeLoaded( int, int ))); + connect( d->mLoader, SIGNAL( imageChanged( const QRect& )), SLOT( imageChanged( const QRect& ))); + connect( d->mLoader, SIGNAL( imageLoaded( bool )), SLOT( imageLoaded( bool ))); + + // it's possible the loader already has the whole or at least part of the image loaded + QImage image = d->mLoader->processedImage(); + if (!image.isNull()) { + if( d->mLoader->frames().count() > 0 ) { + setImage( d->mLoader->frames().first().image); + emitImageRectUpdated(); + } else { + setImage(image); + QMemArray< QRect > rects = d->mLoader->loadedRegion().rects(); + for( unsigned int i = 0; i < rects.count(); ++i ) { + emit rectUpdated(rects[i]); + } + } + } + if( d->mLoader->completed()) imageLoaded( d->mLoader->frames().count() != 0 ); + // 'this' may be deleted here +} + + +void DocumentLoadingImpl::slotURLKindDetermined() { + LOG(""); + if (d->mLoader->urlKind()==MimeTypeUtils::KIND_FILE) { + switchToImpl(new DocumentOtherLoadedImpl(mDocument)); + } +} + + +void DocumentLoadingImpl::imageLoaded( bool ok ) { + LOG(""); + + QCString format = d->mLoader->imageFormat(); + if ( !ok || format.isEmpty()) { + // Unknown format, no need to go further + emit finished(false); + switchToImpl(new DocumentEmptyImpl(mDocument)); + return; + } + setImageFormat( format ); + setMimeType(d->mLoader->mimeType()); + + // Update file info + setFileSize(d->mLoader->rawData().size()); + + // Now we switch to a loaded implementation + if ( d->mLoader->frames().count() > 1 ) { + switchToImpl( new DocumentAnimatedLoadedImpl(mDocument, d->mLoader->frames())); + } else if ( format == "JPEG" ) { + switchToImpl( new DocumentJPEGLoadedImpl(mDocument, d->mLoader->rawData()) ); + } else { + switchToImpl(new DocumentLoadedImpl(mDocument)); + } +} + + +void DocumentLoadingImpl::imageChanged(const QRect& rect) { + LOG(rect); + setImage(d->mLoader->processedImage()); + emit rectUpdated(rect); +} + + +void DocumentLoadingImpl::sizeLoaded(int width, int height) { + LOG(width << "x" << height); + // Silence compiler + width=width; + height=height; + + setImage(d->mLoader->processedImage()); + emit sizeUpdated(); +} + +} // namespace diff --git a/src/gvcore/documentloadingimpl.h b/src/gvcore/documentloadingimpl.h new file mode 100644 index 0000000..a17b25d --- /dev/null +++ b/src/gvcore/documentloadingimpl.h @@ -0,0 +1,55 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef DOCUMENTLOADINGIMPL_H +#define DOCUMENTLOADINGIMPL_H + +// Local +#include "documentimpl.h" +#include "mimetypeutils.h" + +namespace Gwenview { + +class Document; + +class DocumentLoadingImplPrivate; + +class DocumentLoadingImpl : public DocumentImpl { +Q_OBJECT +public: + DocumentLoadingImpl(Document* document); + ~DocumentLoadingImpl(); + virtual void init(); + virtual MimeTypeUtils::Kind urlKind() const { return MimeTypeUtils::KIND_RASTER_IMAGE; } + virtual bool canBeSaved() const { return false; } + +private: + DocumentLoadingImplPrivate* d; + +private slots: + void slotURLKindDetermined(); + void sizeLoaded(int, int); + void imageChanged(const QRect&); + void imageLoaded( bool ok ); +}; + +} // namespace +#endif /* DOCUMENTLOADINGIMPL_H */ + diff --git a/src/gvcore/documentotherloadedimpl.cpp b/src/gvcore/documentotherloadedimpl.cpp new file mode 100644 index 0000000..1e3674b --- /dev/null +++ b/src/gvcore/documentotherloadedimpl.cpp @@ -0,0 +1,58 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2006 Aurelien Gateau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Self +#include "documentotherloadedimpl.h" + +// KDE +#include <kdebug.h> +#include <kfilemetainfo.h> + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + +namespace Gwenview { + +int DocumentOtherLoadedImpl::duration() const { + KFileMetaInfo fmi(mDocument->url()); + if (!fmi.isValid()) { + LOG("No meta info available for " << mDocument->url()); + return 0; + } + + KFileMetaInfoItem item=fmi.item("Length"); + if (!item.isValid()) { + kdWarning() << "Can't adjust slideshow time: meta info for " << mDocument->url() << " does not contain 'Length' information."; + return 0; + } + + int length = item.value().toInt(); + LOG("Length for " << mDocument->url() << " is " << length); + + return length; +} + +} // namespace diff --git a/src/gvcore/documentotherloadedimpl.h b/src/gvcore/documentotherloadedimpl.h new file mode 100644 index 0000000..d9ffbdb --- /dev/null +++ b/src/gvcore/documentotherloadedimpl.h @@ -0,0 +1,52 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2006 Aurelien Gateau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef DOCUMENTOTHERLOADEDIMPL_H +#define DOCUMENTOTHERLOADEDIMPL_H + +// Local +#include "document.h" +#include "documentimpl.h" + +namespace Gwenview { +class Document; + +class DocumentOtherLoadedImpl : public DocumentImpl { +public: + DocumentOtherLoadedImpl(Document* document) + : DocumentImpl(document) { + setImage(QImage()); + setImageFormat(0); + } + + void init() { + emit finished(true); + } + + virtual MimeTypeUtils::Kind urlKind() const { return MimeTypeUtils::KIND_FILE; } + + virtual bool canBeSaved() const { return false; } + + virtual int duration() const; +}; + +} // namespace +#endif /* DOCUMENTOTHERLOADEDIMPL_H */ + diff --git a/src/gvcore/dragpixmapgenerator.h b/src/gvcore/dragpixmapgenerator.h new file mode 100644 index 0000000..7699eed --- /dev/null +++ b/src/gvcore/dragpixmapgenerator.h @@ -0,0 +1,179 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef DRAGPIXMAPGENERATOR_H +#define DRAGPIXMAPGENERATOR_H + +// Qt +#include <qapplication.h> +#include <qpixmap.h> +#include <qtooltip.h> +#include <qvaluelist.h> +#include <qvaluevector.h> + +// KDE +#include <klocale.h> + +namespace Gwenview { + + +template <class T> +class DragPixmapGenerator; + + +template <class T> +class DragPixmapItemDrawer { +public: + DragPixmapItemDrawer() + : mGenerator(0) {} + virtual ~DragPixmapItemDrawer() {} + virtual void setGenerator(DragPixmapGenerator<T>* generator) { + mGenerator = generator; + } + virtual QSize itemSize(T)=0; + virtual void drawItem(QPainter*, int left, int top, T)=0; + virtual int spacing() const { + return 0; + } + +protected: + DragPixmapGenerator<T>* mGenerator; +}; + + +QPixmap dragPixmapGeneratorHelper(QValueVector<QPixmap> pixmapVector); + + +template <class T> +class DragPixmapGenerator { +public: + /** Offset between cursor and dragged images */ + static const uint DRAG_OFFSET=16; + + + /** Maximum width of an item painted by DragPixmapItemDrawer */ + static const int ITEM_MAX_WIDTH=128; + + static const int MAX_HEIGHT=200; + + static const int DRAG_MARGIN=4; + + + DragPixmapGenerator() + : mPixmapWidth(0) {} + + + void addItem(const T& item) { + mItemList << item; + } + + + int maxWidth() const { + return ITEM_MAX_WIDTH; + } + + + /** + * Returns the width of the generated pixmap, not including the margin. + * To be used by DragPixmapItemDrawer<T>::drawItem. Should not be used + * anywhere else since this value is initialized in generate(). + */ + int pixmapWidth() const { + return mPixmapWidth; + } + + void setItemDrawer(DragPixmapItemDrawer<T>* drawer) { + mItemDrawer = drawer; + drawer->setGenerator(this); + } + + QPixmap generate() { + int width = 0, height = 0; + int dragCount = 0; + int spacing = mItemDrawer->spacing(); + bool listCropped; + QString bottomText; + QFontMetrics fm = QApplication::fontMetrics(); + + // Compute pixmap size and update dragCount + QValueListIterator<T> it = mItemList.begin(); + QValueListIterator<T> end = mItemList.end(); + height = -spacing; + for (; it!= end && height < MAX_HEIGHT; ++dragCount, ++it) { + QSize itemSize = mItemDrawer->itemSize(*it); + Q_ASSERT(itemSize.width() <= ITEM_MAX_WIDTH); + + width = QMAX(width, itemSize.width()); + height += itemSize.height() + spacing; + } + + listCropped = it != end; + if (listCropped) { + // If list has been cropped, leave space for item count text + height += fm.height(); + bottomText = i18n("%1 items").arg(mItemList.count()); + width = QMAX(width, fm.width("... " + bottomText)); + } + + mPixmapWidth = width; + + // Init pixmap + QPixmap pixmap(width + 2*DRAG_MARGIN, height + 2*DRAG_MARGIN); + QColorGroup cg = QToolTip::palette().active(); + + pixmap.fill(cg.base()); + QPainter painter(&pixmap); + + // Draw border + painter.setPen(cg.dark()); + painter.drawRect(pixmap.rect()); + + // Draw items + it = mItemList.begin(); + height = DRAG_MARGIN; + for (int pos=0; pos < dragCount; ++pos, ++it) { + mItemDrawer->drawItem(&painter, DRAG_MARGIN, height, *it); + height += mItemDrawer->itemSize(*it).height() + spacing; + } + + // Draw text if necessary + if (listCropped) { + int posY= height + fm.ascent(); + painter.drawText(DRAG_MARGIN, posY, "..."); + int offset = width - fm.width(bottomText); + painter.drawText(DRAG_MARGIN + offset, posY, bottomText); + } + painter.end(); + + return pixmap; + } + + +private: + QValueList<T> mItemList; + DragPixmapItemDrawer<T>* mItemDrawer; + int mPixmapWidth; +}; + + +} // namespace + + +#endif /* DRAGPIXMAPGENERATOR_H */ diff --git a/src/gvcore/externaltoolaction.cpp b/src/gvcore/externaltoolaction.cpp new file mode 100644 index 0000000..db47ff8 --- /dev/null +++ b/src/gvcore/externaltoolaction.cpp @@ -0,0 +1,56 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Qt +#include <qdir.h> + +// KDE +#include <kdebug.h> +#include <krun.h> +#include <kservice.h> + +// Local +#include "externaltoolaction.moc" +namespace Gwenview { + +ExternalToolAction::ExternalToolAction( + QObject* parent, const KService* service, + const KURL::List& urls) +: KAction(parent) +, mService(service) +, mURLs(urls) +{ + setText(service->name()); + setIcon(service->icon()); + connect(this, SIGNAL(activated()), + this, SLOT(openExternalTool()) ); + +} + + +void ExternalToolAction::openExternalTool() { + QString dir=mURLs.first().directory(); + QDir::setCurrent(dir); + + QStringList args=KRun::processDesktopExec(*mService, mURLs, true); + KRun::runCommand(args.join(" "), mService->name(), mService->icon()); +} + +} // namespace diff --git a/src/gvcore/externaltoolaction.h b/src/gvcore/externaltoolaction.h new file mode 100644 index 0000000..7f62160 --- /dev/null +++ b/src/gvcore/externaltoolaction.h @@ -0,0 +1,50 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef EXTERNALTOOLACTION_H +#define EXTERNALTOOLACTION_H + +// KDE +#include <kaction.h> +#include <kurl.h> + +class KService; + +namespace Gwenview { +/** + * A specialized version of KAction, which is aware of the tool to run as well + * as the urls to call it with. + */ +class ExternalToolAction : public KAction { +Q_OBJECT +public: + ExternalToolAction(QObject* parent, const KService*, const KURL::List&); + +private slots: + void openExternalTool(); + +private: + const KService* mService; + const KURL::List& mURLs; +}; + +} // namespace +#endif + diff --git a/src/gvcore/externaltoolcontext.cpp b/src/gvcore/externaltoolcontext.cpp new file mode 100644 index 0000000..fef6ac0 --- /dev/null +++ b/src/gvcore/externaltoolcontext.cpp @@ -0,0 +1,80 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#include "externaltoolcontext.moc" + +// KDE +#include <kaction.h> +#include <kapplication.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kpopupmenu.h> +#include <krun.h> +#include <kservice.h> + +// Local +#include "externaltoolaction.h" +#include "externaltooldialog.h" + +namespace Gwenview { + +ExternalToolContext::ExternalToolContext( + QObject* parent, + std::list<KService*> services, + KURL::List urls) +: QObject(parent) +, mServices(services) +, mURLs(urls) +{} + + +void ExternalToolContext::showExternalToolDialog() { + ExternalToolDialog* dialog=new ExternalToolDialog(kapp->mainWidget()); + dialog->show(); +} + + +void ExternalToolContext::showOpenWithDialog() { + KRun::displayOpenWithDialog(mURLs, false /*tempFiles*/); +} + + +QPopupMenu* ExternalToolContext::popupMenu() { + QPopupMenu* menu=new QPopupMenu(); + std::list<KService*>::const_iterator it=mServices.begin(); + std::list<KService*>::const_iterator itEnd=mServices.end(); + for (;it!=itEnd; ++it) { + ExternalToolAction* action= + new ExternalToolAction(this, *it, mURLs); + action->plug(menu); + } + + menu->insertSeparator(); + menu->insertItem(i18n("Other..."), + this, SLOT(showOpenWithDialog()) ); + menu->insertItem( + SmallIcon("configure"), + i18n("Configure External Tools..."), + this, SLOT(showExternalToolDialog()) ); + + return menu; +} + +} // namespace diff --git a/src/gvcore/externaltoolcontext.h b/src/gvcore/externaltoolcontext.h new file mode 100644 index 0000000..a253ede --- /dev/null +++ b/src/gvcore/externaltoolcontext.h @@ -0,0 +1,59 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef EXTERNALTOOLCONTEXT_H +#define EXTERNALTOOLCONTEXT_H + +// STL +#include <list> + +// Qt +#include <qobject.h> + +// KDE +#include <kurl.h> + +// Local +#include "libgwenview_export.h" + +class QPopupMenu; +class KService; + +namespace Gwenview { +class LIBGWENVIEW_EXPORT ExternalToolContext : public QObject { +Q_OBJECT +public: + ExternalToolContext(QObject* parent, + std::list<KService*> services, + KURL::List urls); + QPopupMenu* popupMenu(); + +private slots: + void showExternalToolDialog(); + void showOpenWithDialog(); + +private: + std::list<KService*> mServices; + KURL::List mURLs; +}; + +} // namespace +#endif + diff --git a/src/gvcore/externaltooldialog.cpp b/src/gvcore/externaltooldialog.cpp new file mode 100644 index 0000000..89ffb79 --- /dev/null +++ b/src/gvcore/externaltooldialog.cpp @@ -0,0 +1,355 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Qt +#include <qbuttongroup.h> +#include <qheader.h> +#include <qwhatsthis.h> + +// KDE +#include <kdebug.h> +#include <kdesktopfile.h> +#include <kicondialog.h> +#include <kiconloader.h> +#include <kimageio.h> +#include <klineedit.h> +#include <klistview.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <krun.h> +#include <kurllabel.h> +#include <kurlrequester.h> + +// Local +#include "archive.h" +#include "mimetypeutils.h" +#include "externaltoolmanager.h" +#include "externaltooldialogbase.h" +#include "externaltooldialog.moc" +namespace Gwenview { + + +enum { ID_ALL_IMAGES=0, ID_ALL_FILES, ID_CUSTOM }; + + +class ToolListViewItem : public KListViewItem { +public: + ToolListViewItem(KListView* parent, const QString& label) + : KListViewItem(parent, label), mDesktopFile(0L) {} + + void setDesktopFile(KDesktopFile* df) { + mDesktopFile=df; + } + + KDesktopFile* desktopFile() const { + return mDesktopFile; + } + +private: + KDesktopFile* mDesktopFile; +}; + + +struct ExternalToolDialogPrivate { + ExternalToolDialogBase* mContent; + QPtrList<KDesktopFile> mDeletedTools; + ToolListViewItem* mSelectedItem; + + + ExternalToolDialogPrivate() + : mSelectedItem(0L) {} + + void fillMimeTypeListView() { + QStringList mimeTypes=MimeTypeUtils::rasterImageMimeTypes(); + mimeTypes.append("inode/directory"); + mimeTypes+=Archive::mimeTypes(); + + QStringList::const_iterator it=mimeTypes.begin(); + for(; it!=mimeTypes.end(); ++it) { + (void)new QCheckListItem(mContent->mMimeTypeListView, *it, QCheckListItem::CheckBox); + } + } + + + void fillToolListView() { + QDict<KDesktopFile> desktopFiles=ExternalToolManager::instance()->desktopFiles(); + + QDictIterator<KDesktopFile> it(desktopFiles); + for (; it.current(); ++it) { + ToolListViewItem* item=new ToolListViewItem(mContent->mToolListView, it.current()->readName()); + item->setPixmap(0, SmallIcon(it.current()->readIcon()) ); + item->setDesktopFile(it.current()); + } + mContent->mToolListView->setSortColumn(0); + mContent->mToolListView->sort(); + } + + + void writeServiceTypes(KDesktopFile* desktopFile) { + QButton* button=mContent->mFileAssociationGroup->selected(); + if (!button) { + desktopFile->writeEntry("ServiceTypes", "*"); + return; + } + + int id=mContent->mFileAssociationGroup->id(button); + if (id==ID_ALL_IMAGES) { + desktopFile->writeEntry("ServiceTypes", "image/*"); + return; + } + if (id==ID_ALL_FILES) { + desktopFile->writeEntry("ServiceTypes", "*"); + return; + } + + QStringList mimeTypes; + QListViewItem* item=mContent->mMimeTypeListView->firstChild(); + for (; item; item=item->nextSibling()) { + if (static_cast<QCheckListItem*>(item)->isOn()) { + mimeTypes.append(item->text(0)); + } + } + desktopFile->writeEntry("ServiceTypes", mimeTypes); + } + + + bool saveChanges() { + if (!mSelectedItem) return true; + + // Check name + QString name=mContent->mName->text().stripWhiteSpace(); + if (name.isEmpty()) { + KMessageBox::sorry(mContent, i18n("The tool name cannot be empty")); + return false; + } + + QListViewItem* item=mContent->mToolListView->firstChild(); + for (; item; item=item->nextSibling()) { + if (item==mSelectedItem) continue; + if (name==item->text(0)) { + KMessageBox::sorry(mContent, i18n("There is already a tool named \"%1\"").arg(name)); + return false; + } + } + + // Save data + KDesktopFile* desktopFile=mSelectedItem->desktopFile(); + if (desktopFile) { + if (desktopFile->isReadOnly()) { + desktopFile=ExternalToolManager::instance()->editSystemDesktopFile(desktopFile); + mSelectedItem->setDesktopFile(desktopFile); + } + } else { + desktopFile=ExternalToolManager::instance()->createUserDesktopFile(name); + mSelectedItem->setDesktopFile(desktopFile); + } + desktopFile->writeEntry("Name", name); + desktopFile->writeEntry("Icon", mContent->mIconButton->icon()); + desktopFile->writeEntry("Exec", mContent->mCommand->url()); + writeServiceTypes(desktopFile); + + mSelectedItem->setPixmap(0, SmallIcon(mContent->mIconButton->icon()) ); + mSelectedItem->setText(0, name); + + return true; + } + + + void updateFileAssociationGroup(const QStringList& serviceTypes) { + QListViewItem* item=mContent->mMimeTypeListView->firstChild(); + for (; item; item=item->nextSibling()) { + static_cast<QCheckListItem*>(item)->setOn(false); + } + + if (serviceTypes.size()==0) { + mContent->mFileAssociationGroup->setButton(ID_ALL_FILES); + return; + } + if (serviceTypes.size()==1) { + QString serviceType=serviceTypes[0]; + if (serviceType=="image/*") { + mContent->mFileAssociationGroup->setButton(ID_ALL_IMAGES); + return; + } + if (serviceType=="*") { + mContent->mFileAssociationGroup->setButton(ID_ALL_FILES); + return; + } + } + + mContent->mFileAssociationGroup->setButton(ID_CUSTOM); + QStringList::ConstIterator it=serviceTypes.begin(); + for (;it!=serviceTypes.end(); ++it) { + QListViewItem* item= + mContent->mMimeTypeListView->findItem(*it, 0, Qt::ExactMatch); + if (item) static_cast<QCheckListItem*>(item)->setOn(true); + } + } + + + void updateDetails() { + mContent->mDetails->setEnabled(mSelectedItem!=0); + + if (mSelectedItem) { + KDesktopFile* desktopFile=mSelectedItem->desktopFile(); + if (desktopFile) { + mContent->mName->setText(desktopFile->readName()); + mContent->mCommand->setURL(desktopFile->readEntry("Exec")); + mContent->mIconButton->setIcon(desktopFile->readIcon()); + QStringList serviceTypes=desktopFile->readListEntry("ServiceTypes"); + updateFileAssociationGroup(serviceTypes); + return; + } + } + + mContent->mName->setText(QString::null); + mContent->mCommand->setURL(QString::null); + mContent->mIconButton->setIcon(QString::null); + mContent->mFileAssociationGroup->setButton(ID_ALL_IMAGES); + } + + bool apply() { + if (!saveChanges()) return false; + QPtrListIterator<KDesktopFile> it(mDeletedTools); + for(; it.current(); ++it) { + ExternalToolManager::instance()->hideDesktopFile(it.current()); + } + ExternalToolManager::instance()->updateServices(); + return true; + } +}; + + +/** + * This event filter object is here to prevent the user from selecting a + * different tool in the tool list view if the current tool could not be saved. + */ +class ToolListViewFilterObject : public QObject { + ExternalToolDialogPrivate* d; +public: + ToolListViewFilterObject(QObject* parent, ExternalToolDialogPrivate* _d) + : QObject(parent), d(_d) {} + + bool eventFilter(QObject*, QEvent* event) { + if (event->type()!=QEvent::MouseButtonPress) return false; + return !d->saveChanges(); + } +}; + + +ExternalToolDialog::ExternalToolDialog(QWidget* parent) +: KDialogBase( + parent,0, false, QString::null, KDialogBase::Ok|KDialogBase::Apply|KDialogBase::Cancel, + KDialogBase::Ok, true) +{ + setWFlags(getWFlags() | Qt::WDestructiveClose); + d=new ExternalToolDialogPrivate; + + d->mContent=new ExternalToolDialogBase(this); + setMainWidget(d->mContent); + setCaption(d->mContent->caption()); + + d->mContent->mToolListView->header()->hide(); + d->mContent->mMimeTypeListView->header()->hide(); + + d->fillMimeTypeListView(); + d->fillToolListView(); + d->mContent->mToolListView->viewport()->installEventFilter( + new ToolListViewFilterObject(this, d)); + + connect( d->mContent->mToolListView, SIGNAL(selectionChanged(QListViewItem*)), + this, SLOT(slotSelectionChanged(QListViewItem*)) ); + connect( d->mContent->mAddButton, SIGNAL(clicked()), + this, SLOT(addTool()) ); + connect( d->mContent->mDeleteButton, SIGNAL(clicked()), + this, SLOT(deleteTool()) ); + connect( d->mContent->mHelp, SIGNAL(leftClickedURL()), + this, SLOT(showCommandHelp()) ); + connect( d->mContent->mMoreTools, SIGNAL(leftClickedURL(const QString&)), + this, SLOT(openURL(const QString&)) ); + + KListView* view=d->mContent->mToolListView; + if (view->firstChild()) { + view->setSelected(view->firstChild(), true); + } + d->updateDetails(); +} + + +ExternalToolDialog::~ExternalToolDialog() { + delete d; +} + + +void ExternalToolDialog::slotOk() { + if (!d->apply()) return; + accept(); +} + + +void ExternalToolDialog::slotApply() { + d->apply(); +} + + +void ExternalToolDialog::slotCancel() { + KDialogBase::slotCancel(); +} + + +void ExternalToolDialog::slotSelectionChanged(QListViewItem* item) { + d->mSelectedItem=static_cast<ToolListViewItem*>(item); + d->updateDetails(); +} + + +void ExternalToolDialog::addTool() { + KListView* view=d->mContent->mToolListView; + QString name=i18n("<Unnamed tool>"); + ToolListViewItem* item=new ToolListViewItem(view, name); + view->setSelected(item, true); +} + + +void ExternalToolDialog::deleteTool() { + KListView* view=d->mContent->mToolListView; + ToolListViewItem* item=static_cast<ToolListViewItem*>(view->selectedItem()); + if (!item) return; + + KDesktopFile* desktopFile=item->desktopFile(); + delete item; + d->mDeletedTools.append(desktopFile); + d->mSelectedItem=0L; + d->updateDetails(); +} + + +void ExternalToolDialog::showCommandHelp() { + KURLRequester* lbl=d->mContent->mCommand; + QWhatsThis::display(QWhatsThis::textFor(lbl), + lbl->mapToGlobal( lbl->rect().bottomRight() ) ); +} + + +void ExternalToolDialog::openURL(const QString& url) { + new KRun(KURL(url)); +} + +} // namespace diff --git a/src/gvcore/externaltooldialog.h b/src/gvcore/externaltooldialog.h new file mode 100644 index 0000000..306cb59 --- /dev/null +++ b/src/gvcore/externaltooldialog.h @@ -0,0 +1,59 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef EXTERNALTOOLDIALOG_H +#define EXTERNALTOOLDIALOG_H + +// KDE +#include <kdialogbase.h> + +// Our includes +#include "libgwenview_export.h" + +namespace Gwenview { + + +class ExternalToolDialogPrivate; +class LIBGWENVIEW_EXPORT ExternalToolDialog : public KDialogBase { +Q_OBJECT +public: + ExternalToolDialog(QWidget* parent); + ~ExternalToolDialog(); + +protected slots: + void slotOk(); + void slotApply(); + void slotCancel(); + +private slots: + void slotSelectionChanged(QListViewItem*); + void addTool(); + void deleteTool(); + void showCommandHelp(); + void openURL(const QString& url); + +private: + ExternalToolDialogPrivate* d; +}; + + +} // namespace +#endif + diff --git a/src/gvcore/externaltooldialogbase.ui b/src/gvcore/externaltooldialogbase.ui new file mode 100644 index 0000000..7b17ed4 --- /dev/null +++ b/src/gvcore/externaltooldialogbase.ui @@ -0,0 +1,373 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>ExternalToolDialogBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>ExternalToolDialogBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>723</width> + <height>361</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>720</width> + <height>0</height> + </size> + </property> + <property name="caption"> + <string>Configure External Tools</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <spacer row="1" column="2"> + <property name="name"> + <cstring>spacer3_2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>487</width> + <height>16</height> + </size> + </property> + </spacer> + <widget class="QPushButton" row="1" column="1"> + <property name="name"> + <cstring>mDeleteButton</cstring> + </property> + <property name="text"> + <string>&Delete</string> + </property> + </widget> + <widget class="QPushButton" row="1" column="0"> + <property name="name"> + <cstring>mAddButton</cstring> + </property> + <property name="text"> + <string>&Add</string> + </property> + </widget> + <widget class="KListView" row="0" column="0" rowspan="1" colspan="2"> + <column> + <property name="text"> + <string>Name</string> + </property> + <property name="clickable"> + <bool>false</bool> + </property> + <property name="resizable"> + <bool>true</bool> + </property> + </column> + <property name="name"> + <cstring>mToolListView</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="fullWidth"> + <bool>true</bool> + </property> + </widget> + <widget class="KURLLabel" row="1" column="3"> + <property name="name"> + <cstring>mMoreTools</cstring> + </property> + <property name="text"> + <string>Get more tools</string> + </property> + <property name="url" stdset="0"> + <string>http://gwenview.sourceforge.net/tools</string> + </property> + <property name="useTips"> + <bool>true</bool> + </property> + </widget> + <widget class="QFrame" row="0" column="2" rowspan="1" colspan="2"> + <property name="name"> + <cstring>mDetails</cstring> + </property> + <property name="frameShape"> + <enum>NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>Raised</enum> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="QLabel" row="1" column="0"> + <property name="name"> + <cstring>textLabel1_2</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <bold>1</bold> + </font> + </property> + <property name="text"> + <string>File Associations</string> + </property> + </widget> + <widget class="QLayoutWidget" row="0" column="0"> + <property name="name"> + <cstring>layout3</cstring> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="KIconButton" row="0" column="3" rowspan="2" colspan="1"> + <property name="name"> + <cstring>mIconButton</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>1</hsizetype> + <vsizetype>1</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string></string> + </property> + </widget> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>textLabel1</cstring> + </property> + <property name="text"> + <string>Name:</string> + </property> + </widget> + <widget class="KURLLabel" row="1" column="2"> + <property name="name"> + <cstring>mHelp</cstring> + </property> + <property name="text"> + <string>Help</string> + </property> + <property name="url" stdset="0"> + <string></string> + </property> + </widget> + <widget class="QLabel" row="1" column="0"> + <property name="name"> + <cstring>textLabel2</cstring> + </property> + <property name="text"> + <string>Command:</string> + </property> + </widget> + <widget class="KLineEdit" row="0" column="1" rowspan="1" colspan="2"> + <property name="name"> + <cstring>mName</cstring> + </property> + </widget> + <widget class="KURLRequester" row="1" column="1"> + <property name="name"> + <cstring>mCommand</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="whatsThis" stdset="0"> + <string><qt> +You can use keywords in the Command field: +<ul> +<li><tt>%u</tt>: Current URL.</li> +<li><tt>%U</tt>: Current URLs. Use this if the tool can handle multiple files.</li> +<li><tt>%f</tt>: Current file. Use this if the tool can't handle URLs.</li> +<li><tt>%F</tt>: Same as %f, but for multiple files.</li> +</ul> +</qt></string> + </property> + </widget> + </grid> + </widget> + <widget class="QButtonGroup" row="2" column="0"> + <property name="name"> + <cstring>mFileAssociationGroup</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>NoFrame</enum> + </property> + <property name="title"> + <string></string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <property name="spacing"> + <number>6</number> + </property> + <widget class="QRadioButton" row="0" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>radioButton1</cstring> + </property> + <property name="text"> + <string>All images</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <property name="buttonGroupId"> + <number>0</number> + </property> + </widget> + <widget class="QRadioButton" row="1" column="0" rowspan="1" colspan="2"> + <property name="name"> + <cstring>radioButton2</cstring> + </property> + <property name="text"> + <string>All files</string> + </property> + <property name="buttonGroupId"> + <number>1</number> + </property> + </widget> + <widget class="QRadioButton" row="2" column="0"> + <property name="name"> + <cstring>radioButton3</cstring> + </property> + <property name="text"> + <string>Custom:</string> + </property> + </widget> + <spacer row="3" column="0"> + <property name="name"> + <cstring>spacer3</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>21</width> + <height>140</height> + </size> + </property> + </spacer> + <widget class="KListView" row="2" column="1" rowspan="2" colspan="1"> + <column> + <property name="text"> + <string>Mime Type</string> + </property> + <property name="clickable"> + <bool>false</bool> + </property> + <property name="resizable"> + <bool>true</bool> + </property> + </column> + <property name="name"> + <cstring>mMimeTypeListView</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="fullWidth"> + <bool>true</bool> + </property> + </widget> + </grid> + </widget> + </grid> + </widget> + </grid> +</widget> +<customwidgets> +</customwidgets> +<connections> + <connection> + <sender>radioButton3</sender> + <signal>toggled(bool)</signal> + <receiver>mMimeTypeListView</receiver> + <slot>setEnabled(bool)</slot> + </connection> +</connections> +<tabstops> + <tabstop>mToolListView</tabstop> + <tabstop>mAddButton</tabstop> + <tabstop>mDeleteButton</tabstop> + <tabstop>mName</tabstop> + <tabstop>mCommand</tabstop> + <tabstop>radioButton1</tabstop> + <tabstop>mMimeTypeListView</tabstop> +</tabstops> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>klistview.h</includehint> + <includehint>kurllabel.h</includehint> + <includehint>kicondialog.h</includehint> + <includehint>kurllabel.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>klistview.h</includehint> +</includehints> +</UI> diff --git a/src/gvcore/externaltoolmanager.cpp b/src/gvcore/externaltoolmanager.cpp new file mode 100644 index 0000000..1df124f --- /dev/null +++ b/src/gvcore/externaltoolmanager.cpp @@ -0,0 +1,294 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// STL +#include <list> + +// Qt +#include <qdir.h> + +// KDE +#include <kdebug.h> +#include <kdesktopfile.h> +#include <kglobal.h> +#include <kservice.h> +#include <kstandarddirs.h> +#include <kurl.h> + +// Local +#include "externaltoolcontext.h" +#include "externaltoolmanager.h" +namespace Gwenview { + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + +// Helper functions for createContextInternal +inline bool mimeTypeMatches(const QString& candidate, const QString& reference) { + if (reference=="*") return true; + + if (reference.right(1)=="*") { + return candidate.startsWith(reference.left(reference.length()-1)); + } else { + return candidate==reference; + } +} + +inline bool isSubSetOf(const QStringList& subSet, const QStringList& set) { + // Simple implementation, might need some optimization + QStringList::ConstIterator itSubSet=subSet.begin(); + QStringList::ConstIterator itSetBegin=set.begin(); + QStringList::ConstIterator itSetEnd=set.end(); + + for (; itSubSet!=subSet.end(); ++itSubSet) { + bool matchFound=false; + QStringList::ConstIterator itSet=itSetBegin; + for (; itSet!=itSetEnd; ++itSet) { + if (mimeTypeMatches(*itSubSet, *itSet)) { + matchFound=true; + break; + } + } + if (!matchFound) { + return false; + } + } + return true; +} + + +struct ExternalToolManagerPrivate { + QDict<KDesktopFile> mDesktopFiles; + QPtrList<KService> mServices; + QString mUserToolDir; + + /** + * Helper function for createContextInternal + */ + static bool compareKServicePtrByName(const KService* s1, const KService* s2) { + Q_ASSERT(s1); + Q_ASSERT(s2); + return s1->name() < s2->name(); + } + + ExternalToolContext* createContextInternal( + QObject* parent, const KURL::List& urls, const QStringList& mimeTypes) + { + bool onlyOneURL=urls.size()==1; + + // Only add to selectionServices the services which can handle all the + // different mime types present in the selection + // + // We use std::list instead of QValueList because it's not possible to + // pass a sort functor to qHeapSort + std::list<KService*> selectionServices; + QPtrListIterator<KService> it(mServices); + for (; it.current(); ++it) { + KService* service=it.current(); + if (!onlyOneURL && !service->allowMultipleFiles()) { + continue; + } + + QStringList serviceTypes=service->serviceTypes(); + if (isSubSetOf(mimeTypes, serviceTypes)) { + selectionServices.push_back(service); + } + } + selectionServices.sort(compareKServicePtrByName); + + return new ExternalToolContext(parent, selectionServices, urls); + } + +}; + + +// Helper function for ctor +void loadDesktopFiles(QDict<KDesktopFile>& dict, const QString& dirString) { + QDir dir(dirString); + QStringList list=dir.entryList("*.desktop"); + QStringList::ConstIterator it=list.begin(); + for (; it!=list.end(); ++it) { + KDesktopFile* df=new KDesktopFile( dir.filePath(*it) ); + dict.insert(*it, df); + } +} + +inline QString addSlash(const QString& _str) { + QString str(_str); + if (str.right(1)!="/") str.append('/'); + return str; +} + +ExternalToolManager::ExternalToolManager() { + d=new ExternalToolManagerPrivate; + + // Getting dirs + d->mUserToolDir=KGlobal::dirs()->saveLocation("appdata", "tools"); + d->mUserToolDir=addSlash(d->mUserToolDir); + Q_ASSERT(!d->mUserToolDir.isEmpty()); + LOG("d->mUserToolDir:" << d->mUserToolDir); + + QStringList dirs=KGlobal::dirs()->findDirs("appdata", "tools"); + LOG("dirs:" << dirs.join(",")); + + // Loading desktop files + QDict<KDesktopFile> systemDesktopFiles; + QStringList::ConstIterator it; + for (it=dirs.begin(); it!=dirs.end(); ++it) { + if (addSlash(*it)==d->mUserToolDir) { + LOG("skipping " << *it); + continue; + } + LOG("loading system desktop files from " << *it); + loadDesktopFiles(systemDesktopFiles, *it); + } + QDict<KDesktopFile> userDesktopFiles; + loadDesktopFiles(userDesktopFiles, d->mUserToolDir); + + // Merge system and user desktop files into our KDesktopFile dictionary + d->mDesktopFiles=systemDesktopFiles; + d->mDesktopFiles.setAutoDelete(true); + QDictIterator<KDesktopFile> itDict(userDesktopFiles); + + for (; itDict.current(); ++itDict) { + QString name=itDict.currentKey(); + KDesktopFile* df=itDict.current(); + if (d->mDesktopFiles.find(name)) { + d->mDesktopFiles.remove(name); + } + if (df->readBoolEntry("Hidden")) { + delete df; + } else { + d->mDesktopFiles.insert(name, df); + } + } + + d->mServices.setAutoDelete(true); + updateServices(); +} + + +ExternalToolManager::~ExternalToolManager() { + delete d; +} + + +ExternalToolManager* ExternalToolManager::instance() { + static ExternalToolManager manager; + return &manager; +} + + +void ExternalToolManager::updateServices() { + d->mServices.clear(); + QDictIterator<KDesktopFile> it(d->mDesktopFiles); + for (; it.current(); ++it) { + KDesktopFile* desktopFile=it.current(); + // If sync() is not called, KService does not read up to date content + desktopFile->sync(); + KService* service=new KService(desktopFile); + d->mServices.append(service); + } +} + + +QDict<KDesktopFile>& ExternalToolManager::desktopFiles() const { + return d->mDesktopFiles; +} + + +void ExternalToolManager::hideDesktopFile(KDesktopFile* desktopFile) { + QFileInfo fi(desktopFile->fileName()); + QString name=QString("%1.desktop").arg( fi.baseName(true) ); + d->mDesktopFiles.take(name); + + if (desktopFile->isReadOnly()) { + delete desktopFile; + desktopFile=new KDesktopFile(d->mUserToolDir + "/" + name, false); + } + desktopFile->writeEntry("Hidden", true); + desktopFile->sync(); + delete desktopFile; +} + + +KDesktopFile* ExternalToolManager::editSystemDesktopFile(const KDesktopFile* desktopFile) { + Q_ASSERT(desktopFile); + QFileInfo fi(desktopFile->fileName()); + + QString name=fi.baseName(true); + d->mDesktopFiles.remove(QString("%1.desktop").arg(name)); + + return createUserDesktopFile(name); +} + + +KDesktopFile* ExternalToolManager::createUserDesktopFile(const QString& name) { + Q_ASSERT(!name.isEmpty()); + KDesktopFile* desktopFile=new KDesktopFile( + d->mUserToolDir + "/" + name + ".desktop", false); + d->mDesktopFiles.insert(QString("%1.desktop").arg(name), desktopFile); + + return desktopFile; +} + + +ExternalToolContext* ExternalToolManager::createContext( + QObject* parent, const KFileItemList* items) +{ + KURL::List urls; + QStringList mimeTypes; + + // Create our URL list and a list of the different mime types present in + // the selection + QPtrListIterator<KFileItem> it(*items); + for (; it.current(); ++it) { + urls.append(it.current()->url()); + QString mimeType=it.current()->mimetype(); + if (!mimeTypes.contains(mimeType)) { + mimeTypes.append(mimeType); + } + } + + return d->createContextInternal(parent, urls, mimeTypes); +} + + +ExternalToolContext* ExternalToolManager::createContext( + QObject* parent, const KURL& url) +{ + KURL::List urls; + QStringList mimeTypes; + + urls.append(url); + QString mimeType=KMimeType::findByURL(url, 0, url.isLocalFile(), true)->name(); + mimeTypes.append(mimeType); + + return d->createContextInternal(parent, urls, mimeTypes); +} + + +} // namespace diff --git a/src/gvcore/externaltoolmanager.h b/src/gvcore/externaltoolmanager.h new file mode 100644 index 0000000..8cf01ab --- /dev/null +++ b/src/gvcore/externaltoolmanager.h @@ -0,0 +1,67 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef EXTERNALTOOLMANAGER_H +#define EXTERNALTOOLMANAGER_H + +// Qt +#include <qdict.h> +#include <qptrlist.h> + +// KDE +#include <kfileitem.h> + +// Local +#include "libgwenview_export.h" + +class KActionMenu; +class KURL; + +namespace Gwenview { +class ExternalToolContext; +class ExternalToolManagerPrivate; + +class LIBGWENVIEW_EXPORT ExternalToolManager { +public: + ~ExternalToolManager(); + + ExternalToolContext* createContext(QObject* parent, const KFileItemList*); + ExternalToolContext* createContext(QObject* parent, const KURL&); + + static ExternalToolManager* instance(); + QDict<KDesktopFile>& desktopFiles() const; + + void hideDesktopFile(KDesktopFile*); + + // Create a new desktop file + KDesktopFile* createUserDesktopFile(const QString& name); + + // Create a desktop file based on a existing (system) desktop file + KDesktopFile* editSystemDesktopFile(const KDesktopFile* desktopFile); + void updateServices(); + +private: + ExternalToolManager(); + ExternalToolManagerPrivate* d; +}; + +} // namespace +#endif + diff --git a/src/gvcore/filedetailview.cpp b/src/gvcore/filedetailview.cpp new file mode 100644 index 0000000..b36a1dd --- /dev/null +++ b/src/gvcore/filedetailview.cpp @@ -0,0 +1,552 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* This file is based on kfiledetailview.cpp v1.43 from the KDE libs. Original + copyright follows. +*/ +/* This file is part of the KDE libraries + Copyright (C) 1997 Stephan Kulow <coolo@kde.org> + 2000, 2001 Carsten Pfeiffer <pfeiffer@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ + +// Qt +#include <qbitmap.h> +#include <qevent.h> +#include <qheader.h> +#include <qkeycode.h> +#include <qpainter.h> +#include <qpixmap.h> + +// KDE +#include <kapplication.h> +#include <kdebug.h> +#include <kfileitem.h> +#include <kglobalsettings.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kurldrag.h> +#include <kwordwrap.h> + +// Local +#include "archive.h" +#include "dragpixmapgenerator.h" +#include "filedetailviewitem.h" +#include "filedetailview.moc" +#include "timeutils.h" +namespace Gwenview { + + +static QPixmap createShownItemPixmap(int size, const QColor& color) { + QPixmap pix(size, size); + pix.fill(Qt::red); + QPainter painter(&pix); + int margin = 2; + + QPointArray pa(3); + int arrowSize = size/2 - margin; + int center = size/2 - 1; + pa[0] = QPoint((size - arrowSize) / 2, center - arrowSize); + pa[1] = QPoint((size + arrowSize) / 2, center); + pa[2] = QPoint(pa[0].x(), center + arrowSize); + + painter.setBrush(color); + painter.setPen(color); + painter.drawPolygon(pa); + painter.end(); + + pix.setMask(pix.createHeuristicMask()); + + return pix; +} + + +FileDetailView::FileDetailView(QWidget *parent, const char *name) + : KListView(parent, name), FileViewBase() +{ + mSortingCol = COL_NAME; + mBlockSortingSignal = false; + + addColumn( i18n( "Name" ) ); + addColumn( i18n( "Size" ) ); + addColumn( i18n( "Date" ) ); + addColumn( i18n( "Permissions" ) ); + addColumn( i18n( "Owner" ) ); + addColumn( i18n( "Group" ) ); + setShowSortIndicator( TRUE ); + setAllColumnsShowFocus( TRUE ); + + connect( header(), SIGNAL( sectionClicked(int)), + SLOT(slotSortingChanged(int) )); + + + connect( this, SIGNAL( returnPressed(QListViewItem *) ), + SLOT( slotActivate( QListViewItem *) ) ); + + connect( this, SIGNAL( clicked(QListViewItem *, const QPoint&, int)), + SLOT( selected( QListViewItem *) ) ); + connect( this, SIGNAL( doubleClicked(QListViewItem *, const QPoint&, int)), + SLOT( slotActivate( QListViewItem *) ) ); + + connect( this, SIGNAL(contextMenuRequested( QListViewItem *, + const QPoint &, int )), + this, SLOT( slotActivateMenu( QListViewItem *, const QPoint& ))); + + QListView::setSelectionMode( QListView::Extended ); + connect( this, SIGNAL( selectionChanged() ), + SLOT( slotSelectionChanged() )); + + // FileViewStack need to be aware of sort changes, to update the sort menu + connect( sig, SIGNAL(sortingChanged(QDir::SortSpec)), + this, SIGNAL(sortingChanged(QDir::SortSpec)) ); + + setSorting( sorting() ); + + + mResolver = + new KMimeTypeResolver<FileDetailViewItem,FileDetailView>( this ); + + setDragEnabled(true); + + setAcceptDrops(true); + setDropVisualizer(false); + setDropHighlighter(false); + + int size = IconSize(KIcon::Small); + mShownItemUnselectedPixmap = createShownItemPixmap(size, colorGroup().highlight()); + mShownItemSelectedPixmap = createShownItemPixmap(size, colorGroup().highlightedText()); +} + + +FileDetailView::~FileDetailView() +{ + delete mResolver; +} + + +void FileDetailView::setSelected( const KFileItem *info, bool enable ) +{ + if (!info) return; + FileDetailViewItem *item = viewItem(info); + if (item) KListView::setSelected(item, enable); +} + +void FileDetailView::setCurrentItem( const KFileItem *item ) +{ + if (!item) return; + FileDetailViewItem *listItem = viewItem(item); + if (listItem) KListView::setCurrentItem(listItem); +} + +KFileItem * FileDetailView::currentFileItem() const +{ + FileDetailViewItem *current = static_cast<FileDetailViewItem*>( currentItem() ); + if ( current ) return current->fileInfo(); + + return 0L; +} + +void FileDetailView::clearSelection() +{ + KListView::clearSelection(); +} + +void FileDetailView::selectAll() +{ + KListView::selectAll( true ); +} + +void FileDetailView::invertSelection() +{ + KListView::invertSelection(); +} + +void FileDetailView::slotActivateMenu (QListViewItem *item,const QPoint& pos ) +{ + if ( !item ) { + sig->activateMenu( 0, pos ); + return; + } + FileDetailViewItem *i = (FileDetailViewItem*) item; + sig->activateMenu( i->fileInfo(), pos ); +} + +void FileDetailView::clearView() +{ + mResolver->m_lstPendingMimeIconItems.clear(); + mShownFileItem=0L; + KListView::clear(); +} + +void FileDetailView::insertItem( KFileItem *i ) +{ + KFileView::insertItem( i ); + + FileDetailViewItem *item = new FileDetailViewItem( (QListView*) this, i ); + + setSortingKey( item, i ); + + i->setExtraData( this, item ); + + if ( !i->isMimeTypeKnown() ) + mResolver->m_lstPendingMimeIconItems.append( item ); +} + +void FileDetailView::slotActivate( QListViewItem *item ) +{ + if ( !item ) return; + + const KFileItem *fi = ( (FileDetailViewItem*)item )->fileInfo(); + if ( fi ) sig->activate( fi ); +} + +void FileDetailView::selected( QListViewItem *item ) +{ + if ( !item ) return; + + if ( KGlobalSettings::singleClick() ) { + const KFileItem *fi = ( (FileDetailViewItem*)item )->fileInfo(); + if ( fi && (fi->isDir() || !onlyDoubleClickSelectsFiles()) ) + sig->activate( fi ); + } +} + +void FileDetailView::highlighted( QListViewItem *item ) +{ + if ( !item ) return; + + const KFileItem *fi = ( (FileDetailViewItem*)item )->fileInfo(); + if ( fi ) sig->highlightFile( fi ); +} + + +bool FileDetailView::isSelected(const KFileItem* fileItem) const +{ + if (!fileItem) return false; + + FileDetailViewItem *item = viewItem(fileItem); + return item && item->isSelected(); +} + + +void FileDetailView::updateView( bool b ) +{ + if ( !b ) return; + + QListViewItemIterator it( (QListView*)this ); + for ( ; it.current(); ++it ) { + FileDetailViewItem *item=static_cast<FileDetailViewItem *>(it.current()); + item->setPixmap( 0, item->fileInfo()->pixmap(KIcon::SizeSmall) ); + } +} + +void FileDetailView::updateView( const KFileItem *i ) +{ + if ( !i ) return; + + FileDetailViewItem *item = viewItem(i); + if ( !item ) return; + + item->init(); + setSortingKey( item, i ); +} + + +void FileDetailView::setSortingKey( FileDetailViewItem *dvItem, const KFileItem *item) +{ + QDir::SortSpec spec = KFileView::sorting(); + bool isDirOrArchive=item->isDir() || Archive::fileItemIsArchive(item); + + QString key; + if ( spec & QDir::Time ) { + time_t time = TimeUtils::getTime(item); + key=sortingKey(time, isDirOrArchive, spec); + + } else if ( spec & QDir::Size ) { + key=sortingKey( item->size(), isDirOrArchive, spec ); + + } else { + // Name or Unsorted + key=sortingKey( item->text(), isDirOrArchive, spec ); + } + + dvItem->setKey(key); +} + + +void FileDetailView::removeItem( const KFileItem *i ) +{ + if ( !i ) return; + + FileDetailViewItem *item = viewItem(i); + mResolver->m_lstPendingMimeIconItems.remove( item ); + if(mShownFileItem==i) mShownFileItem=0L; + delete item; + + KFileView::removeItem( i ); +} + +void FileDetailView::slotSortingChanged( int col ) +{ + QDir::SortSpec sort = sorting(); + int sortSpec = -1; + bool reversed = col == mSortingCol && (sort & QDir::Reversed) == 0; + mSortingCol = col; + + switch( col ) { + case COL_NAME: + sortSpec = (sort & ~QDir::SortByMask | QDir::Name); + break; + case COL_SIZE: + sortSpec = (sort & ~QDir::SortByMask | QDir::Size); + break; + case COL_DATE: + sortSpec = (sort & ~QDir::SortByMask | QDir::Time); + break; + + // the following columns have no equivalent in QDir, so we set it + // to QDir::Unsorted and remember the column (mSortingCol) + case COL_OWNER: + case COL_GROUP: + case COL_PERM: + // grmbl, QDir::Unsorted == SortByMask. + sortSpec = (sort & ~QDir::SortByMask);// | QDir::Unsorted; + break; + default: + break; + } + + if ( reversed ) + sortSpec |= QDir::Reversed; + else + sortSpec &= ~QDir::Reversed; + + if ( sort & QDir::IgnoreCase ) + sortSpec |= QDir::IgnoreCase; + else + sortSpec &= ~QDir::IgnoreCase; + + + KFileView::setSorting( static_cast<QDir::SortSpec>( sortSpec ) ); + + KFileItem *item; + KFileItemListIterator it( *items() ); + + for ( ; (item = it.current() ); ++it ) { + FileDetailViewItem* thumbItem=viewItem( item ); + if (thumbItem) setSortingKey(thumbItem,item); + } + + KListView::setSorting( mSortingCol, !reversed ); + KListView::sort(); + + if (!mBlockSortingSignal) sig->changeSorting( static_cast<QDir::SortSpec>( sortSpec ) ); +} + + +void FileDetailView::setSorting( QDir::SortSpec spec ) +{ + int col = 0; + if ( spec & QDir::Time ) + col = COL_DATE; + else if ( spec & QDir::Size ) + col = COL_SIZE; + else if ( spec & QDir::Unsorted ) + col = mSortingCol; + else + col = COL_NAME; + + // inversed, because slotSortingChanged will reverse it + if ( spec & QDir::Reversed ) + spec = (QDir::SortSpec) (spec & ~QDir::Reversed); + else + spec = (QDir::SortSpec) (spec | QDir::Reversed); + + mSortingCol = col; + KFileView::setSorting( (QDir::SortSpec) spec ); + + + // don't emit sortingChanged() when called via setSorting() + mBlockSortingSignal = true; // can't use blockSignals() + slotSortingChanged( col ); + mBlockSortingSignal = false; +} + +void FileDetailView::ensureItemVisible( const KFileItem *i ) +{ + if ( !i ) return; + + FileDetailViewItem *item = viewItem(i); + + if ( item ) KListView::ensureItemVisible( item ); +} + +// we're in multiselection mode +void FileDetailView::slotSelectionChanged() +{ + sig->highlightFile( 0L ); +} + +KFileItem * FileDetailView::firstFileItem() const +{ + FileDetailViewItem *item = static_cast<FileDetailViewItem*>( firstChild() ); + if ( item ) return item->fileInfo(); + return 0L; +} + +KFileItem * FileDetailView::nextItem( const KFileItem *fileItem ) const +{ + if ( fileItem ) { + FileDetailViewItem *item = viewItem( fileItem ); + if ( item && item->itemBelow() ) + return ((FileDetailViewItem*) item->itemBelow())->fileInfo(); + else + return 0L; + } + else + return firstFileItem(); +} + +KFileItem * FileDetailView::prevItem( const KFileItem *fileItem ) const +{ + if ( fileItem ) { + FileDetailViewItem *item = viewItem( fileItem ); + if ( item && item->itemAbove() ) + return ((FileDetailViewItem*) item->itemAbove())->fileInfo(); + else + return 0L; + } + else + return firstFileItem(); +} + +void FileDetailView::keyPressEvent( QKeyEvent *e ) +{ + KListView::keyPressEvent( e ); + + if ( e->key() == Key_Return || e->key() == Key_Enter ) { + if ( e->state() & ControlButton ) + e->ignore(); + else + e->accept(); + } +} + +// +// mimetype determination on demand +// +void FileDetailView::mimeTypeDeterminationFinished() +{ + // anything to do? +} + +void FileDetailView::determineIcon( FileDetailViewItem *item ) +{ + (void) item->fileInfo()->determineMimeType(); + updateView( item->fileInfo() ); +} + +void FileDetailView::listingCompleted() +{ + mResolver->start(); +} + +void FileDetailView::startDrag() +{ + /** + * The item drawer for DragPixmapGenerator + */ + struct ItemDrawer : public DragPixmapItemDrawer<KFileItem*> { + ItemDrawer(const QFontMetrics& fontMetrics) + : mFontMetrics(fontMetrics) {} + + QSize itemSize(KFileItem* fileItem) { + if (!fileItem) return QSize(); + QString name = fileItem->name(); + int width = QMIN(mGenerator->maxWidth(), mFontMetrics.width(name)); + int height = mFontMetrics.height(); + return QSize(width, height); + } + + void drawItem(QPainter* painter, int left, int top, KFileItem* fileItem) { + QString name = fileItem->name(); + painter->save(); + KWordWrap::drawFadeoutText(painter, + left, top + mFontMetrics.ascent(), + mGenerator->maxWidth(), name); + painter->restore(); + } + + QFontMetrics mFontMetrics; + }; + ItemDrawer drawer(fontMetrics()); + + + KURL::List urls; + KFileItemListIterator it(*KFileView::selectedItems()); + + DragPixmapGenerator<KFileItem*> generator; + generator.setItemDrawer(&drawer); + + for ( ; it.current(); ++it ) { + urls.append(it.current()->url()); + generator.addItem(it.current()); + } + + if (urls.isEmpty()) { + kdWarning() << "No item to drag\n"; + return; + } + + QDragObject* drag=new KURLDrag(urls, this, 0); + QPixmap dragPixmap = generator.generate(); + + drag->setPixmap( dragPixmap, QPoint(-generator.DRAG_OFFSET, -generator.DRAG_OFFSET)); + drag->dragCopy(); +} + + +void FileDetailView::setShownFileItem(KFileItem* fileItem) +{ + if( fileItem == mShownFileItem ) return; + FileDetailViewItem* oldShownItem=viewItem(mShownFileItem); + FileDetailViewItem* newShownItem=viewItem(fileItem); + + FileViewBase::setShownFileItem(fileItem); + if (oldShownItem) oldShownItem->repaint(); + if (newShownItem) newShownItem->repaint(); +} + + +//---------------------------------------------------------------------- +// +// Drop support +// +//---------------------------------------------------------------------- +bool FileDetailView::acceptDrag(QDropEvent* event) const { + return KURLDrag::canDecode(event); +} + +void FileDetailView::contentsDropEvent(QDropEvent *event) { + KFileItem* fileItem=0L; + QListViewItem *item=itemAt(contentsToViewport(event->pos() ) ); + + if (item) { + fileItem=static_cast<FileDetailViewItem*>(item)->fileInfo(); + } + emit dropped(event,fileItem); +} + +} // namespace diff --git a/src/gvcore/filedetailview.h b/src/gvcore/filedetailview.h new file mode 100644 index 0000000..12313f6 --- /dev/null +++ b/src/gvcore/filedetailview.h @@ -0,0 +1,133 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* This file is based on kfiledetailview.h v1.30 from the KDE libs. Original + copyright follows. +*/ +/* This file is part of the KDE libraries + Copyright (C) 1997 Stephan Kulow <coolo@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ + +#ifndef FILEDETAILVIEW_H +#define FILEDETAILVIEW_H + +class KFileItem; +class QWidget; +class QKeyEvent; + +// Qt includes +#include <qdir.h> + +// KDE includes +#include <klistview.h> +#include <kmimetyperesolver.h> + +// Our includes +#include "fileviewbase.h" +namespace Gwenview { + +class FileDetailViewItem; + +class FileDetailView : public KListView, public FileViewBase +{ + Q_OBJECT + + friend class FileDetailViewItem; + +public: + FileDetailView(QWidget* parent, const char* name); + virtual ~FileDetailView(); + + virtual QWidget* widget() { return this; } + virtual void clearView(); + + virtual void updateView( bool ); + virtual void updateView(const KFileItem*); + virtual void removeItem( const KFileItem* ); + virtual void listingCompleted(); + + virtual void setSelected(const KFileItem* , bool); + virtual bool isSelected(const KFileItem* i) const; + virtual void clearSelection(); + virtual void selectAll(); + virtual void invertSelection(); + + virtual void setCurrentItem( const KFileItem* ); + virtual KFileItem* currentFileItem() const; + virtual KFileItem* firstFileItem() const; + virtual KFileItem* nextItem( const KFileItem* ) const; + virtual KFileItem* prevItem( const KFileItem* ) const; + + virtual void insertItem( KFileItem* i ); + + // implemented to get noticed about sorting changes (for sortingIndicator) + virtual void setSorting( QDir::SortSpec ); + + void ensureItemVisible( const KFileItem* ); + + // for KMimeTypeResolver + void mimeTypeDeterminationFinished(); + void determineIcon( FileDetailViewItem* item ); + QScrollView* scrollWidget() { return this; } + + void setShownFileItem(KFileItem* fileItem); + +signals: + void dropped(QDropEvent* event, KFileItem* item); + void sortingChanged(QDir::SortSpec); + +protected: + virtual bool acceptDrag(QDropEvent*) const; + virtual void contentsDropEvent(QDropEvent*); + virtual void keyPressEvent(QKeyEvent*); + + int mSortingCol; + +protected slots: + void slotSelectionChanged(); + +private slots: + void slotSortingChanged( int ); + void selected( QListViewItem* item ); + void slotActivate( QListViewItem* item ); + void highlighted( QListViewItem* item ); + void slotActivateMenu ( QListViewItem* item, const QPoint& pos ); + +private: + bool mBlockSortingSignal; + KMimeTypeResolver<FileDetailViewItem,FileDetailView>* mResolver; + + virtual void insertItem(QListViewItem* i) { KListView::insertItem(i); } + virtual void setSorting(int i, bool b) { KListView::setSorting(i, b); } + virtual void setSelected(QListViewItem* i, bool b) { KListView::setSelected(i, b); } + + FileDetailViewItem* viewItem( const KFileItem* item ) const { + if (item) return (FileDetailViewItem*)item->extraData(this); + return 0L; + } + + void setSortingKey(FileDetailViewItem* item, const KFileItem* i); + + void startDrag(); + + QPixmap mShownItemSelectedPixmap; + QPixmap mShownItemUnselectedPixmap; +}; + + +} // namespace +#endif + diff --git a/src/gvcore/filedetailviewitem.cpp b/src/gvcore/filedetailviewitem.cpp new file mode 100644 index 0000000..d02427d --- /dev/null +++ b/src/gvcore/filedetailviewitem.cpp @@ -0,0 +1,70 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* This file is based on kfiledetailview.cpp from the KDE libs. Original + copyright follows. +*/ +/* This file is part of the KDE libraries + Copyright (C) 1997 Stephan Kulow <coolo@kde.org> + 2000, 2001 Carsten Pfeiffer <pfeiffer@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ + +// KDE includes +#include <kglobal.h> +#include <klocale.h> + +// Our includes +#include "filedetailviewitem.h" +#include "filedetailview.h" +#include "timeutils.h" +namespace Gwenview { + + +void FileDetailViewItem::init() +{ + time_t time = TimeUtils::getTime(inf); + setPixmap( COL_NAME, inf->pixmap(KIcon::SizeSmall)); + + setText( COL_NAME, inf->text() ); + setText( COL_SIZE, KGlobal::locale()->formatNumber( inf->size(), 0)); + setText( COL_DATE, TimeUtils::formatTime(time) ); + setText( COL_PERM, inf->permissionsString() ); + setText( COL_OWNER, inf->user() ); + setText( COL_GROUP, inf->group() ); +} + + +const QPixmap* FileDetailViewItem::pixmap(int column) const { + const QPixmap* normalPix = KListViewItem::pixmap(column); + if (column!=0) { + return normalPix; + } + + FileDetailView* view=static_cast<FileDetailView*>(listView()); + FileDetailViewItem* viewedItem=view->viewItem(view->shownFileItem()); + if (viewedItem!=this) { + return normalPix; + } + + if (isSelected()) { + return &view->mShownItemSelectedPixmap; + } else { + return &view->mShownItemUnselectedPixmap; + } +} + + +} // namespace diff --git a/src/gvcore/filedetailviewitem.h b/src/gvcore/filedetailviewitem.h new file mode 100644 index 0000000..2e4c8ff --- /dev/null +++ b/src/gvcore/filedetailviewitem.h @@ -0,0 +1,89 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* This file is based on kfiledetailview.h from the KDE libs. Original + copyright follows. +*/ +/* This file is part of the KDE libraries + Copyright (C) 1997 Stephan Kulow <coolo@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ + +#ifndef FILEDETAILVIEWITEM_H +#define FILEDETAILVIEWITEM_H + +// KDE includes +#include <klistview.h> +#include <kfileitem.h> +namespace Gwenview { + +#define COL_NAME 0 +#define COL_SIZE 1 +#define COL_DATE 2 +#define COL_PERM 3 +#define COL_OWNER 4 +#define COL_GROUP 5 + +class FileDetailViewItem : public KListViewItem +{ +public: + FileDetailViewItem( QListView* parent, const QString &text, + const QPixmap &icon, KFileItem* fi ) + : KListViewItem( parent, text ), inf( fi ) { + setPixmap( 0, icon ); + setText( 0, text ); + } + + FileDetailViewItem( QListView* parent, KFileItem* fi ) + : KListViewItem( parent ), inf( fi ) { + init(); + } + + FileDetailViewItem( QListView* parent, const QString &text, + const QPixmap &icon, KFileItem* fi, + QListViewItem* after) + : KListViewItem( parent, after ), inf( fi ) { + setPixmap( 0, icon ); + setText( 0, text ); + } + + ~FileDetailViewItem() { + inf->removeExtraData( listView() ); + } + + KFileItem* fileInfo() const { return inf; } + + virtual QString key( int /*column*/, bool /*ascending*/ ) const { return m_key; } + + void setKey( const QString& key ) { m_key = key; } + + QRect rect() const + { + QRect r = listView()->itemRect(this); + return QRect( listView()->viewportToContents( r.topLeft() ), + QSize( r.width(), r.height() ) ); + } + + void init(); + virtual const QPixmap* pixmap(int column) const; + +private: + KFileItem* inf; + QString m_key; +}; + +} // namespace +#endif + diff --git a/src/gvcore/fileoperation.cpp b/src/gvcore/fileoperation.cpp new file mode 100644 index 0000000..99b015b --- /dev/null +++ b/src/gvcore/fileoperation.cpp @@ -0,0 +1,119 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Self +#include "fileoperation.moc" + +// Qt +#include <qcursor.h> +#include <qpopupmenu.h> +#include <qobject.h> + +// KDE +#include <kconfig.h> +#include <kiconloader.h> +#include <klocale.h> + +// Local +#include "fileopobject.h" +#include "fileoperationconfig.h" + +namespace Gwenview { + + +namespace FileOperation { + +void copyTo(const KURL::List& srcURL,QWidget* parent) { + FileOpObject* op=new FileOpCopyToObject(srcURL,parent); + (*op)(); +} + +void linkTo(const KURL::List& srcURL,QWidget* parent) { + FileOpObject* op=new FileOpLinkToObject(srcURL,parent); + (*op)(); +} + +void moveTo(const KURL::List& srcURL,QWidget* parent,QObject* receiver,const char* slot) { + FileOpObject* op=new FileOpMoveToObject(srcURL,parent); + if (receiver && slot) QObject::connect(op,SIGNAL(success()),receiver,slot); + (*op)(); +} + +void makeDir(const KURL& parentURL, QWidget* parent, QObject* receiver, const char* slot) { + FileOpObject* op=new FileOpMakeDirObject(parentURL, parent); + if (receiver && slot) QObject::connect(op,SIGNAL(success()),receiver,slot); + (*op)(); +} + +void del(const KURL::List& url,QWidget* parent,QObject* receiver,const char* slot) { + FileOpObject* op = new FileOpDelObject(url,parent); + if (receiver && slot) QObject::connect(op,SIGNAL(success()),receiver,slot); + (*op)(); +} + + +void trash(const KURL::List& url, QWidget* parent, QObject* receiver, const char* slot) { + FileOpObject* op = new FileOpTrashObject(url,parent); + if (receiver && slot) QObject::connect(op,SIGNAL(success()),receiver,slot); + (*op)(); +} + + +void realDelete(const KURL::List& url, QWidget* parent, QObject* receiver, const char* slot) { + FileOpObject* op = new FileOpRealDeleteObject(url,parent); + if (receiver && slot) QObject::connect(op,SIGNAL(success()),receiver,slot); + (*op)(); +} + + +void rename(const KURL& url,QWidget* parent,QObject* receiver,const char* slot) { + FileOpObject* op=new FileOpRenameObject(url,parent); + if (receiver && slot) QObject::connect(op,SIGNAL(renamed(const QString&)),receiver,slot); + (*op)(); +} + + +void fillDropURLMenu(QPopupMenu* menu, const KURL::List& urls, const KURL& target, bool* wasMoved) { + DropMenuContext* context=new DropMenuContext(menu, urls, target, wasMoved); + menu->insertItem( SmallIcon("goto"), i18n("&Move Here"), + context, SLOT(move()) ); + menu->insertItem( SmallIcon("editcopy"), i18n("&Copy Here"), + context, SLOT(copy()) ); + menu->insertItem( SmallIcon("www"), i18n("&Link Here"), + context, SLOT(link()) ); +} + + +void openDropURLMenu(QWidget* parent, const KURL::List& urls, const KURL& target, bool* wasMoved) { + QPopupMenu menu(parent); + if (wasMoved) *wasMoved=false; + + fillDropURLMenu(&menu, urls, target, wasMoved); + menu.insertSeparator(); + menu.insertItem( SmallIcon("cancel"), i18n("Cancel") ); + + menu.setMouseTracking(true); + menu.exec(QCursor::pos()); +} + + +} // namespace FileOperation + +} // namespace diff --git a/src/gvcore/fileoperation.h b/src/gvcore/fileoperation.h new file mode 100644 index 0000000..bb71ed6 --- /dev/null +++ b/src/gvcore/fileoperation.h @@ -0,0 +1,95 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef FILEOPERATION_H +#define FILEOPERATION_H + +// KDE +#include <kurl.h> +#include <kio/job.h> + +#include "libgwenview_export.h" + +class QPopupMenu; +class QWidget; + +namespace Gwenview { +/** + * This namespace handles all steps of a file operation : + * - asking the user what to do with a file + * - performing the operation + * - showing result dialogs + */ +namespace FileOperation { + +LIBGWENVIEW_EXPORT void copyTo(const KURL::List&,QWidget* parent=0L); +LIBGWENVIEW_EXPORT void moveTo(const KURL::List&,QWidget* parent,QObject* receiver=0L,const char* slot=0L); +LIBGWENVIEW_EXPORT void linkTo(const KURL::List& srcURL,QWidget* parent); +LIBGWENVIEW_EXPORT void makeDir(const KURL& parentURL, QWidget* parent, QObject* receiver=0L, const char* slot=0L); +LIBGWENVIEW_EXPORT void del(const KURL::List&,QWidget* parent,QObject* receiver=0L,const char* slot=0L); +LIBGWENVIEW_EXPORT void trash(const KURL::List&,QWidget* parent,QObject* receiver=0L,const char* slot=0L); +LIBGWENVIEW_EXPORT void realDelete(const KURL::List&,QWidget* parent,QObject* receiver=0L,const char* slot=0L); +LIBGWENVIEW_EXPORT void rename(const KURL&,QWidget* parent,QObject* receiver=0L,const char* slot=0L); + + +/** + * @internal + */ +class DropMenuContext : public QObject { +Q_OBJECT +public: + DropMenuContext(QObject* parent, const KURL::List& src, const KURL& dst, bool* wasMoved) + : QObject(parent) + , mSrc(src) + , mDst(dst) + , mWasMoved(wasMoved) + { + if (mWasMoved) *mWasMoved=false; + } + +public slots: + void copy() { + KIO::copy(mSrc, mDst, true); + } + + void move() { + KIO::move(mSrc, mDst, true); + if (mWasMoved) *mWasMoved=true; + } + + void link() { + KIO::link(mSrc, mDst, true); + } + +private: + KURL::List mSrc; + KURL mDst; + bool* mWasMoved; +}; + + +LIBGWENVIEW_EXPORT void fillDropURLMenu(QPopupMenu*, const KURL::List&, const KURL& target, bool* wasMoved=0L); +LIBGWENVIEW_EXPORT void openDropURLMenu(QWidget* parent, const KURL::List&, const KURL& target, bool* wasMoved=0L); + +} // namespace + +} // namespace +#endif + diff --git a/src/gvcore/fileoperationconfig.kcfg b/src/gvcore/fileoperationconfig.kcfg new file mode 100644 index 0000000..c876396 --- /dev/null +++ b/src/gvcore/fileoperationconfig.kcfg @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE kcfg SYSTEM "http://www.kde.org/standards/kcfg/1.0/kcfg.dtd"> +<kcfg> + <kcfgfile name="gwenviewrc"/> + <group name="file operations"> + <entry name="deleteToTrash" key="delete to trash" type="Bool"> + <default>true</default> + </entry> + <entry name="confirmDelete" key="confirm file delete" type="Bool"> + <default>true</default> + </entry> + <entry name="confirmMove" key="confirm file move" type="Bool"> + <default>true</default> + </entry> + <entry name="confirmCopy" key="confirm file copy" type="Bool"> + <default>true</default> + </entry> + + <entry name="destDir" key="destination dir" type="Path"> + </entry> + </group> +</kcfg> diff --git a/src/gvcore/fileoperationconfig.kcfgc b/src/gvcore/fileoperationconfig.kcfgc new file mode 100644 index 0000000..1b236c8 --- /dev/null +++ b/src/gvcore/fileoperationconfig.kcfgc @@ -0,0 +1,7 @@ +File=fileoperationconfig.kcfg +ClassName=FileOperationConfig +NameSpace=Gwenview +Singleton=true +Mutators=true +IncludeFiles=gvcore/libgwenview_export.h +Visibility=LIBGWENVIEW_EXPORT diff --git a/src/gvcore/fileopobject.cpp b/src/gvcore/fileopobject.cpp new file mode 100644 index 0000000..c480156 --- /dev/null +++ b/src/gvcore/fileopobject.cpp @@ -0,0 +1,347 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2004 Aur�ien G�eau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +// Qt +#include <qfile.h> +#include <qstylesheet.h> +#include <qwidget.h> + +// KDE +#include <kdeversion.h> +#include <kfiledialog.h> +#include <kfilefiltercombo.h> +#include <kglobalsettings.h> +#include <klineedit.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kstandarddirs.h> +#include <kurlcombobox.h> + +// Local +#include "deletedialog.h" +#include "fileoperation.h" +#include "fileopobject.moc" +#include "fileoperationconfig.h" +#include "inputdialog.h" +namespace Gwenview { + + +/** + * A tweaked KFileDialog used to select an existing directory. More efficient + * than KDirSelectDialog, since it provides access to bookmarks and let you + * create a dir. + */ +class DirSelectDialog : public KFileDialog { +public: + DirSelectDialog(const QString& startDir, QWidget* parent) + : KFileDialog(startDir, QString::null, parent, "dirselectdialog", true) { + locationEdit->setEnabled(false); + filterWidget->setEnabled(false); + setMode(KFile::Directory | KFile::ExistingOnly); + + // Cast to avoid gcc being confused + setPreviewWidget(static_cast<KPreviewWidgetBase*>(0)); + } +}; + + +//-FileOpObject-------------------------------------------------------------------- +FileOpObject::FileOpObject(const KURL& url,QWidget* parent) +: mParent(parent) +{ + mURLList.append(url); +} + + +FileOpObject::FileOpObject(const KURL::List& list,QWidget* parent) +: mParent(parent), mURLList(list) +{} + + +void FileOpObject::slotResult(KIO::Job* job) { + if (job->error()) { + job->showErrorDialog(mParent); + } + + emit success(); + +// Let's shoot ourself in the foot... + delete this; +} + + +void FileOpObject::polishJob(KIO::Job* job) { + job->setWindow(mParent->topLevelWidget()); + connect( job, SIGNAL( result(KIO::Job*) ), + this, SLOT( slotResult(KIO::Job*) ) ); +} + + +//-FileOpCopyToObject-------------------------------------------------------------- + + +void FileOpCopyToObject::operator()() { + KURL destURL; + + if (FileOperationConfig::confirmCopy()) { + QString destDir = FileOperationConfig::destDir(); + if( !destDir.isEmpty()) { + destDir += "/"; + } + if (mURLList.size()==1) { + destURL=KFileDialog::getSaveURL(destDir + mURLList.first().fileName(), + QString::null, mParent, i18n("Copy File")); + } else { + DirSelectDialog dialog(destDir, mParent); + dialog.setCaption(i18n("Select Folder Where Files Will be Copied")); + dialog.exec(); + destURL=dialog.selectedURL(); + } + } else { + destURL.setPath(FileOperationConfig::destDir()); + } + if (destURL.isEmpty()) return; + +// Copy the file + KIO::Job* job=KIO::copy(mURLList,destURL,true); + polishJob(job); + +} + + +//-FileOpCopyToObject-------------------------------------------------------------- + + +void FileOpLinkToObject::operator()() { + KURL destURL; + + if (FileOperationConfig::confirmCopy()) { + QString destDir = FileOperationConfig::destDir(); + if( !destDir.isEmpty()) { + destDir += "/"; + } + if (mURLList.size()==1) { + destURL=KFileDialog::getSaveURL(destDir + mURLList.first().fileName(), + QString::null, mParent, i18n("Link File")); + } else { + DirSelectDialog dialog(destDir, mParent); + dialog.setCaption(i18n("Select Folder Where the Files Will be Linked")); + dialog.exec(); + destURL=dialog.selectedURL(); + } + } else { + destURL.setPath(FileOperationConfig::destDir()); + } + if (destURL.isEmpty()) return; + +// Copy the file + KIO::Job* job=KIO::link(mURLList,destURL,true); + polishJob(job); +} + + +//-FileOpMoveToObject-------------------------------------------------------------- +void FileOpMoveToObject::operator()() { + KURL destURL; + + if (FileOperationConfig::confirmMove()) { + QString destDir = FileOperationConfig::destDir(); + if( !destDir.isEmpty()) { + destDir += "/"; + } + if (mURLList.size()==1) { + destURL=KFileDialog::getSaveURL(destDir + mURLList.first().fileName(), + QString::null, mParent, i18n("Move File")); + } else { + DirSelectDialog dialog(destDir, mParent); + dialog.setCaption(i18n("Select Folder Where Files Will be Moved")); + dialog.exec(); + destURL=dialog.selectedURL(); + } + } else { + destURL.setPath(FileOperationConfig::destDir()); + } + if (destURL.isEmpty()) return; + +// Move the file + KIO::Job* job=KIO::move(mURLList,destURL,true); + polishJob(job); +} + + +//-FileOpMakeDirObject------------------------------------------------------------- +void FileOpMakeDirObject::operator()() { + InputDialog dlg(mParent); + dlg.setCaption( i18n("Creating Folder") ); + dlg.setLabel( i18n("Enter the name of the new folder:") ); + dlg.setButtonOK( KGuiItem(i18n("Create Folder"), "folder_new") ); + if (!dlg.exec()) return; + + QString newDir = dlg.lineEdit()->text(); + + KURL newURL(mURLList.first()); + newURL.addPath(newDir); + KIO::Job* job=KIO::mkdir(newURL); + polishJob(job); +} + + +static KIO::Job* createTrashJob(KURL::List lst) { + KURL trashURL("trash:/"); + // Go do it + if (lst.count()==1) { + // If there's only one file, KIO::move will think we want to overwrite + // the trash dir with the file to trash, so we add the file name + trashURL.addPath(lst.first().fileName()); + } + return KIO::move(lst, trashURL); +} + +static KIO::Job* createDeleteJob(KURL::List lst) { + return KIO::del(lst, false, true); +} + + +//-FileOpDelObject----------------------------------------------------------------- +void FileOpDelObject::operator()() { + bool shouldDelete; + if (FileOperationConfig::confirmDelete()) { + DeleteDialog dlg(mParent); + dlg.setURLList(mURLList); + if (!dlg.exec()) return; + shouldDelete = dlg.shouldDelete(); + } else { + shouldDelete = not FileOperationConfig::deleteToTrash(); + } + + + KIO::Job* job; + if (shouldDelete) { + job = createDeleteJob(mURLList); + } else { + job = createTrashJob(mURLList); + } + polishJob(job); +} + + +//-FileOpTrashObject--------------------------------------------------------------- +void FileOpTrashObject::operator()() { + // Confirm operation + if (FileOperationConfig::confirmDelete()) { + int response; + if (mURLList.count()>1) { + QStringList fileList; + KURL::List::ConstIterator it=mURLList.begin(); + for (; it!=mURLList.end(); ++it) { + fileList.append((*it).filename()); + } + response=KMessageBox::warningContinueCancelList(mParent, + i18n("Do you really want to trash these files?"),fileList,i18n("Trash used as a verb", "Trash Files"),KGuiItem(i18n("Trash used as a verb", "&Trash"),"edittrash")); + } else { + QString filename=QStyleSheet::escape(mURLList.first().filename()); + response=KMessageBox::warningContinueCancel(mParent, + i18n("<p>Do you really want to move <b>%1</b> to the trash?</p>").arg(filename),i18n("Trash used as a verb", "Trash File"),KGuiItem(i18n("Trash used as a verb", "&Trash"),"edittrash")); + } + if (response!=KMessageBox::Continue) return; + } + + KIO::Job* job = createTrashJob(mURLList); + polishJob(job); +} + +//-FileOpRealDeleteObject---------------------------------------------------------- +void FileOpRealDeleteObject::operator()() { + // Confirm operation + if (FileOperationConfig::confirmDelete()) { + int response; + if (mURLList.count()>1) { + QStringList fileList; + KURL::List::ConstIterator it=mURLList.begin(); + for (; it!=mURLList.end(); ++it) { + fileList.append((*it).filename()); + } + response=KMessageBox::warningContinueCancelList(mParent, + i18n("Do you really want to delete these files?"),fileList, + i18n("Delete Files"), + KStdGuiItem::del() + ); + } else { + QString filename=QStyleSheet::escape(mURLList.first().filename()); + response=KMessageBox::warningContinueCancel(mParent, + i18n("<p>Do you really want to delete <b>%1</b>?</p>").arg(filename), + i18n("Delete File"), + KStdGuiItem::del() + ); + } + if (response!=KMessageBox::Continue) return; + } + + // Delete the file + KIO::Job* job = createDeleteJob(mURLList); + polishJob(job); +} + + +//-FileOpRenameObject-------------------------------------------------------------- +void FileOpRenameObject::operator()() { + KURL srcURL=mURLList.first(); + + // Prompt for the new filename + QString filename = srcURL.filename(); + InputDialog dlg(mParent); + dlg.setCaption(i18n("Renaming File")); + dlg.setLabel(i18n("<p>Rename file <b>%1</b> to:</p>").arg(QStyleSheet::escape(filename))); + dlg.setButtonOK( KGuiItem(i18n("&Rename"), "edit") ); + + dlg.lineEdit()->setText(filename); + int extPos = filename.findRev('.'); + if (extPos != -1) { + if (filename.mid(extPos - 4, 4) == ".tar") { + // Special case: *.tar.* + extPos -= 4; + } + dlg.lineEdit()->setSelection(0, extPos); + } + if (!dlg.exec()) return; + mNewFilename = dlg.lineEdit()->text(); + + // Rename the file + KURL destURL=srcURL; + destURL.setFileName(mNewFilename); + KIO::Job* job=KIO::move(srcURL,destURL); + polishJob(job); +} + + +void FileOpRenameObject::slotResult(KIO::Job* job) { + if (job->error()) { + job->showErrorDialog(mParent); + } + + emit success(); + emit renamed(mNewFilename); + +// Let's shoot ourself in the foot... + delete this; +} + +} // namespace diff --git a/src/gvcore/fileopobject.h b/src/gvcore/fileopobject.h new file mode 100644 index 0000000..d26f763 --- /dev/null +++ b/src/gvcore/fileopobject.h @@ -0,0 +1,143 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef FILEOPOBJECT_H +#define FILEOPOBJECT_H + +// Qt includes +#include <qobject.h> +#include <qstring.h> + +// KDE includes +#include <kio/job.h> +#include <kurl.h> + +class QWidget; + + +namespace Gwenview { +/** + * This class is a base class for wrappers to KIO slaves asynchronous + * file operations. These classes handle all steps of a file operation : + * - asking the user what to do with a file + * - performing the operation + * - showing result dialogs + * + * All these classes are used by the @FileOperation namespace-like class + */ +class FileOpObject : public QObject { +Q_OBJECT +public: + FileOpObject(const KURL&,QWidget* parent=0L); + FileOpObject(const KURL::List&,QWidget* parent=0L); + virtual void operator()()=0; + +signals: + void success(); + +protected slots: + virtual void slotResult(KIO::Job*); + +protected: + QWidget* mParent; + KURL::List mURLList; + + void polishJob(KIO::Job*); +}; + + +class FileOpCopyToObject : public FileOpObject { +Q_OBJECT +public: + FileOpCopyToObject(const KURL& url,QWidget* parent=0L) : FileOpObject(url,parent) {} + FileOpCopyToObject(const KURL::List& urlList,QWidget* parent=0L) : FileOpObject(urlList,parent) {} + void operator()(); +}; + +class FileOpLinkToObject : public FileOpObject { +Q_OBJECT +public: + FileOpLinkToObject(const KURL& url,QWidget* parent=0L) : FileOpObject(url,parent) {} + FileOpLinkToObject(const KURL::List& urlList,QWidget* parent=0L) : FileOpObject(urlList,parent) {} + void operator()(); +}; + +class FileOpMoveToObject : public FileOpObject { +Q_OBJECT +public: + FileOpMoveToObject(const KURL& url,QWidget* parent=0L) : FileOpObject(url,parent) {} + FileOpMoveToObject(const KURL::List& urlList,QWidget* parent=0L) : FileOpObject(urlList,parent) {} + void operator()(); +}; + +class FileOpMakeDirObject : public FileOpObject { +Q_OBJECT +public: + FileOpMakeDirObject(const KURL& url, QWidget* parent=0L) : FileOpObject(url, parent) {} + void operator()(); +}; + +class FileOpDelObject : public FileOpObject { +Q_OBJECT +public: + FileOpDelObject(const KURL& url,QWidget* parent=0L) : FileOpObject(url,parent) {} + FileOpDelObject(const KURL::List& urlList,QWidget* parent=0L) : FileOpObject(urlList,parent) {} + void operator()(); +}; + + +class FileOpTrashObject : public FileOpObject { +Q_OBJECT +public: + FileOpTrashObject(const KURL& url,QWidget* parent=0L) : FileOpObject(url,parent) {} + FileOpTrashObject(const KURL::List& urlList,QWidget* parent=0L) : FileOpObject(urlList,parent) {} + void operator()(); +}; + + +class FileOpRealDeleteObject : public FileOpObject { +Q_OBJECT +public: + FileOpRealDeleteObject(const KURL& url,QWidget* parent=0L) : FileOpObject(url,parent) {} + FileOpRealDeleteObject(const KURL::List& urlList,QWidget* parent=0L) : FileOpObject(urlList,parent) {} + void operator()(); +}; + + +class FileOpRenameObject : public FileOpObject { +Q_OBJECT +public: + FileOpRenameObject(const KURL& url,QWidget* parent=0L) : FileOpObject(url,parent) {} + void operator()(); + +signals: + void renamed(const QString& newName); + +protected slots: + virtual void slotResult(KIO::Job*); + +private: + QString mNewFilename; +}; + + +} // namespace +#endif + diff --git a/src/gvcore/filethumbnailview.cpp b/src/gvcore/filethumbnailview.cpp new file mode 100644 index 0000000..d9d3ca2 --- /dev/null +++ b/src/gvcore/filethumbnailview.cpp @@ -0,0 +1,866 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +// Qt +#include <qframe.h> +#include <qlayout.h> +#include <qpainter.h> +#include <qpen.h> +#include <qpixmap.h> +#include <qpushbutton.h> +#include <qtimer.h> +#include <qvaluevector.h> + +// KDE +#include <kapplication.h> +#include <kconfig.h> +#include <kdebug.h> +#include <kglobalsettings.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kprogress.h> +#include <kstandarddirs.h> +#include <kurldrag.h> +#include <kwordwrap.h> + +// Local +#include "fileviewconfig.h" +#include "filethumbnailviewitem.h" +#include "archive.h" +#include "dragpixmapgenerator.h" +#include "thumbnailloadjob.h" +#include "busylevelmanager.h" +#include "imageloader.h" +#include "timeutils.h" +#include "thumbnailsize.h" +#include "thumbnaildetailsdialog.h" + +#undef ENABLE_LOG +#undef LOG +#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + +#include "filethumbnailview.moc" +namespace Gwenview { + +static const int THUMBNAIL_UPDATE_DELAY=500; + +static const int RIGHT_TEXT_WIDTH=128; +static const int BOTTOM_MIN_TEXT_WIDTH=96; + +class ProgressWidget : public QFrame { + KProgress* mProgressBar; + QPushButton* mStop; +public: + ProgressWidget(FileThumbnailView* view, int count) + : QFrame(view) + { + QHBoxLayout* layout=new QHBoxLayout(this, 3, 3); + layout->setAutoAdd(true); + setFrameStyle( QFrame::StyledPanel | QFrame::Raised ); + + mStop=new QPushButton(this); + mStop->setPixmap(SmallIcon("stop")); + mStop->setFlat(true); + + mProgressBar=new KProgress(count, this); + mProgressBar->setFormat("%v/%m"); + + view->clipper()->installEventFilter(this); + } + + void polish() { + QFrame::polish(); + setMinimumWidth(layout()->minimumSize().width()); + //setFixedHeight( mProgressBar->height() ); + setFixedHeight( mStop->height() ); + } + + void showEvent(QShowEvent*) { + updatePosition(); + } + + bool eventFilter(QObject*, QEvent* event) { + if (event->type()==QEvent::Resize) { + updatePosition(); + } + return false; + } + + void updatePosition() { + FileThumbnailView* view=static_cast<FileThumbnailView*>(parent()); + QSize tmp=view->clipper()->size() - size(); + move(tmp.width() - 2, tmp.height() - 2); + } + + KProgress* progressBar() const { return mProgressBar; } + QPushButton* stopButton() const { return mStop; } +}; + + +struct FileThumbnailView::Private { + int mThumbnailSize; + int mMarginSize; + bool mUpdateThumbnailsOnNextShow; + QPixmap mWaitPixmap; // The wait pixmap (32 x 32) + QPixmap mWaitThumbnail; // The wait thumbnail (mThumbnailSize x mThumbnailSize) + ProgressWidget* mProgressWidget; + + QGuardedPtr<ThumbnailLoadJob> mThumbnailLoadJob; + + QTimer* mThumbnailUpdateTimer; + + int mItemDetails; + + ImageLoader* mPrefetch; + ThumbnailDetailsDialog* mThumbnailsDetailDialog; + + void updateWaitThumbnail(const FileThumbnailView* view) { + mWaitThumbnail=QPixmap(mThumbnailSize, mThumbnailSize); + mWaitThumbnail.fill(view->paletteBackgroundColor()); + QPainter painter(&mWaitThumbnail); + + painter.setPen(view->colorGroup().button()); + painter.drawRect(0,0,mThumbnailSize,mThumbnailSize); + painter.drawPixmap( + (mThumbnailSize-mWaitPixmap.width())/2, + (mThumbnailSize-mWaitPixmap.height())/2, + mWaitPixmap); + painter.end(); + } +}; + + +static FileThumbnailViewItem* viewItem(const FileThumbnailView* view, const KFileItem* fileItem) { + if (!fileItem) return 0L; + return static_cast<FileThumbnailViewItem*>( const_cast<void*>(fileItem->extraData(view) ) ); +} + + +FileThumbnailView::FileThumbnailView(QWidget* parent) +: KIconView(parent), FileViewBase() +{ + d=new Private; + d->mUpdateThumbnailsOnNextShow=false; + d->mThumbnailLoadJob=0L; + d->mWaitPixmap=QPixmap(::locate("appdata", "thumbnail/wait.png")); + d->mProgressWidget=0L; + d->mThumbnailUpdateTimer=new QTimer(this); + d->mMarginSize=FileViewConfig::thumbnailMarginSize(); + d->mItemDetails=FileViewConfig::thumbnailDetails(); + d->mPrefetch = NULL; + d->mThumbnailSize = 0; + d->mThumbnailsDetailDialog = 0; + + setItemTextPos( QIconView::ItemTextPos(FileViewConfig::thumbnailTextPos()) ); + setAutoArrange(true); + QIconView::setSorting(true); + setItemsMovable(false); + setResizeMode(Adjust); + setShowToolTips(true); + setSpacing(0); + setAcceptDrops(true); + + // We can't use KIconView::Execute mode because in this mode the current + // item is unselected after being clicked, so we use KIconView::Select mode + // and emit the execute() signal with slotClicked() ourself. + setMode(KIconView::Select); + connect(this, SIGNAL(clicked(QIconViewItem*)), + this, SLOT(slotClicked(QIconViewItem*)) ); + connect(this, SIGNAL(doubleClicked(QIconViewItem*)), + this, SLOT(slotDoubleClicked(QIconViewItem*)) ); + + connect(this, SIGNAL(dropped(QDropEvent*,const QValueList<QIconDragItem>&)), + this, SLOT(slotDropped(QDropEvent*)) ); + connect(this, SIGNAL( contentsMoving( int, int )), + this, SLOT( slotContentsMoving( int, int ))); + connect(this, SIGNAL(currentChanged(QIconViewItem*)), + this, SLOT(slotCurrentChanged(QIconViewItem*)) ); + + QIconView::setSelectionMode(Extended); + + connect(BusyLevelManager::instance(), SIGNAL(busyLevelChanged(BusyLevel)), + this, SLOT( slotBusyLevelChanged(BusyLevel))); + + connect(d->mThumbnailUpdateTimer, SIGNAL(timeout()), + this, SLOT( startThumbnailUpdate()) ); +} + + +FileThumbnailView::~FileThumbnailView() { + stopThumbnailUpdate(); + FileViewConfig::setThumbnailDetails(d->mItemDetails); + FileViewConfig::setThumbnailTextPos( int(itemTextPos()) ); + FileViewConfig::writeConfig(); + delete d; +} + + +void FileThumbnailView::setThumbnailSize(int value) { + if (value==d->mThumbnailSize) return; + d->mThumbnailSize=value; + updateGrid(); + + KFileItemListIterator it( *items() ); + for ( ; it.current(); ++it ) { + KFileItem *item=it.current(); + QPixmap pixmap=createItemPixmap(item); + QIconViewItem* iconItem=viewItem(this, item); + if (iconItem) iconItem->setPixmap(pixmap); + } + arrangeItemsInGrid(); + d->mThumbnailUpdateTimer->start(THUMBNAIL_UPDATE_DELAY, true); +} + + +int FileThumbnailView::thumbnailSize() const { + return d->mThumbnailSize; +} + + +/** + * Overriden to call updateGrid + */ +void FileThumbnailView::setItemTextPos(ItemTextPos pos) { + QIconView::setItemTextPos(pos); + updateGrid(); +} + + +void FileThumbnailView::setMarginSize(int value) { + if (value==d->mMarginSize) return; + d->mMarginSize=value; + updateGrid(); +} + + +int FileThumbnailView::marginSize() const { + return d->mMarginSize; +} + + +void FileThumbnailView::setItemDetails(int details) { + d->mItemDetails=details; + for (QIconViewItem* item=firstItem(); item; item=item->nextItem()) { + static_cast<FileThumbnailViewItem*>(item)->updateLines(); + } + arrangeItemsInGrid(); +} + + +int FileThumbnailView::itemDetails() const { + return d->mItemDetails; +} + + +void FileThumbnailView::setThumbnailPixmap(const KFileItem* fileItem, const QPixmap& thumbnail, const QSize& size) { + FileThumbnailViewItem* iconItem=viewItem(this, fileItem); + if (!iconItem) return; + + iconItem->setPixmap(thumbnail); + + // Update item info + if (size.isValid()) { + iconItem->setImageSize(size); + } + iconItem->repaint(); + + // Notify progress + if (d->mProgressWidget) { + // mProgressWidget might be null if we get called after the thumbnail + // job finished. This can happen when the thumbnail job use KPreviewJob + // to generate a thumbnail. + d->mProgressWidget->progressBar()->advance(1); + } +} + + + + +void FileThumbnailView::setShownFileItem(KFileItem* fileItem) { + if( fileItem == mShownFileItem ) return; + FileThumbnailViewItem* oldShownItem=viewItem(this, mShownFileItem); + FileThumbnailViewItem* newShownItem=viewItem(this, fileItem); + + FileViewBase::setShownFileItem(fileItem); + if (oldShownItem) repaintItem(oldShownItem); + if (newShownItem) repaintItem(newShownItem); +} + + +//----------------------------------------------------------------------------- +// +// Thumbnail code +// +//----------------------------------------------------------------------------- +QPixmap FileThumbnailView::createItemPixmap(const KFileItem* item) const { + bool isDirOrArchive=item->isDir() || Archive::fileItemIsArchive(item); + if (!isDirOrArchive) { + if (d->mWaitThumbnail.width()!=d->mThumbnailSize) { + d->updateWaitThumbnail(this); + } + return d->mWaitThumbnail; + } + + QPixmap thumbnail(d->mThumbnailSize, d->mThumbnailSize); + thumbnail.fill(paletteBackgroundColor()); + QPainter painter(&thumbnail); + + // Load the icon + QPixmap itemPix=item->pixmap(QMIN(d->mThumbnailSize, ThumbnailSize::NORMAL)); + painter.drawPixmap( + (d->mThumbnailSize-itemPix.width())/2, + (d->mThumbnailSize-itemPix.height())/2, + itemPix); + + return thumbnail; +} + + +void FileThumbnailView::startThumbnailUpdate() { + // Delay thumbnail update if the widget is not visible + if (!isVisible()) { + d->mUpdateThumbnailsOnNextShow=true; + return; + } + d->mUpdateThumbnailsOnNextShow=false; + stopThumbnailUpdate(); // just in case + doStartThumbnailUpdate(items()); +} + + +void FileThumbnailView::doStartThumbnailUpdate(const KFileItemList* list) { + QValueVector<const KFileItem*> imageList; + imageList.reserve( list->count()); + QPtrListIterator<KFileItem> it(*list); + for (;it.current(); ++it) { + KFileItem* item=it.current(); + if (!item->isDir() && !Archive::fileItemIsArchive(item)) { + imageList.append( item ); + } + } + if (imageList.empty()) return; + + BusyLevelManager::instance()->setBusyLevel( this, BUSY_THUMBNAILS ); + + Q_ASSERT(!d->mProgressWidget); + d->mProgressWidget=new ProgressWidget(this, imageList.count() ); + + connect(d->mProgressWidget->stopButton(), SIGNAL(clicked()), + this, SLOT(stopThumbnailUpdate()) ); + d->mProgressWidget->show(); + + d->mThumbnailLoadJob = new ThumbnailLoadJob(&imageList, d->mThumbnailSize); + + connect(d->mThumbnailLoadJob, SIGNAL(thumbnailLoaded(const KFileItem*, const QPixmap&, const QSize&)), + this, SLOT(setThumbnailPixmap(const KFileItem*,const QPixmap&, const QSize&)) ); + connect(d->mThumbnailLoadJob, SIGNAL(result(KIO::Job*)), + this, SLOT(slotUpdateEnded()) ); + + slotBusyLevelChanged( BusyLevelManager::instance()->busyLevel()); + // start updating at visible position + slotContentsMoving( contentsX(), contentsY()); + d->mThumbnailLoadJob->start(); +} + + +void FileThumbnailView::stopThumbnailUpdate() { + if (!d->mThumbnailLoadJob.isNull()) { + d->mThumbnailLoadJob->kill(false); + } +} + + +void FileThumbnailView::slotUpdateEnded() { + Q_ASSERT(d->mProgressWidget); + delete d->mProgressWidget; + d->mProgressWidget=0L; + + BusyLevelManager::instance()->setBusyLevel( this, BUSY_NONE ); +} + + +void FileThumbnailView::updateThumbnail(const KFileItem* fileItem) { + if (fileItem->isDir() || Archive::fileItemIsArchive(fileItem)) { + return; + } + + ThumbnailLoadJob::deleteImageThumbnail(fileItem->url()); + if (d->mThumbnailLoadJob.isNull()) { + KFileItemList list; + list.append(fileItem); + doStartThumbnailUpdate(&list); + } else { + d->mThumbnailLoadJob->appendItem(fileItem); + } +} + +// temporarily stop loading thumbnails when busy loading the selected image, +// otherwise thumbnail loading slows it down +void FileThumbnailView::slotBusyLevelChanged(BusyLevel level) { + if( !d->mThumbnailLoadJob.isNull()) { + if( level > BUSY_THUMBNAILS ) { + d->mThumbnailLoadJob->suspend(); + } else { + d->mThumbnailLoadJob->resume(); + } + } +} + +//----------------------------------------------------------------------------- +// +// KFileView methods +// +//----------------------------------------------------------------------------- +void FileThumbnailView::clearView() { + stopThumbnailUpdate(); + mShownFileItem=0L; + QIconView::clear(); +} + + +void FileThumbnailView::insertItem(KFileItem* item) { + if (!item) return; + bool isDirOrArchive=item->isDir() || Archive::fileItemIsArchive(item); + + QPixmap thumbnail=createItemPixmap(item); + FileThumbnailViewItem* iconItem=new FileThumbnailViewItem(this,item->text(),thumbnail,item); + iconItem->setDropEnabled(isDirOrArchive); + + setSortingKey(iconItem, item); + item->setExtraData(this,iconItem); +} + + +void FileThumbnailView::updateView(const KFileItem* fileItem) { + if (!fileItem) return; + + FileThumbnailViewItem* iconItem=viewItem(this, fileItem); + if (iconItem) { + iconItem->setText(fileItem->text()); + updateThumbnail(fileItem); + } + sort(); +} + + +void FileThumbnailView::ensureItemVisible(const KFileItem* fileItem) { + if (!fileItem) return; + + FileThumbnailViewItem* iconItem=viewItem(this, fileItem); + if (iconItem) QIconView::ensureItemVisible(iconItem); +} + + +void FileThumbnailView::setCurrentItem(const KFileItem* fileItem) { + if (!fileItem) return; + + FileThumbnailViewItem* iconItem=viewItem(this, fileItem); + if (iconItem) QIconView::setCurrentItem(iconItem); +} + + +void FileThumbnailView::setSelected(const KFileItem* fileItem,bool enable) { + if (!fileItem) return; + + FileThumbnailViewItem* iconItem=viewItem(this, fileItem); + if (iconItem) QIconView::setSelected(iconItem, enable, true /* do not unselect others */); +} + + +bool FileThumbnailView::isSelected(const KFileItem* fileItem) const { + if (!fileItem) return false; + + FileThumbnailViewItem* iconItem=viewItem(this, fileItem); + if (!iconItem) return false; + + return iconItem->isSelected(); +} + + +void FileThumbnailView::removeItem(const KFileItem* fileItem) { + if (!fileItem) return; + + // Remove it from the image preview job + if (!d->mThumbnailLoadJob.isNull()) + d->mThumbnailLoadJob->itemRemoved(fileItem); + + if (fileItem==mShownFileItem) mShownFileItem=0L; + + // Remove it from our view + FileThumbnailViewItem* iconItem=viewItem(this, fileItem); + if (iconItem) delete iconItem; + KFileView::removeItem(fileItem); + arrangeItemsInGrid(); +} + + +KFileItem* FileThumbnailView::firstFileItem() const { + FileThumbnailViewItem* iconItem=static_cast<FileThumbnailViewItem*>(firstItem()); + if (!iconItem) return 0L; + return iconItem->fileItem(); +} + + +KFileItem* FileThumbnailView::prevItem(const KFileItem* fileItem) const { + const FileThumbnailViewItem* iconItem=viewItem(this, fileItem); + if (!iconItem) return 0L; + + iconItem=static_cast<const FileThumbnailViewItem*>(iconItem->prevItem()); + if (!iconItem) return 0L; + + return iconItem->fileItem(); +} + + +KFileItem* FileThumbnailView::currentFileItem() const { + const QIconViewItem* iconItem=currentItem(); + if (!iconItem) return 0L; + + return static_cast<const FileThumbnailViewItem*>(iconItem)->fileItem(); +} + + +KFileItem* FileThumbnailView::nextItem(const KFileItem* fileItem) const { + const FileThumbnailViewItem* iconItem=viewItem(this, fileItem); + if (!iconItem) return 0L; + + iconItem=static_cast<const FileThumbnailViewItem*>(iconItem->nextItem()); + if (!iconItem) return 0L; + + return iconItem->fileItem(); +} + + +void FileThumbnailView::setSorting(QDir::SortSpec spec) { + KFileView::setSorting(spec); + + KFileItem *item; + KFileItemListIterator it( *items() ); + + for ( ; (item = it.current() ); ++it ) { + QIconViewItem* iconItem=viewItem(this, item); + if (iconItem) setSortingKey(iconItem, item); + } + + KIconView::sort(! (spec & QDir::Reversed) ); +} + +//-------------------------------------------------------------------------- +// +// Drop support +// +//-------------------------------------------------------------------------- +void FileThumbnailView::contentsDragEnterEvent(QDragEnterEvent* event) { + return event->accept( KURLDrag::canDecode(event) ); +} + + +void FileThumbnailView::slotDropped(QDropEvent* event) { + emit dropped(event,0L); +} + + +void FileThumbnailView::showEvent(QShowEvent* event) { + KIconView::showEvent(event); + if (!d->mUpdateThumbnailsOnNextShow) return; + + d->mUpdateThumbnailsOnNextShow=false; + QTimer::singleShot(0, this, SLOT(startThumbnailUpdate())); +} + + +//-------------------------------------------------------------------------- +// +// Private +// +//-------------------------------------------------------------------------- +void FileThumbnailView::updateGrid() { + if (itemTextPos()==Right) { + setGridX( + d->mThumbnailSize + + FileThumbnailViewItem::PADDING*3 + + RIGHT_TEXT_WIDTH); + } else { + setGridX( + QMAX(d->mThumbnailSize, BOTTOM_MIN_TEXT_WIDTH) + + FileThumbnailViewItem::PADDING*2); + } + setSpacing(d->mMarginSize); +} + + +void FileThumbnailView::setSortingKey(QIconViewItem *iconItem, const KFileItem *item) +{ + // see also setSorting() + QDir::SortSpec spec = KFileView::sorting(); + bool isDirOrArchive=item->isDir() || Archive::fileItemIsArchive(item); + + QString key; + if ( spec & QDir::Time ) { + time_t time = TimeUtils::getTime(item); + key=sortingKey(time, isDirOrArchive, spec); + + } else if ( spec & QDir::Size ) { + key=sortingKey( item->size(), isDirOrArchive, spec ); + + } else { + // Name or Unsorted + key=sortingKey( item->text(), isDirOrArchive, spec ); + } + + iconItem->setKey(key); +} + + +//-------------------------------------------------------------------------- +// +// Private slots +// +//-------------------------------------------------------------------------- +void FileThumbnailView::slotDoubleClicked(QIconViewItem* iconItem) { + if (!iconItem) return; + if (KGlobalSettings::singleClick()) return; + FileThumbnailViewItem* thumbItem=static_cast<FileThumbnailViewItem*>(iconItem); + + KFileItem* fileItem=thumbItem->fileItem(); + + if (fileItem->isDir() || Archive::fileItemIsArchive(fileItem)) { + emit executed(iconItem); + } +} + + +void FileThumbnailView::slotClicked(QIconViewItem* iconItem) { + if (!iconItem) return; + if (!KGlobalSettings::singleClick()) return; + FileThumbnailViewItem* thumbItem=static_cast<FileThumbnailViewItem*>(iconItem); + + KFileItem* fileItem=thumbItem->fileItem(); + + if (fileItem->isDir() || Archive::fileItemIsArchive(fileItem)) { + emit executed(iconItem); + } +} + +void FileThumbnailView::slotContentsMoving( int x, int y ) { + updateVisibilityInfo( x, y ); // use x,y, the signal is emitted before moving +} + +void FileThumbnailView::slotCurrentChanged(QIconViewItem* item ) { + // trigger generating thumbnails from the current one + updateVisibilityInfo( contentsX(), contentsY()); + prefetchDone(); + // if the first image is selected, no matter how, preload the next one + for( QIconViewItem* pos = item; + pos != NULL; + pos = pos->nextItem()) { + FileThumbnailViewItem* cur = static_cast< FileThumbnailViewItem* >( pos ); + if( cur->fileItem()->isDir() || Archive::fileItemIsArchive(cur->fileItem())) continue; + if( pos == item && pos->nextItem() != NULL ) { + d->mPrefetch = ImageLoader::loader( + static_cast<const FileThumbnailViewItem*>( cur->nextItem() )->fileItem()->url(), + this, BUSY_PRELOADING ); + connect( d->mPrefetch, SIGNAL( imageLoaded( bool )), SLOT( prefetchDone())); + } + } +} + +/** + * when generating thumbnails, make the current thumbnail + * to be the next one processed by the thumbnail job, if visible, + * otherwise use the first visible thumbnail + */ +void FileThumbnailView::updateVisibilityInfo( int x, int y ) { + if (d->mThumbnailLoadJob.isNull()) return; + + QRect rect( x, y, visibleWidth(), visibleHeight()); + FileThumbnailViewItem* first = static_cast< FileThumbnailViewItem* >( findFirstVisibleItem( rect )); + if (!first) { + d->mThumbnailLoadJob->setPriorityItems(NULL,NULL,NULL); + return; + } + + FileThumbnailViewItem* last = static_cast< FileThumbnailViewItem* >( findLastVisibleItem( rect )); + Q_ASSERT(last); // If we get a first item, then there must be a last + + if (currentItem() && currentItem()->intersects(rect)) { + KFileItem* fileItem = currentFileItem(); + d->mThumbnailLoadJob->setPriorityItems(fileItem, + first->fileItem(), last->fileItem()); + return; + } + + d->mThumbnailLoadJob->setPriorityItems( + first->fileItem(), + first->fileItem(), + last->fileItem()); +} + +void FileThumbnailView::keyPressEvent( QKeyEvent* e ) { +// When the user presses e.g. the Down key, try to preload the next image in that direction. + if( e->key() != Key_Left + && e->key() != Key_Right + && e->key() != Key_Up + && e->key() != Key_Down ) return KIconView::keyPressEvent( e ); + + QIconViewItem* current = currentItem(); + KIconView::keyPressEvent( e ); + QIconViewItem* next = NULL; + if( current != currentItem() && currentItem() != NULL ) { // it actually moved + switch( e->key()) { + case Key_Left: + next = currentItem()->prevItem(); + break; + case Key_Right: + next = currentItem()->nextItem(); + break; + case Key_Up: + // This relies on the thumbnails being in a grid ( x() == x() ) + for( next = currentItem()->prevItem(); + next != NULL && next->x() != currentItem()->x(); + next = next->prevItem()) + ; + break; + case Key_Down: + for( next = currentItem()->nextItem(); + next != NULL && next->x() != currentItem()->x(); + next = next->nextItem()) + ; + break; + } + + } + prefetchDone(); + if( next != NULL ) { + d->mPrefetch = ImageLoader::loader( + static_cast<const FileThumbnailViewItem*>( next )->fileItem()->url(), + this, BUSY_PRELOADING ); + connect( d->mPrefetch, SIGNAL( imageLoaded( bool )), SLOT( prefetchDone())); + } +} + +void FileThumbnailView::prefetchDone() { + if( d->mPrefetch != NULL ) { + d->mPrefetch->release( this ); + d->mPrefetch = NULL; + } +} + +//-------------------------------------------------------------------------- +// +// Protected +// +//-------------------------------------------------------------------------- +void FileThumbnailView::startDrag() { + /** + * The item drawer for DragPixmapGenerator + */ + struct ItemDrawer : public DragPixmapItemDrawer<KFileItem*> { + ItemDrawer(FileThumbnailView* view) + : mView(view) {} + + QSize itemSize(KFileItem* fileItem) { + QPixmap* pix = pixmapFromFileItem(fileItem); + if (!pix) return QSize(); + + QSize size = pix->size(); + int maxWidth = mGenerator->maxWidth(); + if (size.width() > maxWidth) { + size.rheight() = size.height() * maxWidth / size.width(); + size.rwidth() = maxWidth; + } + return size; + } + + int spacing() const { + return 2; + } + + void drawItem(QPainter* painter, int left, int top, KFileItem* fileItem) { + QPixmap* pix = pixmapFromFileItem(fileItem); + if (!pix) return; + + QSize size = itemSize(fileItem); + left += (mGenerator->pixmapWidth() - size.width()) / 2; + if (size == pix->size()) { + painter->drawPixmap(left, top, *pix); + return; + } + + QImage img = pix->convertToImage(); + img = img.smoothScale(size, QImage::ScaleMin); + painter->drawImage(left, top, img); + } + + QPixmap* pixmapFromFileItem(KFileItem* fileItem) { + FileThumbnailViewItem* iconItem = viewItem(mView, fileItem); + Q_ASSERT(iconItem); + if (!iconItem) return 0; + + QPixmap* pix = iconItem->pixmap(); + Q_ASSERT(pix); + if (!pix) return 0; + return pix; + } + + FileThumbnailView* mView; + }; + ItemDrawer drawer(this); + + + KURL::List urls; + KFileItemListIterator it(*KFileView::selectedItems()); + + DragPixmapGenerator<KFileItem*> generator; + generator.setItemDrawer(&drawer); + + for ( ; it.current(); ++it ) { + urls.append(it.current()->url()); + generator.addItem(it.current()); + } + + if (urls.isEmpty()) { + kdWarning() << "No item to drag\n"; + return; + } + + QDragObject* drag=new KURLDrag(urls, this, 0); + QPixmap dragPixmap = generator.generate(); + + drag->setPixmap( dragPixmap, QPoint(generator.DRAG_OFFSET, -generator.DRAG_OFFSET)); + drag->dragCopy(); +} + + +void FileThumbnailView::showThumbnailDetailsDialog() { + if (!d->mThumbnailsDetailDialog) { + d->mThumbnailsDetailDialog = new ThumbnailDetailsDialog(this); + } + d->mThumbnailsDetailDialog->show(); +} + + +} // namespace diff --git a/src/gvcore/filethumbnailview.h b/src/gvcore/filethumbnailview.h new file mode 100644 index 0000000..25258b7 --- /dev/null +++ b/src/gvcore/filethumbnailview.h @@ -0,0 +1,131 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef FILETHUMBNAILVIEW_H +#define FILETHUMBNAILVIEW_H + +// Qt includes +#include <qguardedptr.h> +#include <qptrlist.h> + +// KDE includes +#include <kiconview.h> + +// Our includes +#include "fileviewbase.h" +#include "busylevelmanager.h" +#include "libgwenview_export.h" + +class QDragEnterEvent; +class QIconViewItem; +class QPopupMenu; +class QShowEvent; + +class KConfig; +class KFileItem; +typedef QPtrList<KFileItem> KFileItemList; + +namespace Gwenview { +class FileThumbnailViewItem; + +class LIBGWENVIEW_EXPORT FileThumbnailView : public KIconView, public FileViewBase { +Q_OBJECT + friend class FileThumbnailViewItem; + +public: + enum ItemDetail { FILENAME=1, FILESIZE=2, FILEDATE=4, IMAGESIZE=8 }; + FileThumbnailView(QWidget* parent); + ~FileThumbnailView(); + + QWidget* widget() { return this; } + + // KFileView methods + void clearView(); + void clearSelection() { QIconView::clearSelection(); } + void insertItem(KFileItem* item); + void ensureItemVisible(const KFileItem* item); + void setCurrentItem(const KFileItem* item); + void setSelected(const KFileItem* item,bool enable); + bool isSelected(const KFileItem* item) const; + void removeItem(const KFileItem* item); + void updateView(const KFileItem* item); + void setSorting(QDir::SortSpec); + + KFileItem* firstFileItem() const; + KFileItem* prevItem( const KFileItem*) const; + KFileItem* currentFileItem() const; + KFileItem* nextItem( const KFileItem*) const; + + void setThumbnailSize(int value); + int thumbnailSize() const; + + void setMarginSize(int value); + int marginSize() const; + + void setItemDetails(int); + int itemDetails() const; + + void setItemTextPos(ItemTextPos); + + void setShownFileItem(KFileItem*); + + void updateThumbnail(const KFileItem*); + +public slots: + void setThumbnailPixmap(const KFileItem*,const QPixmap&, const QSize&); + void startThumbnailUpdate(); + void stopThumbnailUpdate(); + + void showThumbnailDetailsDialog(); + +signals: + void dropped(QDropEvent*, KFileItem* target); + +protected: + void showEvent(QShowEvent*); + void contentsDragEnterEvent(QDragEnterEvent*); + void startDrag(); + virtual void keyPressEvent( QKeyEvent* ); + +private: + class Private; + Private* d; + + void updateGrid(); + QPixmap createItemPixmap(const KFileItem*) const; + void doStartThumbnailUpdate(const KFileItemList*); + void setSortingKey(QIconViewItem*, const KFileItem*); + void updateVisibilityInfo( int x, int y ); + +private slots: + void slotClicked(QIconViewItem*); + void slotDoubleClicked(QIconViewItem*); + void slotDropped(QDropEvent*); + void slotContentsMoving( int, int ); + void slotCurrentChanged(QIconViewItem*); + void slotBusyLevelChanged( BusyLevel ); + void slotUpdateEnded(); + void prefetchDone(); +}; + + +} // namespace +#endif + diff --git a/src/gvcore/filethumbnailviewitem.cpp b/src/gvcore/filethumbnailviewitem.cpp new file mode 100644 index 0000000..df4e548 --- /dev/null +++ b/src/gvcore/filethumbnailviewitem.cpp @@ -0,0 +1,394 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* Gwenview - A simple image viewer for KDE + Copyright 2000-2004 Aurélien Gâteau + This class is based on the KIconViewItem class from KDE libs. + Original copyright follows. +*/ +/* This file is part of the KDE libraries + Copyright (C) 1999 Torben Weis <weis@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2 as published by the Free Software Foundation. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ +// Qt includes +#include <qapplication.h> +#include <qcolor.h> +#include <qpainter.h> +#include <qpen.h> +#include <qpixmap.h> + +// KDE includes +#include <kdebug.h> +#include <kwordwrap.h> +#include <kurldrag.h> + +// Our includes +#include "archive.h" +#include "filethumbnailview.h" +#include "filethumbnailviewitem.h" +#include "fileviewconfig.h" +#include "timeutils.h" + +namespace Gwenview { + +const int SHOWN_ITEM_INDICATOR_SIZE = 8; + +#if 0 +static void printRect(const QString& txt,const QRect& rect) { + kdWarning() << txt << " : " << rect.x() << "x" << rect.y() << " " << rect.width() << "x" << rect.height() << endl; +} +#endif + + +/** + * An helper class to handle a caption line and help drawing it + */ +class FileThumbnailViewItem::Line { +protected: + const QIconViewItem* mItem; + QString mTxt; + int mWidth; +public: + Line(const QIconViewItem* item, const QString& txt) + : mItem(item) + , mTxt(txt) + , mWidth(-1) { + } + virtual ~Line() {} + + virtual void setWidth(int width) { + mWidth=width; + } + + virtual int height() const=0; + + void paint(QPainter* p, int textX, int textY, int align) const { + Q_ASSERT(mWidth!=-1); + int length=fontMetrics().width(mTxt); + if (length<=mWidth ) { + p->drawText( + textX, + textY, + mWidth, + fontMetrics().height(), + align, + mTxt); + } else { + p->save(); + complexPaint(p, textX, textY, align); + p->restore(); + } + }; + +protected: + const FileThumbnailView* view() const { + return static_cast<const FileThumbnailView*>(mItem->iconView()); + } + + QFontMetrics fontMetrics() const { + return view()->fontMetrics(); + } + + /** + * Called when the text won't fit the available space + */ + virtual void complexPaint(QPainter* p, int textX, int textY, int align) const=0; +}; + + +/** + * A line which will get cropped if necessary + */ +class FileThumbnailViewItem::CroppedLine : public FileThumbnailViewItem::Line { +public: + CroppedLine(const QIconViewItem* item, const QString& txt) + : Line(item, txt) {} + + int height() const { + return fontMetrics().height(); + } + + void complexPaint(QPainter* p, int textX, int textY, int /*align*/) const { + KWordWrap::drawFadeoutText(p, + textX, + textY + fontMetrics().ascent(), + mWidth, + mTxt); + } +}; + +/** + * A line which will get wrapped if necessary + */ + +class FileThumbnailViewItem::WrappedLine : public FileThumbnailViewItem::Line { + KWordWrap* mWordWrap; +public: + WrappedLine(const QIconViewItem* item, const QString& txt) + : Line(item, txt) + , mWordWrap(0) {} + + ~WrappedLine() { + delete mWordWrap; + } + + int height() const { + Q_ASSERT(mWordWrap); + if (!mWordWrap) return 0; + return mWordWrap->boundingRect().height(); + } + + /** + * Regenerates mWordWrap if the width has changed + */ + void setWidth(int width) { + if (width==mWidth) return; + mWidth=width; + delete mWordWrap; + QFontMetrics fm=fontMetrics(); + mWordWrap=KWordWrap::formatText(fm, + QRect(0, 0, mWidth, fm.height()*3), + 0 /*flags*/, + mTxt); + } + + void complexPaint(QPainter* p, int textX, int textY, int align) const { + Q_ASSERT(mWordWrap); + if (!mWordWrap) return; + + int xpos=0; + if (align & AlignHCenter) { + xpos=( mWidth - mWordWrap->boundingRect().width() ) / 2; + } + + mWordWrap->drawText(p, + textX + xpos, + textY, + align); + } +}; + + +FileThumbnailViewItem::FileThumbnailViewItem(QIconView* view,const QString& text,const QPixmap& icon, KFileItem* fileItem) +: QIconViewItem(view,text,icon), mFileItem(fileItem) { + updateLines(); + calcRect(); +} + + +FileThumbnailViewItem::~FileThumbnailViewItem() { + QValueVector<Line*>::ConstIterator it=mLines.begin(); + QValueVector<Line*>::ConstIterator itEnd=mLines.end(); + for (;it!=itEnd; ++it) { + delete *it; + } +} + + +void FileThumbnailViewItem::updateLines() { + QValueVector<Line*>::ConstIterator it=mLines.begin(); + QValueVector<Line*>::ConstIterator itEnd=mLines.end(); + for (;it!=itEnd; ++it) { + delete *it; + } + mLines.clear(); + if (!mFileItem) return; + + bool isDir=mFileItem->isDir(); + if (iconView()->itemTextPos()==QIconView::Right) { + // Text is on the right, show everything + + time_t time = TimeUtils::getTime(mFileItem); + mLines.append( new WrappedLine(this, mFileItem->name()) ); + mLines.append( new CroppedLine(this, TimeUtils::formatTime(time)) ); + if (mImageSize.isValid()) { + QString txt=QString::number(mImageSize.width())+"x"+QString::number(mImageSize.height()); + mLines.append( new CroppedLine(this, txt) ); + } + if (!isDir) { + mLines.append( new CroppedLine(this, KIO::convertSize(mFileItem->size())) ); + } + + } else { + // Text is below the icon, only show details selected in + // view->itemDetails() + FileThumbnailView *view=static_cast<FileThumbnailView*>(iconView()); + int details=view->itemDetails(); + bool isImage=!Archive::fileItemIsDirOrArchive(mFileItem); + + if (!isImage || (details & FileThumbnailView::FILENAME)) { + mLines.append( new WrappedLine(this, mFileItem->name()) ); + } + if (details & FileThumbnailView::FILEDATE) { + time_t time = TimeUtils::getTime(mFileItem); + mLines.append( new CroppedLine(this, TimeUtils::formatTime(time)) ); + } + if (details & FileThumbnailView::IMAGESIZE) { + QString txt; + if (mImageSize.isValid()) { + txt=QString::number(mImageSize.width())+"x"+QString::number(mImageSize.height()); + } + mLines.append( new CroppedLine(this, txt) ); + } + if (!isDir && (details & FileThumbnailView::FILESIZE)) { + mLines.append( new CroppedLine(this, KIO::convertSize(mFileItem->size())) ); + } + + } + + calcRect(); +} + + +void FileThumbnailViewItem::calcRect(const QString&) { + FileThumbnailView *view=static_cast<FileThumbnailView*>(iconView()); + bool isRight=view->itemTextPos()==QIconView::Right; + + int textW=view->gridX(); + int thumbnailSize=FileViewConfig::thumbnailSize(); + if (isRight) { + textW-=PADDING * 3 + thumbnailSize; + } else { + textW-=PADDING * 2; + } + + int textH=0; + QValueVector<Line*>::ConstIterator it=mLines.begin(); + QValueVector<Line*>::ConstIterator itEnd=mLines.end(); + for (;it!=itEnd; ++it) { + (*it)->setWidth(textW); + textH+=(*it)->height(); + } + + QRect itemRect(x(), y(), view->gridX(), 0); + QRect itemPixmapRect(PADDING, PADDING, thumbnailSize, thumbnailSize); + QRect itemTextRect(0, 0, textW, textH); + if (isRight) { + itemRect.setHeight( QMAX(thumbnailSize + PADDING*2, textH) ); + itemTextRect.moveLeft(thumbnailSize + PADDING * 2 ); + itemTextRect.moveTop((itemRect.height() - textH)/2); + } else { + itemPixmapRect.moveLeft( (itemRect.width() - itemPixmapRect.width()) / 2 ); + itemRect.setHeight(thumbnailSize + PADDING*3 + textH); + itemTextRect.moveLeft(PADDING); + itemTextRect.moveTop(thumbnailSize + PADDING * 2); + } + + // Update rects + if ( itemPixmapRect != pixmapRect() ) { + setPixmapRect( itemPixmapRect ); + } + if ( itemTextRect != textRect() ) { + setTextRect( itemTextRect ); + } + if ( itemRect != rect() ) { + setItemRect( itemRect ); + } +} + + +void FileThumbnailViewItem::paintItem(QPainter *p, const QColorGroup &cg) { + FileThumbnailView *view=static_cast<FileThumbnailView*>(iconView()); + Q_ASSERT(view); + if (!view) return; + + bool isRight=view->itemTextPos()==QIconView::Right; + bool isShownItem=view->shownFileItem() && view->shownFileItem()->extraData(view)==this; + bool isImage=!Archive::fileItemIsDirOrArchive(mFileItem); + int textX, textY, textW, textH; + int thumbnailSize=FileViewConfig::thumbnailSize(); + + textX=textRect(false).x(); + textY=textRect(false).y(); + textW=textRect(false).width(); + textH=textRect(false).height(); + + // Draw pixmap + QRect pRect = pixmapRect(false); + int pixX = pRect.left() + ( thumbnailSize - pixmap()->width() ) / 2; + int pixY = pRect.top() + ( thumbnailSize - pixmap()->height() ) / 2; + p->drawPixmap( pixX, pixY, *pixmap() ); + + QColor bg; + if ( isSelected() ) { + bg=cg.highlight(); + } else { + bg=cg.mid(); + } + + // Draw shown item indicator + if (isShownItem) { + QPointArray pa(3); + pa[0] = pixmapRect(false).bottomLeft(); + pa[0].rx() += pixmapRect(false).width() / 2; + pa[0].ry() += PADDING - 1; + pa[0].ry() -= SHOWN_ITEM_INDICATOR_SIZE; + + pa[1] = pa[0]; + pa[1].rx() -= SHOWN_ITEM_INDICATOR_SIZE; + pa[1].ry() += SHOWN_ITEM_INDICATOR_SIZE; + + pa[2] = pa[1]; + pa[2].rx() += SHOWN_ITEM_INDICATOR_SIZE * 2; + + p->setBrush(cg.highlight()); + p->setPen(cg.base()); + p->drawPolygon(pa); + } + + if (isImage || isSelected()) { + // Draw frame + QRect frmRect=pixmapRect(false); + frmRect.addCoords(-PADDING, -PADDING, PADDING, PADDING); + + p->setBrush(QBrush()); + p->setPen(bg); + p->drawRect(frmRect); + if (isSelected()) { + frmRect.addCoords(1, 1, -1, -1); + p->drawRect(frmRect); + } + } + + // Draw text + p->setPen(cg.text()); + p->setBackgroundColor(cg.base()); + int align = (isRight ? AlignAuto : AlignHCenter) | AlignTop; + + QValueVector<Line*>::ConstIterator it=mLines.begin(); + QValueVector<Line*>::ConstIterator itEnd=mLines.end(); + for (;it!=itEnd; ++it) { + const Line* line=*it; + line->paint(p, textX, textY, align); + textY+=line->height(); + } +} + + +bool FileThumbnailViewItem::acceptDrop(const QMimeSource* source) const { + return KURLDrag::canDecode(source); +} + + +void FileThumbnailViewItem::dropped(QDropEvent* event, const QValueList<QIconDragItem>&) { + FileThumbnailView *view=static_cast<FileThumbnailView*>(iconView()); + emit view->dropped(event,mFileItem); +} + +void FileThumbnailViewItem::setImageSize(const QSize& size) { + mImageSize=size; + updateLines(); +} + +} // namespace diff --git a/src/gvcore/filethumbnailviewitem.h b/src/gvcore/filethumbnailviewitem.h new file mode 100644 index 0000000..154c4b7 --- /dev/null +++ b/src/gvcore/filethumbnailviewitem.h @@ -0,0 +1,69 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#ifndef FILETHUMBNAILVIEWITEM_H +#define FILETHUMBNAILVIEWITEM_H + +// Qt +#include <qiconview.h> +#include <qpixmap.h> +#include <qstring.h> +#include <qvaluevector.h> + +class KFileItem; + +namespace Gwenview { +/** + * We override the QIconViewItem to control the look of selected items + * and get a pointer to our KFileItem + */ +class FileThumbnailViewItem : public QIconViewItem { +public: + class Line; + class CroppedLine; + class WrappedLine; + enum { PADDING=4 }; + + FileThumbnailViewItem(QIconView* parent,const QString& text,const QPixmap& icon, KFileItem* fileItem); + ~FileThumbnailViewItem(); + + KFileItem* fileItem() const { return mFileItem; } + + void setImageSize(const QSize&); + + void updateLines(); + +protected: + void paintItem(QPainter* painter, const QColorGroup& colorGroup); + void calcRect( const QString& text_=QString::null ); + void paintFocus(QPainter*, const QColorGroup&) {} + bool acceptDrop(const QMimeSource*) const; + void dropped(QDropEvent*, const QValueList<QIconDragItem>&); + + KFileItem* mFileItem; + QValueVector<Line*> mLines; + + QSize mImageSize; +}; + +} // namespace +#endif + diff --git a/src/gvcore/fileviewbase.h b/src/gvcore/fileviewbase.h new file mode 100644 index 0000000..9d36598 --- /dev/null +++ b/src/gvcore/fileviewbase.h @@ -0,0 +1,47 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#ifndef FILEVIEWBASE_H +#define FILEVIEWBASE_H + +// Qt includes +#include <qcolor.h> + +// KDE includes +#include <kfileview.h> +namespace Gwenview { + +class FileViewBase : public KFileView { +public: + FileViewBase() : mShownFileItem(0L) {} + + KFileItem* shownFileItem() const { return mShownFileItem; } + virtual void setShownFileItem(KFileItem* fileItem) { mShownFileItem=fileItem; } + + virtual void updateFromSettings() {} + +protected: + KFileItem* mShownFileItem; +}; + +} // namespace +#endif + diff --git a/src/gvcore/fileviewconfig.kcfg b/src/gvcore/fileviewconfig.kcfg new file mode 100644 index 0000000..0ef5f1a --- /dev/null +++ b/src/gvcore/fileviewconfig.kcfg @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE kcfg SYSTEM "http://www.kde.org/standards/kcfg/1.0/kcfg.dtd"> +<kcfg> + <include>qiconview.h</include> + <kcfgfile name="gwenviewrc"/> + <group name="file widget"> + <entry name="showDirs" key="show dirs" type="Bool"> + <default>true</default> + </entry> + <entry name="showDotFiles" key="show dot files" type="Bool"> + <default>false</default> + </entry> + <entry name="startWithThumbnails" key="start with thumbnails" type="Bool"> + <default>true</default> + </entry> + <entry name="thumbnailTextPos" key="item text pos" type="Int"> + <default code="true">QIconView::Right</default> + </entry> + <entry name="thumbnailSize" key="thumbnail size" type="Int"> + <default>48</default> + </entry> + <entry name="thumbnailMarginSize" key="margin size" type="Int"> + <default>5</default> + </entry> + <entry name="thumbnailDetails" key="item details" type="Int"> + <default>9</default> + <description> + This is a bit set of FileThumbnailView::ItemDetail: + enum ItemDetail { FILENAME=1, FILESIZE=2, FILEDATE=4, IMAGESIZE=8 }; + </description> + </entry> + <entry name="filterMode" type="Enum"> + <default>All</default> + <choices> + <choice name="All"/> + <choice name="ImagesOnly"/> + <choice name="VideosOnly"/> + </choices> + </entry> + <entry name="showFilterBar" type="Bool"> + <default>false</default> + </entry> + <entry name="nameFilter" type="String"></entry> + <entry name="fromDateFilter" type="DateTime"></entry> + <entry name="toDateFilter" type="DateTime"></entry> + </group> + + + <!-- thumbnail cache keys are really a mess :-( --> + <group name="thumbnail loading"> + <entry name="storeThumbnailsInCache" key="path" type="Bool"> + <default>true</default> + </entry> + </group> + <group name="main window"> + <entry name="deleteCacheOnExit" key="Delete Thumbnail Cache whe exit" type="Bool"> + <default>false</default> + </entry> + </group> + + +</kcfg> diff --git a/src/gvcore/fileviewconfig.kcfgc b/src/gvcore/fileviewconfig.kcfgc new file mode 100644 index 0000000..ae7a375 --- /dev/null +++ b/src/gvcore/fileviewconfig.kcfgc @@ -0,0 +1,7 @@ +File=fileviewconfig.kcfg +ClassName=FileViewConfig +NameSpace=Gwenview +Singleton=true +Mutators=true +IncludeFiles=gvcore/libgwenview_export.h +Visibility=LIBGWENVIEW_EXPORT diff --git a/src/gvcore/fileviewcontroller.cpp b/src/gvcore/fileviewcontroller.cpp new file mode 100644 index 0000000..5daf10b --- /dev/null +++ b/src/gvcore/fileviewcontroller.cpp @@ -0,0 +1,1321 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2004 Aur�ien G�eau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +// Qt +#include <qcheckbox.h> +#include <qdatetimeedit.h> +#include <qhbox.h> +#include <qlayout.h> +#include <qpopupmenu.h> +#include <qpushbutton.h> +#include <qtooltip.h> +#include <qwidgetstack.h> + +// KDE +#include <kaction.h> +#include <kapplication.h> +#include <kcombobox.h> +#include <kdebug.h> +#include <kdirlister.h> +#include <kicontheme.h> +#include <kiconeffect.h> +#include <kiconloader.h> +#include <kimageio.h> +#include <klistview.h> +#include <klocale.h> +#include <kpropertiesdialog.h> +#include <kprotocolinfo.h> +#include <kstdaction.h> +#include <ktoolbar.h> +#include <ktoolbarlabelaction.h> +#include <kurldrag.h> +#include <kio/job.h> +#include <kio/file.h> + +// Local +#include "archive.h" +#include "cache.h" +#include "clicklineedit.h" +#include "cursortracker.h" +#include "filedetailview.h" +#include "fileoperation.h" +#include "filethumbnailview.h" +#include "filterbar.h" +#include "imageloader.h" +#include "mimetypeutils.h" +#include "timeutils.h" +#include "thumbnailsize.h" +#include "fileviewconfig.h" +#include "miscconfig.h" + +#include "fileviewcontroller.moc" +namespace Gwenview { + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + +static const int SLIDER_RESOLUTION=4; + + +//----------------------------------------------------------------------- +// +// internal class which allows dynamically turning off visual error reporting +// +//----------------------------------------------------------------------- +class DirLister : public KDirLister { +public: + DirLister() + : KDirLister() + , mError(false) + , mCheck(false) {} + + virtual bool validURL(const KURL& url) const { + if( !url.isValid()) mError = true; + if( mCheck ) return KDirLister::validURL( url ); + return url.isValid(); + } + + virtual void handleError(KIO::Job* job) { + mError = true; + if(mCheck) KDirLister::handleError( job ); + }; + + bool error() const { + return mError; + } + + void clearError() { + mError = false; + } + + void setCheck(bool c) { + mCheck = c; + } + + void setDateFilter(const QDate& from, const QDate& to) { + mFromDate = from; + mToDate =to; + } + + virtual bool itemMatchFilters(const KFileItem* item) const { + if (!matchesFilter(item)) return false; + return matchesMimeFilter(item); + } + +public: + virtual bool matchesMimeFilter(const KFileItem* item) const { + // Do mime filtering ourself because we use startsWith instead of == + QStringList lst = mimeFilters(); + QStringList::Iterator it = lst.begin(), end = lst.end(); + bool result = false; + QString type = item->mimetype(); + for (; it!=end; ++it) { + if (type.startsWith(*it)) { + result = true; + break; + } + } + if (!result) return false; + + if (item->isDir() || Archive::fileItemIsArchive(item)) { + // Do not filter out dirs or archives + return true; + } + + if (!mFromDate.isValid() && !mToDate.isValid()) return result; + + // Convert item time to a QDate + time_t time=TimeUtils::getTime(item); + QDateTime dateTime; + dateTime.setTime_t(time); + QDate date=dateTime.date(); + + if (mFromDate.isValid() && date < mFromDate) return false; + if (mToDate.isValid() && date > mToDate) return false; + return true; + } + +private: + mutable bool mError; + bool mCheck; + QDate mFromDate; + QDate mToDate; +}; + + +//----------------------------------------------------------------------- +// +// FileViewController::Private +// +//----------------------------------------------------------------------- +class FileViewController::Private { +public: + ~Private() { + delete mSliderTracker; + } + FileViewController* that; + FilterBar* mFilterBar; + KToolBar* mToolBar; + QWidgetStack* mStack; + KSelectAction* mSortAction; + KToggleAction* mRevertSortAction; + TipTracker* mSliderTracker; + + QHBox* mFilterHBox; + QComboBox* mFilterComboBox; + QCheckBox* mShowFilterBarCheckBox; + + void initFilterBar() { + mFilterBar=new FilterBar(that); + mFilterBar->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + mFilterBar->hide(); + + QIconSet resetIS=BarIcon("locationbar_erase"); + mFilterBar->mResetNameCombo->setIconSet(resetIS); + mFilterBar->mResetFrom->setIconSet(resetIS); + mFilterBar->mResetTo->setIconSet(resetIS); + + QObject::connect( + mFilterBar->mResetNameCombo, SIGNAL(clicked()), + that, SLOT(resetNameFilter()) ); + QObject::connect( + mFilterBar->mResetFrom, SIGNAL(clicked()), + that, SLOT(resetFromFilter()) ); + QObject::connect( + mFilterBar->mResetTo, SIGNAL(clicked()), + that, SLOT(resetToFilter()) ); + + QObject::connect( + mFilterBar->mFilterButton, SIGNAL(clicked()), + that, SLOT(applyFilter()) ); + } + + void initFilterCombo() { + mFilterHBox=new QHBox(mToolBar, "kde toolbar widget"); + mFilterHBox->setSpacing(KDialog::spacingHint()); + + mFilterComboBox=new QComboBox(mFilterHBox); + mFilterComboBox->insertItem(i18n("All files"), ALL); + mFilterComboBox->insertItem(i18n("Images only"), IMAGES_ONLY); + mFilterComboBox->insertItem(i18n("Videos only"), VIDEOS_ONLY); + + QObject::connect( + mFilterComboBox, SIGNAL(activated(int)), + that, SLOT(applyFilter()) ); + + mShowFilterBarCheckBox = new QCheckBox(i18n("More"), mFilterHBox); + QObject::connect( + mShowFilterBarCheckBox, SIGNAL(toggled(bool)), + mFilterBar, SLOT(setShown(bool)) ); + QObject::connect( + mShowFilterBarCheckBox, SIGNAL(toggled(bool)), + that, SLOT(applyFilter()) ); + } + + + void loadFilterSettings() { + mFilterComboBox->setCurrentItem(FileViewConfig::filterMode()); + mShowFilterBarCheckBox->setChecked(FileViewConfig::showFilterBar()); + mFilterBar->mNameEdit->setText(FileViewConfig::nameFilter()); + mFilterBar->mFromDateEdit->setDate(FileViewConfig::fromDateFilter().date()); + mFilterBar->mToDateEdit->setDate(FileViewConfig::toDateFilter().date()); + } +}; + + +//----------------------------------------------------------------------- +// +// FileViewController +// +//----------------------------------------------------------------------- +FileViewController::FileViewController(QWidget* parent,KActionCollection* actionCollection) +: QWidget(parent) +, mMode(FILE_LIST) +, mPrefetch( NULL ) +, mChangeDirStatus(CHANGE_DIR_STATUS_NONE) +, mBrowsing(false) +, mSelecting(false) +{ + d=new Private; + d->that=this; + setMinimumWidth(1); + d->mToolBar=new KToolBar(this, "", true); + d->initFilterBar(); + d->initFilterCombo(); + d->mStack=new QWidgetStack(this); + + QVBoxLayout *layout=new QVBoxLayout(this); + layout->addWidget(d->mToolBar); + layout->addWidget(d->mFilterBar); + layout->addWidget(d->mStack); + + // Actions + mSelectFirst=new KAction(i18n("&First"), + QApplication::reverseLayout() ? "2rightarrow":"2leftarrow", Key_Home, + this,SLOT(slotSelectFirst()), actionCollection, "first"); + + mSelectLast=new KAction(i18n("&Last"), + QApplication::reverseLayout() ? "2leftarrow":"2rightarrow", Key_End, + this,SLOT(slotSelectLast()), actionCollection, "last"); + + mSelectPrevious=new KAction(i18n("&Previous"), + QApplication::reverseLayout() ? "1rightarrow":"1leftarrow", Key_BackSpace, + this,SLOT(slotSelectPrevious()), actionCollection, "previous"); + + mSelectNext=new KAction(i18n("&Next"), + QApplication::reverseLayout() ? "1leftarrow":"1rightarrow", Key_Space, + this,SLOT(slotSelectNext()), actionCollection, "next"); + + mSelectPreviousDir=new KAction(i18n("&Previous Folder"), + QApplication::reverseLayout() ? "player_fwd":"player_rew", ALT + Key_BackSpace, + this,SLOT(slotSelectPreviousDir()), actionCollection, "previous_folder"); + + mSelectNextDir=new KAction(i18n("&Next Folder"), + QApplication::reverseLayout() ? "player_rew":"player_fwd", ALT + Key_Space, + this,SLOT(slotSelectNextDir()), actionCollection, "next_folder"); + + mSelectFirstSubDir=new KAction(i18n("&First Sub Folder"), "down", ALT + Key_Down, + this,SLOT(slotSelectFirstSubDir()), actionCollection, "first_sub_folder"); + + mListMode=new KRadioAction(i18n("Details"),"view_detailed",0,this,SLOT(updateViewMode()),actionCollection,"list_mode"); + mListMode->setExclusiveGroup("thumbnails"); + mSideThumbnailMode=new KRadioAction(i18n("Thumbnails with Info on Side"),"view_multicolumn",0,this,SLOT(updateViewMode()),actionCollection,"side_thumbnail_mode"); + mSideThumbnailMode->setExclusiveGroup("thumbnails"); + mBottomThumbnailMode=new KRadioAction(i18n("Thumbnails with Info on Bottom"),"view_icon",0,this,SLOT(updateViewMode()),actionCollection,"bottom_thumbnail_mode"); + mBottomThumbnailMode->setExclusiveGroup("thumbnails"); + + // Size slider + mSizeSlider=new QSlider(Horizontal, d->mToolBar); + mSizeSlider->setFixedWidth(120); + mSizeSlider->setRange( + ThumbnailSize::MIN/SLIDER_RESOLUTION, + ThumbnailSize::LARGE/SLIDER_RESOLUTION); + mSizeSlider->setValue(FileViewConfig::thumbnailSize() / SLIDER_RESOLUTION); + + connect(mSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(updateThumbnailSize(int)) ); + connect(mListMode, SIGNAL(toggled(bool)), mSizeSlider, SLOT(setDisabled(bool)) ); + KAction* sliderAction=new KWidgetAction(mSizeSlider, i18n("Thumbnail Size"), 0, 0, 0, actionCollection, "size_slider"); + d->mSliderTracker=new TipTracker("", mSizeSlider); + // /Size slider + + mShowDotFiles=new KToggleAction(i18n("Show &Hidden Files"),CTRL + Key_H,this,SLOT(toggleShowDotFiles()),actionCollection,"show_dot_files"); + + d->mSortAction=new KSelectAction(i18n("Sort"), 0, this, SLOT(setSorting()), actionCollection, "view_sort"); + QStringList sortItems; + sortItems << i18n("By Name") << i18n("By Date") << i18n("By Size"); + d->mSortAction->setItems(sortItems); + d->mSortAction->setCurrentItem(0); + + d->mRevertSortAction=new KToggleAction(i18n("Descending"),0, this, SLOT(setSorting()), actionCollection, "descending"); + QPopupMenu* sortMenu=d->mSortAction->popupMenu(); + Q_ASSERT(sortMenu); + sortMenu->insertSeparator(); + d->mRevertSortAction->plug(sortMenu); + + // Dir lister + mDirLister=new DirLister; + mDirLister->setMainWindow(topLevelWidget()); + connect(mDirLister,SIGNAL(clear()), + this,SLOT(dirListerClear()) ); + + connect(mDirLister,SIGNAL(newItems(const KFileItemList&)), + this,SLOT(dirListerNewItems(const KFileItemList&)) ); + + connect(mDirLister,SIGNAL(deleteItem(KFileItem*)), + this,SLOT(dirListerDeleteItem(KFileItem*)) ); + + connect(mDirLister,SIGNAL(refreshItems(const KFileItemList&)), + this,SLOT(dirListerRefreshItems(const KFileItemList&)) ); + + connect(mDirLister,SIGNAL(started(const KURL&)), + this,SLOT(dirListerStarted()) ); + + connect(mDirLister,SIGNAL(completed()), + this,SLOT(dirListerCompleted()) ); + + connect(mDirLister,SIGNAL(canceled()), + this,SLOT(dirListerCanceled()) ); + + // Propagate canceled signals + connect(mDirLister,SIGNAL(canceled()), + this,SIGNAL(canceled()) ); + + // File detail widget + mFileDetailView=new FileDetailView(d->mStack, "filedetailview"); + d->mStack->addWidget(mFileDetailView,0); + mFileDetailView->viewport()->installEventFilter(this); + + connect(mFileDetailView,SIGNAL(executed(QListViewItem*)), + this,SLOT(slotViewExecuted()) ); + connect(mFileDetailView,SIGNAL(returnPressed(QListViewItem*)), + this,SLOT(slotViewExecuted()) ); + connect(mFileDetailView,SIGNAL(currentChanged(QListViewItem*)), + this,SLOT(slotViewClicked()) ); + connect(mFileDetailView,SIGNAL(selectionChanged()), + this,SLOT(slotViewClicked()) ); + connect(mFileDetailView,SIGNAL(clicked(QListViewItem*)), + this,SLOT(slotViewClicked()) ); + connect(mFileDetailView,SIGNAL(contextMenu(KListView*, QListViewItem*, const QPoint&)), + this,SLOT(openContextMenu(KListView*, QListViewItem*, const QPoint&)) ); + connect(mFileDetailView,SIGNAL(dropped(QDropEvent*,KFileItem*)), + this,SLOT(openDropURLMenu(QDropEvent*, KFileItem*)) ); + connect(mFileDetailView, SIGNAL(sortingChanged(QDir::SortSpec)), + this, SLOT(updateSortMenu(QDir::SortSpec)) ); + connect(mFileDetailView, SIGNAL(doubleClicked(QListViewItem*)), + this, SLOT(slotViewDoubleClicked()) ); + connect(mFileDetailView, SIGNAL(selectionChanged()), + this, SIGNAL(selectionChanged()) ); + + // Thumbnail widget + mFileThumbnailView=new FileThumbnailView(d->mStack); + d->mStack->addWidget(mFileThumbnailView,1); + mFileThumbnailView->viewport()->installEventFilter(this); + + connect(mFileThumbnailView,SIGNAL(executed(QIconViewItem*)), + this,SLOT(slotViewExecuted()) ); + connect(mFileThumbnailView,SIGNAL(returnPressed(QIconViewItem*)), + this,SLOT(slotViewExecuted()) ); + connect(mFileThumbnailView,SIGNAL(currentChanged(QIconViewItem*)), + this,SLOT(slotViewClicked()) ); + connect(mFileThumbnailView,SIGNAL(selectionChanged()), + this,SLOT(slotViewClicked()) ); + connect(mFileThumbnailView,SIGNAL(clicked(QIconViewItem*)), + this,SLOT(slotViewClicked()) ); + connect(mFileThumbnailView,SIGNAL(contextMenuRequested(QIconViewItem*,const QPoint&)), + this,SLOT(openContextMenu(QIconViewItem*,const QPoint&)) ); + connect(mFileThumbnailView,SIGNAL(dropped(QDropEvent*,KFileItem*)), + this,SLOT(openDropURLMenu(QDropEvent*, KFileItem*)) ); + connect(mFileThumbnailView, SIGNAL(doubleClicked(QIconViewItem*)), + this, SLOT(slotViewDoubleClicked()) ); + connect(mFileThumbnailView, SIGNAL(selectionChanged()), + this, SIGNAL(selectionChanged()) ); + + // Thumbnail details dialog + KAction* thumbnailDetailsDialogAction=new KAction(i18n("Edit Thumbnail Details..."), "configure", 0, mFileThumbnailView, SLOT(showThumbnailDetailsDialog()), actionCollection, "thumbnail_details_dialog"); + connect(mBottomThumbnailMode, SIGNAL(toggled(bool)), + thumbnailDetailsDialogAction, SLOT(setEnabled(bool)) ); + + // Fill toolbar + mListMode->plug(d->mToolBar); + mSideThumbnailMode->plug(d->mToolBar); + mBottomThumbnailMode->plug(d->mToolBar); + d->mToolBar->insertSeparator(); + sliderAction->plug(d->mToolBar); + d->mToolBar->insertSeparator(); + thumbnailDetailsDialogAction->plug(d->mToolBar); + + int id=d->mToolBar->insertWidget(-1, 0, d->mFilterHBox); + d->mToolBar->alignItemRight(id, true); + + mShowDotFiles->setChecked(FileViewConfig::showDotFiles()); + + bool startWithThumbnails=FileViewConfig::startWithThumbnails(); + setMode(startWithThumbnails?THUMBNAIL:FILE_LIST); + mSizeSlider->setEnabled(startWithThumbnails); + + if (startWithThumbnails) { + if (mFileThumbnailView->itemTextPos()==QIconView::Right) { + mSideThumbnailMode->setChecked(true); + } else { + mBottomThumbnailMode->setChecked(true); + } + // Make sure the thumbnail view and the slider tooltip are updated + updateThumbnailSize(mSizeSlider->value()); + mFileThumbnailView->startThumbnailUpdate(); + } else { + mListMode->setChecked(true); + } + thumbnailDetailsDialogAction->setEnabled(mBottomThumbnailMode->isChecked()); + + if (MiscConfig::rememberFilter()) { + d->loadFilterSettings(); + } + updateFromSettings(); +} + + +FileViewController::~FileViewController() { + // Save various settings + FileViewConfig::setStartWithThumbnails(mMode==THUMBNAIL); + + int filterMode = d->mFilterComboBox->currentItem(); + FileViewConfig::setFilterMode(filterMode); + + FileViewConfig::setShowFilterBar(d->mShowFilterBarCheckBox->isChecked()); + FileViewConfig::setNameFilter(d->mFilterBar->mNameEdit->text()); + FileViewConfig::setFromDateFilter(d->mFilterBar->mFromDateEdit->date()); + FileViewConfig::setToDateFilter(d->mFilterBar->mToDateEdit->date()); + + FileViewConfig::writeConfig(); + delete mDirLister; + delete d; +} + + +void FileViewController::setFocus() { + currentFileView()->widget()->setFocus(); +} + + +/** + * Do not let double click events propagate if Ctrl or Shift is down, to avoid + * toggling fullscreen + */ +bool FileViewController::eventFilter(QObject*, QEvent* event) { + if (event->type()!=QEvent::MouseButtonDblClick) return false; + + QMouseEvent* mouseEvent=static_cast<QMouseEvent*>(event); + if (mouseEvent->state() & Qt::ControlButton || mouseEvent->state() & Qt::ShiftButton) { + return true; + } + return false; +} + + +bool FileViewController::lastURLError() const { + return mDirLister->error(); +} + + +//----------------------------------------------------------------------- +// +// Public slots +// +//----------------------------------------------------------------------- +void FileViewController::setDirURL(const KURL& url) { + LOG(url.prettyURL()); + if ( mDirURL.equals(url,true) ) { + LOG("Same URL"); + return; + } + prefetchDone(); + mDirURL=url; + if (!KProtocolInfo::supportsListing(mDirURL)) { + LOG("Protocol does not support listing"); + return; + } + + mDirLister->clearError(); + currentFileView()->setShownFileItem(0L); + mFileNameToSelect=QString::null; + mDirLister->openURL(mDirURL); + emit urlChanged(mDirURL); + emit directoryChanged(mDirURL); + updateActions(); +} + + +/** + * Sets the file to select once the dir lister is done. If it's not running, + * immediatly selects the file. + */ +void FileViewController::setFileNameToSelect(const QString& fileName) { + mFileNameToSelect=fileName; + if (mDirLister->isFinished()) { + browseToFileNameToSelect(); + } +} + +void FileViewController::prefetch( KFileItem* item ) { + prefetchDone(); + if( item == NULL ) return; + mPrefetch = ImageLoader::loader( item->url(), this, BUSY_PRELOADING ); + connect( mPrefetch, SIGNAL( imageLoaded( bool )), SLOT( prefetchDone())); +} + +void FileViewController::prefetchDone() { + if( mPrefetch != NULL ) { + mPrefetch->release( this ); + mPrefetch = NULL; + } +} + +void FileViewController::slotSelectFirst() { + browseTo( findFirstImage()); + prefetch( findNextImage()); +} + +void FileViewController::slotSelectLast() { + browseTo(findLastImage()); + prefetch( findPreviousImage()); +} + +void FileViewController::slotSelectPrevious() { + browseTo(findPreviousImage()); + prefetch( findPreviousImage()); +} + +void FileViewController::slotSelectNext() { + browseTo(findNextImage()); + prefetch( findNextImage()); +} + +void FileViewController::slotSelectPreviousDir() { + mChangeDirStatus = CHANGE_DIR_STATUS_PREV; + mDirLister->clearError(); + mDirLister->openURL(mDirURL.upURL()); +} + +void FileViewController::slotSelectNextDir() { + mChangeDirStatus = CHANGE_DIR_STATUS_NEXT; + mDirLister->clearError(); + mDirLister->openURL(mDirURL.upURL()); +} + +void FileViewController::slotSelectFirstSubDir() { + KFileItem* item=currentFileView()->firstFileItem(); + while (item && !Archive::fileItemIsDirOrArchive(item)) { + item=currentFileView()->nextItem(item); + } + if (!item) { + LOG("No item found"); + return; + } + LOG("item->url(): " << item->url().prettyURL()); + KURL tmp=item->url(); + if (Archive::fileItemIsArchive(item)) { + tmp.setProtocol(Archive::protocolForMimeType(item->mimetype())); + } + tmp.adjustPath(1); + setDirURL(tmp); +} + + +void FileViewController::resetNameFilter() { + d->mFilterBar->mNameEdit->clear(); +} + + +void FileViewController::resetFromFilter() { + d->mFilterBar->mFromDateEdit->setDate(QDate()); +} + + +void FileViewController::resetToFilter() { + d->mFilterBar->mToDateEdit->setDate(QDate()); +} + + +void FileViewController::browseTo(KFileItem* item) { + prefetchDone(); + if (mBrowsing) return; + mBrowsing = true; + if (item) { + currentFileView()->setCurrentItem(item); + currentFileView()->clearSelection(); + currentFileView()->setSelected(item,true); + currentFileView()->ensureItemVisible(item); + if (!item->isDir() && !Archive::fileItemIsArchive(item)) { + emitURLChanged(); + } + } + updateActions(); + mBrowsing = false; +} + + +void FileViewController::browseToFileNameToSelect() { + // There's something to select + if (!mFileNameToSelect.isEmpty()) { + browseTo(findItemByFileName(mFileNameToSelect)); + mFileNameToSelect=QString::null; + return; + } + + // Nothing to select, but an item is already shown + if (currentFileView()->shownFileItem()) return; + + // Now we have to make some default choice + slotSelectFirst(); + + // If no item is selected, make sure the first one is + if (currentFileView()->selectedItems()->count()==0) { + KFileItem* item=currentFileView()->firstFileItem(); + if (item) { + currentFileView()->setCurrentItem(item); + currentFileView()->setSelected(item, true); + currentFileView()->ensureItemVisible(item); + } + } +} + + +void FileViewController::updateThumbnail(const KURL& url) { + if (mMode==FILE_LIST) return; + + KFileItem* item=mDirLister->findByURL(url); + if (!item) return; + mFileThumbnailView->updateThumbnail(item); +} + + +//----------------------------------------------------------------------- +// +// Private slots +// +//----------------------------------------------------------------------- +void FileViewController::slotViewExecuted() { + KFileItem* item=currentFileView()->currentFileItem(); + if (!item) return; + + bool isDir=item->isDir(); + bool isArchive=Archive::fileItemIsArchive(item); + if (isDir || isArchive) { + KURL tmp=url(); + + if (isArchive) { + tmp.setProtocol(Archive::protocolForMimeType(item->mimetype())); + } + tmp.adjustPath(1); + setDirURL(tmp); + } else { + emitURLChanged(); + } +} + + +void FileViewController::slotViewClicked() { + updateActions(); + KFileItem* item=currentFileView()->currentFileItem(); + if (!item || Archive::fileItemIsDirOrArchive(item)) return; + + mSelecting = true; + emitURLChanged(); + mSelecting = false; +} + + +void FileViewController::slotViewDoubleClicked() { + updateActions(); + KFileItem* item=currentFileView()->currentFileItem(); + if (item && !Archive::fileItemIsDirOrArchive(item)) emit imageDoubleClicked(); +} + + +void FileViewController::updateViewMode() { + if (mListMode->isChecked()) { + setMode(FILE_LIST); + return; + } + if (mSideThumbnailMode->isChecked()) { + mFileThumbnailView->setItemTextPos(QIconView::Right); + } else { + mFileThumbnailView->setItemTextPos(QIconView::Bottom); + } + + // Only switch the view if we are going from no thumbs to either side or + // bottom thumbs, not when switching between side and bottom thumbs + if (mMode==FILE_LIST) { + setMode(THUMBNAIL); + } else { + KFileItemList items=*mFileThumbnailView->items(); + KFileItem* shownFileItem=mFileThumbnailView->shownFileItem(); + + mFileThumbnailView->FileViewBase::clear(); + mFileThumbnailView->addItemList(items); + mFileThumbnailView->setShownFileItem(shownFileItem); + } + + updateThumbnailSize(mSizeSlider->value()); + mFileThumbnailView->startThumbnailUpdate(); +} + + +void FileViewController::updateThumbnailSize(int size) { + size*=SLIDER_RESOLUTION; + d->mSliderTracker->setText(i18n("Thumbnail size: %1x%2").arg(size).arg(size)); + FileViewConfig::setThumbnailSize(size); + mFileThumbnailView->setThumbnailSize(size); + Cache::instance()->checkThumbnailSize(size); +} + + +void FileViewController::toggleShowDotFiles() { + mDirLister->setShowingDotFiles(mShowDotFiles->isChecked()); + mDirLister->openURL(mDirURL); +} + + +void FileViewController::updateSortMenu(QDir::SortSpec _spec) { + int spec=_spec & (QDir::Name | QDir::Time | QDir::Size); + int item; + switch (spec) { + case QDir::Name: + item=0; + break; + case QDir::Time: + item=1; + break; + case QDir::Size: + item=2; + break; + default: + item=-1; + break; + } + d->mSortAction->setCurrentItem(item); +} + + +void FileViewController::setSorting() { + QDir::SortSpec spec; + + switch (d->mSortAction->currentItem()) { + case 0: // Name + spec=QDir::Name; + break; + case 1: // Date + spec=QDir::Time; + break; + case 2: // Size + spec=QDir::Size; + break; + default: + return; + } + if (d->mRevertSortAction->isChecked()) { + spec=QDir::SortSpec(spec | QDir::Reversed); + } + currentFileView()->setSorting(QDir::SortSpec(spec | QDir::DirsFirst)); + emit sortingChanged(); +} + + +//----------------------------------------------------------------------- +// +// Context menu +// +//----------------------------------------------------------------------- +void FileViewController::openContextMenu(KListView*,QListViewItem* item,const QPoint& pos) { + emit requestContextMenu(pos, item!=0); +} + + +void FileViewController::openContextMenu(QIconViewItem* item,const QPoint& pos) { + emit requestContextMenu(pos, item!=0); +} + + +//----------------------------------------------------------------------- +// +// Drop URL menu +// +//----------------------------------------------------------------------- +void FileViewController::openDropURLMenu(QDropEvent* event, KFileItem* item) { + KURL dest; + + if (item) { + dest=item->url(); + } else { + dest=mDirURL; + } + + KURL::List urls; + if (!KURLDrag::decode(event,urls)) return; + + FileOperation::openDropURLMenu(d->mStack, urls, dest); +} + + +//----------------------------------------------------------------------- +// +// File operations +// +//----------------------------------------------------------------------- +KURL::List FileViewController::selectedURLs() const { + KURL::List list; + + KFileItemListIterator it( *currentFileView()->selectedItems() ); + for ( ; it.current(); ++it ) { + list.append(it.current()->url()); + } + if (list.isEmpty()) { + const KFileItem* item=currentFileView()->shownFileItem(); + if (item) list.append(item->url()); + } + return list; +} + + +KURL::List FileViewController::selectedImageURLs() const { + KURL::List list; + + KFileItemListIterator it( *currentFileView()->selectedItems() ); + for ( ; it.current(); ++it ) { + KFileItem* item=it.current(); + if (!Archive::fileItemIsDirOrArchive(item)) { + list.append(item->url()); + } + } + if (list.isEmpty()) { + const KFileItem* item=currentFileView()->shownFileItem(); + if (item && !Archive::fileItemIsDirOrArchive(item)) list.append(item->url()); + } + return list; +} + + +//----------------------------------------------------------------------- +// +// Properties +// +//----------------------------------------------------------------------- +QString FileViewController::fileName() const { + KFileItem* item=currentFileView()->currentFileItem(); + if (!item) return ""; + return item->text(); +} + + +FileViewBase* FileViewController::currentFileView() const { + if (mMode==FILE_LIST) { + return mFileDetailView; + } else { + return mFileThumbnailView; + } +} + + +uint FileViewController::fileCount() const { + uint count=currentFileView()->count(); + + KFileItem* item=currentFileView()->firstFileItem(); + while (item && Archive::fileItemIsDirOrArchive(item)) { + item=currentFileView()->nextItem(item); + count--; + } + return count; +} + + +int FileViewController::shownFilePosition() const { + KFileItem* shownItem=currentFileView()->shownFileItem(); + if (!shownItem) return -1; + KFileItem* item=currentFileView()->firstFileItem(); + int position=0; + for (; + item && item!=shownItem; + item=currentFileView()->nextItem(item) ) + { + if (!Archive::fileItemIsDirOrArchive(item)) ++position; + } + return position; +} + + +KURL FileViewController::url() const { + KFileItem* item=currentFileView()->currentFileItem(); + if (!item) return mDirURL; + return item->url(); +} + +KURL FileViewController::dirURL() const { + return mDirURL; +} + + +uint FileViewController::selectionSize() const { + const KFileItemList* selectedItems=currentFileView()->selectedItems(); + return selectedItems->count(); +} + + +void FileViewController::setMode(FileViewController::Mode mode) { + const KFileItemList* items; + FileViewBase* oldView; + FileViewBase* newView; + + mMode=mode; + + if (mMode==FILE_LIST) { + mFileThumbnailView->stopThumbnailUpdate(); + oldView=mFileThumbnailView; + newView=mFileDetailView; + } else { + oldView=mFileDetailView; + newView=mFileThumbnailView; + } + + bool wasFocused=oldView->widget()->hasFocus(); + // Show the new active view + d->mStack->raiseWidget(newView->widget()); + if (wasFocused) newView->widget()->setFocus(); + + // Fill the new view + newView->clear(); + newView->addItemList(*oldView->items()); + + // Set the new view to the same state as the old + items=oldView->selectedItems(); + for(KFileItemListIterator it(*items);it.current()!=0L;++it) { + newView->setSelected(it.current(), true); + } + newView->setShownFileItem(oldView->shownFileItem()); + newView->setCurrentItem(oldView->currentFileItem()); + + // Remove references to the old view from KFileItems + items=oldView->items(); + for(KFileItemListIterator it(*items);it.current()!=0L;++it) { + it.current()->removeExtraData(oldView); + } + + // Update sorting + newView->setSorting(oldView->sorting()); + + // Clear the old view + oldView->FileViewBase::clear(); +} + + +void FileViewController::updateFromSettings() { + applyFilter(); + mFileThumbnailView->setMarginSize(FileViewConfig::thumbnailMarginSize()); + mFileThumbnailView->setItemDetails(FileViewConfig::thumbnailDetails()); + currentFileView()->widget()->update(); +} + + +void FileViewController::setSilentMode( bool silent ) { + mDirLister->setCheck( !silent ); +} + + +void FileViewController::retryURL() { + mDirLister->clearError(); + mDirLister->openURL( url()); +} + + +//----------------------------------------------------------------------- +// +// Dir lister slots +// +//----------------------------------------------------------------------- +void FileViewController::dirListerDeleteItem(KFileItem* item) { + KFileItem* newShownItem=0L; + const KFileItem* shownItem=currentFileView()->shownFileItem(); + if (shownItem==item) { + newShownItem=findNextImage(); + if (!newShownItem) newShownItem=findPreviousImage(); + } + + currentFileView()->removeItem(item); + + if (shownItem==item) { + currentFileView()->setCurrentItem(newShownItem); + currentFileView()->setSelected(newShownItem, true); + if (newShownItem) { + emit urlChanged(newShownItem->url()); + } else { + emit urlChanged(KURL()); + } + } +} + + +void FileViewController::dirListerNewItems(const KFileItemList& items) { + LOG(""); + mThumbnailsNeedUpdate=true; + currentFileView()->addItemList(items); +} + + +void FileViewController::dirListerRefreshItems(const KFileItemList& list) { + LOG(""); + const KFileItem* item=currentFileView()->shownFileItem(); + KFileItemListIterator it(list); + for (; *it!=0L; ++it) { + currentFileView()->updateView(*it); + if (*it==item) { + emit shownFileItemRefreshed(item); + } + } +} + + +void FileViewController::refreshItems(const KURL::List& urls) { + LOG(""); + KFileItemList list; + for( KURL::List::ConstIterator it = urls.begin(); + it != urls.end(); + ++it ) { + KURL dir = *it; + dir.setFileName( QString::null ); + if( dir != mDirURL ) continue; + // TODO this could be quite slow for many images? + KFileItem* item = findItemByFileName( (*it).filename()); + if( item ) list.append( item ); + } + dirListerRefreshItems( list ); +} + + +void FileViewController::dirListerClear() { + currentFileView()->clear(); +} + + +void FileViewController::dirListerStarted() { + LOG(""); + mThumbnailsNeedUpdate=false; +} + + +void FileViewController::dirListerCompleted() { + LOG(""); + // Delay the code to be executed when the dir lister has completed its job + // to avoid crash in KDirLister (see bug #57991) + QTimer::singleShot(0,this,SLOT(delayedDirListerCompleted())); +} + + +void FileViewController::delayedDirListerCompleted() { + // The call to sort() is a work around to a bug which causes + // FileThumbnailView::firstFileItem() to return a wrong item. This work + // around is not in firstFileItem() because it's const and sort() is a non + // const method + if (mMode!=FILE_LIST) { + mFileThumbnailView->sort(mFileThumbnailView->sortDirection()); + } + + if (mChangeDirStatus != CHANGE_DIR_STATUS_NONE) { + KFileItem *item; + QString fileName = mDirURL.filename(); + for (item=currentFileView()->firstFileItem(); item; item=currentFileView()->nextItem(item) ) { + if (item->name() == fileName) { + if (mChangeDirStatus == CHANGE_DIR_STATUS_NEXT) { + do { + item=currentFileView()->nextItem(item); + } while (item && !Archive::fileItemIsDirOrArchive(item)); + } else { + do { + item=currentFileView()->prevItem(item); + } while (item && !Archive::fileItemIsDirOrArchive(item)); + } + break; + }; + } + mChangeDirStatus = CHANGE_DIR_STATUS_NONE; + if (!item) { + mDirLister->openURL(mDirURL); + } else { + KURL tmp=item->url(); + LOG("item->url(): " << item->url().prettyURL()); + if (Archive::fileItemIsArchive(item)) { + tmp.setProtocol(Archive::protocolForMimeType(item->mimetype())); + } + tmp.adjustPath(1); + setDirURL(tmp); + } + } else { + browseToFileNameToSelect(); + emit completed(); + + if (mMode!=FILE_LIST && mThumbnailsNeedUpdate) { + mFileThumbnailView->startThumbnailUpdate(); + } + } +} + + +void FileViewController::dirListerCanceled() { + if (mMode!=FILE_LIST) { + mFileThumbnailView->stopThumbnailUpdate(); + } + + browseToFileNameToSelect(); +} + + +void FileViewController::setShowFilterBar(bool value) { + d->mShowFilterBarCheckBox->setChecked(value); +} + + +void FileViewController::setFilterMode(int mode) { + d->mFilterComboBox->setCurrentItem(mode); +} + + +void FileViewController::setFilterName(const QString& name) { + d->mFilterBar->mNameEdit->setText(name); +} + + +void FileViewController::setFilterFromDate(const QDate& date) { + d->mFilterBar->mFromDateEdit->setDate(date); +} + + +void FileViewController::setFilterToDate(const QDate& date) { + d->mFilterBar->mToDateEdit->setDate(date); +} + + +void FileViewController::applyFilter() { + QStringList mimeTypes; + FilterMode filterMode = static_cast<FilterMode>( d->mFilterComboBox->currentItem() ); + + if (FileViewConfig::showDirs()) { + mimeTypes << "inode/directory"; + mimeTypes += Archive::mimeTypes(); + } + + if (filterMode != VIDEOS_ONLY) { + mimeTypes += MimeTypeUtils::rasterImageMimeTypes(); + mimeTypes << "image/svg"; + } + + if (filterMode != IMAGES_ONLY) { + mimeTypes << "video/"; + } + + if (d->mShowFilterBarCheckBox->isChecked()) { + QString txt=d->mFilterBar->mNameEdit->text(); + QDate from=d->mFilterBar->mFromDateEdit->date(); + QDate to=d->mFilterBar->mToDateEdit->date(); + + mDirLister->setNameFilter(txt); + mDirLister->setDateFilter(from, to); + } else { + mDirLister->setNameFilter(QString::null); + mDirLister->setDateFilter(QDate(), QDate()); + } + + mDirLister->setShowingDotFiles(mShowDotFiles->isChecked()); + mDirLister->setMimeFilter(mimeTypes); + + // Find next item matching the filter if any, so that we can keep it + // current + KFileItem* item=currentFileView()->currentFileItem(); + for (; item; item=currentFileView()->nextItem(item)) { + if (mDirLister->itemMatchFilters(item)) { + mFileNameToSelect=item->name(); + break; + } + } + + mDirLister->openURL(mDirURL); +} + + +void FileViewController::updateActions() { + KFileItem* firstImage=findFirstImage(); + + // There isn't any image, no need to continue + if (!firstImage) { + mSelectFirst->setEnabled(false); + mSelectPrevious->setEnabled(false); + mSelectNext->setEnabled(false); + mSelectLast->setEnabled(false); + return; + } + + // We did not select any image, let's activate everything + KFileItem* currentItem=currentFileView()->currentFileItem(); + if (!currentItem || Archive::fileItemIsDirOrArchive(currentItem)) { + mSelectFirst->setEnabled(true); + mSelectPrevious->setEnabled(true); + mSelectNext->setEnabled(true); + mSelectLast->setEnabled(true); + return; + } + + // There is at least one image, and an image is selected, let's be precise + bool isFirst=currentItem==firstImage; + bool isLast=currentItem==findLastImage(); + + mSelectFirst->setEnabled(!isFirst); + mSelectPrevious->setEnabled(!isFirst); + mSelectNext->setEnabled(!isLast); + mSelectLast->setEnabled(!isLast); +} + + +void FileViewController::emitURLChanged() { + KFileItem* item=currentFileView()->currentFileItem(); + currentFileView()->setShownFileItem(item); + + // We use a tmp value because the signal parameter is a reference + KURL tmp=url(); + LOG("urlChanged: " << tmp.prettyURL()); + emit urlChanged(tmp); +} + +KFileItem* FileViewController::findFirstImage() const { + KFileItem* item=currentFileView()->firstFileItem(); + while (item && Archive::fileItemIsDirOrArchive(item)) { + item=currentFileView()->nextItem(item); + } + if (item) { + LOG("item->url(): " << item->url().prettyURL()); + } else { + LOG("No item found"); + } + return item; +} + +KFileItem* FileViewController::findLastImage() const { + KFileItem* item=currentFileView()->items()->getLast(); + while (item && Archive::fileItemIsDirOrArchive(item)) { + item=currentFileView()->prevItem(item); + } + return item; +} + +KFileItem* FileViewController::findPreviousImage() const { + KFileItem* item=currentFileView()->shownFileItem(); + if (!item) return 0L; + do { + item=currentFileView()->prevItem(item); + } while (item && Archive::fileItemIsDirOrArchive(item)); + return item; +} + +KFileItem* FileViewController::findNextImage() const { + KFileItem* item=currentFileView()->shownFileItem(); + if (!item) return 0L; + do { + item=currentFileView()->nextItem(item); + } while (item && Archive::fileItemIsDirOrArchive(item)); + return item; +} + +KFileItem* FileViewController::findItemByFileName(const QString& fileName) const { + KFileItem *item; + if (fileName.isEmpty()) return 0L; + for (item=currentFileView()->firstFileItem(); + item; + item=currentFileView()->nextItem(item) ) { + if (item->name()==fileName) return item; + } + + return 0L; +} + + +} // namespace diff --git a/src/gvcore/fileviewcontroller.h b/src/gvcore/fileviewcontroller.h new file mode 100644 index 0000000..773de57 --- /dev/null +++ b/src/gvcore/fileviewcontroller.h @@ -0,0 +1,256 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef FILEVIEWCONTROLLER_H +#define FILEVIEWCONTROLLER_H + +// Qt +#include <qdir.h> +#include <qslider.h> +#include <qwidget.h> + +// KDE +#include <kdirlister.h> +#include <kfileitem.h> +#include <kio/job.h> +#include <kurl.h> + +#include "libgwenview_export.h" +class QIconViewItem; +class QListViewItem; +class QPopupMenu; + +class KAccel; +class KAction; +class KActionCollection; +class KConfig; +class KListView; +class KRadioAction; +class KToggleAction; + +namespace Gwenview { +class FileViewBase; +class FileDetailView; +class FileThumbnailView; +class ImageLoader; + + +class DirLister; + +class LIBGWENVIEW_EXPORT FileViewController : public QWidget { +Q_OBJECT + +public: + enum Mode { FILE_LIST, THUMBNAIL}; + enum FilterMode { ALL, IMAGES_ONLY, VIDEOS_ONLY }; + + FileViewController(QWidget* parent,KActionCollection*); + ~FileViewController(); + + // Properties + void setMode(Mode); + + QString fileName() const; + KURL url() const; + KURL dirURL() const; + uint fileCount() const; + int shownFilePosition() const; + + uint selectionSize() const; + + FileViewBase* currentFileView() const; + FileThumbnailView* fileThumbnailView() const { return mFileThumbnailView; } + + KAction* selectFirst() const { return mSelectFirst; } + KAction* selectLast() const { return mSelectLast; } + KAction* selectPrevious() const { return mSelectPrevious; } + KAction* selectNext() const { return mSelectNext; } + KAction* selectPreviousDir() const { return mSelectPreviousDir; } + KAction* selectNextDir() const { return mSelectNextDir; } + KAction* selectFirstSubDir() const { return mSelectFirstSubDir; } + KRadioAction* listMode() const { return mListMode; } + KRadioAction* sideThumbnailMode() const { return mSideThumbnailMode; } + KRadioAction* bottomThumbnailMode() const { return mBottomThumbnailMode; } + KToggleAction* showDotFiles() const { return mShowDotFiles; } + + KURL::List selectedURLs() const; + KURL::List selectedImageURLs() const; + /** + * If set to true, no error messages will be displayed. + */ + void setSilentMode( bool silent ); + /** + * Returns true if there was an error since last URL had been opened. + */ + bool lastURLError() const; + /** + * Tries to open again the active URL. Useful for showing error messages + * initially supressed by silent mode. + */ + void retryURL(); + + void refreshItems( const KURL::List& urls ); // used by a workaround in KIPIInterface + + virtual void setFocus(); + +public slots: + void setDirURL(const KURL&); + void setFileNameToSelect(const QString&); + + void slotSelectFirst(); + void slotSelectLast(); + void slotSelectPrevious(); + void slotSelectNext(); + void slotSelectPreviousDir(); + void slotSelectNextDir(); + void slotSelectFirstSubDir(); + + void updateThumbnail(const KURL&); + + void updateFromSettings(); + + void setShowFilterBar(bool); + // 'int' suck, but I don't want to #include fileviewconfig.h + void setFilterMode(int); + void setFilterName(const QString&); + void setFilterFromDate(const QDate&); + void setFilterToDate(const QDate&); + void applyFilter(); + +signals: + void urlChanged(const KURL&); + /** + * Used by DirPart to tell Konqueror to change directory + */ + void directoryChanged(const KURL&); + + void selectionChanged(); + void completed(); + void canceled(); + void imageDoubleClicked(); + void shownFileItemRefreshed(const KFileItem*); + void sortingChanged(); + void requestContextMenu(const QPoint& pos, bool onItem); + +private slots: + void delayedDirListerCompleted(); + + // Used to enter directories + void slotViewExecuted(); + + // Used to change the current image + void slotViewClicked(); + + void slotViewDoubleClicked(); + + // These two methods forward the context menu requests from either view to + // openContextMenu(const QPoint&); + void openContextMenu(KListView*, QListViewItem*, const QPoint&); + void openContextMenu(QIconViewItem*,const QPoint&); + + // Get called by the thumbnail mode actions + void updateViewMode(); + + // Get called by the thumbnail slider + void updateThumbnailSize(int); + + void toggleShowDotFiles(); + void setSorting(); + void updateSortMenu(QDir::SortSpec); + + // Dir lister slots + void dirListerDeleteItem(KFileItem* item); + void dirListerNewItems(const KFileItemList& items); + void dirListerRefreshItems(const KFileItemList&); + void dirListerClear(); + void dirListerStarted(); + void dirListerCanceled(); + void dirListerCompleted(); + + void openDropURLMenu(QDropEvent*, KFileItem*); + + void prefetchDone(); + + void resetNameFilter(); + void resetFromFilter(); + void resetToFilter(); + +private: + struct Private; + Private* d; + Mode mMode; + FileDetailView* mFileDetailView; + FileThumbnailView* mFileThumbnailView; + DirLister* mDirLister; + KURL mDirURL; + ImageLoader* mPrefetch; + + // Our actions + KAction* mSelectFirst; + KAction* mSelectLast; + KAction* mSelectPrevious; + KAction* mSelectNext; + KAction* mSelectPreviousDir; + KAction* mSelectNextDir; + KAction* mSelectFirstSubDir; + + KRadioAction* mListMode; + KRadioAction* mSideThumbnailMode; + KRadioAction* mBottomThumbnailMode; + + QSlider* mSizeSlider; + + KToggleAction* mShowDotFiles; + + // Temp data used by the dir lister + bool mThumbnailsNeedUpdate; + QString mFileNameToSelect; + enum ChangeDirStatusVals { + CHANGE_DIR_STATUS_NONE, + CHANGE_DIR_STATUS_PREV, + CHANGE_DIR_STATUS_NEXT + } mChangeDirStatus; + + bool mBrowsing; + bool mSelecting; + + /** + * Browse to the given item. Prevents multiple calls using mBrowsing. + */ + void browseTo(KFileItem* item); + + void browseToFileNameToSelect(); + void emitURLChanged(); + void updateActions(); + void prefetch( KFileItem* item ); + + KFileItem* findFirstImage() const; + KFileItem* findLastImage() const; + KFileItem* findPreviousImage() const; + KFileItem* findNextImage() const; + KFileItem* findItemByFileName(const QString& fileName) const; + + bool eventFilter(QObject*, QEvent*); +}; + + +} // namespace +#endif + diff --git a/src/gvcore/filterbar.ui b/src/gvcore/filterbar.ui new file mode 100644 index 0000000..461e9f7 --- /dev/null +++ b/src/gvcore/filterbar.ui @@ -0,0 +1,255 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>FilterBar</class> +<widget class="QWidget"> + <property name="name"> + <cstring>FilterBar</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>809</width> + <height>30</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>3</number> + </property> + <widget class="QPushButton"> + <property name="name"> + <cstring>mResetNameCombo</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string></string> + </property> + </widget> + <widget class="Gwenview::ClickLineEdit"> + <property name="name"> + <cstring>mNameEdit</cstring> + </property> + <property name="clickMessage" stdset="0"> + <string>Name</string> + </property> + <property name="toolTip" stdset="0"> + <string>Filter files with wildcards, like *.png</string> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer1_2_2_2_3</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Maximum</enum> + </property> + <property name="sizeHint"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + </spacer> + <widget class="QPushButton"> + <property name="name"> + <cstring>mResetFrom</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string></string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel1_2</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>From:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>mFromDateEdit</cstring> + </property> + </widget> + <widget class="QDateEdit"> + <property name="name"> + <cstring>mFromDateEdit</cstring> + </property> + <property name="toolTip" stdset="0"> + <string>Only show files newer than or +equal to this date</string> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer1_2_2_2_2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Maximum</enum> + </property> + <property name="sizeHint"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + </spacer> + <widget class="QPushButton"> + <property name="name"> + <cstring>mResetTo</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string></string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel2</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>To:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>mToDateEdit</cstring> + </property> + </widget> + <widget class="QDateEdit"> + <property name="name"> + <cstring>mToDateEdit</cstring> + </property> + <property name="toolTip" stdset="0"> + <string>Only show files older than or equal to this date</string> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer1_2_2_2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Maximum</enum> + </property> + <property name="sizeHint"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + </spacer> + <widget class="QPushButton"> + <property name="name"> + <cstring>mFilterButton</cstring> + </property> + <property name="text"> + <string>&Filter</string> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer1_2_2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>16</width> + <height>16</height> + </size> + </property> + </spacer> + </hbox> +</widget> +<customwidgets> + <customwidget> + <class>Gwenview::ClickLineEdit</class> + <header location="local">clicklineedit.h</header> + <sizehint> + <width>100</width> + <height>20</height> + </sizehint> + <container>0</container> + <sizepolicy> + <hordata>1</hordata> + <verdata>0</verdata> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + <pixmap>image0</pixmap> + <property type="String">clickMessage</property> + </customwidget> +</customwidgets> +<images> + <image name="image0"> + <data format="PNG" length="818">89504e470d0a1a0a0000000d4948445200000016000000160806000000c4b46c3b000002f949444154388db5943168dc6614c77f57343cc10d12dc20c1054e5b0e6ac875ea1dedd2ed0e3ad4e0a1052f860c69c7ae490a1d1bdc4c4d8752770a5d42132834834137f4a20b047a1e0a2ab8200f067d83411f44a03708d2413e3bcef9da42ddb7083ebdeff7fddfff7ddf6b2549c23246a3d12bae20922469b596e067f367afa228425f2ab9c9b1a5fd57102d95344d8967315aead9bab354fae8a747848140d712f615384f1267997909b8ae181c0322fcf2e40d3080be54e85af232c6960ba82b10a0762972302715e0127484f09a22ed0aad4f37b7c10b2f1e7806ce4d4ed4d7536801e293a510ff5c319f5960698dc7e086c764cb673004289aaada6bc0b6b4287aaad4e7b75fe1c1bdbc417584fedb3dc481ec48591c581607969d5b21938f5da0827a0df8dc50c85278702f0360733b623206afd3fc53f549f661ef7ec6deb71961d0673816702e36fbad1570ed123f2ecea09f6c0bd2a9400aec89103f553ef850f8ec4e04c0c31f72d40a82ac572c0e14392c9e2b41c7633206757204416dc8f75f17cc67066b02766e85c44f3d16cf2dd9a1aed47e51b1d3745f51a2eb82d70171042d7dbef9aa81f6ba1ec3f77c5c4f190c7c00ccf13f8101700145a4f1d49e9c43015c11fc50c05196d557b5ac5056c05e07c0233b52548564aacc678620f0e8f502d23f0dbb7733b23f02cc61850051af91b3d66380e81af46f78a40796641f265b1ed6060cdff5f14361f7ae92fe6eb8fd29a8b5785d21da0073fc778a6b9076c5e6478d777bf733e65365e766c83b4388ae5b3effb24faf17608d416bd8dc0a89ba50bd360256153ba0350c86b0733364efbb94dd2f32e27d8fc186db3437cb3046c1697c9d4f2b46630fd75973ddb454b4ae4e570b26db2e6130e0e18f398b996131836630094157d8dc8a984e0b8a524169be9781d3346da6541b96ad188e61f07e487a18628f9650883620ecc1681c35673996f4c51a703c8b41042f04695ba89b2b25c8799603c640661af5ae231425a42f2cf91bcdbb60c5ebf3f4bfc6250fe40ac14992b4fe17f055c3932469fd051dce4f49a767eff00000000049454e44ae426082</data> + </image> +</images> +<tabstops> + <tabstop>mResetNameCombo</tabstop> + <tabstop>mResetFrom</tabstop> + <tabstop>mFromDateEdit</tabstop> + <tabstop>mResetTo</tabstop> + <tabstop>mToDateEdit</tabstop> + <tabstop>mFilterButton</tabstop> +</tabstops> +<layoutdefaults spacing="6" margin="11"/> +</UI> diff --git a/src/gvcore/fullscreenbar.cpp b/src/gvcore/fullscreenbar.cpp new file mode 100644 index 0000000..fb837ec --- /dev/null +++ b/src/gvcore/fullscreenbar.cpp @@ -0,0 +1,171 @@ +// vim:set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2004 Aur�ien G�eau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Qt +#include <qbitmap.h> +#include <qevent.h> +#include <qlayout.h> +#include <qpainter.h> +#include <qtimer.h> + +// KDE +#include <kdebug.h> + +// Local +#include "fullscreenbar.moc" +namespace Gwenview { + + +const int FULLSCREEN_ICON_SIZE = 32; +const int FULLSCREEN_LABEL_RADIUS = 6; +// Intervals are in milliseconds +const int SLIDE_IN_INTERVAL = 4; +const int SLIDE_OUT_INTERVAL = 12; +// Step is in pixels +const int SLIDE_STEP = 4; + + +static void fillMask(QPainter& painter, const QRect& rect) { + painter.fillRect( + rect.left(), + rect.top(), + rect.width() - FULLSCREEN_LABEL_RADIUS, + rect.height(), + painter.brush()); + + painter.fillRect( + rect.right() - FULLSCREEN_LABEL_RADIUS + 1, + rect.top(), + FULLSCREEN_LABEL_RADIUS, + rect.height() - FULLSCREEN_LABEL_RADIUS, + painter.brush()); + + painter.drawPie( + rect.right() - 2*FULLSCREEN_LABEL_RADIUS + 1, + rect.bottom() - 2*FULLSCREEN_LABEL_RADIUS + 1, + FULLSCREEN_LABEL_RADIUS*2, FULLSCREEN_LABEL_RADIUS*2, + 0, -16*90); +} + + +enum BarState { OUT, SLIDING_OUT, SLIDING_IN, IN }; + + +struct FullScreenBar::Private { + QTimer mTimer; + BarState mState; + bool mFirstShow; +}; + + +FullScreenBar::FullScreenBar(QWidget* parent) +: KToolBar(parent, "FullScreenBar") { + d=new Private; + d->mState=OUT; + d->mFirstShow=true; + setIconSize(FULLSCREEN_ICON_SIZE); + setMovingEnabled(false); + + QColor bg=colorGroup().highlight(); + QColor fg=colorGroup().highlightedText(); + QPalette pal(palette()); + pal.setColor(QColorGroup::Background, bg); + pal.setColor(QColorGroup::Foreground, fg); + pal.setColor(QColorGroup::Button, bg); + pal.setColor(QColorGroup::ButtonText, fg); + setPalette(pal); + + // Timer + connect(&d->mTimer, SIGNAL(timeout()), this, SLOT(slotUpdateSlide()) ); +} + + +FullScreenBar::~FullScreenBar() { + delete d; +} + + +void FullScreenBar::resizeEvent(QResizeEvent* event) { + KToolBar::resizeEvent(event); + + // Create a mask + QPainter painter; + QBitmap mask(size(), true); + painter.begin(&mask); + painter.setBrush(Qt::white); + fillMask(painter, rect()); + painter.end(); + + setMask(mask); +} + + +void FullScreenBar::showEvent(QShowEvent* event) { + KToolBar::showEvent(event); + // Make sure the bar position corresponds to the OUT state + if (!d->mFirstShow) return; + d->mFirstShow=false; + move(0, -height()); + layout()->setResizeMode(QLayout::Fixed); +} + + +void FullScreenBar::slideIn() { + if (d->mState!=IN) { + d->mState=SLIDING_IN; + d->mTimer.start(SLIDE_IN_INTERVAL); + } +} + + +void FullScreenBar::slideOut() { + if (d->mState!=OUT) { + d->mState=SLIDING_OUT; + d->mTimer.start(SLIDE_OUT_INTERVAL); + } +} + + +void FullScreenBar::slotUpdateSlide() { + int pos=y(); + + switch (d->mState) { + case SLIDING_OUT: + pos-=SLIDE_STEP; + if (pos<=-height()) { + d->mState=OUT; + d->mTimer.stop(); + } + break; + case SLIDING_IN: + pos+=SLIDE_STEP; + if (pos>=0) { + pos=0; + d->mState=IN; + d->mTimer.stop(); + } + break; + default: + kdWarning() << k_funcinfo << "We should not get there\n"; + } + move(0, pos); +} + +} // namespace diff --git a/src/gvcore/fullscreenbar.h b/src/gvcore/fullscreenbar.h new file mode 100644 index 0000000..3eb1919 --- /dev/null +++ b/src/gvcore/fullscreenbar.h @@ -0,0 +1,56 @@ +// vim:set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2004 Aur�ien G�eau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#ifndef FULLSCREENBAR_H +#define FULLSCREENBAR_H + +// KDE +#include <ktoolbar.h> + +class QResizeEvent; +class QShowEvent; +class QString; + +namespace Gwenview { +class FullScreenBar : public KToolBar { +Q_OBJECT +public: + FullScreenBar(QWidget* parent); + ~FullScreenBar(); + + void slideIn(); + void slideOut(); + +protected: + virtual void resizeEvent(QResizeEvent*); + virtual void showEvent(QShowEvent*); + +private slots: + void slotUpdateSlide(); + +private: + class Private; + Private* d; +}; + +} // namespace +#endif /* FULLSCREENBAR_H */ + diff --git a/src/gvcore/fullscreenconfig.kcfg b/src/gvcore/fullscreenconfig.kcfg new file mode 100644 index 0000000..38dbc4e --- /dev/null +++ b/src/gvcore/fullscreenconfig.kcfg @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE kcfg SYSTEM "http://www.kde.org/standards/kcfg/1.0/kcfg.dtd"> +<kcfg> + <kcfgfile name="gwenviewrc"/> + <group name="main window"> + <entry name="showBusyPtr" key="busy ptr in full screen" type="Bool"> + <default>true</default> + </entry> + </group> + <group name="pixmap widget"> + <entry name="osdFormat" type="String"> + <default>%f - %n/%N +%c</default> + </entry> + </group> +</kcfg> diff --git a/src/gvcore/fullscreenconfig.kcfgc b/src/gvcore/fullscreenconfig.kcfgc new file mode 100644 index 0000000..74be809 --- /dev/null +++ b/src/gvcore/fullscreenconfig.kcfgc @@ -0,0 +1,7 @@ +File=fullscreenconfig.kcfg +ClassName=FullScreenConfig +NameSpace=Gwenview +Singleton=true +Mutators=true +IncludeFiles=gvcore/libgwenview_export.h +Visibility=LIBGWENVIEW_EXPORT diff --git a/src/gvcore/gimp.h b/src/gvcore/gimp.h new file mode 100644 index 0000000..0c92b51 --- /dev/null +++ b/src/gvcore/gimp.h @@ -0,0 +1,138 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* -*- c++ -*- + * gimp.h: Header for a Qt 3 plug-in for reading GIMP XCF image files + * Copyright (C) 2001 lignum Computing, Inc. <allen@lignumcomputing.com> + * + * This plug-in is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +/* + * These are the constants and functions I extracted from The GIMP source + * code. If the reader fails to work, this is probably the place to start + * looking for discontinuities. + */ + +// From GIMP "tile.h" v1.2 + +const uint TILE_WIDTH = 64; //!< Width of a tile in the XCF file. +const uint TILE_HEIGHT = 64; //!< Height of a tile in the XCF file. + +// From GIMP "paint_funcs.c" v1.2 + +const int RANDOM_TABLE_SIZE = 4096; //!< Size of dissolve random number table. +const int RANDOM_SEED = 314159265; //!< Seed for dissolve random number table. +const double EPSILON = 0.0001; //!< Roundup in alpha blending. + +// From GIMP "paint_funcs.h" v1.2 + +const uchar OPAQUE_OPACITY = 255; //!< Opaque value for 8-bit alpha component. + +// From GIMP "apptypes.h" v1.2 + +//! Basic GIMP image type. QImage converter may produce a deeper image +//! than is specified here. For example, a grayscale image with an +//! alpha channel must (currently) use a 32-bit Qt image. + +typedef enum +{ + RGB, + GRAY, + INDEXED +} GimpImageBaseType; + +//! Type of individual layers in an XCF file. + +typedef enum +{ + RGB_GIMAGE, + RGBA_GIMAGE, + GRAY_GIMAGE, + GRAYA_GIMAGE, + INDEXED_GIMAGE, + INDEXEDA_GIMAGE +} GimpImageType; + +//! Effect to apply when layers are merged together. + +typedef enum +{ + NORMAL_MODE, + DISSOLVE_MODE, + BEHIND_MODE, + MULTIPLY_MODE, + SCREEN_MODE, + OVERLAY_MODE, + DIFFERENCE_MODE, + ADDITION_MODE, + SUBTRACT_MODE, + DARKEN_ONLY_MODE, + LIGHTEN_ONLY_MODE, + HUE_MODE, + SATURATION_MODE, + COLOR_MODE, + VALUE_MODE, + DIVIDE_MODE, + ERASE_MODE, + REPLACE_MODE, + ANTI_ERASE_MODE +} LayerModeEffects; + +// From GIMP "xcf.c" v1.2 + +//! Properties which can be stored in an XCF file. + +typedef enum +{ + PROP_END = 0, + PROP_COLORMAP = 1, + PROP_ACTIVE_LAYER = 2, + PROP_ACTIVE_CHANNEL = 3, + PROP_SELECTION = 4, + PROP_FLOATING_SELECTION = 5, + PROP_OPACITY = 6, + PROP_MODE = 7, + PROP_VISIBLE = 8, + PROP_LINKED = 9, + PROP_PRESERVE_TRANSPARENCY = 10, + PROP_APPLY_MASK = 11, + PROP_EDIT_MASK = 12, + PROP_SHOW_MASK = 13, + PROP_SHOW_MASKED = 14, + PROP_OFFSETS = 15, + PROP_COLOR = 16, + PROP_COMPRESSION = 17, + PROP_GUIDES = 18, + PROP_RESOLUTION = 19, + PROP_TATTOO = 20, + PROP_PARASITES = 21, + PROP_UNIT = 22, + PROP_PATHS = 23, + PROP_USER_UNIT = 24 +} PropType; + +// From GIMP "xcf.c" v1.2 + +//! Compression type used in layer tiles. + +typedef enum +{ + COMPRESS_NONE = 0, + COMPRESS_RLE = 1, + COMPRESS_ZLIB = 2, + COMPRESS_FRACTAL = 3 /* Unused. */ +} CompressionType; + + diff --git a/src/gvcore/imageframe.h b/src/gvcore/imageframe.h new file mode 100644 index 0000000..3825f06 --- /dev/null +++ b/src/gvcore/imageframe.h @@ -0,0 +1,42 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef IMAGEFRAME_H +#define IMAGEFRAME_H + +// Qt +#include <qimage.h> +#include <qvaluevector.h> +namespace Gwenview { + +// Local + +struct ImageFrame { + ImageFrame( const QImage& i, int d ) : image( i ), delay( d ) {}; + ImageFrame() : delay( 0 ) {} // stupid Qt containers + QImage image; + int delay; // how long this frame should be shown in the animation +}; + +typedef QValueVector< ImageFrame > ImageFrames; + +} // namespace +#endif /* IMAGEFRAME_H */ + diff --git a/src/gvcore/imageloader.cpp b/src/gvcore/imageloader.cpp new file mode 100644 index 0000000..e1be4e8 --- /dev/null +++ b/src/gvcore/imageloader.cpp @@ -0,0 +1,917 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "imageloader.h" + +#include <assert.h> + +// Qt +#include <qtimer.h> +#include <qwmatrix.h> + +// KDE +#include <kapplication.h> +#include <kimageio.h> +#include <kmimetype.h> + +// 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 QMap< KURL, ImageLoader* > sLoaders; + +//--------------------------------------------------------------------- +// +// CancellableBuffer +// This class acts like QBuffer, but will simulates a truncated file if the +// TSThread which was passed to its constructor has been asked for cancellation +// +//--------------------------------------------------------------------- +class CancellableBuffer : public QBuffer { +public: + CancellableBuffer(QByteArray buffer, TSThread* thread) + : QBuffer(buffer), mThread(thread) {} + + bool atEnd() const { + if (mThread->testCancel()) { + LOG("cancel detected"); + return true; + } + return QBuffer::atEnd(); + } + + Q_LONG readBlock(char * data, Q_ULONG maxlen) { + if (mThread->testCancel()) { + LOG("cancel detected"); + return 0; + } + return QBuffer::readBlock(data, maxlen); + } + + Q_LONG readLine(char * data, Q_ULONG maxlen) { + if (mThread->testCancel()) { + LOG("cancel detected"); + return 0; + } + return QBuffer::readLine(data, maxlen); + } + + QByteArray readAll() { + if (mThread->testCancel()) { + LOG("cancel detected"); + return QByteArray(); + } + return QBuffer::readAll(); + } + + int getch() { + if (mThread->testCancel()) { + LOG("cancel detected"); + setStatus(IO_ReadError); + return -1; + } + return QBuffer::getch(); + } + +private: + TSThread* mThread; +}; + + +//--------------------------------------------------------------------- +// +// DecoderThread +// +//--------------------------------------------------------------------- +void DecoderThread::run() { + QMutexLocker locker(&mMutex); + LOG(""); + + // This block makes sure imageIO won't access the image after the signal + // has been posted + { + QImageIO imageIO; + + CancellableBuffer buffer(mRawData, this); + buffer.open(IO_ReadOnly); + imageIO.setIODevice(&buffer); + bool ok=imageIO.read(); + if (testCancel()) { + LOG("cancelled"); + return; + } + + if (!ok) { + LOG("failed"); + postSignal( this, SIGNAL(failed()) ); + return; + } + + LOG("succeeded"); + mImage=imageIO.image(); + } + + LOG("succeeded, emitting signal"); + postSignal( this, SIGNAL(succeeded()) ); +} + + +void DecoderThread::setRawData(const QByteArray& data) { + QMutexLocker locker(&mMutex); + mRawData=data.copy(); +} + + +QImage DecoderThread::popLoadedImage() { + QMutexLocker locker(&mMutex); + QImage img=mImage; + mImage=QImage(); + return img; +} + + + +//--------------------------------------------------------------------- +// +// ImageLoaderPrivate +// +//--------------------------------------------------------------------- +struct OwnerData { + const QObject* 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 + QDateTime mTimestamp; + + // The raw data we get + QByteArray mRawData; + + // The async decoder and it's waking timer + QImageDecoder mDecoder; + QTimer 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 + QRect mLoadChangedRect; + + // The time since we last emitted the imageChanged() signal + QTime mTimeSinceLastUpdate; + + // Whether the loading should be suspended + bool mSuspended; + + // Delay used for next frame after it's finished decoding. + int mNextFrameDelay; + + bool mWasFrameData; + + QImage mProcessedImage; // image frame currently being decoded + + QRegion mLoadedRegion; // loaded parts of mProcessedImage + + ImageFrames mFrames; + + QCString mImageFormat; + + ImageUtils::Orientation mOrientation; + + QString mMimeType; + MimeTypeUtils::Kind mURLKind; + + QValueVector< OwnerData > mOwners; // loaders may be shared + + + void determineImageFormat() { + Q_ASSERT(mRawData.size()>0); + QBuffer buffer(mRawData); + buffer.open(IO_ReadOnly); + mImageFormat = QImageIO::imageFormat(&buffer); + } +}; + + +//--------------------------------------------------------------------- +// +// ImageLoader +// +//--------------------------------------------------------------------- +ImageLoader::ImageLoader() { + LOG(""); + d = new ImageLoaderPrivate(this); + connect( BusyLevelManager::instance(), SIGNAL( busyLevelChanged(BusyLevel)), + this, 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, SIGNAL(timeout()), this, SLOT(decodeChunk()) ); + + connect(&d->mDecoderThread, SIGNAL(succeeded()), + this, SLOT(slotDecoderThreadSucceeded()) ); + connect(&d->mDecoderThread, SIGNAL(failed()), + this, 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, SIGNAL(result(KIO::Job*)), + this, 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<KIO::StatJob*>(job)->statResult(); + KIO::UDSEntry::ConstIterator it= entry.begin(); + QDateTime 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, SIGNAL(data(KIO::Job*, const QByteArray&)), + this, SLOT(slotDataReceived(KIO::Job*, const QByteArray&)) ); + + connect(getJob, SIGNAL(result(KIO::Job*)), + this, 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 QString mimeTypeFromFormat(const char* format) { + QStringList formats = KImageIO::types(KImageIO::Reading); + QStringList mimeTypes = KImageIO::mimeTypes(KImageIO::Reading); + int pos = formats.findIndex(QString::fromAscii(format)); + Q_ASSERT(pos != -1); + return mimeTypes[pos]; +} + +void ImageLoader::slotDataReceived(KIO::Job* job, const QByteArray& 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 + QBuffer buffer(d->mRawData); + buffer.open(IO_ReadOnly); + const char* format = QImageIO::imageFormat(&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 = QMIN(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 = QByteArray(); + d->mImageFormat = QCString(); + d->mProcessedImage = QImage(); + 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( QValueVector< OwnerData >::ConstIterator it = d->mOwners.begin(); + it != d->mOwners.end(); + ++it ) { + mylevel = QMAX( 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(); +} + + +//--------------------------------------------------------------------- +// +// QImageConsumer +// +//--------------------------------------------------------------------- +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". + QTimer::singleShot(0, this, SLOT(callFinish()) ); +} + + +void ImageLoader::callFinish() { + finish(true); +} + + +void ImageLoader::changed(const QRect& constRect) { + LOG2(""); + QRect 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) { + QSize size = content.size(); + d->mProcessedImage = QImage(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 + QImage 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 + QWMatrix matrix = ImageUtils::transformMatrix(d->mOrientation); + + QRect 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 = QRect(); + } +} + +void ImageLoader::frameDone() { + frameDone( QPoint( 0, 0 ), d->mDecoder.image().rect()); +} + +void ImageLoader::frameDone(const QPoint& offset, const QRect& rect) { + LOG(""); + // Another case where the image loading in Qt'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 = QRect(); + d->mTimeSinceLastUpdate.start(); + } + d->mLoadedRegion = QRegion(); + + QImage image; + if (d->mProcessedImage.isNull()) { + image = d->mDecoder.image().copy(); + } else { + image = d->mProcessedImage.copy(); + } + + if( offset != QPoint( 0, 0 ) || rect != image.rect()) { + // Blit last frame below 'image' + if( !d->mFrames.isEmpty()) { + QImage 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 +} + + +QImage ImageLoader::processedImage() const { + return d->mProcessedImage; +} + + +ImageFrames ImageLoader::frames() const { + return d->mFrames; +} + + +QCString ImageLoader::imageFormat() const { + return d->mImageFormat; +} + + +QByteArray ImageLoader::rawData() const { + return d->mRawData; +} + + +QString ImageLoader::mimeType() const { + return d->mMimeType; +} + + +MimeTypeUtils::Kind ImageLoader::urlKind() const { + return d->mURLKind; +} + + +KURL ImageLoader::url() const { + return d->mURL; +} + + +QRegion ImageLoader::loadedRegion() const { + return d->mLoadedRegion; +} + + +bool ImageLoader::completed() const { + return d->mDecodeState == DECODE_DONE; +} + + +void ImageLoader::ref( const QObject* owner, BusyLevel priority ) { + OwnerData data; + data.owner = owner; + data.priority = priority; + d->mOwners.append( data ); + connect( owner, SIGNAL( destroyed()), SLOT( ownerDestroyed())); +} + +void ImageLoader::deref( const QObject* owner ) { + for( QValueVector< 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 QObject* owner ) { + disconnect( owner ); + deref( owner ); +} + +void ImageLoader::ownerDestroyed() { + deref( sender()); +} + +//--------------------------------------------------------------------- +// +// Managing loaders +// +//--------------------------------------------------------------------- + +ImageLoader* ImageLoader::loader( const KURL& url, const QObject* 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. + QTimer::singleShot( priority >= BUSY_LOADING ? 0 : 10, loader, SLOT( startLoading())); + return loader; +} + +} // namespace diff --git a/src/gvcore/imageloader.h b/src/gvcore/imageloader.h new file mode 100644 index 0000000..c1cc1ec --- /dev/null +++ b/src/gvcore/imageloader.h @@ -0,0 +1,123 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef IMAGELOADER_H +#define IMAGELOADER_H + +// Qt +#include <qasyncimageio.h> +#include <qbuffer.h> +#include <qcstring.h> + +// KDE +#include <kio/job.h> + +// Local +#include "tsthread/tsthread.h" +#include "imageframe.h" +#include "busylevelmanager.h" +#include "mimetypeutils.h" + +#include "libgwenview_export.h" +namespace Gwenview { +class DecoderThread : public TSThread { +Q_OBJECT +public: + void setRawData(const QByteArray&); + QImage popLoadedImage(); + +signals: + void failed(); + void succeeded(); + +protected: + void run(); + +private: + QMutex mMutex; + QByteArray mRawData; + QImage mImage; +}; + +class ImageLoaderPrivate; + +class LIBGWENVIEW_EXPORT ImageLoader : public QObject, public QImageConsumer { +Q_OBJECT +public: + static ImageLoader* loader( const KURL& url, const QObject* owner, BusyLevel priority ); // use this instead of ctor + void release( const QObject* owner ); // use this instead of dtor + + QImage processedImage() const; + ImageFrames frames() const; + QCString imageFormat() const; + QByteArray rawData() const; + QString mimeType() const; + MimeTypeUtils::Kind urlKind() const; + KURL url() const; + QRegion loadedRegion() const; // valid parts of processedImage() + bool completed() const; + +signals: + void urlKindDetermined(); + void sizeLoaded(int, int); + void imageChanged(const QRect&); + void imageLoaded( bool ok ); + +private slots: + void slotStatResult(KIO::Job*); + void slotDataReceived(KIO::Job*, const QByteArray& chunk); + void slotGetResult(KIO::Job*); + void decodeChunk(); + void slotDecoderThreadFailed(); + void slotDecoderThreadSucceeded(); + void slotBusyLevelChanged( BusyLevel ); + void ownerDestroyed(); + void startLoading(); + void callFinish(); + +private: + ImageLoader(); + ~ImageLoader(); + void ref( const QObject* owner, BusyLevel priority ); + void deref( const QObject* owner ); + void suspendLoading(); + void resumeLoading(); + void finish( bool ok ); + void startThread(); + void setURL( const KURL& url ); + void checkPendingStat(); + void checkPendingGet(); + BusyLevel priority() const; + + // QImageConsumer methods + void end(); + void changed(const QRect&); + void frameDone(); + void frameDone(const QPoint& offset, const QRect& rect); + void setLooping(int); + void setFramePeriod(int milliseconds); + void setSize(int, int); + + ImageLoaderPrivate* d; +}; + +} // namespace +#endif /* IMAGELOADER_H */ + diff --git a/src/gvcore/imagesavedialog.cpp b/src/gvcore/imagesavedialog.cpp new file mode 100644 index 0000000..f7405af --- /dev/null +++ b/src/gvcore/imagesavedialog.cpp @@ -0,0 +1,131 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Qt includes +#include <qtimer.h> + +// KDE includes +#include <kdebug.h> +#include <kdeversion.h> +#include <kfilefiltercombo.h> +#include <kimageio.h> +#include <klocale.h> +#include <kurlcombobox.h> + +// Our includes +#include "imagesavedialog.moc" +namespace Gwenview { + + +static int findFormatInFilterList(const QStringList& filters, const QString& format) { + int pos=0; + for(QStringList::const_iterator it=filters.begin(); it!=filters.end(); ++it,++pos) { + QStringList list=QStringList::split("|",*it); + if ( list[1].startsWith(format) ) return pos; + } + return -1; +} + + +ImageSaveDialog::ImageSaveDialog(KURL& url, const QCString& imageFormat, QWidget* parent) +: KFileDialog(":ImageSaveDialog",QString::null,parent,"imagesavedialog",true) +, mURL(url) +, mImageFormat(imageFormat) +{ + setOperationMode(KFileDialog::Saving); + + // FIXME: Ugly code to define the filter combo label. + KMimeType::List types; + setFilterMimeType(i18n("Format:"),types,KMimeType::mimeType("")); + + QStringList filters; + + // Create our filter list + QStringList mimeTypes=KImageIO::mimeTypes(); + for(QStringList::const_iterator it=mimeTypes.begin(); it!=mimeTypes.end(); ++it) { + QString format=KImageIO::typeForMime(*it); + + // Create the pattern part of the filter string + KMimeType::Ptr mt=KMimeType::mimeType(*it); + QStringList patterns; + for (QStringList::const_iterator patIt=mt->patterns().begin();patIt!=mt->patterns().end();++patIt) { + QString pattern=(*patIt).lower(); + if (!patterns.contains(pattern)) patterns.append(pattern); + } + if (patterns.isEmpty()) { + patterns.append( QString("*.%1").arg(format.lower()) ); + } + QString patternString=patterns.join(" "); + + // Create the filter string + QString filter=patternString + "|" + + format + " - " + mt->comment() + + " (" + patternString + ")"; + + // Add it to our list + filters.append(filter); + } + + qHeapSort(filters); + setFilter(filters.join("\n")); + + // Select the default image format + int pos=findFormatInFilterList(filters,mImageFormat); + if (pos==-1) { + pos=findFormatInFilterList(filters,"PNG"); + mImageFormat="PNG"; + } + + filterWidget->setCurrentItem(pos); + + // Tweak the filter widget + filterWidget->setEditable(false); + + connect(filterWidget,SIGNAL(activated(const QString&)), + this,SLOT(updateImageFormat(const QString&)) ); + + // Call slotFilterChanged() to get the list filtered by the filter we + // selected. If we don't use a single shot, it leads to a crash :-/ + QTimer::singleShot(0,this,SLOT(slotFilterChanged())); +} + + +void ImageSaveDialog::accept() { + KFileDialog::accept(); + mURL=selectedURL(); +} + + +void ImageSaveDialog::updateImageFormat(const QString& text) { + QStringList list=QStringList::split(" ",text); + mImageFormat=list[0].local8Bit(); + + QString name=locationEdit->currentText(); + QString suffix=KImageIO::suffix(mImageFormat); + int dotPos=name.findRev('.'); + if (dotPos>-1) { + name=name.left(dotPos); + } + name.append('.').append(suffix); + locationEdit->setCurrentText(name); +} + + +} // namespace diff --git a/src/gvcore/imagesavedialog.h b/src/gvcore/imagesavedialog.h new file mode 100644 index 0000000..6aba763 --- /dev/null +++ b/src/gvcore/imagesavedialog.h @@ -0,0 +1,53 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef IMAGESAVEDIALOG_H +#define IMAGESAVEDIALOG_H + +// KDE includes +#include <kfiledialog.h> + +class QString; +class QWidget; + +class KURL; + +namespace Gwenview { +class ImageSaveDialog : public KFileDialog { +Q_OBJECT +public: + ImageSaveDialog(KURL& url,const QCString& imageFormat,QWidget* parent); + QCString imageFormat() const { return mImageFormat; } + +protected slots: + void accept(); + +private slots: + void updateImageFormat(const QString&); + +private: + KURL& mURL; + QCString mImageFormat; + QMap<QString,QString> mImageFormats; +}; + +} // namespace +#endif + diff --git a/src/gvcore/imageview.cpp b/src/gvcore/imageview.cpp new file mode 100644 index 0000000..f22b171 --- /dev/null +++ b/src/gvcore/imageview.cpp @@ -0,0 +1,1469 @@ +// vim:set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2004 Aur�ien G�eau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "config.h" + +#include "imageview.moc" + +#include <assert.h> +#include <math.h> + +// Qt +#include <qcolor.h> +#include <qcombobox.h> +#include <qcursor.h> +#include <qdatetime.h> +#include <qevent.h> +#include <qpainter.h> +#include <qpixmap.h> +#include <qlabel.h> +#include <qtimer.h> +#include <qvaluevector.h> + +// KDE +#include <kaction.h> +#include <kconfig.h> +#include <kdebug.h> +#include <kdeversion.h> +#include <klocale.h> +#include <kstandarddirs.h> +#include <kstdaction.h> +#include <kurldrag.h> +#include <kapplication.h> + +// Local +#include "document.h" +#include "imageutils/imageutils.h" +#include "bcgdialog.h" +#include "busylevelmanager.h" +#include "imageviewtools.h" +#include "imageutils/croppedqimage.h" +#include "imageviewconfig.h" + +namespace Gwenview { + +/* + +Coordinates: + +The image can be zoomed, can have a position offset, and additionally there is +QScrollView's viewport. This means there are several coordinate systems. + + + +Let's start from simple things. Viewport ignored, zoom ignored: + + A----------------------------------- + | | + | | + | B--------------------- | + | | | | + | | | | + | | | | + | | | | + | | | | + | | | | + | ---------------------C | + | | + | | + ------------------------------------ + + +The inner rectangle is the image, outer rectangle is the widget. +A = [ 0, 0 ] +B = [ mXOffset, mYOffset ] +C = B + [ mDocument->width(), mDocument->height() ] + + + +The same, additionally the image is zoomed. + +A = [ 0, 0 ] +B = [ mXOffset, mYOffset ] +C = [ mZoom * mDocument->width(), mZoom * mDocument->height()) ] + +The groups of functions imageToWidget() and widgetToImage() do conversions +between the image and widget coordinates, i.e. imageToWidget() accepts coordinates +in the image (original,not zoomed,image's topleft corner is [0,0]) and returns +coordinates in the picture above, widgetToImage() works the other way around. + +There's no bounds checking, so widgetToImage( A ) in the example above would +return image coordinate with negative x,y. + +The widgetToImage() functions round the values (in order to have the conversion +as approximate as possible). However when converting from widget to image and back +this can result in the final rectangle being smaller than the original. +The widgetToImageBounding() function converts from widget to image coordinates +in a way which makes sure the reverse conversion will be at least as large +as the original geometry. + +There are no conversion functions for only width/height, as their conversion +depends on the position (because of the rounding etc.). For similar reasons +conversions should not be done with the bottomright corner of a rectangle, +but with the point next to it. + + + +For conversions from/to QScrollView's viewport, usually QScrollView methods should +be used: contentsX(), contentsY(), contentsWidth(), contentsHeight(), visibleWidth(), +visibleHeight(), contentsToViewport() and viewportToContents(). + +*/ + +const double MAX_ZOOM=16.0; // Same value as GIMP + +const int DEFAULT_MAX_REPAINT_SIZE = 10000; +const int LIMIT_MAX_REPAINT_SIZE = 10000000; + +#ifndef HAVE_LROUND +inline +long int lround( double x ) { + return static_cast< long int >( x >= 0 ? x + 0.5 : x - 0.5 ); +} +#endif + + +struct ImageView::Private { + Document* mDocument; + + Tools mTools; + + ToolID mToolID; + + // Offset to center images + int mXOffset, mYOffset; + + // Zoom info + ZoomMode mZoomMode; + double mZoom; + + // Gamma, brightness, contrast - multiplied by 100 + int mGamma, mBrightness, mContrast; + + // Our actions + QComboBox* mZoomCombo; + // We do not use KSelectAction because it's not possible to set the combo text + KWidgetAction* mZoomComboAction; + KToggleAction* mZoomToFit; + KToggleAction* mZoomToWidth; + KToggleAction* mZoomToHeight; + QValueVector<KToggleAction*> mZoomComboActions; + KAction* mZoomIn; + KAction* mZoomOut; + KAction* mResetZoom; + KToggleAction* mLockZoom; + KAction* mAdjustBCG; + KAction* mIncreaseGamma; + KAction* mDecreaseGamma; + KAction* mIncreaseBrightness; + KAction* mDecreaseBrightness; + KAction* mIncreaseContrast; + KAction* mDecreaseContrast; + KActionCollection* mActionCollection; + BCGDialog* mBCGDialog; + + // Fullscreen stuff + bool mFullScreen; + + // Object state info + bool mOperaLikePrevious; // Flag to avoid showing the popup menu on Opera like previous + double mZoomBeforeAuto; + int mXCenterBeforeAuto, mYCenterBeforeAuto; + + QMap< long long, PendingPaint > mPendingPaints; + QRegion mPendingNormalRegion; + QRegion mPendingSmoothRegion; + int mPendingOperations; + QTimer mPendingPaintTimer; + bool mSmoothingSuspended; + QRegion mValidImageArea; + + int imageToWidgetX( int x ) const { + if( mZoom == 1.0 ) return x + mXOffset; + return lround( x * mZoom ) + mXOffset; + } + + int imageToWidgetY( int y ) const { + if( mZoom == 1.0 ) return y + mYOffset; + return lround( y * mZoom ) + mYOffset; + } + + QPoint imageToWidget( const QPoint& p ) const { + return QPoint( imageToWidgetX( p.x()), imageToWidgetY( p.y())); + } + + QRect imageToWidget( const QRect& r ) const { + return QRect( imageToWidget( r.topLeft()), + // don't use bottomright corner for conversion, but the one next to it + imageToWidget( r.bottomRight() + QPoint( 1, 1 )) - QPoint( 1, 1 )); + } + + int widgetToImageX( int x ) const { + if( mZoom == 1.0 ) return x - mXOffset; + return lround( ( x - mXOffset ) / mZoom ); + } + + int widgetToImageY( int y ) const { + if( mZoom == 1.0 ) return y - mYOffset; + return lround( ( y - mYOffset ) / mZoom ); + } + + QPoint widgetToImage( const QPoint& p ) const { + return QPoint( widgetToImageX( p.x()), widgetToImageY( p.y())); + } + + QRect widgetToImage( const QRect& r ) const { + return QRect( widgetToImage( r.topLeft()), + // don't use bottomright corner for conversion, but the one next to it + widgetToImage( r.bottomRight() + QPoint( 1, 1 )) - QPoint( 1, 1 )); + } + + QRect widgetToImageBounding( const QRect& r, int extra ) const { + QRect ret = widgetToImage( r ); + // make sure converting to image and back always returns QRect at least as large as 'r' + extra += mZoom == 1.0 ? 0 : int( ceil( 1 / mZoom )); + ret.addCoords( -extra, -extra, extra, extra ); + return ret; + } + + void initZoomCombo() { + mZoomCombo->clear(); + for (QValueVector<KToggleAction*>::iterator it=mZoomComboActions.begin(); + it!=mZoomComboActions.end(); + ++it) + { + QString txt=(*it)->plainText(); + mZoomCombo->insertItem(txt); + } + + const double zoomValues[] = { 0.5, 1, 2 }; + int nbValues=sizeof(zoomValues) / sizeof(double); + for (int pos=0; pos<nbValues; ++pos) { + QString txt=QString("%1%").arg( int(zoomValues[pos]*100) ); + mZoomCombo->insertItem(txt); + } + } +}; + + +inline bool doDelayedSmoothing() { + return ImageViewConfig::delayedSmoothing() + && ImageViewConfig::smoothAlgorithm()!=ImageUtils::SMOOTH_NONE; +} + + +class ImageView::EventFilter : public QObject { +public: + EventFilter(ImageView* parent) + : QObject(parent) {} + + bool eventFilter(QObject*, QEvent* event) { + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: + case QEvent::AccelOverride: + return static_cast< ImageView* >( parent()) + ->viewportKeyEvent(static_cast<QKeyEvent*>(event)); + default: + break; + } + return false; + } +}; + + + +ImageView::ImageView(QWidget* parent,Document* document, KActionCollection* actionCollection) +: QScrollView(parent,0L,WResizeNoErase|WRepaintNoErase|WPaintClever) +{ + d=new Private; + d->mDocument=document; + d->mToolID=SCROLL; + d->mXOffset=0; + d->mYOffset=0; + d->mZoomMode=static_cast<ZoomMode>( ImageViewConfig::zoomMode() ); + d->mZoom=1; + d->mActionCollection=actionCollection; + d->mFullScreen=false; + d->mOperaLikePrevious=false; + d->mZoomBeforeAuto=1; + d->mPendingOperations= 0 ; + d->mSmoothingSuspended= false ; + d->mGamma = 100; + d->mBrightness = 0; + d->mContrast = 100; + d->mBCGDialog = 0; + + viewport()->setFocusPolicy(WheelFocus); + setFrameStyle(NoFrame); + setAcceptDrops( true ); + viewport()->setAcceptDrops( true ); + + updateScrollBarMode(); + viewport()->setBackgroundColor(ImageViewConfig::backgroundColor() ); + + d->mTools[SCROLL]=new ScrollTool(this); + d->mTools[ZOOM]=new ZoomTool(this); + d->mTools[d->mToolID]->updateCursor(); + + // Create actions + d->mZoomToFit=new KToggleAction(i18n("Fit to &Window"),"viewmagfit",0,d->mActionCollection,"view_zoom_to_fit"); + connect(d->mZoomToFit,SIGNAL(toggled(bool)), + this,SLOT(setZoomToFit(bool)) ); + d->mZoomToWidth=new KToggleAction(i18n("Fit to &Width"),0,0,d->mActionCollection,"view_zoom_to_width"); + connect(d->mZoomToWidth,SIGNAL(toggled(bool)), + this,SLOT(setZoomToWidth(bool)) ); + d->mZoomToHeight=new KToggleAction(i18n("Fit to &Height"),0,0,d->mActionCollection,"view_zoom_to_height"); + connect(d->mZoomToHeight,SIGNAL(toggled(bool)), + this,SLOT(setZoomToHeight(bool)) ); + + d->mZoomIn=KStdAction::zoomIn(this,SLOT(slotZoomIn()),d->mActionCollection); + + d->mZoomOut=KStdAction::zoomOut(this,SLOT(slotZoomOut()),d->mActionCollection); + + d->mResetZoom=KStdAction::actualSize(this,SLOT(slotResetZoom()),d->mActionCollection); + d->mResetZoom->setIcon("viewmag1"); + + d->mLockZoom=new KToggleAction(i18n("&Lock Zoom"),"lock",0,d->mActionCollection,"view_zoom_lock"); + d->mLockZoom->setChecked(ImageViewConfig::lockZoom()); + connect(d->mLockZoom,SIGNAL(toggled(bool)), + this,SLOT(setLockZoom(bool)) ); + + d->mZoomCombo=new QComboBox(true); + // Avoid stealing focus + d->mZoomCombo->setFocusPolicy(ClickFocus); + connect(d->mZoomCombo, SIGNAL(activated(int)), + this, SLOT(slotSelectZoom()) ); + + d->mZoomComboAction=new KWidgetAction(d->mZoomCombo, i18n("Zoom"), 0, 0, 0, d->mActionCollection, "view_zoom_to"); + + d->mZoomComboActions.append(d->mZoomToFit); + d->mZoomComboActions.append(d->mZoomToWidth); + d->mZoomComboActions.append(d->mZoomToHeight); + if (d->mZoomMode!=ZOOM_FREE) { + d->mZoomComboActions[d->mZoomMode]->setChecked(true); + } + d->initZoomCombo(); + + d->mAdjustBCG=new KAction(i18n("Adjust Brightness/Contrast/Gamma"), "colorize", 0, + this, SLOT(showBCGDialog()), d->mActionCollection, "adjust_bcg"); + d->mIncreaseGamma=new KAction(i18n("Increase Gamma"),0,CTRL+Key_G, + this,SLOT(increaseGamma()),d->mActionCollection,"increase_gamma"); + d->mDecreaseGamma=new KAction(i18n("Decrease Gamma"),0,SHIFT+CTRL+Key_G, + this,SLOT(decreaseGamma()),d->mActionCollection,"decrease_gamma"); + d->mIncreaseBrightness=new KAction(i18n("Increase Brightness" ),0,CTRL+Key_B, + this,SLOT(increaseBrightness()),d->mActionCollection,"increase_brightness"); + d->mDecreaseBrightness=new KAction(i18n("Decrease Brightness" ),0,SHIFT+CTRL+Key_B, + this,SLOT(decreaseBrightness()),d->mActionCollection,"decrease_brightness"); + d->mIncreaseContrast=new KAction(i18n("Increase Contrast" ),0,CTRL+Key_C, + this,SLOT(increaseContrast()),d->mActionCollection,"increase_contrast"); + d->mDecreaseContrast=new KAction(i18n("Decrease Contrast" ),0,SHIFT+CTRL+Key_C, + this,SLOT(decreaseContrast()),d->mActionCollection,"decrease_contrast"); + + // Connect to some interesting signals + connect(d->mDocument,SIGNAL(loaded(const KURL&)), + this,SLOT(slotLoaded()) ); + + connect(d->mDocument,SIGNAL(loading()), + this,SLOT( loadingStarted()) ); + + connect(d->mDocument,SIGNAL(modified()), + this,SLOT(slotModified()) ); + + connect(d->mDocument, SIGNAL(sizeUpdated()), + this, SLOT(slotImageSizeUpdated()) ); + + connect(d->mDocument, SIGNAL(rectUpdated(const QRect&)), + this, SLOT(slotImageRectUpdated(const QRect&)) ); + + connect(&d->mPendingPaintTimer,SIGNAL(timeout()), + this,SLOT(checkPendingOperations()) ); + + connect(BusyLevelManager::instance(),SIGNAL(busyLevelChanged(BusyLevel)), + this,SLOT(slotBusyLevelChanged(BusyLevel) )); + + // This event filter is here to make sure the pixmap view is aware of the changes + // in the keyboard modifiers, even if it isn't focused. However, making this widget + // itself the filter would lead to doubled paint events, because QScrollView + // installs an event filter on its viewport, and doesn't filter out the paint + // events -> it'd get it twice, first from app filter, second from viewport filter. + EventFilter* filter=new EventFilter(this); + kapp->installEventFilter(filter); +} + + +ImageView::~ImageView() { + ImageViewConfig::setZoomMode(d->mZoomMode); + ImageViewConfig::setLockZoom(d->mLockZoom->isChecked()); + ImageViewConfig::self()->writeConfig(); + delete d->mTools[SCROLL]; + delete d->mTools[ZOOM]; + delete d; +} + + +void ImageView::slotLoaded() { + if (d->mDocument->isNull()) { + resizeContents(0,0); + viewport()->repaint(false); + return; + } + + if (doDelayedSmoothing()) scheduleOperation( SMOOTH_PASS ); +} + + +void ImageView::slotModified() { + if (d->mZoomMode!=ZOOM_FREE) { + updateZoom(d->mZoomMode); + } else { + updateContentSize(); + updateImageOffset(); + updateZoomActions(); + fullRepaint(); + } +} + + +void ImageView::loadingStarted() { + cancelPending(); + d->mSmoothingSuspended = true; + d->mValidImageArea = QRegion(); + d->mGamma = 100; + d->mBrightness = 0; + d->mContrast = 100; + + if (!d->mLockZoom->isChecked()) { + d->mZoomBeforeAuto = 1.; + } +} + +//------------------------------------------------------------------------ +// +// Properties +// +//------------------------------------------------------------------------ +double ImageView::zoom() const { + return d->mZoom; +} + + +bool ImageView::fullScreen() const { + return d->mFullScreen; +} + + +QPoint ImageView::offset() const { + return QPoint(d->mXOffset, d->mYOffset); +} + + +bool ImageView::canZoom(bool in) const { + KAction* zoomAction=in ? d->mZoomIn : d->mZoomOut; + return zoomAction->isEnabled(); +} + + +KToggleAction* ImageView::zoomToFit() const { + return d->mZoomToFit; +} + + +void ImageView::updateFromSettings() { + // Reset, so that next repaint doesn't possibly take longer because of + // smoothing + ImageViewConfig::setMaxRepaintSize(DEFAULT_MAX_REPAINT_SIZE); + ImageViewConfig::setMaxScaleRepaintSize(DEFAULT_MAX_REPAINT_SIZE); + ImageViewConfig::setMaxSmoothRepaintSize(DEFAULT_MAX_REPAINT_SIZE); + + if( doDelayedSmoothing() ) { + scheduleOperation( SMOOTH_PASS ); + } else { + fullRepaint(); + } + + // If enlargeSmallImage changed + if (d->mZoomMode!=ZOOM_FREE) { + updateZoom(d->mZoomMode); + } + + updateScrollBarMode(); + + if (!d->mFullScreen) { + viewport()->setBackgroundColor(ImageViewConfig::backgroundColor() ); + } +} + + +void ImageView::setZoom(double zoom, int centerX, int centerY) { + updateZoom(ZOOM_FREE, zoom, centerX, centerY); +} + + +void ImageView::updateZoom(ZoomMode zoomMode, double value, int centerX, int centerY) { + ZoomMode oldZoomMode = d->mZoomMode; + double oldZoom=d->mZoom; + d->mZoomMode=zoomMode; + KAction* checkedZoomAction=0; + + viewport()->setUpdatesEnabled(false); + + if (zoomMode==ZOOM_FREE) { + Q_ASSERT(value!=0); + d->mZoom=value; + } else { + if (oldZoomMode == ZOOM_FREE) { + // Only store zoom before auto if we were in ZOOM_FREE mode, otherwise + // we will store the computed auto zoom value (Bug 134590) + d->mZoomBeforeAuto = d->mZoom; + } + d->mXCenterBeforeAuto=width()/2 + contentsX() + d->mXOffset; + d->mYCenterBeforeAuto=height()/2 + contentsY() + d->mYOffset; + + if (zoomMode==ZOOM_FIT) { + d->mZoom=computeZoomToFit(); + checkedZoomAction=d->mZoomToFit; + + } else if (zoomMode==ZOOM_FIT_WIDTH) { + d->mZoom=computeZoomToWidth(); + checkedZoomAction=d->mZoomToWidth; + + } else { + d->mZoom=computeZoomToHeight(); + checkedZoomAction=d->mZoomToHeight; + } + } + + // Make sure only one zoom action is toggled on + d->mZoomToFit->setChecked( checkedZoomAction==d->mZoomToFit); + d->mZoomToWidth->setChecked( checkedZoomAction==d->mZoomToWidth); + d->mZoomToHeight->setChecked(checkedZoomAction==d->mZoomToHeight); + + updateContentSize(); + + // Find the coordinate of the center of the image + // and center the view on it + if (centerX==-1) { + centerX=int( ((visibleWidth()/2+contentsX()-d->mXOffset)/oldZoom)*d->mZoom ); + } + if (centerY==-1) { + centerY=int( ((visibleHeight()/2+contentsY()-d->mYOffset)/oldZoom)*d->mZoom ); + } + center(centerX,centerY); + + updateScrollBarMode(); + updateImageOffset(); + updateZoomActions(); + + viewport()->setUpdatesEnabled(true); + fullRepaint(); +} + + +void ImageView::setFullScreen(bool fullScreen) { + d->mFullScreen=fullScreen; + + if (d->mFullScreen) { + viewport()->setBackgroundColor(black); + } else { + viewport()->setBackgroundColor(ImageViewConfig::backgroundColor() ); + } +} + + +//------------------------------------------------------------------------ +// +// Overloaded methods +// +//------------------------------------------------------------------------ +void ImageView::resizeEvent(QResizeEvent* event) { + QScrollView::resizeEvent(event); + if (d->mZoomMode!=ZOOM_FREE) { + updateZoom(d->mZoomMode); + } else { + updateContentSize(); + updateImageOffset(); + } +} + + +inline void composite(uint* rgba,uint value) { + uint alpha=(*rgba) >> 24; + if (alpha<255) { + uint alphaValue=(255-alpha)*value; + + uint c1=( ( (*rgba & 0xFF0000) >> 16 ) * alpha + alphaValue ) >> 8; + uint c2=( ( (*rgba & 0x00FF00) >> 8 ) * alpha + alphaValue ) >> 8; + uint c3=( ( (*rgba & 0x0000FF) >> 0 ) * alpha + alphaValue ) >> 8; + *rgba=0xFF000000 + (c1<<16) + (c2<<8) + c3; + } +} + +void ImageView::drawContents(QPainter* painter,int clipx,int clipy,int clipw,int cliph) { + // Erase borders + QRect imageRect(0, 0, d->mDocument->width(), d->mDocument->height()); + imageRect = d->imageToWidget(imageRect); + + QRect widgetRect = QRect(0, 0, visibleWidth(), visibleHeight()); + + QRegion region = QRegion(widgetRect) - imageRect; + QMemArray<QRect> rects = region.rects(); + for(unsigned int pos = 0; pos < rects.count(); ++pos ) { + painter->eraseRect(rects[pos]); + } + + // Repaint + if( !d->mValidImageArea.isEmpty()) { + addPendingPaint( false, QRect( clipx, clipy, clipw, cliph )); + } +} + +// How this pending stuff works: +// There's a queue of areas to paint (each with bool saying whether it's smooth pass). +// Also, there's a bitfield of pending operations, operations are handled only after +// there's nothing more to paint (so that smooth pass is started). +void ImageView::addPendingPaint( bool smooth, QRect rect ) { + if( d->mSmoothingSuspended && smooth ) return; + + // try to avoid scheduling already scheduled areas + QRegion& region = smooth ? d->mPendingSmoothRegion : d->mPendingNormalRegion; + if( region.intersect( rect ) == QRegion( rect )) + return; // whole rect has already pending paints + // at least try to remove the part that's already scheduled + rect = ( QRegion( rect ) - region ).boundingRect(); + region += rect; + if( rect.isEmpty()) + return; + addPendingPaintInternal( smooth, rect ); +} + +void ImageView::addPendingPaintInternal( bool smooth, QRect rect ) { + const long long MAX_DIM = 1000000; // if monitors get larger than this, we're in trouble :) + // QMap will ensure ordering (non-smooth first, top-to-bottom, left-to-right) + long long key = ( smooth ? MAX_DIM * MAX_DIM : 0 ) + rect.y() * MAX_DIM + rect.x(); + // handle the case of two different paints at the same position (just in case) + key *= 100; + bool insert = true; + while( d->mPendingPaints.contains( key )) { + if( d->mPendingPaints[ key ].rect.contains( rect )) { + insert = false; + break; + } + if( rect.contains( d->mPendingPaints[ key ].rect )) { + break; + } + ++key; + } + if( insert ) { + d->mPendingPaints[ key ] = PendingPaint( smooth, rect ); + } + scheduleOperation( CHECK_OPERATIONS ); +} + +void ImageView::checkPendingOperations() { + checkPendingOperationsInternal(); + if( d->mPendingPaints.isEmpty() && d->mPendingOperations == 0 ) { + d->mPendingPaintTimer.stop(); + } + updateBusyLevels(); +} + +void ImageView::limitPaintSize( PendingPaint& paint ) { + // The only thing that makes time spent in performPaint() vary + // is whether there will be scaling and whether there will be smoothing. + // So there are three max sizes for each mode. + int maxSize = ImageViewConfig::maxRepaintSize(); + if( d->mZoom != 1.0 ) { + if( paint.smooth || !doDelayedSmoothing() ) { + maxSize = ImageViewConfig::maxSmoothRepaintSize(); + } else { + maxSize = ImageViewConfig::maxScaleRepaintSize(); + } + } + // don't paint more than max_size pixels at a time + int maxHeight = ( maxSize + paint.rect.width() - 1 ) / paint.rect.width(); // round up + maxHeight = QMAX( maxHeight, 5 ); // at least 5 lines together + // can't repaint whole paint at once, adjust height and schedule the rest + if( maxHeight < paint.rect.height()) { + QRect remaining = paint.rect; + remaining.setTop( remaining.top() + maxHeight ); + addPendingPaintInternal( paint.smooth, remaining ); + paint.rect.setHeight( maxHeight ); + } +} + + +void ImageView::checkPendingOperationsInternal() { + if( !d->mPendingPaintTimer.isActive()) // suspended + return; + while( !d->mPendingPaints.isEmpty()) { + PendingPaint paint = *d->mPendingPaints.begin(); + d->mPendingPaints.remove( d->mPendingPaints.begin()); + limitPaintSize( paint ); // modifies paint.rect if necessary + QRegion& region = paint.smooth ? d->mPendingSmoothRegion : d->mPendingNormalRegion; + region -= paint.rect; + QRect visibleRect( contentsX(), contentsY(), visibleWidth(), visibleHeight()); + QRect paintRect = paint.rect.intersect( visibleRect ); + if( !paintRect.isEmpty()) { + QPainter painter( viewport()); + painter.translate( -contentsX(), -contentsY()); + performPaint( &painter, paintRect.x(), paintRect.y(), + paintRect.width(), paintRect.height(), paint.smooth ); + return; + } + } + if( d->mPendingOperations & SMOOTH_PASS ) { + d->mSmoothingSuspended = false; + if( doDelayedSmoothing() ) { + QRect visibleRect( contentsX(), contentsY(), visibleWidth(), visibleHeight()); + addPendingPaint( true, visibleRect ); + } + d->mPendingOperations &= ~SMOOTH_PASS; + return; + } +} + +void ImageView::scheduleOperation( Operation operation ) +{ + d->mPendingOperations |= operation; + slotBusyLevelChanged( BusyLevelManager::instance()->busyLevel()); + updateBusyLevels(); +} + +void ImageView::updateBusyLevels() { + if( !d->mPendingPaintTimer.isActive()) { + BusyLevelManager::instance()->setBusyLevel( this, BUSY_NONE ); + } else if( !d->mPendingPaints.isEmpty() && !(*d->mPendingPaints.begin()).smooth ) { + BusyLevelManager::instance()->setBusyLevel( this, BUSY_PAINTING ); + } else if(( d->mPendingOperations & SMOOTH_PASS ) + || ( !d->mPendingPaints.isEmpty() && (*d->mPendingPaints.begin()).smooth )) { + BusyLevelManager::instance()->setBusyLevel( this, BUSY_SMOOTHING ); + } else { + assert( false ); + } +} + +void ImageView::slotBusyLevelChanged( BusyLevel level ) { + bool resume = false; + if( level <= BUSY_PAINTING + && !d->mPendingPaints.isEmpty() && !(*d->mPendingPaints.begin()).smooth ) { + resume = true; + } else if( level <= BUSY_SMOOTHING + && (( d->mPendingOperations & SMOOTH_PASS ) + || ( !d->mPendingPaints.isEmpty() && (*d->mPendingPaints.begin()).smooth ))) { + resume = true; + } + if( resume ) { + d->mPendingPaintTimer.start( 0 ); + } else { + d->mPendingPaintTimer.stop(); + } +} + +// How to do painting: +// When something needs to be erased: QPainter on viewport and eraseRect() +// When whole picture needs to be repainted: fullRepaint() +// When a part of the picture needs to be updated: viewport()->repaint(area,false) +// All other paints will be changed to progressive painting. +void ImageView::fullRepaint() { + if( !viewport()->isUpdatesEnabled()) return; + cancelPending(); + viewport()->repaint(false); +} + +void ImageView::cancelPending() { + d->mPendingPaints.clear(); + d->mPendingNormalRegion = QRegion(); + d->mPendingSmoothRegion = QRegion(); + d->mPendingPaintTimer.stop(); + d->mPendingOperations = 0; + updateBusyLevels(); +} + +//#define DEBUG_RECTS + +// do the actual painting +void ImageView::performPaint( QPainter* painter, int clipx, int clipy, int clipw, int cliph, bool secondPass ) { + #ifdef DEBUG_RECTS + static QColor colors[4]={QColor(255,0,0),QColor(0,255,0),QColor(0,0,255),QColor(255,255,0) }; + static int numColor=0; + #endif + + QTime t; + t.start(); + + if (d->mDocument->isNull()) { + painter->eraseRect(clipx,clipy,clipw,cliph); + return; + } + + // True if another pass will follow + bool fastpass = doDelayedSmoothing() && zoom() != 1.0 && !secondPass; + + ImageUtils::SmoothAlgorithm smoothAlgo = ImageUtils::SMOOTH_NONE; + if( zoom() != 1.0 ) { + if (doDelayedSmoothing() && !secondPass) { + // Add a second, smoothing pass + addPendingPaint( true, QRect( clipx, clipy, clipw, cliph )); + } else { + // We need to smooth now + smoothAlgo = static_cast<ImageUtils::SmoothAlgorithm>( ImageViewConfig::smoothAlgorithm() ); + } + } + + int extraPixels = ImageUtils::extraScalePixels( smoothAlgo, zoom()); + QRect imageRect = d->widgetToImageBounding( QRect(clipx,clipy,clipw,cliph), extraPixels ); + imageRect = imageRect.intersect( QRect( 0, 0, d->mDocument->width(), d->mDocument->height())); + QMemArray< QRect > rects = d->mValidImageArea.intersect( imageRect ).rects(); + for( unsigned int i = 1; i < rects.count(); ++i ) { + addPendingPaint( secondPass, d->imageToWidget( rects[ i ] )); + } + imageRect = rects.count() > 0 ? rects[ 0 ] : QRect(); + if (imageRect.isEmpty()) { + painter->eraseRect(clipx,clipy,clipw,cliph); + return; + } + QRect widgetRect = d->imageToWidget( imageRect ); + if (widgetRect.isEmpty() || imageRect.isEmpty()) { + painter->eraseRect(clipx,clipy,clipw,cliph); + return; + } + +// With very large images, just getting a subimage using QImage::copy( QRect ) takes a significant +// portion of time here (even though it's just copying of data - probably because it's a lot of data). +// So don't do any subimage copying but instead use CroppedQImage which just manipulates scanline +// pointers. Note however that it's a bit hackish and there may be trouble if any code accesses +// the image data directly as a whole. See CroppedQImage for details. + +// QImage image = d->mDocument->image().copy( imageRect ); + ImageUtils::CroppedQImage image( d->mDocument->image(), imageRect ); + + if( zoom() != 1.0 ) { + image=ImageUtils::scale(image,widgetRect.width(),widgetRect.height(), smoothAlgo ); + } + + if( d->mBrightness != 0 ) { + image.normalize(); // needed, it will be modified + image = ImageUtils::changeBrightness( image, d->mBrightness ); + } + + if( d->mContrast != 100 ) { // != 1.0 + image.normalize(); // needed, it will be modified + image = ImageUtils::changeContrast( image, d->mContrast ); + } + + if( d->mGamma != 100 ) { // != 1.0 + image.normalize(); // needed, it will be modified + image = ImageUtils::changeGamma( image, d->mGamma ); + } + +// Calling normalize() here would make image to be a proper QImage without modified scanlines, +// so that even calling QImage::copy() would work. However, it seems it's not necessary to call +// it here. The code above checks that QImage::copy() or similar doesn't occur (that zoom() != 1.0 +// is there primarily to avoid that). If any kind of redraw trouble occurs, try uncommenting this +// line below first. +// image.normalize(); // make it use its own data, if needed + + if (image.hasAlphaBuffer()) { + image.normalize(); // needed, it will be modified + if (image.depth()!=32) { + image=image.convertDepth(32); + } + + bool light; + + int imageXOffset=widgetRect.x()-d->mXOffset; + int imageYOffset=widgetRect.y()-d->mYOffset; + int imageWidth=image.width(); + int imageHeight=image.height(); + for (int y=0;y<imageHeight;++y) { + uint* rgba=(uint*)(image.scanLine(y)); + for(int x=0;x<imageWidth;x++) { + light= ((x+imageXOffset) & 16) ^ ((y+imageYOffset) & 16); + composite(rgba,light?192:128); + rgba++; + } + } + image.setAlphaBuffer(false); + } + + QRect paintRect( clipx, clipy, clipw, cliph ); + QPixmap buffer( paintRect.size()); + { + QPainter bufferPainter(&buffer); + bufferPainter.setBackgroundColor(painter->backgroundColor()); + bufferPainter.eraseRect(0,0,paintRect.width(),paintRect.height()); + bufferPainter.drawImage(widgetRect.topLeft()-paintRect.topLeft(),image, + fastpass?ThresholdDither:0); + } + painter->drawPixmap(paintRect.topLeft(),buffer); + + if( paintRect.width() * paintRect.height() >= 10000 ) { // ignore small repaints + // try to do one step in 0.1sec + int size = paintRect.width() * paintRect.height() * 100 / QMAX( t.elapsed(), 1 ); + + int maxRepaintSize; + if (zoom() == 1.0) { + maxRepaintSize=ImageViewConfig::maxRepaintSize(); + } else { + if (smoothAlgo!=ImageUtils::SMOOTH_NONE) { + maxRepaintSize=ImageViewConfig::maxSmoothRepaintSize(); + } else { + maxRepaintSize=ImageViewConfig::maxScaleRepaintSize(); + } + } + + maxRepaintSize = KCLAMP( + ( size + maxRepaintSize ) / 2, + 10000, LIMIT_MAX_REPAINT_SIZE); + + if (zoom() == 1.0) { + ImageViewConfig::setMaxRepaintSize(maxRepaintSize); + } else { + if (smoothAlgo!=ImageUtils::SMOOTH_NONE) { + ImageViewConfig::setMaxSmoothRepaintSize(maxRepaintSize); + } else { + ImageViewConfig::setMaxScaleRepaintSize(maxRepaintSize); + } + } + } + + #ifdef DEBUG_RECTS + painter->setPen(colors[numColor]); + numColor=(numColor+1)%4; + painter->drawRect(paintRect); + #endif + + QApplication::flushX(); +} + + +void ImageView::viewportMousePressEvent(QMouseEvent* event) { + viewport()->setFocus(); + switch (event->button()) { + case Qt::LeftButton: + d->mTools[d->mToolID]->leftButtonPressEvent(event); + break; + case Qt::RightButton: + d->mTools[d->mToolID]->rightButtonPressEvent(event); + break; + default: // Avoid compiler complain + break; + } +} + + +void ImageView::viewportMouseMoveEvent(QMouseEvent* event) { + selectTool(event->state(), true); + d->mTools[d->mToolID]->mouseMoveEvent(event); +} + + +void ImageView::viewportMouseReleaseEvent(QMouseEvent* event) { + switch (event->button()) { + case Qt::LeftButton: + if (event->stateAfter() & Qt::RightButton) { + d->mOperaLikePrevious=true; + emit selectPrevious(); + return; + } + d->mTools[d->mToolID]->leftButtonReleaseEvent(event); + break; + + case Qt::MidButton: + d->mTools[d->mToolID]->midButtonReleaseEvent(event); + break; + + case Qt::RightButton: + if (event->stateAfter() & Qt::LeftButton) { + emit selectNext(); + return; + } + + if (d->mOperaLikePrevious) { // Avoid showing the popup menu after Opera like previous + d->mOperaLikePrevious=false; + } else { + d->mTools[d->mToolID]->rightButtonReleaseEvent(event); + } + break; + + default: // Avoid compiler complain + break; + } +} + + +bool ImageView::eventFilter(QObject* obj, QEvent* event) { + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: + case QEvent::AccelOverride: + return viewportKeyEvent(static_cast<QKeyEvent*>(event)); + + case QEvent::MouseButtonDblClick: + if (d->mToolID==ZOOM) return false; + emit doubleClicked(); + return true; + + // Getting/loosing focus causes repaints, but repainting here is expensive, + // and there's no need to repaint on focus changes, as the focus is not + // indicated. + case QEvent::FocusIn: + case QEvent::FocusOut: + return true; + + case QEvent::Enter: + selectTool( kapp->keyboardMouseState(), true ); + emitRequestHintDisplay(); + break; + + default: + break; + } + return QScrollView::eventFilter(obj,event); +} + + +bool ImageView::viewportKeyEvent(QKeyEvent* event) { + selectTool(event->stateAfter(), false); + return false; +} + + +void ImageView::contentsDragEnterEvent(QDragEnterEvent* event) { + event->accept( QUriDrag::canDecode( event )); +} + +void ImageView::contentsDropEvent(QDropEvent* event) { + KURL::List list; + if( KURLDrag::decode( event, list )) { + d->mDocument->setURL( list.first()); + } +} + +void ImageView::keyPressEvent( QKeyEvent *event ) { + QScrollView::keyPressEvent( event ); + int deltaX, deltaY; + + if (event->state() != Qt::NoButton) { + return; + } + switch (event->key()) { + case Key_Up: + deltaX = 0; + deltaY = -1; + break; + case Key_Down: + deltaX = 0; + deltaY = 1; + break; + case Key_Left: + deltaX = -1; + deltaY = 0; + break; + case Key_Right: + deltaX = 1; + deltaY = 0; + break; + default: + return; + } + deltaX *= width() / 2; + deltaY *= height() / 2; + scrollBy (deltaX, deltaY); +} + +/** + * If force is set, the cursor will be updated even if the tool is not + * different from the current one. + */ +void ImageView::selectTool(ButtonState state, bool force) { + ToolID oldToolID=d->mToolID; + if (state & ControlButton) { + d->mToolID=ZOOM; + if (d->mToolID!=oldToolID) { + emitRequestHintDisplay(); + } + } else { + d->mToolID=SCROLL; + } + + if (d->mToolID!=oldToolID || force) { + d->mTools[d->mToolID]->updateCursor(); + } +} + + +void ImageView::wheelEvent(QWheelEvent* event) { + d->mTools[d->mToolID]->wheelEvent(event); +} + + +//------------------------------------------------------------------------ +// +// Slots +// +//------------------------------------------------------------------------ +void ImageView::slotZoomIn() { + updateZoom(ZOOM_FREE, computeZoom(true)); +} + + +void ImageView::slotZoomOut() { + updateZoom(ZOOM_FREE, computeZoom(false)); +} + + +void ImageView::slotResetZoom() { + updateZoom(ZOOM_FREE, 1.0); +} + + +void ImageView::slotSelectZoom() { + int currentItem=d->mZoomCombo->currentItem(); + + if (currentItem>=int(d->mZoomComboActions.count()) ) { + QString txt=d->mZoomCombo->currentText(); + txt=txt.left(txt.find('%')); + double value=KGlobal::locale()->readNumber(txt) / 100.0; + updateZoom(ZOOM_FREE, value); + } else { + d->mZoomComboActions[currentItem]->activate(); + } +} + + +void ImageView::setZoomToFit(bool on) { + if (on) { + updateZoom(ZOOM_FIT); + } else { + updateZoom(ZOOM_FREE, d->mZoomBeforeAuto, d->mXCenterBeforeAuto, d->mYCenterBeforeAuto); + } +} + + +void ImageView::setZoomToWidth(bool on) { + if (on) { + updateZoom(ZOOM_FIT_WIDTH); + } else { + updateZoom(ZOOM_FREE, d->mZoomBeforeAuto, d->mXCenterBeforeAuto, d->mYCenterBeforeAuto); + } +} + + +void ImageView::setZoomToHeight(bool on) { + if (on) { + updateZoom(ZOOM_FIT_HEIGHT); + } else { + updateZoom(ZOOM_FREE, d->mZoomBeforeAuto, d->mXCenterBeforeAuto, d->mYCenterBeforeAuto); + } +} + + +void ImageView::setLockZoom(bool value) { + if( value ) { + d->mZoomToFit->setChecked( false ); + d->mZoomToWidth->setChecked( false ); + d->mZoomToHeight->setChecked( false ); + } + // don't change zoom here, keep it even if it was from some auto zoom mode +} + + +void ImageView::showBCGDialog() { + if (!d->mBCGDialog) { + d->mBCGDialog=new BCGDialog(this); + } + d->mBCGDialog->show(); +} + + +int ImageView::brightness() const { + return d->mBrightness; +} + + +void ImageView::setBrightness(int value) { + d->mBrightness=value; + fullRepaint(); +} + +void ImageView::increaseBrightness() { + d->mBrightness = KCLAMP( d->mBrightness + 5, -100, 100 ); + emit bcgChanged(); + fullRepaint(); +} + +void ImageView::decreaseBrightness() { + d->mBrightness = KCLAMP( d->mBrightness - 5, -100, 100 ); + emit bcgChanged(); + fullRepaint(); +} + + + +int ImageView::contrast() const { + return d->mContrast - 100; +} + +void ImageView::setContrast(int value) { + d->mContrast=value + 100; + fullRepaint(); +} + +void ImageView::increaseContrast() { + d->mContrast = KCLAMP( d->mContrast + 10, 0, 500 ); + emit bcgChanged(); + fullRepaint(); +} + +void ImageView::decreaseContrast() { + d->mContrast = KCLAMP( d->mContrast - 10, 0, 500 ); + emit bcgChanged(); + fullRepaint(); +} + + + +int ImageView::gamma() const { + return d->mGamma - 100; +} + + +void ImageView::setGamma(int value) { + d->mGamma=value + 100; + fullRepaint(); +} + +void ImageView::increaseGamma() { + d->mGamma = KCLAMP( d->mGamma + 10, 10, 500 ); + emit bcgChanged(); + fullRepaint(); +} + +void ImageView::decreaseGamma() { + d->mGamma = KCLAMP( d->mGamma - 10, 10, 500 ); + emit bcgChanged(); + fullRepaint(); +} +//------------------------------------------------------------------------ +// +// Private +// +//------------------------------------------------------------------------ +void ImageView::emitRequestHintDisplay() { + if (d->mDocument->isNull()) return; + + emit requestHintDisplay( d->mTools[d->mToolID]->hint() ); +} + + +void ImageView::slotImageSizeUpdated() { + d->mXOffset=0; + d->mYOffset=0; + + d->mValidImageArea = QRegion(); + if (d->mZoomMode!=ZOOM_FREE) { + d->mXCenterBeforeAuto=0; + d->mYCenterBeforeAuto=0; + } else { + horizontalScrollBar()->setValue(0); + verticalScrollBar()->setValue(0); + } + if (d->mZoomMode!=ZOOM_FREE) { + updateZoom(d->mZoomMode); + } else { + if( !d->mLockZoom->isChecked()) { + setZoom( 1.0 ); + } + } + + updateZoomActions(); + d->mAdjustBCG->setEnabled(!d->mDocument->isNull()); + d->mIncreaseGamma->setEnabled(!d->mDocument->isNull()); + d->mDecreaseGamma->setEnabled(!d->mDocument->isNull()); + d->mIncreaseBrightness->setEnabled(!d->mDocument->isNull()); + d->mDecreaseBrightness->setEnabled(!d->mDocument->isNull()); + d->mIncreaseContrast->setEnabled(!d->mDocument->isNull()); + d->mDecreaseContrast->setEnabled(!d->mDocument->isNull()); + + updateContentSize(); + updateImageOffset(); + updateScrollBarMode(); + fullRepaint(); +} + +void ImageView::slotImageRectUpdated(const QRect& imageRect) { + d->mValidImageArea += imageRect; + viewport()->repaint( d->imageToWidget( imageRect ), false ); +} + + +void ImageView::updateScrollBarMode() { + if (d->mZoomMode==ZOOM_FIT || !ImageViewConfig::showScrollBars()) { + setVScrollBarMode(AlwaysOff); + setHScrollBarMode(AlwaysOff); + } else { + setVScrollBarMode(Auto); + setHScrollBarMode(Auto); + } +} + + +void ImageView::updateContentSize() { + resizeContents( + int(d->mDocument->width()*d->mZoom), + int(d->mDocument->height()*d->mZoom) ); +} + +double ImageView::computeZoomToFit() const { + if (d->mDocument->isNull()) { + return 1.0; + } + QSize size=d->mDocument->image().size(); + size.scale(width(),height(),QSize::ScaleMin); + + double zoom=double(size.width())/d->mDocument->width(); + if (zoom>1.0 && !ImageViewConfig::enlargeSmallImages()) return 1.0; + return zoom; +} + +double ImageView::computeZoomToWidth() const { + if (d->mDocument->isNull()) { + return 1.0; + } + int sw = verticalScrollBar()->sizeHint().width(); // geometry is not valid before first show() + int w = width(); + int dw = d->mDocument->width(); + switch( vScrollBarMode()) { + case AlwaysOff: + return double(w)/dw; + case AlwaysOn: + return double(w-sw)/dw; + case Auto: + default: + // there will be a vertical scrollbar if the image's height will be too large + if( d->mDocument->height() * (double(w)/dw) > height()) return double(w-sw)/dw; + return double(w)/dw; + } +} + +double ImageView::computeZoomToHeight() const { + if (d->mDocument->isNull()) { + return 1.0; + } + int sh = horizontalScrollBar()->sizeHint().height(); + int h = height(); + int dh = d->mDocument->height(); + switch( vScrollBarMode()) { + case AlwaysOff: + return double(h)/dh; + case AlwaysOn: + return double(h-sh)/dh; + case Auto: + default: + if( d->mDocument->width() * (double(h)/dh) > width()) return double(h-sh)/dh; + return double(h)/dh; + } +} + +double ImageView::computeZoom(bool in) const { + const double F = 0.5; // change in 0.5 steps + double zoomtofit = computeZoomToFit(); + double zoomtowidth = computeZoomToWidth(); + double zoomtoheight = computeZoomToHeight(); + if (in) { + double newzoom; + if (d->mZoom>=1.0) { + newzoom = (floor(d->mZoom/F)+1.0)*F; + } else { + newzoom = 1/(( ceil(1/d->mZoom/F)-1.0 )*F); + } + if( d->mZoom < zoomtofit && zoomtofit < newzoom ) newzoom = zoomtofit; + if( d->mZoom < zoomtowidth && zoomtowidth < newzoom ) newzoom = zoomtowidth; + if( d->mZoom < zoomtoheight && zoomtoheight < newzoom ) newzoom = zoomtoheight; + return newzoom; + } else { + double newzoom; + if (d->mZoom>1.0) { + newzoom = (ceil(d->mZoom/F)-1.0)*F; + } else { + newzoom = 1/(( floor(1/d->mZoom/F)+1.0 )*F); + } + if( d->mZoom > zoomtofit && zoomtofit > newzoom ) newzoom = zoomtofit; + if( d->mZoom > zoomtowidth && zoomtowidth > newzoom ) newzoom = zoomtowidth; + if( d->mZoom > zoomtoheight && zoomtoheight > newzoom ) newzoom = zoomtoheight; + return newzoom; + } +} + +void ImageView::updateImageOffset() { + int viewWidth=width(); + int viewHeight=height(); + + // Compute d->mXOffset and d->mYOffset in case the image does not fit + // the view width or height + int zpixWidth=int(d->mDocument->width() * d->mZoom); + int zpixHeight=int(d->mDocument->height() * d->mZoom); + + if (zpixWidth>viewWidth && hScrollBarMode()!=AlwaysOff) { + // use sizeHint() - geometry is not valid before first show() + viewHeight-=horizontalScrollBar()->sizeHint().height(); + } + if (zpixHeight>viewHeight && vScrollBarMode()!=AlwaysOff) { + viewWidth-=verticalScrollBar()->sizeHint().width(); + } + + d->mXOffset=QMAX(0,(viewWidth-zpixWidth)/2); + d->mYOffset=QMAX(0,(viewHeight-zpixHeight)/2); +} + + +void ImageView::updateZoomActions() { + // Disable most actions if there's no image + if (d->mDocument->isNull()) { + d->mZoomComboAction->setEnabled(false); + d->mZoomIn->setEnabled(false); + d->mZoomOut->setEnabled(false); + d->mResetZoom->setEnabled(false); + return; + } + + d->mZoomComboAction->setEnabled(true); + d->mZoomToFit->setEnabled(true); + d->mZoomToWidth->setEnabled(true); + d->mZoomToHeight->setEnabled(true); + d->mResetZoom->setEnabled(true); + + + if (d->mZoomMode==ZOOM_FREE) { + d->mZoomIn->setEnabled(d->mZoom<MAX_ZOOM); + d->mZoomOut->setEnabled(d->mZoom>1/MAX_ZOOM); + QString zoomText=QString("%1%").arg(int(d->mZoom*100)); + d->mZoomCombo->setCurrentText(zoomText); + } else { + d->mZoomIn->setEnabled(true); + d->mZoomOut->setEnabled(true); + d->mZoomCombo->setCurrentItem(d->mZoomMode); + } +} + +} // namespace diff --git a/src/gvcore/imageview.h b/src/gvcore/imageview.h new file mode 100644 index 0000000..74e3c66 --- /dev/null +++ b/src/gvcore/imageview.h @@ -0,0 +1,190 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + + +#ifndef IMAGEVIEW_H +#define IMAGEVIEW_H + +// Qt +#include <qmap.h> +#include <qscrollview.h> +#include <qtimer.h> +#include <qvaluelist.h> + +// Local +#include "busylevelmanager.h" +#include "imageutils/imageutils.h" +#include "libgwenview_export.h" +class QEvent; +class QLabel; +class QMouseEvent; +class QPainter; +class QTimer; +class QWheelEvent; +class KAction; +class KActionCollection; +class KToggleAction; +typedef QValueList<KAction *> KActionPtrList; + +namespace Gwenview { +class Document; + +class LIBGWENVIEW_EXPORT ImageView : public QScrollView { +Q_OBJECT + +public: + class ToolBase; + class ZoomTool; + class ScrollTool; + class EventFilter; +#if __GNUC__ < 3 + friend class ToolBase; + friend class ZoomTool; + friend class ScrollTool; +#endif + friend class EventFilter; + + enum ToolID { SCROLL, ZOOM }; + enum ZoomMode { ZOOM_FIT, ZOOM_FIT_WIDTH, ZOOM_FIT_HEIGHT, ZOOM_FREE }; + typedef QMap<ToolID,ToolBase*> Tools; + + ImageView(QWidget* parent,Document*,KActionCollection*); + ~ImageView(); + + // Properties + double zoom() const; + void setZoom(double zoom, int centerX=-1, int centerY=-1); + bool fullScreen() const; + void setFullScreen(bool); + + int brightness() const; + int contrast() const; + int gamma() const; + +public slots: + void setBrightness(int); + void setContrast(int); + void setGamma(int); + void updateFromSettings(); + +signals: + void selectPrevious(); + void selectNext(); + void doubleClicked(); + void requestContextMenu(const QPoint&); + + // Emitted whenever an hint should be displayed + void requestHintDisplay(const QString& hint); + + // Emitted whenever brightness, contrast or gamma changes + void bcgChanged(); + +protected: + virtual void contentsDragEnterEvent(QDragEnterEvent*); + virtual void contentsDropEvent(QDropEvent*); + virtual void keyPressEvent(QKeyEvent*); + +private: + struct Private; + Private* d; + + struct PendingPaint { + PendingPaint( bool s, const QRect& r ) : rect( r ), smooth( s ) {}; + PendingPaint() {}; // stupid Qt containers + QRect rect; + bool smooth; + }; + enum Operation { CHECK_OPERATIONS = 0, SMOOTH_PASS = 1 << 0 }; + + void addPendingPaint( bool smooth, QRect rect = QRect()); + void addPendingPaintInternal( bool smooth, QRect rect = QRect()); + void performPaint( QPainter* painter, int clipx, int clipy, int clipw, int cliph, bool smooth ); + void limitPaintSize( PendingPaint& paint ); + void fullRepaint(); + void cancelPending(); + void scheduleOperation( Operation operation ); + void checkPendingOperationsInternal(); + void updateBusyLevels(); + + void updateZoom(ZoomMode, double value=0, int centerX=-1, int centerY=-1); + double computeZoom(bool in) const; + double computeZoomToFit() const; + double computeZoomToWidth() const; + double computeZoomToHeight() const; + + void updateImageOffset(); + void updateScrollBarMode(); + void updateContentSize(); + void updateFullScreenLabel(); + void updateZoomActions(); + void selectTool(ButtonState, bool force); + void restartAutoHideTimer(); + + void emitRequestHintDisplay(); + + // Used by the scroll tool + void emitSelectPrevious() { emit selectPrevious(); } + void emitSelectNext() { emit selectNext(); } + + // Used by the zoom tool + QPoint offset() const; + bool canZoom(bool in) const; + KToggleAction* zoomToFit() const; + +private slots: + void slotLoaded(); + void slotModified(); + void slotZoomIn(); + void slotZoomOut(); + void slotResetZoom(); + void slotSelectZoom(); + void setZoomToFit(bool); + void setZoomToWidth(bool); + void setZoomToHeight(bool); + void setLockZoom(bool); + void increaseGamma(); + void decreaseGamma(); + void increaseBrightness(); + void decreaseBrightness(); + void increaseContrast(); + void decreaseContrast(); + void slotImageSizeUpdated(); + void slotImageRectUpdated(const QRect&); + void checkPendingOperations(); + void loadingStarted(); + void slotBusyLevelChanged(BusyLevel); + void showBCGDialog(); + +protected: + // Overloaded methods + bool eventFilter(QObject*, QEvent*); + void viewportMousePressEvent(QMouseEvent*); + void viewportMouseMoveEvent(QMouseEvent*); + void viewportMouseReleaseEvent(QMouseEvent*); + bool viewportKeyEvent(QKeyEvent*); // This one is not inherited, it's called from the eventFilter + void wheelEvent(QWheelEvent* event); + void resizeEvent(QResizeEvent* event); + void drawContents(QPainter* p,int clipx,int clipy,int clipw,int cliph); +}; + +} // namespace +#endif + diff --git a/src/gvcore/imageviewconfig.kcfg b/src/gvcore/imageviewconfig.kcfg new file mode 100644 index 0000000..f13dfad --- /dev/null +++ b/src/gvcore/imageviewconfig.kcfg @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE kcfg SYSTEM "http://www.kde.org/standards/kcfg/1.0/kcfg.dtd"> +<kcfg> + <include>qapplication.h</include> + <include>qpalette.h</include> + <kcfgfile name="gwenviewrc"/> + <group name="pixmap widget"> + <entry name="smoothAlgorithm" key="smooth scale" type="Enum"> + <choices> + <choice name="None"/> + <choice name="Fast"/> + <choice name="Normal"/> + <choice name="Best"/> + </choices> + <default>None</default> + </entry> + <entry name="delayedSmoothing" key="delayed smoothing" type="Bool"> + <default>false</default> + </entry> + <entry name="backgroundColor" key="background color" type="Color"> + <default code="true">QApplication::palette().active().dark()</default> + </entry> + <entry name="enlargeSmallImages" key="enlarge small images" type="Bool"> + <default>false</default> + </entry> + <entry name="showScrollBars" key="show scroll bars" type="Bool"> + <default>true</default> + </entry> + <entry name="mouseWheelScroll" key="mouse wheel scrolls image" type="Bool"> + <default>false</default> + </entry> + <entry name="zoomMode" type="Enum"> + <choices> + <choice name="FitWindow"/> + <choice name="FitWidth"/> + <choice name="FitHeight"/> + <choice name="Free"/> + </choices> + <default>FitWindow</default> + </entry> + <entry name="lockZoom" key="lockZoom" type="Bool"> + <default>false</default> + </entry> + <entry name="maxRepaintSize" key="max repaint size" type="Int"> + <default>10000</default> + </entry> + <entry name="maxScaleRepaintSize" key="max scale repaint size" type="Int"> + <default>10000</default> + </entry> + <entry name="maxSmoothRepaintSize" key="max smooth repaint size" type="Int"> + <default>10000</default> + </entry> + </group> +</kcfg> diff --git a/src/gvcore/imageviewconfig.kcfgc b/src/gvcore/imageviewconfig.kcfgc new file mode 100644 index 0000000..6336197 --- /dev/null +++ b/src/gvcore/imageviewconfig.kcfgc @@ -0,0 +1,7 @@ +File=imageviewconfig.kcfg +ClassName=ImageViewConfig +NameSpace=Gwenview +Singleton=true +Mutators=true +IncludeFiles=gvcore/libgwenview_export.h +Visibility=LIBGWENVIEW_EXPORT diff --git a/src/gvcore/imageviewcontroller.cpp b/src/gvcore/imageviewcontroller.cpp new file mode 100644 index 0000000..85a587a --- /dev/null +++ b/src/gvcore/imageviewcontroller.cpp @@ -0,0 +1,527 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2006 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#include <imageviewcontroller.moc> + +// Qt +#include <qcursor.h> +#include <qlayout.h> +#include <qpopupmenu.h> +#include <qvbox.h> +#include <qwidgetstack.h> + +// KDE +#include <kaction.h> +#include <kapplication.h> +#include <kdebug.h> +#include <klocale.h> +#include <kmediaplayer/player.h> +#include <kmimetype.h> +#include <ktoolbar.h> +#include <kuserprofile.h> +#include <kparts/componentfactory.h> +#include <kxmlguibuilder.h> +#include <kxmlguifactory.h> + +// Local +#include <document.h> +#include <externaltoolcontext.h> +#include <externaltoolmanager.h> +#include <fullscreenbar.h> +#include <imageview.h> + +namespace Gwenview { + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + + +/** + * A KXMLGUIBuilder which only creates containers for toolbars. + */ +class XMLGUIBuilder : public KXMLGUIBuilder { +public: + XMLGUIBuilder(QWidget* parent) : KXMLGUIBuilder(parent) {} + + virtual QWidget* createContainer(QWidget *parent, int index, const QDomElement &element, int &id) { + if (element.tagName().lower() == "toolbar") { + return KXMLGUIBuilder::createContainer(parent, index, element, id); + } else { + return 0; + } + } +}; + + +const int AUTO_HIDE_TIMEOUT=4000; + + +//------------------------------------------------------------------------ +// +// ImageViewController::Private +// +//------------------------------------------------------------------------ +struct ImageViewController::Private { + ImageViewController* mImageViewController; + + Document* mDocument; + KActionCollection* mActionCollection; + QWidget* mContainer; + KToolBar* mToolBar; + KXMLGUIFactory* mFactory; + XMLGUIBuilder* mBuilder; + QWidgetStack* mStack; + + ImageView* mImageView; + KActionPtrList mImageViewActions; + + // Hide cursor stuff + QTimer* mAutoHideTimer; + bool mCursorHidden; + + KParts::ReadOnlyPart* mPlayerPart; + + // Fullscreen stuff + bool mFullScreen; + FullScreenBar* mFullScreenBar; + KActionPtrList mFullScreenCommonActions; + + + void setXMLGUIClient(KXMLGUIClient* client) { + QPtrList<KXMLGUIClient> list=mFactory->clients(); + KXMLGUIClient* oldClient=list.getFirst(); + if (oldClient) { + mFactory->removeClient(oldClient); + // There should be at most one client, so the list should be empty + // now + Q_ASSERT(!mFactory->clients().getFirst()); + } + + // Unplug image view actions, if plugged + KActionPtrList::Iterator + it=mImageViewActions.begin(), + end=mImageViewActions.end(); + for (; it!=end; ++it) { + KAction* action=*it; + if (action->isPlugged(mToolBar)) { + action->unplug(mToolBar); + } + } + + if (client) { + mFactory->addClient(client); + } + } + + + void createPlayerPart(void) { + if (mPlayerPart) { + setXMLGUIClient(0); + delete mPlayerPart; + } + mPlayerPart=0; + + QString mimeType=KMimeType::findByURL(mDocument->url())->name(); + KService::Ptr service = KServiceTypeProfile::preferredService(mimeType, "KParts/ReadOnlyPart"); + if (!service) { + kdWarning() << "Couldn't find a KPart for " << mimeType << endl; + return; + } + + QString library=service->library(); + Q_ASSERT(!library.isNull()); + LOG("Library:" << library); + mPlayerPart = KParts::ComponentFactory::createPartInstanceFromService<KParts::ReadOnlyPart>(service, mStack, 0, mStack, 0); + if (!mPlayerPart) { + kdWarning() << "Failed to instantiate KPart from library " << library << endl; + return; + } + mStack->addWidget(mPlayerPart->widget()); + setXMLGUIClient(mPlayerPart); + } + + + void showPlayerPart(void) { + LOG(""); + createPlayerPart(); + if (!mPlayerPart) return; + mStack->raiseWidget(mPlayerPart->widget()); + mPlayerPart->openURL(mDocument->url()); + + // If the part implements the KMediaPlayer::Player interface, start + // playing (needed for Kaboodle) + KMediaPlayer::Player* player=dynamic_cast<KMediaPlayer::Player *>(mPlayerPart); + if (player) { + player->play(); + } + } + + + void showImageView(void) { + LOG(""); + if (mStack->visibleWidget()==mImageView) { + KAction* action=mImageViewActions.first(); + if (action && !action->isPlugged(mToolBar)) { + // In the ctor, we set the imageview as the visible widget but + // we did not fill the toolbar because mImageViewActions was + // empty. In this case, fill the toolbar now. + plugImageViewActions(); + } + return; + } + + if (mPlayerPart) { + setXMLGUIClient(0); + delete mPlayerPart; + mPlayerPart=0; + } + plugImageViewActions(); + mStack->raiseWidget(mImageView); + } + + void plugImageViewActions() { + KActionPtrList::Iterator + it=mImageViewActions.begin(), + end=mImageViewActions.end(); + for (; it!=end; ++it) { + KAction* action=*it; + action->plug(mToolBar); + } + } + + + void restartAutoHideTimer() { + mAutoHideTimer->start(AUTO_HIDE_TIMEOUT,true); + } + + + void updateFullScreenBarPosition() { + int mouseY=mStack->mapFromGlobal(QCursor::pos()).y(); + bool visible = mFullScreenBar->y()==0; + + if (visible && mouseY>mFullScreenBar->height()) { + mFullScreenBar->slideOut(); + } + + if (!visible && mouseY<2) { + mFullScreenBar->slideIn(); + } + } + + + /** + * This function creates the fullscreen toolbar. + * NOTE: It should not be called from/merged with setFullScreenActions, + * otherwise the toolbar will have a one pixel border which will prevent + * reaching easily buttons by pushing the mouse to the top edge of the + * screen. + * My guess is that instanciating the toolbar *before* the main + * window is shown causes the main window to tweak its bars. This happens + * with KDE 3.5.1. + */ + void initFullScreenBar() { + Q_ASSERT(!mFullScreenBar); + mFullScreenBar=new FullScreenBar(mContainer); + + KActionPtrList::ConstIterator + it=mFullScreenCommonActions.begin(), + end=mFullScreenCommonActions.end(); + + for (; it!=end; ++it) { + (*it)->plug(mFullScreenBar); + } + } +}; + + +//------------------------------------------------------------------------ +// +// ImageViewController +// +//------------------------------------------------------------------------ + + +ImageViewController::ImageViewController(QWidget* parent, Document* document, KActionCollection* actionCollection) +: QObject(parent) { + d=new Private; + d->mImageViewController=this; + d->mDocument=document; + d->mActionCollection=actionCollection; + d->mAutoHideTimer=new QTimer(this); + d->mCursorHidden=false; + + d->mContainer=new QWidget(parent); + d->mContainer->setMinimumWidth(1); // Make sure we can resize the toolbar smaller than its minimum size + QVBoxLayout* layout=new QVBoxLayout(d->mContainer); + d->mToolBar=new KToolBar(d->mContainer, "", true); + + layout->add(d->mToolBar); + d->mStack=new QWidgetStack(d->mContainer); + layout->add(d->mStack); + + d->mImageView=new ImageView(d->mStack, document, actionCollection); + d->mStack->addWidget(d->mImageView); + + KApplication::kApplication()->installEventFilter(this); + + d->mPlayerPart=0; + d->mBuilder=new XMLGUIBuilder(d->mToolBar); + d->mFactory=new KXMLGUIFactory(d->mBuilder, this); + + d->mFullScreen=false; + d->mFullScreenBar=0; + + connect(d->mDocument,SIGNAL(loaded(const KURL&)), + this,SLOT(slotLoaded()) ); + + connect(d->mImageView, SIGNAL(requestContextMenu(const QPoint&)), + this, SLOT(openImageViewContextMenu(const QPoint&)) ); + + connect(d->mImageView, SIGNAL(requestHintDisplay(const QString&)), + this, SIGNAL(requestHintDisplay(const QString&)) ); + + connect(d->mAutoHideTimer,SIGNAL(timeout()), + this,SLOT(slotAutoHide()) ); + + // Forward Image view signals + connect(d->mImageView, SIGNAL(selectPrevious()), SIGNAL(selectPrevious()) ); + connect(d->mImageView, SIGNAL(selectNext()), SIGNAL(selectNext()) ); + connect(d->mImageView, SIGNAL(doubleClicked()), SIGNAL(doubleClicked()) ); +} + + +ImageViewController::~ImageViewController() { + delete d->mBuilder; + delete d; +} + + +void ImageViewController::setFocus() { + QWidget* view; + if (d->mPlayerPart) { + view = d->mPlayerPart->widget(); + } else { + view = d->mImageView; + } + view->setFocus(); +} + + +void ImageViewController::slotLoaded() { + LOG(""); + if (d->mDocument->urlKind()==MimeTypeUtils::KIND_FILE) { + d->showPlayerPart(); + } else { + d->showImageView(); + } +} + + +void ImageViewController::setFullScreen(bool fullScreen) { + d->mFullScreen=fullScreen; + d->mImageView->setFullScreen(fullScreen); + + if (d->mFullScreen) { + d->restartAutoHideTimer(); + if (!d->mFullScreenBar) { + d->initFullScreenBar(); + } + } else { + d->mAutoHideTimer->stop(); + QApplication::restoreOverrideCursor(); + d->mCursorHidden=false; + } + + d->mToolBar->setHidden(d->mFullScreen); + if (d->mFullScreenBar) { + d->mFullScreenBar->setHidden(!d->mFullScreen); + } +} + + +void ImageViewController::setNormalCommonActions(const KActionPtrList& actions) { + KActionPtrList::ConstIterator + it=actions.begin(), + end=actions.end(); + + for (; it!=end; ++it) { + (*it)->plug(d->mToolBar); + } + d->mToolBar->insertLineSeparator(); +} + + +void ImageViewController::setFullScreenCommonActions(const KActionPtrList& actions) { + d->mFullScreenCommonActions=actions; +} + + +void ImageViewController::setImageViewActions(const KActionPtrList& actions) { + d->mImageViewActions=actions; +} + + +void ImageViewController::slotAutoHide() { + if (d->mFullScreenBar) { + // Do not auto hide if the cursor is over the bar + QPoint pos=d->mFullScreenBar->mapFromGlobal(QCursor::pos()); + if (d->mFullScreenBar->rect().contains(pos)) { + d->restartAutoHideTimer(); + return; + } + } + + // Only hide cursor if we are not over a dialog + QWidget* widget = KApplication::kApplication()->activeWindow(); + if (!widget || !widget->inherits("QDialog")) { + QApplication::setOverrideCursor(blankCursor); + d->mCursorHidden=true; + } +} + + +QWidget* ImageViewController::widget() const { + return d->mContainer; +} + + +void ImageViewController::updateFromSettings() { + d->mImageView->updateFromSettings(); +} + + +/** + * This event filter monitors mouse moves and make sure the position of the + * fullscreen bar is updated. + */ +bool ImageViewController::eventFilter(QObject* object, QEvent* event) { + if (!d->mFullScreen) return false; + if (!event->type()==QEvent::MouseMove) return false; + + // Check we must filter this object. This is an application filter, so we + // have to check we are not dealing with another object. + bool isAChildOfStack=false; + QObject* parentObject; + for (parentObject=object->parent(); parentObject; parentObject=parentObject->parent()) { + if (parentObject==d->mStack) { + isAChildOfStack=true; + break; + } + } + if (!isAChildOfStack) return false; + + d->updateFullScreenBarPosition(); + + if (event->type()==QEvent::MouseMove) { + d->mCursorHidden=false; + d->restartAutoHideTimer(); + } + + if (d->mCursorHidden) { + QApplication::setOverrideCursor(blankCursor,true); + } else { + QApplication::restoreOverrideCursor(); + } + + return false; +} + + +/** + * Little helper to plug an action if it exists + */ +inline void plugAction(QPopupMenu* menu, KActionCollection* actionCollection, const char* actionName) { + KAction* action=actionCollection->action(actionName); + if (action) action->plug(menu); +} + + +void ImageViewController::openImageViewContextMenu(const QPoint& pos) { + QPopupMenu menu(d->mImageView); + bool noImage=d->mDocument->filename().isEmpty(); + bool validImage=!d->mDocument->isNull(); + + // The fullscreen item is always there, to be able to leave fullscreen mode + // if necessary. But KParts may not have the action itself. + plugAction(&menu, d->mActionCollection, "fullscreen"); + + plugAction(&menu, d->mActionCollection, "slideshow"); + + if (validImage) { + menu.insertSeparator(); + + plugAction(&menu, d->mActionCollection, "view_zoom_to_fit"); + plugAction(&menu, d->mActionCollection, "view_zoom_in"); + plugAction(&menu, d->mActionCollection, "view_zoom_out"); + plugAction(&menu, d->mActionCollection, "view_actual_size"); + plugAction(&menu, d->mActionCollection, "view_zoom_lock"); + } + + menu.insertSeparator(); + + plugAction(&menu, d->mActionCollection, "first"); + plugAction(&menu, d->mActionCollection, "previous"); + plugAction(&menu, d->mActionCollection, "next"); + plugAction(&menu, d->mActionCollection, "last"); + + if (validImage) { + menu.insertSeparator(); + + QPopupMenu* editMenu=new QPopupMenu(&menu); + plugAction(editMenu, d->mActionCollection, "rotate_left"); + plugAction(editMenu, d->mActionCollection, "rotate_right"); + plugAction(editMenu, d->mActionCollection, "mirror"); + plugAction(editMenu, d->mActionCollection, "flip"); + plugAction(editMenu, d->mActionCollection, "adjust_bcg"); + menu.insertItem( i18n("Edit"), editMenu ); + + ExternalToolContext* externalToolContext= + ExternalToolManager::instance()->createContext( + this, d->mDocument->url()); + + menu.insertItem( + i18n("External Tools"), externalToolContext->popupMenu()); + } + + if (!noImage) { + menu.insertSeparator(); + + plugAction(&menu, d->mActionCollection, "file_rename"); + plugAction(&menu, d->mActionCollection, "file_copy"); + plugAction(&menu, d->mActionCollection, "file_move"); + plugAction(&menu, d->mActionCollection, "file_link"); + plugAction(&menu, d->mActionCollection, "file_delete"); + + menu.insertSeparator(); + + plugAction(&menu, d->mActionCollection, "file_properties"); + } + + menu.exec(pos); +} + + +} // namespace diff --git a/src/gvcore/imageviewcontroller.h b/src/gvcore/imageviewcontroller.h new file mode 100644 index 0000000..7ccb3be --- /dev/null +++ b/src/gvcore/imageviewcontroller.h @@ -0,0 +1,84 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2006 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef IMAGEVIEWCONTROLLER_H +#define IMAGEVIEWCONTROLLER_H + +// Qt +#include <qobject.h> + +// KDE +#include <kaction.h> + +// Local +#include "libgwenview_export.h" + +class QPoint; +class QWidget; + +class KToolBar; + +namespace Gwenview { + + +class Document; +class ImageView; + + +class LIBGWENVIEW_EXPORT ImageViewController : public QObject { +Q_OBJECT +public: + ImageViewController(QWidget* parent, Document*, KActionCollection*); + ~ImageViewController(); + + QWidget* widget() const; + + void setImageViewActions(const KActionPtrList&); + + void setFullScreen(bool); + void setNormalCommonActions(const KActionPtrList&); + void setFullScreenCommonActions(const KActionPtrList&); + void setFocus(); + +protected: + virtual bool eventFilter(QObject*, QEvent*); + +public slots: + void updateFromSettings(); + +signals: + void requestHintDisplay(const QString&); + void selectPrevious(); + void selectNext(); + void doubleClicked(); + +private slots: + void slotLoaded(); + void openImageViewContextMenu(const QPoint&); + void slotAutoHide(); + +private: + struct Private; + Private* d; +}; + +} // namespace + +#endif /* IMAGEVIEWCONTROLLER_H */ diff --git a/src/gvcore/imageviewtools.cpp b/src/gvcore/imageviewtools.cpp new file mode 100644 index 0000000..06a483b --- /dev/null +++ b/src/gvcore/imageviewtools.cpp @@ -0,0 +1,213 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Our header +#include "imageviewtools.h" + +// KDE +#include <kaction.h> +#include <kdebug.h> +#include <klocale.h> +#include <kstandarddirs.h> + +// Local +#include "imageviewconfig.h" + +namespace Gwenview { + + +// Helper function +static QCursor loadCursor(const QString& name) { + QString path; + path=locate("data", QString("gwenview/cursors/%1.png").arg(name)); + return QCursor(QPixmap(path)); +} + + +//---------------------------------------------------------------------------- +// +// ImageView::ToolBase +// +//---------------------------------------------------------------------------- +ImageView::ToolBase::ToolBase(ImageView* view) +: mView(view) {} + + +ImageView::ToolBase::~ToolBase() {} + +void ImageView::ToolBase::mouseMoveEvent(QMouseEvent*) {} +void ImageView::ToolBase::leftButtonPressEvent(QMouseEvent*) {} +void ImageView::ToolBase::leftButtonReleaseEvent(QMouseEvent*) {} + +void ImageView::ToolBase::midButtonReleaseEvent(QMouseEvent*) { + mView->zoomToFit()->activate(); +} + +void ImageView::ToolBase::rightButtonPressEvent(QMouseEvent* event) { + emit mView->requestContextMenu(event->globalPos()); +} + +void ImageView::ToolBase::rightButtonReleaseEvent(QMouseEvent*) { +} + +void ImageView::ToolBase::wheelEvent(QWheelEvent* event) { + event->accept(); +} + +void ImageView::ToolBase::updateCursor() { + mView->viewport()->setCursor(ArrowCursor); +} + + +//---------------------------------------------------------------------------- +// +// ImageView::ZoomTool +// +//---------------------------------------------------------------------------- +ImageView::ZoomTool::ZoomTool(ImageView* view) +: ImageView::ToolBase(view) { + mZoomCursor=loadCursor("zoom"); +} + + +void ImageView::ZoomTool::zoomTo(const QPoint& pos, bool in) { + if (!mView->canZoom(in)) return; + + QPoint centerPos=QPoint(mView->visibleWidth(), mView->visibleHeight())/2; + // Compute image position + QPoint imgPos=mView->viewportToContents(pos) - mView->offset(); + double newZoom=mView->computeZoom(in); + + imgPos*=newZoom/mView->zoom(); + imgPos=imgPos-pos+centerPos; + mView->setZoom(newZoom, imgPos.x(), imgPos.y()); +} + + +void ImageView::ZoomTool::leftButtonReleaseEvent(QMouseEvent* event) { + zoomTo(event->pos(), true); +} + + +void ImageView::ZoomTool::wheelEvent(QWheelEvent* event) { + zoomTo(event->pos(), event->delta()>0); + event->accept(); +} + + +void ImageView::ZoomTool::rightButtonPressEvent(QMouseEvent*) { +} + + +void ImageView::ZoomTool::rightButtonReleaseEvent(QMouseEvent* event) { + zoomTo(event->pos(), false); +} + + +void ImageView::ZoomTool::updateCursor() { + mView->viewport()->setCursor(mZoomCursor); +} + + +QString ImageView::ZoomTool::hint() const { + return i18n("Left click to zoom in, right click to zoom out. You can also use the mouse wheel."); +} + + +//---------------------------------------------------------------------------- +// +// ImageView::ScrollTool +// +//---------------------------------------------------------------------------- +ImageView::ScrollTool::ScrollTool(ImageView* view) +: ImageView::ToolBase(view) +, mScrollStartX(0), mScrollStartY(0) +, mDragStarted(false) { +} + + +void ImageView::ScrollTool::leftButtonPressEvent(QMouseEvent* event) { + mScrollStartX=event->x(); + mScrollStartY=event->y(); + mView->viewport()->setCursor(SizeAllCursor); + mDragStarted=true; +} + + +void ImageView::ScrollTool::mouseMoveEvent(QMouseEvent* event) { + if (!mDragStarted) return; + + int deltaX,deltaY; + + deltaX=mScrollStartX - event->x(); + deltaY=mScrollStartY - event->y(); + + mScrollStartX=event->x(); + mScrollStartY=event->y(); + mView->scrollBy(deltaX,deltaY); +} + + +void ImageView::ScrollTool::leftButtonReleaseEvent(QMouseEvent*) { + if (!mDragStarted) return; + + mDragStarted=false; + mView->viewport()->setCursor(ArrowCursor); +} + + +void ImageView::ScrollTool::wheelEvent(QWheelEvent* event) { + if (ImageViewConfig::mouseWheelScroll()) { + int deltaX, deltaY; + + if (event->state() & AltButton || event->orientation()==Horizontal) { + deltaX = event->delta(); + deltaY = 0; + } else { + deltaX = 0; + deltaY = event->delta(); + } + mView->scrollBy(-deltaX, -deltaY); + } else { + if (event->delta()<0) { + mView->emitSelectNext(); + } else { + mView->emitSelectPrevious(); + } + } + event->accept(); +} + + +void ImageView::ScrollTool::updateCursor() { + if (mDragStarted) { + mView->viewport()->setCursor(SizeAllCursor); + } else { + mView->viewport()->setCursor(ArrowCursor); + } +} + + +QString ImageView::ScrollTool::hint() const { + return i18n("Drag to move the image, middle-click to toggle auto-zoom. Hold the Control key to switch to the zoom tool."); +} + + +} // namespace diff --git a/src/gvcore/imageviewtools.h b/src/gvcore/imageviewtools.h new file mode 100644 index 0000000..59ce3e2 --- /dev/null +++ b/src/gvcore/imageviewtools.h @@ -0,0 +1,96 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2004 Aur�ien G�eau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef IMAGEVIEWTOOLS_H +#define IMAGEVIEWTOOLS_H + +// Qt +#include <qcursor.h> + +// Local +#include "imageview.h" +namespace Gwenview { + + +class ImageView::ToolBase { +protected: + ImageView* mView; + +public: + ToolBase(ImageView* view); + virtual ~ToolBase(); + virtual void mouseMoveEvent(QMouseEvent*); + + virtual void leftButtonPressEvent(QMouseEvent*); + virtual void leftButtonReleaseEvent(QMouseEvent*); + + virtual void midButtonReleaseEvent(QMouseEvent*); + + virtual void rightButtonPressEvent(QMouseEvent* event); + virtual void rightButtonReleaseEvent(QMouseEvent*); + + virtual void wheelEvent(QWheelEvent* event); + + virtual void updateCursor(); + + /** + * Return a hint about the use of the tool + */ + virtual QString hint() const=0; +}; + + +class ImageView::ZoomTool : public ImageView::ToolBase { +private: + QCursor mZoomCursor; + void zoomTo(const QPoint& pos, bool in); + +public: + ZoomTool(ImageView* view); + void leftButtonReleaseEvent(QMouseEvent* event); + + void wheelEvent(QWheelEvent* event); + void rightButtonPressEvent(QMouseEvent*); + void rightButtonReleaseEvent(QMouseEvent* event); + + void updateCursor(); + virtual QString hint() const; +}; + + +class ImageView::ScrollTool : public ImageView::ToolBase { + int mScrollStartX,mScrollStartY; + bool mDragStarted; + +public: + ScrollTool(ImageView* view); + void leftButtonPressEvent(QMouseEvent* event); + void mouseMoveEvent(QMouseEvent* event); + void leftButtonReleaseEvent(QMouseEvent*); + void wheelEvent(QWheelEvent* event); + + void updateCursor(); + virtual QString hint() const; +}; + + +} // namespace +#endif /* IMAGEVIEWTOOLS_H */ + diff --git a/src/gvcore/inputdialog.cpp b/src/gvcore/inputdialog.cpp new file mode 100644 index 0000000..3da70e9 --- /dev/null +++ b/src/gvcore/inputdialog.cpp @@ -0,0 +1,78 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2006 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Self +#include "inputdialog.moc" + +// Qt +#include <qlabel.h> +#include <qvbox.h> + +// KDE +#include <klineedit.h> + +namespace Gwenview { + +struct InputDialog::Private { + KLineEdit* mLineEdit; + QLabel* mLabel; +}; + + +InputDialog::InputDialog(QWidget* parent) +: KDialogBase(parent, "InputDialog", true, QString::null, + KDialogBase::Ok|KDialogBase::Cancel) +{ + d = new Private; + QVBox* page = makeVBoxMainWidget(); + d->mLabel = new QLabel(page); + + d->mLineEdit = new KLineEdit(page); + d->mLineEdit->setFocus(); + + setMinimumWidth(350); + + connect(d->mLineEdit, SIGNAL(textChanged(const QString&)), + this, SLOT(updateButtons()) ); +} + + +InputDialog::~InputDialog() { + delete d; +} + + +void InputDialog::setLabel(const QString& label) { + d->mLabel->setText(label); +} + + +KLineEdit* InputDialog::lineEdit() const { + return d->mLineEdit; +} + + +void InputDialog::updateButtons() { + enableButtonOK( ! d->mLineEdit->text().isEmpty() ); +} + + +} // namespace + diff --git a/src/gvcore/inputdialog.h b/src/gvcore/inputdialog.h new file mode 100644 index 0000000..a849eb5 --- /dev/null +++ b/src/gvcore/inputdialog.h @@ -0,0 +1,55 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2006 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef INPUTDIALOG_H +#define INPUTDIALOG_H + +// KDE +#include <kdialogbase.h> + + +class KLineEdit; + + +namespace Gwenview { + + +/** + * An input dialog which give access to its line edit + */ +class InputDialog : public KDialogBase { +Q_OBJECT +public: + InputDialog(QWidget* parent); + ~InputDialog(); + void setLabel(const QString& label); + KLineEdit* lineEdit() const; + +private slots: + void updateButtons(); + +private: + struct Private; + Private* d; +}; + +} // namespace + +#endif /* INPUTDIALOG_H */ diff --git a/src/gvcore/jpegformattype.cpp b/src/gvcore/jpegformattype.cpp new file mode 100644 index 0000000..ccb020e --- /dev/null +++ b/src/gvcore/jpegformattype.cpp @@ -0,0 +1,527 @@ +/* This file is based on kdelibs-3.2.0/khtml/misc/loader_jpeg.cpp. Original + * copyright follows. + */ +/* + This file is part of the KDE libraries + + Copyright (C) 2000 Dirk Mueller (mueller@kde.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + + +// System +#include <stdio.h> +#include <setjmp.h> +extern "C" { +#define XMD_H +#include <jpeglib.h> +#undef const +} + +// Qt +#include <qdatetime.h> + +// KDE +#include <kdebug.h> +#include <kglobal.h> + +// Local +#include "jpegformattype.h" +#include "imageutils/jpegerrormanager.h" + +namespace Gwenview { + +#undef BUFFER_DEBUG +//#define BUFFER_DEBUG + +#undef JPEG_DEBUG +//#define JPEG_DEBUG + + +static const int MAX_BUFFER = 32768; +// how long it will consume data before starting outputing progressive scan +static const int MAX_CONSUMING_TIME = 100; + +//----------------------------------------------------------------------------- +// +// JPEGSourceManager +// (Does not follow HACKING file recommandation to be consistent with +// jpeg_source_mgr naming) +// +//----------------------------------------------------------------------------- +struct JPEGSourceManager : public jpeg_source_mgr { + JOCTET jpeg_buffer[MAX_BUFFER]; + + int valid_buffer_length; + size_t skip_input_bytes; + bool at_eof; + QRect change_rect; + QRect old_change_rect; + QTime decoder_timestamp; + bool final_pass; + bool decoding_done; + bool do_progressive; + + JPEGSourceManager() { + // jpeg_source_mgr fields + init_source = gvJPEGDummyDecompress; + fill_input_buffer = gvFillInputBuffer; + skip_input_data = gvSkipInputData; + resync_to_restart = jpeg_resync_to_restart; + term_source = gvJPEGDummyDecompress; + + bytes_in_buffer = 0; + next_input_byte = jpeg_buffer; + + // JPEGSourceManager fields + valid_buffer_length = 0; + skip_input_bytes = 0; + at_eof = 0; + final_pass = false; + decoding_done = false; + } + + static void gvJPEGDummyDecompress(j_decompress_ptr) {} + + /* Do not replace boolean with bool, it's the libjpeg boolean type */ + static boolean gvFillInputBuffer(j_decompress_ptr cinfo) { +#ifdef BUFFER_DEBUG + qDebug("FillInputBuffer called!"); +#endif + + JPEGSourceManager* src = (JPEGSourceManager*)cinfo->src; + + if ( src->at_eof ) { + /* Insert a fake EOI marker - as per jpeglib recommendation */ + src->jpeg_buffer[0] = (JOCTET) 0xFF; + src->jpeg_buffer[1] = (JOCTET) JPEG_EOI; + src->bytes_in_buffer = 2; + src->next_input_byte = (JOCTET *) src->jpeg_buffer; +#ifdef BUFFER_DEBUG + qDebug("...returning true!"); +#endif + return true; + } else { + return false; /* I/O suspension mode */ + } + } + + static void gvSkipInputData(j_decompress_ptr cinfo, long num_bytes) { + if(num_bytes <= 0) return; /* required noop */ + +#ifdef BUFFER_DEBUG + qDebug("SkipInputData (%d) called!", num_bytes); +#endif + + JPEGSourceManager* src = (JPEGSourceManager*)cinfo->src; + src->skip_input_bytes += num_bytes; + + unsigned int skipbytes = kMin(src->bytes_in_buffer, src->skip_input_bytes); + +#ifdef BUFFER_DEBUG + qDebug("skip_input_bytes is now %d", src->skip_input_bytes); + qDebug("skipbytes is now %d", skipbytes); + qDebug("valid_buffer_length is before %d", src->valid_buffer_length); + qDebug("bytes_in_buffer is %d", src->bytes_in_buffer); +#endif + + if(skipbytes < src->bytes_in_buffer) { + memmove(src->jpeg_buffer, + src->next_input_byte+skipbytes, + src->bytes_in_buffer - skipbytes); + } + + src->bytes_in_buffer -= skipbytes; + src->valid_buffer_length = src->bytes_in_buffer; + src->skip_input_bytes -= skipbytes; + + /* adjust data for jpeglib */ + cinfo->src->next_input_byte = (JOCTET *) src->jpeg_buffer; + cinfo->src->bytes_in_buffer = (size_t) src->valid_buffer_length; +#ifdef BUFFER_DEBUG + qDebug("valid_buffer_length is afterwards %d", src->valid_buffer_length); + qDebug("skip_input_bytes is now %d", src->skip_input_bytes); +#endif + + } +}; + + +//----------------------------------------------------------------------------- +// +// JPEGFormat +// +//----------------------------------------------------------------------------- +class JPEGFormat : public QImageFormat { +public: + JPEGFormat(); + + virtual ~JPEGFormat(); + + virtual int decode(QImage& img, QImageConsumer* consumer, + const uchar* buffer, int length); +private: + + enum { + INIT, + START_DECOMPRESS, + DECOMPRESS_STARTED, + CONSUME_INPUT, + PREPARE_OUTPUT_SCAN, + DO_OUTPUT_SCAN, + READ_DONE, + INVALID + } mState; + + // structs for the jpeglib + jpeg_decompress_struct mDecompress; + ImageUtils::JPEGErrorManager mErrorManager; + JPEGSourceManager mSourceManager; +}; + + +JPEGFormat::JPEGFormat() { + memset(&mDecompress, 0, sizeof(mDecompress)); + mDecompress.err = &mErrorManager; + jpeg_create_decompress(&mDecompress); + mDecompress.src = &mSourceManager; + mState = INIT; +} + + +JPEGFormat::~JPEGFormat() { + (void) jpeg_destroy_decompress(&mDecompress); +} + +/* + * return > 0 means "consumed x bytes, need more" + * return == 0 means "end of frame reached" + * return < 0 means "fatal error in image decoding, don't call me ever again" + */ + +int JPEGFormat::decode(QImage& image, QImageConsumer* consumer, const uchar* buffer, int length) { +#ifdef JPEG_DEBUG + qDebug("JPEGFormat::decode(%p, %p, %p, %d)", + &image, consumer, buffer, length); +#endif + + if(mSourceManager.at_eof) { +#ifdef JPEG_DEBUG + qDebug("at_eof, eating"); +#endif + return length; + } + + if(setjmp(mErrorManager.jmp_buffer)) { +#ifdef JPEG_DEBUG + qDebug("jump into state INVALID"); +#endif + if(consumer) consumer->end(); + + // this is fatal + return -1; + } + + int consumed = kMin(length, MAX_BUFFER - mSourceManager.valid_buffer_length); + +#ifdef BUFFER_DEBUG + qDebug("consuming %d bytes", consumed); +#endif + + // filling buffer with the new data + memcpy(mSourceManager.jpeg_buffer + mSourceManager.valid_buffer_length, buffer, consumed); + mSourceManager.valid_buffer_length += consumed; + + if(mSourceManager.skip_input_bytes) { +#ifdef BUFFER_DEBUG + qDebug("doing skipping"); + qDebug("valid_buffer_length %d", mSourceManager.valid_buffer_length); + qDebug("skip_input_bytes %d", mSourceManager.skip_input_bytes); +#endif + int skipbytes = kMin((size_t) mSourceManager.valid_buffer_length, mSourceManager.skip_input_bytes); + + if(skipbytes < mSourceManager.valid_buffer_length) { + memmove(mSourceManager.jpeg_buffer, + mSourceManager.jpeg_buffer+skipbytes, + mSourceManager.valid_buffer_length - skipbytes); + } + + mSourceManager.valid_buffer_length -= skipbytes; + mSourceManager.skip_input_bytes -= skipbytes; + + // still more bytes to skip + if(mSourceManager.skip_input_bytes) { + if(consumed <= 0) qDebug("ERROR!!!"); + return consumed; + } + + } + + mDecompress.src->next_input_byte = (JOCTET *) mSourceManager.jpeg_buffer; + mDecompress.src->bytes_in_buffer = (size_t) mSourceManager.valid_buffer_length; + +#ifdef BUFFER_DEBUG + qDebug("buffer contains %d bytes", mSourceManager.valid_buffer_length); +#endif + + if(mState == INIT) { + if(jpeg_read_header(&mDecompress, true) != JPEG_SUSPENDED) { + if (consumer) { + consumer->setSize( + mDecompress.image_width/mDecompress.scale_denom, + mDecompress.image_height/mDecompress.scale_denom); + } + + mState = START_DECOMPRESS; + } + } + + if(mState == START_DECOMPRESS) { + mSourceManager.do_progressive = jpeg_has_multiple_scans( &mDecompress ); + +#ifdef JPEG_DEBUG + qDebug( "**** DOPROGRESSIVE: %d", mSourceManager.do_progressive ); +#endif + mDecompress.buffered_image = mSourceManager.do_progressive; + + // setup image sizes + jpeg_calc_output_dimensions( &mDecompress ); + + if (mDecompress.jpeg_color_space == JCS_YCbCr) { + mDecompress.out_color_space = JCS_RGB; + } + + mDecompress.do_fancy_upsampling = true; + mDecompress.do_block_smoothing = false; + mDecompress.quantize_colors = false; + + // false: IO suspension + if(jpeg_start_decompress(&mDecompress)) { + if ( mDecompress.output_components == 3 || mDecompress.output_components == 4) { + image.create( mDecompress.output_width, mDecompress.output_height, 32 ); + } else if ( mDecompress.output_components == 1 ) { + image.create( mDecompress.output_width, mDecompress.output_height, 8, 256 ); + for (int i=0; i<256; i++) { + image.setColor(i, qRgb(i,i,i)); + } + } + +#ifdef JPEG_DEBUG + qDebug("will create a picture %d/%d in size", mDecompress.output_width, mDecompress.output_height); +#endif + +#ifdef JPEG_DEBUG + qDebug("ok, going to DECOMPRESS_STARTED"); +#endif + + mSourceManager.decoder_timestamp.start(); + mState = mSourceManager.do_progressive ? DECOMPRESS_STARTED : DO_OUTPUT_SCAN; + } + } + +again: + + if(mState == DECOMPRESS_STARTED) { + mState = (!mSourceManager.final_pass && mSourceManager.decoder_timestamp.elapsed() < MAX_CONSUMING_TIME) + ? CONSUME_INPUT : PREPARE_OUTPUT_SCAN; + } + + if(mState == CONSUME_INPUT) { + int retval; + + do { + retval = jpeg_consume_input(&mDecompress); + } while (retval != JPEG_SUSPENDED && retval != JPEG_REACHED_EOI + && (retval != JPEG_REACHED_SOS || mSourceManager.decoder_timestamp.elapsed() < MAX_CONSUMING_TIME)); + + if( mSourceManager.final_pass + || mSourceManager.decoder_timestamp.elapsed() >= MAX_CONSUMING_TIME + || retval == JPEG_REACHED_EOI + || retval == JPEG_REACHED_SOS) { + mState = PREPARE_OUTPUT_SCAN; + } + } + + if(mState == PREPARE_OUTPUT_SCAN) { + if ( jpeg_start_output(&mDecompress, mDecompress.input_scan_number) ) { + mState = DO_OUTPUT_SCAN; + } + } + + if(mState == DO_OUTPUT_SCAN) { + if(image.isNull() || mSourceManager.decoding_done) { +#ifdef JPEG_DEBUG + qDebug("complete in doOutputscan, eating.."); +#endif + return consumed; + } + uchar** lines = image.jumpTable(); + int oldoutput_scanline = mDecompress.output_scanline; + + while(mDecompress.output_scanline < mDecompress.output_height && + jpeg_read_scanlines(&mDecompress, lines+mDecompress.output_scanline, mDecompress.output_height)) + ; // here happens all the magic of decoding + + int completed_scanlines = mDecompress.output_scanline - oldoutput_scanline; +#ifdef JPEG_DEBUG + qDebug("completed now %d scanlines", completed_scanlines); +#endif + + if ( mDecompress.output_components == 3 ) { + // Expand 24->32 bpp. + for (int j=oldoutput_scanline; j<oldoutput_scanline+completed_scanlines; j++) { + uchar *in = image.scanLine(j) + mDecompress.output_width * 3; + QRgb *out = (QRgb*)image.scanLine(j); + + for (uint i=mDecompress.output_width; i--; ) { + in-=3; + out[i] = qRgb(in[0], in[1], in[2]); + } + } + } + + if(consumer && completed_scanlines) { + QRect r(0, oldoutput_scanline, mDecompress.output_width, completed_scanlines); +#ifdef JPEG_DEBUG + qDebug("changing %d/%d %d/%d", r.x(), r.y(), r.width(), r.height()); +#endif + mSourceManager.change_rect |= r; + + if ( mSourceManager.decoder_timestamp.elapsed() >= MAX_CONSUMING_TIME ) { + if( !mSourceManager.old_change_rect.isEmpty()) { + consumer->changed(mSourceManager.old_change_rect); + mSourceManager.old_change_rect = QRect(); + } + consumer->changed(mSourceManager.change_rect); + mSourceManager.change_rect = QRect(); + mSourceManager.decoder_timestamp.restart(); + } + } + + if(mDecompress.output_scanline >= mDecompress.output_height) { + if ( mSourceManager.do_progressive ) { + jpeg_finish_output(&mDecompress); + mSourceManager.final_pass = jpeg_input_complete(&mDecompress); + mSourceManager.decoding_done = mSourceManager.final_pass && mDecompress.input_scan_number == mDecompress.output_scan_number; + if ( !mSourceManager.decoding_done ) { + mSourceManager.old_change_rect |= mSourceManager.change_rect; + mSourceManager.change_rect = QRect(); + } + } else { + mSourceManager.decoding_done = true; + } +#ifdef JPEG_DEBUG + qDebug("one pass is completed, final_pass = %d, dec_done: %d, complete: %d", + mSourceManager.final_pass, mSourceManager.decoding_done, jpeg_input_complete(&mDecompress)); +#endif + if(!mSourceManager.decoding_done) { +#ifdef JPEG_DEBUG + qDebug("starting another one, input_scan_number is %d/%d", mDecompress.input_scan_number, + mDecompress.output_scan_number); +#endif + mSourceManager.decoder_timestamp.restart(); + mState = DECOMPRESS_STARTED; + // don't return until necessary! + goto again; + } + } + + if(mState == DO_OUTPUT_SCAN && mSourceManager.decoding_done) { +#ifdef JPEG_DEBUG + qDebug("input is complete, cleaning up, returning.."); +#endif + if ( consumer && !mSourceManager.change_rect.isEmpty() ) { + consumer->changed( mSourceManager.change_rect ); + } + + if(consumer) consumer->end(); + + // get the density X and Y info and the related units to have + // the aspect ratio of the image + // field: units -- one byte: Units for the X and Y densities + // 0 => no units, X and Y specify the pixel aspect ratio + // 1 => X and Y are dots per inch + // 2 => X and Y are dots per cm + // Xdensity -- two bytes + // Ydensity -- two bytes + const float INCHESPERMETER = (100. / 2.54); + switch (mDecompress.density_unit) + { + case 0: // no units + break; + case 1: // dots per inch + image.setDotsPerMeterX(int(mDecompress.X_density * INCHESPERMETER)); + image.setDotsPerMeterY(int(mDecompress.Y_density * INCHESPERMETER)); + break; + case 2: // dots per cm + image.setDotsPerMeterX(mDecompress.X_density * 100); + image.setDotsPerMeterY(mDecompress.Y_density * 100); + break; + } + + mSourceManager.at_eof = true; + + (void) jpeg_finish_decompress(&mDecompress); + (void) jpeg_destroy_decompress(&mDecompress); + + mState = READ_DONE; + + return 0; + } + } + +#ifdef BUFFER_DEBUG + qDebug("valid_buffer_length is now %d", mSourceManager.valid_buffer_length); + qDebug("bytes_in_buffer is now %d", mSourceManager.bytes_in_buffer); + qDebug("consumed %d bytes", consumed); +#endif + if(mSourceManager.bytes_in_buffer + && mSourceManager.jpeg_buffer != mSourceManager.next_input_byte) { + memmove(mSourceManager.jpeg_buffer, + mSourceManager.next_input_byte, + mSourceManager.bytes_in_buffer); + } + mSourceManager.valid_buffer_length = mSourceManager.bytes_in_buffer; + return consumed; +} + + +//----------------------------------------------------------------------------- +// +// JPEGFormatType +// +//----------------------------------------------------------------------------- +QImageFormat* JPEGFormatType::decoderFor(const uchar* buffer, int length) { + if(length < 3) return 0; + + if(buffer[0] == 0377 && + buffer[1] == 0330 && + buffer[2] == 0377) { + return new JPEGFormat; + } + + return 0; +} + +const char* JPEGFormatType::formatName() const { + return "JPEG"; +} + +} // namespace diff --git a/src/gvcore/jpegformattype.h b/src/gvcore/jpegformattype.h new file mode 100644 index 0000000..b92b69d --- /dev/null +++ b/src/gvcore/jpegformattype.h @@ -0,0 +1,51 @@ +/* This file is based on kdelibs-3.2.0/khtml/misc/loader_jpeg.h. Original + * copyright follows. + */ +/* + This file is part of the KDE libraries + + Copyright (C) 2000 Dirk Mueller (mueller@kde.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + This is a helper for progressive loading of JPEG's. +*/ + +#ifndef gvjpegformattype_h +#define gvjpegformattype_h + +#include <qasyncimageio.h> + +namespace Gwenview { + +/** + * @internal + * + * An incremental loader factory for JPEG's. + */ +class JPEGFormatType : public QImageFormatType { +public: + QImageFormat* decoderFor(const uchar* buffer, int length); + const char* formatName() const; +}; + + +// ----------------------------------------------------------------------------- + +} // namespace +#endif // gvjpegformattype_h diff --git a/src/gvcore/libgwenview_export.h b/src/gvcore/libgwenview_export.h new file mode 100644 index 0000000..769141f --- /dev/null +++ b/src/gvcore/libgwenview_export.h @@ -0,0 +1,36 @@ +/* + This file is part of gwenview project + Copyright (c) 2005 Laurent Montel <montel@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ + +#ifndef _LIBGWENVIEW_EXPORT_H +#define _LIBGWENVIEW_EXPORT_H + +#include <kdemacros.h> + +#ifdef __KDE_HAVE_GCC_VISIBILITY + +#define LIBGWENVIEW_EXPORT KDE_EXPORT + +#else +#define LIBGWENVIEW_EXPORT +#endif + +#endif /* _LIBGWENVIEW_EXPORT_H */ + + diff --git a/src/gvcore/mimetypeutils.cpp b/src/gvcore/mimetypeutils.cpp new file mode 100644 index 0000000..954a24b --- /dev/null +++ b/src/gvcore/mimetypeutils.cpp @@ -0,0 +1,87 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2006 Aurelien Gateau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#include "mimetypeutils.h" + +// Qt +#include <qstringlist.h> + +// KDE +#include <kapplication.h> +#include <kfileitem.h> +#include <kimageio.h> +#include <kio/netaccess.h> +#include <kmimetype.h> +#include <kurl.h> + +// Local +#include "archive.h" + + +namespace Gwenview { + +namespace MimeTypeUtils { + +const QStringList& rasterImageMimeTypes() { + static QStringList list; + if (list.isEmpty()) { + list=KImageIO::mimeTypes(KImageIO::Reading); + list.append("image/x-xcf-gimp"); + list.append("image/x-xcursor"); + // KImageIO does not return this one :'( + list.append("image/pjpeg"); + } + return list; +} + + +Kind mimeTypeKind(const QString& mimeType) { + if (mimeType.startsWith("inode/directory")) { + return KIND_DIR; + } + if (Archive::mimeTypes().contains(mimeType)) { + return KIND_ARCHIVE; + } + if (rasterImageMimeTypes().contains(mimeType)) { + return KIND_RASTER_IMAGE; + } + + return KIND_FILE; +} + + +Kind fileItemKind(const KFileItem* item) { + return mimeTypeKind(item->mimetype()); +} + + +Kind urlKind(const KURL& url) { + QString mimeType; + if (url.isLocalFile()) { + mimeType=KMimeType::findByURL(url)->name(); + } else { + mimeType=KIO::NetAccess::mimetype(url, KApplication::kApplication()->mainWidget()); + } + return mimeTypeKind(mimeType); +} + + +} // namespace MimeTypeUtils +} // namespace Gwenview diff --git a/src/gvcore/mimetypeutils.h b/src/gvcore/mimetypeutils.h new file mode 100644 index 0000000..02aa6dc --- /dev/null +++ b/src/gvcore/mimetypeutils.h @@ -0,0 +1,47 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2006 Aurelien Gateau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef MIMETYPEUTILS_H +#define MIMETYPEUTILS_H + +// Local +#include "libgwenview_export.h" + +class KFileItem; +class KURL; + +class QString; +class QStringList; + +namespace Gwenview { + +namespace MimeTypeUtils { +enum Kind { KIND_UNKNOWN, KIND_DIR, KIND_ARCHIVE, KIND_FILE, KIND_RASTER_IMAGE }; + +LIBGWENVIEW_EXPORT const QStringList& rasterImageMimeTypes(); +Kind fileItemKind(const KFileItem*); +Kind urlKind(const KURL&); +Kind mimeTypeKind(const QString& mimeType); + +} // namespace FileUtils + +} // namespace Gwenview + +#endif /* MIMETYPEUTILS_H */ diff --git a/src/gvcore/miscconfig.kcfg b/src/gvcore/miscconfig.kcfg new file mode 100644 index 0000000..c3cc726 --- /dev/null +++ b/src/gvcore/miscconfig.kcfg @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE kcfg SYSTEM "http://www.kde.org/standards/kcfg/1.0/kcfg.dtd"> +<kcfg> + <kcfgfile name="gwenviewrc"/> + <group name="misc"> + <entry name="autoRotateImages" type="Bool"> + <label>Whether Gwenview should rotate images if orientation information is available.</label> + <default>true</default> + </entry> + <entry name="history" type="PathList"> + </entry> + <entry name="rememberFilter" type="Bool"> + <label>Whether Gwenview should remember the file filter.</label> + <default>false</default> + </entry> + <entry name="rememberURL" type="Bool"> + <label>Whether Gwenview should remember the last URL.</label> + <default>false</default> + </entry> + </group> + <!-- This is the group for KDialog don't-ask-again checkboxes --> + <group name="Notification Messages"> + <entry name="modifiedBehavior" key="save automatically" type="Enum"> + <choices> + <choice name="ask"/> <!-- == 'ask what to do' --> + <choice name="yes"/> <!-- == 'save' --> + <choice name="no"/> <!-- == 'discard' --> + </choices> + </entry> + </group> +</kcfg> diff --git a/src/gvcore/miscconfig.kcfgc b/src/gvcore/miscconfig.kcfgc new file mode 100644 index 0000000..3123823 --- /dev/null +++ b/src/gvcore/miscconfig.kcfgc @@ -0,0 +1,7 @@ +File=miscconfig.kcfg +ClassName=MiscConfig +NameSpace=Gwenview +Singleton=true +Mutators=true +IncludeFiles=gvcore/libgwenview_export.h +Visibility=LIBGWENVIEW_EXPORT diff --git a/src/gvcore/mngformattype.cpp b/src/gvcore/mngformattype.cpp new file mode 100644 index 0000000..60f64ef --- /dev/null +++ b/src/gvcore/mngformattype.cpp @@ -0,0 +1,520 @@ +// this code is copied from Qt, with fixes for not finishing decoding +// prematurely + +/* The actual patch is: +===== +--- /opt/_q/src/kernel/qmngio.cpp 2004-05-04 18:28:15.000000000 +0200 ++++ gvmngformattype.cpp 2005-04-13 16:11:50.000000000 +0200 +@@ -411,8 +417,11 @@ int QMNGFormat::decode( QImage& img, QIm + } + + losttime += losingtimer.elapsed(); +- if ( ndata || !length ) +- mng_display_resume(handle); ++ bool needmore = false; ++ if ( ndata ) { ++ mng_retcode r = mng_display_resume(handle); ++ needmore = ( r == MNG_NEEDMOREDATA ); ++ } + losingtimer.start(); + + image = 0; +@@ -422,6 +431,13 @@ int QMNGFormat::decode( QImage& img, QIm + // Move back unused tail + memcpy(buffer,buffer+ubuffer,nbuffer); + } ++ // "The function should return without processing all the data if it reaches the end of a frame in the input." ++ if( ndata && !needmore ) { ++ length -= ndata; ++ ndata = 0; ++ if( length == 0 ) // 0 means done, process at least one byte ++ length = ndata = 1; ++ } + if ( ndata ) { + // Not all used. + enlargeBuffer(nbuffer+ndata); +===== +*/ + +/**************************************************************************** +** +** +** Implementation of MNG QImage IOHandler +** +** Created : 970521 +** +** Copyright (C) 1997-2004 Trolltech AS. All rights reserved. +** +** This file is part of the kernel module of the Qt GUI Toolkit. +** +** This file may be distributed under the terms of the Q Public License +** as defined by Trolltech AS of Norway and appearing in the file +** LICENSE.QPL included in the packaging of this file. +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** Licensees holding valid Qt Enterprise Edition or Qt Professional Edition +** licenses may use this file in accordance with the Qt Commercial License +** Agreement provided with the Software. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See http://www.trolltech.com/pricing.html or email sales@trolltech.com for +** information about Qt Commercial License Agreements. +** See http://www.trolltech.com/qpl/ for QPL licensing information. +** See http://www.trolltech.com/gpl/ for GPL licensing information. +** +** Contact info@trolltech.com if any conditions of this licensing are +** not clear to you. +** +**********************************************************************/ + +#include <config.h> + +#ifndef QT_CLEAN_NAMESPACE +#define QT_CLEAN_NAMESPACE +#endif + +#include "qdatetime.h" +#include "mngformattype.h" + +//#ifndef QT_NO_IMAGEIO_MNG +#ifdef HAVE_LIBMNG + +#include "qimage.h" +#include "qasyncimageio.h" +#include "qiodevice.h" + +// Define XMD_H prohibits the included headers of libmng.h to typedef INT32. +// This is needed for Borland with STL support, since in that case, INT32 is +// already defined by some Borland header. +#define XMD_H +#if defined(Q_OS_UNIXWARE) +# define HAVE_BOOLEAN // libjpeg under Unixware seems to need this +#endif +#include <libmng.h> +#include <stdlib.h> + + +#ifndef QT_NO_ASYNC_IMAGE_IO +namespace Gwenview { + +class MNGFormat : public QImageFormat { +public: + MNGFormat(); + virtual ~MNGFormat(); + + int decode(QImage& img, QImageConsumer* consumer, + const uchar* buffer, int length); + + bool openstream() + { + // ### We should figure out how many loops an MNG has, but for now always assume infinite. + if (consumer) + consumer->setLooping(0); + return TRUE; + } + bool closestream( ) + { + if (consumer) + consumer->end(); + return TRUE; + } + bool readdata( mng_ptr pBuf, mng_uint32 iBuflen, mng_uint32p pRead ) + { + uint m = ndata + nbuffer - ubuffer; + if ( iBuflen > m ) { + iBuflen = m; + } + *pRead = iBuflen; + uint n = nbuffer-ubuffer; + if ( iBuflen < n ) { + // enough in buffer + memcpy(pBuf, buffer+ubuffer, iBuflen); + ubuffer += iBuflen; + return TRUE; + } + if ( n ) { + // consume buffer + memcpy(pBuf, buffer+ubuffer, n ); + pBuf = (mng_ptr)((char*)pBuf + n); + iBuflen -= n; + ubuffer = nbuffer; + } + if ( iBuflen ) { + // fill from incoming data + memcpy(pBuf, data, iBuflen); + data += iBuflen; + ndata -= iBuflen; + } + return TRUE; + } + bool errorproc( mng_int32 iErrorcode, + mng_int8 /*iSeverity*/, + mng_chunkid iChunkname, + mng_uint32 /*iChunkseq*/, + mng_int32 iExtra1, + mng_int32 iExtra2, + mng_pchar zErrortext ) + { + qWarning("MNG error %d: %s; chunk %c%c%c%c; subcode %d:%d", + iErrorcode,zErrortext, + (iChunkname>>24)&0xff, + (iChunkname>>16)&0xff, + (iChunkname>>8)&0xff, + (iChunkname>>0)&0xff, + iExtra1,iExtra2); + return TRUE; + } + bool processheader( mng_uint32 iWidth, mng_uint32 iHeight ) + { + image->create(iWidth,iHeight,32); + image->setAlphaBuffer(TRUE); + memset(image->bits(),0,iWidth*iHeight*4); + consumer->setSize(iWidth,iHeight); + mng_set_canvasstyle(handle, + QImage::systemByteOrder() == QImage::LittleEndian + ? MNG_CANVAS_BGRA8 : MNG_CANVAS_ARGB8 ); + return TRUE; + } + mng_ptr getcanvasline( mng_uint32 iLinenr ) + { + return image->scanLine(iLinenr); + } + mng_bool refresh( mng_uint32 x, mng_uint32 y, mng_uint32 w, mng_uint32 h ) + { + QRect r(x,y,w,h); + consumer->changed(r); + consumer->setFramePeriod(0); + consumer->frameDone(); + return TRUE; + } + mng_uint32 gettickcount( ) + { + return timer.elapsed() - losttime; + } + bool settimer( mng_uint32 iMsecs ) + { + consumer->setFramePeriod(iMsecs); + consumer->frameDone(); + state = Time; + losingtimer.start(); + losttime -= iMsecs; + return TRUE; + } + +private: + // Animation-level information + enum { MovieStart, Time, Data, Data2 } state; + + // Image-level information + mng_handle handle; + + // For storing unused data + uchar *buffer; + uint maxbuffer; + uint nbuffer; + + // Timing + QTime timer; + QTime losingtimer; + int losttime; + + void enlargeBuffer(uint n) + { + if ( n > maxbuffer ) { + maxbuffer = n; + buffer = (uchar*)realloc(buffer,n); + } + } + + // Temporary locals during single data-chunk processing + const uchar* data; + uint ndata; + uint ubuffer; + QImageConsumer* consumer; + QImage* image; +}; + +class MNGFormatType : public QImageFormatType +{ + QImageFormat* decoderFor(const uchar* buffer, int length); + const char* formatName() const; +}; + + +/* + \class QMNGFormat qmngio.h + \brief Incremental image decoder for MNG image format. + + \ingroup images + \ingroup graphics + + This subclass of QImageFormat decodes MNG format images, + including animated MNGs. + + Animated MNG images are standard MNG images. The MNG standard + defines two extension chunks that are useful for animations: + + <dl> + <dt>gIFg - GIF-like Graphic Control Extension + <dd>Includes frame disposal, user input flag (we ignore this), + and inter-frame delay. + <dt>gIFx - GIF-like Application Extension + <dd>Multi-purpose, but we just use the Netscape extension + which specifies looping. + </dl> + + The subimages usually contain a offset chunk (oFFs) but need not. + + The first image defines the "screen" size. Any subsequent image that + doesn't fit is clipped. + +TODO: decide on this point. gIFg gives disposal types, so it can be done. + All images paste (\e not composite, just place all-channel copying) + over the previous image to produce a subsequent frame. +*/ + +/* + \class QMNGFormatType qasyncimageio.h + \brief Incremental image decoder for MNG image format. + + \ingroup images + \ingroup graphics + \ingroup io + + This subclass of QImageFormatType recognizes MNG + format images, creating a QMNGFormat when required. An instance + of this class is created automatically before any other factories, + so you should have no need for such objects. +*/ + +QImageFormat* MNGFormatType::decoderFor( const uchar* buffer, int length ) +{ + if (length < 8) return 0; + + if (buffer[0]==138 // MNG signature + && buffer[1]=='M' + && buffer[2]=='N' + && buffer[3]=='G' + && buffer[4]==13 + && buffer[5]==10 + && buffer[6]==26 + && buffer[7]==10 + || buffer[0]==139 // JNG signature + && buffer[1]=='J' + && buffer[2]=='N' + && buffer[3]=='G' + && buffer[4]==13 + && buffer[5]==10 + && buffer[6]==26 + && buffer[7]==10 +#ifdef QT_NO_IMAGEIO_PNG // if we don't have native PNG support use libmng + || buffer[0]==137 // PNG signature + && buffer[1]=='P' + && buffer[2]=='N' + && buffer[3]=='G' + && buffer[4]==13 + && buffer[5]==10 + && buffer[6]==26 + && buffer[7]==10 +#endif + ) + return new MNGFormat; + return 0; +} + +const char* MNGFormatType::formatName() const +{ + return "MNG"; +} + + +/*! + Constructs a QMNGFormat. +*/ +MNGFormat::MNGFormat() +{ + state = MovieStart; + handle = 0; + nbuffer = 0; + maxbuffer = 0; + buffer = 0; + losttime = 0; +} + +/* + Destroys a QMNGFormat. +*/ +MNGFormat::~MNGFormat() +{ + // We're setting the consumer to 0 since it may have been + // deleted by read_async_image in qimage.cpp + consumer = 0; + if (handle) mng_cleanup(&handle); +} + + +// C-callback to C++-member-function conversion +// +static mng_bool openstream( mng_handle handle ) +{ + return ((MNGFormat*)mng_get_userdata(handle))->openstream(); +} +static mng_bool closestream( mng_handle handle ) +{ + return ((MNGFormat*)mng_get_userdata(handle))->closestream(); +} +static mng_bool readdata( mng_handle handle, mng_ptr pBuf, mng_uint32 iBuflen, mng_uint32p pRead ) +{ + return ((MNGFormat*)mng_get_userdata(handle))->readdata(pBuf,iBuflen,pRead); +} +static mng_bool errorproc( mng_handle handle, + mng_int32 iErrorcode, + mng_int8 iSeverity, + mng_chunkid iChunkname, + mng_uint32 iChunkseq, + mng_int32 iExtra1, + mng_int32 iExtra2, + mng_pchar zErrortext ) +{ + return ((MNGFormat*)mng_get_userdata(handle))->errorproc(iErrorcode, + iSeverity,iChunkname,iChunkseq,iExtra1,iExtra2,zErrortext); +} +static mng_bool processheader( mng_handle handle, + mng_uint32 iWidth, mng_uint32 iHeight ) +{ + return ((MNGFormat*)mng_get_userdata(handle))->processheader(iWidth,iHeight); +} +static mng_ptr getcanvasline( mng_handle handle, mng_uint32 iLinenr ) +{ + return ((MNGFormat*)mng_get_userdata(handle))->getcanvasline(iLinenr); +} +static mng_bool refresh( mng_handle handle, + mng_uint32 iTop, + mng_uint32 iLeft, + mng_uint32 iBottom, + mng_uint32 iRight ) +{ + return ((MNGFormat*)mng_get_userdata(handle))->refresh(iTop,iLeft,iBottom,iRight); +} +static mng_uint32 gettickcount( mng_handle handle ) +{ + return ((MNGFormat*)mng_get_userdata(handle))->gettickcount(); +} +static mng_bool settimer( mng_handle handle, mng_uint32 iMsecs ) +{ + return ((MNGFormat*)mng_get_userdata(handle))->settimer(iMsecs); +} + +static mng_ptr memalloc( mng_size_t iLen ) +{ + return calloc(1,iLen); +} +static void memfree( mng_ptr iPtr, mng_size_t /*iLen*/ ) +{ + free(iPtr); +} + +/*! + This function decodes some data into image changes. + + Returns the number of bytes consumed. +*/ +int MNGFormat::decode( QImage& img, QImageConsumer* cons, + const uchar* buf, int length ) +{ + consumer = cons; + image = &img; + + data = buf; + ndata = length; + ubuffer = 0; + + if ( state == MovieStart ) { + handle = mng_initialize( (mng_ptr)this, Gwenview::memalloc, Gwenview::memfree, 0 ); + mng_set_suspensionmode( handle, MNG_TRUE ); + mng_setcb_openstream( handle, Gwenview::openstream ); + mng_setcb_closestream( handle, Gwenview::closestream ); + mng_setcb_readdata( handle, Gwenview::readdata ); + mng_setcb_errorproc( handle, Gwenview::errorproc ); + mng_setcb_processheader( handle, Gwenview::processheader ); + mng_setcb_getcanvasline( handle, Gwenview::getcanvasline ); + mng_setcb_refresh( handle, Gwenview::refresh ); + mng_setcb_gettickcount( handle, Gwenview::gettickcount ); + mng_setcb_settimer( handle, Gwenview::settimer ); + state = Data; + mng_readdisplay(handle); + losingtimer.start(); + } + + losttime += losingtimer.elapsed(); + bool needmore = false; + if ( ndata ) { + mng_retcode r = mng_display_resume(handle); + needmore = ( r == MNG_NEEDMOREDATA ); + } + losingtimer.start(); + + image = 0; + + nbuffer -= ubuffer; + if ( nbuffer ) { + // Move back unused tail + memcpy(buffer,buffer+ubuffer,nbuffer); + } + // "The function should return without processing all the data if it reaches the end of a frame in the input." + if( ndata && !needmore ) { + length -= ndata; + ndata = 0; + if( length == 0 ) // 0 means done, process at least one byte + length = ndata = 1; + } + if ( ndata ) { + // Not all used. + enlargeBuffer(nbuffer+ndata); + memcpy(buffer+nbuffer,data,ndata); + nbuffer += ndata; + } + + return length; +} + +static MNGFormatType* globalMngFormatTypeObject = 0; + +#endif // QT_NO_ASYNC_IMAGE_IO + +#ifndef QT_NO_ASYNC_IMAGE_IO +void gvCleanupMngIO() +{ + if ( globalMngFormatTypeObject ) { + delete globalMngFormatTypeObject; + globalMngFormatTypeObject = 0; + } +} +#endif + +void gvInitMngIO() +{ + static bool done = FALSE; + if ( !done ) { + done = TRUE; +#ifndef QT_NO_ASYNC_IMAGE_IO + globalMngFormatTypeObject = new MNGFormatType; + qAddPostRoutine( gvCleanupMngIO ); +#endif + } +} + +#else +void gvInitMngIO() {} + +namespace Gwenview { +#endif + +MNG::MNG() { gvInitMngIO(); } +} // namespace diff --git a/src/gvcore/mngformattype.h b/src/gvcore/mngformattype.h new file mode 100644 index 0000000..9e60b64 --- /dev/null +++ b/src/gvcore/mngformattype.h @@ -0,0 +1,55 @@ +// this code is copied from Qt, with fixes for not finishing decoding +// prematurely + +/**************************************************************************** +** +** +** Definition of MNG QImage IOHandler +** +** Created : 970521 +** +** Copyright (C) 2000 Trolltech AS. All rights reserved. +** +** This file is part of the kernel module of the Qt GUI Toolkit. +** +** This file may be distributed under the terms of the Q Public License +** as defined by Trolltech AS of Norway and appearing in the file +** LICENSE.QPL included in the packaging of this file. +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** Licensees holding valid Qt Enterprise Edition or Qt Professional Edition +** licenses may use this file in accordance with the Qt Commercial License +** Agreement provided with the Software. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See http://www.trolltech.com/pricing.html or email sales@trolltech.com for +** information about Qt Commercial License Agreements. +** See http://www.trolltech.com/qpl/ for QPL licensing information. +** See http://www.trolltech.com/gpl/ for GPL licensing information. +** +** Contact info@trolltech.com if any conditions of this licensing are +** not clear to you. +** +**********************************************************************/ + +#ifndef QMNGIO_H +#define QMNGIO_H + +#ifndef QT_H +#endif // QT_H +namespace Gwenview { + +class MNG +{ +public: + MNG(); +}; + +} // namespace +#endif // QMNGIO_H diff --git a/src/gvcore/pngformattype.cpp b/src/gvcore/pngformattype.cpp new file mode 100644 index 0000000..36c1064 --- /dev/null +++ b/src/gvcore/pngformattype.cpp @@ -0,0 +1,559 @@ +// this code is copied from Qt, with code added to actually call consumer +// methods that inform about the progress of loading + +/**************************************************************************** +** +** +** Implementation of PNG QImage IOHandler +** +** Created : 970521 +** +** Copyright (C) 1992-2003 Trolltech AS. All rights reserved. +** +** This file is part of the kernel module of the Qt GUI Toolkit. +** +** This file may be distributed under the terms of the Q Public License +** as defined by Trolltech AS of Norway and appearing in the file +** LICENSE.QPL included in the packaging of this file. +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** Licensees holding valid Qt Enterprise Edition or Qt Professional Edition +** licenses may use this file in accordance with the Qt Commercial License +** Agreement provided with the Software. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See http://www.trolltech.com/pricing.html or email sales@trolltech.com for +** information about Qt Commercial License Agreements. +** See http://www.trolltech.com/qpl/ for QPL licensing information. +** See http://www.trolltech.com/gpl/ for GPL licensing information. +** +** Contact info@trolltech.com if any conditions of this licensing are +** not clear to you. +** +**********************************************************************/ + +#include "pngformattype.h" + +#include <png.h> + +namespace Gwenview { + +class PNGFormat : public QImageFormat { +public: + PNGFormat(); + virtual ~PNGFormat(); + + int decode(QImage& img, QImageConsumer* consumer, + const uchar* buffer, int length); + + void info(png_structp png_ptr, png_infop info); + void row(png_structp png_ptr, png_bytep new_row, + png_uint_32 row_num, int pass); + void end(png_structp png_ptr, png_infop info); +#ifdef PNG_USER_CHUNKS_SUPPORTED + int user_chunk(png_structp png_ptr, + png_bytep data, png_uint_32 length); +#endif + +private: + // Animation-level information + enum { MovieStart, FrameStart, Inside, End } state; + int first_frame; + int base_offx; + int base_offy; + + // Image-level information + png_structp png_ptr; + png_infop info_ptr; + + // Temporary locals during single data-chunk processing + QImageConsumer* consumer; + QImage* image; + int unused_data; + QRect changed_rect; +}; + +/* + \class QPNGFormat qpngio.h + \brief The QPNGFormat class provides an incremental image decoder for PNG + image format. + + \ingroup images + \ingroup graphics + + This subclass of QImageFormat decodes PNG format images, + including animated PNGs. + + Animated PNG images are standard PNG images. The PNG standard + defines two extension chunks that are useful for animations: + + \list + \i gIFg - GIF-like Graphic Control Extension. + This includes frame disposal, user input flag (we ignore this), + and inter-frame delay. + \i gIFx - GIF-like Application Extension. + This is multi-purpose, but we just use the Netscape extension + which specifies looping. + \endlist + + The subimages usually contain a offset chunk (oFFs) but need not. + + The first image defines the "screen" size. Any subsequent image that + doesn't fit is clipped. +*/ +/* ###TODO: decide on this point. gIFg gives disposal types, so it can be done. + All images paste (\e not composite, just place all-channel copying) + over the previous image to produce a subsequent frame. +*/ + +/* + \class QPNGFormatType qasyncimageio.h + \brief The QPNGFormatType class provides an incremental image decoder + for PNG image format. + + \ingroup images + \ingroup graphics + \ingroup io + + This subclass of QImageFormatType recognizes PNG format images, creating + a QPNGFormat when required. An instance of this class is created + automatically before any other factories, so you should have no need for + such objects. +*/ + +QImageFormat* PNGFormatType::decoderFor( + const uchar* buffer, int length) +{ + if (length < 8) return 0; + if (buffer[0]==137 + && buffer[1]=='P' + && buffer[2]=='N' + && buffer[3]=='G' + && buffer[4]==13 + && buffer[5]==10 + && buffer[6]==26 + && buffer[7]==10) + return new PNGFormat; + return 0; +} + +const char* PNGFormatType::formatName() const +{ + return "PNG"; +} + +extern "C" { + +static void +info_callback(png_structp png_ptr, png_infop info) +{ + PNGFormat* that = (PNGFormat*)png_get_progressive_ptr(png_ptr); + that->info(png_ptr,info); +} + +static void +row_callback(png_structp png_ptr, png_bytep new_row, + png_uint_32 row_num, int pass) +{ + PNGFormat* that = (PNGFormat*)png_get_progressive_ptr(png_ptr); + that->row(png_ptr,new_row,row_num,pass); +} + +static void +end_callback(png_structp png_ptr, png_infop info) +{ + PNGFormat* that = (PNGFormat*)png_get_progressive_ptr(png_ptr); + that->end(png_ptr,info); +} + +#if 0 +#ifdef PNG_USER_CHUNKS_SUPPORTED +static int +CALLBACK_CALL_TYPE user_chunk_callback(png_structp png_ptr, + png_unknown_chunkp chunk) +{ + PNGFormat* that = (PNGFormat*)png_get_progressive_ptr(png_ptr); + return that->user_chunk(png_ptr,chunk->data,chunk->size); +} +#endif +#endif + +static void qt_png_warning(png_structp /*png_ptr*/, png_const_charp message) +{ + qWarning("libpng warning: %s", message); +} + +} + +static +void setup_qt( QImage& image, png_structp png_ptr, png_infop info_ptr ) +{ + // For now, we will use the PC monitor gamma, if you own a Mac, you'd better use 1.8 + const double SCREEN_GAMMA=2.2; + if ( png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA) ) { + double file_gamma; + png_get_gAMA(png_ptr, info_ptr, &file_gamma); + png_set_gamma( png_ptr, SCREEN_GAMMA, file_gamma ); + } + + png_uint_32 width; + png_uint_32 height; + int bit_depth; + int color_type; + png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, + 0, 0, 0); + + if ( color_type == PNG_COLOR_TYPE_GRAY ) { + // Black & White or 8-bit grayscale + if ( bit_depth == 1 && info_ptr->channels == 1 ) { + png_set_invert_mono( png_ptr ); + png_read_update_info( png_ptr, info_ptr ); + if (!image.create( width, height, 1, 2, QImage::BigEndian )) + return; + image.setColor( 1, qRgb(0,0,0) ); + image.setColor( 0, qRgb(255,255,255) ); + } else if (bit_depth == 16 && png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { + png_set_expand(png_ptr); + png_set_strip_16(png_ptr); + png_set_gray_to_rgb(png_ptr); + + if (!image.create(width, height, 32)) + return; + image.setAlphaBuffer(TRUE); + + if (QImage::systemByteOrder() == QImage::BigEndian) + png_set_swap_alpha(png_ptr); + + png_read_update_info(png_ptr, info_ptr); + } else { + if ( bit_depth == 16 ) + png_set_strip_16(png_ptr); + else if ( bit_depth < 8 ) + png_set_packing(png_ptr); + int ncols = bit_depth < 8 ? 1 << bit_depth : 256; + png_read_update_info(png_ptr, info_ptr); + if (!image.create(width, height, 8, ncols)) + return; + for (int i=0; i<ncols; i++) { + int c = i*255/(ncols-1); + image.setColor( i, qRgba(c,c,c,0xff) ); + } + if ( png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) ) { + const int g = info_ptr->trans_values.gray; + if (g < ncols) { + image.setAlphaBuffer(TRUE); + image.setColor(g, image.color(g) & RGB_MASK); + } + } + } + } else if ( color_type == PNG_COLOR_TYPE_PALETTE + && png_get_valid(png_ptr, info_ptr, PNG_INFO_PLTE) + && info_ptr->num_palette <= 256 ) + { + // 1-bit and 8-bit color + if ( bit_depth != 1 ) + png_set_packing( png_ptr ); + png_read_update_info( png_ptr, info_ptr ); + png_get_IHDR(png_ptr, info_ptr, + &width, &height, &bit_depth, &color_type, 0, 0, 0); + if (!image.create(width, height, bit_depth, info_ptr->num_palette, + QImage::BigEndian)) + return; + int i = 0; + if ( png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) ) { + image.setAlphaBuffer( TRUE ); + while ( i < info_ptr->num_trans ) { + image.setColor(i, qRgba( + info_ptr->palette[i].red, + info_ptr->palette[i].green, + info_ptr->palette[i].blue, + info_ptr->trans[i] + ) + ); + i++; + } + } + while ( i < info_ptr->num_palette ) { + image.setColor(i, qRgba( + info_ptr->palette[i].red, + info_ptr->palette[i].green, + info_ptr->palette[i].blue, + 0xff + ) + ); + i++; + } + } else { + // 32-bit + if ( bit_depth == 16 ) + png_set_strip_16(png_ptr); + + png_set_expand(png_ptr); + + if ( color_type == PNG_COLOR_TYPE_GRAY_ALPHA ) + png_set_gray_to_rgb(png_ptr); + + if (!image.create(width, height, 32)) + return; + + // Only add filler if no alpha, or we can get 5 channel data. + if (!(color_type & PNG_COLOR_MASK_ALPHA) + && !png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { + png_set_filler(png_ptr, 0xff, + QImage::systemByteOrder() == QImage::BigEndian ? + PNG_FILLER_BEFORE : PNG_FILLER_AFTER); + // We want 4 bytes, but it isn't an alpha channel + } else { + image.setAlphaBuffer(TRUE); + } + + if ( QImage::systemByteOrder() == QImage::BigEndian ) { + png_set_swap_alpha(png_ptr); + } + + png_read_update_info(png_ptr, info_ptr); + } + + // Qt==ARGB==Big(ARGB)==Little(BGRA) + if ( QImage::systemByteOrder() == QImage::LittleEndian ) { + png_set_bgr(png_ptr); + } +} + + + +/*! + Constructs a QPNGFormat object. +*/ +PNGFormat::PNGFormat() +{ + state = MovieStart; + first_frame = 1; + base_offx = 0; + base_offy = 0; + png_ptr = 0; + info_ptr = 0; +} + + +/*! + Destroys a QPNGFormat object. +*/ +PNGFormat::~PNGFormat() +{ + if ( png_ptr ) + png_destroy_read_struct(&png_ptr, &info_ptr, 0); +} + + +/*! + This function decodes some data into image changes. + + Returns the number of bytes consumed. +*/ +int PNGFormat::decode(QImage& img, QImageConsumer* cons, + const uchar* buffer, int length) +{ + consumer = cons; + image = &img; + + if ( state != Inside ) { + png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); + if (!png_ptr) { + info_ptr = 0; + image = 0; + return -1; + } + + png_set_error_fn(png_ptr, 0, 0, qt_png_warning); + png_set_compression_level(png_ptr, 9); + + info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + image = 0; + return -1; + } + + if (setjmp((png_ptr)->jmpbuf)) { + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + image = 0; + return -1; + } + + png_set_progressive_read_fn(png_ptr, (void *)this, + info_callback, row_callback, end_callback); + +#ifdef PNG_USER_CHUNKS_SUPPORTED + // Can't do this yet. libpng has a crash bug with unknown (user) chunks. + // Warwick has sent them a patch. + // png_set_read_user_chunk_fn(png_ptr, 0, user_chunk_callback); + // png_set_keep_unknown_chunks(png_ptr, 2/*HANDLE_CHUNK_IF_SAFE*/, 0, 0); +#endif + + if ( state != MovieStart && *buffer != 0211 ) { + // Good, no signature - the preferred way to concat PNG images. + // Skip them. + png_set_sig_bytes(png_ptr, 8); + } + + state = Inside; + changed_rect = QRect(); + } + + if ( !png_ptr ) return 0; + + if (setjmp(png_ptr->jmpbuf)) { + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + image = 0; + state = MovieStart; + return -1; + } + unused_data = 0; + png_process_data(png_ptr, info_ptr, (png_bytep)buffer, length); + int l = length - unused_data; + + if( !changed_rect.isNull()) { + consumer->changed( changed_rect ); + changed_rect = QRect(); + } + + if ( state != Inside ) { + if ( png_ptr ) + png_destroy_read_struct(&png_ptr, &info_ptr, 0); + } + + image = 0; + return l; +} + +void PNGFormat::info(png_structp png, png_infop) +{ + png_set_interlace_handling(png); + setup_qt(*image, png, info_ptr); + consumer->setSize( image->width(), image->height()); +} + +void PNGFormat::row(png_structp png, png_bytep new_row, + png_uint_32 row_num, int) +{ + uchar* old_row = image->scanLine(row_num); + png_progressive_combine_row(png, old_row, new_row); + changed_rect |= QRect( 0, row_num, image->width(), 1 ); +} + + +void PNGFormat::end(png_structp png, png_infop info) +{ + int offx = png_get_x_offset_pixels(png,info) - base_offx; + int offy = png_get_y_offset_pixels(png,info) - base_offy; + if ( first_frame ) { + base_offx = offx; + base_offy = offy; + first_frame = 0; + } + image->setOffset(QPoint(offx,offy)); + image->setDotsPerMeterX(png_get_x_pixels_per_meter(png,info)); + image->setDotsPerMeterY(png_get_y_pixels_per_meter(png,info)); + png_textp text_ptr; + int num_text=0; + png_get_text(png,info,&text_ptr,&num_text); + while (num_text--) { + image->setText(text_ptr->key,0,text_ptr->text); + text_ptr++; + } + if( !changed_rect.isNull()) { + consumer->changed( changed_rect ); + changed_rect = QRect(); + } + QRect r(0,0,image->width(),image->height()); + consumer->frameDone(QPoint(offx,offy),r); + consumer->end(); + state = FrameStart; + unused_data = (int)png->buffer_size; // Since libpng doesn't tell us +} + +#ifdef PNG_USER_CHUNKS_SUPPORTED + +/* +#ifndef QT_NO_IMAGE_TEXT +static bool skip(png_uint_32& max, png_bytep& data) +{ + while (*data) { + if ( !max ) return FALSE; + max--; + data++; + } + if ( !max ) return FALSE; + max--; + data++; // skip to after NUL + return TRUE; +} +#endif +*/ + +int PNGFormat::user_chunk(png_structp png, + png_bytep data, png_uint_32 length) +{ +#if 0 // NOT SUPPORTED: experimental PNG animation. + // qDebug("Got %ld-byte %s chunk", length, png->chunk_name); + if ( 0==qstrcmp((char*)png->chunk_name, "gIFg") + && length == 4 ) { + + //QPNGImageWriter::DisposalMethod disposal = + // (QPNGImageWriter::DisposalMethod)data[0]; + // ### TODO: use the disposal method + int ms_delay = ((data[2] << 8) | data[3])*10; + consumer->setFramePeriod(ms_delay); + return 1; + } else if ( 0==qstrcmp((char*)png->chunk_name, "gIFx") + && length == 13 ) { + if ( qstrncmp((char*)data,"NETSCAPE2.0",11)==0 ) { + int looping = (data[0xC]<<8)|data[0xB]; + consumer->setLooping(looping); + return 1; + } + } +#else + Q_UNUSED( png ) + Q_UNUSED( data ) + Q_UNUSED( length ) +#endif + + /* + + libpng now supports this chunk. + + + if ( 0==qstrcmp((char*)png->chunk_name, "iTXt") && length>=6 ) { + const char* keyword = (const char*)data; + if ( !skip(length,data) ) return 0; + if ( length >= 4 ) { + char compression_flag = *data++; + char compression_method = *data++; + if ( compression_flag == compression_method ) { + // fool the compiler into thinking they're used + } + const char* lang = (const char*)data; + if ( !skip(length,data) ) return 0; + // const char* keyword_utf8 = (const char*)data; + if ( !skip(length,data) ) return 0; + const char* text_utf8 = (const char*)data; + if ( !skip(length,data) ) return 0; + QString text = QString::fromUtf8(text_utf8); + image->setText(keyword,lang[0] ? lang : 0,text); + return 1; + } + } + */ + + return 0; +} +} // namespace +#endif diff --git a/src/gvcore/pngformattype.h b/src/gvcore/pngformattype.h new file mode 100644 index 0000000..920ee0c --- /dev/null +++ b/src/gvcore/pngformattype.h @@ -0,0 +1,63 @@ +// this code is copied from Qt, with code added to actually call consumer +// methods that inform about the progress of loading + +/**************************************************************************** +** +** +** Implementation of PNG QImage IOHandler +** +** Created : 970521 +** +** Copyright (C) 1992-2003 Trolltech AS. All rights reserved. +** +** This file is part of the kernel module of the Qt GUI Toolkit. +** +** This file may be distributed under the terms of the Q Public License +** as defined by Trolltech AS of Norway and appearing in the file +** LICENSE.QPL included in the packaging of this file. +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** Licensees holding valid Qt Enterprise Edition or Qt Professional Edition +** licenses may use this file in accordance with the Qt Commercial License +** Agreement provided with the Software. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See http://www.trolltech.com/pricing.html or email sales@trolltech.com for +** information about Qt Commercial License Agreements. +** See http://www.trolltech.com/qpl/ for QPL licensing information. +** See http://www.trolltech.com/gpl/ for GPL licensing information. +** +** Contact info@trolltech.com if any conditions of this licensing are +** not clear to you. +** +**********************************************************************/ + +#ifndef gvpngformattype_h +#define gvpngformattype_h + +#include <qasyncimageio.h> + +namespace Gwenview { + +/** + * @internal + * + * An incremental loader factory for PNG's. + */ +class PNGFormatType : public QImageFormatType { +public: + QImageFormat* decoderFor(const uchar* buffer, int length); + const char* formatName() const; +}; + +} // namespace + +// ----------------------------------------------------------------------------- + +#endif // gvpngformattype_h diff --git a/src/gvcore/printdialog.cpp b/src/gvcore/printdialog.cpp new file mode 100644 index 0000000..6e3a162 --- /dev/null +++ b/src/gvcore/printdialog.cpp @@ -0,0 +1,297 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - printing support +Copyright (c) 2003 Angelo Naselli + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// Qt +#include <qcheckbox.h> +#include <qlayout.h> +#include <qpainter.h> +#include <qradiobutton.h> +#include <qvbuttongroup.h> + +// KDE +#include <kcombobox.h> +#include <kdebug.h> +#include <kdialog.h> +#include <klocale.h> +#include <knuminput.h> +#include <kprinter.h> + +// Local +#include "document.h" +#include "printdialogpagebase.h" +#include "printdialog.moc" + +namespace Gwenview { + + +const char* STR_TRUE="true"; +const char* STR_FALSE="false"; + +static inline Unit stringToUnit(const QString& unit) { + if (unit == i18n("Millimeters")) { + return GV_MILLIMETERS; + } else if (unit == i18n("Centimeters")) { + return GV_CENTIMETERS; + } else {//Inches + return GV_INCHES; + } +} + +static inline QString unitToString(Unit unit) { + if (unit == GV_MILLIMETERS) { + return i18n("Millimeters"); + } else if (unit == GV_CENTIMETERS) { + return i18n("Centimeters"); + } else { //GV_INCHES + return i18n("Inches"); + } +} + + +static inline double unitToMM(Unit unit) { + if (unit == GV_MILLIMETERS) { + return 1.; + } else if (unit == GV_CENTIMETERS) { + return 10.; + } else { //GV_INCHES + return 25.4; + } +} + + +PrintDialogPage::PrintDialogPage( Document* document, QWidget *parent, const char *name ) + : KPrintDialogPage( parent, name ) { + mDocument = document; + mContent = new PrintDialogPageBase(this); + setTitle( mContent->caption() ); + + QVBoxLayout *layout = new QVBoxLayout( this ); + layout->addWidget( mContent ); + + connect(mContent->mWidth, SIGNAL( valueChanged( double )), SLOT( slotWidthChanged( double ))); + connect(mContent->mHeight, SIGNAL( valueChanged( double )), SLOT( slotHeightChanged( double ))); + connect(mContent->mKeepRatio, SIGNAL( toggled( bool )), SLOT( toggleRatio( bool ))); + connect(mContent->mUnit, SIGNAL(activated(const QString &)), SLOT(slotUnitChanged(const QString& ))); + + mPreviousUnit = GV_MILLIMETERS; +} + +PrintDialogPage::~PrintDialogPage() {} + +void PrintDialogPage::getOptions( QMap<QString,QString>& opts, bool /*incldef*/ ) { + opts["app-gwenview-position"] = QString::number(getPosition(mContent->mPosition->currentText())); + opts["app-gwenview-printFilename"] = mContent->mAddFileName->isChecked() ? STR_TRUE : STR_FALSE; + opts["app-gwenview-printComment"] = mContent->mAddComment->isChecked() ? STR_TRUE : STR_FALSE; + opts["app-gwenview-scale"] = QString::number( + mContent->mNoScale->isChecked() ? GV_NOSCALE + : mContent->mFitToPage->isChecked() ? GV_FITTOPAGE + : GV_SCALE); + opts["app-gwenview-fitToPage"] = mContent->mFitToPage->isChecked() ? STR_TRUE : STR_FALSE; + opts["app-gwenview-enlargeToFit"] = mContent->mEnlargeToFit->isChecked() ? STR_TRUE : STR_FALSE; + + opts["app-gwenview-scaleKeepRatio"] = mContent->mKeepRatio->isChecked() ? STR_TRUE : STR_FALSE; + opts["app-gwenview-scaleUnit"] = QString::number(stringToUnit(mContent->mUnit->currentText())); + opts["app-gwenview-scaleWidth"] = QString::number( scaleWidth() ); + opts["app-gwenview-scaleHeight"] = QString::number( scaleHeight() ); + +} + +void PrintDialogPage::setOptions( const QMap<QString,QString>& opts ) { + int val; + bool ok; + QString stVal; + + val = opts["app-gwenview-position"].toInt( &ok ); + if (ok) { + stVal = setPosition(val); + mContent->mPosition->setCurrentItem(stVal); + } + + mContent->mAddFileName->setChecked( opts["app-gwenview-printFilename"] == STR_TRUE ); + mContent->mAddComment->setChecked( opts["app-gwenview-printComment"] == STR_TRUE ); + // Starts from id 1 because 0 is returned if not ok, and seems to have a weird + // problem with id 2 (last id) otherwise :( + ScaleId scaleButtonId = static_cast<ScaleId>( opts["app-gwenview-scale"].toInt( &ok ) ); + if (ok) { + mContent->mScaleGroup->setButton( scaleButtonId ); + } else { + mContent->mScaleGroup->setButton( GV_NOSCALE ); + } + mContent->mEnlargeToFit->setChecked( opts["app-gwenview-enlargeToFit"] == STR_TRUE ); + + Unit unit = static_cast<Unit>( opts["app-gwenview-scaleUnit"].toInt( &ok ) ); + if (ok) { + stVal = unitToString(unit); + mContent->mUnit->setCurrentItem(stVal); + mPreviousUnit = unit; + } + + mContent->mKeepRatio->setChecked( opts["app-gwenview-scaleKeepRatio"] == STR_TRUE ); + + double dbl; + dbl = opts["app-gwenview-scaleWidth"].toDouble( &ok ); + if ( ok ) setScaleWidth( dbl ); + dbl = opts["app-gwenview-scaleHeight"].toDouble( &ok ); + if ( ok ) setScaleHeight( dbl ); +} + +double PrintDialogPage::scaleWidth() const { + return mContent->mWidth->value(); +} + +double PrintDialogPage::scaleHeight() const { + return mContent->mHeight->value(); +} + +void PrintDialogPage::setScaleWidth( double value ) { + mContent->mWidth->setValue(value); +} + +void PrintDialogPage::setScaleHeight( double value ) { + mContent->mHeight->setValue(value); +} + +int PrintDialogPage::getPosition(const QString& align) { + int alignment; + + if (align == i18n("Central-Left")) { + alignment = Qt::AlignLeft | Qt::AlignVCenter; + } else if (align == i18n("Central-Right")) { + alignment = Qt::AlignRight | Qt::AlignVCenter; + } else if (align == i18n("Top-Left")) { + alignment = Qt::AlignTop | Qt::AlignLeft; + } else if (align == i18n("Top-Right")) { + alignment = Qt::AlignTop | Qt::AlignRight; + } else if (align == i18n("Bottom-Left")) { + alignment = Qt::AlignBottom | Qt::AlignLeft; + } else if (align == i18n("Bottom-Right")) { + alignment = Qt::AlignBottom | Qt::AlignRight; + } else if (align == i18n("Top-Central")) { + alignment = Qt::AlignTop | Qt::AlignHCenter; + } else if (align == i18n("Bottom-Central")) { + alignment = Qt::AlignBottom | Qt::AlignHCenter; + } else { + // Central + alignment = Qt::AlignCenter; // Qt::AlignHCenter || Qt::AlignVCenter + } + + return alignment; +} + +QString PrintDialogPage::setPosition(int align) { + QString alignment; + + if (align == (Qt::AlignLeft | Qt::AlignVCenter)) { + alignment = i18n("Central-Left"); + } else if (align == (Qt::AlignRight | Qt::AlignVCenter)) { + alignment = i18n("Central-Right"); + } else if (align == (Qt::AlignTop | Qt::AlignLeft)) { + alignment = i18n("Top-Left"); + } else if (align == (Qt::AlignTop | Qt::AlignRight)) { + alignment = i18n("Top-Right"); + } else if (align == (Qt::AlignBottom | Qt::AlignLeft)) { + alignment = i18n("Bottom-Left"); + } else if (align == (Qt::AlignBottom | Qt::AlignRight)) { + alignment = i18n("Bottom-Right"); + } else if (align == (Qt::AlignTop | Qt::AlignHCenter)) { + alignment = i18n("Top-Central"); + } else if (align == (Qt::AlignBottom | Qt::AlignHCenter)) { + alignment = i18n("Bottom-Central"); + } else { + // Central: Qt::AlignCenter or (Qt::AlignHCenter || Qt::AlignVCenter) + alignment = i18n("Central"); + } + + return alignment; +} + +// SLOTS +void PrintDialogPage::slotHeightChanged (double value) { + mContent->mWidth->blockSignals(true); + mContent->mHeight->blockSignals(true); + + if (mContent->mKeepRatio->isChecked()) { + double width = (mDocument->width() * value) / mDocument->height(); + mContent->mWidth->setValue( width ? width : 1.); + } + mContent->mHeight->setValue(value); + + mContent->mWidth->blockSignals(false); + mContent->mHeight->blockSignals(false); + +} + +void PrintDialogPage::slotWidthChanged (double value) { + mContent->mWidth->blockSignals(true); + mContent->mHeight->blockSignals(true); + if (mContent->mKeepRatio->isChecked()) { + double height = (mDocument->height() * value) / mDocument->width(); + mContent->mHeight->setValue( height ? height : 1); + } + mContent->mWidth->setValue(value); + mContent->mWidth->blockSignals(false); + mContent->mHeight->blockSignals(false); +} + +void PrintDialogPage::toggleRatio(bool enable) { + if (!enable) return; + // choosing a startup value of 15x10 cm (common photo dimention) + // mContent->mHeight->value() or mContent->mWidth->value() + // are usually empty at startup and hxw (0x0) isn't good IMO keeping ratio + double hValue, wValue; + if (mDocument->height() > mDocument->width()) { + hValue = mContent->mHeight->value(); + if (!hValue) hValue = 150*unitToMM(mPreviousUnit); + wValue = (mDocument->width() * hValue)/ mDocument->height(); + } else { + wValue = mContent->mWidth->value(); + if (!wValue) wValue = 150*unitToMM(mPreviousUnit); + hValue = (mDocument->height() * wValue)/ mDocument->width(); + } + + mContent->mWidth->blockSignals(true); + mContent->mHeight->blockSignals(true); + mContent->mWidth->setValue(wValue); + mContent->mHeight->setValue(hValue); + mContent->mWidth->blockSignals(false); + mContent->mHeight->blockSignals(false); +} + + +void PrintDialogPage::slotUnitChanged(const QString& string) { + Unit newUnit = stringToUnit(string); + double ratio = unitToMM(mPreviousUnit) / unitToMM(newUnit); + + mContent->mWidth->blockSignals(true); + mContent->mHeight->blockSignals(true); + + mContent->mWidth->setValue( mContent->mWidth->value() * ratio); + mContent->mHeight->setValue( mContent->mHeight->value() * ratio); + + mContent->mWidth->blockSignals(false); + mContent->mHeight->blockSignals(false); + + mPreviousUnit = newUnit; +} + + + + +} // namespace diff --git a/src/gvcore/printdialog.h b/src/gvcore/printdialog.h new file mode 100644 index 0000000..261583f --- /dev/null +++ b/src/gvcore/printdialog.h @@ -0,0 +1,80 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - printing support +Copyright (c) 2003 Angelo Naselli + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef PRINTDIALOG_H +#define PRINTDIALOG_H + +//Qt +#include <qfontmetrics.h> +#include <qstring.h> + +// KDE +#include <kdockwidget.h> +#include <kdeprint/kprintdialogpage.h> + +#include "libgwenview_export.h" +class PrintDialogPageBase; +namespace Gwenview { +class Document; + +enum Unit { + GV_MILLIMETERS = 1, + GV_CENTIMETERS, + GV_INCHES +}; + +enum ScaleId { + GV_NOSCALE=1, + GV_FITTOPAGE, + GV_SCALE +}; + +class LIBGWENVIEW_EXPORT PrintDialogPage : public KPrintDialogPage { + Q_OBJECT + +public: + PrintDialogPage( Document* document, QWidget *parent = 0L, const char *name = 0 ); + ~PrintDialogPage(); + + virtual void getOptions(QMap<QString,QString>& opts, bool incldef = false); + virtual void setOptions(const QMap<QString,QString>& opts); + +private slots: + void toggleRatio(bool enable); + void slotUnitChanged(const QString& string); + void slotHeightChanged(double value); + void slotWidthChanged(double value); + +private: + double scaleWidth() const; + double scaleHeight() const; + void setScaleWidth(double pixels); + void setScaleHeight(double pixels); + int getPosition(const QString& align); + QString setPosition(int align); + + Document *mDocument; + PrintDialogPageBase* mContent; + Unit mPreviousUnit; +}; + +} // namespace +#endif + diff --git a/src/gvcore/printdialogpagebase.ui b/src/gvcore/printdialogpagebase.ui new file mode 100644 index 0000000..f5d5c8c --- /dev/null +++ b/src/gvcore/printdialogpagebase.ui @@ -0,0 +1,408 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>PrintDialogPageBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>PrintDialogPageBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>511</width> + <height>260</height> + </rect> + </property> + <property name="caption"> + <string>Image Settings</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout2</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel1</cstring> + </property> + <property name="text"> + <string>Image position:</string> + </property> + </widget> + <widget class="KComboBox"> + <item> + <property name="text"> + <string>Top-Left</string> + </property> + </item> + <item> + <property name="text"> + <string>Top-Central</string> + </property> + </item> + <item> + <property name="text"> + <string>Top-Right</string> + </property> + </item> + <item> + <property name="text"> + <string>Central-Left</string> + </property> + </item> + <item> + <property name="text"> + <string>Central</string> + </property> + </item> + <item> + <property name="text"> + <string>Central-Right</string> + </property> + </item> + <item> + <property name="text"> + <string>Bottom-Left</string> + </property> + </item> + <item> + <property name="text"> + <string>Bottom-Central</string> + </property> + </item> + <item> + <property name="text"> + <string>Bottom-Right</string> + </property> + </item> + <property name="name"> + <cstring>mPosition</cstring> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer1</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>101</width> + <height>21</height> + </size> + </property> + </spacer> + </hbox> + </widget> + <widget class="QCheckBox"> + <property name="name"> + <cstring>mAddFileName</cstring> + </property> + <property name="text"> + <string>Print fi&lename below image</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + <widget class="QCheckBox"> + <property name="name"> + <cstring>mAddComment</cstring> + </property> + <property name="text"> + <string>Print image comment</string> + </property> + <property name="accel"> + <string></string> + </property> + </widget> + <widget class="QButtonGroup"> + <property name="name"> + <cstring>mScaleGroup</cstring> + </property> + <property name="title"> + <string>Scaling</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QRadioButton"> + <property name="name"> + <cstring>mNoScale</cstring> + </property> + <property name="text"> + <string>&No scaling</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <property name="buttonGroupId"> + <number>1</number> + </property> + </widget> + <widget class="QRadioButton"> + <property name="name"> + <cstring>mFitToPage</cstring> + </property> + <property name="text"> + <string>&Fit image to page</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <property name="buttonGroupId"> + <number>2</number> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout4</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer> + <property name="name"> + <cstring>spacer4</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Fixed</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="QCheckBox"> + <property name="name"> + <cstring>mEnlargeToFit</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Enlarge smaller images</string> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer6</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>240</width> + <height>21</height> + </size> + </property> + </spacer> + </hbox> + </widget> + <widget class="QRadioButton"> + <property name="name"> + <cstring>mScale</cstring> + </property> + <property name="text"> + <string>&Scale to:</string> + </property> + <property name="buttonGroupId"> + <number>3</number> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout4</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer> + <property name="name"> + <cstring>spacer4_2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Fixed</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="KDoubleSpinBox"> + <property name="name"> + <cstring>mWidth</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="maxValue"> + <number>1e+06</number> + </property> + <property name="lineStep"> + <number>1</number> + </property> + <property name="acceptLocalizedNumbers"> + <bool>false</bool> + </property> + <property name="precision"> + <number>2</number> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel2</cstring> + </property> + <property name="text"> + <string>x</string> + </property> + </widget> + <widget class="KDoubleSpinBox"> + <property name="name"> + <cstring>mHeight</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="maxValue"> + <number>1e+06</number> + </property> + <property name="lineStep"> + <number>1</number> + </property> + <property name="acceptLocalizedNumbers"> + <bool>false</bool> + </property> + <property name="precision"> + <number>2</number> + </property> + </widget> + <widget class="KComboBox"> + <item> + <property name="text"> + <string>Millimeters</string> + </property> + </item> + <item> + <property name="text"> + <string>Centimeters</string> + </property> + </item> + <item> + <property name="text"> + <string>Inches</string> + </property> + </item> + <property name="name"> + <cstring>mUnit</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + <widget class="QCheckBox"> + <property name="name"> + <cstring>mKeepRatio</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Keep ratio</string> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>16</width> + <height>20</height> + </size> + </property> + </spacer> + </hbox> + </widget> + </vbox> + </widget> + </vbox> +</widget> +<customwidgets> +</customwidgets> +<connections> + <connection> + <sender>mScale</sender> + <signal>toggled(bool)</signal> + <receiver>mUnit</receiver> + <slot>setEnabled(bool)</slot> + </connection> + <connection> + <sender>mScale</sender> + <signal>toggled(bool)</signal> + <receiver>mKeepRatio</receiver> + <slot>setEnabled(bool)</slot> + </connection> + <connection> + <sender>mFitToPage</sender> + <signal>toggled(bool)</signal> + <receiver>mEnlargeToFit</receiver> + <slot>setEnabled(bool)</slot> + </connection> + <connection> + <sender>mScale</sender> + <signal>toggled(bool)</signal> + <receiver>mWidth</receiver> + <slot>setEnabled(bool)</slot> + </connection> + <connection> + <sender>mScale</sender> + <signal>toggled(bool)</signal> + <receiver>mHeight</receiver> + <slot>setEnabled(bool)</slot> + </connection> +</connections> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kcombobox.h</includehint> + <includehint>knuminput.h</includehint> + <includehint>knuminput.h</includehint> + <includehint>kcombobox.h</includehint> +</includehints> +</UI> diff --git a/src/gvcore/qxcfi.cpp b/src/gvcore/qxcfi.cpp new file mode 100644 index 0000000..2a771bc --- /dev/null +++ b/src/gvcore/qxcfi.cpp @@ -0,0 +1,2405 @@ +/* + * qxcfi.cpp: A Qt 3 plug-in for reading GIMP XCF image files + * Copyright (C) 2001 lignum Computing, Inc. <allen@lignumcomputing.com> + * $Id: qxcfi.cpp 531593 2006-04-19 15:46:52Z gateau $ + * + * This plug-in is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include <qiodevice.h> +#include <kdeversion.h> +#include <stdlib.h> +#include "qxcfi.h" + + +// Change a QRgb value's alpha only. (an optimization) +inline QRgb qRgba ( QRgb rgb, int a ) +{ + return ( ( a & 0xff ) << 24 | ( rgb & RGB_MASK ) ); +} + + +namespace Gwenview { + +int SafeDataStream::at() const { + return mDevice->at(); +} + +const float INCHESPERMETER = (100. / 2.54); + +// Static global values + +int XCFImageFormat::random_table[RANDOM_TABLE_SIZE]; + +int XCFImageFormat::add_lut[256][256]; + +XCFImageFormat::LayerModes XCFImageFormat::layer_modes[] = { + { true }, // NORMAL_MODE + { true }, // DISSOLVE_MODE + { true }, // BEHIND_MODE + { false }, // MULTIPLY_MODE + { false }, // SCREEN_MODE + { false }, // OVERLAY_MODE + { false }, // DIFFERENCE_MODE + { false }, // ADDITION_MODE + { false }, // SUBTRACT_MODE + { false }, // DARKEN_ONLY_MODE + { false }, // LIGHTEN_ONLY_MODE + { false }, // HUE_MODE + { false }, // SATURATION_MODE + { false }, // COLOR_MODE + { false }, // VALUE_MODE + { false }, // DIVIDE_MODE + { true }, // ERASE_MODE + { true }, // REPLACE_MODE + { true }, // ANTI_ERASE_MODE +}; + +////////////////////////////////////////////////////////////////////////////////// +// From GIMP "paint_funcs.c" v1.2 + +/*! + * Multiply two color components. Really expects the arguments to be + * 8-bit quantities. + * \param a first minuend. + * \param b second minuend. + * \return product of arguments. + */ +inline int INT_MULT ( int a, int b ) +{ + int c = a * b + 0x80; + return ( ( c >> 8 ) + c ) >> 8; +} + +/*! + * Blend the two color components in the proportion alpha: + * + * result = alpha a + ( 1 - alpha b) + * + * \param a first component. + * \param b second component. + * \param alpha blend proportion. + * \return blended color components. + */ + +inline int INT_BLEND ( int a, int b, int alpha ) +{ + return INT_MULT( a - b, alpha ) + b; +} + +// Actually from GLIB + +inline int MIN ( int a, int b ) +{ + return ( a < b ? a : b ); +} + +inline int MAX ( int a, int b ) +{ + return ( a > b ? a : b ); +} + +// From GIMP "gimpcolorspace.c" v1.2 + +/*! + * Convert a color in RGB space to HSV space (Hue, Saturation, Value). + * \param red the red component (modified in place). + * \param green the green component (modified in place). + * \param blue the blue component (modified in place). + */ +void RGBTOHSV ( uchar& red, uchar& green, uchar& blue ) +{ + int r, g, b; + double h, s, v; + int min, max; + + h = 0.; + + r = red; + g = green; + b = blue; + + if ( r > g ) { + max = MAX( r, b ); + min = MIN( g, b ); + } + else { + max = MAX( g, b ); + min = MIN( r, b ); + } + + v = max; + + if ( max != 0 ) + s = ( ( max - min ) * 255 ) / (double)max; + else + s = 0; + + if ( s == 0 ) + h = 0; + else { + int delta = max - min; + if ( r == max ) + h = ( g - b ) / (double)delta; + else if ( g == max ) + h = 2 + ( b - r ) / (double)delta; + else if ( b == max ) + h = 4 + ( r - g ) / (double)delta; + h *= 42.5; + + if ( h < 0 ) + h += 255; + if ( h > 255 ) + h -= 255; + } + + red = (uchar)h; + green = (uchar)s; + blue = (uchar)v; +} + +/*! + * Convert a color in HSV space to RGB space. + * \param hue the hue component (modified in place). + * \param saturation the saturation component (modified in place). + * \param value the value component (modified in place). + */ +void HSVTORGB ( uchar& hue, uchar& saturation, uchar& value ) +{ + if ( saturation == 0 ) { + hue = value; + saturation = value; + value = value; + } + else { + double h = hue * 6. / 255.; + double s = saturation / 255.; + double v = value / 255.; + + double f = h - (int)h; + double p = v * ( 1. - s ); + double q = v * ( 1. - ( s * f ) ); + double t = v * ( 1. - ( s * ( 1. - f ) ) ); + + // Worth a note here that gcc 2.96 will generate different results + // depending on optimization mode on i386. + + switch ((int)h) { + case 0: + hue = (uchar)( v * 255 ); + saturation = (uchar)( t * 255 ); + value = (uchar)( p * 255 ); + break; + case 1: + hue = (uchar)( q * 255 ); + saturation = (uchar)( v * 255 ); + value = (uchar)( p * 255 ); + break; + case 2: + hue = (uchar)( p * 255 ); + saturation = (uchar)( v * 255 ); + value = (uchar)( t * 255 ); + break; + case 3: + hue = (uchar)( p * 255 ); + saturation = (uchar)( q * 255 ); + value = (uchar)( v * 255 ); + break; + case 4: + hue = (uchar)( t * 255 ); + saturation = (uchar)( p * 255 ); + value = (uchar)( v * 255 ); + break; + case 5: + hue = (uchar)( v * 255 ); + saturation = (uchar)( p * 255 ); + value = (uchar)( q * 255 ); + } + } +} + +/*! + * Convert a color in RGB space to HLS space (Hue, Lightness, Saturation). + * \param red the red component (modified in place). + * \param green the green component (modified in place). + * \param blue the blue component (modified in place). + */ +void RGBTOHLS ( uchar& red, uchar& green, uchar& blue ) +{ + int r = red; + int g = green; + int b = blue; + + int min, max; + + if ( r > g ) { + max = MAX( r, b ); + min = MIN( g, b ); + } + else { + max = MAX( g, b ); + min = MIN( r, b ); + } + + double h; + double l = ( max + min ) / 2.; + double s; + + if ( max == min ) { + s = 0.; + h = 0.; + } + else { + int delta = max - min; + + if ( l < 128 ) + s = 255 * (double)delta / (double)( max + min ); + else + s = 255 * (double)delta / (double)( 511 - max - min ); + + if ( r == max ) + h = ( g - b ) / (double)delta; + else if ( g == max ) + h = 2 + ( b - r ) / (double)delta; + else + h = 4 + ( r - g ) / (double)delta; + + h *= 42.5; + + if ( h < 0 ) + h += 255; + else if ( h > 255 ) + h -= 255; + } + + red = (uchar)h; + green = (uchar)l; + blue = (uchar)s; +} + +/*! + * Implement the HLS "double hex-cone". + * \param n1 lightness fraction (?) + * \param n2 saturation fraction (?) + * \param hue hue "angle". + * \return HLS value. + */ +int HLSVALUE ( double n1, double n2, double hue ) +{ + double value; + + if ( hue > 255 ) + hue -= 255; + else if ( hue < 0 ) + hue += 255; + + if ( hue < 42.5 ) + value = n1 + ( n2 - n1 ) * ( hue / 42.5 ); + else if ( hue < 127.5 ) + value = n2; + else if ( hue < 170 ) + value = n1 + ( n2 - n1 ) * ( ( 170 - hue ) / 42.5 ); + else + value = n1; + + return (int)( value * 255 ); +} + +/*! + * Convert a color in HLS space to RGB space. + * \param hue the hue component (modified in place). + * \param lightness the lightness component (modified in place). + * \param saturation the saturation component (modified in place). + */ +void HLSTORGB ( uchar& hue, uchar& lightness, uchar& saturation ) +{ + double h = hue; + double l = lightness; + double s = saturation; + + if ( s == 0 ) { + hue = (uchar)l; + lightness = (uchar)l; + saturation = (uchar)l; + } + else { + double m1, m2; + + if ( l < 128 ) + m2 = ( l * ( 255 + s ) ) / 65025.; + else + m2 = ( l + s - ( l * s ) / 255. ) / 255.; + + m1 = ( l / 127.5 ) - m2; + + hue = HLSVALUE( m1, m2, h + 85 ); + lightness = HLSVALUE( m1, m2, h ); + saturation = HLSVALUE( m1, m2, h - 85 ); + } +} +////////////////////////////////////////////////////////////////////////////////// + + +XCFImageFormat::XCFImageFormat() { + // From GIMP "paint_funcs.c" v1.2 + srand( RANDOM_SEED ); + + for ( int i = 0; i < RANDOM_TABLE_SIZE; i++ ) + random_table[i] = rand(); + + for ( int i = 0; i < RANDOM_TABLE_SIZE; i++ ) { + int tmp; + int swap = i + rand() % ( RANDOM_TABLE_SIZE - i ); + tmp = random_table[i]; + random_table[i] = random_table[swap]; + random_table[swap] = tmp; + } + + for ( int j = 0; j < 256; j++ ) { + for ( int k = 0; k < 256; k++ ) { + int tmp_sum = j + k; + if ( tmp_sum > 255 ) + tmp_sum = 255; + add_lut[j][k] = tmp_sum; + } + } + } + + +bool XCFImageFormat::installIOHandler ( const QString& ) { + QImageIO::defineIOHandler( "XCF", "gimp xcf", 0, + &XCFImageFormat::readXCF, +#ifdef TMP_WRITE + &XCFImageFormat::writeXCF ); +#else + 0 ); +#endif + return true; +} + + +void XCFImageFormat::registerFormat() { + QImageIO::defineIOHandler( "XCF","^gimp xcf", + 0,XCFImageFormat::readXCF,0L); +} + + +/*! + * The Qt QImageIO architecture invokes this routine to read the image. + * The file (or other data stream) is already open and the + * initial string indicating a XCF file has been matched (but the stream + * is positioned at its beginning). + * + * The XCF file is binary and is stored in big endian format. The + * SafeDataStream class is used to read the file. Even though the XCF file + * was not written with SafeDataStream, there is still a good match. At least + * in version 001 of XCF and version 4 of SafeDataStream. Any other combination + * is suspect. + * + * Any failures while reading the XCF image are reported by the + * QImage::status() method. + * + * \param image_io the QImageIO object connected to the XCF image. + */ +void XCFImageFormat::readXCF ( QImageIO* image_io ) +{ + XCFImage xcf_image; + + // The XCF data is stored in big endian format, which SafeDataStream handles + // very well. + + SafeDataStream xcf_io( image_io->ioDevice() ); + + char tag[14]; + xcf_io.readRawBytes( tag, sizeof(tag) ); + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on header tag" ); + return; + } + + xcf_io >> xcf_image.width >> xcf_image.height >> xcf_image.type; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on image info" ); + return; + } + + if ( !loadImageProperties( xcf_io, xcf_image ) ) return; + + // The layers appear to be stored in top-to-bottom order. This is + // the reverse of how a merged image must be computed. So, the layer + // offsets are pushed onto a LIFO stack (thus, we don't have to load + // all the data of all layers before beginning to construct the + // merged image). + + QValueStack< Q_INT32 > layer_offsets; + + while ( true ) { + Q_INT32 layer_offset; + + xcf_io >> layer_offset; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on layer offsets" ); + return; + } + + if ( layer_offset == 0 ) break; + + layer_offsets.push( layer_offset ); + } + + xcf_image.num_layers = layer_offsets.size(); + + if ( layer_offsets.size() == 0 ) { + qDebug( "XCF: no layers!" ); + return; + } + + // Load each layer and add it to the image + + while ( !layer_offsets.isEmpty() ) { + Q_INT32 layer_offset = layer_offsets.pop(); + + xcf_io.device()->at( layer_offset ); + + if ( !loadLayer( xcf_io, xcf_image ) ) return; + } + + if ( !xcf_image.initialized ) { + qDebug( "XCF: no visible layers!" ); + return; + } + + image_io->setImage( xcf_image.image ); + image_io->setStatus( 0 ); +} + +/*! + * Construct the QImage which will eventually be returned to the QImage + * loader. + * + * There are a couple of situations which require that the QImage is not + * exactly the same as The GIMP's representation. The full table is: + * \verbatim + * Grayscale opaque : 8 bpp indexed + * Grayscale translucent : 32 bpp + alpha + * Indexed opaque : 1 bpp if num_colors <= 2 + * : 8 bpp indexed otherwise + * Indexed translucent : 8 bpp indexed + alpha if num_colors < 256 + * : 32 bpp + alpha otherwise + * RGB opaque : 32 bpp + * RGBA translucent : 32 bpp + alpha + * \endverbatim + * Whether the image is translucent or not is determined by the bottom layer's + * alpha channel. However, even if the bottom layer lacks an alpha channel, + * it can still have an opacity < 1. In this case, the QImage is promoted + * to 32-bit. (Note this is different from the output from the GIMP image + * exporter, which seems to ignore this attribute.) + * + * Independently, higher layers can be translucent, but the background of + * the image will not show through if the bottom layer is opaque. + * + * For indexed images, translucency is an all or nothing effect. + * \param xcf_image contains image info and bottom-most layer. + */ +void XCFImageFormat::initializeImage ( XCFImage& xcf_image ) +{ + // (Aliases to make the code look a little better.) + Layer& layer( xcf_image.layer ); + QImage& image( xcf_image.image ); + + switch ( layer.type ) { + case RGB_GIMAGE: + if ( layer.opacity == OPAQUE_OPACITY ) { + image.create( xcf_image.width, xcf_image.height, 32 ); + image.fill( qRgb( 255, 255, 255 ) ); + break; + } // else, fall through to 32-bit representation + + case RGBA_GIMAGE: + image.create( xcf_image.width, xcf_image.height, 32 ); + image.fill( qRgba( 255, 255, 255, 0 ) ); + // Turning this on prevents fill() from affecting the alpha channel, + // by the way. + image.setAlphaBuffer( true ); + break; + + case GRAY_GIMAGE: + if ( layer.opacity == OPAQUE_OPACITY ) { + image.create( xcf_image.width, xcf_image.height, 8, 256 ); + setGrayPalette( image ); + image.fill( 255 ); + break; + } // else, fall through to 32-bit representation + + case GRAYA_GIMAGE: + image.create( xcf_image.width, xcf_image.height, 32 ); + image.fill( qRgba( 255, 255, 255, 0 ) ); + image.setAlphaBuffer( true ); + break; + + case INDEXED_GIMAGE: + // As noted in the table above, there are quite a few combinations + // which are possible with indexed images, depending on the + // presence of transparency (note: not translucency, which is not + // supported by The GIMP for indexed images) and the number of + // individual colors. + + // Note: Qt treats a bitmap with a Black and White color palette + // as a mask, so only the "on" bits are drawn, regardless of the + // order color table entries. Otherwise (i.e., at least one of the + // color table entries is not black or white), it obeys the one- + // or two-color palette. Have to ask about this... + + if ( xcf_image.num_colors <= 2 ) { + image.create( xcf_image.width, xcf_image.height, + 1, xcf_image.num_colors, + QImage::LittleEndian ); + image.fill( 0 ); + setPalette( xcf_image, image ); + } + + else if ( xcf_image.num_colors <= 256 ) { + image.create( xcf_image.width, xcf_image.height, + 8, xcf_image.num_colors, + QImage::LittleEndian ); + image.fill( 0 ); + setPalette( xcf_image, image ); + } + + break; + + case INDEXEDA_GIMAGE: + + if ( xcf_image.num_colors == 1 ) { + + // Plenty(!) of room to add a transparent color + + xcf_image.num_colors++; + xcf_image.palette.resize( xcf_image.num_colors ); + xcf_image.palette[1] = xcf_image.palette[0]; + xcf_image.palette[0] = qRgba( 255, 255, 255, 0 ); + + image.create( xcf_image.width, xcf_image.height, + 1, xcf_image.num_colors, + QImage::LittleEndian ); + image.fill( 0 ); + setPalette( xcf_image, image ); + image.setAlphaBuffer( true ); + } + + else if ( xcf_image.num_colors < 256 ) { + + // Plenty of room to add a transparent color + + xcf_image.num_colors++; + xcf_image.palette.resize( xcf_image.num_colors ); + for ( int c = xcf_image.num_colors-1; c >= 1; c-- ) + xcf_image.palette[c] = xcf_image.palette[c-1]; + xcf_image.palette[0] = qRgba( 255, 255, 255, 0 ); + + image.create( xcf_image.width, xcf_image.height, + 8, xcf_image.num_colors ); + image.fill( 0 ); + setPalette( xcf_image, image ); + image.setAlphaBuffer( true ); + } + + else { + // No room for a transparent color, so this has to be promoted to + // true color. (There is no equivalent PNG representation output + // from The GIMP as of v1.2.) + image.create( xcf_image.width, xcf_image.height, 32 ); + image.fill( qRgba( 255, 255, 255, 0 ) ); + image.setAlphaBuffer( true ); + } + + break; + } + + image.setDotsPerMeterX( (int)( xcf_image.x_resolution * INCHESPERMETER ) ); + image.setDotsPerMeterY( (int)( xcf_image.y_resolution * INCHESPERMETER ) ); +} + +/*! + * Compute the number of tiles in the current layer and allocate + * QImage structures for each of them. + * \param xcf_image contains the current layer. + */ +void XCFImageFormat::composeTiles ( XCFImage& xcf_image ) +{ + Layer& layer( xcf_image.layer ); + + layer.nrows = ( layer.height + TILE_HEIGHT - 1 ) / TILE_HEIGHT; + layer.ncols = ( layer.width + TILE_WIDTH - 1 ) / TILE_WIDTH; + + layer.image_tiles.resize( layer.nrows ); + + if ( layer.type == GRAYA_GIMAGE || layer.type == INDEXEDA_GIMAGE ) + layer.alpha_tiles.resize( layer.nrows ); + + if ( layer.mask_offset != 0 ) + layer.mask_tiles.resize( layer.nrows ); + + for ( uint j = 0; j < layer.nrows; j++ ) { + layer.image_tiles[j].resize( layer.ncols ); + + if ( layer.type == GRAYA_GIMAGE || layer.type == INDEXEDA_GIMAGE ) + layer.alpha_tiles[j].resize( layer.ncols ); + + if ( layer.mask_offset != 0 ) + layer.mask_tiles[j].resize( layer.ncols ); + } + + for ( uint j = 0; j < layer.nrows; j++ ) { + for ( uint i = 0; i < layer.ncols; i++ ) { + + uint tile_width = (i+1) * TILE_WIDTH <= layer.width ? + TILE_WIDTH : layer.width - i*TILE_WIDTH; + + uint tile_height = (j+1) * TILE_HEIGHT <= layer.height ? + TILE_HEIGHT : layer.height - j*TILE_HEIGHT; + + // Try to create the most appropriate QImage (each GIMP layer + // type is treated slightly differently) + + switch ( layer.type ) { + case RGB_GIMAGE: + layer.image_tiles[j][i] = QImage( tile_width, tile_height, 32, 0 ); + layer.image_tiles[j][i].setAlphaBuffer( false ); + break; + + case RGBA_GIMAGE: + layer.image_tiles[j][i] = QImage( tile_width, tile_height, 32, 0 ); + layer.image_tiles[j][i].setAlphaBuffer( true ); + break; + + case GRAY_GIMAGE: + layer.image_tiles[j][i] = QImage( tile_width, tile_height, 8, 256 ); + setGrayPalette( layer.image_tiles[j][i] ); + break; + + case GRAYA_GIMAGE: + layer.image_tiles[j][i] = QImage( tile_width, tile_height, 8, 256 ); + setGrayPalette( layer.image_tiles[j][i] ); + + layer.alpha_tiles[j][i] = QImage( tile_width, tile_height, 8, 256 ); + setGrayPalette( layer.alpha_tiles[j][i] ); + break; + + case INDEXED_GIMAGE: + layer.image_tiles[j][i] = QImage( tile_width, tile_height, 8, + xcf_image.num_colors ); + setPalette( xcf_image, layer.image_tiles[j][i] ); + break; + + case INDEXEDA_GIMAGE: + layer.image_tiles[j][i] = QImage( tile_width, tile_height, 8, + xcf_image.num_colors ); + setPalette( xcf_image, layer.image_tiles[j][i] ); + + layer.alpha_tiles[j][i] = QImage( tile_width, tile_height, 8, 256 ); + setGrayPalette( layer.alpha_tiles[j][i] ); + } + + if ( layer.mask_offset != 0 ) { + layer.mask_tiles[j][i] = QImage( tile_width, tile_height, 8, 256 ); + setGrayPalette( layer.mask_tiles[j][i] ); + } + } + } +} + +/*! + * Apply a grayscale palette to the QImage. Note that Qt does not distinguish + * between grayscale and indexed images. A grayscale image is just + * an indexed image with a 256-color, grayscale palette. + * \param image image to set to a grayscale palette. + */ +void XCFImageFormat::setGrayPalette ( QImage& image ) +{ + for ( int i = 0; i < 256; i++ ) + image.setColor( i, qRgb(i,i,i) ); +} + +/*! + * Copy the indexed palette from the XCF image into the QImage. + * \param xcf_image XCF image containing the palette read from the data stream. + * \param image image to apply the palette to. + */ +void XCFImageFormat::setPalette ( XCFImage& xcf_image, QImage& image ) +{ + for ( int i = 0; i < xcf_image.num_colors; i++ ) + image.setColor( i, xcf_image.palette[i] ); +} + +/*! + * An XCF file can contain an arbitrary number of properties associated + * with the image (and layer and mask). + * \param xcf_io the data stream connected to the XCF image + * \param xcf_image XCF image data. + * \return true if there were no I/O errors. + */ +bool XCFImageFormat::loadImageProperties ( SafeDataStream& xcf_io, + XCFImage& xcf_image ) +{ + while ( true ) { + PropType type; + QByteArray bytes; + + if ( !loadProperty( xcf_io, type, bytes ) ) { + qDebug( "XCF: error loading global image properties" ); + return false; + } + + QDataStream property( bytes, IO_ReadOnly ); + + switch ( type ) { + case PROP_END: + return true; + + case PROP_COMPRESSION: + property >> xcf_image.compression; + break; + + case PROP_GUIDES: + // This property is ignored. + break; + + case PROP_RESOLUTION: + property >> xcf_image.x_resolution >> xcf_image.y_resolution; + break; + + case PROP_TATTOO: + property >> xcf_image.tattoo; + break; + + case PROP_PARASITES: + while ( !property.atEnd() ) { + char* tag; + Q_UINT32 size; + + property.readBytes( tag, size ); + + Q_UINT32 flags; + char* data; + property >> flags >> data; + + if ( strcmp( tag, "gimp-comment" ) == 0 ) + xcf_image.image.setText( "Comment", 0, data ); + + delete[] tag; + delete[] data; + } + break; + + case PROP_UNIT: + property >> xcf_image.unit; + break; + + case PROP_PATHS: + // This property is ignored. + break; + + case PROP_USER_UNIT: + // This property is ignored. + break; + + case PROP_COLORMAP: + property >> xcf_image.num_colors; + + xcf_image.palette.reserve( xcf_image.num_colors ); + + for ( int i = 0; i < xcf_image.num_colors; i++ ) { + uchar r, g, b; + property >> r >> g >> b; + xcf_image.palette.push_back( qRgb(r,g,b) ); + } + break; + + default: + qDebug( "XCF: unimplemented image property %d, size %d", type, bytes.size() ); + } + } +} + +/*! + * Load a layer from the XCF file. The data stream must be positioned at + * the beginning of the layer data. + * \param xcf_io the image file data stream. + * \param xcf_image contains the layer and the color table + * (if the image is indexed). + * \return true if there were no I/O errors. + */ +bool XCFImageFormat::loadLayer ( SafeDataStream& xcf_io, XCFImage& xcf_image ) +{ + Layer& layer( xcf_image.layer ); + + if ( layer.name != 0 ) delete[] layer.name; + + xcf_io >> layer.width >> layer.height >> layer.type >> layer.name; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on layer" ); + return false; + } + + if ( !loadLayerProperties( xcf_io, layer ) ) return false; +#if 0 + cout << "layer: \"" << layer.name << "\", size: " << layer.width << " x " + << layer.height << ", type: " << layer.type << ", mode: " << layer.mode + << ", opacity: " << layer.opacity << ", visible: " << layer.visible + << ", offset: " << layer.x_offset << ", " << layer.y_offset << endl; +#endif + // Skip reading the rest of it if it is not visible. Typically, when + // you export an image from the The GIMP it flattens (or merges) only + // the visible layers into the output image. + + if ( layer.visible == 0 ) return true; + + // If there are any more layers, merge them into the final QImage. + + xcf_io >> layer.hierarchy_offset >> layer.mask_offset; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on layer image offsets" ); + return false; + } + + // Allocate the individual tile QImages based on the size and type + // of this layer. + + composeTiles( xcf_image ); + + xcf_io.device()->at( layer.hierarchy_offset ); + + // As tiles are loaded, they are copied into the layers tiles by + // this routine. (loadMask(), below, uses a slightly different + // version of assignBytes().) + + layer.assignBytes = assignImageBytes; + + if ( !loadHierarchy( xcf_io, layer ) ) return false; + + if ( layer.mask_offset != 0 ) { + xcf_io.device()->at( layer.mask_offset ); + + if ( !loadMask( xcf_io, layer ) ) return false; + } + + // Now we should have enough information to initialize the final + // QImage. The first visible layer determines the attributes + // of the QImage. + + if ( !xcf_image.initialized ) { + initializeImage( xcf_image ); + + copyLayerToImage( xcf_image ); + + xcf_image.initialized = true; + } + else + mergeLayerIntoImage( xcf_image ); + + return true; +} + +/*! + * An XCF file can contain an arbitrary number of properties associated + * with a layer. + * \param xcf_io the data stream connected to the XCF image. + * \param layer layer to collect the properties. + * \return true if there were no I/O errors. + */ +bool XCFImageFormat::loadLayerProperties ( SafeDataStream& xcf_io, Layer& layer ) +{ + while ( true ) { + PropType type; + QByteArray bytes; + + if ( !loadProperty( xcf_io, type, bytes ) ) { + qDebug( "XCF: error loading layer properties" ); + return false; + } + + QDataStream property( bytes, IO_ReadOnly ); + + switch ( type ) { + case PROP_END: + return true; + + case PROP_ACTIVE_LAYER: + layer.active = true; + break; + + case PROP_OPACITY: + property >> layer.opacity; + break; + + case PROP_VISIBLE: + property >> layer.visible; + break; + + case PROP_LINKED: + property >> layer.linked; + break; + + case PROP_PRESERVE_TRANSPARENCY: + property >> layer.preserve_transparency; + break; + + case PROP_APPLY_MASK: + property >> layer.apply_mask; + break; + + case PROP_EDIT_MASK: + property >> layer.edit_mask; + break; + + case PROP_SHOW_MASK: + property >> layer.show_mask; + break; + + case PROP_OFFSETS: + property >> layer.x_offset >> layer.y_offset; + break; + + case PROP_MODE: + property >> layer.mode; + break; + + case PROP_TATTOO: + property >> layer.tattoo; + break; + + default: + qDebug( "XCF: unimplemented layer property %d, size %d", type, bytes.size() ); + } + } +} + +/*! + * An XCF file can contain an arbitrary number of properties associated + * with a channel. Note that this routine only reads mask channel properties. + * \param xcf_io the data stream connected to the XCF image. + * \param layer layer containing the mask channel to collect the properties. + * \return true if there were no I/O errors. + */ +bool XCFImageFormat::loadChannelProperties ( SafeDataStream& xcf_io, Layer& layer ) +{ + while ( true ) { + PropType type; + QByteArray bytes; + + if ( !loadProperty( xcf_io, type, bytes ) ) { + qDebug( "XCF: error loading channel properties" ); + return false; + } + + QDataStream property( bytes, IO_ReadOnly ); + + switch ( type ) { + case PROP_END: + return true; + + case PROP_OPACITY: + property >> layer.mask_channel.opacity; + break; + + case PROP_VISIBLE: + property >> layer.mask_channel.visible; + break; + + case PROP_SHOW_MASKED: + property >> layer.mask_channel.show_masked; + break; + + case PROP_COLOR: + property >> layer.mask_channel.red >> layer.mask_channel.green + >> layer.mask_channel.blue; + break; + + case PROP_TATTOO: + property >> layer.mask_channel.tattoo; + break; + + default: + qDebug( "XCF: unimplemented channel property %d, size %d", type, bytes.size() ); + } + } +} + +/*! + * The GIMP stores images in a "mipmap"-like hierarchy. As far as the QImage + * is concerned, however, only the top level (i.e., the full resolution image) + * is used. + * \param xcf_io the data stream connected to the XCF image. + * \param layer the layer to collect the image. + * \return true if there were no I/O errors. + */ +bool XCFImageFormat::loadHierarchy ( SafeDataStream& xcf_io, Layer& layer ) +{ + Q_INT32 width; + Q_INT32 height; + Q_INT32 bpp; + Q_UINT32 offset; + + xcf_io >> width >> height >> bpp >> offset; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on layer %s image header", layer.name ); + return false; + } + + // GIMP stores images in a "mipmap"-like format (multiple levels of + // increasingly lower resolution). Only the top level is used here, + // however. + + Q_UINT32 junk; + do { + xcf_io >> junk; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on layer %s level offsets", layer.name ); + return false; + } + } while ( junk != 0 ); + + QIODevice::Offset saved_pos = xcf_io.device()->at(); + + xcf_io.device()->at( offset ); + + if ( !loadLevel( xcf_io, layer, bpp ) ) return false; + + xcf_io.device()->at( saved_pos ); + + return true; +} + +/*! + * Load one level of the image hierarchy (but only the top level is ever used). + * \param xcf_io the data stream connected to the XCF image. + * \param layer the layer to collect the image. + * \param bpp the number of bytes in a pixel. + * \return true if there were no I/O errors. + * \sa loadTileRLE(). + */ +bool XCFImageFormat::loadLevel ( SafeDataStream& xcf_io, Layer& layer, Q_INT32 bpp ) +{ + Q_INT32 width; + Q_INT32 height; + Q_UINT32 offset; + + xcf_io >> width >> height >> offset; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on layer %s level info", layer.name ); + return false; + } + + if ( offset == 0 ) return true; + + for ( uint j = 0; j < layer.nrows; j++ ) { + for ( uint i = 0; i < layer.ncols; i++ ) { + + if ( offset == 0 ) { + qDebug( "XCF: incorrect number of tiles in layer %s", layer.name ); + return false; + } + + QIODevice::Offset saved_pos = xcf_io.device()->at(); + + Q_UINT32 offset2; + + xcf_io >> offset2; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on layer %s level offset look-ahead", + layer.name ); + return false; + } + + // Evidently, RLE can occasionally expand a tile instead of compressing it! + + if ( offset2 == 0 ) + offset2 = offset + (uint)( TILE_WIDTH * TILE_HEIGHT * 4 * 1.5 ); + + xcf_io.device()->at( offset ); + + int size = layer.image_tiles[j][i].width() * layer.image_tiles[j][i].height(); + + if ( !loadTileRLE( xcf_io, layer.tile, size, offset2 - offset, bpp ) ) + return false; + + // The bytes in the layer tile are juggled differently depending on + // the target QImage. The caller has set layer.assignBytes to the + // appropriate routine. + + layer.assignBytes( layer, i, j ); + + xcf_io.device()->at( saved_pos ); + + xcf_io >> offset; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on layer %s level offset", layer.name ); + return false; + } + } + } + + return true; +} + +/*! + * A layer can have a one channel image which is used as a mask. + * \param xcf_io the data stream connected to the XCF image. + * \param layer the layer to collect the mask image. + * \return true if there were no I/O errors. + */ +bool XCFImageFormat::loadMask ( SafeDataStream& xcf_io, Layer& layer ) +{ + Q_INT32 width; + Q_INT32 height; + char* name; + + xcf_io >> width >> height >> name; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on mask info" ); + return false; + } + + delete name; + + if ( !loadChannelProperties( xcf_io, layer ) ) return false; + + Q_UINT32 hierarchy_offset; + + xcf_io >> hierarchy_offset; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on mask image offset" ); + return false; + } + + xcf_io.device()->at( hierarchy_offset ); + + layer.assignBytes = assignMaskBytes; + + if ( !loadHierarchy( xcf_io, layer ) ) return false; + + return true; +} + +/*! + * This is the routine for which all the other code is simply + * infrastructure. Read the image bytes out of the file and + * store them in the tile buffer. This is passed a full 32-bit deep + * buffer, even if bpp is smaller. The caller can figure out what to + * do with the bytes. + * + * The tile is stored in "channels", i.e. the red component of all + * pixels, then the green component of all pixels, then blue then + * alpha, or, for indexed images, the color indices of all pixels then + * the alpha of all pixels. + * + * The data is compressed with "run length encoding". Some simple data + * integrity checks are made. + * + * \param xcf_io the data stream connected to the XCF image. + * \param tile the buffer to expand the RLE into. + * \param image_size number of bytes expected to be in the image tile. + * \param data_length number of bytes expected in the RLE. + * \param bpp number of bytes per pixel. + * \return true if there were no I/O errors and no obvious corruption of + * the RLE data. + */ +bool XCFImageFormat::loadTileRLE ( SafeDataStream& xcf_io, uchar* tile, int image_size, + int data_length, Q_INT32 bpp ) +{ + uchar* data; + + uchar* xcfdata; + uchar* xcfodata; + uchar* xcfdatalimit; + + xcfdata = xcfodata = new uchar[data_length]; + + int read_length=xcf_io.device()->readBlock( (char*)xcfdata, data_length ); + + if ( read_length<=0 ) { + delete[] xcfodata; + qDebug( "XCF: read failure on tile" ); + return false; + } + + xcfdatalimit = &xcfodata[read_length-1]; + + for ( int i = 0; i < bpp; ++i ) { + + data = tile + i; + + int count = 0; + int size = image_size; + + while ( size > 0 ) { + if ( xcfdata > xcfdatalimit ) + goto bogus_rle; + + uchar val = *xcfdata++; + + uint length = val; + + if ( length >= 128 ) { + length = 255 - ( length - 1 ); + if ( length == 128 ) { + if ( xcfdata >= xcfdatalimit ) + goto bogus_rle; + + length = ( *xcfdata << 8 ) + xcfdata[1]; + + xcfdata += 2; + } + + count += length; + size -= length; + + if ( size < 0 ) + goto bogus_rle; + + if ( &xcfdata[length-1] > xcfdatalimit ) + goto bogus_rle; + + while ( length-- > 0 ) { + *data = *xcfdata++; + data += sizeof(QRgb); + } + } + else { + length += 1; + if ( length == 128 ) { + + if ( xcfdata >= xcfdatalimit ) + goto bogus_rle; + + length = ( *xcfdata << 8 ) + xcfdata[1]; + xcfdata += 2; + } + + count += length; + size -= length; + + if ( size < 0 ) + goto bogus_rle; + + if ( xcfdata > xcfdatalimit ) + goto bogus_rle; + + val = *xcfdata++; + + while ( length-- > 0 ) { + *data = val; + data += sizeof(QRgb); + } + } + } + } + + delete[] xcfodata; + return true; + + bogus_rle: + + qDebug( "The run length encoding could not be decoded properly" ); + delete[] xcfodata; + return false; +} + +/*! + * Copy the bytes from the tile buffer into the image tile QImage, taking into + * account all the myriad different modes. + * \param layer layer containing the tile buffer and the image tile matrix. + * \param i column index of current tile. + * \param j row index of current tile. + */ +void XCFImageFormat::assignImageBytes ( Layer& layer, uint i, uint j ) +{ + uchar* tile = layer.tile; + + switch ( layer.type ) { + case RGB_GIMAGE: + for ( int l = 0; l < layer.image_tiles[j][i].height(); l++ ) { + for ( int k = 0; k < layer.image_tiles[j][i].width(); k++ ) { + layer.image_tiles[j][i].setPixel( k, l, qRgb( tile[0], tile[1], tile[2] ) ); + tile += sizeof(QRgb); + } + } + break; + + case RGBA_GIMAGE: + for ( int l = 0; l < layer.image_tiles[j][i].height(); l++ ) { + for ( int k = 0; k < layer.image_tiles[j][i].width(); k++ ) { + layer.image_tiles[j][i].setPixel( k, l, + qRgba( tile[0], tile[1], tile[2], tile[3] ) ); + tile += sizeof(QRgb); + } + } + break; + + case GRAY_GIMAGE: + case INDEXED_GIMAGE: + for ( int l = 0; l < layer.image_tiles[j][i].height(); l++ ) { + for ( int k = 0; k < layer.image_tiles[j][i].width(); k++ ) { + layer.image_tiles[j][i].setPixel( k, l, tile[0] ); + tile += sizeof(QRgb); + } + } + break; + + case GRAYA_GIMAGE: + case INDEXEDA_GIMAGE: + for ( int l = 0; l < layer.image_tiles[j][i].height(); l++ ) { + for ( int k = 0; k < layer.image_tiles[j][i].width(); k++ ) { + + // The "if" here should not be necessary, but apparently there + // are some cases where the image can contain larger indices + // than there are colors in the palette. (A bug in The GIMP?) + + if ( tile[0] < layer.image_tiles[j][i].numColors() ) + layer.image_tiles[j][i].setPixel( k, l, tile[0] ); + + layer.alpha_tiles[j][i].setPixel( k, l, tile[1] ); + tile += sizeof(QRgb); + } + } + break; + } +} + +/*! + * Copy the bytes from the tile buffer into the mask tile QImage. + * \param layer layer containing the tile buffer and the mask tile matrix. + * \param i column index of current tile. + * \param j row index of current tile. + */ +void XCFImageFormat::assignMaskBytes ( Layer& layer, uint i, uint j ) +{ + uchar* tile = layer.tile; + + for ( int l = 0; l < layer.image_tiles[j][i].height(); l++ ) { + for ( int k = 0; k < layer.image_tiles[j][i].width(); k++ ) { + layer.mask_tiles[j][i].setPixel( k, l, tile[0] ); + tile += sizeof(QRgb); + } + } +} + +/*! + * Read a single property from the image file. The property type is returned + * in type and the data is returned in bytes. + * \param xcf the image file data stream. + * \param type returns with the property type. + * \param bytes returns with the property data. + * \return true if there were no IO errors. */ +bool XCFImageFormat::loadProperty ( SafeDataStream& xcf_io, PropType& type, + QByteArray& bytes ) +{ + Q_UINT32 tmp; + xcf_io >> tmp; + type=static_cast<PropType>(tmp); + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on property type" ); + return false; + } + + char* data; + Q_UINT32 size; + + // The COLORMAP property is tricky: in version of GIMP older than 2.0.2, the + // property size was wrong (it was 4 + ncolors instead of 4 + 3*ncolors). + // This has been fixed in 2.0.2 (*), but the XCF format version has not been + // increased, so we can't rely on the property size. The UINT32 after the + // property size is the number of colors, which has always been correct, so + // we read it, compute the size from it and put it back in the stream. + // + // * See http://bugzilla.gnome.org/show_bug.cgi?id=142149 and + // gimp/app/xcf-save.c, revision 1.42 + if ( type == PROP_COLORMAP ) { + Q_UINT32 ignoredSize, ncolors; + xcf_io >> ignoredSize; + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on property %d size", type ); + return false; + } + + xcf_io >> ncolors; + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on property %d size", type ); + return false; + } + xcf_io.device()->ungetch( ncolors & 0xff); + xcf_io.device()->ungetch( (ncolors>> 8) & 0xff ); + xcf_io.device()->ungetch( (ncolors>>16) & 0xff ); + xcf_io.device()->ungetch( (ncolors>>24) & 0xff ); + + size=4 + 3 * ncolors; + data = new char[size]; + + xcf_io.readRawBytes( data, size ); + } + + // The USER UNIT property size is not correct. I'm not sure why, though. + + else if ( type == PROP_USER_UNIT ) { + float factor; + Q_INT32 digits; + char* unit_strings; + + xcf_io >> size >> factor >> digits; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on property %d", type ); + return false; + } + + for ( int i = 0; i < 5; i++ ) { + xcf_io >> unit_strings; + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on property %d", type ); + return false; + } + + delete[] unit_strings; + } + + size = 0; + } + + else + xcf_io.readBytes( data, size ); + + if ( xcf_io.failed() ) { + qDebug( "XCF: read failure on property %d data, size %d", type, size ); + return false; + } + + if ( size != 0 ) { + bytes.resize( size ); + + for ( uint i = 0; i < size; i++ ) bytes[i] = data[i]; + + delete[] data; + } + + return true; +} + +/*! + * Copy a layer into an image, taking account of the manifold modes. The + * contents of the image are replaced. + * \param xcf_image contains the layer and image to be replaced. + */ +void XCFImageFormat::copyLayerToImage ( XCFImage& xcf_image ) +{ + Layer& layer( xcf_image.layer ); + QImage& image( xcf_image.image ); + + PixelCopyOperation copy = 0; + + switch ( layer.type ) { + case RGB_GIMAGE: + case RGBA_GIMAGE: + copy = copyRGBToRGB; break; + case GRAY_GIMAGE: + if ( layer.opacity == OPAQUE_OPACITY ) + copy = copyGrayToGray; + else + copy = copyGrayToRGB; + break; + case GRAYA_GIMAGE: + copy = copyGrayAToRGB; break; + case INDEXED_GIMAGE: + copy = copyIndexedToIndexed; break; + case INDEXEDA_GIMAGE: + if ( xcf_image.image.depth() <= 8 ) + copy = copyIndexedAToIndexed; + else + copy = copyIndexedAToRGB; + } + + // For each tile... + + for ( uint j = 0; j < layer.nrows; j++ ) { + uint y = j * TILE_HEIGHT; + + for ( uint i = 0; i < layer.ncols; i++ ) { + uint x = i * TILE_WIDTH; + + // This seems the best place to apply the dissolve because it + // depends on the global position of each tile's + // pixels. Apparently it's the only mode which can apply to a + // single layer. + + if ( layer.mode == DISSOLVE_MODE ) { + if ( layer.type == RGBA_GIMAGE ) + dissolveRGBPixels( layer.image_tiles[j][i], x, y ); + + else if ( layer.type == GRAYA_GIMAGE ) + dissolveAlphaPixels( layer.alpha_tiles[j][i], x, y ); + } + + for ( int l = 0; l < layer.image_tiles[j][i].height(); l++ ) { + for ( int k = 0; k < layer.image_tiles[j][i].width(); k++ ) { + + int m = x + k + layer.x_offset; + int n = y + l + layer.y_offset; + + if ( m < 0 || m >= image.width() || n < 0 || n >= image.height() ) + continue; + + (*copy)( layer, i, j, k, l, image, m, n ); + } + } + } + } +} + +/*! + * Copy an RGB pixel from the layer to the RGB image. Straight-forward. + * The only thing this has to take account of is the opacity of the + * layer. Evidently, the GIMP exporter itself does not actually do this. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::copyRGBToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + QRgb src = layer.image_tiles[j][i].pixel( k, l ); + + uchar src_a = layer.opacity; + + if ( layer.type == RGBA_GIMAGE ) + src_a = INT_MULT( src_a, qAlpha( src ) ); + + // Apply the mask (if any) + + if ( layer.apply_mask == 1 && layer.mask_tiles.size() > j && + layer.mask_tiles[j].size() > i ) + src_a = INT_MULT( src_a, layer.mask_tiles[j][i].pixelIndex( k, l ) ); + + image.setPixel( m, n, qRgba( src, src_a ) ); +} + +/*! + * Copy a Gray pixel from the layer to the Gray image. Straight-forward. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::copyGrayToGray ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + int src = layer.image_tiles[j][i].pixelIndex( k, l ); + + image.setPixel( m, n, src ); +} + +/*! + * Copy a Gray pixel from the layer to an RGB image. Straight-forward. + * The only thing this has to take account of is the opacity of the + * layer. Evidently, the GIMP exporter itself does not actually do this. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::copyGrayToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + QRgb src = layer.image_tiles[j][i].pixel( k, l ); + + uchar src_a = layer.opacity; + + image.setPixel( m, n, qRgba( src, src_a ) ); +} + +/*! + * Copy a GrayA pixel from the layer to an RGB image. Straight-forward. + * The only thing this has to take account of is the opacity of the + * layer. Evidently, the GIMP exporter itself does not actually do this. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::copyGrayAToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + QRgb src = layer.image_tiles[j][i].pixel( k, l ); + + uchar src_a = layer.alpha_tiles[j][i].pixelIndex( k, l ); + + src_a = INT_MULT( src_a, layer.opacity ); + + // Apply the mask (if any) + + if ( layer.apply_mask == 1 && layer.mask_tiles.size() > j && + layer.mask_tiles[j].size() > i ) + src_a = INT_MULT( src_a, layer.mask_tiles[j][i].pixelIndex( k, l ) ); + + image.setPixel( m, n, qRgba( src, src_a ) ); +} + +/*! + * Copy an Indexed pixel from the layer to the Indexed image. Straight-forward. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::copyIndexedToIndexed ( Layer& layer, uint i,uint j,int k,int l, + QImage& image, int m, int n ) +{ + int src = layer.image_tiles[j][i].pixelIndex( k, l ); + + image.setPixel( m, n, src ); +} + +/*! + * Copy an IndexedA pixel from the layer to the Indexed image. Straight-forward. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::copyIndexedAToIndexed ( Layer& layer,uint i,uint j,int k,int l, + QImage& image, int m, int n ) +{ + uchar src = layer.image_tiles[j][i].pixelIndex( k, l ); + + uchar src_a = layer.alpha_tiles[j][i].pixelIndex( k, l ); + + src_a = INT_MULT( src_a, layer.opacity ); + + if ( layer.apply_mask == 1 && + layer.mask_tiles.size() > j && + layer.mask_tiles[j].size() > i ) + src_a = INT_MULT( src_a, + layer.mask_tiles[j][i].pixelIndex( k, l ) ); + + if ( src_a > 127 ) + src++; + else + src = 0; + + image.setPixel( m, n, src ); +} + +/*! + * Copy an IndexedA pixel from the layer to an RGB image. Straight-forward. + * The only thing this has to take account of is the opacity of the + * layer. Evidently, the GIMP exporter itself does not actually do this. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::copyIndexedAToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + QRgb src = layer.image_tiles[j][i].pixel( k, l ); + + uchar src_a = layer.alpha_tiles[j][i].pixelIndex( k, l ); + + src_a = INT_MULT( src_a, layer.opacity ); + + // Apply the mask (if any) + + if ( layer.apply_mask == 1 && layer.mask_tiles.size() > j && + layer.mask_tiles[j].size() > i ) + src_a = INT_MULT( src_a, layer.mask_tiles[j][i].pixelIndex( k, l ) ); + + // This is what appears in the GIMP window + + if ( src_a <= 127 ) + src_a = 0; + else + src_a = OPAQUE_OPACITY; + + image.setPixel( m, n, qRgba( src, src_a ) ); +} + +/*! + * Merge a layer into an image, taking account of the manifold modes. + * \param xcf_image contains the layer and image to merge. + */ +void XCFImageFormat::mergeLayerIntoImage ( XCFImage& xcf_image ) +{ + Layer& layer( xcf_image.layer ); + QImage& image( xcf_image.image ); + + PixelMergeOperation merge = 0; + + switch ( layer.type ) { + case RGB_GIMAGE: + case RGBA_GIMAGE: + merge = mergeRGBToRGB; break; + case GRAY_GIMAGE: + if ( layer.opacity == OPAQUE_OPACITY ) + merge = mergeGrayToGray; + else + merge = mergeGrayToRGB; + break; + case GRAYA_GIMAGE: + if ( xcf_image.image.depth() <= 8 ) + merge = mergeGrayAToGray; + else + merge = mergeGrayAToRGB; + break; + case INDEXED_GIMAGE: + merge = mergeIndexedToIndexed; break; + case INDEXEDA_GIMAGE: + if ( xcf_image.image.depth() <= 8 ) + merge = mergeIndexedAToIndexed; + else + merge = mergeIndexedAToRGB; + } + + for ( uint j = 0; j < layer.nrows; j++ ) { + uint y = j * TILE_HEIGHT; + + for ( uint i = 0; i < layer.ncols; i++ ) { + uint x = i * TILE_WIDTH; + + // This seems the best place to apply the dissolve because it + // depends on the global position of each tile's + // pixels. Apparently it's the only mode which can apply to a + // single layer. + + if ( layer.mode == DISSOLVE_MODE ) { + if ( layer.type == RGBA_GIMAGE ) + dissolveRGBPixels( layer.image_tiles[j][i], x, y ); + + else if ( layer.type == GRAYA_GIMAGE ) + dissolveAlphaPixels( layer.alpha_tiles[j][i], x, y ); + } + + for ( int l = 0; l < layer.image_tiles[j][i].height(); l++ ) { + for ( int k = 0; k < layer.image_tiles[j][i].width(); k++ ) { + + int m = x + k + layer.x_offset; + int n = y + l + layer.y_offset; + + if ( m < 0 || m >= image.width() || n < 0 || n >= image.height() ) + continue; + + (*merge)( layer, i, j, k, l, image, m, n ); + } + } + } + } +} + +/*! + * Merge an RGB pixel from the layer to the RGB image. Straight-forward. + * The only thing this has to take account of is the opacity of the + * layer. Evidently, the GIMP exporter itself does not actually do this. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::mergeRGBToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + QRgb src = layer.image_tiles[j][i].pixel( k, l ); + QRgb dst = image.pixel( m, n ); + + uchar src_r = qRed( src ); + uchar src_g = qGreen( src ); + uchar src_b = qBlue( src ); + uchar src_a = qAlpha( src ); + + uchar dst_r = qRed( dst ); + uchar dst_g = qGreen( dst ); + uchar dst_b = qBlue( dst ); + uchar dst_a = qAlpha( dst ); + + switch ( layer.mode ) { + case MULTIPLY_MODE: { + src_r = INT_MULT( src_r, dst_r ); + src_g = INT_MULT( src_g, dst_g ); + src_b = INT_MULT( src_b, dst_b ); + src_a = MIN( src_a, dst_a ); + } + break; + case DIVIDE_MODE: { + src_r = MIN( ( dst_r * 256 ) / ( 1 + src_r ), 255 ); + src_g = MIN( ( dst_g * 256 ) / ( 1 + src_g ), 255 ); + src_b = MIN( ( dst_b * 256 ) / ( 1 + src_b ), 255 ); + src_a = MIN( src_a, dst_a ); + } + break; + case SCREEN_MODE: { + src_r = 255 - INT_MULT( 255 - dst_r, 255 - src_r ); + src_g = 255 - INT_MULT( 255 - dst_g, 255 - src_g ); + src_b = 255 - INT_MULT( 255 - dst_b, 255 - src_b ); + src_a = MIN( src_a, dst_a ); + } + break; + case OVERLAY_MODE: { + src_r = INT_MULT( dst_r, dst_r + INT_MULT( 2 * src_r, 255 - dst_r ) ); + src_g = INT_MULT( dst_g, dst_g + INT_MULT( 2 * src_g, 255 - dst_g ) ); + src_b = INT_MULT( dst_b, dst_b + INT_MULT( 2 * src_b, 255 - dst_b ) ); + src_a = MIN( src_a, dst_a ); + } + break; + case DIFFERENCE_MODE: { + src_r = dst_r > src_r ? dst_r - src_r : src_r - dst_r; + src_g = dst_g > src_g ? dst_g - src_g : src_g - dst_g; + src_b = dst_b > src_b ? dst_b - src_b : src_b - dst_b; + src_a = MIN( src_a, dst_a ); + } + break; + case ADDITION_MODE: { + src_r = add_lut[dst_r][src_r]; + src_g = add_lut[dst_g][src_g]; + src_b = add_lut[dst_b][src_b]; + src_a = MIN( src_a, dst_a ); + } + break; + case SUBTRACT_MODE: { + src_r = dst_r > src_r ? dst_r - src_r : 0; + src_g = dst_g > src_g ? dst_g - src_g : 0; + src_b = dst_b > src_b ? dst_b - src_b : 0; + src_a = MIN( src_a, dst_a ); + } + break; + case DARKEN_ONLY_MODE: { + src_r = dst_r < src_r ? dst_r : src_r; + src_g = dst_g < src_g ? dst_g : src_g; + src_b = dst_b < src_b ? dst_b : src_b; + src_a = MIN( src_a, dst_a ); + } + break; + case LIGHTEN_ONLY_MODE: { + src_r = dst_r < src_r ? src_r : dst_r; + src_g = dst_g < src_g ? src_g : dst_g; + src_b = dst_b < src_b ? src_b : dst_b; + src_a = MIN( src_a, dst_a ); + } + break; + case HUE_MODE: { + uchar new_r = dst_r; + uchar new_g = dst_g; + uchar new_b = dst_b; + + RGBTOHSV( src_r, src_g, src_b ); + RGBTOHSV( new_r, new_g, new_b ); + + new_r = src_r; + + HSVTORGB( new_r, new_g, new_b ); + + src_r = new_r; + src_g = new_g; + src_b = new_b; + src_a = MIN( src_a, dst_a ); + } + break; + case SATURATION_MODE: { + uchar new_r = dst_r; + uchar new_g = dst_g; + uchar new_b = dst_b; + + RGBTOHSV( src_r, src_g, src_b ); + RGBTOHSV( new_r, new_g, new_b ); + + new_g = src_g; + + HSVTORGB( new_r, new_g, new_b ); + + src_r = new_r; + src_g = new_g; + src_b = new_b; + src_a = MIN( src_a, dst_a ); + } + break; + case VALUE_MODE: { + uchar new_r = dst_r; + uchar new_g = dst_g; + uchar new_b = dst_b; + + RGBTOHSV( src_r, src_g, src_b ); + RGBTOHSV( new_r, new_g, new_b ); + + new_b = src_b; + + HSVTORGB( new_r, new_g, new_b ); + + src_r = new_r; + src_g = new_g; + src_b = new_b; + src_a = MIN( src_a, dst_a ); + } + break; + case COLOR_MODE: { + uchar new_r = dst_r; + uchar new_g = dst_g; + uchar new_b = dst_b; + + RGBTOHLS( src_r, src_g, src_b ); + RGBTOHLS( new_r, new_g, new_b ); + + new_r = src_r; + new_b = src_b; + + HLSTORGB( new_r, new_g, new_b ); + + src_r = new_r; + src_g = new_g; + src_b = new_b; + src_a = MIN( src_a, dst_a ); + } + break; + } + + src_a = INT_MULT( src_a, layer.opacity ); + + // Apply the mask (if any) + + if ( layer.apply_mask == 1 && layer.mask_tiles.size() > j && + layer.mask_tiles[j].size() > i ) + src_a = INT_MULT( src_a, layer.mask_tiles[j][i].pixelIndex( k, l ) ); + + uchar new_r, new_g, new_b, new_a; + + new_a = dst_a + INT_MULT( OPAQUE_OPACITY - dst_a, src_a ); + + float src_ratio = (float)src_a / new_a; + float dst_ratio = 1. - src_ratio; + + new_r = (uchar)( src_ratio * src_r + dst_ratio * dst_r + EPSILON ); + new_g = (uchar)( src_ratio * src_g + dst_ratio * dst_g + EPSILON ); + new_b = (uchar)( src_ratio * src_b + dst_ratio * dst_b + EPSILON ); + + if ( !layer_modes[layer.mode].affect_alpha ) + new_a = dst_a; + + image.setPixel( m, n, qRgba( new_r, new_g, new_b, new_a ) ); +} + +/*! + * Merge a Gray pixel from the layer to the Gray image. Straight-forward. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::mergeGrayToGray ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + int src = layer.image_tiles[j][i].pixelIndex( k, l ); + + image.setPixel( m, n, src ); +} + +/*! + * Merge a GrayA pixel from the layer to the Gray image. Straight-forward. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::mergeGrayAToGray ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + int src = qGray( layer.image_tiles[j][i].pixel( k, l ) ); + int dst = image.pixelIndex( m, n ); + + uchar src_a = layer.alpha_tiles[j][i].pixelIndex( k, l ); + + switch ( layer.mode ) { + case MULTIPLY_MODE: { + src = INT_MULT( src, dst ); + } + break; + case DIVIDE_MODE: { + src = MIN( ( dst * 256 ) / ( 1 + src ), 255 ); + } + break; + case SCREEN_MODE: { + src = 255 - INT_MULT( 255 - dst, 255 - src ); + } + break; + case OVERLAY_MODE: { + src = INT_MULT( dst, dst + INT_MULT( 2 * src, 255 - dst ) ); + } + break; + case DIFFERENCE_MODE: { + src = dst > src ? dst - src : src - dst; + } + break; + case ADDITION_MODE: { + src = add_lut[dst][src]; + } + break; + case SUBTRACT_MODE: { + src = dst > src ? dst - src : 0; + } + break; + case DARKEN_ONLY_MODE: { + src = dst < src ? dst : src; + } + break; + case LIGHTEN_ONLY_MODE: { + src = dst < src ? src : dst; + } + break; + } + + src_a = INT_MULT( src_a, layer.opacity ); + + // Apply the mask (if any) + + if ( layer.apply_mask == 1 && layer.mask_tiles.size() > j && + layer.mask_tiles[j].size() > i ) + src_a = INT_MULT( src_a, layer.mask_tiles[j][i].pixelIndex( k, l ) ); + + uchar new_a = OPAQUE_OPACITY; + + float src_ratio = (float)src_a / new_a; + float dst_ratio = 1. - src_ratio; + + uchar new_g = (uchar)( src_ratio * src + dst_ratio * dst + EPSILON ); + + image.setPixel( m, n, new_g ); +} + +/*! + * Merge a Gray pixel from the layer to an RGB image. Straight-forward. + * The only thing this has to take account of is the opacity of the + * layer. Evidently, the GIMP exporter itself does not actually do this. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::mergeGrayToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + QRgb src = layer.image_tiles[j][i].pixel( k, l ); + + uchar src_a = layer.opacity; + + image.setPixel( m, n, qRgba( src, src_a ) ); +} + +/*! + * Merge a GrayA pixel from the layer to an RGB image. Straight-forward. + * The only thing this has to take account of is the opacity of the + * layer. Evidently, the GIMP exporter itself does not actually do this. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::mergeGrayAToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + int src = qGray( layer.image_tiles[j][i].pixel( k, l ) ); + int dst = qGray( image.pixel( m, n ) ); + + uchar src_a = layer.alpha_tiles[j][i].pixelIndex( k, l ); + uchar dst_a = qAlpha( image.pixel( m, n ) ); + + switch ( layer.mode ) { + case MULTIPLY_MODE: { + src = INT_MULT( src, dst ); + src_a = MIN( src_a, dst_a ); + } + break; + case DIVIDE_MODE: { + src = MIN( ( dst * 256 ) / ( 1 + src ), 255 ); + src_a = MIN( src_a, dst_a ); + } + break; + case SCREEN_MODE: { + src = 255 - INT_MULT( 255 - dst, 255 - src ); + src_a = MIN( src_a, dst_a ); + } + break; + case OVERLAY_MODE: { + src = INT_MULT( dst, dst + INT_MULT( 2 * src, 255 - dst ) ); + src_a = MIN( src_a, dst_a ); + } + break; + case DIFFERENCE_MODE: { + src = dst > src ? dst - src : src - dst; + src_a = MIN( src_a, dst_a ); + } + break; + case ADDITION_MODE: { + src = add_lut[dst][src]; + src_a = MIN( src_a, dst_a ); + } + break; + case SUBTRACT_MODE: { + src = dst > src ? dst - src : 0; + src_a = MIN( src_a, dst_a ); + } + break; + case DARKEN_ONLY_MODE: { + src = dst < src ? dst : src; + src_a = MIN( src_a, dst_a ); + } + break; + case LIGHTEN_ONLY_MODE: { + src = dst < src ? src : dst; + src_a = MIN( src_a, dst_a ); + } + break; + } + + src_a = INT_MULT( src_a, layer.opacity ); + + // Apply the mask (if any) + + if ( layer.apply_mask == 1 && layer.mask_tiles.size() > j && + layer.mask_tiles[j].size() > i ) + src_a = INT_MULT( src_a, layer.mask_tiles[j][i].pixelIndex( k, l ) ); + + uchar new_a = dst_a + INT_MULT( OPAQUE_OPACITY - dst_a, src_a ); + + float src_ratio = (float)src_a / new_a; + float dst_ratio = 1. - src_ratio; + + uchar new_g = (uchar)( src_ratio * src + dst_ratio * dst + EPSILON ); + + if ( !layer_modes[layer.mode].affect_alpha ) + new_a = dst_a; + + image.setPixel( m, n, qRgba( new_g, new_g, new_g, new_a ) ); +} + +/*! + * Merge an Indexed pixel from the layer to the Indexed image. Straight-forward. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::mergeIndexedToIndexed ( Layer& layer, uint i,uint j,int k,int l, + QImage& image, int m, int n ) +{ + int src = layer.image_tiles[j][i].pixelIndex( k, l ); + + image.setPixel( m, n, src ); +} + +/*! + * Merge an IndexedA pixel from the layer to the Indexed image. Straight-forward. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::mergeIndexedAToIndexed ( Layer& layer,uint i,uint j,int k,int l, + QImage& image, int m, int n ) +{ + uchar src = layer.image_tiles[j][i].pixelIndex( k, l ); + + uchar src_a = layer.alpha_tiles[j][i].pixelIndex( k, l ); + + src_a = INT_MULT( src_a, layer.opacity ); + + if ( layer.apply_mask == 1 && + layer.mask_tiles.size() > j && + layer.mask_tiles[j].size() > i ) + src_a = INT_MULT( src_a, + layer.mask_tiles[j][i].pixelIndex( k, l ) ); + + if ( src_a > 127 ) { + src++; + image.setPixel( m, n, src ); + } +} + +/*! + * Merge an IndexedA pixel from the layer to an RGB image. Straight-forward. + * The only thing this has to take account of is the opacity of the + * layer. Evidently, the GIMP exporter itself does not actually do this. + * \param layer source layer. + * \param i x tile index. + * \param j y tile index. + * \param k x pixel index of tile i,j. + * \param l y pixel index of tile i,j. + * \param image destination image. + * \param m x pixel of destination image. + * \param n y pixel of destination image. + */ +void XCFImageFormat::mergeIndexedAToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ) +{ + QRgb src = layer.image_tiles[j][i].pixel( k, l ); + + uchar src_a = layer.alpha_tiles[j][i].pixelIndex( k, l ); + + src_a = INT_MULT( src_a, layer.opacity ); + + // Apply the mask (if any) + + if ( layer.apply_mask == 1 && layer.mask_tiles.size() > j && + layer.mask_tiles[j].size() > i ) + src_a = INT_MULT( src_a, layer.mask_tiles[j][i].pixelIndex( k, l ) ); + + // This is what appears in the GIMP window + + if ( src_a <= 127 ) + src_a = 0; + else + src_a = OPAQUE_OPACITY; + + image.setPixel( m, n, qRgba( src, src_a ) ); +} + +/*! + * Dissolving pixels: pick a random number between 0 and 255. If the pixel's + * alpha is less than that, make it transparent. + * \param image the image tile to dissolve. + * \param x the global x position of the tile. + * \param y the global y position of the tile. + */ +void XCFImageFormat::dissolveRGBPixels ( QImage& image, int x, int y ) +{ + // The apparently spurious rand() calls are to wind the random + // numbers up to the same point for each tile. + + for ( int l = 0; l < image.height(); l++ ) { + srand( random_table[( l + y ) % RANDOM_TABLE_SIZE] ); + + for ( int k = 0; k < x; k++ ) + rand(); + + for ( int k = 0; k < image.width(); k++ ) { + int rand_val = rand() & 0xff; + QRgb pixel = image.pixel( k, l ); + + if ( rand_val > qAlpha( pixel ) ) { + image.setPixel( k, l, qRgba( pixel, 0 ) ); + } + } + } +} + +/*! + * Dissolving pixels: pick a random number between 0 and 255. If the pixel's + * alpha is less than that, make it transparent. This routine works for + * the GRAYA and INDEXEDA image types where the pixel alpha's are stored + * separately from the pixel themselves. + * \param image the alpha tile to dissolve. + * \param x the global x position of the tile. + * \param y the global y position of the tile. + */ +void XCFImageFormat::dissolveAlphaPixels ( QImage& image, int x, int y ) +{ + // The apparently spurious rand() calls are to wind the random + // numbers up to the same point for each tile. + + for ( int l = 0; l < image.height(); l++ ) { + srand( random_table[( l + y ) % RANDOM_TABLE_SIZE] ); + + for ( int k = 0; k < x; k++ ) + rand(); + + for ( int k = 0; k < image.width(); k++ ) { + int rand_val = rand() & 0xff; + uchar alpha = image.pixelIndex( k, l ); + + if ( rand_val > alpha ) { + image.setPixel( k, l, 0 ); + } + } + } +} + +KDE_Q_EXPORT_PLUGIN( XCFImageFormat ) + +} // namespace diff --git a/src/gvcore/qxcfi.h b/src/gvcore/qxcfi.h new file mode 100644 index 0000000..ac82173 --- /dev/null +++ b/src/gvcore/qxcfi.h @@ -0,0 +1,332 @@ +#ifndef QXCFI_H +#define QXCFI_H + +#include <qimage.h> +#include <qimageformatplugin.h> +#include <qvaluestack.h> +#include <qvaluevector.h> + +#include "gimp.h" +namespace Gwenview { + +// Safe readBlock helper functions +class SafeDataStream { +public: + SafeDataStream(QIODevice* device) + : mDevice(device), mFailed(false) {} + + bool failed() const { return mFailed; } + QIODevice* device() const { return mDevice; } + + SafeDataStream& readRawBytes(char* data, uint length) { + if (mFailed) return *this; + int read_length=mDevice->readBlock(data, length); + if (read_length==-1) mFailed=true; + if ((uint)read_length!=length) mFailed=true; + return *this; + } + + SafeDataStream& operator>>(Q_INT8& value) { + return readRawBytes((char*)&value, 1); + } + + SafeDataStream& operator>>(Q_UINT32& value) { + if (mFailed) return *this; + uchar *p = (uchar *)(&value); + char b[4]; + if (mDevice->readBlock( b, 4 )==4) { + *p++ = b[3]; + *p++ = b[2]; + *p++ = b[1]; + *p = b[0]; + } else { + mFailed=true; + } + return *this; + } + + SafeDataStream& operator>>(Q_INT32& value) { + return *this >>((Q_UINT32&)value); + } + + SafeDataStream& operator>>(float& value) { + return *this >>((Q_UINT32&)value); + } + + SafeDataStream& operator>>(char*& value) { + if (mFailed) return *this; + + Q_UINT32 len; + *this >> len; + if (mFailed) return *this; + if ( len == 0 ) { + value = 0; + return *this; + } + if (mDevice->atEnd() ) { + value = 0; + mFailed=true; + return *this; + } + value = new char[len]; + Q_CHECK_PTR( value ); + if ( !value ) { + mFailed=true; + return *this; + } + return readRawBytes(value, len); + } + + SafeDataStream& readBytes(char*& data, uint& len) { + if (mFailed) return *this; + + *this >> len; + if (mFailed) return *this; + data=new char[len]; + Q_CHECK_PTR( data ); + if ( !data ) { + mFailed=true; + return *this; + } + return readRawBytes(data, len); + } + + // This method is usefull to debug with gdb. Do not inline it! + int at() const; + +private: + QIODevice* mDevice; + bool mFailed; +}; + +//! Plug-in for loading a GIMP XCF image file directly. +/*! + * This class uses the Qt 3.0 Image format plug-in loader to provide + * the ability to read The GIMP XCF image files. This plug-in will + * be dynamically loaded as needed. + */ +class XCFImageFormat : public QImageFormatPlugin { + + /*! + * Each layer in an XCF file is stored as a matrix of + * 64-pixel by 64-pixel images. The GIMP has a sophisticated + * method of handling very large images as well as implementing + * parallel processing on a tile-by-tile basis. Here, though, + * we just read them in en-masse and store them in a matrix. + */ + typedef QValueVector< QValueVector< QImage > > Tiles; + + /*! + * Each GIMP image is composed of one or more layers. A layer can + * be one of any three basic types: RGB, grayscale or indexed. With an + * optional alpha channel, there are six possible types altogether. + * + * Note: there is only ever one instance of this structure. The + * layer info is discarded after it is merged into the final QImage. + */ + struct Layer { + Q_UINT32 width; //!< Width of the layer + Q_UINT32 height; //!< Height of the layer + Q_INT32 type; //!< Type of the layer (GimpImageType) + char* name; //!< Name of the layer + Q_UINT32 hierarchy_offset; //!< File position of Tile hierarchy + Q_UINT32 mask_offset; //!< File position of mask image + + uint nrows; //!< Number of rows of tiles (y direction) + uint ncols; //!< Number of columns of tiles (x direction) + + Tiles image_tiles; //!< The basic image + //! For Grayscale and Indexed images, the alpha channel is stored + //! separately (in this data structure, anyway). + Tiles alpha_tiles; + Tiles mask_tiles; //!< The layer mask (optional) + + //! Additional information about a layer mask. + struct { + Q_UINT32 opacity; + Q_UINT32 visible; + Q_UINT32 show_masked; + uchar red, green, blue; + Q_UINT32 tattoo; + } mask_channel; + + bool active; //!< Is this layer the active layer? + Q_UINT32 opacity; //!< The opacity of the layer + Q_UINT32 visible; //!< Is the layer visible? + Q_UINT32 linked; //!< Is this layer linked (geometrically) + Q_UINT32 preserve_transparency; //!< Preserve alpha when drawing on layer? + Q_UINT32 apply_mask; //!< Apply the layer mask? + Q_UINT32 edit_mask; //!< Is the layer mask the being edited? + Q_UINT32 show_mask; //!< Show the layer mask rather than the image? + Q_INT32 x_offset; //!< x offset of the layer relative to the image + Q_INT32 y_offset; //!< y offset of the layer relative to the image + Q_UINT32 mode; //!< Combining mode of layer (LayerModeEffects) + Q_UINT32 tattoo; //!< (unique identifier?) + + //! As each tile is read from the file, it is buffered here. + uchar tile[TILE_WIDTH * TILE_HEIGHT * sizeof(QRgb)]; + + //! The data from tile buffer is copied to the Tile by this + //! method. Depending on the type of the tile (RGB, Grayscale, + //! Indexed) and use (image or mask), the bytes in the buffer are + //! copied in different ways. + void (*assignBytes)( Layer& layer, uint i, uint j ); + + //! Construct a layer. + Layer ( void ) : name( 0 ) {} + //! Destruct the layer. + ~Layer ( void ) { if ( name != 0 ) delete[] name; } + }; + + /*! + * The in-memory representation of the XCF Image. It contains a few + * metadata items, but is mostly a container for the layer information. + */ + struct XCFImage { + Q_UINT32 width; //!< width of the XCF image + Q_UINT32 height; //!< height of the XCF image + Q_INT32 type; //!< type of the XCF image (GimpImageBaseType) + + Q_UINT8 compression; //!< tile compression method (CompressionType) + float x_resolution; //!< x resolution in dots per inch + float y_resolution; //!< y resolution in dots per inch + Q_INT32 tattoo; //!< (unique identifier?) + Q_UINT32 unit; //!< Units of The GIMP (inch, mm, pica, etc...) + Q_INT32 num_colors; //!< number of colors in an indexed image + QValueVector< QRgb > palette; //!< indexed image color palette + + int num_layers; //!< number of layers + Layer layer; //!< most recently read layer + + bool initialized; //!< Is the QImage initialized? + QImage image; //!< final QImage + + //! Simple constructor. + XCFImage ( void ) : initialized( false ) {} + }; + + //! The bottom-most layer is copied into the final QImage by this + //! routine. + typedef void (*PixelCopyOperation) ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + + //! Higher layers are merged into the the final QImage by this routine. + typedef void (*PixelMergeOperation) ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + + //! In layer DISSOLVE mode, a random number is chosen to compare to a + //! pixel's alpha. If the alpha is greater than the random number, the + //! pixel is drawn. This table merely contains the random number seeds + //! for each ROW of an image. Therefore, the random numbers chosen + //! are consistent from run to run. + + static int random_table[RANDOM_TABLE_SIZE]; + + //! This table provides the add_pixel saturation values (i.e. 250 + 250 = 255). + + static int add_lut[256][256]; + + //! Layer mode static data. + typedef struct { + bool affect_alpha; //!< Does this mode affect the source alpha? + } LayerModes; + + //! Array of layer mode structures for the modes described by + //! LayerModeEffects. + static LayerModes layer_modes[]; + +public: + /*! + * The constructor for the XCF image loader. This initializes the + * tables used in the layer merging routines. + */ + XCFImageFormat (); + + + /*! + * The image loader makes no (direct) use of dynamic memory + * and the Qt infrastructure takes care of constructing and destructing + * the loader so there is not much to do here. + */ + ~XCFImageFormat () {} + + /*! + * You can query Qt about the types of image file formats it knows about + * via QImage::inputFormats or QImage::inputFormatList(). + * This method returns "xcf". + */ + QStringList keys () const { + return QStringList() << "XCF"; + } + + /*! + * This method installs the XCF reader on demand. + */ + bool installIOHandler ( const QString& ); + + static void registerFormat(); + +private: + static void readXCF ( QImageIO* image_io ); +#ifdef TMP_WRITE + static void writeXCF ( QImageIO* ) {} +#endif + static void initializeImage ( XCFImage& xcf_image ); + static void composeTiles ( XCFImage& xcf_image ); + static bool loadImageProperties ( SafeDataStream& xcf_io, XCFImage& image ); + static bool loadLayer ( SafeDataStream& xcf_io, XCFImage& xcf_image ); + static bool loadLayerProperties ( SafeDataStream& xcf_io, Layer& layer ); + static bool loadChannelProperties ( SafeDataStream& xcf_io, Layer& layer ); + static bool loadHierarchy ( SafeDataStream& xcf_io, Layer& layer ); + static bool loadMask ( SafeDataStream& xcf_io, Layer& layer ); + static bool loadLevel ( SafeDataStream& xcf_io, Layer& layer, Q_INT32 bpp ); + static bool loadTileRLE ( SafeDataStream& xcf_io, uchar* tile, int size, + int data_length, Q_INT32 bpp ); + static bool loadProperty ( SafeDataStream& xcf_io, PropType& type, + QByteArray& bytes ); + static void setGrayPalette ( QImage& image ); + static void setPalette ( XCFImage& xcf_image, QImage& image ); + static void assignImageBytes ( Layer& layer, uint i, uint j ); + static void assignMaskBytes ( Layer& layer, uint i, uint j ); + + static void copyLayerToImage ( XCFImage& xcf_image ); + static void copyRGBToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void copyGrayToGray ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void copyGrayToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void copyGrayAToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void copyIndexedToIndexed ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void copyIndexedAToIndexed ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void copyIndexedAToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + + static void mergeLayerIntoImage ( XCFImage& xcf_image ); + static void mergeRGBToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void mergeGrayToGray ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void mergeGrayAToGray ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void mergeGrayToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void mergeGrayAToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void mergeIndexedToIndexed ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void mergeIndexedAToIndexed ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + static void mergeIndexedAToRGB ( Layer& layer, uint i, uint j, int k, int l, + QImage& image, int m, int n ); + + static void dissolveRGBPixels ( QImage& image, int x, int y ); + static void dissolveAlphaPixels ( QImage& image, int x, int y ); +}; + +} // namespace +#endif + diff --git a/src/gvcore/slideshow.cpp b/src/gvcore/slideshow.cpp new file mode 100644 index 0000000..fb9442a --- /dev/null +++ b/src/gvcore/slideshow.cpp @@ -0,0 +1,217 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +// kate: indent-mode csands; indent-width 4; replace-tabs-save off; replace-tabs off; replace-trailing-space-save off; space-indent off; tabs-indents on; tab-width 4; +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2004 Aur�ien G�eau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +// STL +#include <algorithm> + +// Qt +#include <qtimer.h> + +// KDE +#include <kconfig.h> +#include <kdebug.h> + +// Local +#include <../gvcore/slideshowconfig.h> +#include "slideshow.moc" + +#include "document.h" +#include "imageloader.h" +#include "cache.h" + +namespace Gwenview { + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + +SlideShow::SlideShow(Document* document) +: mDocument(document), mStarted(false), mPrefetch( NULL ) { + mTimer=new QTimer(this); + connect(mTimer, SIGNAL(timeout()), + this, SLOT(slotTimeout()) ); + connect(mDocument, SIGNAL(loaded(const KURL&)), + this, SLOT(slotLoaded()) ); +} + +SlideShow::~SlideShow() { + if( !mPriorityURL.isEmpty()) Cache::instance()->setPriorityURL( mPriorityURL, false ); +} + + +void SlideShow::slotSettingsChanged() { + if (mTimer->isActive()) { + mTimer->changeInterval(timerInterval()); + } +} + + +int SlideShow::timerInterval() { + int documentDuration = mDocument->duration(); + if (documentDuration != 0) { + return documentDuration * 1000; + } else { + return int(SlideShowConfig::delay()*1000); + } +} + + +void SlideShow::start(const KURL::List& urls) { + mURLs.resize(urls.size()); + qCopy(urls.begin(), urls.end(), mURLs.begin()); + if (SlideShowConfig::random()) { + std::random_shuffle(mURLs.begin(), mURLs.end()); + } + + mStartIt=qFind(mURLs.begin(), mURLs.end(), mDocument->url()); + if (mStartIt==mURLs.end()) { + kdWarning() << k_funcinfo << "Current URL not found in list, aborting.\n"; + return; + } + + mTimer->start(timerInterval(), true); + mStarted=true; + prefetch(); + emit stateChanged(true); +} + + +void SlideShow::stop() { + mTimer->stop(); + mStarted=false; + emit stateChanged(false); + if( !mPriorityURL.isEmpty()) { + Cache::instance()->setPriorityURL( mPriorityURL, false ); + mPriorityURL = KURL(); + } +} + + +QValueVector<KURL>::ConstIterator SlideShow::findNextURL() const { + QValueVector<KURL>::ConstIterator it=qFind(mURLs.begin(), mURLs.end(), mDocument->url()); + if (it==mURLs.end()) { + kdWarning() << k_funcinfo << "Current URL not found in list. This should not happen.\n"; + return it; + } + + ++it; + if (SlideShowConfig::loop()) { + // Looping, if we reach the end, start again + if (it==mURLs.end()) { + it = mURLs.begin(); + } + } else { + // Not looping, have we reached the end? + if ((it==mURLs.end() && SlideShowConfig::stopAtEnd()) || it==mStartIt) { + it = mURLs.end(); + } + } + + return it; +} + + +void SlideShow::slotTimeout() { + LOG(""); + // wait for prefetching to finish + if( mPrefetch != NULL ) { + LOG("mPrefetch is working"); + return; + } + + QValueVector<KURL>::ConstIterator it=findNextURL(); + if (it==mURLs.end()) { + stop(); + return; + } + emit nextURL(*it); +} + + +void SlideShow::slotLoaded() { + if (mStarted) { + mTimer->start(timerInterval(), true); + prefetch(); + } +} + + +void SlideShow::prefetch() { + LOG(""); + QValueVector<KURL>::ConstIterator it=findNextURL(); + if (it==mURLs.end()) { + return; + } + LOG("url=" << (*it).pathOrURL()); + + if( mPrefetch != NULL ) mPrefetch->release( this ); + // TODO don't use prefetching with disabled optimizations (and add that option ;) ) + // (and also don't use prefetching in other places if the image won't fit in cache) + mPrefetch = ImageLoader::loader( *it, this, BUSY_PRELOADING ); + if( !mPriorityURL.isEmpty()) Cache::instance()->setPriorityURL( mPriorityURL, false ); + mPriorityURL = *it; + Cache::instance()->setPriorityURL( mPriorityURL, true ); // make sure it will stay in the cache + connect( mPrefetch, SIGNAL( urlKindDetermined()), SLOT( slotUrlKindDetermined())); + connect( mPrefetch, SIGNAL( imageLoaded( bool )), SLOT( prefetchDone())); + + if (mPrefetch->urlKind()==MimeTypeUtils::KIND_FILE) { + // Prefetch is already done, and this is not a raster image + prefetchDone(); + } +} + +void SlideShow::slotUrlKindDetermined() { + LOG(""); + if (!mPrefetch) return; + + LOG("mPrefetch!=0"); + if (mPrefetch->urlKind()==MimeTypeUtils::KIND_FILE) { + LOG("KIND_FILE"); + // This is not a raster image, imageLoaded will not be emitted + prefetchDone(); + } +} + + +void SlideShow::prefetchDone() { + LOG(""); + if( mPrefetch != NULL ) { + LOG("mPrefetch!=0"); + // don't call Cache::setPriorityURL( ... , false ) here - it will still take + // a short while to reuse the image from the cache + mPrefetch->release( this ); + mPrefetch = NULL; + // prefetching completed and delay has already elapsed + if( mStarted && !mTimer->isActive()) { + LOG("Calling slotTimeout"); + slotTimeout(); + } + } +} + + +} // namespace diff --git a/src/gvcore/slideshow.h b/src/gvcore/slideshow.h new file mode 100644 index 0000000..fcb1d1a --- /dev/null +++ b/src/gvcore/slideshow.h @@ -0,0 +1,86 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +// kate: indent-mode csands; indent-width 4; replace-tabs-save off; replace-tabs off; replace-trailing-space-save off; space-indent off; tabs-indents on; tab-width 4; +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2004 Aur�ien G�eau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#ifndef SLIDESHOW_H +#define SLIDESHOW_H + +// Qt +#include <qobject.h> +#include <qvaluevector.h> + +// KDE +#include <kurl.h> +#include "libgwenview_export.h" +class QTimer; + +class KConfig; + +namespace Gwenview { +class Document; +class ImageLoader; + +class LIBGWENVIEW_EXPORT SlideShow : public QObject +{ +Q_OBJECT +public: + SlideShow(Document* document); + virtual ~SlideShow(); + + void start(const KURL::List& urls); + void stop(); + + /** @return true if the slideshow is running */ + bool isRunning() { return mStarted; } + +public slots: + void slotSettingsChanged(); + +signals: + void nextURL( const KURL& ); + /** + * Slideshow has been started or stopped + */ + void stateChanged(bool running); + +private slots: + void slotTimeout(); + void slotLoaded(); + void slotUrlKindDetermined(); + void prefetchDone(); + +private: + QValueVector<KURL>::ConstIterator findNextURL() const; + void prefetch(); + int timerInterval(); + + QTimer* mTimer; + Document* mDocument; + bool mStarted; + QValueVector<KURL> mURLs; + QValueVector<KURL>::ConstIterator mStartIt; + ImageLoader* mPrefetch; + KURL mPriorityURL; +}; + +} // namespace +#endif // SLIDESHOW_H + diff --git a/src/gvcore/slideshowconfig.kcfg b/src/gvcore/slideshowconfig.kcfg new file mode 100644 index 0000000..9f110d9 --- /dev/null +++ b/src/gvcore/slideshowconfig.kcfg @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE kcfg SYSTEM "http://www.kde.org/standards/kcfg/1.0/kcfg.dtd"> +<kcfg> + <kcfgfile name="gwenviewrc"/> + <group name="slide show"> + <entry name="random" type="Bool"> + <label>Display slide show images in random order</label> + <default>false</default> + </entry> + <entry name="fullscreen" type="Bool"> + <label>Show slideshow in fullscreen mode</label> + <default>true</default> + </entry> + <entry name="loop" type="Bool"> + <label>Loop on images</label> + <default>false</default> + </entry> + <entry name="stopAtEnd" key="stop at end" type="Bool"> + <label>Stop at last image of folder</label> + <default>false</default> + </entry> + <entry name="delay" type="Double"> + <label>Delay between images (in seconds)</label> + <default>10.0</default> + </entry> + </group> +</kcfg> diff --git a/src/gvcore/slideshowconfig.kcfgc b/src/gvcore/slideshowconfig.kcfgc new file mode 100644 index 0000000..85b2ff8 --- /dev/null +++ b/src/gvcore/slideshowconfig.kcfgc @@ -0,0 +1,7 @@ +File=slideshowconfig.kcfg +ClassName=SlideShowConfig +NameSpace=Gwenview +Singleton=true +Mutators=true +IncludeFiles=gvcore/libgwenview_export.h +Visibility=LIBGWENVIEW_EXPORT diff --git a/src/gvcore/threadgate.cpp b/src/gvcore/threadgate.cpp new file mode 100644 index 0000000..3d09b9f --- /dev/null +++ b/src/gvcore/threadgate.cpp @@ -0,0 +1,60 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "threadgate.moc" + +#include "tsthread/tsthread.h" + +namespace Gwenview { + +// The trick is simple. This object connects its slot to its signal, then +// emits the signal using emitSignal(), and the slot gets called in the main +// thread. In the main thread the slot does everything that should be done +// in the main thread, and returns the data using the signal/slot reference +// arguments. As the thread is blocked waiting on the signal to finish, +// there's even no need to do any locking. + +ThreadGate::ThreadGate() { + connect( this, SIGNAL( signalColor( QColor&, const char* )), + this, SLOT( slotColor( QColor&, const char* ))); +} + +ThreadGate* ThreadGate::instance() { + static ThreadGate gate; + return &gate; +} + +QColor ThreadGate::color( const char* name ) { + if( name == NULL || name[ 0 ] == '\0' || name[ 0 ] == '#' ) + return QColor( name ); + // named color ... needs to be created in the main thread + if( TSThread::currentThread() == TSThread::mainThread()) + return QColor( name ); + QColor col; + TSThread::currentThread()->emitCancellableSignal( this, SIGNAL( signalColor( QColor&, const char* )), col, name ); + return col; +} + +void ThreadGate::slotColor( QColor& col, const char* name ) { + col = QColor( name ); +} + +} // namespace diff --git a/src/gvcore/threadgate.h b/src/gvcore/threadgate.h new file mode 100644 index 0000000..4783198 --- /dev/null +++ b/src/gvcore/threadgate.h @@ -0,0 +1,45 @@ +// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#ifndef THREADGATE_H +#define THREADGATE_H + +#include <qobject.h> +#include <qcolor.h> +namespace Gwenview { + +class ThreadGate : public QObject +{ +Q_OBJECT +public: + static ThreadGate* instance(); + QColor color( const char* name ); +private: + ThreadGate(); +signals: + void signalColor( QColor&, const char* ); +private slots: + void slotColor( QColor&, const char* ); +}; + +} // namespace +#endif + diff --git a/src/gvcore/thumbnaildetailsdialog.cpp b/src/gvcore/thumbnaildetailsdialog.cpp new file mode 100644 index 0000000..ed514ed --- /dev/null +++ b/src/gvcore/thumbnaildetailsdialog.cpp @@ -0,0 +1,78 @@ +// vi: tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2005 Aurelien Gateau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "thumbnaildetailsdialog.moc" + +// Qt +#include <qcheckbox.h> + +// Local +#include "filethumbnailview.h" +#include "thumbnaildetailsdialogbase.h" + + +namespace Gwenview { + +struct ThumbnailDetailsDialog::Private { + FileThumbnailView* mView; + ThumbnailDetailsDialogBase* mContent; +}; + +ThumbnailDetailsDialog::ThumbnailDetailsDialog(FileThumbnailView* view) +: KDialogBase( + view, 0, false /* modal */, QString::null, KDialogBase::Close, + KDialogBase::Close, true /* separator */) +, d(new ThumbnailDetailsDialog::Private) +{ + d->mView=view; + d->mContent=new ThumbnailDetailsDialogBase(this); + setMainWidget(d->mContent); + setCaption(d->mContent->caption()); + + int details=d->mView->itemDetails(); + d->mContent->mShowFileName->setChecked(details & FileThumbnailView::FILENAME); + d->mContent->mShowFileDate->setChecked(details & FileThumbnailView::FILEDATE); + d->mContent->mShowFileSize->setChecked(details & FileThumbnailView::FILESIZE); + d->mContent->mShowImageSize->setChecked(details & FileThumbnailView::IMAGESIZE); + + connect(d->mContent->mShowFileName, SIGNAL(toggled(bool)), SLOT(applyChanges()) ); + connect(d->mContent->mShowFileDate, SIGNAL(toggled(bool)), SLOT(applyChanges()) ); + connect(d->mContent->mShowFileSize, SIGNAL(toggled(bool)), SLOT(applyChanges()) ); + connect(d->mContent->mShowImageSize, SIGNAL(toggled(bool)), SLOT(applyChanges()) ); +} + +ThumbnailDetailsDialog::~ThumbnailDetailsDialog() { + delete d; +} + + +void ThumbnailDetailsDialog::applyChanges() { + int details= + (d->mContent->mShowFileName->isChecked() ? FileThumbnailView::FILENAME : 0) + | (d->mContent->mShowFileDate->isChecked() ? FileThumbnailView::FILEDATE : 0) + | (d->mContent->mShowFileSize->isChecked() ? FileThumbnailView::FILESIZE : 0) + | (d->mContent->mShowImageSize->isChecked() ? FileThumbnailView::IMAGESIZE : 0) + ; + d->mView->setItemDetails(details); +} + + +} // namespace diff --git a/src/gvcore/thumbnaildetailsdialog.h b/src/gvcore/thumbnaildetailsdialog.h new file mode 100644 index 0000000..9706c20 --- /dev/null +++ b/src/gvcore/thumbnaildetailsdialog.h @@ -0,0 +1,47 @@ +// vi: tabstop=4 shiftwidth=4 noexpandtab +/* +Gwenview - A simple image viewer for KDE +Copyright 2005 Aurelien Gateau + +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef THUMBNAILDETAILSDIALOG_H +#define THUMBNAILDETAILSDIALOG_H + +// KDE +#include <kdialogbase.h> + +namespace Gwenview { + +class FileThumbnailView; + +class ThumbnailDetailsDialog : public KDialogBase { +Q_OBJECT +public: + ThumbnailDetailsDialog(FileThumbnailView* view); + ~ThumbnailDetailsDialog(); + +private slots: + void applyChanges(); + +private: + struct Private; + Private* d; +}; + +} // namespace + +#endif /* THUMBNAILDETAILSDIALOG_H */ diff --git a/src/gvcore/thumbnaildetailsdialogbase.ui b/src/gvcore/thumbnaildetailsdialogbase.ui new file mode 100644 index 0000000..4057968 --- /dev/null +++ b/src/gvcore/thumbnaildetailsdialogbase.ui @@ -0,0 +1,117 @@ +<!DOCTYPE UI><UI version="3.1" stdsetdef="1"> +<class>ThumbnailDetailsDialogBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>ThumbnailDetailsDialogBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>390</width> + <height>207</height> + </rect> + </property> + <property name="caption"> + <string>Thumbnail Details</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel1</cstring> + </property> + <property name="text"> + <string>Information to display in the thumbnail text:</string> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout6</cstring> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer row="1" column="0"> + <property name="name"> + <cstring>spacer7</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Fixed</enum> + </property> + <property name="sizeHint"> + <size> + <width>21</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="QCheckBox" row="0" column="1"> + <property name="name"> + <cstring>mShowFileName</cstring> + </property> + <property name="text"> + <string>File name</string> + </property> + </widget> + <widget class="QCheckBox" row="3" column="1"> + <property name="name"> + <cstring>mShowImageSize</cstring> + </property> + <property name="text"> + <string>Image size</string> + </property> + </widget> + <widget class="QCheckBox" row="2" column="1"> + <property name="name"> + <cstring>mShowFileSize</cstring> + </property> + <property name="text"> + <string>File size</string> + </property> + </widget> + <widget class="QCheckBox" row="1" column="1"> + <property name="name"> + <cstring>mShowFileDate</cstring> + </property> + <property name="text"> + <string>File date</string> + </property> + </widget> + </grid> + </widget> + <spacer> + <property name="name"> + <cstring>spacer7_2</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Fixed</enum> + </property> + <property name="sizeHint"> + <size> + <width>21</width> + <height>21</height> + </size> + </property> + </spacer> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel1_2</cstring> + </property> + <property name="text"> + <string><i>For more options, use the "Configure Gwenview" dialog</i></string> + </property> + </widget> + </vbox> +</widget> +<layoutdefaults spacing="6" margin="11"/> +</UI> diff --git a/src/gvcore/thumbnailloadjob.cpp b/src/gvcore/thumbnailloadjob.cpp new file mode 100644 index 0000000..92290ac --- /dev/null +++ b/src/gvcore/thumbnailloadjob.cpp @@ -0,0 +1,763 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* Gwenview - A simple image viewer for KDE + Copyright 2000-2004 Aurélien Gâteau + This class is based on the ImagePreviewJob class from Konqueror. + Original copyright follows. +*/ +/* This file is part of the KDE project + Copyright (C) 2000 David Faure <faure@kde.org> + 2000 Carsten Pfeiffer <pfeiffer@kde.org> + + 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ +#include "thumbnailloadjob.moc" + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <assert.h> + +// Qt +#include <qdir.h> +#include <qfile.h> +#include <qimage.h> +#include <qpixmap.h> +#include <qtimer.h> + +// KDE +#include <kapplication.h> +#include <kdebug.h> +#include <kfileitem.h> +#include <kiconloader.h> +#include <kio/previewjob.h> +#include <klargefile.h> +#include <kmdcodec.h> +#include <kstandarddirs.h> +#include <ktempfile.h> + +// libjpeg +#include <setjmp.h> +#define XMD_H +extern "C" { +#include <jpeglib.h> +} + +// Local +#include "cache.h" +#include "mimetypeutils.h" +#include "miscconfig.h" +#include "imageutils/jpegcontent.h" +#include "imageutils/imageutils.h" +#include "thumbnailsize.h" +#include "fileviewconfig.h" +namespace Gwenview { + +#undef ENABLE_LOG +#undef LOG +//#define ENABLE_LOG +#ifdef ENABLE_LOG +#define LOG(x) kdDebug() << k_funcinfo << x << endl +#else +#define LOG(x) ; +#endif + + +static QString generateOriginalURI(KURL url) { + // Don't include the password if any + url.setPass(QString::null); + return url.url(); +} + + +static QString generateThumbnailPath(const QString& uri, int size) { + KMD5 md5( QFile::encodeName(uri) ); + QString baseDir=ThumbnailLoadJob::thumbnailBaseDir(size); + return baseDir + QString(QFile::encodeName( md5.hexDigest())) + ".png"; +} + +//------------------------------------------------------------------------ +// +// ThumbnailThread +// +//------------------------------------------------------------------------ +void ThumbnailThread::load( + const QString& originalURI, time_t originalTime, int originalSize, const QString& originalMimeType, + const QString& pixPath, + const QString& thumbnailPath, + int size, bool storeThumbnail) +{ + QMutexLocker lock( &mMutex ); + assert( mPixPath.isNull()); + + mOriginalURI = TSDeepCopy(originalURI); + mOriginalTime = originalTime; + mOriginalSize = originalSize; + mOriginalMimeType = TSDeepCopy(originalMimeType); + mPixPath = TSDeepCopy(pixPath); + mThumbnailPath = TSDeepCopy(thumbnailPath); + mThumbnailSize = size; + mStoreThumbnailsInCache = storeThumbnail; + if(!running()) start(); + mCond.wakeOne(); +} + +void ThumbnailThread::run() { + QMutexLocker lock( &mMutex ); + while( !testCancel()) { + // empty mPixPath means nothing to do + while( mPixPath.isNull()) { + mCond.cancellableWait( &mMutex ); + if( testCancel()) return; + } + loadThumbnail(); + mPixPath = QString(); // done, ready for next + QSize size(mOriginalWidth, mOriginalHeight); + emitCancellableSignal( this, SIGNAL( done( const QImage&, const QSize&)), mImage, size); + } +} + +void ThumbnailThread::loadThumbnail() { + mImage = QImage(); + bool loaded=false; + bool needCaching=true; + + // If it's a JPEG, try to load a small image directly from the file + if (isJPEG()) { + ImageUtils::JPEGContent content; + content.load(mPixPath); + mOriginalWidth = content.size().width(); + mOriginalHeight = content.size().height(); + mImage = content.thumbnail(); + + if( !mImage.isNull() + && ( mImage.width() >= mThumbnailSize // don't use small thumbnails + || mImage.height() >= mThumbnailSize )) { + loaded = true; + needCaching = false; + } + if(!loaded) { + loaded=loadJPEG(); + } + if (loaded && MiscConfig::autoRotateImages()) { + // Rotate if necessary + ImageUtils::Orientation orientation = content.orientation(); + mImage=ImageUtils::transform(mImage,orientation); + } + } + // File is not a JPEG, or JPEG optimized load failed, load file using Qt + if (!loaded) { + QImage originalImage; + if (originalImage.load(mPixPath)) { + mOriginalWidth=originalImage.width(); + mOriginalHeight=originalImage.height(); + int thumbSize=mThumbnailSize<=ThumbnailSize::NORMAL ? ThumbnailSize::NORMAL : ThumbnailSize::LARGE; + + if( testCancel()) return; + + if (QMAX(mOriginalWidth, mOriginalHeight)<=thumbSize ) { + mImage=originalImage; + needCaching = false; + } else { + mImage=ImageUtils::scale(originalImage,thumbSize,thumbSize,ImageUtils::SMOOTH_FAST,QImage::ScaleMin); + } + loaded = true; + } + } + + if( testCancel()) return; + + if( mStoreThumbnailsInCache && needCaching ) { + mImage.setText("Thumb::URI", 0, mOriginalURI); + mImage.setText("Thumb::MTime", 0, QString::number(mOriginalTime)); + mImage.setText("Thumb::Size", 0, QString::number(mOriginalSize)); + mImage.setText("Thumb::Mimetype", 0, mOriginalMimeType); + mImage.setText("Thumb::Image::Width", 0, QString::number(mOriginalWidth)); + mImage.setText("Thumb::Image::Height", 0, QString::number(mOriginalHeight)); + mImage.setText("Software", 0, "Gwenview"); + + QString thumbnailDir = ThumbnailLoadJob::thumbnailBaseDir(mThumbnailSize); + KStandardDirs::makeDir(thumbnailDir, 0700); + + KTempFile tmp(thumbnailDir + "/gwenview", ".png"); + tmp.setAutoDelete(true); + if (tmp.status()!=0) { + QString reason( strerror(tmp.status()) ); + kdWarning() << "Could not create a temporary file.\nReason: " << reason << endl; + return; + } + + if (!mImage.save(tmp.name(), "PNG")) { + kdWarning() << "Could not save thumbnail for file " << mOriginalURI << endl; + return; + } + + rename(QFile::encodeName(tmp.name()), QFile::encodeName(mThumbnailPath)); + } +} + + +bool ThumbnailThread::isJPEG() { + QString format=QImageIO::imageFormat(mPixPath); + return format=="JPEG"; +} + + + +struct JPEGFatalError : public jpeg_error_mgr { + jmp_buf mJmpBuffer; + + static void handler(j_common_ptr cinfo) { + JPEGFatalError* error=static_cast<JPEGFatalError*>(cinfo->err); + (error->output_message)(cinfo); + longjmp(error->mJmpBuffer,1); + } +}; + +bool ThumbnailThread::loadJPEG() { + struct jpeg_decompress_struct cinfo; + + // Open file + FILE* inputFile=fopen(QFile::encodeName( mPixPath ).data(), "rb"); + if(!inputFile) return false; + + // Error handling + struct JPEGFatalError jerr; + cinfo.err = jpeg_std_error(&jerr); + cinfo.err->error_exit = JPEGFatalError::handler; + if (setjmp(jerr.mJmpBuffer)) { + jpeg_destroy_decompress(&cinfo); + fclose(inputFile); + return false; + } + + // Init decompression + jpeg_create_decompress(&cinfo); + jpeg_stdio_src(&cinfo, inputFile); + jpeg_read_header(&cinfo, TRUE); + + // Get image size and check if we need a thumbnail + int size= mThumbnailSize <= ThumbnailSize::NORMAL ? ThumbnailSize::NORMAL : ThumbnailSize::LARGE; + int imgSize = QMAX(cinfo.image_width, cinfo.image_height); + + if (imgSize<=size) { + fclose(inputFile); + return mImage.load(mPixPath); + } + + // Compute scale value + int scale=1; + while(size*scale*2<=imgSize) { + scale*=2; + } + if(scale>8) scale=8; + + cinfo.scale_num=1; + cinfo.scale_denom=scale; + + // Create QImage + jpeg_start_decompress(&cinfo); + + switch(cinfo.output_components) { + case 3: + case 4: + mImage.create( cinfo.output_width, cinfo.output_height, 32 ); + break; + case 1: // B&W image + mImage.create( cinfo.output_width, cinfo.output_height, 8, 256 ); + for (int i=0; i<256; i++) + mImage.setColor(i, qRgb(i,i,i)); + break; + default: + jpeg_destroy_decompress(&cinfo); + fclose(inputFile); + return false; + } + + uchar** lines = mImage.jumpTable(); + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, lines + cinfo.output_scanline, cinfo.output_height); + } + jpeg_finish_decompress(&cinfo); + +// Expand 24->32 bpp + if ( cinfo.output_components == 3 ) { + for (uint j=0; j<cinfo.output_height; j++) { + uchar *in = mImage.scanLine(j) + cinfo.output_width*3; + QRgb *out = (QRgb*)( mImage.scanLine(j) ); + + for (uint i=cinfo.output_width; i--; ) { + in-=3; + out[i] = qRgb(in[0], in[1], in[2]); + } + } + } + + int newMax = QMAX(cinfo.output_width, cinfo.output_height); + int newx = size*cinfo.output_width / newMax; + int newy = size*cinfo.output_height / newMax; + + mImage=ImageUtils::scale(mImage,newx, newy,ImageUtils::SMOOTH_FAST); + + jpeg_destroy_decompress(&cinfo); + fclose(inputFile); + + return true; +} + + +//------------------------------------------------------------------------ +// +// ThumbnailLoadJob static methods +// +//------------------------------------------------------------------------ +QString ThumbnailLoadJob::thumbnailBaseDir() { + static QString dir; + if (!dir.isEmpty()) return dir; + dir=QDir::homeDirPath() + "/.thumbnails/"; + return dir; +} + + +QString ThumbnailLoadJob::thumbnailBaseDir(int size) { + QString dir = thumbnailBaseDir(); + if (size<=ThumbnailSize::NORMAL) { + dir+="normal/"; + } else { + dir+="large/"; + } + return dir; +} + + +void ThumbnailLoadJob::deleteImageThumbnail(const KURL& url) { + QString uri=generateOriginalURI(url); + QFile::remove(generateThumbnailPath(uri, ThumbnailSize::NORMAL)); + QFile::remove(generateThumbnailPath(uri, ThumbnailSize::LARGE)); +} + + +//------------------------------------------------------------------------ +// +// ThumbnailLoadJob implementation +// +//------------------------------------------------------------------------ + + +/* + + This class tries to first generate the most important thumbnails, i.e. + first the currently selected one, then the ones that are visible, and then + the rest, the closer the the currently selected one the sooner + + mAllItems contains all thumbnails + mItems contains pending thumbnails, in the priority order + mCurrentItem is currently processed thumbnail, already removed from mItems + mProcessedState needs to match mAllItems, and contains information about every + thumbnail whether it has been already processed + + thumbnailIndex() returns index of a thumbnail in mAllItems, or -1 + updateItemsOrder() builds mItems from mAllItems +*/ + +ThumbnailLoadJob::ThumbnailLoadJob(const QValueVector<const KFileItem*>* items, int size) +: KIO::Job(false), mState( STATE_NEXTTHUMB ), + mCurrentVisibleIndex( -1 ), mFirstVisibleIndex( -1 ), mLastVisibleIndex( -1 ), + mThumbnailSize(size), mSuspended( false ) +{ + LOG(""); + + mBrokenPixmap=KGlobal::iconLoader()->loadIcon("file_broken", + KIcon::NoGroup, ThumbnailSize::MIN); + + // Look for images and store the items in our todo list + Q_ASSERT(!items->empty()); + mAllItems=*items; + mProcessedState.resize( mAllItems.count()); + qFill( mProcessedState.begin(), mProcessedState.end(), false ); + mCurrentItem = NULL; + + connect(&mThumbnailThread, SIGNAL(done(const QImage&, const QSize&)), + SLOT(thumbnailReady(const QImage&, const QSize&)) ); + Cache::instance()->updateAge(); // see addThumbnail in Cache +} + + +ThumbnailLoadJob::~ThumbnailLoadJob() { + LOG(""); + mThumbnailThread.cancel(); + mThumbnailThread.wait(); +} + + +void ThumbnailLoadJob::start() { + // build mItems from mAllItems if not done yet + if (mLastVisibleIndex == -1 ) { + setPriorityItems( NULL, NULL, NULL ); + } + if (mItems.isEmpty()) { + LOG("Nothing to do"); + emit result(this); + delete this; + return; + } + + determineNextIcon(); +} + +void ThumbnailLoadJob::suspend() { + mSuspended = true; +} + +void ThumbnailLoadJob::resume() { + if( !mSuspended ) return; + mSuspended = false; + if( mState == STATE_NEXTTHUMB ) // don't load next while already loading + determineNextIcon(); +} + +//-Internal-------------------------------------------------------------- +void ThumbnailLoadJob::appendItem(const KFileItem* item) { + int index = thumbnailIndex( item ); + if( index >= 0 ) { + mProcessedState[ index ] = false; + return; + } + mAllItems.append(item); + mProcessedState.append( false ); + updateItemsOrder(); +} + + +void ThumbnailLoadJob::itemRemoved(const KFileItem* item) { + Q_ASSERT(item); + + // If we are removing the next item, update to be the item after or the + // first if we removed the last item + mItems.remove( item ); + int index = thumbnailIndex( item ); + if( index >= 0 ) { + mAllItems.erase( mAllItems.begin() + index ); + mProcessedState.erase( mProcessedState.begin() + index ); + } + + if (item == mCurrentItem) { + // Abort + mCurrentItem = NULL; + if (subjobs.first()) { + subjobs.first()->kill(); + subjobs.removeFirst(); + } + determineNextIcon(); + } +} + + +void ThumbnailLoadJob::setPriorityItems(const KFileItem* current, const KFileItem* first, const KFileItem* last) { + if( mAllItems.isEmpty()) { + mCurrentVisibleIndex = mFirstVisibleIndex = mLastVisibleIndex = 0; + return; + } + mFirstVisibleIndex = -1; + mLastVisibleIndex = - 1; + mCurrentVisibleIndex = -1; + if( first != NULL ) mFirstVisibleIndex = thumbnailIndex( first ); + if( last != NULL ) mLastVisibleIndex = thumbnailIndex( last ); + if( current != NULL ) mCurrentVisibleIndex = thumbnailIndex( current ); + if( mFirstVisibleIndex == -1 ) mFirstVisibleIndex = 0; + if( mLastVisibleIndex == -1 ) mLastVisibleIndex = mAllItems.count() - 1; + if( mCurrentVisibleIndex == -1 ) mCurrentVisibleIndex = mFirstVisibleIndex; + updateItemsOrder(); +} + +void ThumbnailLoadJob::updateItemsOrder() { + mItems.clear(); + int forward = mCurrentVisibleIndex + 1; + int backward = mCurrentVisibleIndex; + int first = mFirstVisibleIndex; + int last = mLastVisibleIndex; + updateItemsOrderHelper( forward, backward, first, last ); + if( first != 0 || last != int( mAllItems.count()) - 1 ) { + // add non-visible items + updateItemsOrderHelper( last + 1, first - 1, 0, mAllItems.count() - 1); + } +} + +void ThumbnailLoadJob::updateItemsOrderHelper( int forward, int backward, int first, int last ) { + // start from the current item, add one following it, and one preceding it, for all visible items + while( forward <= last || backward >= first ) { + // start with backward - that's the curent item for the first time + while( backward >= first ) { + if( !mProcessedState[ backward ] ) { + mItems.append( mAllItems[ backward ] ); + --backward; + break; + } + --backward; + } + while( forward <= last ) { + if( !mProcessedState[ forward ] ) { + mItems.append( mAllItems[ forward ] ); + ++forward; + break; + } + ++forward; + } + } +} + +void ThumbnailLoadJob::determineNextIcon() { + mState = STATE_NEXTTHUMB; + if( mSuspended ) { + return; + } + + // No more items ? + if (mItems.isEmpty()) { + // Done + LOG("emitting result"); + emit result(this); + delete this; + return; + } + + mCurrentItem=mItems.first(); + mItems.pop_front(); + Q_ASSERT( !mProcessedState[ thumbnailIndex( mCurrentItem )] ); + mProcessedState[ thumbnailIndex( mCurrentItem )] = true; + + // First, stat the orig file + mState = STATE_STATORIG; + mOriginalTime = 0; + mCurrentURL = mCurrentItem->url(); + mCurrentURL.cleanPath(); + + // Do direct stat instead of using KIO if the file is local (faster) + if( mCurrentURL.isLocalFile() + && !KIO::probably_slow_mounted( mCurrentURL.path())) { + KDE_struct_stat buff; + if ( KDE_stat( QFile::encodeName(mCurrentURL.path()), &buff ) == 0 ) { + mOriginalTime = buff.st_mtime; + QTimer::singleShot( 0, this, SLOT( checkThumbnail())); + } + } + if( mOriginalTime == 0 ) { // KIO must be used + KIO::Job* job = KIO::stat(mCurrentURL,false); + job->setWindow(KApplication::kApplication()->mainWidget()); + LOG( "KIO::stat orig " << mCurrentURL.url() ); + addSubjob(job); + } +} + + +void ThumbnailLoadJob::slotResult(KIO::Job * job) { + LOG(mState); + subjobs.remove(job); + Q_ASSERT(subjobs.isEmpty()); // We should have only one job at a time ... + + switch (mState) { + case STATE_NEXTTHUMB: + Q_ASSERT(false); + determineNextIcon(); + return; + + case STATE_STATORIG: { + // Could not stat original, drop this one and move on to the next one + if (job->error()) { + emitThumbnailLoadingFailed(); + determineNextIcon(); + return; + } + + // Get modification time of the original file + KIO::UDSEntry entry = static_cast<KIO::StatJob*>(job)->statResult(); + KIO::UDSEntry::ConstIterator it= entry.begin(); + mOriginalTime = 0; + for (; it!=entry.end(); ++it) { + if ((*it).m_uds == KIO::UDS_MODIFICATION_TIME) { + mOriginalTime = (time_t)((*it).m_long); + break; + } + } + checkThumbnail(); + return; + } + + case STATE_DOWNLOADORIG: + if (job->error()) { + emitThumbnailLoadingFailed(); + LOG("Delete temp file " << mTempPath); + QFile::remove(mTempPath); + mTempPath = QString::null; + determineNextIcon(); + } else { + startCreatingThumbnail(mTempPath); + } + return; + + case STATE_PREVIEWJOB: + determineNextIcon(); + return; + } +} + + +void ThumbnailLoadJob::thumbnailReady( const QImage& im, const QSize& _size) { + QImage img = TSDeepCopy( im ); + QSize size = _size; + if ( !img.isNull()) { + emitThumbnailLoaded(img, size); + } else { + emitThumbnailLoadingFailed(); + } + if( !mTempPath.isEmpty()) { + LOG("Delete temp file " << mTempPath); + QFile::remove(mTempPath); + mTempPath = QString::null; + } + determineNextIcon(); +} + +void ThumbnailLoadJob::checkThumbnail() { + // If we are in the thumbnail dir, just load the file + if (mCurrentURL.isLocalFile() + && mCurrentURL.directory(false).startsWith(thumbnailBaseDir()) ) + { + QImage image(mCurrentURL.path()); + emitThumbnailLoaded(image, image.size()); + determineNextIcon(); + return; + } + QSize imagesize; + if( mOriginalTime == time_t( Cache::instance()->timestamp( mCurrentURL ).toTime_t())) { + QPixmap cached = Cache::instance()->thumbnail( mCurrentURL, imagesize ); + if( !cached.isNull()) { + emit thumbnailLoaded(mCurrentItem, cached, imagesize); + determineNextIcon(); + return; + } + } + + mOriginalURI=generateOriginalURI(mCurrentURL); + mThumbnailPath=generateThumbnailPath(mOriginalURI, mThumbnailSize); + + LOG("Stat thumb " << mThumbnailPath); + + QImage thumb; + if ( thumb.load(mThumbnailPath) ) { + if (thumb.text("Thumb::URI", 0) == mOriginalURI && + thumb.text("Thumb::MTime", 0).toInt() == mOriginalTime ) + { + int width=0, height=0; + QSize size; + bool ok; + + width=thumb.text("Thumb::Image::Width", 0).toInt(&ok); + if (ok) height=thumb.text("Thumb::Image::Height", 0).toInt(&ok); + if (ok) { + size=QSize(width, height); + } else { + LOG("Thumbnail for " << mOriginalURI << " does not contain correct image size information"); + KFileMetaInfo fmi(mCurrentURL); + if (fmi.isValid()) { + KFileMetaInfoItem item=fmi.item("Dimensions"); + if (item.isValid()) { + size=item.value().toSize(); + } else { + LOG("KFileMetaInfoItem for " << mOriginalURI << " did not get image size information"); + } + } else { + LOG("Could not get a valid KFileMetaInfo instance for " << mOriginalURI); + } + } + emitThumbnailLoaded(thumb, size); + determineNextIcon(); + return; + } + } + + // Thumbnail not found or not valid + if ( MimeTypeUtils::rasterImageMimeTypes().contains(mCurrentItem->mimetype()) ) { + // This is a raster image + if (mCurrentURL.isLocalFile()) { + // Original is a local file, create the thumbnail + startCreatingThumbnail(mCurrentURL.path()); + } else { + // Original is remote, download it + mState=STATE_DOWNLOADORIG; + KTempFile tmpFile; + mTempPath=tmpFile.name(); + KURL url; + url.setPath(mTempPath); + KIO::Job* job=KIO::file_copy(mCurrentURL, url,-1,true,false,false); + job->setWindow(KApplication::kApplication()->mainWidget()); + LOG("Download remote file " << mCurrentURL.prettyURL()); + addSubjob(job); + } + } else { + // Not a raster image, use a KPreviewJob + mState=STATE_PREVIEWJOB; + KFileItemList list; + list.append(mCurrentItem); + KIO::Job* job=KIO::filePreview(list, mThumbnailSize); + job->setWindow(KApplication::kApplication()->mainWidget()); + connect(job, SIGNAL(gotPreview(const KFileItem*, const QPixmap&)), + this, SLOT(slotGotPreview(const KFileItem*, const QPixmap&)) ); + connect(job, SIGNAL(failed(const KFileItem*)), + this, SLOT(emitThumbnailLoadingFailed()) ); + addSubjob(job); + return; + } +} + +void ThumbnailLoadJob::startCreatingThumbnail(const QString& pixPath) { + LOG("Creating thumbnail from " << pixPath); + mThumbnailThread.load( mOriginalURI, mOriginalTime, mCurrentItem->size(), + mCurrentItem->mimetype(), pixPath, mThumbnailPath, mThumbnailSize, + FileViewConfig::storeThumbnailsInCache()); +} + + +void ThumbnailLoadJob::slotGotPreview(const KFileItem* item, const QPixmap& pixmap) { + LOG(""); + QSize size; + emit thumbnailLoaded(item, pixmap, size); +} + + +void ThumbnailLoadJob::emitThumbnailLoaded(const QImage& img, QSize size) { + int biggestDimension=QMAX(img.width(), img.height()); + + QImage thumbImg; + if (biggestDimension>mThumbnailSize) { + // Scale down thumbnail if necessary + thumbImg=ImageUtils::scale(img,mThumbnailSize, mThumbnailSize, ImageUtils::SMOOTH_FAST,QImage::ScaleMin); + } else { + thumbImg=img; + } + QDateTime tm; + tm.setTime_t( mOriginalTime ); + QPixmap thumb( thumbImg ); // store as QPixmap in cache (faster to retrieve, no conversion needed) + Cache::instance()->addThumbnail( mCurrentURL, thumb, size, tm ); + emit thumbnailLoaded(mCurrentItem, thumb, size); +} + + +void ThumbnailLoadJob::emitThumbnailLoadingFailed() { + QSize size; + emit thumbnailLoaded(mCurrentItem, mBrokenPixmap, size); +} + + +} // namespace diff --git a/src/gvcore/thumbnailloadjob.h b/src/gvcore/thumbnailloadjob.h new file mode 100644 index 0000000..331708d --- /dev/null +++ b/src/gvcore/thumbnailloadjob.h @@ -0,0 +1,211 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab +/* Gwenview - A simple image viewer for KDE + Copyright 2000-2004 Aurélien Gâteau + This class is based on the ImagePreviewJob class from Konqueror. + Original copyright follows. +*/ +/* This file is part of the KDE project + Copyright (C) 2000 David Faure <faure@kde.org> + + 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#ifndef THUMBNAILLOADJOB_H +#define THUMBNAILLOADJOB_H + +// Qt +#include <qimage.h> +#include <qpixmap.h> +#include <qvaluelist.h> +#include <qvaluevector.h> + +// KDE +#include <kio/job.h> + +// Local +#include "tsthread/tsthread.h" +#include "tsthread/tswaitcondition.h" + +#include "libgwenview_export.h" +class KConfig; +class KFileItem; + +typedef QPtrList<KFileItem> KFileItemList; + +namespace Gwenview { +class ThumbnailThread : public TSThread { +Q_OBJECT +public: + void load( + const QString& originalURI, + time_t originalTime, + int originalSize, + const QString& originalMimeType, + const QString& pixPath, + const QString& thumbnailPath, + int size, + bool storeThumbnail); +protected: + virtual void run(); +signals: + void done( const QImage&, const QSize&); +private: + bool isJPEG(); + bool loadJPEG(); + void loadThumbnail(); + QImage mImage; + QString mPixPath; + QString mThumbnailPath; + QString mOriginalURI; + time_t mOriginalTime; + int mOriginalSize; + QString mOriginalMimeType; + int mOriginalWidth; + int mOriginalHeight; + QMutex mMutex; + TSWaitCondition mCond; + int mThumbnailSize; + bool mStoreThumbnailsInCache; +}; + +/** + * A job that determines the thumbnails for the images in the current directory + */ +class LIBGWENVIEW_EXPORT ThumbnailLoadJob : public KIO::Job { +Q_OBJECT +public: + /** + * Create a job for determining the pixmaps of the images in the @p itemList + */ + ThumbnailLoadJob(const QValueVector<const KFileItem*>* itemList, int size); + virtual ~ThumbnailLoadJob(); + + /** + * Call this to get started + */ + void start(); + + /** + * To be called whenever an item is removed from the view + */ + void itemRemoved(const KFileItem* item); + + + /** + * Add an item to a running job + */ + void appendItem(const KFileItem* item); + + + /** + * Sets items in range first..last to be generated first, starting with current. + */ + void setPriorityItems(const KFileItem* current, const KFileItem* first, const KFileItem* last); + + /** + * Temporarily suspends loading. Used if there's a more + * important action going on (loading an image etc.). + */ + void suspend(); + + /** + * Resumes loading if suspended. + */ + void resume(); + + /** + * Returns the thumbnail base dir, independent of the thumbnail size + */ + static QString thumbnailBaseDir(); + + /** + * Returns the thumbnail base dir, for the @p size + */ + static QString thumbnailBaseDir(int size); + + + /** + * Delete the thumbnail for the @p url + */ + static void deleteImageThumbnail(const KURL& url); + +signals: + /** + * Emitted when the thumbnail for the @p item has been loaded + */ + void thumbnailLoaded(const KFileItem* item, const QPixmap&, const QSize&); + +private slots: + void slotResult( KIO::Job *job ); + void slotGotPreview(const KFileItem*, const QPixmap&); + void checkThumbnail(); + void thumbnailReady(const QImage& im, const QSize&); + void emitThumbnailLoadingFailed(); + +private: + enum { STATE_STATORIG, STATE_DOWNLOADORIG, STATE_PREVIEWJOB, STATE_NEXTTHUMB } mState; + + QValueList<const KFileItem*> mItems; + QValueVector<const KFileItem* > mAllItems; + QValueVector< bool > mProcessedState; + const KFileItem *mCurrentItem; + int thumbnailIndex( const KFileItem* item ) const; + void updateItemsOrder(); + + // indexes of the current, fist and last visible thumbnails + int mCurrentVisibleIndex, mFirstVisibleIndex, mLastVisibleIndex; + + // The URL of the current item (always equivalent to m_items.first()->item()->url()) + KURL mCurrentURL; + + // The URI of the original image (might be different from mCurrentURL.url()) + QString mOriginalURI; + + // The modification time of the original image + time_t mOriginalTime; + + // The thumbnail path + QString mThumbnailPath; + + // The temporary path for remote urls + QString mTempPath; + + // Thumbnail size + int mThumbnailSize; + + QPixmap mBrokenPixmap; + + bool mSuspended; + + ThumbnailThread mThumbnailThread; + + void determineNextIcon(); + void startCreatingThumbnail(const QString& path); + + void emitThumbnailLoaded(const QImage& img, QSize size); + + void updateItemsOrderHelper( int forward, int backward, int first, int last ); +}; + +inline +int ThumbnailLoadJob::thumbnailIndex( const KFileItem* item ) const { + QValueVector<const KFileItem* >::ConstIterator pos = qFind( mAllItems.begin(), mAllItems.end(), item ); + if( pos != mAllItems.end()) return pos - mAllItems.begin(); + return -1; +} + +} // namespace +#endif + diff --git a/src/gvcore/thumbnailsize.h b/src/gvcore/thumbnailsize.h new file mode 100644 index 0000000..400c22e --- /dev/null +++ b/src/gvcore/thumbnailsize.h @@ -0,0 +1,42 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* Gwenview - A simple image viewer for KDE + Copyright 2000-2004 Aurélien Gâteau + This class is based on the ImagePreviewJob class from Konqueror. + Original copyright follows. +*/ +/* This file is part of the KDE project + Copyright (C) 2000 David Faure <faure@kde.org> + 2000 Carsten Pfeiffer <pfeiffer@kde.org> + + 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ +#ifndef THUMBNAILSIZE_H +#define THUMBNAILSIZE_H + +namespace Gwenview { + +/** + * This namespace stores constants used by several files. It avoids multiple + * includes. + */ +namespace ThumbnailSize { + const int MIN=48; + const int NORMAL=128; + const int LARGE=256; +} + +} // namespace +#endif /* THUMBNAILSIZE_H */ + diff --git a/src/gvcore/timeutils.cpp b/src/gvcore/timeutils.cpp new file mode 100644 index 0000000..d79c24d --- /dev/null +++ b/src/gvcore/timeutils.cpp @@ -0,0 +1,53 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2006 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#include "timeutils.h" + +// KDE +#include <kdebug.h> +#include <kfileitem.h> +#include <kfilemetainfo.h> +#include <kglobal.h> +#include <klocale.h> + +namespace Gwenview { +namespace TimeUtils { + +time_t getTime(const KFileItem* item) { + const KFileMetaInfo& info = item->metaInfo(); + if (info.isValid()) { + QVariant value = info.value("Date/time"); + QDateTime dt = value.toDateTime(); + if (dt.isValid()) { + return dt.toTime_t(); + } + } + return item->time(KIO::UDS_MODIFICATION_TIME); +} + +QString formatTime(time_t time) { + QDateTime dt; + dt.setTime_t(time); + return KGlobal::locale()->formatDateTime(dt); +} + + +} // namespace TimeUtils +} // namespace Gwenview diff --git a/src/gvcore/timeutils.h b/src/gvcore/timeutils.h new file mode 100644 index 0000000..5815965 --- /dev/null +++ b/src/gvcore/timeutils.h @@ -0,0 +1,44 @@ +// vim: set tabstop=4 shiftwidth=4 noexpandtab: +/* +Gwenview - A simple image viewer for KDE +Copyright 2000-2006 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +#ifndef TIMEUTILS_H +#define TIMEUTILS_H + +#include <time.h> + +class KFileItem; +class QString; + +namespace Gwenview { +namespace TimeUtils { + +/** + * Returns the time of an item, using EXIF info if available + */ +time_t getTime(const KFileItem*); + +QString formatTime(time_t); + +} // namespace TimeUtils +} // namespace Gwenview + + + +#endif /* TIMEUTILS_H */ diff --git a/src/gvcore/xcursor.cpp b/src/gvcore/xcursor.cpp new file mode 100644 index 0000000..79df377 --- /dev/null +++ b/src/gvcore/xcursor.cpp @@ -0,0 +1,190 @@ +/* This file is part of the KDE libraries + Copyright (C) 2003 Fredrik Höglund <fredrik@kde.org> + Copyright (C) 2005 Lubos Lunak <l.lunak@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include <config.h> + +#include <assert.h> + +#include "xcursor.h" + +#include <qimage.h> +#include <kdebug.h> +#include <qvaluevector.h> + +#ifdef GV_HAVE_XCURSOR +#include <X11/Xlib.h> +#include <X11/Xcursor/Xcursor.h> +#include <fixx11h.h> +#endif + +namespace Gwenview { + +#ifdef GV_HAVE_XCURSOR +class XCursorFormat : public QImageFormat { +public: + XCursorFormat(); + virtual int decode(QImage& img, QImageConsumer* consumer, + const uchar* buffer, int length); + QByteArray array; + int pos; + bool was_seek_error; +}; + +extern "C" +int xcursor_read( XcursorFile* file, unsigned char* buf, int len ) +{ + XCursorFormat* data = reinterpret_cast< XCursorFormat* >( file->closure ); + if( int( data->array.size()) - data->pos < len ) + { + data->was_seek_error = true; + len = data->array.size() - data->pos; + } + memcpy( buf, data->array.data() + data->pos, len ); + data->pos += len; + return len; +} + +extern "C" +int xcursor_write( XcursorFile*, unsigned char*, int ) +{ + assert( false ); // not write support + return 0; +} + +extern "C" +int xcursor_seek( XcursorFile* file, long offset, int whence ) +{ + XCursorFormat* data = reinterpret_cast< XCursorFormat* >( file->closure ); + if( whence == SEEK_CUR ) + offset += data->pos; + else if( whence == SEEK_END ) + offset = data->array.size() + offset; + if( offset < 0 || offset >= int( data->array.size())) + { + data->was_seek_error = true; + return -1; + } + data->pos = offset; + return 0; +} + +XCursorFormat::XCursorFormat() +{ +} + +int XCursorFormat::decode(QImage& img, QImageConsumer* cons, + const uchar* buffer, int length) +{ + if( length > 0 ) + { + int old_size = array.size(); + array.resize( old_size + length ); + memcpy( array.data() + old_size, buffer, length ); + } +// Qt's image async IO and Xcursor library have so nicely incompatible data reading :-/ +// Xcursor can read a file only as a whole. One can provide XcursorFile with functions +// for reading, writing and seeking. And seeking is the stupid part on Xcursor's side, +// as there's no way to suspend reading until more data is available. This means we +// basically have to read the whole file first. However, Qt's image async IO just keeps +// feeding data and when there's end of the data it just does nothing and doesn't tell +// the decoder. +// So the trick will be calling XcursorXcFileLoadImage() but whenever there will be +// a seeking/reading error, then the function will fail and we'll try again when there's +// more data. Also, reading everything first means we can't return without processing +// further data after reaching end of a frame in the input as decode() doc says, but oh well. + XcursorFile file; + file.closure = this; + file.read = xcursor_read; + file.write = xcursor_write; + file.seek = xcursor_seek; + pos = 0; + was_seek_error = false; + XcursorImages *cursors = XcursorXcFileLoadImages( &file, 1024 ); // largest possible cursor size + if ( cursors ) + { + for( int cur = 0; + cur < cursors->nimage; + ++cur ) + { + XcursorImage* cursor = cursors->images[ cur ]; + img = QImage( reinterpret_cast<uchar *>( cursor->pixels ), + cursor->width, cursor->height, 32, NULL, 0, QImage::BigEndian ); + img.setAlphaBuffer( true ); + // Convert the image to non-premultiplied alpha + Q_UINT32 *pixels = reinterpret_cast<Q_UINT32 *>( img.bits() ); + for ( int i = 0; i < (img.width() * img.height()); i++ ) + { + float alpha = qAlpha( pixels[i] ) / 255.0; + if ( alpha > 0.0 && alpha < 1.0 ) + pixels[i] = qRgba( int( qRed(pixels[i]) / alpha ), + int( qGreen(pixels[i]) / alpha ), + int( qBlue(pixels[i]) / alpha ), + qAlpha(pixels[i]) ); + } + // Create a deep copy of the image so the image data is preserved + img = img.copy(); + if( cons ) + { + if( cur == 0 ) + { + cons->setSize( img.width(), img.height()); + if( cursors->nimage > 1 ) + cons->setLooping( 0 ); + } + cons->changed( QRect( QPoint( 0, 0 ), img.size())); + cons->frameDone(); + cons->setFramePeriod( cursor->delay ); + } + } + XcursorImagesDestroy( cursors ); + if( cons ) + cons->end(); + return length; + } + else if( was_seek_error ) // try again next time + return length; + return -1; // failure +} +#endif + +QImageFormat* XCursorFormatType::decoderFor( + const uchar* buffer, int length) +{ +#ifdef GV_HAVE_XCURSOR + if (length < 4) return 0; + if (buffer[0]=='X' + && buffer[1]=='c' + && buffer[2]=='u' + && buffer[3]=='r') + return new XCursorFormat; +#else + Q_UNUSED( buffer ); + Q_UNUSED( length ); +#endif + return 0; +} + +const char* XCursorFormatType::formatName() const +{ + return "XCursor"; +} + + +} // namespace diff --git a/src/gvcore/xcursor.h b/src/gvcore/xcursor.h new file mode 100644 index 0000000..27bec9a --- /dev/null +++ b/src/gvcore/xcursor.h @@ -0,0 +1,37 @@ +/* This file is part of the KDE libraries + Copyright (C) 2003 Fredrik Höglund <fredrik@kde.org> + Copyright (C) 2005 Lubos Lunak <l.lunak@kde.org> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef gvxcursor_h +#define gvxcursor_h + +#include <qasyncimageio.h> + +namespace Gwenview { + +class XCursorFormatType : public QImageFormatType { +public: + QImageFormat* decoderFor(const uchar* buffer, int length); + const char* formatName() const; +}; + +// ----------------------------------------------------------------------------- +} // namespace + +#endif // gvxcursor_h diff --git a/src/gvcore/xpm.cpp b/src/gvcore/xpm.cpp new file mode 100644 index 0000000..ce7656f --- /dev/null +++ b/src/gvcore/xpm.cpp @@ -0,0 +1,439 @@ +/* This file is based on qt-3.3.2/src/qimage.cpp , plus it includes + * a fix from qt-bugs@ issue #49934. Original copyright follows. + */ +/**************************************************************************** +** +** +** Implementation of QImage and QImageIO classes +** +** Created : 950207 +** +** Copyright (C) 1992-2003 Trolltech AS. All rights reserved. +** +** This file is part of the kernel module of the Qt GUI Toolkit. +** +** This file may be distributed under the terms of the Q Public License +** as defined by Trolltech AS of Norway and appearing in the file +** LICENSE.QPL included in the packaging of this file. +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** Licensees holding valid Qt Enterprise Edition or Qt Professional Edition +** licenses may use this file in accordance with the Qt Commercial License +** Agreement provided with the Software. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See http://www.trolltech.com/pricing.html or email sales@trolltech.com for +** information about Qt Commercial License Agreements. +** See http://www.trolltech.com/qpl/ for QPL licensing information. +** See http://www.trolltech.com/gpl/ for GPL licensing information. +** +** Contact info@trolltech.com if any conditions of this licensing are +** not clear to you. +** +**********************************************************************/ + +/* + + This code is xpm loader copied from qimage.cpp, with changes making it + possible to use this class from another thread. The problem is that + QColor::setNamedColor() isn't reentrant without thread-safe Xlib, + i.e. without XInitThreads() called. Current KDE applications + don't call XInitThreads(), and since it needs to be the very first + Xlib function called, and Gwenview is also a KPart, it's not possible + to rely on Xlib being thread-safe. + + Changes are marked using #ifdef GV_XPM_CHANGES. + +*/ + +#define GV_XPM_CHANGES + +#include "xpm.h" + +#include <qimage.h> +#include <qmap.h> +#include <qregexp.h> + +#include "threadgate.h" + +namespace Gwenview { + +// Qt code start --------------------------- +static QString fbname( const QString &fileName ) // get file basename (sort of) +{ + QString s = fileName; + if ( !s.isEmpty() ) { + int i; + if ( (i = s.findRev('/')) >= 0 ) + s = s.mid( i ); + if ( (i = s.findRev('\\')) >= 0 ) + s = s.mid( i ); + QRegExp r( QString::fromLatin1("[a-zA-Z][a-zA-Z0-9_]*") ); + int p = r.search( s ); + if ( p == -1 ) + s.truncate( 0 ); + else + s = s.mid( p, r.matchedLength() ); + } + if ( s.isEmpty() ) + s = QString::fromLatin1( "dummy" ); + return s; +} + +/***************************************************************************** + XPM image read/write functions + *****************************************************************************/ + + +// Skip until ", read until the next ", return the rest in *buf +// Returns FALSE on error, TRUE on success + +static bool read_xpm_string( QCString &buf, QIODevice *d, + const char * const *source, int &index ) +{ + if ( source ) { + buf = source[index++]; + return TRUE; + } + + if ( buf.size() < 69 ) //# just an approximation + buf.resize( 123 ); + + buf[0] = '\0'; + int c; + int i; + while ( (c=d->getch()) != EOF && c != '"' ) { } + if ( c == EOF ) { + return FALSE; + } + i = 0; + while ( (c=d->getch()) != EOF && c != '"' ) { + if ( i == (int)buf.size() ) + buf.resize( i*2+42 ); + buf[i++] = c; + } + if ( c == EOF ) { + return FALSE; + } + + if ( i == (int)buf.size() ) // always use a 0 terminator + buf.resize( i+1 ); + buf[i] = '\0'; + return TRUE; +} + + + +static int nextColorSpec(const QCString & buf) +{ + int i = buf.find(" c "); + if (i < 0) + i = buf.find(" g "); + if (i < 0) + i = buf.find(" g4 "); + if (i < 0) + i = buf.find(" m "); + if (i < 0) + i = buf.find(" s "); + return i; +} + +// +// INTERNAL +// +// Reads an .xpm from either the QImageIO or from the QString *. +// One of the two HAS to be 0, the other one is used. +// + +static void read_xpm_image_or_array( QImageIO * iio, const char * const * source, + QImage & image) +{ + QCString buf; + QIODevice *d = 0; + buf.resize( 200 ); + + int i, cpp, ncols, w, h, index = 0; + + if ( iio ) { + iio->setStatus( 1 ); + d = iio ? iio->ioDevice() : 0; + d->readLine( buf.data(), buf.size() ); // "/* XPM */" + QRegExp r( QString::fromLatin1("/\\*.XPM.\\*/") ); + if ( buf.find(r) == -1 ) + return; // bad magic + } else if ( !source ) { + return; + } + + if ( !read_xpm_string( buf, d, source, index ) ) + return; + + if ( sscanf( buf, "%d %d %d %d", &w, &h, &ncols, &cpp ) < 4 ) + return; // < 4 numbers parsed + + if ( cpp > 15 ) + return; + + if ( ncols > 256 ) { + image.create( w, h, 32 ); + } else { + image.create( w, h, 8, ncols ); + } + + if (image.isNull()) + return; + + QMap<QString, int> colorMap; + int currentColor; + + for( currentColor=0; currentColor < ncols; ++currentColor ) { + if ( !read_xpm_string( buf, d, source, index ) ) { +#if defined(QT_CHECK_RANGE) + qWarning( "QImage: XPM color specification missing"); +#endif + return; + } + QString index; + index = buf.left( cpp ); + buf = buf.mid( cpp ).simplifyWhiteSpace().lower(); + buf.prepend( " " ); + i = nextColorSpec(buf); + if ( i < 0 ) { +#if defined(QT_CHECK_RANGE) + qWarning( "QImage: XPM color specification is missing: %s", buf.data()); +#endif + return; // no c/g/g4/m/s specification at all + } + buf = buf.mid( i+3 ); + // Strip any other colorspec + int end = nextColorSpec(buf); + if (end != -1) + buf.truncate(end); + buf = buf.stripWhiteSpace(); + if ( buf == "none" ) { + image.setAlphaBuffer( TRUE ); + int transparentColor = currentColor; + if ( image.depth() == 8 ) { + image.setColor( transparentColor, + RGB_MASK & qRgb(198,198,198) ); + colorMap.insert( index, transparentColor ); + } else { + QRgb rgb = RGB_MASK & qRgb(198,198,198); + colorMap.insert( index, rgb ); + } + } else { + if ( ((buf.length()-1) % 3) && (buf[0] == '#') ) { + buf.truncate (((buf.length()-1) / 4 * 3) + 1); // remove alpha channel left by imagemagick + } +#ifdef GV_XPM_CHANGES + QColor c = ThreadGate::instance()->color( buf.data()); +#else + QColor c( buf.data() ); +#endif + if ( image.depth() == 8 ) { + image.setColor( currentColor, 0xff000000 | c.rgb() ); + colorMap.insert( index, currentColor ); + } else { + QRgb rgb = 0xff000000 | c.rgb(); + colorMap.insert( index, rgb ); + } + } + } + + // Read pixels + for( int y=0; y<h; y++ ) { + if ( !read_xpm_string( buf, d, source, index ) ) { +#if defined(QT_CHECK_RANGE) + qWarning( "QImage: XPM pixels missing on image line %d", y); +#endif + return; + } + if ( image.depth() == 8 ) { + uchar *p = image.scanLine(y); + uchar *d = (uchar *)buf.data(); + uchar *end = d + buf.length(); + int x; + if ( cpp == 1 ) { + char b[2]; + b[1] = '\0'; + for ( x=0; x<w && d<end; x++ ) { + b[0] = *d++; + *p++ = (uchar)colorMap[b]; + } + } else { + char b[16]; + b[cpp] = '\0'; + for ( x=0; x<w && d<end; x++ ) { + strncpy( b, (char *)d, cpp ); + *p++ = (uchar)colorMap[b]; + d += cpp; + } + } + } else { + QRgb *p = (QRgb*)image.scanLine(y); + uchar *d = (uchar *)buf.data(); + uchar *end = d + buf.length(); + int x; + char b[16]; + b[cpp] = '\0'; + for ( x=0; x<w && d<end; x++ ) { + strncpy( b, (char *)d, cpp ); + *p++ = (QRgb)colorMap[b]; + d += cpp; + } + } + } + if ( iio ) { + iio->setImage( image ); + iio->setStatus( 0 ); // image ok + } +} + + +static void read_xpm_image( QImageIO * iio ) +{ + QImage i; + (void)read_xpm_image_or_array( iio, 0, i ); + return; +} + + +static const char* xpm_color_name( int cpp, int index ) +{ + static char returnable[5]; + static const char code[] = ".#abcdefghijklmnopqrstuvwxyzABCD" + "EFGHIJKLMNOPQRSTUVWXYZ0123456789"; + // cpp is limited to 4 and index is limited to 64^cpp + if ( cpp > 1 ) { + if ( cpp > 2 ) { + if ( cpp > 3 ) { + returnable[3] = code[index % 64]; + index /= 64; + } else + returnable[3] = '\0'; + returnable[2] = code[index % 64]; + index /= 64; + } else + returnable[2] = '\0'; + // the following 4 lines are a joke! + if ( index == 0 ) + index = 64*44+21; + else if ( index == 64*44+21 ) + index = 0; + returnable[1] = code[index % 64]; + index /= 64; + } else + returnable[1] = '\0'; + returnable[0] = code[index]; + + return returnable; +} + + +// write XPM image data +static void write_xpm_image( QImageIO * iio ) +{ + if ( iio ) + iio->setStatus( 1 ); + else + return; + + // ### 8-bit case could be made faster + QImage image; + if ( iio->image().depth() != 32 ) + image = iio->image().convertDepth( 32 ); + else + image = iio->image(); + + QMap<QRgb, int> colorMap; + + int w = image.width(), h = image.height(), ncolors = 0; + int x, y; + + // build color table + for( y=0; y<h; y++ ) { + QRgb * yp = (QRgb *)image.scanLine( y ); + for( x=0; x<w; x++ ) { + QRgb color = *(yp + x); + if ( !colorMap.contains(color) ) + colorMap.insert( color, ncolors++ ); + } + } + + // number of 64-bit characters per pixel needed to encode all colors + int cpp = 1; + for ( int k = 64; ncolors > k; k *= 64 ) { + ++cpp; + // limit to 4 characters per pixel + // 64^4 colors is enough for a 4096x4096 image + if ( cpp > 4) + break; + } + + QString line; + + // write header + QTextStream s( iio->ioDevice() ); + s << "/* XPM */" << endl + << "static char *" << fbname(iio->fileName()) << "[]={" << endl + << "\"" << w << " " << h << " " << ncolors << " " << cpp << "\""; + + // write palette + QMap<QRgb, int>::Iterator c = colorMap.begin(); + while ( c != colorMap.end() ) { + QRgb color = c.key(); + if ( image.hasAlphaBuffer() && color == (color & RGB_MASK) ) + line.sprintf( "\"%s c None\"", + xpm_color_name(cpp, *c) ); + else + line.sprintf( "\"%s c #%02x%02x%02x\"", + xpm_color_name(cpp, *c), + qRed(color), + qGreen(color), + qBlue(color) ); + ++c; + s << "," << endl << line; + } + + // write pixels, limit to 4 characters per pixel + line.truncate( cpp*w ); + for( y=0; y<h; y++ ) { + QRgb * yp = (QRgb *) image.scanLine( y ); + int cc = 0; + for( x=0; x<w; x++ ) { + int color = (int)(*(yp + x)); + QCString chars = xpm_color_name( cpp, colorMap[color] ); + line[cc++] = chars[0]; + if ( cpp > 1 ) { + line[cc++] = chars[1]; + if ( cpp > 2 ) { + line[cc++] = chars[2]; + if ( cpp > 3 ) { + line[cc++] = chars[3]; + } + } + } + } + s << "," << endl << "\"" << line << "\""; + } + s << "};" << endl; + + iio->setStatus( 0 ); +} + +// Qt code end --------------------------- + +XPM::XPM() +{ + QImageIO::inputFormats(); // trigger registration of Qt's handlers + QImageIO::defineIOHandler( "XPM", "/\\*.XPM.\\*/", "T", + read_xpm_image, write_xpm_image ); +} + +} // namespace diff --git a/src/gvcore/xpm.h b/src/gvcore/xpm.h new file mode 100644 index 0000000..f506ea6 --- /dev/null +++ b/src/gvcore/xpm.h @@ -0,0 +1,54 @@ +/* This file is based on qt-3.3.2/src/qimage.cpp . Original + * copyright follows. + */ +/**************************************************************************** +** +** +** Implementation of QImage and QImageIO classes +** +** Created : 950207 +** +** Copyright (C) 1992-2003 Trolltech AS. All rights reserved. +** +** This file is part of the kernel module of the Qt GUI Toolkit. +** +** This file may be distributed under the terms of the Q Public License +** as defined by Trolltech AS of Norway and appearing in the file +** LICENSE.QPL included in the packaging of this file. +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** Licensees holding valid Qt Enterprise Edition or Qt Professional Edition +** licenses may use this file in accordance with the Qt Commercial License +** Agreement provided with the Software. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See http://www.trolltech.com/pricing.html or email sales@trolltech.com for +** information about Qt Commercial License Agreements. +** See http://www.trolltech.com/qpl/ for QPL licensing information. +** See http://www.trolltech.com/gpl/ for GPL licensing information. +** +** Contact info@trolltech.com if any conditions of this licensing are +** not clear to you. +** +**********************************************************************/ + +#ifndef gvxpm_h +#define gvxpm_h + +namespace Gwenview { + +class XPM { +public: + XPM(); +}; + +// ----------------------------------------------------------------------------- +} // namespace + +#endif // gvxpm_h |