diff --git a/DB/MD5.cpp b/DB/MD5.cpp
index 3304f6cf..26453296 100644
--- a/DB/MD5.cpp
+++ b/DB/MD5.cpp
@@ -1,87 +1,119 @@
/* Copyright (C) 2018 Johannes Zarl-Zierl
Copyright (C) 2007-2010 Tuomas Suutari
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 "MD5.h"
+#include "FileName.h"
+
+#include
+#include
+#include
+
DB::MD5::MD5():
m_isNull(true),
m_v0(0),
m_v1(0)
{
}
DB::MD5::MD5(const QString &md5str):
m_isNull(md5str.isEmpty()),
m_v0(md5str.left(16).toULongLong(0, 16)),
m_v1(md5str.mid(16, 16).toULongLong(0, 16))
{
}
bool DB::MD5::isNull() const
{
return m_isNull;
}
DB::MD5 &DB::MD5::operator=(const QString &md5str)
{
if (md5str.isEmpty()) {
m_isNull = true;
}
else {
m_isNull = false;
m_v0 = md5str.left(16).toULongLong(0, 16);
m_v1 = md5str.mid(16, 16).toULongLong(0, 16);
}
return *this;
}
QString DB::MD5::toHexString() const
{
QString res;
static QChar ZERO(QChar::fromLatin1('0'));
if (!isNull()) {
res += QString::number(m_v0, 16).rightJustified(16, ZERO);
res += QString::number(m_v1, 16).rightJustified(16, ZERO);
}
return res;
}
bool DB::MD5::operator==(const DB::MD5 &other) const
{
if (isNull() || other.isNull())
return isNull() == other.isNull();
return (m_v0 == other.m_v0 &&
m_v1 == other.m_v1);
}
bool DB::MD5::operator!=(const DB::MD5 &other) const
{
return !(*this == other);
}
bool DB::MD5::operator<(const DB::MD5 &other) const
{
if (isNull() || other.isNull())
return isNull() && !other.isNull();
return (m_v0 < other.m_v0 ||
(m_v0 == other.m_v0 &&
(m_v1 < other.m_v1)));
}
+
+namespace {
+// Determined experimentally to yield best results (on Seagate 2TB 2.5" disk,
+// 5400 RPM). Performance is very similar at 524288. Above that, performance
+// was significantly worse. Below that, performance also deteriorated.
+// This assumes use of one image scout thread (see DB/ImageScout.cpp). Without
+// a scout thread, performance was about 10-15% worse.
+constexpr int MD5_BUFFER_SIZE = 262144;
+}
+
+DB::MD5 DB::MD5Sum( const DB::FileName& fileName )
+{
+ DB::MD5 checksum;
+ QFile file( fileName.absolute() );
+ if ( file.open( QIODevice::ReadOnly ) )
+ {
+ QCryptographicHash md5calculator(QCryptographicHash::Md5);
+ while ( !file.atEnd() ) {
+ QByteArray md5Buffer( file.read( MD5_BUFFER_SIZE ) );
+ md5calculator.addData( md5Buffer );
+ }
+ file.close();
+ checksum = DB::MD5(QString::fromLatin1(md5calculator.result().toHex()));
+ }
+ return checksum;
+}
diff --git a/DB/MD5.h b/DB/MD5.h
index fe1d9457..2f379ade 100644
--- a/DB/MD5.h
+++ b/DB/MD5.h
@@ -1,59 +1,64 @@
/*
Copyright (C) 2018 Johannes Zarl-Zierl
Copyright (C) 2007-2010 Tuomas Suutari
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 St, Fifth Floor, Boston,
MA 02110-1301 USA.
*/
#ifndef DB_MD5_H
#define DB_MD5_H
#include
#include
namespace DB
{
+class FileName;
+
class MD5
{
public:
MD5();
explicit MD5(const QString& md5str);
bool isNull() const;
MD5& operator=(const QString& md5str);
/** Get hex string representation of this.
* If this->isNull(), returns null string.
*/
QString toHexString() const;
bool operator==(const MD5 &other) const;
bool operator!=(const MD5& other) const;
bool operator<(const MD5& other) const;
private:
bool m_isNull;
qulonglong m_v0;
qulonglong m_v1;
};
+
+DB::MD5 MD5Sum( const DB::FileName& fileName );
+
}
#endif /* DB_MD5_H */
// vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/DB/NewImageFinder.cpp b/DB/NewImageFinder.cpp
index f872f722..2a4289d0 100644
--- a/DB/NewImageFinder.cpp
+++ b/DB/NewImageFinder.cpp
@@ -1,740 +1,740 @@
/* 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 = Utilities::MD5Sum( newFileName );
+ 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;
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;
return dirty;
}
}
- MD5 md5 = Utilities::MD5Sum( fileName );
+ MD5 md5 = MD5Sum( fileName );
if (md5.isNull()) {
cantRead << fileName;
continue;
}
ImageInfoPtr info = ImageDB::instance()->info(fileName);
if ( info->MD5Sum() != md5 ) {
info->setMD5Sum( md5 );
dirty = true;
ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName);
}
md5Map->insert( md5, fileName );
++count;
}
if ( wasCanceled )
*wasCanceled = false;
if ( !cantRead.empty() )
KMessageBox::informationList( nullptr, i18n("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/ImportExport/ImportHandler.cpp b/ImportExport/ImportHandler.cpp
index 575674dc..49a68451 100644
--- a/ImportExport/ImportHandler.cpp
+++ b/ImportExport/ImportHandler.cpp
@@ -1,357 +1,357 @@
/* 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 "ImportHandler.h"
#include "Logging.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
-#include "Utilities/Util.h"
#include "KimFileReader.h"
#include "ImportSettings.h"
#include "MainWindow/Window.h"
#include "DB/ImageDB.h"
#include "Browser/BrowserWidget.h"
+#include "DB/MD5.h"
#include "DB/MD5Map.h"
#include "DB/Category.h"
#include "DB/CategoryCollection.h"
#include "Utilities/UniqFilenameMapper.h"
#include "kio/job.h"
#include
using namespace ImportExport;
ImportExport::ImportHandler::ImportHandler()
: m_fileMapper(nullptr), m_finishedPressed(false), m_progress(0), m_reportUnreadableFiles( true )
, m_eventLoop( new QEventLoop )
{
}
ImportHandler::~ImportHandler() {
delete m_fileMapper;
delete m_eventLoop;
}
bool ImportExport::ImportHandler::exec( const ImportSettings& settings, KimFileReader* kimFileReader )
{
m_settings = settings;
m_kimFileReader = kimFileReader;
m_finishedPressed = true;
delete m_fileMapper;
m_fileMapper = new Utilities::UniqFilenameMapper(m_settings.destination());
bool ok;
// copy images
if ( m_settings.externalSource() ) {
copyFromExternal();
// If none of the images were to be copied, then we flushed the loop before we got started, in that case, don't start the loop.
qCDebug(ImportExportLog) << "Copying" << m_pendingCopies.count() << "files from external source...";
if ( m_pendingCopies.count() > 0 )
ok = m_eventLoop->exec();
else
ok = false;
}
else {
ok = copyFilesFromZipFile();
if ( ok )
updateDB();
}
if ( m_progress )
delete m_progress;
return ok;
}
void ImportExport::ImportHandler::copyFromExternal()
{
m_pendingCopies = m_settings.selectedImages();
m_totalCopied = 0;
m_progress = new QProgressDialog( MainWindow::Window::theMainWindow());
m_progress->setWindowTitle(i18nc("@title:window", "Copying Images") );
m_progress->setMinimum( 0 );
m_progress->setMaximum( 2 * m_pendingCopies.count() );
m_progress->show();
connect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages);
copyNextFromExternal();
}
void ImportExport::ImportHandler::copyNextFromExternal()
{
DB::ImageInfoPtr info = m_pendingCopies[0];
if ( isImageAlreadyInDB( info ) ) {
qCDebug(ImportExportLog) << info->fileName().relative() << "is already in database.";
aCopyJobCompleted(0);
return;
}
const DB::FileName fileName = info->fileName();
bool succeeded = false;
QStringList tried;
// First search for images next to the .kim file
// Second search for images base on the image root as specified in the .kim file
QList searchUrls {
m_settings.kimFile().adjusted(QUrl::RemoveFilename)
, m_settings.baseURL().adjusted(QUrl::RemoveFilename)
};
Q_FOREACH(const QUrl& url, searchUrls)
{
QUrl src (url);
src.setPath(src.path() + fileName.relative() );
std::unique_ptr statJob { KIO::stat(src, KIO::StatJob::SourceSide, 0 /* just query for existence */ ) };
KJobWidgets::setWindow(statJob.get(), MainWindow::Window::theMainWindow());
if ( statJob->exec() )
{
QUrl dest = QUrl::fromLocalFile( m_fileMapper->uniqNameFor(fileName) );
m_job = KIO::file_copy( src, dest, -1, KIO::HideProgressInfo );
connect(m_job, &KIO::FileCopyJob::result, this, &ImportHandler::aCopyJobCompleted);
succeeded = true;
qCDebug(ImportExportLog) << "Copying" << src << "to" << dest;
break;
} else
tried << src.toDisplayString();
}
if (!succeeded)
aCopyFailed( tried );
}
bool ImportExport::ImportHandler::copyFilesFromZipFile()
{
DB::ImageInfoList images = m_settings.selectedImages();
m_totalCopied = 0;
m_progress = new QProgressDialog( MainWindow::Window::theMainWindow());
m_progress->setWindowTitle(i18nc("@title:window", "Copying Images") );
m_progress->setMinimum( 0 );
m_progress->setMaximum( 2 * m_pendingCopies.count() );
m_progress->show();
for( DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it ) {
if ( !isImageAlreadyInDB( *it ) ) {
const DB::FileName fileName = (*it)->fileName();
QByteArray data = m_kimFileReader->loadImage( fileName.relative() );
if ( data.isNull() )
return false;
QString newName = m_fileMapper->uniqNameFor(fileName);
QFile out( newName );
if ( !out.open( QIODevice::WriteOnly ) ) {
KMessageBox::error( MainWindow::Window::theMainWindow(), i18n("Error when writing image %1", newName ) );
return false;
}
out.write( data.constData(), data.size() );
out.close();
}
qApp->processEvents();
m_progress->setValue( ++m_totalCopied );
if ( m_progress->wasCanceled() ) {
return false;
}
}
return true;
}
void ImportExport::ImportHandler::updateDB()
{
disconnect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages);
m_progress->setLabelText( i18n("Updating Database") );
int len = Settings::SettingsData::instance()->imageDirectory().length();
// image directory is always a prefix of destination
if ( len == m_settings.destination().length() )
len = 0;
else
qCDebug(ImportExportLog)
<< "Re-rooting of ImageInfos from " << Settings::SettingsData::instance()->imageDirectory()
<< " to " << m_settings.destination();
// Run though all images
DB::ImageInfoList images = m_settings.selectedImages();
for( DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it ) {
DB::ImageInfoPtr info = *it;
if ( len != 0) {
// exchange prefix:
QString name = m_settings.destination() + info->fileName().absolute().mid(len);
qCDebug(ImportExportLog) << info->fileName().absolute() << " -> " << name;
info->setFileName( DB::FileName::fromAbsolutePath(name) );
}
if ( isImageAlreadyInDB( info ) ) {
qCDebug(ImportExportLog) << "Updating ImageInfo for " << info->fileName().absolute();
updateInfo( matchingInfoFromDB( info ), info );
} else {
qCDebug(ImportExportLog) << "Adding ImageInfo for " << info->fileName().absolute();
addNewRecord( info );
}
m_progress->setValue( ++m_totalCopied );
if ( m_progress->wasCanceled() )
break;
}
Browser::BrowserWidget::instance()->home();
}
void ImportExport::ImportHandler::stopCopyingImages()
{
m_job->kill();
}
void ImportExport::ImportHandler::aCopyFailed( QStringList files )
{
int result = m_reportUnreadableFiles ?
KMessageBox::warningYesNoCancelList( m_progress,
i18n("Cannot copy from any of the following locations:"),
files, QString(), KStandardGuiItem::cont(), KGuiItem( i18n("Continue without Asking") )) : KMessageBox::Yes;
switch (result) {
case KMessageBox::Cancel:
// This might be late -- if we managed to copy some files, we will
// just throw away any changes to the DB, but some new image files
// might be in the image directory...
m_eventLoop->exit(false);
m_pendingCopies.pop_front();
break;
case KMessageBox::No:
m_reportUnreadableFiles = false;
// fall through
default:
aCopyJobCompleted( 0 );
}
}
void ImportExport::ImportHandler::aCopyJobCompleted( KJob* job )
{
qCDebug(ImportExportLog) << "CopyJob" << job << "completed.";
m_pendingCopies.pop_front();
if ( job && job->error() ) {
job->uiDelegate()->showErrorMessage();
m_eventLoop->exit(false);
}
else if ( m_pendingCopies.count() == 0 ) {
updateDB();
m_eventLoop->exit(true);
}
else if ( m_progress->wasCanceled() ) {
m_eventLoop->exit(false);
}
else {
m_progress->setValue( ++m_totalCopied );
copyNextFromExternal();
}
}
bool ImportExport::ImportHandler::isImageAlreadyInDB( const DB::ImageInfoPtr& info )
{
return DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum());
}
DB::ImageInfoPtr ImportExport::ImportHandler::matchingInfoFromDB( const DB::ImageInfoPtr& info )
{
const DB::FileName name = DB::ImageDB::instance()->md5Map()->lookup(info->MD5Sum());
return DB::ImageDB::instance()->info(name);
}
/**
* Merge the ImageInfo data from the kim file into the existing ImageInfo.
*/
void ImportExport::ImportHandler::updateInfo( DB::ImageInfoPtr dbInfo, DB::ImageInfoPtr newInfo )
{
if ( dbInfo->label() != newInfo->label() && m_settings.importAction(QString::fromLatin1("*Label*")) == ImportSettings::Replace )
dbInfo->setLabel( newInfo->label() );
if ( dbInfo->description().simplified() != newInfo->description().simplified() ) {
if ( m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Replace )
dbInfo->setDescription( newInfo->description() );
else if ( m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Merge )
dbInfo->setDescription( dbInfo->description() + QString::fromLatin1("
") + newInfo->description() );
}
if (dbInfo->angle() != newInfo->angle() && m_settings.importAction(QString::fromLatin1("*Orientation*")) == ImportSettings::Replace )
dbInfo->setAngle( newInfo->angle() );
if (dbInfo->date() != newInfo->date() && m_settings.importAction(QString::fromLatin1("*Date*")) == ImportSettings::Replace )
dbInfo->setDate( newInfo->date() );
updateCategories( newInfo, dbInfo, false );
}
void ImportExport::ImportHandler::addNewRecord( DB::ImageInfoPtr info )
{
const DB::FileName importName = info->fileName();
DB::ImageInfoPtr updateInfo(new DB::ImageInfo(importName, info->mediaType(), false /*don't read exif */));
updateInfo->setLabel( info->label() );
updateInfo->setDescription( info->description() );
updateInfo->setDate( info->date() );
updateInfo->setAngle( info->angle() );
- updateInfo->setMD5Sum( Utilities::MD5Sum( updateInfo->fileName() ) );
+ updateInfo->setMD5Sum( DB::MD5Sum( updateInfo->fileName() ) );
DB::ImageInfoList list;
list.append(updateInfo);
DB::ImageDB::instance()->addImages( list );
updateCategories( info, updateInfo, true );
}
void ImportExport::ImportHandler::updateCategories( DB::ImageInfoPtr XMLInfo, DB::ImageInfoPtr DBInfo, bool forceReplace )
{
// Run though the categories
const QList matches = m_settings.categoryMatchSetting();
for ( const CategoryMatchSetting& match : matches ) {
QString XMLCategoryName = match.XMLCategoryName();
QString DBCategoryName = match.DBCategoryName();
ImportSettings::ImportAction action = m_settings.importAction(DBCategoryName);
const Utilities::StringSet items = XMLInfo->itemsOfCategory(XMLCategoryName);
DB::CategoryPtr DBCategoryPtr = DB::ImageDB::instance()->categoryCollection()->categoryForName( DBCategoryName );
if ( !forceReplace && action == ImportSettings::Replace )
DBInfo->setCategoryInfo( DBCategoryName, Utilities::StringSet() );
if ( action == ImportSettings::Merge || action == ImportSettings::Replace || forceReplace ) {
for ( const QString& item : items ) {
if (match.XMLtoDB().contains( item ) ) {
DBInfo->addCategoryInfo( DBCategoryName, match.XMLtoDB()[item] );
DBCategoryPtr->addItem( match.XMLtoDB()[item] );
}
}
}
}
}
// vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/MainWindow/Window.cpp b/MainWindow/Window.cpp
index 9853b5fa..9fa80337 100644
--- a/MainWindow/Window.cpp
+++ b/MainWindow/Window.cpp
@@ -1,1987 +1,1987 @@
/* 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 "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.";
createSarchBar();
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 = Utilities::MD5Sum( fileName );
+ 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() );
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->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 );
}
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();
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();
createSarchBar();
}
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 = Utilities::MD5Sum( fileName );
+ 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::createSarchBar()
{
// 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:
diff --git a/Utilities/Util.cpp b/Utilities/Util.cpp
index 50461ad9..796a7a77 100644
--- a/Utilities/Util.cpp
+++ b/Utilities/Util.cpp
@@ -1,671 +1,644 @@
/* 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 "Util.h"
#include "Logging.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
extern "C" {
#include
}
-namespace {
-// Determined experimentally to yield best results (on Seagate 2TB 2.5" disk,
-// 5400 RPM). Performance is very similar at 524288. Above that, performance
-// was significantly worse. Below that, performance also deteriorated.
-// This assumes use of one image scout thread (see DB/ImageScout.cpp). Without
-// a scout thread, performance was about 10-15% worse.
-constexpr int MD5_BUFFER_SIZE = 262144;
-}
-
/**
* Add a line label + info text to the result text if info is not empty.
* If the result already contains something, a HTML newline is added first.
* To be used in createInfoText().
*/
static void AddNonEmptyInfo(const QString &label, const QString &info,
QString *result) {
if (info.isEmpty())
return;
if (!result->isEmpty())
*result += QString::fromLatin1("
");
result->append(label).append(info);
}
/**
* Given an ImageInfoPtr this function will create an HTML blob about the
* image. The blob is used in the viewer and in the tool tip box from the
* thumbnail view.
*
* As the HTML text is created, the parameter linkMap is filled with
* information about hyperlinks. The map maps from an index to a pair of
* (categoryName, categoryItem). This linkMap is used when the user selects
* one of the hyberlinks.
*/
QString Utilities::createInfoText( DB::ImageInfoPtr info, QMap< int,QPair >* linkMap )
{
Q_ASSERT( info );
QString result;
if ( Settings::SettingsData::instance()->showFilename() ) {
AddNonEmptyInfo(i18n("File Name: "), info->fileName().relative(), &result);
}
if ( Settings::SettingsData::instance()->showDate() ) {
AddNonEmptyInfo(i18n("Date: "), info->date().toString( Settings::SettingsData::instance()->showTime() ? true : false ),
&result);
}
/* XXX */
if ( Settings::SettingsData::instance()->showImageSize() && info->mediaType() == DB::Image) {
const QSize imageSize = info->size();
// Do not add -1 x -1 text
if (imageSize.width() >= 0 && imageSize.height() >= 0) {
const double megapix = imageSize.width() * imageSize.height() / 1000000.0;
QString info = i18nc("width x height","%1x%2"
,QString::number(imageSize.width())
,QString::number(imageSize.height()));
if (megapix > 0.05) {
info += i18nc("short for: x megapixels"," (%1MP)"
,QString::number(megapix, 'f', 1));
}
const double aspect = (double) imageSize.width() / (double) imageSize.height();
if (aspect > 1)
info += i18nc("aspect ratio"," (%1:1)"
,QLocale::system().toString(aspect, 'f', 2));
else if (aspect >= 0.995 && aspect < 1.005)
info += i18nc("aspect ratio"," (1:1)");
else
info += i18nc("aspect ratio"," (1:%1)"
,QLocale::system().toString(1.0/aspect, 'f', 2));
AddNonEmptyInfo(i18n("Image Size: "), info, &result);
}
}
if ( Settings::SettingsData::instance()->showRating() ) {
if ( info->rating() != -1 ) {
if ( ! result.isEmpty() )
result += QString::fromLatin1("
");
QUrl rating;
rating.setScheme(QString::fromLatin1("kratingwidget"));
// we don't use the host part, but if we don't set it, we can't use port:
rating.setHost(QString::fromLatin1("int"));
rating.setPort(qMin( qMax( static_cast(0), info->rating() ), static_cast(10)));
result += QString::fromLatin1("").arg( rating.toString(QUrl::None) );
}
}
QList categories = DB::ImageDB::instance()->categoryCollection()->categories();
int link = 0;
Q_FOREACH( const DB::CategoryPtr category, categories ) {
const QString categoryName = category->name();
if ( category->doShow() ) {
StringSet items = info->itemsOfCategory( categoryName );
if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()
&& ! Settings::SettingsData::instance()->untaggedImagesTagVisible()) {
if (categoryName == Settings::SettingsData::instance()->untaggedCategory()) {
if (items.contains(Settings::SettingsData::instance()->untaggedTag())) {
items.remove(Settings::SettingsData::instance()->untaggedTag());
}
}
}
if (!items.empty()) {
QString title = QString::fromUtf8("%1: ").arg(category->name());
QString infoText;
bool first = true;
Q_FOREACH( const QString &item, items) {
if ( first )
first = false;
else
infoText += QString::fromLatin1( ", " );
if ( linkMap ) {
++link;
(*linkMap)[link] = QPair( categoryName, item );
infoText += QString::fromLatin1( "%2").arg( link ).arg( item );
infoText += formatAge(category, item, info);
}
else
infoText += item;
}
AddNonEmptyInfo(title, infoText, &result);
}
}
}
if ( Settings::SettingsData::instance()->showLabel()) {
AddNonEmptyInfo(i18n("Label: "), info->label(), &result);
}
if ( Settings::SettingsData::instance()->showDescription() && !info->description().trimmed().isEmpty() ) {
AddNonEmptyInfo(i18n("Description: "), info->description(),
&result);
}
QString exifText;
if ( Settings::SettingsData::instance()->showEXIF() ) {
typedef QMap ExifMap;
typedef ExifMap::const_iterator ExifMapIterator;
ExifMap exifMap = Exif::Info::instance()->infoForViewer( info->fileName(), Settings::SettingsData::instance()->iptcCharset() );
for( ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt ) {
if ( exifIt.key().startsWith( QString::fromLatin1( "Exif." ) ) )
for ( QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt ) {
QString exifName = exifIt.key().split( QChar::fromLatin1('.') ).last();
AddNonEmptyInfo(QString::fromLatin1( "%1: ").arg(exifName),
*valuesIt, &exifText);
}
}
QString iptcText;
for( ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt ) {
if ( !exifIt.key().startsWith( QString::fromLatin1( "Exif." ) ) )
for ( QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt ) {
QString iptcName = exifIt.key().split( QChar::fromLatin1('.') ).last();
AddNonEmptyInfo(QString::fromLatin1( "%1: ").arg(iptcName),
*valuesIt, &iptcText);
}
}
if ( !iptcText.isEmpty() ) {
if ( exifText.isEmpty() )
exifText = iptcText;
else
exifText += QString::fromLatin1( "
" ) + iptcText;
}
}
if ( !result.isEmpty() && !exifText.isEmpty() )
result += QString::fromLatin1( "
" );
result += exifText;
return result;
}
using DateSpec = QPair;
DateSpec dateDiff(const QDate& birthDate, const QDate& imageDate)
{
const int bday = birthDate.day();
const int iday = imageDate.day();
const int bmonth = birthDate.month();
const int imonth = imageDate.month();
const int byear = birthDate.year();
const int iyear = imageDate.year();
// Image before birth
const int diff = birthDate.daysTo(imageDate);
if (diff < 0)
return qMakePair(0, 'I');
if (diff < 31)
return qMakePair(diff, 'D');
int months = (iyear-byear)*12;
months += (imonth-bmonth);
months += (iday >= bday) ? 0 : -1;
if ( months < 24)
return qMakePair(months, 'M');
else
return qMakePair(months/12, 'Y');
}
QString formatDate(const DateSpec& date)
{
if (date.second == 'I')
return {};
else if (date.second == 'D')
return i18np("1 day", "%1 days", date.first);
else if (date.second == 'M')
return i18np("1 month", "%1 months", date.first);
else
return i18np("1 year", "%1 years", date.first);
}
void test() {
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,7,11))) == QString::fromLatin1("0 days"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,8,10))) == QString::fromLatin1("30 days"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,8,11))) == QString::fromLatin1("1 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,8,12))) == QString::fromLatin1("1 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,9,10))) == QString::fromLatin1("1 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,9,11))) == QString::fromLatin1("2 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,6,10))) == QString::fromLatin1("10 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,6,11))) == QString::fromLatin1("11 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,6,12))) == QString::fromLatin1("11 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,7,10))) == QString::fromLatin1("11 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,7,11))) == QString::fromLatin1("12 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,7,12))) == QString::fromLatin1("12 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,12,11))) == QString::fromLatin1("17 month"));
Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1973,7,11))) == QString::fromLatin1("2 years"));
}
QString Utilities::formatAge(DB::CategoryPtr category, const QString &item, DB::ImageInfoPtr info)
{
// test(); // I wish I could get my act together to set up a test suite.
const QDate birthDate = category->birthDate(item);
const QDate start = info->date().start().date();
const QDate end = info->date().end().date();
if (birthDate.isNull() || start.isNull())
return {};
if ( start == end)
return QString::fromUtf8(" (%1)").arg(formatDate(dateDiff(birthDate, start)));
else {
DateSpec lower = dateDiff(birthDate,start);
DateSpec upper = dateDiff(birthDate,end);
if (lower == upper)
return QString::fromUtf8(" (%1)").arg(formatDate(lower));
else if (lower.second == 'I')
return QString::fromUtf8(" (< %1)").arg(formatDate(upper));
else {
if (lower.second == upper.second)
return QString::fromUtf8(" (%1-%2)").arg(lower.first).arg(formatDate(upper));
else
return QString::fromUtf8(" (%1-%2)").arg(formatDate(lower)).arg(formatDate(upper));
}
}
}
void Utilities::checkForBackupFile( const QString& fileName, const QString& message )
{
QString backupName = QFileInfo( fileName ).absolutePath() + QString::fromLatin1("/.#") + QFileInfo( fileName ).fileName();
QFileInfo backUpFile( backupName);
QFileInfo indexFile( fileName );
if ( !backUpFile.exists() || indexFile.lastModified() > backUpFile.lastModified() || backUpFile.size() == 0 )
if ( !( backUpFile.exists() && !message.isNull() ) )
return;
int code;
if ( message.isNull() )
code = KMessageBox::questionYesNo( nullptr, i18n("Autosave file '%1' exists (size %3 KB) and is newer than '%2'. "
"Should the autosave file be used?", backupName, fileName, backUpFile.size() >> 10 ),
i18n("Found Autosave File") );
else if ( backUpFile.size() > 0 )
code = KMessageBox::warningYesNo( nullptr,i18n( "Error: Cannot use current database file '%1':
%2
"
"Do you want to use autosave (%3 - size %4 KB) instead of exiting?
"
"(Manually verifying and copying the file might be a good idea)
", fileName, message, backupName, backUpFile.size() >> 10 ),
i18n("Recover from Autosave?") );
else {
KMessageBox::error( nullptr, i18n( "Error: %1
Also autosave file is empty, check manually "
"if numbered backup files exist and can be used to restore index.xml.
", message ) );
exit(-1);
}
if ( code == KMessageBox::Yes ) {
QFile in( backupName );
if ( in.open( QIODevice::ReadOnly ) ) {
QFile out( fileName );
if (out.open( QIODevice::WriteOnly ) ) {
char data[1024];
int len;
while ( (len = in.read( data, 1024 ) ) )
out.write( data, len );
}
}
} else if ( !message.isNull() )
exit(-1);
}
bool Utilities::ctrlKeyDown()
{
return QApplication::keyboardModifiers() & Qt::ControlModifier;
}
void Utilities::copyList( const QStringList& from, const QString& directoryTo )
{
for( QStringList::ConstIterator it = from.constBegin(); it != from.constEnd(); ++it ) {
QString destFile = directoryTo + QString::fromLatin1( "/" ) + QFileInfo(*it).fileName();
if ( ! QFileInfo( destFile ).exists() ) {
const bool ok = copy( *it, destFile );
if ( !ok ) {
KMessageBox::error( nullptr, i18n("Unable to copy '%1' to '%2'.", *it , destFile ), i18n("Error Running Demo") );
exit(-1);
}
}
}
}
QString Utilities::setupDemo()
{
QString demoDir = QString::fromLatin1( "%1/kphotoalbum-demo-%2" ).arg(QDir::tempPath()).arg(QString::fromLocal8Bit( qgetenv( "LOGNAME" ) ));
QFileInfo fi(demoDir);
if ( ! fi.exists() ) {
bool ok = QDir().mkdir( demoDir );
if ( !ok ) {
KMessageBox::error( nullptr, i18n("Unable to create directory '%1' needed for demo.", demoDir ), i18n("Error Running Demo") );
exit(-1);
}
}
// index.xml
QString demoDB = locateDataFile(QString::fromLatin1("demo/index.xml"));
if ( demoDB.isEmpty() )
{
qCDebug(UtilitiesLog) << "No demo database in standard locations:" << QStandardPaths::standardLocations(QStandardPaths::DataLocation);
exit(-1);
}
QString configFile = demoDir + QString::fromLatin1( "/index.xml" );
copy(demoDB, configFile);
// Images
const QStringList kpaDemoDirs = QStandardPaths::locateAll(
QStandardPaths::DataLocation,
QString::fromLatin1("demo"),
QStandardPaths::LocateDirectory);
QStringList images;
Q_FOREACH(const QString &dir, kpaDemoDirs)
{
QDirIterator it(dir, QStringList() << QStringLiteral("*.jpg") << QStringLiteral("*.avi"));
while (it.hasNext()) {
images.append(it.next());
}
}
copyList( images, demoDir );
// CategoryImages
QString catDir = demoDir + QString::fromLatin1("/CategoryImages");
fi = QFileInfo(catDir);
if ( ! fi.exists() ) {
bool ok = QDir().mkdir( catDir );
if ( !ok ) {
KMessageBox::error( nullptr, i18n("Unable to create directory '%1' needed for demo.", catDir ), i18n("Error Running Demo") );
exit(-1);
}
}
const QStringList kpaDemoCatDirs = QStandardPaths::locateAll(
QStandardPaths::DataLocation,
QString::fromLatin1("demo/CategoryImages"),
QStandardPaths::LocateDirectory);
QStringList catImages;
Q_FOREACH(const QString &dir, kpaDemoCatDirs)
{
QDirIterator it(dir, QStringList() << QStringLiteral("*.jpg"));
while (it.hasNext()) {
catImages.append(it.next());
}
}
copyList( catImages, catDir );
return configFile;
}
bool Utilities::copy( const QString& from, const QString& to )
{
if ( QFileInfo(to).exists())
QDir().remove(to);
return QFile::copy(from,to);
}
bool Utilities::makeHardLink( const QString& from, const QString& to )
{
if (link(from.toLocal8Bit().constData(), to.toLocal8Bit().constData()) != 0)
return false;
else
return true;
}
bool Utilities::makeSymbolicLink( const QString& from, const QString& to )
{
if (symlink(from.toLocal8Bit().constData(), to.toLocal8Bit().constData()) != 0)
return false;
else
return true;
}
bool Utilities::canReadImage( const DB::FileName& fileName )
{
bool fastMode = !Settings::SettingsData::instance()->ignoreFileExtension();
QMimeDatabase::MatchMode mode = fastMode ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault;
QMimeDatabase db;
QMimeType mimeType = db.mimeTypeForFile( fileName.absolute(), mode );
return QImageReader::supportedMimeTypes().contains( mimeType.name().toUtf8() )
|| ImageManager::ImageDecoder::mightDecode( fileName );
}
QString Utilities::locateDataFile(const QString& fileName)
{
return QStandardPaths::locate(QStandardPaths::DataLocation, fileName);
}
QString Utilities::readFile( const QString& fileName )
{
if ( fileName.isEmpty() ) {
KMessageBox::error( nullptr, i18n("No file name given!
") );
return QString();
}
QFile file( fileName );
if ( !file.open( QIODevice::ReadOnly ) ) {
//KMessageBox::error( nullptr, i18n("Could not open file %1").arg( fileName ) );
return QString();
}
QTextStream stream( &file );
QString content = stream.readAll();
file.close();
return content;
}
namespace Utilities
{
QString normalizedFileName( const QString& fileName )
{
return QFileInfo(fileName).absoluteFilePath();
}
QString dereferenceSymLinks( const QString& fileName )
{
QFileInfo fi(fileName);
int rounds = 256;
while (fi.isSymLink() && --rounds > 0)
fi = QFileInfo(fi.readLink());
if (rounds == 0)
return QString();
return fi.filePath();
}
}
QString Utilities::stripEndingForwardSlash( const QString& fileName )
{
static QString slash = QString::fromLatin1("/");
if ( fileName.endsWith( slash ) )
return fileName.left( fileName.length()-1);
else
return fileName;
}
QString Utilities::relativeFolderName( const QString& fileName)
{
int index= fileName.lastIndexOf( QChar::fromLatin1('/'), -1);
if (index == -1)
return QString();
else
return fileName.left( index );
}
void Utilities::deleteDemo()
{
QString dir = QString::fromLatin1( "%1/kphotoalbum-demo-%2" ).arg(QDir::tempPath()).arg(QString::fromLocal8Bit( qgetenv( "LOGNAME" ) ) );
QUrl demoUrl = QUrl::fromLocalFile( dir );
KJob *delDemoJob = KIO::del( demoUrl );
KJobWidgets::setWindow( delDemoJob, MainWindow::Window::theMainWindow());
delDemoJob->exec();
}
QString Utilities::absoluteImageFileName( const QString& relativeName )
{
return stripEndingForwardSlash( Settings::SettingsData::instance()->imageDirectory() ) + QString::fromLatin1( "/" ) + relativeName;
}
QString Utilities::imageFileNameToAbsolute( const QString& fileName )
{
if ( fileName.startsWith( Settings::SettingsData::instance()->imageDirectory() ) )
return fileName;
else if ( fileName.startsWith( QString::fromLatin1("file://") ) )
return imageFileNameToAbsolute( fileName.mid( 7 ) ); // 7 == length("file://")
else if ( fileName.startsWith( QString::fromLatin1("/") ) )
return QString(); // Not within our image root
else
return absoluteImageFileName( fileName );
}
bool operator>( const QPoint& p1, const QPoint& p2)
{
return p1.y() > p2.y() || (p1.y() == p2.y() && p1.x() > p2.x() );
}
bool operator<( const QPoint& p1, const QPoint& p2)
{
return p1.y() < p2.y() || ( p1.y() == p2.y() && p1.x() < p2.x() );
}
const QSet& Utilities::supportedVideoExtensions()
{
static QSet videoExtensions;
if ( videoExtensions.empty() ) {
videoExtensions.insert( QString::fromLatin1( "3gp" ) );
videoExtensions.insert( QString::fromLatin1( "avi" ) );
videoExtensions.insert( QString::fromLatin1( "mp4" ) );
videoExtensions.insert( QString::fromLatin1( "m4v" ) );
videoExtensions.insert( QString::fromLatin1( "mpeg" ) );
videoExtensions.insert( QString::fromLatin1( "mpg" ) );
videoExtensions.insert( QString::fromLatin1( "qt" ) );
videoExtensions.insert( QString::fromLatin1( "mov" ) );
videoExtensions.insert( QString::fromLatin1( "moov" ) );
videoExtensions.insert( QString::fromLatin1( "qtvr" ) );
videoExtensions.insert( QString::fromLatin1( "rv" ) );
videoExtensions.insert( QString::fromLatin1( "3g2" ) );
videoExtensions.insert( QString::fromLatin1( "fli" ) );
videoExtensions.insert( QString::fromLatin1( "flc" ) );
videoExtensions.insert( QString::fromLatin1( "mkv" ) );
videoExtensions.insert( QString::fromLatin1( "mng" ) );
videoExtensions.insert( QString::fromLatin1( "asf" ) );
videoExtensions.insert( QString::fromLatin1( "asx" ) );
videoExtensions.insert( QString::fromLatin1( "wmp" ) );
videoExtensions.insert( QString::fromLatin1( "wmv" ) );
videoExtensions.insert( QString::fromLatin1( "ogm" ) );
videoExtensions.insert( QString::fromLatin1( "rm" ) );
videoExtensions.insert( QString::fromLatin1( "flv" ) );
videoExtensions.insert( QString::fromLatin1( "webm" ) );
videoExtensions.insert( QString::fromLatin1( "mts" ) );
videoExtensions.insert( QString::fromLatin1( "ogg" ) );
videoExtensions.insert( QString::fromLatin1( "ogv" ) );
videoExtensions.insert( QString::fromLatin1( "m2ts" ) );
}
return videoExtensions;
}
bool Utilities::isVideo( const DB::FileName& fileName )
{
QFileInfo fi( fileName.relative() );
QString ext = fi.suffix().toLower();
return supportedVideoExtensions().contains( ext );
}
bool Utilities::isRAW( const DB::FileName& fileName )
{
return ImageManager::RAWImageDecoder::isRAW( fileName );
}
QImage Utilities::scaleImage(const QImage &image, int w, int h, Qt::AspectRatioMode mode )
{
return image.scaled( w, h, mode, Settings::SettingsData::instance()->smoothScale() ? Qt::SmoothTransformation : Qt::FastTransformation );
}
QImage Utilities::scaleImage(const QImage &image, const QSize& s, Qt::AspectRatioMode mode )
{
return scaleImage( image, s.width(), s.height(), mode );
}
QString Utilities::cStringWithEncoding( const char *c_str, const QString& charset )
{
QTextCodec* codec = QTextCodec::codecForName( charset.toLatin1() );
if (!codec)
codec = QTextCodec::codecForLocale();
return codec->toUnicode( c_str );
}
-DB::MD5 Utilities::MD5Sum( const DB::FileName& fileName )
-{
- DB::MD5 checksum;
- QFile file( fileName.absolute() );
- if ( file.open( QIODevice::ReadOnly ) )
- {
- QCryptographicHash md5calculator(QCryptographicHash::Md5);
- while ( !file.atEnd() ) {
- QByteArray md5Buffer( file.read( MD5_BUFFER_SIZE ) );
- md5calculator.addData( md5Buffer );
- }
- file.close();
- checksum = DB::MD5(QString::fromLatin1(md5calculator.result().toHex()));
- }
- return checksum;
-}
-
QColor Utilities::contrastColor( const QColor& col )
{
if ( col.red() < 127 && col.green() < 127 && col.blue() < 127 )
return Qt::white;
else
return Qt::black;
}
void Utilities::saveImage( const DB::FileName& fileName, const QImage& image, const char* format )
{
const QFileInfo info(fileName.absolute());
QDir().mkpath(info.path());
const bool ok = image.save(fileName.absolute(),format);
Q_ASSERT(ok); Q_UNUSED(ok);
}
// vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/Utilities/Util.h b/Utilities/Util.h
index e961a08d..a683b319 100644
--- a/Utilities/Util.h
+++ b/Utilities/Util.h
@@ -1,75 +1,72 @@
/* 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.
*/
#ifndef UTIL_H
#define UTIL_H
#include "DB/CategoryPtr.h"
#include "DB/FileName.h"
#include "DB/ImageInfoPtr.h"
-#include "DB/MD5.h"
#include
#include
#include
#include
#include
namespace Utilities
{
QString createInfoText( DB::ImageInfoPtr info, QMap >* );
QString formatAge(DB::CategoryPtr category,const QString& item, DB::ImageInfoPtr info);
void checkForBackupFile( const QString& fileName, const QString& message = QString() );
bool ctrlKeyDown();
bool copy( const QString& from, const QString& to );
void copyList( const QStringList& from, const QString& directoryTo );
bool makeSymbolicLink( const QString& from, const QString& to );
bool makeHardLink( const QString& from, const QString& to );
void deleteDemo();
QString setupDemo();
bool canReadImage( const DB::FileName& fileName );
const QSet& supportedVideoExtensions();
bool isVideo( const DB::FileName& fileName );
bool isRAW( const DB::FileName& fileName );
QString locateDataFile(const QString& fileName);
QString readFile( const QString& fileName );
QString stripEndingForwardSlash( const QString& fileName );
QString absoluteImageFileName( const QString& relativeName );
QString imageFileNameToAbsolute( const QString& fileName );
QString relativeFolderName( const QString& fileName);
QImage scaleImage(const QImage &image, int w, int h, Qt::AspectRatioMode mode=Qt::IgnoreAspectRatio );
QImage scaleImage(const QImage &image, const QSize& s, Qt::AspectRatioMode mode=Qt::IgnoreAspectRatio );
QString cStringWithEncoding( const char *c_str, const QString& charset );
-DB::MD5 MD5Sum( const DB::FileName& fileName );
-
QColor contrastColor( const QColor& );
void saveImage( const DB::FileName& fileName, const QImage& image, const char* format );
}
bool operator>( const QPoint&, const QPoint& );
bool operator<( const QPoint&, const QPoint& );
#endif /* UTIL_H */
// vi:expandtab:tabstop=4 shiftwidth=4: