diff --git a/DB/ImageInfo.cpp b/DB/ImageInfo.cpp index 2cc8fef0..c709d41e 100644 --- a/DB/ImageInfo.cpp +++ b/DB/ImageInfo.cpp @@ -1,821 +1,827 @@ /* Copyright (C) 2003-2015 Jesper K. Pedersen 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageInfo.h" #include "FileInfo.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #include using namespace DB; ImageInfo::ImageInfo() :m_null( true ), m_rating(-1), m_stackId(0), m_stackOrder(0) , m_videoLength(-1) , m_locked( false ), m_dirty( false ), m_delaySaving( false ) { } ImageInfo::ImageInfo( const DB::FileName& fileName, MediaType type, bool readExifInfo, bool storeExifInfo) : m_imageOnDisk( YesOnDisk ), m_null( false ), m_size( -1, -1 ), m_type( type ) , m_rating(-1), m_stackId(0), m_stackOrder(0) , m_videoLength(-1) , m_locked(false), m_delaySaving( true ) { QFileInfo fi( fileName.absolute() ); m_label = fi.completeBaseName(); m_angle = 0; setFileName(fileName); // Read Exif information if ( readExifInfo ) { ExifMode mode = EXIFMODE_INIT; if ( ! storeExifInfo) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName, mode); } m_dirty = false; m_delaySaving = false; } /** Change delaying of saving changes. * * Will save changes when set to false. * * Use this method to set multiple attributes with only one * database operation. * * Example: * \code * info.delaySavingChanges(true); * info.setLabel("Hello"); * info.setDescription("Hello world"); * info.delaySavingChanges(false); * \endcode * * \see saveChanges() */ void ImageInfo::delaySavingChanges(bool b) { m_delaySaving = b; if (!b) saveChanges(); } void ImageInfo::setLabel( const QString& desc ) { if (desc != m_label) m_dirty = true; m_label = desc; saveChangesIfNotDelayed(); } QString ImageInfo::label() const { return m_label; } void ImageInfo::setDescription( const QString& desc ) { if (desc != m_description) m_dirty = true; m_description = desc.trimmed(); saveChangesIfNotDelayed(); } QString ImageInfo::description() const { return m_description; } void ImageInfo::setCategoryInfo( const QString& key, const StringSet& value ) { // Don't check if really changed, because it's too slow. m_dirty = true; m_categoryInfomation[key] = value; saveChangesIfNotDelayed(); } bool ImageInfo::hasCategoryInfo( const QString& key, const QString& value ) const { return m_categoryInfomation[key].contains(value); } bool DB::ImageInfo::hasCategoryInfo( const QString& key, const StringSet& values ) const { return values.intersects( m_categoryInfomation[key] ); } StringSet ImageInfo::itemsOfCategory( const QString& key ) const { return m_categoryInfomation[key]; } void ImageInfo::renameItem( const QString& category, const QString& oldValue, const QString& newValue ) { if (m_taggedAreas.contains(category)) { if (m_taggedAreas[category].contains(oldValue)) { m_taggedAreas[category][newValue] = m_taggedAreas[category][oldValue]; m_taggedAreas[category].remove(oldValue); } } StringSet& set = m_categoryInfomation[category]; StringSet::iterator it = set.find( oldValue ); if ( it != set.end() ) { m_dirty = true; set.erase( it ); set.insert( newValue ); saveChangesIfNotDelayed(); } } DB::FileName ImageInfo::fileName() const { return m_fileName; } void ImageInfo::setFileName( const DB::FileName& fileName ) { if (fileName != m_fileName) m_dirty = true; m_fileName = fileName; m_imageOnDisk = Unchecked; DB::CategoryPtr folderCategory = DB::ImageDB::instance()->categoryCollection()-> categoryForSpecial(DB::Category::FolderCategory); if (folderCategory) { DB::MemberMap& map = DB::ImageDB::instance()->memberMap(); createFolderCategoryItem( folderCategory, map ); //ImageDB::instance()->setMemberMap( map ); } saveChangesIfNotDelayed(); } void ImageInfo::rotate( int degrees, RotationMode mode ) { // ensure positive degrees: degrees += 360; degrees = degrees % 360; if ( degrees == 0 ) return; m_dirty = true; m_angle = ( m_angle + degrees ) % 360; if (degrees == 90 || degrees == 270) { m_size.transpose(); } // the AnnotationDialog manages this by itself and sets RotateImageInfoOnly: if ( mode == RotateImageInfoAndAreas ) { for ( auto& areasOfCategory : m_taggedAreas ) { for ( auto& area : areasOfCategory ) { QRect rotatedArea; // parameter order for QRect::setCoords: // setCoords( left, top, right, bottom ) // keep in mind that _size is already transposed switch (degrees) { case 90: rotatedArea.setCoords( m_size.width() - area.bottom(), area.left(), m_size.width() - area.top(), area.right() ); break; case 180: rotatedArea.setCoords( m_size.width() - area.right(), m_size.height() - area.bottom(), m_size.width() - area.left(), m_size.height() - area.top() ); break; case 270: rotatedArea.setCoords( area.top(), m_size.height() - area.right(), area.bottom(), m_size.height() - area.left() ); break; default: // degrees==0; "odd" values won't happen. rotatedArea = area; break; } // update _taggedAreas[category][tag]: area = rotatedArea; } } } saveChangesIfNotDelayed(); } int ImageInfo::angle() const { return m_angle; } void ImageInfo::setAngle( int angle ) { if (angle != m_angle) m_dirty = true; m_angle = angle; saveChangesIfNotDelayed(); } short ImageInfo::rating() const { return m_rating; } void ImageInfo::setRating( short rating ) { Q_ASSERT( (rating >= 0 && rating <= 10) || rating == -1 ); if ( rating > 10 ) rating = 10; if ( rating < -1 ) rating = -1; if ( m_rating != rating ) m_dirty = true; m_rating = rating; saveChangesIfNotDelayed(); } DB::StackID ImageInfo::stackId() const { return m_stackId; } void ImageInfo::setStackId( const DB::StackID stackId ) { if ( stackId != m_stackId ) m_dirty = true; m_stackId = stackId; saveChangesIfNotDelayed(); } unsigned int ImageInfo::stackOrder() const { return m_stackOrder; } void ImageInfo::setStackOrder( const unsigned int stackOrder ) { if ( stackOrder != m_stackOrder ) m_dirty = true; m_stackOrder = stackOrder; saveChangesIfNotDelayed(); } void ImageInfo::setVideoLength(int length) { if ( m_videoLength != length ) m_dirty = true; m_videoLength = length; saveChangesIfNotDelayed(); } int ImageInfo::videoLength() const { return m_videoLength; } void ImageInfo::setDate( const ImageDate& date ) { if (date != m_date) m_dirty = true; m_date = date; saveChangesIfNotDelayed(); } ImageDate& ImageInfo::date() { return m_date; } ImageDate ImageInfo::date() const { return m_date; } bool ImageInfo::operator!=( const ImageInfo& other ) const { return !(*this == other); } bool ImageInfo::operator==( const ImageInfo& other ) const { bool changed = ( m_fileName != other.m_fileName || m_label != other.m_label || ( !m_description.isEmpty() && !other.m_description.isEmpty() && m_description != other.m_description ) || // one might be isNull. m_date != other.m_date || m_angle != other.m_angle || m_rating != other.m_rating || ( m_stackId != other.m_stackId || ! ( ( m_stackId == 0 ) ? true : ( m_stackOrder == other.m_stackOrder ) ) ) ); if ( !changed ) { QStringList keys = DB::ImageDB::instance()->categoryCollection()->categoryNames(); for( QStringList::ConstIterator it = keys.constBegin(); it != keys.constEnd(); ++it ) changed |= m_categoryInfomation[*it] != other.m_categoryInfomation[*it]; } return !changed; } void ImageInfo::renameCategory( const QString& oldName, const QString& newName ) { m_dirty = true; m_categoryInfomation[newName] = m_categoryInfomation[oldName]; m_categoryInfomation.remove(oldName); m_taggedAreas[newName] = m_taggedAreas[oldName]; m_taggedAreas.remove(oldName); saveChangesIfNotDelayed(); } void ImageInfo::setMD5Sum( const MD5& sum, bool storeEXIF ) { if (sum != m_md5sum) { // if we make a QObject derived class out of imageinfo, we might invalidate thumbnails from here // file changed -> reload/invalidate metadata: ExifMode mode = EXIFMODE_ORIENTATION | EXIFMODE_DATABASE_UPDATE; // fuzzy dates are usually set for a reason if (!m_date.isFuzzy()) mode |= EXIFMODE_DATE; // FIXME (ZaJ): the "right" thing to do would be to update the description // - if it is currently empty (done.) // - if it has been set from the exif info and not been changed (TODO) if (m_description.isEmpty()) mode |= EXIFMODE_DESCRIPTION; if (!storeEXIF) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif( fileName(), mode); // FIXME (ZaJ): it *should* make sense to set the ImageDB::md5Map() from here, but I want // to make sure I fully understand everything first... // this could also be done as signal md5Changed(old,new) + // Note: I've now figured out why this isn't done: + // for the imagesWithMD5SumChanged() function, setMD5Sum is called when a + // changed MD5 sum is detected, but the (global) md5 map is not updated, so + // that the difference can be calculated afterwards. + // OTOH, this leads to a difference between imageInfos and md5 map. I think that's not obvious to the user + // and can probably done differently. // image size is invalidated by the thumbnail builder, if needed m_dirty = true; } m_md5sum = sum; saveChangesIfNotDelayed(); } void ImageInfo::setLocked( bool locked ) { m_locked = locked; } bool ImageInfo::isLocked() const { return m_locked; } void ImageInfo::readExif(const DB::FileName& fullPath, DB::ExifMode mode) { DB::FileInfo exifInfo = DB::FileInfo::read( fullPath, mode ); bool oldDelaySaving = m_delaySaving; delaySavingChanges(true); // Date if ( updateDateInformation(mode) ) { const ImageDate newDate ( exifInfo.dateTime() ); setDate( newDate ); } // Orientation if ( (mode & EXIFMODE_ORIENTATION) && Settings::SettingsData::instance()->useEXIFRotate() ) { setAngle( exifInfo.angle() ); } // Description if ( (mode & EXIFMODE_DESCRIPTION) && Settings::SettingsData::instance()->useEXIFComments() ) { bool doSetDescription = true; QString desc = exifInfo.description(); if ( Settings::SettingsData::instance()->stripEXIFComments() ) { for( const auto& ignoredComment : Settings::SettingsData::instance()->EXIFCommentsToStrip() ) { if ( desc == ignoredComment ) { doSetDescription = false; break; } } } if (doSetDescription) { setDescription(desc); } } delaySavingChanges(false); m_delaySaving = oldDelaySaving; // Database update if ( mode & EXIFMODE_DATABASE_UPDATE ) { Exif::Database::instance()->add( exifInfo ); #ifdef HAVE_KGEOMAP // GPS coords might have changed... m_coordsIsSet = false; #endif } } QStringList ImageInfo::availableCategories() const { return m_categoryInfomation.keys(); } QSize ImageInfo::size() const { return m_size; } void ImageInfo::setSize( const QSize& size ) { if (size != m_size) m_dirty = true; m_size = size; saveChangesIfNotDelayed(); } bool ImageInfo::imageOnDisk( const DB::FileName& fileName ) { return fileName.exists(); } ImageInfo::ImageInfo( const DB::FileName& fileName, const QString& label, const QString& description, const ImageDate& date, int angle, const MD5& md5sum, const QSize& size, MediaType type, short rating, unsigned int stackId, unsigned int stackOrder ) { m_delaySaving = true; m_fileName = fileName; m_label =label; m_description =description; m_date = date; m_angle =angle; m_md5sum =md5sum; m_size = size; m_imageOnDisk = Unchecked; m_locked = false; m_null = false; m_type = type; m_dirty = true; delaySavingChanges(false); if ( rating > 10 ) rating = 10; if ( rating < -1 ) rating = -1; m_rating = rating; m_stackId = stackId; m_stackOrder = stackOrder; m_videoLength= -1; } // TODO: we should get rid of this operator. It seems only be necessary // because of the 'delaySavings' field that gets a special value. // ImageInfo should just be a dumb data object holder and not incorporate // storing strategies. ImageInfo& ImageInfo::operator=( const ImageInfo& other ) { m_fileName = other.m_fileName; m_label = other.m_label; m_description = other.m_description; m_date = other.m_date; m_categoryInfomation = other.m_categoryInfomation; m_taggedAreas = other.m_taggedAreas; m_angle = other.m_angle; m_imageOnDisk = other.m_imageOnDisk; m_md5sum = other.m_md5sum; m_null = other.m_null; m_size = other.m_size; m_type = other.m_type; m_dirty = other.m_dirty; m_rating = other.m_rating; m_stackId = other.m_stackId; m_stackOrder = other.m_stackOrder; m_videoLength = other.m_videoLength; delaySavingChanges(false); return *this; } MediaType DB::ImageInfo::mediaType() const { return m_type; } bool ImageInfo::isVideo() const { return m_type == Video; } void DB::ImageInfo::createFolderCategoryItem( DB::CategoryPtr folderCategory, DB::MemberMap& memberMap ) { QString folderName = Utilities::relativeFolderName( m_fileName.relative() ); if ( folderName.isEmpty() ) return; if ( ! memberMap.contains( folderCategory->name(), folderName ) ) { QStringList directories = folderName.split(QString::fromLatin1( "/" ) ); QString curPath; for( QStringList::ConstIterator directoryIt = directories.constBegin(); directoryIt != directories.constEnd(); ++directoryIt ) { if ( curPath.isEmpty() ) curPath = *directoryIt; else { QString oldPath = curPath; curPath = curPath + QString::fromLatin1( "/" ) + *directoryIt; memberMap.addMemberToGroup( folderCategory->name(), oldPath, curPath ); } } folderCategory->addItem( folderName ); } m_categoryInfomation.insert( folderCategory->name() , StringSet() << folderName ); } void DB::ImageInfo::copyExtraData( const DB::ImageInfo& from, bool copyAngle) { m_categoryInfomation = from.m_categoryInfomation; m_description = from.m_description; // Hmm... what should the date be? orig or modified? // _date = from._date; if (copyAngle) m_angle = from.m_angle; m_rating = from.m_rating; } void DB::ImageInfo::removeExtraData () { m_categoryInfomation.clear(); m_description.clear(); m_rating = -1; } void ImageInfo::merge(const ImageInfo &other) { // Merge description if ( !other.description().isEmpty() ) { if ( m_description.isEmpty() ) m_description = other.description(); else if (m_description != other.description()) m_description += QString::fromUtf8("\n-----------\n") + other.m_description; } // Clear untagged tag if one of the images was untagged const QString untaggedCategory = Settings::SettingsData::instance()->untaggedCategory(); const QString untaggedTag = Settings::SettingsData::instance()->untaggedTag(); const bool isCompleted = !m_categoryInfomation[untaggedCategory].contains(untaggedTag) || !other.m_categoryInfomation[untaggedCategory].contains(untaggedTag); // Merge tags QSet keys = QSet::fromList(m_categoryInfomation.keys()); keys.unite(QSet::fromList(other.m_categoryInfomation.keys())); for( const QString& key : keys) { m_categoryInfomation[key].unite(other.m_categoryInfomation[key]); } // Clear untagged tag if one of the images was untagged if (isCompleted) m_categoryInfomation[untaggedCategory].remove(untaggedTag); // merge stacks: if (isStacked() || other.isStacked()) { DB::FileNameList stackImages; if (!isStacked()) stackImages.append(fileName()); else stackImages.append(DB::ImageDB::instance()->getStackFor(fileName())); stackImages.append(DB::ImageDB::instance()->getStackFor(other.fileName())); DB::ImageDB::instance()->unstack(stackImages); if (!DB::ImageDB::instance()->stack(stackImages)) qCWarning(DBLog, "Could not merge stacks!"); } } void DB::ImageInfo::addCategoryInfo( const QString& category, const StringSet& values ) { for ( StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt ) { if (! m_categoryInfomation[category].contains( *valueIt ) ) { m_dirty = true; m_categoryInfomation[category].insert( *valueIt ); } } saveChangesIfNotDelayed(); } void DB::ImageInfo::clearAllCategoryInfo() { m_categoryInfomation.clear(); m_taggedAreas.clear(); } void DB::ImageInfo::removeCategoryInfo( const QString& category, const StringSet& values ) { for ( StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt ) { if ( m_categoryInfomation[category].contains( *valueIt ) ) { m_dirty = true; m_categoryInfomation[category].remove(*valueIt); m_taggedAreas[category].remove(*valueIt); } } saveChangesIfNotDelayed(); } void DB::ImageInfo::addCategoryInfo( const QString& category, const QString& value, const QRect& area ) { if (! m_categoryInfomation[category].contains( value ) ) { m_dirty = true; m_categoryInfomation[category].insert( value ); if (area.isValid()) { m_taggedAreas[category][value] = area; } } saveChangesIfNotDelayed(); } void DB::ImageInfo::removeCategoryInfo( const QString& category, const QString& value ) { if ( m_categoryInfomation[category].contains( value ) ) { m_dirty = true; m_categoryInfomation[category].remove( value ); m_taggedAreas[category].remove( value ); } saveChangesIfNotDelayed(); } void DB::ImageInfo::setPositionedTags(const QString& category, const QMap &positionedTags) { m_dirty = true; m_taggedAreas[category] = positionedTags; saveChangesIfNotDelayed(); } bool DB::ImageInfo::updateDateInformation( int mode ) const { if ((mode & EXIFMODE_DATE) == 0) return false; if ( (mode & EXIFMODE_FORCE) != 0 ) return true; return true; } QMap> DB::ImageInfo::taggedAreas() const { return m_taggedAreas; } QRect DB::ImageInfo::areaForTag(QString category, QString tag) const { // QMap::value returns a default constructed value if the key is not found: return m_taggedAreas.value(category).value(tag); } #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates DB::ImageInfo::coordinates() const { if (m_coordsIsSet) { return m_coordinates; } static const int EXIF_GPS_VERSIONID = 0; static const int EXIF_GPS_LATREF = 1; static const int EXIF_GPS_LAT = 2; static const int EXIF_GPS_LONREF = 3; static const int EXIF_GPS_LON = 4; static const int EXIF_GPS_ALTREF = 5; static const int EXIF_GPS_ALT = 6; static const QString S = QString::fromUtf8("S"); static const QString W = QString::fromUtf8("W"); static QList fields; if (fields.isEmpty()) { // the order here matters! we use the named int constants afterwards to refer to them: fields.append( new Exif::IntExifElement( "Exif.GPSInfo.GPSVersionID" ) ); // actually a byte value fields.append( new Exif::StringExifElement( "Exif.GPSInfo.GPSLatitudeRef" ) ); fields.append( new Exif::RationalExifElement( "Exif.GPSInfo.GPSLatitude" ) ); fields.append( new Exif::StringExifElement( "Exif.GPSInfo.GPSLongitudeRef" ) ); fields.append( new Exif::RationalExifElement( "Exif.GPSInfo.GPSLongitude" ) ); fields.append( new Exif::IntExifElement( "Exif.GPSInfo.GPSAltitudeRef" ) ); // actually a byte value fields.append( new Exif::RationalExifElement( "Exif.GPSInfo.GPSAltitude" ) ); } // read field values from database: bool foundIt = Exif::Database::instance()->readFields( m_fileName, fields ); // if the Database query result doesn't contain exif GPS info (-> upgraded exifdb from DBVersion < 2), it is null // if the result is int 0, then there's no exif gps information in the image // otherwise we can proceed to parse the information if ( foundIt && fields[EXIF_GPS_VERSIONID]->value().isNull() ) { // update exif DB and repeat the search: Exif::Database::instance()->remove( fileName() ); Exif::Database::instance()->add( fileName() ); Exif::Database::instance()->readFields( m_fileName, fields ); Q_ASSERT( !fields[EXIF_GPS_VERSIONID]->value().isNull() ); } KGeoMap::GeoCoordinates coords; // gps info set? // don't use the versionid field here, because some cameras use 0 as its value if ( foundIt && fields[EXIF_GPS_LAT]->value().toInt() != -1.0 && fields[EXIF_GPS_LON]->value().toInt() != -1.0 ) { // lat/lon/alt reference determines sign of float: double latr = (fields[EXIF_GPS_LATREF]->value().toString() == S ) ? -1.0 : 1.0; double lat = fields[EXIF_GPS_LAT]->value().toFloat(); double lonr = (fields[EXIF_GPS_LONREF]->value().toString() == W ) ? -1.0 : 1.0; double lon = fields[EXIF_GPS_LON]->value().toFloat(); double altr = (fields[EXIF_GPS_ALTREF]->value().toInt() == 1 ) ? -1.0 : 1.0; double alt = fields[EXIF_GPS_ALT]->value().toFloat(); if (lat != -1.0 && lon != -1.0) { coords.setLatLon(latr * lat, lonr * lon); if (alt != 0.0f) { coords.setAlt(altr * alt); } } } m_coordinates = coords; m_coordsIsSet = true; return m_coordinates; } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/NewImageFinder.cpp b/DB/NewImageFinder.cpp index 2a4289d0..e07a3a4c 100644 --- a/DB/NewImageFinder.cpp +++ b/DB/NewImageFinder.cpp @@ -1,740 +1,748 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "NewImageFinder.h" #include "FastDir.h" #include "Logging.h" #include "ImageScout.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace DB; /***************************************************************** * * NOTES ON PERFORMANCE * ===== == =========== * * - Robert Krawitz 2018-05-24 * * * GENERAL NOTES ON STORAGE I/O * ------- ----- -- ------- --- * * The two main gates to loading new images are: * * 1) I/O (how fast can we read images off mass storage) * * Different I/O devices have different characteristics in terms of * througput, media latency, and protocol latency. * * - Throughput is the raw speed at which data can be transferred, * limited by the physical and/or electronic characteristics of * the medium and the interface. Short of reducing the amount of * data that's transferred, or clever games with using the most * efficient part of the medium (the outer tracks only for HDD's, * a practice referred to as "short stroking" because it reduces * the distance the head has to seek, at the cost of wasting a * lot of capacity), there's nothing that can be done about this. * * - Media latency is the latency component due to characteristics * of the underlying storage medium. For spinning disks, this is * a function of rotational latency and sek latency. In some * cases, particularly with hard disks, it is possible to reduce * media latency by arranging to access the data in a way that * reduces seeking. See DB/FastDir.cpp for an example of this. * * While media latency can sometimes be hidden by overlapping * I/O, generally not possible to avoid it. Sometimes trying too * hard can actually increase media latency if it results in I/O * operations competing against each other requiring additional * seeks. * * Overlapping I/O with computation is another matter; that can * easily yield benefit, especially if it eliminates rotational * latency. * * - Protocol latency. This refers to things like SATA overhead, * network overhead (for images stored on a network), and so * forth. This can encompass multiple things, and often they can * be pipelined by means of multiple queued I/O operations. For * example, multiple commands can be issued to modern interfaces * (SATA, NVMe) and many network interfaces without waiting for * earlier operations to return. * * If protocol latency is high compared with media latency, * having multiple requests outstanding simultaneously can * yield significant benefits. * * iostat is a valuable tool for investigating throughput and * looking for possible optimizations. The IO/sec and data * read/written per second when compared against known media * characteristics (disk and SSD throughput, network bandwidth) * provides valuable information about whether we're getting close * to full performance from the I/O, and user and system CPU time * give us additional clues about whether we're I/O-bound or * CPU-bound. * * Historically in the computer field, operations that require * relatively simple processing on large volumes of data are I/O * bound. But with very fast I/O devices such as NVMe SSDs, some * of which reach 3 GB/sec, that's not always the case. * * 2) Image (mostly JPEG) loading. * * This is a function of image characteristics and image processing * libraries. Sometimes it's possible to apply parameters to * the underlying image loader to speed it up. This shows up as user * CPU time. Usually the only way to improve this performance * characteristic is to use more or faster CPU cores (sometimes GPUs * can assist here) or use better image loading routines (better * libraries). * * * DESCRIPTION OF KPHOTOALBUM IMAGE LOAD PROCESS * ----------- -- ----------- ----- ---- ------- * * KPhotoAlbum, when it loads an image, performs three processing steps: * * 1) Compute the MD5 checksum * * 2) Extract the Exif metadata * * 3) Generate a thumbnail * * Previous to this round of performance tuning, the first two steps * were performed in the first pass, and thumbnails were generated in * a separate pass. Assuming that the set of new images is large enough * that they cannot all fit in RAM buffers, this results in the I/O * being performed twice. The rewrite results in I/O being performed once. * * In addition, I have made many other changes: * * 1) Prior to the MD5 calculation step, a new thread, called a "scout * thread", reads the files into memory. While this memory is not * directly used in the later computations, it results in the images * being in RAM when they are later needed, making the I/O very fast * (copying data in memory rather than reading it from storage). * * This is a way to overlap I/O with computation. * * 2) The MD5 checksum uses its own I/O to read the data in in larger * chunks than the Qt MD5 routine does. The Qt routine reads it in * in 4KiB chunks; my experimentation has found that 256KiB chunks * are more efficient, even with a scout thread (it reduces the * number of system calls). * * 3) When searching for other images to stack with the image being * loaded, the new image loader no longer attempts to determine * whether other candidate filenames are present, nor does it * compute the MD5 checksum of any such files it does find. Rather, * it only checks for files that are already in KPhotoAlbum, either * previously or as a result of the current load. Merely checking * for the presence of another file is not cheap, and it's not * necessary; if an image will belong to a stack, we'll either know * it now or when other images that can be stacked are loaded. * * 4) The Exif metadata extraction is now done only once; previously * it was performed several times at different stages of the loading * process. * * 5) The thumbnail index is now written out incrementally rather than * the entire index (which can be many megabytes in a large image * database) being rewritten frequently. The index is fully rewritten * prior to exit. * * * BASELINE PERFORMANCE * -------- ----------- * * These measurements were all taken on a Lenovo ThinkPad P70 with 32 * GB of dual-channel DDR4-2400 DRAM, a Xeon E3-1505M CPU (4 cores/8 * total hyperthreads, 2.8-3.7 GHz Skylake; usually runs around * 3.1-3.2 GHz in practice), a Seagate ST2000LM015-2E8174 2TB HDD, and * a Crucial MX300 1TB SATA SSD. Published numbers and measurements I * took otherwise indicate that the HDD can handle about 105-110 * MB/sec with a maximum of 180 IO/sec (in a favorable case). The SSD * is rated to handle 530 MB/sec read, 510 MB/sec write, 92K random * reads/sec, and 83K random writes/sec. * * The image set I used for all measurements, except as noted, * consists of 10839 total files of which about 85% are 20 MP JPEG and * the remainder (with a few exceptions are 20 MP RAW files from a * Canon EOS 7D mkII camera. The total dataset is about 92 GB in * size. * * I baselined both drives by reading the same dataset by means of * * % ls | xargs cat | dd bs=1048576 of=/dev/null * * The HDD required between 850 and 870 seconds (14'10" to 14'30") to * perform this operation, yielding about 105-108 MB/sec. The SSD * achieved about 271 MB/sec, which is well under its rated throughput * (hdparm -Tt yields 355 MB/sec, which is likewise nowhere close to * its rated throughput). hdparm -Tt on the HDD yields about 120 * MB/sec, but throughput to an HDD depends upon which part of the * disk is being read. The outer tracks have a greater angular * density to achieve the same linear density (in other words, the * circumference of an outer track is longer than that of an inner * track, and the data is stored at a constant linear density). So * hdparm isn't very useful on an HDD except as a best case. * * Note also that hdparm does a single stream read from the device. * It does not take advantage of the ability to queue multiple * requests. * * * ANALYSIS OF KPHOTOALBUM LOAD PERFORMANCE * -------- -- ----------- ---- ----------- * * I analyzed the following cases, with images stored both on the * HDD and the SSD: * * 1) Images loaded (All, JPEG only, RAW only) * * B) Thumbnail creation (Including, Excluding) * * C) Scout threads (0, 1, 2, 3) * * The JPG image set constitutes 9293 images totaling about 55 GB. The * JPEG files are mostly 20 MP high quality files, in the range of * 6-10 MB. * The RAW image set constitutes 1544 images totaling about 37 GB. The * RAW files are 20 MP files, in the range of 25 MB. * The ALL set consists of 10839 or 10840 images totaling about 92 GB * (the above set plus 2 .MOV files and in some cases one additional * JPEG file). * * Times are elapsed times; CPU consumption is approximate user+system * CPU consumption. Numbers in parentheses are with thumbnail * building disabled. Note that in the cases with no scout threads on * the SSD the times were reproducibly shorter with thumbnail building * enabled (reasons are not determined at this time). * * Cases building RAW thumbnails generally consumed somewhat more * system CPU (in the range of 10-15%) than JPEG-only cases. This may * be due to custom I/O routines used for generating thumbnails with * JPEG files; RAW files used the I/O provided by libkdcraw, which * uses smaller I/O operations. * * Estimating CPU time for mixed workloads proved very problematic, * as there were significant changes over time. * * Elapsed Time * ------- ---- * * SSD HDD * * JPG - 0 scouts 4:03 (3:59) * JPG - 1 scout 2:46 (2:44) * JPG - 2 scouts 2:20 (2:07) * JPG - 3 scouts 2:21 (1:58) * * ALL - 0 scouts 6:32 (7:03) 16:01 * ALL - 1 scout 4:33 (4:33) 15:01 * ALL - 2 scouts 3:37 (3:28) 16:59 * ALL - 3 scouts 3:36 (3:15) * * RAW - 0 scouts 2:18 (2:46) * RAW - 1 scout 1:46 (1:46) * RAW - 2 scouts 1:17 (1:17) * RAW - 3 scouts 1:13 (1:13) * * User+System CPU * ----------- --- * * SSD HDD * * JPG - 0 scouts 40% (12%) * JPG - 1 scout 70% (20%) * JPG - 2 scouts 85% (15%) * JPG - 3 scouts 85% (15%) * * RAW - 0 scouts 15% (10%) * RAW - 1 scout 18% (12%) * RAW - 2 scouts 25% (15%) * RAW - 3 scouts 25% (15%) * * I also used kcachegrind to measure CPU consumption on smaller * subsets of images (with and without thumbnail creation). In terms * of user CPU consumption, thumbnail creation constitutes the large * majority of CPU cycles for processing JPEG files, followed by MD5 * computation, with Exif parsing lagging far behind. For RAW files, * MD5 computation consumes more cycles, likely in part due to the * larger size of RAW files but possibly also related to the smaller * filesize of embedded thumbnails (on the Canon 7D mkII, the embedded * thumbnail is full size but low quality). * * With thumbnail generation: * ---- --------- ----------- * * RAW JPEG * * Thumbnail generation 44% 82% * libjpeg processing 43% 82% * MD5 computation 51% 13% * Read Exif 1% 1.0% * * Without thumbnail generation: * ------- --------- ----------- * * RAW JPEG * * MD5 computation 92% 80% * Read Exif 4% 10% * * * CONCLUSIONS * ----------- * * For loading files from hard disk (likely the most common case), * there's no reason to consider any loading method other than using a * single scout thread and computing thumbnails concurrently. Even * with thumbnail computation, there is very little CPU utilization. * * Loading from SATA SSD benefits from two scout threads, and possibly * more. For minimal time to regain control, there is some benefit * seen from separating thumbnail generation from the rest of the * processing stages at the cost of more total elapsed time. This is * more evident with JPEG files than with RAW files in this test case. * RAW files typically have smaller thumbnail images which can be * extracted and processed more quickly than full-size JPEG files. On * a slower CPU, it may be desirable to return control to the user * even if the thumbnails are not built yet. * * Two other cases would be NVMe (or other very fast) SSDs and network * storage. Since we're seeing evidence of CPU saturation on SATA * SSDs, we would likely see this even more strongly with NVMe; with * large numbers of images it may be desirable to separate the * thumbnail building from the rest of the processing. It may also be * beneficial to use more scout threads. * * Network storage presents a different problem. It is likely to have * lower throughput -- and certainly much higher latency -- than even * HDD, unless the underlying storage medium is SSD and the data is * located on a very fast, low latency network. So there would be no * benefit to separating thumbnail processing. However, due to * protocol vs. media latency discussed above, it may well work to use * more scout threads. However, this may saturate the network and the * storage, to the detriment of other users, and there's probably no * general (or easily discoverable) optimum for this. * * It's my judgment that most images will be stored on HDDs for at * least the next few years, so tuning for that use case is probably * the best single choice to be made. * *****************************************************************/ namespace { // Number of scout threads for preloading images. More than one scout thread // yields about 10% less performance with higher IO/sec but lower I/O throughput, // most probably due to thrashing. constexpr int IMAGE_SCOUT_THREAD_COUNT = 1; } bool NewImageFinder::findImages() { // Load the information from the XML file. DB::FileNameSet loadedFiles; QElapsedTimer timer; timer.start(); // TODO: maybe the databas interface should allow to query if it // knows about an image ? Here we've to iterate through all of them and it // might be more efficient do do this in the database without fetching the // whole info. for ( const DB::FileName& fileName : DB::ImageDB::instance()->images()) { loadedFiles.insert(fileName); } m_pendingLoad.clear(); searchForNewFiles( loadedFiles, Settings::SettingsData::instance()->imageDirectory() ); int filesToLoad = m_pendingLoad.count(); loadExtraFiles(); qCDebug(TimingLog) << "Loaded " << filesToLoad << " images in " << timer.elapsed() / 1000.0 << " seconds"; // Man this is not super optimal, but will be changed onces the image finder moves to become a background task. if ( MainWindow::FeatureDialog::hasVideoThumbnailer() ) { BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::SearchForVideosWithoutVideoThumbnailsJob ); } // To avoid deciding if the new images are shown in a given thumbnail view or in a given search // we rather just go to home. return (!m_pendingLoad.isEmpty()); // returns if new images was found. } void NewImageFinder::searchForNewFiles( const DB::FileNameSet& loadedFiles, QString directory ) { qApp->processEvents( QEventLoop::AllEvents ); directory = Utilities::stripEndingForwardSlash(directory); const QString imageDir = Utilities::stripEndingForwardSlash(Settings::SettingsData::instance()->imageDirectory()); FastDir dir( directory ); const QStringList dirList = dir.entryList( ); ImageManager::RAWImageDecoder dec; QStringList excluded; excluded << Settings::SettingsData::instance()->excludeDirectories(); excluded = excluded.at(0).split(QString::fromLatin1(",")); bool skipSymlinks = Settings::SettingsData::instance()->skipSymlinks(); // Keep files within a directory more local by processing all files within the // directory, and then all subdirectories. QStringList subdirList; for( QStringList::const_iterator it = dirList.constBegin(); it != dirList.constEnd(); ++it ) { const DB::FileName file = DB::FileName::fromAbsolutePath(directory + QString::fromLatin1("/") + *it); if ( (*it) == QString::fromLatin1(".") || (*it) == QString::fromLatin1("..") || excluded.contains( (*it) ) || loadedFiles.contains( file ) || dec._skipThisFile(loadedFiles, file) || (*it) == QString::fromLatin1("CategoryImages") ) continue; QFileInfo fi( file.absolute() ); if ( !fi.isReadable() ) continue; if ( skipSymlinks && fi.isSymLink() ) continue; if ( fi.isFile() ) { if ( ! DB::ImageDB::instance()->isBlocking( file ) ) { if ( Utilities::canReadImage(file) ) m_pendingLoad.append( qMakePair( file, DB::Image ) ); else if ( Utilities::isVideo( file ) ) m_pendingLoad.append( qMakePair( file, DB::Video ) ); } } else if ( fi.isDir() ) { subdirList.append( file.absolute() ); } } for( QStringList::const_iterator it = subdirList.constBegin(); it != subdirList.constEnd(); ++it ) searchForNewFiles( loadedFiles, *it ); } void NewImageFinder::loadExtraFiles() { // FIXME: should be converted to a threadpool for SMP stuff and whatnot :] QProgressDialog dialog; QElapsedTimer timeSinceProgressUpdate; dialog.setLabelText( i18n("

Loading information from new files

" "

Depending on the number of images, this may take some time.
" "However, there is only a delay when new images are found.

") ); QProgressBar *progressBar = new QProgressBar; progressBar->setFormat( QLatin1String("%v/%m") ); dialog.setBar(progressBar); dialog.setMaximum( m_pendingLoad.count() ); dialog.setMinimumDuration( 1000 ); QAtomicInt loadedCount = 0; setupFileVersionDetection(); int count = 0; ImageScoutQueue asyncPreloadQueue; for( LoadList::Iterator it = m_pendingLoad.begin(); it != m_pendingLoad.end(); ++it ) { asyncPreloadQueue.enqueue((*it).first); } ImageScout scout(asyncPreloadQueue, loadedCount, IMAGE_SCOUT_THREAD_COUNT); scout.start(); Exif::Database::instance()->startInsertTransaction(); dialog.setValue( count ); // ensure to call setProgress(0) timeSinceProgressUpdate.start(); for( LoadList::Iterator it = m_pendingLoad.begin(); it != m_pendingLoad.end(); ++it, ++count ) { qApp->processEvents( QEventLoop::AllEvents ); if ( dialog.wasCanceled() ) { m_pendingLoad.clear(); Exif::Database::instance()->abortInsertTransaction(); return; } // (*it).first: DB::FileName // (*it).second: DB::MediaType loadExtraFile( (*it).first, (*it).second ); loadedCount++; // Atomic if ( timeSinceProgressUpdate.elapsed() >= 1000 ) { dialog.setValue( count ); timeSinceProgressUpdate.restart(); } } dialog.setValue( count ); // loadExtraFile() has already inserted all images into the // database, but without committing the changes DB::ImageDB::instance()->commitDelayedImages(); Exif::Database::instance()->commitInsertTransaction(); ImageManager::ThumbnailBuilder::instance()->save(); } void NewImageFinder::setupFileVersionDetection() { // should be cached because loading once per image is expensive m_modifiedFileCompString = Settings::SettingsData::instance()->modifiedFileComponent(); m_modifiedFileComponent = QRegExp(m_modifiedFileCompString); m_originalFileComponents << Settings::SettingsData::instance()->originalFileComponent(); m_originalFileComponents = m_originalFileComponents.at(0).split(QString::fromLatin1(";")); } void NewImageFinder::loadExtraFile( const DB::FileName& newFileName, DB::MediaType type ) { MD5 sum = MD5Sum( newFileName ); if ( handleIfImageHasBeenMoved(newFileName, sum) ) return; // check to see if this is a new version of a previous image // We'll get the Exif data later, when we get the MD5 checksum. ImageInfoPtr info = ImageInfoPtr(new ImageInfo( newFileName, type, false, false )); ImageInfoPtr originalInfo; DB::FileName originalFileName; if (Settings::SettingsData::instance()->detectModifiedFiles()) { // requires at least *something* in the modifiedFileComponent if (m_modifiedFileCompString.length() >= 0 && newFileName.relative().contains(m_modifiedFileComponent)) { for( QStringList::const_iterator it = m_originalFileComponents.constBegin(); it != m_originalFileComponents.constEnd(); ++it ) { QString tmp = newFileName.relative(); tmp.replace(m_modifiedFileComponent, (*it)); originalFileName = DB::FileName::fromRelativePath(tmp); MD5 originalSum; if (newFileName == originalFileName) originalSum = sum; else if (DB::ImageDB::instance()->md5Map()->containsFile( originalFileName ) ) originalSum = DB::ImageDB::instance()->md5Map()->lookupFile( originalFileName ); else // Do *not* attempt to compute the checksum here. It forces a filesystem // lookup on a file that may not exist and substantially degrades // performance by about 25% on an SSD and about 30% on a spinning disk. // If one of these other files exist, it will be found later in // the image search at which point we'll detect the modified file. continue; if ( DB::ImageDB::instance()->md5Map()->contains( originalSum ) ) { // we have a previous copy of this file; copy it's data // from the original. originalInfo = DB::ImageDB::instance()->info( originalFileName ); if ( !originalInfo ) { qCDebug(DBLog) << "Original info not found by name for " << originalFileName.absolute() << ", trying by MD5 sum."; originalFileName = DB::ImageDB::instance()->md5Map()->lookup( originalSum ); if (!originalFileName.isNull()) { qCDebug(DBLog) << "Substitute image " << originalFileName.absolute() << " found."; originalInfo = DB::ImageDB::instance()->info( originalFileName ); } if ( !originalInfo ) { qCWarning(DBLog,"How did that happen? We couldn't find info for the original image %s; can't copy the original data to %s", qPrintable(originalFileName.absolute()), qPrintable(newFileName.absolute())); continue; } } info->copyExtraData(*originalInfo); /* if requested to move, then delete old data from original */ if (Settings::SettingsData::instance()->moveOriginalContents() ) { originalInfo->removeExtraData(); } break; } } } } ImageInfoList newImages; newImages.append( info ); DB::ImageDB::instance()->addImages( newImages, false ); // also inserts image into exif db if present: info->setMD5Sum( sum ); DB::ImageDB::instance()->md5Map()->insert( sum, info->fileName()); if (originalInfo && Settings::SettingsData::instance()->autoStackNewFiles() ) { // stack the files together DB::FileName olderfile = originalFileName; DB::FileName newerfile = info->fileName(); DB::FileNameList tostack; // the newest file should go to the top of the stack tostack.append(newerfile); DB::FileNameList oldStack; if ( ( oldStack = DB::ImageDB::instance()->getStackFor( olderfile)).isEmpty() ) { tostack.append(olderfile); } else { for ( const DB::FileName& tmp : oldStack ) { tostack.append( tmp ); } } DB::ImageDB::instance()->stack(tostack); MainWindow::Window::theMainWindow()->setStackHead(newerfile); // ordering: XXX we ideally want to place the new image right // after the older one in the list. } markUnTagged(info); ImageManager::ThumbnailBuilder::instance()->buildOneThumbnail( info ); if ( info->isVideo() && MainWindow::FeatureDialog::hasVideoThumbnailer() ) { // needs to be done *after* insertion into database BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::ReadVideoLengthJob(info->fileName(), BackgroundTaskManager::BackgroundVideoPreviewRequest)); } } bool NewImageFinder::handleIfImageHasBeenMoved(const FileName &newFileName, const MD5& sum) { if ( DB::ImageDB::instance()->md5Map()->contains( sum ) ) { const DB::FileName matchedFileName = DB::ImageDB::instance()->md5Map()->lookup(sum); QFileInfo fi( matchedFileName.absolute() ); if ( !fi.exists() ) { // The file we had a collapse with didn't exists anymore so it is likely moved to this new name ImageInfoPtr info = DB::ImageDB::instance()->info( matchedFileName); if ( !info ) qCWarning(DBLog, "How did that happen? We couldn't find info for the images %s", qPrintable(matchedFileName.relative())); else { info->delaySavingChanges(true); fi = QFileInfo ( matchedFileName.relative() ); if ( info->label() == fi.completeBaseName() ) { fi = QFileInfo( newFileName.absolute() ); info->setLabel( fi.completeBaseName() ); } DB::ImageDB::instance()->renameImage( info, newFileName ); // We need to insert the new name into the MD5 map, // as it is a map, the value for the moved file will automatically be deleted. DB::ImageDB::instance()->md5Map()->insert( sum, info->fileName()); Exif::Database::instance()->remove( matchedFileName ); Exif::Database::instance()->add( newFileName); ImageManager::ThumbnailBuilder::instance()->buildOneThumbnail( info ); return true; } } } return false; // The image wasn't just moved } bool NewImageFinder::calculateMD5sums( const DB::FileNameList& list, DB::MD5Map* md5Map, bool* wasCanceled) { // FIXME: should be converted to a threadpool for SMP stuff and whatnot :] QProgressDialog dialog; dialog.setLabelText( i18np("

Calculating checksum for %1 file

","

Calculating checksums for %1 files

", list.size()) + i18n("

By storing a checksum for each image " "KPhotoAlbum is capable of finding images " "even when you have moved them on the disk.

")); dialog.setMaximum(list.size()); dialog.setMinimumDuration( 1000 ); int count = 0; DB::FileNameList cantRead; bool dirty = false; + QElapsedTimer timer; + timer.start(); for (const FileName& fileName : list) { if ( count % 10 == 0 ) { dialog.setValue( count ); // ensure to call setProgress(0) qApp->processEvents( QEventLoop::AllEvents ); if ( dialog.wasCanceled() ) { if ( wasCanceled ) *wasCanceled = true; + qCDebug(TimingLog) << "Cancelled MD5 sum calculation after" << count << "images in " << timer.elapsed() / 1000.0 << " seconds"; return dirty; } } MD5 md5 = MD5Sum( fileName ); if (md5.isNull()) { cantRead << fileName; + qCInfo(DBLog) << "File" << fileName.relative() << "not found for MD5 sum calculation."; continue; } ImageInfoPtr info = ImageDB::instance()->info(fileName); if ( info->MD5Sum() != md5 ) { + qCWarning(DBLog) << "MD5 sum of file" << fileName.relative() + << "changed from" << info->MD5Sum().toHexString() + << "to" << md5.toHexString(); info->setMD5Sum( md5 ); dirty = true; ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } md5Map->insert( md5, fileName ); ++count; } + qCDebug(TimingLog) << "Calculated MD5 sum of" << count << "images in " << timer.elapsed() / 1000.0 << " seconds"; if ( wasCanceled ) *wasCanceled = false; if ( !cantRead.empty() ) - KMessageBox::informationList( nullptr, i18n("Following files could not be read:"), cantRead.toStringList(DB::RelativeToImageRoot) ); + KMessageBox::informationList( nullptr, i18n("The following files could not be read:"), cantRead.toStringList(DB::RelativeToImageRoot) ); return dirty; } void DB::NewImageFinder::markUnTagged( ImageInfoPtr info ) { if ( Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() ) { info->addCategoryInfo( Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag() ); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Window.cpp b/MainWindow/Window.cpp index 4fd8672c..c3a5e5f0 100644 --- a/MainWindow/Window.cpp +++ b/MainWindow/Window.cpp @@ -1,1992 +1,1986 @@ /* Copyright (C) 2003-2018 Jesper K. Pedersen 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "Window.h" #include #ifdef HAVE_STDLIB_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for #if KIO_VERSION... #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HASKIPI # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HASKIPI # include #endif #include #include #include #include #include #include #include #include #include #include #include "AutoStackImages.h" #include "BreadcrumbViewer.h" #include "CopyPopup.h" #include "DeleteDialog.h" #include "DirtyIndicator.h" #include "DuplicateMerger/DuplicateMerger.h" #include "ExternalPopup.h" #include "FeatureDialog.h" #include "ImageCounter.h" #include "InvalidDateFinder.h" #include "Logging.h" #include "Options.h" #include "SearchBar.h" #include "SplashScreen.h" #include "StatisticsDialog.h" #include "StatusBar.h" #include "TokenEditor.h" #include "UpdateVideoThumbnail.h" #include "WelcomeDialog.h" using namespace DB; MainWindow::Window* MainWindow::Window::s_instance = nullptr; MainWindow::Window::Window( QWidget* parent ) :KXmlGuiWindow( parent ), m_annotationDialog(nullptr), m_deleteDialog( nullptr ), m_htmlDialog(nullptr), m_tokenEditor( nullptr ) { #ifdef HAVE_KGEOMAP m_positionBrowser = 0; #endif qCDebug(MainWindowLog) << "Using icon theme: " << QIcon::themeName(); qCDebug(MainWindowLog) << "Icon search paths: " << QIcon::themeSearchPaths(); QElapsedTimer timer; timer.start(); SplashScreen::instance()->message( i18n("Loading Database") ); s_instance = this; bool gotConfigFile = load(); if ( !gotConfigFile ) throw 0; qCInfo(TimingLog) << "MainWindow: Loading Database: " << timer.restart() << "ms."; SplashScreen::instance()->message( i18n("Loading Main Window") ); QWidget* top = new QWidget( this ); QVBoxLayout* lay = new QVBoxLayout( top ); lay->setSpacing(2); lay->setContentsMargins(2,2,2,2); setCentralWidget( top ); m_stack = new QStackedWidget( top ); lay->addWidget( m_stack, 1 ); m_dateBar = new DateBar::DateBarWidget( top ); lay->addWidget( m_dateBar ); m_dateBarLine = new QFrame( top ); m_dateBarLine->setFrameStyle( QFrame::HLine | QFrame::Plain ); m_dateBarLine->setLineWidth(0); m_dateBarLine->setMidLineWidth(0); QPalette pal = m_dateBarLine->palette(); pal.setColor( QPalette::WindowText, QColor("#c4c1bd") ); m_dateBarLine->setPalette( pal ); lay->addWidget( m_dateBarLine ); setHistogramVisibilty(Settings::SettingsData::instance()->showHistogram()); m_browser = new Browser::BrowserWidget( m_stack ); m_thumbnailView = new ThumbnailView::ThumbnailFacade(); m_stack->addWidget( m_browser ); m_stack->addWidget( m_thumbnailView->gui() ); m_stack->setCurrentWidget( m_browser ); m_settingsDialog = nullptr; qCInfo(TimingLog) << "MainWindow: Loading MainWindow: " << timer.restart() << "ms."; setupMenuBar(); qCInfo(TimingLog) << "MainWindow: setupMenuBar: " << timer.restart() << "ms."; createSearchBar(); qCInfo(TimingLog) << "MainWindow: createSearchBar: " << timer.restart() << "ms."; setupStatusBar(); qCInfo(TimingLog) << "MainWindow: setupStatusBar: " << timer.restart() << "ms."; // Misc m_autoSaveTimer = new QTimer( this ); connect(m_autoSaveTimer, &QTimer::timeout, this, &Window::slotAutoSave); startAutoSaveTimer(); connect(m_browser, &Browser::BrowserWidget::showingOverview, this, &Window::showBrowser); connect( m_browser, SIGNAL(pathChanged(Browser::BreadcrumbList)), m_statusBar->mp_pathIndicator, SLOT(setBreadcrumbs(Browser::BreadcrumbList)) ); connect( m_statusBar->mp_pathIndicator, SIGNAL(widenToBreadcrumb(Browser::Breadcrumb)), m_browser, SLOT(widenToBreadcrumb(Browser::Breadcrumb)) ); connect( m_browser, SIGNAL(pathChanged(Browser::BreadcrumbList)), this, SLOT(updateDateBar(Browser::BreadcrumbList)) ); connect(m_dateBar, &DateBar::DateBarWidget::dateSelected, m_thumbnailView, &ThumbnailView::ThumbnailFacade::gotoDate); connect(m_dateBar, &DateBar::DateBarWidget::toolTipInfo, this, &Window::showDateBarTip); connect( Settings::SettingsData::instance(), SIGNAL(histogramSizeChanged(QSize)), m_dateBar, SLOT(setHistogramBarSize(QSize)) ); connect( Settings::SettingsData::instance(), SIGNAL(actualThumbnailSizeChanged(int)), this, SLOT(slotThumbnailSizeChanged()) ); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeChange, this, &Window::setDateRange); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeCleared, this, &Window::clearDateRange); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::currentDateChanged, m_dateBar, &DateBar::DateBarWidget::setDate); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::showImage, this, &Window::showImage); connect( m_thumbnailView, SIGNAL(showSelection()), this, SLOT(slotView()) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::fileIdUnderCursorChanged, this, &Window::slotSetFileName); connect( DB::ImageDB::instance(), SIGNAL(totalChanged(uint)), this, SLOT(updateDateBar()) ); connect( DB::ImageDB::instance()->categoryCollection(), SIGNAL(categoryCollectionChanged()), this, SLOT(slotOptionGroupChanged()) ); connect( m_browser, SIGNAL(imageCount(uint)), m_statusBar->mp_partial, SLOT(showBrowserMatches(uint)) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, this, &Window::updateContextMenuFromSelectionSize); checkIfMplayerIsInstalled(); executeStartupActions(); qCInfo(TimingLog) << "MainWindow: executeStartupActions " << timer.restart() << "ms."; QTimer::singleShot( 0, this, SLOT(delayedInit()) ); updateContextMenuFromSelectionSize(0); // Automatically save toolbar settings setAutoSaveSettings(); qCInfo(TimingLog) << "MainWindow: misc setup time: " << timer.restart() << "ms."; } MainWindow::Window::~Window() { DB::ImageDB::deleteInstance(); ImageManager::ThumbnailCache::deleteInstance(); Exif::Database::deleteInstance(); } void MainWindow::Window::delayedInit() { QElapsedTimer timer; timer.start(); SplashScreen* splash = SplashScreen::instance(); setupPluginMenu(); qCInfo(TimingLog) << "MainWindow: setupPluginMenu: " << timer.restart() << "ms."; if ( Settings::SettingsData::instance()->searchForImagesOnStart() || Options::the()->searchForImagesOnStart() ) { splash->message( i18n("Searching for New Files") ); qApp->processEvents(); DB::ImageDB::instance()->slotRescan(); qCInfo(TimingLog) << "MainWindow: Search for New Files: " << timer.restart() << "ms."; } if ( !Settings::SettingsData::instance()->delayLoadingPlugins() ) { splash->message( i18n( "Loading Plug-ins" ) ); loadPlugins(); qCInfo(TimingLog) << "MainWindow: Loading Plug-ins: " << timer.restart() << "ms."; } splash->done(); show(); updateDateBar(); qCInfo(TimingLog) << "MainWindow: MainWindow.show():" << timer.restart() << "ms."; QUrl importUrl = Options::the()->importFile(); if ( importUrl.isValid() ) { // I need to do this in delayed init to get the import window on top of the normal window ImportExport::Import::imageImport( importUrl ); qCInfo(TimingLog) << "MainWindow: imageImport:" << timer.restart() << "ms."; } else { // I need to postpone this otherwise the tip dialog will not get focus on start up KTipDialog::showTip( this ); } Exif::Database::instance(); // Load the database qCInfo(TimingLog) << "MainWindow: Loading Exif DB:" << timer.restart() << "ms."; if (!Options::the()->listen().isNull()) RemoteControl::RemoteInterface::instance().listen(Options::the()->listen()); else if ( Settings::SettingsData::instance()->listenForAndroidDevicesOnStartup()) RemoteControl::RemoteInterface::instance().listen(); announceAndroidVersion(); } bool MainWindow::Window::slotExit() { if ( Options::the()->demoMode() ) { QString txt = i18n("

Delete Your Temporary Demo Database

" "

I hope you enjoyed the KPhotoAlbum demo. The demo database was copied to " "/tmp, should it be deleted now? If you do not delete it, it will waste disk space; " "on the other hand, if you want to come back and try the demo again, you " "might want to keep it around with the changes you made through this session.

" ); int ret = KMessageBox::questionYesNoCancel( this, txt, i18n("Delete Demo Database"), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), QString::fromLatin1("deleteDemoDatabase") ); if ( ret == KMessageBox::Cancel ) return false; else if ( ret == KMessageBox::Yes ) { Utilities::deleteDemo(); goto doQuit; } else { // pass through to the check for dirtyness. } } if ( m_statusBar->mp_dirtyIndicator->isSaveDirty() ) { int ret = KMessageBox::warningYesNoCancel( this, i18n("Do you want to save the changes?"), i18n("Save Changes?") ); if (ret == KMessageBox::Cancel) { return false; } if ( ret == KMessageBox::Yes ) { slotSave(); } if ( ret == KMessageBox::No ) { QDir().remove( Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1(".#index.xml") ); } } // Flush any remaining thumbnails ImageManager::ThumbnailCache::instance()->save(); doQuit: ImageManager::AsyncLoader::instance()->requestExit(); qApp->quit(); return true; } void MainWindow::Window::slotOptions() { if ( ! m_settingsDialog ) { m_settingsDialog = new Settings::SettingsDialog( this ); connect( m_settingsDialog, SIGNAL(changed()), this, SLOT(reloadThumbnails()) ); connect(m_settingsDialog, &Settings::SettingsDialog::changed, this, &Window::startAutoSaveTimer); connect(m_settingsDialog, &Settings::SettingsDialog::changed, m_browser, &Browser::BrowserWidget::reload); } m_settingsDialog->show(); } void MainWindow::Window::slotCreateImageStack() { const DB::FileNameList list = selected(); if (list.size() < 2) { // it doesn't make sense to make a stack from one image, does it? return; } bool ok = DB::ImageDB::instance()->stack( list ); if ( !ok ) { if ( KMessageBox::questionYesNo( this, i18n("Some of the selected images already belong to a stack. " "Do you want to remove them from their stacks and create a " "completely new one?"), i18n("Stacking Error")) == KMessageBox::Yes ) { DB::ImageDB::instance()->unstack(list); if ( ! DB::ImageDB::instance()->stack(list)) { KMessageBox::sorry( this, i18n("Unknown error, stack creation failed."), i18n("Stacking Error")); return; } } else { return; } } DirtyIndicator::markDirty(); // The current item might have just became invisible m_thumbnailView->setCurrentItem(list.at(0)); m_thumbnailView->updateDisplayModel(); } /** @short Make the selected image the head of a stack * * The whole point of image stacking is to group images together and then select * one of them as the "most important". This function is (maybe just a * temporary) way of promoting a selected image to the "head" of a stack it * belongs to. In future, it might get replaced by a Ligtroom-like interface. * */ void MainWindow::Window::slotSetStackHead() { const DB::FileNameList list = selected(); if ( list.size() != 1 ) { // this should be checked by enabling/disabling of QActions return; } setStackHead( *list.begin() ); } void MainWindow::Window::setStackHead( const DB::FileName& image ) { if ( ! image.info()->isStacked() ) return; unsigned int oldOrder = image.info()->stackOrder(); DB::FileNameList others = DB::ImageDB::instance()->getStackFor(image); Q_FOREACH( const DB::FileName& current, others ) { if (current == image) { current.info()->setStackOrder( 1 ); } else if ( current.info()->stackOrder() < oldOrder ) { current.info()->setStackOrder( current.info()->stackOrder() + 1 ); } } DirtyIndicator::markDirty(); m_thumbnailView->updateDisplayModel(); } void MainWindow::Window::slotUnStackImages() { const DB::FileNameList& list = selected(); if (list.isEmpty()) return; DB::ImageDB::instance()->unstack(list); DirtyIndicator::markDirty(); m_thumbnailView->updateDisplayModel(); } void MainWindow::Window::slotConfigureAllImages() { configureImages( false ); } void MainWindow::Window::slotConfigureImagesOneAtATime() { configureImages( true ); } void MainWindow::Window::configureImages( bool oneAtATime ) { const DB::FileNameList& list = selected(); if (list.isEmpty()) { KMessageBox::sorry( this, i18n("No item is selected."), i18n("No Selection") ); } else { DB::ImageInfoList images; Q_FOREACH( const DB::FileName& fileName, list) { images.append(fileName.info()); } configureImages( images, oneAtATime ); } } void MainWindow::Window::configureImages( const DB::ImageInfoList& list, bool oneAtATime ) { s_instance->configImages( list, oneAtATime ); } void MainWindow::Window::configImages( const DB::ImageInfoList& list, bool oneAtATime ) { createAnnotationDialog(); if ( m_annotationDialog->configure( list, oneAtATime ) == QDialog::Rejected ) return; reloadThumbnails( ThumbnailView::MaintainSelection ); } void MainWindow::Window::slotSearch() { createAnnotationDialog(); DB::ImageSearchInfo searchInfo = m_annotationDialog->search(); if ( !searchInfo.isNull() ) m_browser->addSearch( searchInfo ); } void MainWindow::Window::createAnnotationDialog() { Utilities::ShowBusyCursor dummy; if ( !m_annotationDialog.isNull() ) return; m_annotationDialog = new AnnotationDialog::Dialog( nullptr ); connect(m_annotationDialog.data(), &AnnotationDialog::Dialog::imageRotated, this, &Window::slotImageRotated); } void MainWindow::Window::slotSave() { Utilities::ShowBusyCursor dummy; m_statusBar->showMessage(i18n("Saving..."), 5000 ); DB::ImageDB::instance()->save( Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("index.xml"), false ); ImageManager::ThumbnailCache::instance()->save(); m_statusBar->mp_dirtyIndicator->saved(); QDir().remove( Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1(".#index.xml") ); m_statusBar->showMessage(i18n("Saving... Done"), 5000 ); } void MainWindow::Window::slotDeleteSelected() { if ( ! m_deleteDialog ) m_deleteDialog = new DeleteDialog( this ); if ( m_deleteDialog->exec( selected() ) != QDialog::Accepted ) return; DirtyIndicator::markDirty(); } void MainWindow::Window::slotCopySelectedURLs() { QList urls; int urlcount = 0; Q_FOREACH(const DB::FileName &fileName, selected()) { urls.append( QUrl::fromLocalFile(fileName.absolute()) ); urlcount++; } if (urlcount == 1) m_paste->setEnabled (true); else m_paste->setEnabled(false); QMimeData* mimeData = new QMimeData; mimeData->setUrls(urls); QApplication::clipboard()->setMimeData( mimeData ); } void MainWindow::Window::slotPasteInformation() { const QMimeData* mimeData = QApplication::clipboard()->mimeData(); // Idealy this would look like // QList urls; // urls.fromMimeData(mimeData); // if ( urls.count() != 1 ) return; // const QString string = urls.first().path(); QString string = mimeData->text(); // fail silent if more than one image is in clipboard. if (string.count(QString::fromLatin1("\n")) != 0) return; const QString urlHead = QLatin1String("file://"); if (string.startsWith(urlHead)) { string = string.right(string.size()-urlHead.size()); } const DB::FileName fileName = DB::FileName::fromAbsolutePath(string); // fail silent if there is no file. if (fileName.isNull()) return; MD5 originalSum = MD5Sum( fileName ); ImageInfoPtr originalInfo; if ( DB::ImageDB::instance()->md5Map()->contains( originalSum ) ) { originalInfo = DB::ImageDB::instance()->info( fileName ); } else { originalInfo = fileName.info(); } // fail silent if there is no info for the file. if (!originalInfo) return; Q_FOREACH(const DB::FileName& newFile, selected()) { newFile.info()->copyExtraData(*originalInfo, false); } DirtyIndicator::markDirty(); } void MainWindow::Window::slotReReadExifInfo() { DB::FileNameList files = selectedOnDisk(); static Exif::ReReadDialog* dialog = nullptr; if ( ! dialog ) dialog = new Exif::ReReadDialog( this ); if ( dialog->exec( files ) == QDialog::Accepted ) DirtyIndicator::markDirty(); } void MainWindow::Window::slotAutoStackImages() { const DB::FileNameList list = selected(); if (list.isEmpty()) { KMessageBox::sorry( this, i18n("No item is selected."), i18n("No Selection") ); return; } QPointer stacker = new AutoStackImages( this, list ); if ( stacker->exec() == QDialog::Accepted ) showThumbNails(); delete stacker; } /** * In thumbnail mode, return a list of files that are selected. * Otherwise, return all images in the current scope/context. */ DB::FileNameList MainWindow::Window::selected( ThumbnailView::SelectionMode mode) const { if ( m_thumbnailView->gui() == m_stack->currentWidget() ) return m_thumbnailView->selection(mode); else // return all images in the current scope (parameter false: include images not on disk) return DB::ImageDB::instance()->currentScope(false); } void MainWindow::Window::slotViewNewWindow() { slotView( false, false ); } /* * Returns a list of files that are both selected and on disk. If there are no * selected files, returns all files form current context that are on disk. * Note: On some setups (NFS), this can be a very time-consuming method! * */ DB::FileNameList MainWindow::Window::selectedOnDisk() { const DB::FileNameList list = selected(ThumbnailView::NoExpandCollapsedStacks); DB::FileNameList listOnDisk; Q_FOREACH(const DB::FileName& fileName, list) { if (DB::ImageInfo::imageOnDisk(fileName)) listOnDisk.append(fileName); } return listOnDisk; } void MainWindow::Window::slotView( bool reuse, bool slideShow, bool random ) { launchViewer(selected(ThumbnailView::NoExpandCollapsedStacks), reuse, slideShow, random ); } void MainWindow::Window::launchViewer(const DB::FileNameList& inputMediaList, bool reuse, bool slideShow, bool random) { DB::FileNameList mediaList = inputMediaList; int seek = -1; if (mediaList.isEmpty()) { mediaList = m_thumbnailView->imageList( ThumbnailView::ViewOrder ); } else if (mediaList.size() == 1) { // we fake it so it appears the user has selected all images // and magically scrolls to the originally selected one const DB::FileName first = mediaList.at(0); mediaList = m_thumbnailView->imageList( ThumbnailView::ViewOrder ); seek = mediaList.indexOf(first); } if (mediaList.isEmpty()) mediaList = DB::ImageDB::instance()->currentScope( false ); if (mediaList.isEmpty()) { KMessageBox::sorry( this, i18n("There are no images to be shown.") ); return; } if (random) { mediaList = DB::FileNameList(Utilities::shuffleList(mediaList)); } Viewer::ViewerWidget* viewer; if ( reuse && Viewer::ViewerWidget::latest() ) { viewer = Viewer::ViewerWidget::latest(); viewer->raise(); viewer->activateWindow(); } else viewer = new Viewer::ViewerWidget(Viewer::ViewerWidget::ViewerWindow, &m_viewerInputMacros); connect(viewer, &Viewer::ViewerWidget::soughtTo, m_thumbnailView, &ThumbnailView::ThumbnailFacade::changeSingleSelection); connect(viewer, &Viewer::ViewerWidget::imageRotated, this, &Window::slotImageRotated); viewer->show( slideShow ); viewer->load( mediaList, seek < 0 ? 0 : seek ); viewer->raise(); } void MainWindow::Window::slotSortByDateAndTime() { DB::ImageDB::instance()->sortAndMergeBackIn(selected()); showThumbNails( DB::ImageDB::instance()->search( Browser::BrowserWidget::instance()->currentContext())); DirtyIndicator::markDirty(); } void MainWindow::Window::slotSortAllByDateAndTime() { DB::ImageDB::instance()->sortAndMergeBackIn(DB::ImageDB::instance()->images()); if ( m_thumbnailView->gui() == m_stack->currentWidget() ) showThumbNails( DB::ImageDB::instance()->search( Browser::BrowserWidget::instance()->currentContext())); DirtyIndicator::markDirty(); } QString MainWindow::Window::welcome() { QString configFileName; QPointer dialog = new WelcomeDialog( this ); // exit if the user dismissed the welcome dialog if (!dialog->exec()) { qApp->quit(); } configFileName = dialog->configFileName(); delete dialog; return configFileName; } void MainWindow::Window::closeEvent( QCloseEvent* e ) { bool quit = true; quit = slotExit(); // If I made it here, then the user canceled if ( !quit ) e->ignore(); else e->setAccepted(true); } void MainWindow::Window::slotLimitToSelected() { Utilities::ShowBusyCursor dummy; showThumbNails( selected() ); } void MainWindow::Window::setupMenuBar() { // File menu KStandardAction::save( this, SLOT(slotSave()), actionCollection() ); KStandardAction::quit( this, SLOT(slotExit()), actionCollection() ); m_generateHtml = actionCollection()->addAction( QString::fromLatin1("exportHTML") ); m_generateHtml->setText( i18n("Generate HTML...") ); connect(m_generateHtml, &QAction::triggered, this, &Window::slotExportToHTML); QAction* a = actionCollection()->addAction( QString::fromLatin1("import"), this, SLOT(slotImport()) ); a->setText( i18n( "Import...") ); a = actionCollection()->addAction( QString::fromLatin1("export"), this, SLOT(slotExport()) ); a->setText( i18n( "Export/Copy Images...") ); // Go menu a = KStandardAction::back( m_browser, SLOT(back()), actionCollection() ); connect(m_browser, &Browser::BrowserWidget::canGoBack, a, &QAction::setEnabled); a->setEnabled( false ); a = KStandardAction::forward( m_browser, SLOT(forward()), actionCollection() ); connect(m_browser, &Browser::BrowserWidget::canGoForward, a, &QAction::setEnabled); a->setEnabled( false ); a = KStandardAction::home( m_browser, SLOT(home()), actionCollection() ); actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_Home); connect(a, &QAction::triggered, m_dateBar, &DateBar::DateBarWidget::clearSelection); KStandardAction::redisplay( m_browser, SLOT(go()), actionCollection() ); // The Edit menu m_copy = KStandardAction::copy( this, SLOT(slotCopySelectedURLs()), actionCollection() ); m_paste = KStandardAction::paste( this, SLOT(slotPasteInformation()), actionCollection() ); m_paste->setEnabled(false); m_selectAll = KStandardAction::selectAll( m_thumbnailView, SLOT(selectAll()), actionCollection() ); m_clearSelection = KStandardAction::deselect( m_thumbnailView, SLOT(clearSelection()), actionCollection() ); m_clearSelection->setEnabled(false); KStandardAction::find( this, SLOT(slotSearch()), actionCollection() ); m_deleteSelected = actionCollection()->addAction(QString::fromLatin1("deleteSelected")); m_deleteSelected->setText( i18nc("Delete selected images", "Delete Selected" ) ); m_deleteSelected->setIcon( QIcon::fromTheme( QString::fromLatin1("edit-delete") ) ); actionCollection()->setDefaultShortcut(m_deleteSelected, Qt::Key_Delete); connect(m_deleteSelected, &QAction::triggered, this, &Window::slotDeleteSelected); a = actionCollection()->addAction(QString::fromLatin1("removeTokens"), this, SLOT(slotRemoveTokens())); a->setText( i18n("Remove Tokens...") ); a = actionCollection()->addAction(QString::fromLatin1("showListOfFiles"), this, SLOT(slotShowListOfFiles())); a->setText( i18n("Open List of Files...")) ; m_configOneAtATime = actionCollection()->addAction( QString::fromLatin1("oneProp"), this, SLOT(slotConfigureImagesOneAtATime()) ); m_configOneAtATime->setText( i18n( "Annotate Individual Items" ) ); actionCollection()->setDefaultShortcut(m_configOneAtATime, Qt::CTRL + Qt::Key_1); m_configAllSimultaniously = actionCollection()->addAction( QString::fromLatin1("allProp"), this, SLOT(slotConfigureAllImages()) ); m_configAllSimultaniously->setText( i18n( "Annotate Multiple Items at a Time" ) ); actionCollection()->setDefaultShortcut(m_configAllSimultaniously, Qt::CTRL + Qt::Key_2); m_createImageStack = actionCollection()->addAction( QString::fromLatin1("createImageStack"), this, SLOT(slotCreateImageStack()) ); m_createImageStack->setText( i18n("Merge Images into a Stack") ); actionCollection()->setDefaultShortcut(m_createImageStack, Qt::CTRL + Qt::Key_3); m_unStackImages = actionCollection()->addAction( QString::fromLatin1("unStackImages"), this, SLOT(slotUnStackImages()) ); m_unStackImages->setText( i18n("Remove Images from Stack") ); m_setStackHead = actionCollection()->addAction( QString::fromLatin1("setStackHead"), this, SLOT(slotSetStackHead()) ); m_setStackHead->setText( i18n("Set as First Image in Stack") ); actionCollection()->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4); m_rotLeft = actionCollection()->addAction( QString::fromLatin1("rotateLeft"), this, SLOT(slotRotateSelectedLeft()) ); m_rotLeft->setText( i18n( "Rotate counterclockwise" ) ); actionCollection()->setDefaultShortcut(m_rotLeft, Qt::Key_7); m_rotRight = actionCollection()->addAction( QString::fromLatin1("rotateRight"), this, SLOT(slotRotateSelectedRight()) ); m_rotRight->setText( i18n( "Rotate clockwise" ) ); actionCollection()->setDefaultShortcut(m_rotRight, Qt::Key_9); // The Images menu m_view = actionCollection()->addAction( QString::fromLatin1("viewImages"), this, SLOT(slotView()) ); m_view->setText( i18n("View") ); actionCollection()->setDefaultShortcut(m_view, Qt::CTRL + Qt::Key_I); m_viewInNewWindow = actionCollection()->addAction( QString::fromLatin1("viewImagesNewWindow"), this, SLOT(slotViewNewWindow()) ); m_viewInNewWindow->setText( i18n("View (In New Window)") ); m_runSlideShow = actionCollection()->addAction( QString::fromLatin1("runSlideShow"), this, SLOT(slotRunSlideShow()) ); m_runSlideShow->setText( i18n("Run Slide Show") ); m_runSlideShow->setIcon( QIcon::fromTheme( QString::fromLatin1("view-presentation") ) ); actionCollection()->setDefaultShortcut(m_runSlideShow, Qt::CTRL + Qt::Key_R); m_runRandomSlideShow = actionCollection()->addAction( QString::fromLatin1("runRandomizedSlideShow"), this, SLOT(slotRunRandomizedSlideShow()) ); m_runRandomSlideShow->setText( i18n( "Run Randomized Slide Show" ) ); a = actionCollection()->addAction( QString::fromLatin1("collapseAllStacks"), m_thumbnailView, SLOT(collapseAllStacks()) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::collapseAllStacksEnabled, a, &QAction::setEnabled); a->setEnabled(false); a->setText( i18n("Collapse all stacks" )); a = actionCollection()->addAction( QString::fromLatin1("expandAllStacks"), m_thumbnailView, SLOT(expandAllStacks()) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::expandAllStacksEnabled, a, &QAction::setEnabled); a->setEnabled(false); a->setText( i18n("Expand all stacks" )); QActionGroup* grp = new QActionGroup( this ); a = actionCollection()->add( QString::fromLatin1("orderIncr"), this, SLOT(slotOrderIncr()) ); a->setText( i18n("Show &Oldest First") ) ; a->setActionGroup(grp); a->setChecked( !Settings::SettingsData::instance()->showNewestThumbnailFirst() ); a = actionCollection()->add( QString::fromLatin1("orderDecr"), this, SLOT(slotOrderDecr()) ); a->setText( i18n("Show &Newest First") ); a->setActionGroup(grp); a->setChecked( Settings::SettingsData::instance()->showNewestThumbnailFirst() ); m_sortByDateAndTime = actionCollection()->addAction( QString::fromLatin1("sortImages"), this, SLOT(slotSortByDateAndTime()) ); m_sortByDateAndTime->setText( i18n("Sort Selected by Date && Time") ); m_limitToMarked = actionCollection()->addAction( QString::fromLatin1("limitToMarked"), this, SLOT(slotLimitToSelected()) ); m_limitToMarked->setText( i18n("Limit View to Selection") ); m_jumpToContext = actionCollection()->addAction( QString::fromLatin1("jumpToContext"), this, SLOT(slotJumpToContext()) ); m_jumpToContext->setText( i18n("Jump to Context") ); actionCollection()->setDefaultShortcut(m_jumpToContext, Qt::CTRL + Qt::Key_J); m_jumpToContext->setIcon( QIcon::fromTheme( QString::fromLatin1( "kphotoalbum" ) ) ); // icon suggestion: go-jump (don't know the exact meaning though, so I didn't replace it right away m_lock = actionCollection()->addAction( QString::fromLatin1("lockToDefaultScope"), this, SLOT(lockToDefaultScope()) ); m_lock->setText( i18n("Lock Images") ); m_unlock = actionCollection()->addAction( QString::fromLatin1("unlockFromDefaultScope"), this, SLOT(unlockFromDefaultScope()) ); m_unlock->setText( i18n("Unlock") ); a = actionCollection()->addAction( QString::fromLatin1("changeScopePasswd"), this, SLOT(changePassword()) ); a->setText( i18n("Change Password...") ); actionCollection()->setDefaultShortcut(a, 0); m_setDefaultPos = actionCollection()->addAction( QString::fromLatin1("setDefaultScopePositive"), this, SLOT(setDefaultScopePositive()) ); m_setDefaultPos->setText( i18n("Lock Away All Other Items") ); m_setDefaultNeg = actionCollection()->addAction( QString::fromLatin1("setDefaultScopeNegative"), this, SLOT(setDefaultScopeNegative()) ); m_setDefaultNeg->setText( i18n("Lock Away Current Set of Items") ); // Maintenance a = actionCollection()->addAction( QString::fromLatin1("findUnavailableImages"), this, SLOT(slotShowNotOnDisk()) ); a->setText( i18n("Display Images and Videos Not on Disk") ); a = actionCollection()->addAction( QString::fromLatin1("findImagesWithInvalidDate"), this, SLOT(slotShowImagesWithInvalidDate()) ); a->setText( i18n("Display Images and Videos with Incomplete Dates...") ); -#ifdef DOES_STILL_NOT_WORK_IN_KPA4 a = actionCollection()->addAction( QString::fromLatin1("findImagesWithChangedMD5Sum"), this, SLOT(slotShowImagesWithChangedMD5Sum()) ); a->setText( i18n("Display Images and Videos with Changed MD5 Sum") ); -#endif //DOES_STILL_NOT_WORK_IN_KPA4 a = actionCollection()->addAction( QLatin1String("mergeDuplicates"), this, SLOT(mergeDuplicates())); a->setText(i18n("Merge duplicates")); a = actionCollection()->addAction( QString::fromLatin1("rebuildMD5s"), this, SLOT(slotRecalcCheckSums()) ); a->setText( i18n("Recalculate Checksum") ); a = actionCollection()->addAction( QString::fromLatin1("rescan"), DB::ImageDB::instance(), SLOT(slotRescan()) ); a->setIcon(QIcon::fromTheme( QString::fromLatin1( "document-import" ) )); a->setText( i18n("Rescan for Images and Videos") ); QAction* recreateExif = actionCollection()->addAction( QString::fromLatin1( "recreateExifDB" ), this, SLOT(slotRecreateExifDB()) ); recreateExif->setText( i18n("Recreate Exif Search Database") ); QAction* rereadExif = actionCollection()->addAction( QString::fromLatin1("reReadExifInfo"), this, SLOT(slotReReadExifInfo()) ); rereadExif->setText( i18n("Read Exif Info from Files...") ); m_sortAllByDateAndTime = actionCollection()->addAction( QString::fromLatin1("sortAllImages"), this, SLOT(slotSortAllByDateAndTime()) ); m_sortAllByDateAndTime->setText( i18n("Sort All by Date && Time") ); m_sortAllByDateAndTime->setEnabled(true); m_AutoStackImages = actionCollection()->addAction( QString::fromLatin1( "autoStack" ), this, SLOT (slotAutoStackImages()) ); m_AutoStackImages->setText( i18n("Automatically Stack Selected Images...") ); a = actionCollection()->addAction( QString::fromLatin1("buildThumbs"), this, SLOT(slotBuildThumbnails()) ); a->setText( i18n("Build Thumbnails") ); a->setText( i18n("Statistics...") ); m_markUntagged = actionCollection()->addAction(QString::fromUtf8("markUntagged"), this, SLOT(slotMarkUntagged())); m_markUntagged->setText(i18n("Mark As Untagged")); // Settings KStandardAction::preferences( this, SLOT(slotOptions()), actionCollection() ); KStandardAction::keyBindings( this, SLOT(slotConfigureKeyBindings()), actionCollection() ); KStandardAction::configureToolbars( this, SLOT(slotConfigureToolbars()), actionCollection() ); a = actionCollection()->addAction( QString::fromLatin1("readdAllMessages"), this, SLOT(slotReenableMessages()) ); a->setText( i18n("Enable All Messages") ); m_viewMenu = actionCollection()->add( QString::fromLatin1("configureView") ); m_viewMenu->setText( i18n("Configure Current View") ); m_viewMenu->setIcon( QIcon::fromTheme( QString::fromLatin1( "view-list-details" ) ) ); m_viewMenu->setDelayed( false ); QActionGroup* viewGrp = new QActionGroup( this ); viewGrp->setExclusive( true ); m_smallListView = actionCollection()->add( QString::fromLatin1("smallListView"), m_browser, SLOT(slotSmallListView()) ); m_smallListView->setText( i18n("Tree") ); m_viewMenu->addAction( m_smallListView ); m_smallListView->setActionGroup( viewGrp ); m_largeListView = actionCollection()->add( QString::fromLatin1("largelistview"), m_browser, SLOT(slotLargeListView()) ); m_largeListView->setText( i18n("Tree with User Icons") ); m_viewMenu->addAction( m_largeListView ); m_largeListView->setActionGroup( viewGrp ); m_largeIconView = actionCollection()->add( QString::fromLatin1("largeiconview"), m_browser, SLOT(slotLargeIconView()) ); m_largeIconView->setText( i18n("Icons") ); m_viewMenu->addAction( m_largeIconView ); m_largeIconView->setActionGroup( viewGrp ); connect(m_browser, &Browser::BrowserWidget::isViewChangeable, viewGrp, &QActionGroup::setEnabled); connect(m_browser, &Browser::BrowserWidget::currentViewTypeChanged, this, &Window::slotUpdateViewMenu); // The help menu KStandardAction::tipOfDay( this, SLOT(showTipOfDay()), actionCollection() ); a = actionCollection()->add( QString::fromLatin1("showToolTipOnImages") ); a->setText( i18n("Show Tooltips in Thumbnails Window") ); actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_T); connect(a, &QAction::toggled, m_thumbnailView, &ThumbnailView::ThumbnailFacade::showToolTipsOnImages); a = actionCollection()->addAction( QString::fromLatin1("runDemo"), this, SLOT(runDemo()) ); a->setText( i18n("Run KPhotoAlbum Demo") ); a = actionCollection()->addAction( QString::fromLatin1("features"), this, SLOT(showFeatures()) ); a->setText( i18n("KPhotoAlbum Feature Status") ); a = actionCollection()->addAction( QString::fromLatin1("showVideo"), this, SLOT(showVideos()) ); a->setText( i18n( "Show Demo Videos") ); // Context menu actions m_showExifDialog = actionCollection()->addAction( QString::fromLatin1("showExifInfo"), this, SLOT(slotShowExifInfo()) ); m_showExifDialog->setText( i18n("Show Exif Info") ); m_recreateThumbnails = actionCollection()->addAction( QString::fromLatin1("recreateThumbnails"), m_thumbnailView, SLOT(slotRecreateThumbnail()) ); m_recreateThumbnails->setText( i18n("Recreate Selected Thumbnails") ); m_useNextVideoThumbnail = actionCollection()->addAction( QString::fromLatin1("useNextVideoThumbnail"), this, SLOT(useNextVideoThumbnail())); m_useNextVideoThumbnail->setText(i18n("Use next video thumbnail")); actionCollection()->setDefaultShortcut(m_useNextVideoThumbnail, Qt::CTRL + Qt::Key_Plus); m_usePreviousVideoThumbnail = actionCollection()->addAction( QString::fromLatin1("usePreviousVideoThumbnail"), this, SLOT(usePreviousVideoThumbnail())); m_usePreviousVideoThumbnail->setText(i18n("Use previous video thumbnail")); actionCollection()->setDefaultShortcut(m_usePreviousVideoThumbnail, Qt::CTRL + Qt::Key_Minus); createGUI( QString::fromLatin1( "kphotoalbumui.rc" ) ); } void MainWindow::Window::slotExportToHTML() { if ( ! m_htmlDialog ) m_htmlDialog = new HTMLGenerator::HTMLDialog( this ); m_htmlDialog->exec(selectedOnDisk()); } void MainWindow::Window::startAutoSaveTimer() { int i = Settings::SettingsData::instance()->autoSave(); m_autoSaveTimer->stop(); if ( i != 0 ) { m_autoSaveTimer->start( i * 1000 * 60 ); } } void MainWindow::Window::slotAutoSave() { if ( m_statusBar->mp_dirtyIndicator->isAutoSaveDirty() ) { Utilities::ShowBusyCursor dummy; m_statusBar->showMessage(i18n("Auto saving....")); DB::ImageDB::instance()->save( Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1(".#index.xml"), true ); ImageManager::ThumbnailCache::instance()->save(); m_statusBar->showMessage(i18n("Auto saving.... Done"), 5000); m_statusBar->mp_dirtyIndicator->autoSaved(); } } void MainWindow::Window::showThumbNails() { m_statusBar->showThumbnailSlider(); reloadThumbnails( ThumbnailView::ClearSelection ); m_stack->setCurrentWidget( m_thumbnailView->gui() ); m_thumbnailView->gui()->setFocus(); updateStates( true ); } void MainWindow::Window::showBrowser() { m_statusBar->clearMessage(); m_statusBar->hideThumbnailSlider(); m_stack->setCurrentWidget( m_browser ); m_browser->setFocus(); updateContextMenuFromSelectionSize( 0 ); updateStates( false ); } void MainWindow::Window::slotOptionGroupChanged() { // FIXME: What if annotation dialog is open? (if that's possible) delete m_annotationDialog; m_annotationDialog = nullptr; DirtyIndicator::markDirty(); } void MainWindow::Window::showTipOfDay() { KTipDialog::showTip( this, QString(), true ); } void MainWindow::Window::runDemo() { KProcess* process = new KProcess; *process << QLatin1String("kphotoalbum") << QLatin1String("--demo"); process->startDetached(); } bool MainWindow::Window::load() { // Let first try to find a config file. QString configFile; QUrl dbFileUrl = Options::the()->dbFile(); if ( !dbFileUrl.isEmpty() && dbFileUrl.isLocalFile() ) { configFile = dbFileUrl.toLocalFile(); } else if ( Options::the()->demoMode() ) { configFile = Utilities::setupDemo(); } else { bool showWelcome = false; KConfigGroup config = KSharedConfig::openConfig()->group(QString::fromUtf8("General")); if ( config.hasKey( QString::fromLatin1("imageDBFile") ) ) { configFile = config.readEntry( QString::fromLatin1("imageDBFile"), QString() ); if ( !QFileInfo( configFile ).exists() ) showWelcome = true; } else showWelcome = true; if ( showWelcome ) { SplashScreen::instance()->hide(); configFile = welcome(); } } if ( configFile.isNull() ) return false; if (configFile.startsWith( QString::fromLatin1( "~" ) ) ) configFile = QDir::home().path() + QString::fromLatin1( "/" ) + configFile.mid(1); // To avoid a race conditions where both the image loader thread creates an instance of // Settings, and where the main thread crates an instance, we better get it created now. Settings::SettingsData::setup( QFileInfo( configFile ).absolutePath() ); if ( Settings::SettingsData::instance()->showSplashScreen() ) { SplashScreen::instance()->show(); qApp->processEvents(); } // Doing some validation on user provided index file if ( Options::the()->dbFile().isValid() ) { QFileInfo fi( configFile ); if ( !fi.dir().exists() ) { KMessageBox::error( this, i18n("

Could not open given index.xml as provided directory does not exist.
%1

", fi.absolutePath()) ); return false; } // We use index.xml as the XML backend, thus we want to test for exactly it fi.setFile( QString::fromLatin1( "%1/index.xml" ).arg( fi.dir().absolutePath() ) ); if ( !fi.exists() ) { int answer = KMessageBox::questionYesNo(this,i18n("

Given index file does not exist, do you want to create following?" "
%1/index.xml

", fi.absolutePath() ) ); if (answer != KMessageBox::Yes) return false; } configFile = fi.absoluteFilePath(); } DB::ImageDB::setupXMLDB( configFile ); // some sanity checks: if ( ! Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && ! (Settings::SettingsData::instance()->untaggedCategory().isEmpty() && Settings::SettingsData::instance()->untaggedTag().isEmpty() ) && ! Options::the()->demoMode() ) { KMessageBox::error( this, i18n( "

You have configured a tag for untagged images, but either the tag itself " "or its category does not exist in the database.

" "

Please review your untagged tag setting under " "Settings|Configure KPhotoAlbum...|Categories

")); } return true; } void MainWindow::Window::contextMenuEvent( QContextMenuEvent* e ) { if ( m_stack->currentWidget() == m_thumbnailView->gui() ) { QMenu menu( this ); menu.addAction( m_configOneAtATime ); menu.addAction( m_configAllSimultaniously ); menu.addSeparator(); menu.addAction( m_createImageStack ); menu.addAction( m_unStackImages ); menu.addAction( m_setStackHead ); menu.addSeparator(); menu.addAction( m_runSlideShow ); menu.addAction(m_runRandomSlideShow ); menu.addAction( m_showExifDialog); menu.addSeparator(); menu.addAction(m_rotLeft); menu.addAction(m_rotRight); menu.addAction(m_recreateThumbnails); menu.addAction(m_useNextVideoThumbnail); menu.addAction(m_usePreviousVideoThumbnail); m_useNextVideoThumbnail->setEnabled(anyVideosSelected()); m_usePreviousVideoThumbnail->setEnabled(anyVideosSelected()); menu.addSeparator(); menu.addAction(m_view); menu.addAction(m_viewInNewWindow); // "Invoke external program" ExternalPopup externalCommands { &menu }; DB::ImageInfoPtr info = m_thumbnailView->mediaIdUnderCursor().info(); externalCommands.populate( info, selected()); QAction* action = menu.addMenu( &externalCommands ); if (!info && selected().isEmpty()) action->setEnabled( false ); QUrl selectedFile = QUrl::fromLocalFile(info->fileName().absolute()); QList allSelectedFiles; for (const QString &selectedFile : selected().toStringList(DB::AbsolutePath)) { allSelectedFiles << QUrl::fromLocalFile(selectedFile); } // "Copy image(s) to ..." CopyPopup copyMenu (&menu, selectedFile, allSelectedFiles, m_lastTarget, CopyPopup::Copy); QAction *copyAction = menu.addMenu(©Menu); if (!info && selected().isEmpty()) { copyAction->setEnabled(false); } // "Link image(s) to ..." CopyPopup linkMenu (&menu, selectedFile, allSelectedFiles, m_lastTarget, CopyPopup::Link); QAction *linkAction = menu.addMenu(&linkMenu); if (!info && selected().isEmpty()) { linkAction->setEnabled(false); } menu.exec( QCursor::pos() ); } e->setAccepted(true); } void MainWindow::Window::setDefaultScopePositive() { Settings::SettingsData::instance()->setCurrentLock( m_browser->currentContext(), false ); } void MainWindow::Window::setDefaultScopeNegative() { Settings::SettingsData::instance()->setCurrentLock( m_browser->currentContext(), true ); } void MainWindow::Window::lockToDefaultScope() { int i = KMessageBox::warningContinueCancel( this, i18n( "

The password protection is only a means of allowing your little sister " "to look in your images, without getting to those embarrassing images from " "your last party.

" "

In other words, anyone with access to the index.xml file can easily " "circumvent this password.

"), i18n("Password Protection"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString::fromLatin1( "lockPassWordIsNotEncruption" ) ); if ( i == KMessageBox::Cancel ) return; setLocked( true, false ); } void MainWindow::Window::unlockFromDefaultScope() { bool OK = ( Settings::SettingsData::instance()->password().isEmpty() ); QPointer dialog = new KPasswordDialog( this ); while ( !OK ) { dialog->setPrompt( i18n("Type in Password to Unlock") ); const int code = dialog->exec(); if ( code == QDialog::Rejected ) return; const QString passwd = dialog->password(); OK = (Settings::SettingsData::instance()->password() == passwd); if ( !OK ) KMessageBox::sorry( this, i18n("Invalid password.") ); } setLocked( false, false ); delete dialog; } void MainWindow::Window::setLocked( bool locked, bool force, bool recount ) { m_statusBar->setLocked( locked ); Settings::SettingsData::instance()->setLocked( locked, force ); m_lock->setEnabled( !locked ); m_unlock->setEnabled( locked ); m_setDefaultPos->setEnabled( !locked ); m_setDefaultNeg->setEnabled( !locked ); if (recount) m_browser->reload(); } void MainWindow::Window::changePassword() { bool OK = ( Settings::SettingsData::instance()->password().isEmpty() ); QPointer dialog = new KPasswordDialog; while ( !OK ) { dialog->setPrompt( i18n("Type in Old Password") ); const int code = dialog->exec(); if ( code == QDialog::Rejected ) return; const QString passwd = dialog->password(); OK = (Settings::SettingsData::instance()->password() == QString(passwd)); if ( !OK ) KMessageBox::sorry( this, i18n("Invalid password.") ); } dialog->setPrompt( i18n("Type in New Password") ); const int code = dialog->exec(); if ( code == QDialog::Accepted ) Settings::SettingsData::instance()->setPassword( dialog->password() ); delete dialog; } void MainWindow::Window::slotConfigureKeyBindings() { Viewer::ViewerWidget* viewer = new Viewer::ViewerWidget; // Do not show, this is only used to get a key configuration KShortcutsDialog* dialog = new KShortcutsDialog(); dialog->addCollection( actionCollection(), i18n( "General" ) ); dialog->addCollection( viewer->actions(), i18n("Viewer") ); #ifdef HASKIPI loadPlugins(); Q_FOREACH( const KIPI::PluginLoader::Info *pluginInfo, m_pluginLoader->pluginList() ) { KIPI::Plugin* plugin = pluginInfo->plugin(); if ( plugin ) dialog->addCollection( plugin->actionCollection(), i18nc("Add 'Plugin' prefix so that KIPI plugins are obvious in KShortcutsDialog…","Plugin: %1", pluginInfo->name()) ); } #endif createAnnotationDialog(); dialog->addCollection( m_annotationDialog->actions(), i18n("Annotation Dialog" ) ); dialog->configure(); delete dialog; delete viewer; } void MainWindow::Window::slotSetFileName( const DB::FileName& fileName ) { ImageInfoPtr info; if ( fileName.isNull() ) m_statusBar->clearMessage(); else { info = fileName.info(); if (info != ImageInfoPtr(nullptr) ) m_statusBar->showMessage( fileName.absolute(), 4000 ); } } void MainWindow::Window::updateContextMenuFromSelectionSize(int selectionSize) { m_configAllSimultaniously->setEnabled(selectionSize > 1); m_configOneAtATime->setEnabled(selectionSize >= 1); m_createImageStack->setEnabled(selectionSize > 1); m_unStackImages->setEnabled(selectionSize >= 1); m_setStackHead->setEnabled(selectionSize == 1); // FIXME: do we want to check if it's stacked here? m_sortByDateAndTime->setEnabled(selectionSize > 1); m_recreateThumbnails->setEnabled(selectionSize >= 1); m_rotLeft->setEnabled(selectionSize >= 1); m_rotRight->setEnabled(selectionSize >= 1); m_AutoStackImages->setEnabled(selectionSize > 1); m_markUntagged->setEnabled(selectionSize >= 1); m_statusBar->mp_selected->setSelectionCount( selectionSize ); m_clearSelection->setEnabled(selectionSize > 0); } void MainWindow::Window::rotateSelected( int angle ) { const DB::FileNameList list = selected(); if (list.isEmpty()) { KMessageBox::sorry( this, i18n("No item is selected."), i18n("No Selection") ); } else { Q_FOREACH(const DB::FileName& fileName, list) { fileName.info()->rotate(angle); ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } m_statusBar->mp_dirtyIndicator->markDirty(); } } void MainWindow::Window::slotRotateSelectedLeft() { rotateSelected( -90 ); reloadThumbnails(); } void MainWindow::Window::slotRotateSelectedRight() { rotateSelected( 90 ); reloadThumbnails(); } void MainWindow::Window::reloadThumbnails( ThumbnailView::SelectionUpdateMethod method ) { m_thumbnailView->reload( method ); updateContextMenuFromSelectionSize( m_thumbnailView->selection().size() ); } void MainWindow::Window::slotUpdateViewMenu( DB::Category::ViewType type ) { if ( type == DB::Category::TreeView ) m_smallListView->setChecked( true ); else if ( type == DB::Category::ThumbedTreeView ) m_largeListView->setChecked( true ); else if ( type == DB::Category::ThumbedIconView ) m_largeIconView->setChecked( true ); } void MainWindow::Window::slotShowNotOnDisk() { DB::FileNameList notOnDisk; Q_FOREACH(const DB::FileName& fileName, DB::ImageDB::instance()->images()) { if ( !fileName.exists() ) notOnDisk.append(fileName); } showThumbNails(notOnDisk); } void MainWindow::Window::slotShowImagesWithChangedMD5Sum() { -#ifdef DOES_STILL_NOT_WORK_IN_KPA4 Utilities::ShowBusyCursor dummy; - StringSet changed = DB::ImageDB::instance()->imagesWithMD5Changed(); + DB::FileNameSet changed = DB::ImageDB::instance()->imagesWithMD5Changed(); showThumbNails( changed.toList() ); -#else // DOES_STILL_NOT_WORK_IN_KPA4 - qFatal("Code commented out in MainWindow::Window::slotShowImagesWithChangedMD5Sum"); -#endif // DOES_STILL_NOT_WORK_IN_KPA4 } void MainWindow::Window::updateStates( bool thumbNailView ) { m_selectAll->setEnabled( thumbNailView ); m_deleteSelected->setEnabled( thumbNailView ); m_limitToMarked->setEnabled( thumbNailView ); m_jumpToContext->setEnabled( thumbNailView ); } void MainWindow::Window::slotRunSlideShow() { slotView( true, true ); } void MainWindow::Window::slotRunRandomizedSlideShow() { slotView( true, true, true ); } MainWindow::Window* MainWindow::Window::theMainWindow() { Q_ASSERT( s_instance ); return s_instance; } void MainWindow::Window::slotConfigureToolbars() { QPointer dlg = new KEditToolBar(guiFactory()); connect(dlg, SIGNAL(newToolbarConfig()), SLOT(slotNewToolbarConfig())); dlg->exec(); delete dlg; } void MainWindow::Window::slotNewToolbarConfig() { createGUI(); createSearchBar(); } void MainWindow::Window::slotImport() { ImportExport::Import::imageImport(); } void MainWindow::Window::slotExport() { ImportExport::Export::imageExport(selectedOnDisk()); } void MainWindow::Window::slotReenableMessages() { int ret = KMessageBox::questionYesNo( this, i18n("

Really enable all message boxes where you previously " "checked the do-not-show-again check box?

" ) ); if ( ret == KMessageBox::Yes ) KMessageBox::enableAllMessages(); } void MainWindow::Window::setupPluginMenu() { QMenu* menu = findChild( QString::fromLatin1("plugins") ); if ( !menu ) { KMessageBox::error( this, i18n("

KPhotoAlbum hit an internal error (missing plug-in menu in MainWindow::Window::setupPluginMenu). This indicate that you forgot to do a make install. If you did compile KPhotoAlbum yourself, then please run make install. If not, please report this as a bug.

KPhotoAlbum will continue execution, but it is not entirely unlikely that it will crash later on due to the missing make install.

" ), i18n("Internal Error") ); m_hasLoadedPlugins = true; return; // This is no good, but lets try and continue. } #ifdef HASKIPI connect(menu, &QMenu::aboutToShow, this, &Window::loadPlugins); m_hasLoadedPlugins = false; #else menu->setEnabled(false); m_hasLoadedPlugins = true; #endif } void MainWindow::Window::loadPlugins() { #ifdef HASKIPI Utilities::ShowBusyCursor dummy; if ( m_hasLoadedPlugins ) return; m_pluginInterface = new Plugins::Interface( this, QString::fromLatin1("KPhotoAlbum kipi interface") ); connect(m_pluginInterface, &Plugins::Interface::imagesChanged, this, &Window::slotImagesChanged); QStringList ignores; ignores << QString::fromLatin1( "CommentsEditor" ) << QString::fromLatin1( "HelloWorld" ); m_pluginLoader = new KIPI::PluginLoader(); m_pluginLoader->setIgnoredPluginsList( ignores ); m_pluginLoader->setInterface( m_pluginInterface ); m_pluginLoader->init(); connect(m_pluginLoader, &KIPI::PluginLoader::replug, this, &Window::plug); m_pluginLoader->loadPlugins(); // Setup signals connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, this, &Window::slotSelectionChanged); m_hasLoadedPlugins = true; // Make sure selection is updated also when plugin loading is // delayed. This is needed, because selection might already be // non-empty when loading the plugins. slotSelectionChanged(selected().size()); #endif // HASKIPI } void MainWindow::Window::plug() { #ifdef HASKIPI unplugActionList( QString::fromLatin1("import_actions") ); unplugActionList( QString::fromLatin1("export_actions") ); unplugActionList( QString::fromLatin1("image_actions") ); unplugActionList( QString::fromLatin1("tool_actions") ); unplugActionList( QString::fromLatin1("batch_actions") ); QList importActions; QList exportActions; QList imageActions; QList toolsActions; QList batchActions; KIPI::PluginLoader::PluginList list = m_pluginLoader->pluginList(); Q_FOREACH( const KIPI::PluginLoader::Info *pluginInfo, list ) { KIPI::Plugin* plugin = pluginInfo->plugin(); if ( !plugin || !pluginInfo->shouldLoad() ) continue; plugin->setup( this ); QList actions = plugin->actions(); Q_FOREACH( QAction *action, actions ) { KIPI::Category category = plugin->category( action ); if ( category == KIPI::ImagesPlugin || category == KIPI::CollectionsPlugin ) imageActions.append( action ); else if ( category == KIPI::ImportPlugin ) importActions.append( action ); else if ( category == KIPI::ExportPlugin ) exportActions.append( action ); else if ( category == KIPI::ToolsPlugin ) toolsActions.append( action ); else if ( category == KIPI::BatchPlugin ) batchActions.append( action ); else { qCWarning(MainWindowLog) << "Unknown category\n"; } } KConfigGroup group = KSharedConfig::openConfig()->group( QString::fromLatin1("Shortcuts") ); plugin->actionCollection()->importGlobalShortcuts( &group ); } setPluginMenuState( "importplugin", importActions ); setPluginMenuState( "exportplugin", exportActions ); setPluginMenuState( "imagesplugins", imageActions ); setPluginMenuState( "batch_plugins", batchActions ); setPluginMenuState( "tool_plugins", toolsActions ); // For this to work I need to pass false as second arg for createGUI plugActionList( QString::fromLatin1("import_actions"), importActions ); plugActionList( QString::fromLatin1("export_actions"), exportActions ); plugActionList( QString::fromLatin1("image_actions"), imageActions ); plugActionList( QString::fromLatin1("tool_actions"), toolsActions ); plugActionList( QString::fromLatin1("batch_actions"), batchActions ); #endif } void MainWindow::Window::setPluginMenuState( const char* name, const QList& actions ) { QMenu* menu = findChild( QString::fromLatin1(name) ); if ( menu ) menu->setEnabled(actions.count() != 0); } void MainWindow::Window::slotImagesChanged( const QList& urls ) { for( QList::ConstIterator it = urls.begin(); it != urls.end(); ++it ) { DB::FileName fileName = DB::FileName::fromAbsolutePath((*it).path()); if ( !fileName.isNull()) { // Plugins may report images outsite of the photodatabase // This seems to be the case with the border image plugin, which reports the destination image ImageManager::ThumbnailCache::instance()->removeThumbnail( fileName ); // update MD5sum: MD5 md5sum = MD5Sum( fileName ); fileName.info()->setMD5Sum( md5sum ); } } m_statusBar->mp_dirtyIndicator->markDirty(); reloadThumbnails( ThumbnailView::MaintainSelection ); } DB::ImageSearchInfo MainWindow::Window::currentContext() { return m_browser->currentContext(); } QString MainWindow::Window::currentBrowseCategory() const { return m_browser->currentCategory(); } void MainWindow::Window::slotSelectionChanged( int count ) { #ifdef HASKIPI m_pluginInterface->slotSelectionChanged( count != 0 ); #else Q_UNUSED( count ); #endif } void MainWindow::Window::resizeEvent( QResizeEvent* ) { if ( Settings::SettingsData::ready() && isVisible() ) Settings::SettingsData::instance()->setWindowGeometry( Settings::MainWindow, geometry() ); } void MainWindow::Window::moveEvent( QMoveEvent * ) { if ( Settings::SettingsData::ready() && isVisible() ) Settings::SettingsData::instance()->setWindowGeometry( Settings::MainWindow, geometry() ); } void MainWindow::Window::slotRemoveTokens() { if ( !m_tokenEditor ) m_tokenEditor = new TokenEditor( this ); m_tokenEditor->show(); connect(m_tokenEditor, &TokenEditor::finished, m_browser, &Browser::BrowserWidget::go); } void MainWindow::Window::slotShowListOfFiles() { QStringList list = QInputDialog::getMultiLineText( this, i18n("Open List of Files"), i18n("You can open a set of files from KPhotoAlbum's image root by listing the files here.") ) .split( QChar::fromLatin1('\n'), QString::SkipEmptyParts ); if ( list.isEmpty() ) return; DB::FileNameList out; for ( QStringList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it ) { QString fileNameStr = Utilities::imageFileNameToAbsolute( *it ); if ( fileNameStr.isNull() ) continue; const DB::FileName fileName = DB::FileName::fromAbsolutePath(fileNameStr); if ( !fileName.isNull() ) out.append(fileName); } if (out.isEmpty()) KMessageBox::sorry( this, i18n("No images matching your input were found."), i18n("No Matches") ); else showThumbNails(out); } void MainWindow::Window::updateDateBar( const Browser::BreadcrumbList& path ) { static QString lastPath = QString::fromLatin1("ThisStringShouldNeverBeSeenSoWeUseItAsInitialContent"); if ( path.toString() != lastPath ) updateDateBar(); lastPath = path.toString(); } void MainWindow::Window::updateDateBar() { m_dateBar->setImageDateCollection( DB::ImageDB::instance()->rangeCollection() ); } void MainWindow::Window::slotShowImagesWithInvalidDate() { QPointer finder = new InvalidDateFinder( this ); if ( finder->exec() == QDialog::Accepted ) showThumbNails(); delete finder; } void MainWindow::Window::showDateBarTip( const QString& msg ) { m_statusBar->showMessage( msg, 3000 ); } void MainWindow::Window::slotJumpToContext() { const DB::FileName fileName =m_thumbnailView->currentItem(); if ( !fileName.isNull() ) { m_browser->addImageView(fileName); } } void MainWindow::Window::setDateRange( const DB::ImageDate& range ) { DB::ImageDB::instance()->setDateRange( range, m_dateBar->includeFuzzyCounts() ); m_statusBar->mp_partial->showBrowserMatches( this->selected().size() ); m_browser->reload(); reloadThumbnails( ThumbnailView::MaintainSelection ); } void MainWindow::Window::clearDateRange() { DB::ImageDB::instance()->clearDateRange(); m_browser->reload(); reloadThumbnails( ThumbnailView::MaintainSelection ); } void MainWindow::Window::showThumbNails(const DB::FileNameList& items) { m_thumbnailView->setImageList(items); m_statusBar->mp_partial->setMatchCount(items.size()); showThumbNails(); } void MainWindow::Window::slotRecalcCheckSums() { DB::ImageDB::instance()->slotRecalcCheckSums( selected() ); } void MainWindow::Window::slotShowExifInfo() { DB::FileNameList items = selectedOnDisk(); if (!items.isEmpty()) { Exif::InfoDialog* exifDialog = new Exif::InfoDialog(items.at(0), this); exifDialog->show(); } } void MainWindow::Window::showFeatures() { FeatureDialog dialog(this); dialog.exec(); } void MainWindow::Window::showImage( const DB::FileName& fileName ) { launchViewer(DB::FileNameList() << fileName, true, false, false); } void MainWindow::Window::slotBuildThumbnails() { ImageManager::ThumbnailBuilder::instance()->buildAll( ImageManager::StartNow ); } void MainWindow::Window::slotBuildThumbnailsIfWanted() { ImageManager::ThumbnailCache::instance()->flush(); if ( ! Settings::SettingsData::instance()->incrementalThumbnails()) ImageManager::ThumbnailBuilder::instance()->buildAll( ImageManager::StartDelayed ); } void MainWindow::Window::slotOrderIncr() { m_thumbnailView->setSortDirection( ThumbnailView::OldestFirst ); } void MainWindow::Window::slotOrderDecr() { m_thumbnailView->setSortDirection( ThumbnailView::NewestFirst ); } void MainWindow::Window::showVideos() { #if (KIO_VERSION >= ((5<<16)|(31<<8)|(0))) KRun::runUrl(QUrl(QString::fromLatin1("http://www.kphotoalbum.org/index.php?page=videos")) , QString::fromLatin1( "text/html" ) , this , KRun::RunFlags() ); #else // this signature is deprecated in newer kio versions // TODO: remove this when we don't support Ubuntu 16.04 LTS anymore KRun::runUrl(QUrl(QString::fromLatin1("http://www.kphotoalbum.org/index.php?page=videos")) , QString::fromLatin1( "text/html" ) , this ); #endif } void MainWindow::Window::slotStatistics() { static StatisticsDialog* dialog = new StatisticsDialog(this); dialog->show(); } void MainWindow::Window::slotMarkUntagged() { if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()) { for (const DB::FileName& newFile : selected()) { newFile.info()->addCategoryInfo(Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag()); } DirtyIndicator::markDirty(); } else { // Note: the same dialog text is used in // Browser::OverviewPage::activateUntaggedImagesAction(), // so if it is changed, be sure to also change it there! KMessageBox::information(this, i18n("

You have not yet configured which tag to use for indicating untagged images." "

" "

Please follow these steps to do so:" "

  • In the menu bar choose Settings
  • " "
  • From there choose Configure KPhotoAlbum
  • " "
  • Now choose the Categories icon
  • " "
  • Now configure section Untagged Images

"), i18n("Feature has not been configured") ); } } void MainWindow::Window::setupStatusBar() { m_statusBar = new MainWindow::StatusBar; setStatusBar( m_statusBar ); setLocked( Settings::SettingsData::instance()->locked(), true, false ); } void MainWindow::Window::slotRecreateExifDB() { Exif::Database::instance()->recreate(); } void MainWindow::Window::useNextVideoThumbnail() { UpdateVideoThumbnail::useNext(selected()); } void MainWindow::Window::usePreviousVideoThumbnail() { UpdateVideoThumbnail::usePrevious(selected()); } void MainWindow::Window::mergeDuplicates() { DuplicateMerger* merger = new DuplicateMerger; merger->show(); } void MainWindow::Window::slotThumbnailSizeChanged() { QString thumbnailSizeMsg = i18nc( "@info:status", //xgettext:no-c-format "Thumbnail width: %1px (storage size: %2px)", Settings::SettingsData::instance()->actualThumbnailSize(), Settings::SettingsData::instance()->thumbnailSize() ); m_statusBar->showMessage( thumbnailSizeMsg, 4000); } void MainWindow::Window::createSearchBar() { // Set up the search tool bar SearchBar* bar = new SearchBar( this ); bar->setLineEditEnabled(false); bar->setObjectName(QString::fromUtf8("searchBar")); connect(bar, &SearchBar::textChanged, m_browser, &Browser::BrowserWidget::slotLimitToMatch); connect(bar, &SearchBar::returnPressed, m_browser, &Browser::BrowserWidget::slotInvokeSeleted); connect(bar, &SearchBar::keyPressed, m_browser, &Browser::BrowserWidget::scrollKeyPressed); connect(m_browser, &Browser::BrowserWidget::viewChanged, bar, &SearchBar::reset); connect(m_browser, &Browser::BrowserWidget::isSearchable, bar, &SearchBar::setLineEditEnabled); } void MainWindow::Window::executeStartupActions() { new ImageManager::ThumbnailBuilder( m_statusBar, this ); if ( ! Settings::SettingsData::instance()->incrementalThumbnails()) ImageManager::ThumbnailBuilder::instance()->buildMissing(); connect( Settings::SettingsData::instance(), SIGNAL(thumbnailSizeChanged(int)), this, SLOT(slotBuildThumbnailsIfWanted()) ); if ( ! FeatureDialog::hasVideoThumbnailer() ) { BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::SearchForVideosWithoutLengthInfo ); BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::SearchForVideosWithoutVideoThumbnailsJob ); } } void MainWindow::Window::checkIfMplayerIsInstalled() { if (Options::the()->demoMode()) return; if ( !FeatureDialog::hasVideoThumbnailer() ) { KMessageBox::information( this, i18n("

Unable to find ffmpeg or MPlayer on the system.

" "

Without either of these, KPhotoAlbum will not be able to display video thumbnails and video lengths. " "Please install the ffmpeg or MPlayer package

"), i18n("Video thumbnails are not available"), QString::fromLatin1("mplayerNotInstalled")); } else { KMessageBox::enableMessage( QString::fromLatin1("mplayerNotInstalled") ); if ( FeatureDialog::ffmpegBinary().isEmpty() && !FeatureDialog::isMplayer2() ) { KMessageBox::information( this, i18n("

You have MPlayer installed on your system, but it is unfortunately not version 2. " "MPlayer2 is on most systems a separate package, please install that if at all possible, " "as that version has much better support for extracting thumbnails from videos.

"), i18n("MPlayer is too old"), QString::fromLatin1("mplayerVersionTooOld")); } else KMessageBox::enableMessage( QString::fromLatin1("mplayerVersionTooOld") ); } } bool MainWindow::Window::anyVideosSelected() const { Q_FOREACH(const DB::FileName& fileName, selected()) { if ( Utilities::isVideo(fileName)) return true; } return false; } void MainWindow::Window::announceAndroidVersion() { // Don't bother people with this information when they are starting KPA the first time if (DB::ImageDB::instance()->totalCount() < 100) return; const QString doNotShowKey = QString::fromLatin1( "announce_android_version_key" ); const QString txt = i18n("

Did you know that there is an Android client for KPhotoAlbum?
" "With the Android client you can view your images from your desktop.

" "

See youtube video or " "install from google play

" ); KMessageBox::information( this, txt, QString(), doNotShowKey, KMessageBox::AllowLink ); } void MainWindow::Window::setHistogramVisibilty( bool visible ) const { if (visible) { m_dateBar->show(); m_dateBarLine->show(); } else { m_dateBar->hide(); m_dateBarLine->hide(); } } void MainWindow::Window::slotImageRotated(const DB::FileName& fileName) { // An image has been rotated by the annotation dialog or the viewer. // We have to reload the respective thumbnail to get it in the right angle ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } bool MainWindow::Window::dbIsDirty() const { return m_statusBar->mp_dirtyIndicator->isSaveDirty(); } #ifdef HAVE_KGEOMAP void MainWindow::Window::showPositionBrowser() { Browser::PositionBrowserWidget *positionBrowser = positionBrowserWidget(); m_stack->setCurrentWidget(positionBrowser); updateStates( false ); } Browser::PositionBrowserWidget* MainWindow::Window::positionBrowserWidget() { if (m_positionBrowser == 0) { m_positionBrowser = createPositionBrowser(); } return m_positionBrowser; } Browser::PositionBrowserWidget* MainWindow::Window::createPositionBrowser() { Browser::PositionBrowserWidget* widget = new Browser::PositionBrowserWidget(m_stack); m_stack->addWidget(widget); return widget; } #endif // vi:expandtab:tabstop=4 shiftwidth=4: