diff --git a/AnnotationDialog/DateEdit.cpp b/AnnotationDialog/DateEdit.cpp index e15113b8..8ff75308 100644 --- a/AnnotationDialog/DateEdit.cpp +++ b/AnnotationDialog/DateEdit.cpp @@ -1,350 +1,362 @@ /* 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. */ /** * A date editing widget that consists of an editable combo box. * The combo box contains the date in text form, and clicking the combo * box arrow will display a 'popup' style date picker. * * This widget also supports advanced features like allowing the user * to type in the day name to get the date. The following keywords * are supported (in the native language): tomorrow, yesterday, today, * monday, tuesday, wednesday, thursday, friday, saturday, sunday. * * @author Cornelius Schumacher * @author Mike Pilone * @author David Jarvie * @author Jesper Pedersen */ #include "DateEdit.h" #include #include #include #include #include #include #include #include #include #include AnnotationDialog::DateEdit::DateEdit(bool isStartEdit, QWidget *parent) : QComboBox(parent) , m_defaultValue(QDate::currentDate()) , m_ReadOnly(false) , m_DiscardNextMousePress(false) , m_IsStartEdit(isStartEdit) { setEditable(true); setMaxCount(1); // need at least one entry for popup to work m_value = m_defaultValue; QString today = QDate::currentDate().toString(QString::fromLatin1("dd. MMM yyyy")); addItem(QString::fromLatin1("")); setCurrentIndex(0); setItemText(0, QString::fromLatin1("")); setMinimumSize(sizeHint()); m_DateFrame = new QFrame; m_DateFrame->setWindowFlags(Qt::Popup); QVBoxLayout *layout = new QVBoxLayout(m_DateFrame); m_DateFrame->setFrameStyle(QFrame::StyledPanel | QFrame::Raised); m_DateFrame->setLineWidth(3); m_DateFrame->hide(); m_DateFrame->installEventFilter(this); m_DatePicker = new KDatePicker(m_value, m_DateFrame); layout->addWidget(m_DatePicker); connect(lineEdit(), &QLineEdit::editingFinished, this, &DateEdit::lineEnterPressed); connect(this, &QComboBox::currentTextChanged, this, &DateEdit::slotTextChanged); connect(m_DatePicker, &KDatePicker::dateEntered, this, &DateEdit::dateEntered); connect(m_DatePicker, &KDatePicker::dateSelected, this, &DateEdit::dateSelected); // Create the keyword list. This will be used to match against when the user // enters information. m_KeywordMap[i18n("tomorrow")] = 1; m_KeywordMap[i18n("today")] = 0; m_KeywordMap[i18n("yesterday")] = -1; QString dayName; for (int i = 1; i <= 7; ++i) { dayName = QLocale().dayName(i, QLocale::LongFormat).toLower(); m_KeywordMap[dayName] = i + 100; } lineEdit()->installEventFilter(this); // handle keyword entry m_TextChanged = false; m_HandleInvalid = false; } AnnotationDialog::DateEdit::~DateEdit() { } void AnnotationDialog::DateEdit::setDate(const QDate &newDate) { QString dateString = QString::fromLatin1(""); if (newDate.isValid()) dateString = DB::ImageDate(newDate).toString(false); m_TextChanged = false; // We do not want to generate a signal here, since we explicitly setting // the date bool b = signalsBlocked(); blockSignals(true); setItemText(0, dateString); blockSignals(b); m_value = newDate; } void AnnotationDialog::DateEdit::setHandleInvalid(bool handleInvalid) { m_HandleInvalid = handleInvalid; } bool AnnotationDialog::DateEdit::handlesInvalid() const { return m_HandleInvalid; } void AnnotationDialog::DateEdit::setReadOnly(bool readOnly) { m_ReadOnly = readOnly; lineEdit()->setReadOnly(readOnly); } bool AnnotationDialog::DateEdit::isReadOnly() const { return m_ReadOnly; } bool AnnotationDialog::DateEdit::validate(const QDate &) { return true; } QDate AnnotationDialog::DateEdit::date() const { QDate dt; readDate(dt, 0); return dt; } QDate AnnotationDialog::DateEdit::defaultDate() const { return m_defaultValue; } void AnnotationDialog::DateEdit::setDefaultDate(const QDate &date) { m_defaultValue = date; } void AnnotationDialog::DateEdit::showPopup() { if (m_ReadOnly) return; QRect desk = QApplication::desktop()->availableGeometry(this); // ensure that the popup is fully visible even when the DateEdit is off-screen QPoint popupPoint = mapToGlobal(QPoint(0, 0)); if (popupPoint.x() < desk.left()) { popupPoint.setX(desk.x()); } else if (popupPoint.x() + width() > desk.right()) { popupPoint.setX(desk.right() - width()); } int dateFrameHeight = m_DateFrame->sizeHint().height(); if (popupPoint.y() + height() + dateFrameHeight > desk.bottom()) { popupPoint.setY(popupPoint.y() - dateFrameHeight); } else { popupPoint.setY(popupPoint.y() + height()); } m_DateFrame->move(popupPoint); QDate date; readDate(date, 0); if (date.isValid()) { m_DatePicker->setDate(date); } else { m_DatePicker->setDate(m_defaultValue); } m_DateFrame->show(); } void AnnotationDialog::DateEdit::dateSelected(QDate newDate) { if ((m_HandleInvalid || newDate.isValid()) && validate(newDate)) { setDate(newDate); emit dateChanged(newDate); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + emit dateChanged(DB::ImageDate(newDate.startOfDay(), newDate.startOfDay())); +#else emit dateChanged(DB::ImageDate(QDateTime(newDate), QDateTime(newDate))); +#endif m_DateFrame->hide(); } } void AnnotationDialog::DateEdit::dateEntered(QDate newDate) { if ((m_HandleInvalid || newDate.isValid()) && validate(newDate)) { setDate(newDate); emit dateChanged(newDate); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + emit dateChanged(DB::ImageDate(newDate.startOfDay(), newDate.startOfDay())); +#else emit dateChanged(DB::ImageDate(QDateTime(newDate), QDateTime(newDate))); +#endif } } void AnnotationDialog::DateEdit::lineEnterPressed() { if (!m_TextChanged) return; QDate date; QDate end; if (readDate(date, &end) && (m_HandleInvalid || date.isValid()) && validate(date)) { // Update the edit. This is needed if the user has entered a // word rather than the actual date. setDate(date); emit(dateChanged(date)); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + emit dateChanged(DB::ImageDate(date.startOfDay(), end.startOfDay())); +#else emit dateChanged(DB::ImageDate(QDateTime(date), QDateTime(end))); +#endif } else { // Invalid or unacceptable date - revert to previous value setDate(m_value); emit invalidDateEntered(); } } bool AnnotationDialog::DateEdit::inputIsValid() const { QDate date; return readDate(date, 0) && date.isValid(); } /* Reads the text from the line edit. If the text is a keyword, the * word will be translated to a date. If the text is not a keyword, the * text will be interpreted as a date. * Returns true if the date text is blank or valid, false otherwise. */ bool AnnotationDialog::DateEdit::readDate(QDate &result, QDate *end) const { QString text = currentText(); if (text.isEmpty()) { result = QDate(); } else if (m_KeywordMap.contains(text.toLower())) { QDate today = QDate::currentDate(); int i = m_KeywordMap[text.toLower()]; if (i >= 100) { /* A day name has been entered. Convert to offset from today. * This uses some math tricks to figure out the offset in days * to the next date the given day of the week occurs. There * are two cases, that the new day is >= the current day, which means * the new day has not occurred yet or that the new day < the current day, * which means the new day is already passed (so we need to find the * day in the next week). */ i -= 100; int currentDay = today.dayOfWeek(); if (i >= currentDay) i -= currentDay; else i += 7 - currentDay; } result = today.addDays(i); } else { result = DB::ImageDate::parseDate(text, m_IsStartEdit); if (end) *end = DB::ImageDate::parseDate(text, false); return result.isValid(); } return true; } void AnnotationDialog::DateEdit::keyPressEvent(QKeyEvent *event) { int step = 0; if (event->key() == Qt::Key_Up) step = 1; else if (event->key() == Qt::Key_Down) step = -1; setDate(m_value.addDays(step)); QComboBox::keyPressEvent(event); } /* Checks for a focus out event. The display of the date is updated * to display the proper date when the focus leaves. */ bool AnnotationDialog::DateEdit::eventFilter(QObject *obj, QEvent *e) { if (obj == lineEdit()) { if (e->type() == QEvent::Wheel) { // Up and down arrow keys step the date QWheelEvent *we = dynamic_cast(e); Q_ASSERT(we != nullptr); int step = 0; step = we->delta() > 0 ? 1 : -1; if (we->orientation() == Qt::Vertical) { setDate(m_value.addDays(step)); } } } else { // It's a date picker event switch (e->type()) { case QEvent::MouseButtonDblClick: case QEvent::MouseButtonPress: { QMouseEvent *me = (QMouseEvent *)e; if (!m_DateFrame->rect().contains(me->pos())) { QPoint globalPos = m_DateFrame->mapToGlobal(me->pos()); if (QApplication::widgetAt(globalPos) == this) { // The date picker is being closed by a click on the // DateEdit widget. Avoid popping it up again immediately. m_DiscardNextMousePress = true; } } break; } default: break; } } return false; } void AnnotationDialog::DateEdit::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton && m_DiscardNextMousePress) { m_DiscardNextMousePress = false; return; } QComboBox::mousePressEvent(e); } void AnnotationDialog::DateEdit::slotTextChanged(const QString &) { m_TextChanged = true; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageDate.cpp b/DB/ImageDate.cpp index 7a1383ba..d82409d0 100644 --- a/DB/ImageDate.cpp +++ b/DB/ImageDate.cpp @@ -1,400 +1,427 @@ /* 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 "ImageDate.h" #include #include #include using namespace DB; ImageDate::ImageDate(const QDate &date) { m_start = QDateTime(date, QTime(0, 0, 0)); m_end = QDateTime(date, QTime(0, 0, 0)); } ImageDate::ImageDate(const QDateTime &date) { m_start = date; m_end = date; } bool ImageDate::operator<=(const ImageDate &other) const { // This operator is used by QMap when checking for equal elements, thus we need the second part too. return m_start < other.m_start || (m_start == other.m_start && m_end <= other.m_end); } ImageDate::ImageDate() { } bool ImageDate::isNull() const { return m_start.isNull(); } bool ImageDate::isFuzzy() const { return m_start != m_end; } static bool isFirstSecOfMonth(const QDateTime &date) { return date.date().day() == 1 && date.time().hour() == 0 && date.time().minute() == 0; } static bool isLastSecOfMonth(QDateTime date) { return isFirstSecOfMonth(date.addSecs(1)); } static bool isFirstSecOfDay(const QDateTime &time) { return time.time().hour() == 0 && time.time().minute() == 0 && time.time().second() == 0; } static bool isLastSecOfDay(const QDateTime &time) { return time.time().hour() == 23 && time.time().minute() == 59 && time.time().second() == 59; } QString ImageDate::toString(bool withTime) const { if (m_start.isNull()) return QString(); if (m_start == m_end) { if (withTime && !isFirstSecOfDay(m_start)) return m_start.toString(QString::fromLatin1("d. MMM yyyy hh:mm:ss")); else return m_start.toString(QString::fromLatin1("d. MMM yyyy")); } // start is different from end. if (isFirstSecOfMonth(m_start) && isLastSecOfMonth(m_end)) { if (m_start.date().month() == 1 && m_end.date().month() == 12) { if (m_start.date().year() == m_end.date().year()) { // 2005 return QString::number(m_start.date().year()); } else { // 2005-2006 return QString::fromLatin1("%1 - %2").arg(m_start.date().year()).arg(m_end.date().year()); } } else { // a whole month, but not a whole year. if (m_start.date().year() == m_end.date().year() && m_start.date().month() == m_end.date().month()) { // jan 2005 return QString::fromLatin1("%1 %2") .arg(QLocale().standaloneMonthName(m_start.date().month(), QLocale::ShortFormat)) .arg(m_start.date().year()); } else { // jan 2005 - feb 2006 return QString::fromLatin1("%1 %2 - %3 %4") .arg(QLocale().standaloneMonthName(m_start.date().month(), QLocale::ShortFormat)) .arg(m_start.date().year()) .arg(QLocale().standaloneMonthName(m_end.date().month(), QLocale::ShortFormat)) .arg(m_end.date().year()); } } } if (!withTime || (isFirstSecOfDay(m_start) && isLastSecOfDay(m_end))) { if (m_start.date() == m_end.date()) { // A whole day return m_start.toString(QString::fromLatin1("d. MMM yyyy")); } else { // A day range return QString::fromLatin1("%1 - %2") .arg(m_start.toString(QString::fromLatin1("d. MMM yyyy"))) .arg(m_end.toString(QString::fromLatin1("d. MMM yyyy"))); } } // Range smaller than one day. if (withTime && (!isFirstSecOfDay(m_start) || !isLastSecOfDay(m_end))) return QString::fromLatin1("%1 - %2") .arg(m_start.toString(QString::fromLatin1("d. MMM yyyy hh:mm"))) .arg(m_end.toString(QString::fromLatin1("d. MMM yyyy hh:mm"))); else return QString::fromLatin1("%1 - %2") .arg(m_start.toString(QString::fromLatin1("d. MMM yyyy"))) .arg(m_end.toString(QString::fromLatin1("d. MMM yyyy"))); } bool ImageDate::operator==(const ImageDate &other) const { return m_start == other.m_start && m_end == other.m_end; } bool ImageDate::operator!=(const ImageDate &other) const { return !(*this == other); } QString ImageDate::formatRegexp() { static QString str; if (str.isEmpty()) { str = QString::fromLatin1("^((\\d\\d?)([-. /]+|$))?(("); QStringList months = monthNames(); for (QStringList::ConstIterator monthIt = months.constBegin(); monthIt != months.constEnd(); ++monthIt) str += QString::fromLatin1("%1|").arg(*monthIt); str += QString::fromLatin1("\\d?\\d)([-. /]+|$))?(\\d\\d(\\d\\d)?)?$"); } return str; } QDateTime ImageDate::start() const { return m_start; } QDateTime ImageDate::end() const { return m_end; } bool ImageDate::operator<(const ImageDate &other) const { return start() < other.start() || (start() == other.start() && end() < other.end()); } ImageDate::ImageDate(const QDateTime &start, const QDateTime &end) { if (!start.isValid() || !end.isValid() || start <= end) { m_start = start; m_end = end; } else { m_start = end; m_end = start; } } ImageDate::ImageDate(const QDate &start, const QDate &end) { if (!start.isValid() || !end.isValid() || start <= end) { m_start = QDateTime(start, QTime(0, 0, 0)); m_end = QDateTime(end, QTime(23, 59, 59)); } else { m_start = QDateTime(end, QTime(0, 0, 0)); m_end = QDateTime(start, QTime(23, 59, 59)); } } static QDate addMonth(int year, int month) { if (month == 12) { year++; month = 1; } else month++; return QDate(year, month, 1); } ImageDate::ImageDate(int yearFrom, int monthFrom, int dayFrom, int yearTo, int monthTo, int dayTo, int hourFrom, int minuteFrom, int secondFrom) { if (yearFrom <= 0) { m_start = QDateTime(); m_end = QDateTime(); return; } if (monthFrom <= 0) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + m_start = QDate(yearFrom, 1, 1).startOfDay(); + m_end = QDate(yearFrom + 1, 1, 1).startOfDay().addSecs(-1); +#else m_start = QDateTime(QDate(yearFrom, 1, 1)); m_end = QDateTime(QDate(yearFrom + 1, 1, 1)).addSecs(-1); +#endif } else if (dayFrom <= 0) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + m_start = QDate(yearFrom, monthFrom, 1).startOfDay(); + m_end = addMonth(yearFrom, monthFrom).startOfDay().addSecs(-1); +#else m_start = QDateTime(QDate(yearFrom, monthFrom, 1)); m_end = QDateTime(addMonth(yearFrom, monthFrom)).addSecs(-1); +#endif } else if (hourFrom < 0) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + m_start = QDate(yearFrom, monthFrom, dayFrom).startOfDay(); + m_end = QDate(yearFrom, monthFrom, dayFrom).addDays(1).startOfDay().addSecs(-1); +#else m_start = QDateTime(QDate(yearFrom, monthFrom, dayFrom)); m_end = QDateTime(QDate(yearFrom, monthFrom, dayFrom).addDays(1)).addSecs(-1); +#endif } else if (minuteFrom < 0) { m_start = QDateTime(QDate(yearFrom, monthFrom, dayFrom), QTime(hourFrom, 0, 0)); m_end = QDateTime(QDate(yearFrom, monthFrom, dayFrom), QTime(hourFrom, 23, 59)); } else if (secondFrom < 0) { m_start = QDateTime(QDate(yearFrom, monthFrom, dayFrom), QTime(hourFrom, minuteFrom, 0)); m_end = QDateTime(QDate(yearFrom, monthFrom, dayFrom), QTime(hourFrom, minuteFrom, 59)); } else { m_start = QDateTime(QDate(yearFrom, monthFrom, dayFrom), QTime(hourFrom, minuteFrom, secondFrom)); m_end = m_start; } if (yearTo > 0) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + m_end = QDate(yearTo + 1, 1, 1).startOfDay().addSecs(-1); +#else m_end = QDateTime(QDate(yearTo + 1, 1, 1)).addSecs(-1); +#endif if (monthTo > 0) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + m_end = addMonth(yearTo, monthTo).startOfDay().addSecs(-1); +#else m_end = QDateTime(addMonth(yearTo, monthTo)).addSecs(-1); +#endif if (dayTo > 0) { if (dayFrom == dayTo && monthFrom == monthTo && yearFrom == yearTo) m_end = m_start; else +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + m_end = QDate(yearTo, monthTo, dayTo).addDays(1).startOfDay().addSecs(-1); +#else m_end = QDateTime(QDate(yearTo, monthTo, dayTo).addDays(1)).addSecs(-1); +#endif } } // It should not be possible here for m_end < m_start. Q_ASSERT(m_start <= m_end); } } QDate ImageDate::parseDate(const QString &date, bool startDate) { int year = 0; int month = 0; int day = 0; QRegExp regexp(formatRegexp(), Qt::CaseInsensitive); if (regexp.exactMatch(date)) { QString dayStr = regexp.cap(2); QString monthStr = regexp.cap(5).toLower(); QString yearStr = regexp.cap(7); if (dayStr.length() != 0) day = dayStr.toInt(); if (yearStr.length() != 0) { year = yearStr.toInt(); if (year < 50) year += 2000; if (year < 100) year += 1900; } if (monthStr.length() != 0) { int index = monthNames().indexOf(monthStr); if (index != -1) month = (index % 12) + 1; else month = monthStr.toInt(); } if (year == 0) year = QDate::currentDate().year(); if (month == 0) { if (startDate) { month = 1; day = 1; } else { month = 12; day = 31; } } else if (day == 0) { if (startDate) day = 1; else day = QDate(year, month, 1).daysInMonth(); } return QDate(year, month, day); } else return QDate(); } bool ImageDate::hasValidTime() const { return m_start == m_end; } ImageDate::ImageDate(const QDate &start, QDate end, const QTime &time) { if (!end.isValid()) end = start; if (start == end && time.isValid()) { m_start = QDateTime(start, time); m_end = m_start; } else { if (start > end) { m_end = QDateTime(start, QTime(0, 0, 0)); m_start = QDateTime(end, QTime(23, 59, 59)); } else { m_start = QDateTime(start, QTime(0, 0, 0)); m_end = QDateTime(end, QTime(23, 59, 59)); } } } ImageDate::MatchType ImageDate::isIncludedIn(const ImageDate &searchRange) const { if (searchRange.start() <= start() && searchRange.end() >= end()) return ExactMatch; if (searchRange.start() <= end() && searchRange.end() >= start()) { return RangeMatch; } return DontMatch; } bool ImageDate::includes(const QDateTime &date) const { return ImageDate(date).isIncludedIn(*this) == ExactMatch; } void ImageDate::extendTo(const ImageDate &other) { if (other.isNull()) return; if (isNull()) { m_start = other.m_start; m_end = other.m_end; } else { if (other.m_start < m_start) m_start = other.m_start; if (other.m_end > m_end) m_end = other.m_end; } } QStringList DB::ImageDate::monthNames() { static QStringList res; if (res.isEmpty()) { for (int i = 1; i <= 12; ++i) { res << QLocale().standaloneMonthName(i, QLocale::ShortFormat); } for (int i = 1; i <= 12; ++i) { res << QLocale().standaloneMonthName(i, QLocale::LongFormat); } res << i18nc("Abbreviated month name", "jan") << i18nc("Abbreviated month name", "feb") << i18nc("Abbreviated month name", "mar") << i18nc("Abbreviated month name", "apr") << i18nc("Abbreviated month name", "may") << i18nc("Abbreviated month name", "jun") << i18nc("Abbreviated month name", "jul") << i18nc("Abbreviated month name", "aug") << i18nc("Abbreviated month name", "sep") << i18nc("Abbreviated month name", "oct") << i18nc("Abbreviated month name", "nov") << i18nc("Abbreviated month name", "dec"); res << QString::fromLatin1("jan") << QString::fromLatin1("feb") << QString::fromLatin1("mar") << QString::fromLatin1("apr") << QString::fromLatin1("may") << QString::fromLatin1("jun") << QString::fromLatin1("jul") << QString::fromLatin1("aug") << QString::fromLatin1("sep") << QString::fromLatin1("oct") << QString::fromLatin1("nov") << QString::fromLatin1("dec"); for (int i = 1; i <= 12; ++i) { res << QLocale().monthName(i, QLocale::ShortFormat); } for (int i = 1; i <= 12; ++i) { res << QLocale().monthName(i, QLocale::LongFormat); } for (QStringList::iterator it = res.begin(); it != res.end(); ++it) *it = it->toLower(); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileReader.cpp b/XMLDB/FileReader.cpp index c853ed71..5d74ff7a 100644 --- a/XMLDB/FileReader.cpp +++ b/XMLDB/FileReader.cpp @@ -1,597 +1,601 @@ /* Copyright (C) 2003-2020 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 "FileReader.h" #include "CompressFileInfo.h" #include "Database.h" #include "Logging.h" #include "XMLCategory.h" #include #include // KDE includes #include // Qt includes #include #include #include #include #include #include #include void XMLDB::FileReader::read(const QString &configFile) { static QString versionString = QString::fromUtf8("version"); static QString compressedString = QString::fromUtf8("compressed"); ReaderPtr reader = readConfigFile(configFile); ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KPhotoAlbum")); if (!info.isStartToken) reader->complainStartElementExpected(QString::fromUtf8("KPhotoAlbum")); m_fileVersion = reader->attribute(versionString, QString::fromLatin1("1")).toInt(); if (m_fileVersion > Database::fileVersion()) { DB::UserFeedback ret = m_db->uiDelegate().warningContinueCancel( QString::fromLatin1("index.xml version %1 is newer than %2!").arg(m_fileVersion).arg(Database::fileVersion()), i18n("

The database file (index.xml) is from a newer version of KPhotoAlbum!

" "

Chances are you will be able to read this file, but when writing it back, " "information saved in the newer version will be lost

"), i18n("index.xml version mismatch"), QString::fromLatin1("checkDatabaseFileVersion")); if (ret != DB::UserFeedback::Confirm) exit(-1); } setUseCompressedFileFormat(reader->attribute(compressedString).toInt()); m_db->m_members.setLoading(true); loadCategories(reader); loadImages(reader); loadBlockList(reader); loadMemberGroups(reader); //loadSettings(reader); repairDB(); m_db->m_members.setLoading(false); checkIfImagesAreSorted(); checkIfAllImagesHaveSizeAttributes(); } void XMLDB::FileReader::createSpecialCategories() { // Setup the "Folder" category m_folderCategory = new XMLCategory(i18n("Folder"), QString::fromLatin1("folder"), DB::Category::TreeView, 32, false); m_folderCategory->setType(DB::Category::FolderCategory); // The folder category is not stored in the index.xml file, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if (m_db->m_categoryCollection.categoryForName(m_folderCategory->name())) m_db->m_categoryCollection.removeCategory(m_folderCategory->name()); m_db->m_categoryCollection.addCategory(m_folderCategory); dynamic_cast(m_folderCategory.data())->setShouldSave(false); // Setup the "Tokens" category DB::CategoryPtr tokenCat; if (m_fileVersion >= 7) { tokenCat = m_db->m_categoryCollection.categoryForSpecial(DB::Category::TokensCategory); } else { // Before version 7, the "Tokens" category name wasn't stored to the settings. So ... // look for a literal "Tokens" category ... tokenCat = m_db->m_categoryCollection.categoryForName(QString::fromUtf8("Tokens")); if (!tokenCat) { // ... and a translated "Tokens" category if we don't have the literal one. tokenCat = m_db->m_categoryCollection.categoryForName(i18n("Tokens")); } if (tokenCat) { // in this case we need to give the tokens category its special meaning: m_db->m_categoryCollection.removeCategory(tokenCat->name()); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } } if (!tokenCat) { // Create a new "Tokens" category tokenCat = new XMLCategory(i18n("Tokens"), QString::fromUtf8("tag"), DB::Category::TreeView, 32, true); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } // KPhotoAlbum 2.2 did not write the tokens to the category section, // so unless we do this small trick they will not show up when importing. for (char ch = 'A'; ch <= 'Z'; ++ch) { tokenCat->addItem(QString::fromUtf8("%1").arg(QChar::fromLatin1(ch))); } // Setup the "Media Type" category DB::CategoryPtr mediaCat; mediaCat = new XMLCategory(i18n("Media Type"), QString::fromLatin1("view-categories"), DB::Category::TreeView, 32, false); mediaCat->addItem(i18n("Image")); mediaCat->addItem(i18n("Video")); mediaCat->setType(DB::Category::MediaTypeCategory); dynamic_cast(mediaCat.data())->setShouldSave(false); // The media type is not stored in the media category, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if (m_db->m_categoryCollection.categoryForName(mediaCat->name())) m_db->m_categoryCollection.removeCategory(mediaCat->name()); m_db->m_categoryCollection.addCategory(mediaCat); } void XMLDB::FileReader::loadCategories(ReaderPtr reader) { static QString nameString = QString::fromUtf8("name"); static QString iconString = QString::fromUtf8("icon"); static QString viewTypeString = QString::fromUtf8("viewtype"); static QString showString = QString::fromUtf8("show"); static QString thumbnailSizeString = QString::fromUtf8("thumbnailsize"); static QString positionableString = QString::fromUtf8("positionable"); static QString metaString = QString::fromUtf8("meta"); static QString tokensString = QString::fromUtf8("tokens"); static QString valueString = QString::fromUtf8("value"); static QString idString = QString::fromUtf8("id"); static QString birthDateString = QString::fromUtf8("birthDate"); static QString categoriesString = QString::fromUtf8("Categories"); static QString categoryString = QString::fromUtf8("Category"); ElementInfo info = reader->readNextStartOrStopElement(categoriesString); if (!info.isStartToken) reader->complainStartElementExpected(categoriesString); while (reader->readNextStartOrStopElement(categoryString).isStartToken) { const QString categoryName = unescape(reader->attribute(nameString)); if (!categoryName.isNull()) { // Read Category info QString icon = reader->attribute(iconString); DB::Category::ViewType type = (DB::Category::ViewType)reader->attribute(viewTypeString, QString::fromLatin1("0")).toInt(); int thumbnailSize = reader->attribute(thumbnailSizeString, QString::fromLatin1("32")).toInt(); bool show = (bool)reader->attribute(showString, QString::fromLatin1("1")).toInt(); bool positionable = (bool)reader->attribute(positionableString, QString::fromLatin1("0")).toInt(); bool tokensCat = reader->attribute(metaString) == tokensString; DB::CategoryPtr cat = m_db->m_categoryCollection.categoryForName(categoryName); bool repairMode = false; if (cat) { DB::UserFeedback choice = m_db->uiDelegate().warningContinueCancel( QString::fromUtf8("Line %1, column %2: duplicate category '%3'") .arg(reader->lineNumber()) .arg(reader->columnNumber()) .arg(categoryName), i18n("

Line %1, column %2: duplicate category '%3'

" "

Choose continue to ignore the duplicate category and try an automatic repair, " "or choose cancel to quit.

", reader->lineNumber(), reader->columnNumber(), categoryName), i18n("Error in database file")); if (choice == DB::UserFeedback::Confirm) repairMode = true; else exit(-1); } else { cat = new XMLCategory(categoryName, icon, type, thumbnailSize, show, positionable); if (tokensCat) cat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(cat); } // Read values QStringList items; while (reader->readNextStartOrStopElement(valueString).isStartToken) { QString value = reader->attribute(valueString); if (reader->hasAttribute(idString)) { int id = reader->attribute(idString).toInt(); if (id != 0) { static_cast(cat.data())->setIdMapping(value, id); } else { if (useCompressedFileFormat()) { qCWarning(XMLDBLog) << "Tag" << categoryName << "/" << value << "has id=0!"; m_repairTagsWithNullIds = true; static_cast(cat.data())->setIdMapping(value, id, XMLCategory::IdMapping::UnsafeMapping); } // else just don't set the id mapping so that a new id gets assigned } } if (reader->hasAttribute(birthDateString)) cat->setBirthDate(value, QDate::fromString(reader->attribute(birthDateString), Qt::ISODate)); items.append(value); reader->readEndElement(); } if (repairMode) { // merge with duplicate category qCInfo(XMLDBLog) << "Repairing category " << categoryName << ": merging items " << cat->items() << " with " << items; items.append(cat->items()); items.removeDuplicates(); } cat->setItems(items); } } createSpecialCategories(); if (m_fileVersion < 7) { m_db->uiDelegate().information( QString::fromLatin1("Standard category names are no longer used since index.xml " "version 7. Standard categories will be left untranslated from now on."), i18nc("Leave \"Folder\" and \"Media Type\" untranslated below, those will show up with " "these exact names. Thanks :-)", "

This version of KPhotoAlbum does not translate \"standard\" categories " "any more.

" "

This may mean that – if you use a locale other than English – some of your " "categories are now displayed in English.

" "

You can manually rename your categories any time and then save your database." "

" "

In some cases, you may get two additional empty categories, \"Folder\" and " "\"Media Type\". You can delete those.

"), i18n("Changed standard category names")); } } void XMLDB::FileReader::loadImages(ReaderPtr reader) { static QString fileString = QString::fromUtf8("file"); static QString imagesString = QString::fromUtf8("images"); static QString imageString = QString::fromUtf8("image"); ElementInfo info = reader->readNextStartOrStopElement(imagesString); if (!info.isStartToken) reader->complainStartElementExpected(imagesString); while (reader->readNextStartOrStopElement(imageString).isStartToken) { const QString fileNameStr = reader->attribute(fileString); if (fileNameStr.isNull()) { qCWarning(XMLDBLog, "Element did not contain a file attribute"); return; } const DB::FileName dbFileName = DB::FileName::fromRelativePath(fileNameStr); DB::ImageInfoPtr info = load(dbFileName, reader); if (m_db->md5Map()->containsFile(dbFileName)) { if (m_db->md5Map()->contains(info->MD5Sum())) { qCWarning(XMLDBLog) << "Merging duplicate entry for file" << dbFileName.relative(); DB::ImageInfoPtr existingInfo = m_db->info(dbFileName); existingInfo->merge(*info); } else { m_db->uiDelegate().error( QString::fromUtf8("Conflicting information for file '%1': duplicate entry with different MD5 sum! Bailing out...") .arg(dbFileName.relative()), i18n("

Line %1, column %2: duplicate entry for file '%3' with different MD5 sum.

" "

Manual repair required!

", reader->lineNumber(), reader->columnNumber(), dbFileName.relative()), i18n("Error in database file")); exit(-1); } } else { m_db->m_images.append(info); m_db->m_md5map.insert(info->MD5Sum(), dbFileName); } } } void XMLDB::FileReader::loadBlockList(ReaderPtr reader) { static QString fileString = QString::fromUtf8("file"); static QString blockListString = QString::fromUtf8("blocklist"); static QString blockString = QString::fromUtf8("block"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == blockListString) { reader->readNextStartOrStopElement(blockListString); while (reader->readNextStartOrStopElement(blockString).isStartToken) { QString fileName = reader->attribute(fileString); if (!fileName.isEmpty()) m_db->m_blockList.insert(DB::FileName::fromRelativePath(fileName)); reader->readEndElement(); } } } void XMLDB::FileReader::loadMemberGroups(ReaderPtr reader) { static QString categoryString = QString::fromUtf8("category"); static QString groupNameString = QString::fromUtf8("group-name"); static QString memberString = QString::fromUtf8("member"); static QString membersString = QString::fromUtf8("members"); static QString memberGroupsString = QString::fromUtf8("member-groups"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == memberGroupsString) { reader->readNextStartOrStopElement(memberGroupsString); while (reader->readNextStartOrStopElement(memberString).isStartToken) { QString category = reader->attribute(categoryString); QString group = reader->attribute(groupNameString); if (reader->hasAttribute(memberString)) { QString member = reader->attribute(memberString); m_db->m_members.addMemberToGroup(category, group, member); } else { const QStringList members = reader->attribute(membersString).split(QString::fromLatin1(","), QString::SkipEmptyParts); for (const QString &memberItem : members) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName(category); if (!catPtr) { // category was not declared in "Categories" qCWarning(XMLDBLog) << "File corruption in index.xml. Inserting missing category: " << category; catPtr = new XMLCategory(category, QString::fromUtf8("dialog-warning"), DB::Category::TreeView, 32, false); m_db->m_categoryCollection.addCategory(catPtr); } XMLCategory *cat = static_cast(catPtr.data()); QString member = cat->nameForId(memberItem.toInt()); if (member.isNull()) continue; m_db->m_members.addMemberToGroup(category, group, member); } if (members.size() == 0) { // Groups are stored even if they are empty, so we also have to read them. // With no members, the above for loop will not be executed. m_db->m_members.addGroup(category, group); } } reader->readEndElement(); } } } /* void XMLDB::FileReader::loadSettings(ReaderPtr reader) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == settingsString) { reader->readNextStartOrStopElement(settingString); while(reader->readNextStartOrStopElement(settingString).isStartToken) { if (reader->hasAttribute(keyString) && reader->hasAttribute(valueString)) { m_db->m_settings.insert(unescape(reader->attribute(keyString)), unescape(reader->attribute(valueString))); } else { qWarning() << "File corruption in index.xml. Setting either lacking a key or a " << "value attribute. Ignoring this entry."; } reader->readEndElement(); } } } */ void XMLDB::FileReader::checkIfImagesAreSorted() { if (m_db->uiDelegate().isDialogDisabled(QString::fromLatin1("checkWhetherImagesAreSorted"))) return; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QDateTime last = QDate(1900, 1, 1).startOfDay(); +#else QDateTime last(QDate(1900, 1, 1)); +#endif bool wrongOrder = false; for (DB::ImageInfoListIterator it = m_db->m_images.begin(); !wrongOrder && it != m_db->m_images.end(); ++it) { if (last > (*it)->date().start() && (*it)->date().start().isValid()) wrongOrder = true; last = (*it)->date().start(); } if (wrongOrder) { m_db->uiDelegate().information( QString::fromLatin1("Database is not sorted by date."), i18n("

Your images/videos are not sorted, which means that navigating using the date bar " "will only work suboptimally.

" "

In the Maintenance menu, you can find Display Images with Incomplete Dates " "which you can use to find the images that are missing date information.

" "

You can then select the images that you have reason to believe have a correct date " "in either their Exif data or on the file, and execute Maintenance->Read Exif Info " "to reread the information.

" "

Finally, once all images have their dates set, you can execute " "Maintenance->Sort All by Date & Time to sort them in the database.

"), i18n("Images/Videos Are Not Sorted"), QString::fromLatin1("checkWhetherImagesAreSorted")); } } void XMLDB::FileReader::checkIfAllImagesHaveSizeAttributes() { if (m_db->uiDelegate().isDialogDisabled(QString::fromLatin1("checkWhetherAllImagesIncludesSize"))) return; if (m_db->s_anyImageWithEmptySize) { m_db->uiDelegate().information( QString::fromLatin1("Found image(s) without size information."), i18n("

Not all the images in the database have information about image sizes; this is needed to " "get the best result in the thumbnail view. To fix this, simply go to the Maintenance menu, " "and first choose Remove All Thumbnails, and after that choose Build Thumbnails.

" "

Not doing so will result in extra space around images in the thumbnail view - that is all - so " "there is no urgency in doing it.

"), i18n("Not All Images Have Size Information"), QString::fromLatin1("checkWhetherAllImagesIncludesSize")); } } void XMLDB::FileReader::repairDB() { if (m_repairTagsWithNullIds) { // the m_repairTagsWithNullIds is set in loadCategories() // -> care is taken so that multiple tags with id=0 all end up in the IdMap // afterwards, loadImages() applies fixes to the affected images // -> this happens in XMLDB::Database::possibleLoadCompressedCategories() // i.e. the zero ids still require cleanup: qCInfo(XMLDBLog) << "Database contained tags with id=0 (possibly related to bug #415415). Assigning new ids for affected categories..."; QString message = i18nc("repair merged tags", "

Inconsistencies were found and repaired in your database. " "Some categories now contain tags that were merged during the repair.

" "

The following tags require manual inspection:" "

    "); QString logSummary = QString::fromLatin1("List of tags where manual inspection is required:\n"); bool manualRepairNeeded = false; for (auto category : m_db->categoryCollection()->categories()) { XMLCategory *xmlCategory = static_cast(category.data()); QStringList tags = xmlCategory->namesForId(0); if (tags.size() > 1) { manualRepairNeeded = true; message += i18nc("repair merged tags", "
  • %1:
    ", category->name()); for (auto tagName : tags) { message += i18nc("repair merged tags", "%1
    ", tagName); logSummary += QString::fromLatin1("%1/%2\n").arg(category->name(), tagName); } message += i18nc("repair merged tags", "
  • "); } xmlCategory->clearNullIds(); } message += i18nc("repair merged tags", "

" "

All affected images have also been marked with a tag " "KPhotoAlbum - manual repair needed.

"); if (manualRepairNeeded) { m_db->uiDelegate().information(logSummary, message, i18n("Database repair required")); } } } DB::ImageInfoPtr XMLDB::FileReader::load(const DB::FileName &fileName, ReaderPtr reader) { DB::ImageInfoPtr info = XMLDB::Database::createImageInfo(fileName, reader, m_db); m_nextStackId = qMax(m_nextStackId, info->stackId() + 1); info->createFolderCategoryItem(m_folderCategory, m_db->m_members); return info; } XMLDB::ReaderPtr XMLDB::FileReader::readConfigFile(const QString &configFile) { ReaderPtr reader = ReaderPtr(new XmlReader(m_db->uiDelegate(), configFile)); QFile file(configFile); if (!file.exists()) { // Load a default setup QFile file(QStandardPaths::locate(QStandardPaths::DataLocation, QString::fromLatin1("default-setup"))); if (!file.open(QIODevice::ReadOnly)) { m_db->uiDelegate().information( QString::fromLatin1("default-setup not found in standard paths."), i18n("

KPhotoAlbum was unable to load a default setup, which indicates an installation error

" "

If you have installed KPhotoAlbum yourself, then you must remember to set the environment variable " "KDEDIRS, to point to the topmost installation directory.

" "

If you for example ran cmake with -DCMAKE_INSTALL_PREFIX=/usr/local/kde, then you must use the following " "environment variable setup (this example is for Bash and compatible shells):

" "

export KDEDIRS=/usr/local/kde

" "

In case you already have KDEDIRS set, simply append the string as if you where setting the PATH " "environment variable

"), i18n("No default setup file found")); } else { QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); QString str = stream.readAll(); // Replace the default setup's category and tag names with localized ones str = str.replace(QString::fromUtf8("People"), i18n("People")); str = str.replace(QString::fromUtf8("Places"), i18n("Places")); str = str.replace(QString::fromUtf8("Events"), i18n("Events")); str = str.replace(QString::fromUtf8("untagged"), i18n("untagged")); str = str.replace(QRegExp(QString::fromLatin1("imageDirectory=\"[^\"]*\"")), QString::fromLatin1("")); str = str.replace(QRegExp(QString::fromLatin1("htmlBaseDir=\"[^\"]*\"")), QString::fromLatin1("")); str = str.replace(QRegExp(QString::fromLatin1("htmlBaseURL=\"[^\"]*\"")), QString::fromLatin1("")); reader->addData(str); } } else { if (!file.open(QIODevice::ReadOnly)) { m_db->uiDelegate().error( QString::fromLatin1("Unable to open '%1' for reading").arg(configFile), i18n("Unable to open '%1' for reading", configFile), i18n("Error Running Demo")); exit(-1); } reader->addData(file.readAll()); #if 0 QString errMsg; int errLine; int errCol; if ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol )) { file.close(); // If parsing index.xml fails let's see if we could use a backup instead Utilities::checkForBackupFile( configFile, i18n( "line %1 column %2 in file %3: %4", errLine , errCol , configFile , errMsg ) ); if ( !file.open( QIODevice::ReadOnly ) || ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol ) ) ) { KMessageBox::error( messageParent(), i18n( "Failed to recover the backup: %1", errMsg ) ); exit(-1); } } #endif } // Now read the content of the file. #if 0 QDomElement top = doc.documentElement(); if ( top.isNull() ) { KMessageBox::error( messageParent(), i18n("Error in file %1: No elements found", configFile ) ); exit(-1); } if ( top.tagName().toLower() != QString::fromLatin1( "kphotoalbum" ) && top.tagName().toLower() != QString::fromLatin1( "kimdaba" ) ) { // KimDaBa compatibility KMessageBox::error( messageParent(), i18n("Error in file %1: expected 'KPhotoAlbum' as top element but found '%2'", configFile , top.tagName() ) ); exit(-1); } #endif file.close(); return reader; } /** * @brief Unescape a string used as an XML attribute name. * * @see XMLDB::FileWriter::escape * * @param str the string to be unescaped * @return the unescaped string */ QString XMLDB::FileReader::unescape(const QString &str) { static bool hashUsesCompressedFormat = useCompressedFileFormat(); static QHash s_cache; if (hashUsesCompressedFormat != useCompressedFileFormat()) s_cache.clear(); if (s_cache.contains(str)) return s_cache[str]; QString tmp(str); // Matches encoded characters in attribute names QRegExp rx(QString::fromLatin1("(_.)([0-9A-F]{2})")); int pos = 0; // Unencoding special characters if compressed XML is selected if (useCompressedFileFormat()) { while ((pos = rx.indexIn(tmp, pos)) != -1) { QString before = rx.cap(1) + rx.cap(2); QString after = QString::fromLatin1(QByteArray::fromHex(rx.cap(2).toLocal8Bit())); tmp.replace(pos, before.length(), after); pos += after.length(); } } else tmp.replace(QString::fromLatin1("_"), QString::fromLatin1(" ")); s_cache.insert(str, tmp); return tmp; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLImageDateCollection.cpp b/XMLDB/XMLImageDateCollection.cpp index 796e6453..5fa052da 100644 --- a/XMLDB/XMLImageDateCollection.cpp +++ b/XMLDB/XMLImageDateCollection.cpp @@ -1,141 +1,153 @@ /* Copyright (C) 2003-2020 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 "XMLImageDateCollection.h" #include #include void XMLDB::XMLImageDateCollection::add(const DB::ImageDate &date) { m_startIndex.insertMulti(date.start(), date); } void XMLDB::XMLImageDateCollection::buildIndex() { StartIndexMap::ConstIterator startSearch = m_startIndex.constBegin(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QDateTime biggestEnd = QDate(1900, 1, 1).startOfDay(); +#else QDateTime biggestEnd = QDateTime(QDate(1900, 1, 1)); +#endif for (StartIndexMap::ConstIterator it = m_startIndex.constBegin(); it != m_startIndex.constEnd(); ++it) { // We want a monotonic mapping end-date -> smallest-in-start-index. // Since we go through the start index sorted, lowest first, we just // have to keep the last pointer as long as we find smaller end-dates. // This should be rare as it only occurs if there are images that // actually represent a range not just a point in time. if (it.value().end() >= biggestEnd) { biggestEnd = it.value().end(); startSearch = it; } m_endIndex.insert(it.value().end(), startSearch); } } /** Previously, counting the elements was done by going through all elements and count the matches for a particular range, this unfortunately had O(n) complexity multiplied by m ranges we would get O(mn). Henner Zeller rewrote it to its current state. The main idea now is to have all dates sorted so that it is possible to only look at the requested range. Since it is not points in time, we can't have just a simple sorted list. So we have two sorted maps, the m_startIndex and m_endIndex. m_startIndex is sorted by the start time of all ImageDates (which are in fact ranges) If we would just look for Images that start _after_ the query-range, we would miscount, because there might be Image ranges starting before the query time but whose end time reaches into the query range this is what the m_endIndex is for: it is sortd by end-date; here we look for everything that is >= our query start. its value() part is basically a pointer to the position in the m_startIndex where we actually have to start looking. The rest is simple: we determine the interesting start in m_startIndex using the m_endIndex and iterate through it until the elements in that sorted list have a start time that is larger than the query-end-range .. there will no more elements coming. The above uses the fact that a QMap::constIterator iterates the map in sorted order. **/ DB::ImageCount XMLDB::XMLImageDateCollection::count(const DB::ImageDate &range) { if (m_cache.contains(range)) return m_cache[range]; int exact = 0, rangeMatch = 0; // We start searching in ranges that overlap our start search range, i.e. // where the end-date is higher than our search start. EndIndexMap::Iterator endSearch = m_endIndex.lowerBound(range.start()); if (endSearch != m_endIndex.end()) { for (StartIndexMap::ConstIterator it = endSearch.value(); it != m_startIndex.constEnd() && it.key() < range.end(); ++it) { DB::ImageDate::MatchType tp = it.value().isIncludedIn(range); switch (tp) { case DB::ImageDate::ExactMatch: exact++; break; case DB::ImageDate::RangeMatch: rangeMatch++; break; case DB::ImageDate::DontMatch: break; } } } DB::ImageCount res(exact, rangeMatch); m_cache.insert(range, res); // TODO(hzeller) this might go now return res; } QDateTime XMLDB::XMLImageDateCollection::lowerLimit() const { if (!m_startIndex.empty()) { // skip null dates: for (StartIndexMap::ConstIterator it = m_startIndex.constBegin(); it != m_startIndex.constEnd(); ++it) { if (it.key().isValid()) return it.key(); } } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + return QDate(1900, 1, 1).startOfDay(); +#else return QDateTime(QDate(1900, 1, 1)); +#endif } QDateTime XMLDB::XMLImageDateCollection::upperLimit() const { if (!m_endIndex.empty()) { EndIndexMap::ConstIterator highest = m_endIndex.constEnd(); --highest; return highest.key(); } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + return QDate(2100, 1, 1).startOfDay(); +#else return QDateTime(QDate(2100, 1, 1)); +#endif } XMLDB::XMLImageDateCollection::XMLImageDateCollection(const DB::ImageInfoList &list) { for (const auto &image : list) { add(image->date()); } buildIndex(); } // vi:expandtab:tabstop=4 shiftwidth=4: