diff --git a/Utilities/DescriptionUtil.cpp b/Utilities/DescriptionUtil.cpp index c69d7aa2..52d68234 100644 --- a/Utilities/DescriptionUtil.cpp +++ b/Utilities/DescriptionUtil.cpp @@ -1,382 +1,399 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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. */ // Local includes #include "DescriptionUtil.h" #include "DB/CategoryCollection.h" #include "DB/ImageDB.h" #include "Exif/Info.h" #include "Logging.h" #include "Settings/SettingsData.h" // KDE includes #include // Qt includes #include #include #include namespace { const QLatin1String LINE_BREAK("
"); } /** * 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 &infoText, QString *result) { if (infoText.isEmpty()) { return; } if (!result->isEmpty()) { *result += LINE_BREAK; } result->append(label).append(infoText); } /** * 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> *linkMap) { Q_ASSERT(info); QString result; if (Settings::SettingsData::instance()->showFilename()) { AddNonEmptyInfo(i18n("File Name: "), info->fileName().relative(), &result); } if (Settings::SettingsData::instance()->showDate()) { QString dateString = info->date().toString(Settings::SettingsData::instance()->showTime() ? true : false); - if (!dateString.isEmpty()) { - dateString.append(i18n(" (%1)", timeAgo(info))); - } + dateString.append(timeAgo(info)); AddNonEmptyInfo(i18n("Date: "), dateString, &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 infoText = i18nc("width x height", "%1x%2", QString::number(imageSize.width()), QString::number(imageSize.height())); if (megapix > 0.05) { infoText += i18nc("short for: x megapixels", " (%1MP)", QString::number(megapix, 'f', 1)); } const double aspect = (double)imageSize.width() / (double)imageSize.height(); // 0.995 - 1.005 can still be considered quadratic if (aspect > 1.005) infoText += i18nc("aspect ratio", " (%1:1)", QLocale::system().toString(aspect, 'f', 2)); else if (aspect >= 0.995) infoText += i18nc("aspect ratio", " (1:1)"); else infoText += i18nc("aspect ratio", " (1:%1)", QLocale::system().toString(1.0 / aspect, 'f', 2)); AddNonEmptyInfo(i18n("Image Size: "), infoText, &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; } namespace { enum class TimeUnit { /** Denotes a negative age. */ Invalid, Days, Months, Years }; class AgeSpec { public: + /** + * @brief The I18nContext enum determines how an age is displayed. + */ + enum class I18nContext { + /// For birthdays, e.g. "Jesper was 30 years in this image". + Birthday, + /// For ages of events, e.g. "This image was taken 30 years ago". + Anniversary + }; int age; ///< The number of \c units, e.g. the "5" in "5 days" TimeUnit unit; AgeSpec(); AgeSpec(int age, TimeUnit unit); /** * @brief format + * @param context the context where the formatted age is used. * @return a localized string describing the time range. */ - QString format() const; + QString format(I18nContext context) const; /** * @brief isValid * @return \c true, if the AgeSpec contains a valid age that is not negative. \c false otherwise. */ bool isValid() const; bool operator==(const AgeSpec &other) const; }; AgeSpec::AgeSpec() : age(70) , unit(TimeUnit::Invalid) { } AgeSpec::AgeSpec(int age, TimeUnit unit) : age(age) , unit(unit) { } -QString AgeSpec::format() const +QString AgeSpec::format(I18nContext context) const { switch (unit) { case TimeUnit::Invalid: return {}; case TimeUnit::Days: - return i18np("1 day", "%1 days", age); + if (context == I18nContext::Birthday) + return i18ncp("As in 'The baby is 1 day old'", "1 day", "%1 days", age); + else + return i18ncp("As in 'This happened 1 day ago'", "1 day ago", "%1 days ago", age); case TimeUnit::Months: - return i18np("1 month", "%1 months", age); + if (context == I18nContext::Birthday) + return i18ncp("As in 'The baby is 1 month old'", "1 month", "%1 months", age); + else + return i18ncp("As in 'This happened 1 month ago'", "1 month ago", "%1 months ago", age); case TimeUnit::Years: - return i18np("1 year", "%1 years", age); + if (context == I18nContext::Birthday) + return i18ncp("As in 'The baby is 1 year old'", "1 year", "%1 years", age); + else + return i18ncp("As in 'This happened 1 year ago'", "1 year ago", "%1 years ago", age); } } bool AgeSpec::isValid() const { return unit != TimeUnit::Invalid; } bool AgeSpec::operator==(const AgeSpec &other) const { return (age == other.age && unit == other.unit); } /** * @brief dateDifference computes the difference between two dates with an appropriate unit. * It can be used to generate human readable date differences, * e.g. "6 months" instead of "0.5 years". * * @param priorDate * @param laterDate * @return a DateSpec with appropriate scale. */ AgeSpec dateDifference(const QDate &priorDate, const QDate &laterDate) { const int priorDay = priorDate.day(); const int laterDay = laterDate.day(); const int priorMonth = priorDate.month(); const int laterMonth = laterDate.month(); const int priorYear = priorDate.year(); const int laterYear = laterDate.year(); // Image before birth const int days = priorDate.daysTo(laterDate); if (days < 0) return {}; if (days < 31) return { days, TimeUnit::Days }; int months = (laterYear - priorYear) * 12; months += (laterMonth - priorMonth); months += (laterDay >= priorDay) ? 0 : -1; if (months < 24) return { months, TimeUnit::Months }; else return { months / 12, TimeUnit::Years }; } void testDateDifference() { using namespace Utilities; - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 7, 11)).format() == QString::fromLatin1("0 days")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 10)).format() == QString::fromLatin1("30 days")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 11)).format() == QString::fromLatin1("1 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 12)).format() == QString::fromLatin1("1 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 9, 10)).format() == QString::fromLatin1("1 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 9, 11)).format() == QString::fromLatin1("2 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 10)).format() == QString::fromLatin1("10 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 11)).format() == QString::fromLatin1("11 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 12)).format() == QString::fromLatin1("11 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 10)).format() == QString::fromLatin1("11 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 11)).format() == QString::fromLatin1("12 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 12)).format() == QString::fromLatin1("12 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 12, 11)).format() == QString::fromLatin1("17 month")); - Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1973, 7, 11)).format() == QString::fromLatin1("2 years")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 7, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("0 days")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 10)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("30 days")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("1 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 12)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("1 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 9, 10)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("1 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 9, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("2 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 10)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("10 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("11 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 12)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("11 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 10)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("11 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("12 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 12)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("12 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 12, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("17 month")); + Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1973, 7, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("2 years")); qDebug() << "Tested dateDifference without problems."; } } QString Utilities::formatAge(DB::CategoryPtr category, const QString &item, DB::ImageInfoPtr info) { testDateDifference(); // 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(dateDifference(birthDate, start).format()); + return QString::fromUtf8(" (%1)").arg(dateDifference(birthDate, start).format(AgeSpec::I18nContext::Birthday)); else { const AgeSpec lower = dateDifference(birthDate, start); const AgeSpec upper = dateDifference(birthDate, end); if (lower == upper) - return QString::fromUtf8(" (%1)").arg(lower.format()); + return QString::fromUtf8(" (%1)").arg(lower.format(AgeSpec::I18nContext::Birthday)); else if (!lower.isValid()) - return QString::fromUtf8(" (< %1)").arg(upper.format()); + return QString::fromUtf8(" (< %1)").arg(upper.format(AgeSpec::I18nContext::Birthday)); else { if (lower.unit == upper.unit) - return QString::fromUtf8(" (%1-%2)").arg(lower.age).arg(upper.format()); + return QString::fromUtf8(" (%1-%2)").arg(lower.age).arg(upper.format(AgeSpec::I18nContext::Birthday)); else - return QString::fromUtf8(" (%1-%2)").arg(lower.format()).arg(upper.format()); + return QString::fromUtf8(" (%1-%2)").arg(lower.format(AgeSpec::I18nContext::Birthday)).arg(upper.format(AgeSpec::I18nContext::Birthday)); } } } QString Utilities::timeAgo(const DB::ImageInfoPtr info) { const QDate startDate = info->date().start().date(); const QDate endDate = info->date().end().date(); const QDate today = QDate::currentDate(); if (startDate == endDate) { - return i18n("%1 ago", dateDifference(startDate, today).format()); + return i18n(" (%1)", dateDifference(startDate, today).format(AgeSpec::I18nContext::Anniversary)); } else { const AgeSpec minTimeAgo = dateDifference(startDate, today); const AgeSpec maxTimeAgo = dateDifference(endDate, today); if (!minTimeAgo.isValid()) { // startDate is in the future return QString(); } if (minTimeAgo == maxTimeAgo) { - return i18n("%1 ago", minTimeAgo.format()); + return i18n(" (%1)", minTimeAgo.format(AgeSpec::I18nContext::Anniversary)); } else { if (minTimeAgo.unit == maxTimeAgo.unit) - return i18n("%1-%2 ago", maxTimeAgo.age, minTimeAgo.format()); + return i18n(" (%1-%2)", maxTimeAgo.age, minTimeAgo.format(AgeSpec::I18nContext::Anniversary)); else - return i18n("%1-%2 ago", maxTimeAgo.format(), minTimeAgo.format()); + return i18n(" (%1-%2)", maxTimeAgo.format(AgeSpec::I18nContext::Anniversary), minTimeAgo.format(AgeSpec::I18nContext::Anniversary)); } } }