diff --git a/AnnotationDialog/Dialog.cpp b/AnnotationDialog/Dialog.cpp index 0e5caa91..c09358b2 100644 --- a/AnnotationDialog/Dialog.cpp +++ b/AnnotationDialog/Dialog.cpp @@ -1,1769 +1,1774 @@ /* 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. */ #include "Dialog.h" #include "DateEdit.h" #include "DescriptionEdit.h" #include "ImagePreviewWidget.h" #include "ListSelect.h" #include "Logging.h" #include "ResizableFrame.h" #include "ShortCutManager.h" #include "ShowSelectionOnlyManager.h" #include "enums.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 #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_MARBLE #include "Map/GeoCoordinates.h" #include #include #include #endif #include #include #include #include #include namespace { inline QPixmap smallIcon(const QString &iconName) { return QIcon::fromTheme(iconName).pixmap(KIconLoader::StdSizes::SizeSmall); } } using Utilities::StringSet; /** * \class AnnotationDialog::Dialog * \brief QDialog subclass used for tagging images */ AnnotationDialog::Dialog::Dialog(QWidget *parent) : QDialog(parent) , m_ratingChanged(false) , m_conflictText(i18n("(You have differing descriptions on individual images, setting text here will override them all)")) { Utilities::ShowBusyCursor dummy; ShortCutManager shortCutManager; m_actions = new KActionCollection(this); // The widget stack QWidget *mainWidget = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(mainWidget); setLayout(layout); layout->addWidget(mainWidget); m_stack = new QStackedWidget(mainWidget); layout->addWidget(m_stack); // The Viewer m_fullScreenPreview = new Viewer::ViewerWidget(Viewer::ViewerWidget::InlineViewer); m_stack->addWidget(m_fullScreenPreview); // The dock widget m_dockWindow = new QMainWindow; m_stack->addWidget(m_dockWindow); m_dockWindow->setDockNestingEnabled(true); // -------------------------------------------------- Dock widgets m_generalDock = createDock(i18n("Label and Dates"), QString::fromLatin1("Label and Dates"), Qt::TopDockWidgetArea, createDateWidget(shortCutManager)); m_previewDock = createDock(i18n("Image Preview"), QString::fromLatin1("Image Preview"), Qt::TopDockWidgetArea, createPreviewWidget()); m_description = new DescriptionEdit(this); m_description->setProperty("WantsFocus", true); m_description->setObjectName(i18n("Description")); m_description->setCheckSpellingEnabled(true); m_description->setTabChangesFocus(true); // this allows tabbing to the next item in the tab order. m_description->setWhatsThis(i18nc("@info:whatsthis", "A descriptive text of the image." "If Use Exif description is enabled under " "Settings|Configure KPhotoAlbum...|General, a description " "embedded in the image Exif information is imported to this field if available.")); m_descriptionDock = createDock(i18n("Description"), QString::fromLatin1("description"), Qt::LeftDockWidgetArea, m_description); shortCutManager.addDock(m_descriptionDock, m_description); connect(m_description, &DescriptionEdit::pageUpDownPressed, this, &Dialog::descriptionPageUpDownPressed); #ifdef HAVE_MARBLE // -------------------------------------------------- Map representation m_annotationMapContainer = new QWidget(this); QVBoxLayout *annotationMapContainerLayout = new QVBoxLayout(m_annotationMapContainer); m_annotationMap = new Map::MapView(this, Map::UsageType::InlineMapView); annotationMapContainerLayout->addWidget(m_annotationMap); QHBoxLayout *mapLoadingProgressLayout = new QHBoxLayout(); annotationMapContainerLayout->addLayout(mapLoadingProgressLayout); m_mapLoadingProgress = new QProgressBar(this); mapLoadingProgressLayout->addWidget(m_mapLoadingProgress); m_mapLoadingProgress->hide(); m_cancelMapLoadingButton = new QPushButton(i18n("Cancel")); mapLoadingProgressLayout->addWidget(m_cancelMapLoadingButton); m_cancelMapLoadingButton->hide(); connect(m_cancelMapLoadingButton, &QPushButton::clicked, this, &Dialog::setCancelMapLoading); m_annotationMapContainer->setObjectName(i18n("Map")); m_mapDock = createDock( i18n("Map"), QString::fromLatin1("map"), Qt::LeftDockWidgetArea, m_annotationMapContainer); shortCutManager.addDock(m_mapDock, m_annotationMapContainer); connect(m_mapDock, &QDockWidget::visibilityChanged, this, &Dialog::annotationMapVisibilityChanged); m_mapDock->setWhatsThis(i18nc("@info:whatsthis", "The map widget allows you to view the location of images if GPS coordinates are found in the Exif information.")); #endif // -------------------------------------------------- Categories QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); // Let's first assume we don't have positionable categories m_positionableCategories = false; for (QList::ConstIterator categoryIt = categories.constBegin(); categoryIt != categories.constEnd(); ++categoryIt) { ListSelect *sel = createListSel(*categoryIt); // Create a QMap of all ListSelect instances, so that we can easily // check if a specific (positioned) tag is (still) selected later m_listSelectList[(*categoryIt)->name()] = sel; QDockWidget *dock = createDock((*categoryIt)->name(), (*categoryIt)->name(), Qt::BottomDockWidgetArea, sel); shortCutManager.addDock(dock, sel->lineEdit()); if ((*categoryIt)->isSpecialCategory()) dock->hide(); // Pass the positionable selection to the object sel->setPositionable((*categoryIt)->positionable()); if (sel->positionable()) { connect(sel, &ListSelect::positionableTagSelected, this, &Dialog::positionableTagSelected); connect(sel, &ListSelect::positionableTagDeselected, this, &Dialog::positionableTagDeselected); connect(sel, &ListSelect::positionableTagRenamed, this, &Dialog::positionableTagRenamed); connect(m_preview->preview(), &ImagePreview::proposedTagSelected, sel, &ListSelect::ensureTagIsSelected); // We have at least one positionable category m_positionableCategories = true; } } // -------------------------------------------------- The buttons. // don't use default buttons (Ok, Cancel): QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::NoButton); connect(buttonBox, &QDialogButtonBox::accepted, this, &Dialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &Dialog::reject); QHBoxLayout *lay1 = new QHBoxLayout; layout->addLayout(lay1); m_revertBut = new QPushButton(i18n("Revert This Item")); KAcceleratorManager::setNoAccel(m_revertBut); lay1->addWidget(m_revertBut); m_clearBut = new QPushButton(); KGuiItem::assign(m_clearBut, KGuiItem(i18n("Clear Form"), QApplication::isRightToLeft() ? QString::fromLatin1("clear_left") : QString::fromLatin1("locationbar_erase"))); KAcceleratorManager::setNoAccel(m_clearBut); lay1->addWidget(m_clearBut); QPushButton *optionsBut = new QPushButton(i18n("Options...")); KAcceleratorManager::setNoAccel(optionsBut); lay1->addWidget(optionsBut); lay1->addStretch(1); m_okBut = new QPushButton(i18n("&Done")); lay1->addWidget(m_okBut); m_continueLaterBut = new QPushButton(i18n("Continue &Later")); lay1->addWidget(m_continueLaterBut); QPushButton *cancelBut = new QPushButton(); KGuiItem::assign(cancelBut, KStandardGuiItem::cancel()); lay1->addWidget(cancelBut); // It is unfortunately not possible to ask KAcceleratorManager not to setup the OK and cancel keys. shortCutManager.addTaken(i18nc("@action:button", "&Search")); shortCutManager.addTaken(m_okBut->text()); shortCutManager.addTaken(m_continueLaterBut->text()); shortCutManager.addTaken(cancelBut->text()); connect(m_revertBut, &QPushButton::clicked, this, &Dialog::slotRevert); connect(m_okBut, &QPushButton::clicked, this, &Dialog::doneTagging); connect(m_continueLaterBut, &QPushButton::clicked, this, &Dialog::continueLater); connect(cancelBut, &QPushButton::clicked, this, &Dialog::reject); connect(m_clearBut, &QPushButton::clicked, this, &Dialog::slotClear); connect(optionsBut, &QPushButton::clicked, this, &Dialog::slotOptions); connect(m_preview, &ImagePreviewWidget::imageRotated, this, &Dialog::rotate); connect(m_preview, &ImagePreviewWidget::indexChanged, this, &Dialog::slotIndexChanged); connect(m_preview, &ImagePreviewWidget::imageDeleted, this, &Dialog::slotDeleteImage); connect(m_preview, &ImagePreviewWidget::copyPrevClicked, this, &Dialog::slotCopyPrevious); connect(m_preview, &ImagePreviewWidget::areaVisibilityChanged, this, &Dialog::slotShowAreas); connect(m_preview->preview(), &ImagePreview::areaCreated, this, &Dialog::slotNewArea); // Disable so no button accept return (which would break with the line edits) m_revertBut->setAutoDefault(false); m_okBut->setAutoDefault(false); m_continueLaterBut->setAutoDefault(false); cancelBut->setAutoDefault(false); m_clearBut->setAutoDefault(false); optionsBut->setAutoDefault(false); m_dockWindowCleanState = m_dockWindow->saveState(); loadWindowLayout(); m_current = -1; setGeometry(Settings::SettingsData::instance()->windowGeometry(Settings::AnnotationDialog)); setupActions(); shortCutManager.setupShortCuts(); // WARNING layout->addWidget(buttonBox) must be last item in layout layout->addWidget(buttonBox); } QDockWidget *AnnotationDialog::Dialog::createDock(const QString &title, const QString &name, Qt::DockWidgetArea location, QWidget *widget) { QDockWidget *dock = new QDockWidget(title); KAcceleratorManager::setNoAccel(dock); dock->setObjectName(name); dock->setAllowedAreas(Qt::AllDockWidgetAreas); dock->setWidget(widget); m_dockWindow->addDockWidget(location, dock); m_dockWidgets.append(dock); return dock; } QWidget *AnnotationDialog::Dialog::createDateWidget(ShortCutManager &shortCutManager) { QWidget *top = new QWidget; QVBoxLayout *lay2 = new QVBoxLayout(top); // Image Label QHBoxLayout *lay3 = new QHBoxLayout; lay2->addLayout(lay3); QLabel *label = new QLabel(i18n("Label: ")); lay3->addWidget(label); m_imageLabel = new KLineEdit; m_imageLabel->setProperty("WantsFocus", true); m_imageLabel->setObjectName(i18n("Label")); lay3->addWidget(m_imageLabel); shortCutManager.addLabel(label); label->setBuddy(m_imageLabel); // Date QHBoxLayout *lay4 = new QHBoxLayout; lay2->addLayout(lay4); label = new QLabel(i18n("Date: ")); lay4->addWidget(label); m_startDate = new ::AnnotationDialog::DateEdit(true); lay4->addWidget(m_startDate, 1); connect(m_startDate, QOverload::of(&DateEdit::dateChanged), this, &Dialog::slotStartDateChanged); shortCutManager.addLabel(label); label->setBuddy(m_startDate); m_endDateLabel = new QLabel(QString::fromLatin1("-")); lay4->addWidget(m_endDateLabel); m_endDate = new ::AnnotationDialog::DateEdit(false); lay4->addWidget(m_endDate, 1); // Time m_timeLabel = new QLabel(i18n("Time: ")); lay4->addWidget(m_timeLabel); m_time = new QTimeEdit; lay4->addWidget(m_time); m_isFuzzyDate = new QCheckBox(i18n("Use Fuzzy Date")); m_isFuzzyDate->setWhatsThis(i18nc("@info", "In KPhotoAlbum, images can either have an exact date and time" ", or a fuzzy date which happened any time during" " a specified time interval. Images produced by digital cameras" " do normally have an exact date." "If you don't know exactly when a photo was taken" " (e.g. if the photo comes from an analog camera), then you should set" " Use Fuzzy Date.")); m_isFuzzyDate->setToolTip(m_isFuzzyDate->whatsThis()); lay4->addWidget(m_isFuzzyDate); lay4->addStretch(1); connect(m_isFuzzyDate, &QCheckBox::stateChanged, this, &Dialog::slotSetFuzzyDate); QHBoxLayout *lay8 = new QHBoxLayout; lay2->addLayout(lay8); m_megapixelLabel = new QLabel(i18n("Minimum megapixels:")); lay8->addWidget(m_megapixelLabel); m_megapixel = new QSpinBox; m_megapixel->setRange(0, 99); m_megapixel->setSingleStep(1); m_megapixelLabel->setBuddy(m_megapixel); lay8->addWidget(m_megapixel); lay8->addStretch(1); m_max_megapixelLabel = new QLabel(i18n("Maximum megapixels:")); lay8->addWidget(m_max_megapixelLabel); m_max_megapixel = new QSpinBox; m_max_megapixel->setRange(0, 99); m_max_megapixel->setSingleStep(1); m_max_megapixelLabel->setBuddy(m_max_megapixel); lay8->addWidget(m_max_megapixel); lay8->addStretch(1); QHBoxLayout *lay9 = new QHBoxLayout; lay2->addLayout(lay9); label = new QLabel(i18n("Rating:")); lay9->addWidget(label); m_rating = new KRatingWidget; m_rating->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); lay9->addWidget(m_rating, 0, Qt::AlignCenter); connect(m_rating, static_cast(&KRatingWidget::ratingChanged), this, &Dialog::slotRatingChanged); m_ratingSearchLabel = new QLabel(i18n("Rating search mode:")); lay9->addWidget(m_ratingSearchLabel); m_ratingSearchMode = new KComboBox(lay9); m_ratingSearchMode->addItems(QStringList() << i18n("==") << i18n(">=") << i18n("<=") << i18n("!=")); m_ratingSearchLabel->setBuddy(m_ratingSearchMode); lay9->addWidget(m_ratingSearchMode); // File name search pattern QHBoxLayout *lay10 = new QHBoxLayout; lay2->addLayout(lay10); m_imageFilePatternLabel = new QLabel(i18n("File Name Pattern: ")); lay10->addWidget(m_imageFilePatternLabel); m_imageFilePattern = new KLineEdit; m_imageFilePattern->setObjectName(i18n("File Name Pattern")); lay10->addWidget(m_imageFilePattern); shortCutManager.addLabel(m_imageFilePatternLabel); m_imageFilePatternLabel->setBuddy(m_imageFilePattern); m_searchRAW = new QCheckBox(i18n("Search only for RAW files")); lay2->addWidget(m_searchRAW); lay9->addStretch(1); lay2->addStretch(1); return top; } QWidget *AnnotationDialog::Dialog::createPreviewWidget() { m_preview = new ImagePreviewWidget(m_actions); connect(m_preview, &ImagePreviewWidget::togglePreview, this, &Dialog::togglePreview); return m_preview; } void AnnotationDialog::Dialog::slotRevert() { if (m_setup == InputSingleImageConfigMode) load(); } void AnnotationDialog::Dialog::slotIndexChanged(int index) { if (m_setup != InputSingleImageConfigMode) return; if (m_current >= 0) writeToInfo(); m_current = index; load(); } void AnnotationDialog::Dialog::doneTagging() { saveAndClose(); if (DB::ImageDB::instance()->untaggedCategoryFeatureConfigured()) { for (DB::ImageInfoListIterator it = m_origList.begin(); it != m_origList.end(); ++it) { (*it)->removeCategoryInfo(Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag()); } } } /* * Copy tags (only tags/categories, not description/label/...) from previous image to the currently showed one */ void AnnotationDialog::Dialog::slotCopyPrevious() { if (m_setup != InputSingleImageConfigMode) return; if (m_current < 1) return; // FIXME: it would be better to compute the "previous image" in a better way, but let's stick with this for now... DB::ImageInfo &old_info = m_editList[m_current - 1]; m_positionableTagCandidates.clear(); m_lastSelectedPositionableTag.first = QString(); m_lastSelectedPositionableTag.second = QString(); for (ListSelect *ls : qAsConst(m_optionList)) { ls->setSelection(old_info.itemsOfCategory(ls->category())); // Also set all positionable tag candidates if (ls->positionable()) { const QString category = ls->category(); const QSet selectedTags = old_info.itemsOfCategory(category); const QSet positionedTagSet = positionedTags(category); // Add the tag to the positionable candiate list, if no area is already associated with it for (const auto &tag : selectedTags) { if (!positionedTagSet.contains(tag)) { addTagToCandidateList(category, tag); } } // Check all areas for a linked tag in this category that is probably not selected anymore const auto allAreas = areas(); for (ResizableFrame *area : allAreas) { QPair tagData = area->tagData(); if (tagData.first == category) { if (!selectedTags.contains(tagData.second)) { // The linked tag is not selected anymore, so remove it area->removeTagData(); } } } } } } void AnnotationDialog::Dialog::load() { // Remove all areas tidyAreas(); // No areas have been changed m_areasChanged = false; // Empty the positionable tag candidate list and the last selected positionable tag m_positionableTagCandidates.clear(); m_lastSelectedPositionableTag = QPair(); DB::ImageInfo &info = m_editList[m_current]; m_startDate->setDate(info.date().start().date()); if (info.date().hasValidTime()) { m_time->show(); m_time->setTime(info.date().start().time()); m_isFuzzyDate->setChecked(false); } else { m_time->hide(); m_isFuzzyDate->setChecked(true); } if (info.date().start().date() == info.date().end().date()) m_endDate->setDate(QDate()); else m_endDate->setDate(info.date().end().date()); m_imageLabel->setText(info.label()); m_description->setPlainText(info.description()); if (m_setup == InputSingleImageConfigMode) m_rating->setRating(qMax(static_cast(0), info.rating())); m_ratingChanged = false; // A category areas have been linked against could have been deleted // or un-marked as positionable in the meantime, so ... QMap categoryIsPositionable; QList positionableCategories; for (ListSelect *ls : qAsConst(m_optionList)) { ls->setSelection(info.itemsOfCategory(ls->category())); ls->rePopulate(); // Get all selected positionable tags and add them to the candidate list if (ls->positionable()) { const QSet selectedTags = ls->itemsOn(); for (const QString &tagName : selectedTags) { addTagToCandidateList(ls->category(), tagName); } } // ... create a list of all categories and their positionability ... categoryIsPositionable[ls->category()] = ls->positionable(); if (ls->positionable()) { positionableCategories << ls->category(); } } // Create all tagged areas DB::TaggedAreas taggedAreas = info.taggedAreas(); DB::TaggedAreasIterator areasInCategory(taggedAreas); while (areasInCategory.hasNext()) { areasInCategory.next(); QString category = areasInCategory.key(); // ... and check if the respective category is actually there yet and still positionable // (operator[] will insert an empty item if the category has been deleted // and is thus missing in the QMap, but the respective key won't be true) if (categoryIsPositionable[category]) { DB::PositionTagsIterator areaData(areasInCategory.value()); while (areaData.hasNext()) { areaData.next(); QString tag = areaData.key(); // Be sure that the corresponding tag is still checked. The category could have // been un-marked as positionable in the meantime and the tag could have been // deselected, without triggering positionableTagDeselected and the area thus // still remaining. If the category is then re-marked as positionable, the area would // show up without the tag being selected. if (m_listSelectList[category]->tagIsChecked(tag)) { m_preview->preview()->createTaggedArea(category, tag, areaData.value(), m_preview->showAreas()); } } } } if (m_setup == InputSingleImageConfigMode) { setWindowTitle(i18nc("@title:window image %1 of %2 images", "Annotations (%1/%2)", m_current + 1, m_origList.count())); m_preview->canCreateAreas( m_setup == InputSingleImageConfigMode && !info.isVideo() && m_positionableCategories); #ifdef HAVE_MARBLE updateMapForCurrentImage(); #endif } m_preview->updatePositionableCategories(positionableCategories); } void AnnotationDialog::Dialog::writeToInfo() { for (ListSelect *ls : qAsConst(m_optionList)) { ls->slotReturn(); } DB::ImageInfo &info = m_editList[m_current]; if (!info.size().isValid()) { // The actual image size has been fetched by ImagePreview, so we can add it to // the database silenty, so that it's saved if the database will be saved. info.setSize(m_preview->preview()->getActualImageSize()); } if (m_time->isHidden()) { if (m_endDate->date().isValid()) info.setDate(DB::ImageDate(QDateTime(m_startDate->date(), QTime(0, 0, 0)), QDateTime(m_endDate->date(), QTime(23, 59, 59)))); else info.setDate(DB::ImageDate(QDateTime(m_startDate->date(), QTime(0, 0, 0)), QDateTime(m_startDate->date(), QTime(23, 59, 59)))); } else info.setDate(DB::ImageDate(QDateTime(m_startDate->date(), m_time->time()))); // Generate a list of all tagged areas DB::TaggedAreas areas = taggedAreas(); info.setLabel(m_imageLabel->text()); info.setDescription(m_description->toPlainText()); for (const ListSelect *ls : qAsConst(m_optionList)) { info.setCategoryInfo(ls->category(), ls->itemsOn()); if (ls->positionable()) { info.setPositionedTags(ls->category(), areas[ls->category()]); } } if (m_ratingChanged) { info.setRating(m_rating->rating()); m_ratingChanged = false; } } void AnnotationDialog::Dialog::ShowHideSearch(bool show) { m_megapixel->setVisible(show); m_megapixelLabel->setVisible(show); m_max_megapixel->setVisible(show); m_max_megapixelLabel->setVisible(show); m_searchRAW->setVisible(show); m_imageFilePatternLabel->setVisible(show); m_imageFilePattern->setVisible(show); m_isFuzzyDate->setChecked(show); m_isFuzzyDate->setVisible(!show); slotSetFuzzyDate(); m_ratingSearchMode->setVisible(show); m_ratingSearchLabel->setVisible(show); } QList AnnotationDialog::Dialog::areas() const { return m_preview->preview()->findChildren(); } DB::TaggedAreas AnnotationDialog::Dialog::taggedAreas() const { DB::TaggedAreas taggedAreas; const auto allAreas = areas(); for (ResizableFrame *area : allAreas) { QPair tagData = area->tagData(); if (!tagData.first.isEmpty()) { taggedAreas[tagData.first][tagData.second] = area->actualCoordinates(); } } return taggedAreas; } int AnnotationDialog::Dialog::configure(DB::ImageInfoList list, bool oneAtATime) { ShowHideSearch(false); if (DB::ImageDB::instance()->untaggedCategoryFeatureConfigured()) { DB::ImageDB::instance()->categoryCollection()->categoryForName(Settings::SettingsData::instance()->untaggedCategory())->addItem(Settings::SettingsData::instance()->untaggedTag()); } if (oneAtATime) { m_setup = InputSingleImageConfigMode; } else { m_setup = InputMultiImageConfigMode; // Hide the default positionable category selector m_preview->updatePositionableCategories(); } #ifdef HAVE_MARBLE m_mapIsPopulated = false; m_annotationMap->clear(); #endif m_origList = list; m_editList.clear(); for (DB::ImageInfoListConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { m_editList.append(*(*it)); } setup(); if (oneAtATime) { m_current = 0; m_preview->configure(&m_editList, true); load(); } else { m_preview->configure(&m_editList, false); m_preview->canCreateAreas(false); m_startDate->setDate(QDate()); m_endDate->setDate(QDate()); m_time->hide(); m_rating->setRating(0); m_ratingChanged = false; m_areasChanged = false; for (ListSelect *ls : qAsConst(m_optionList)) { setUpCategoryListBoxForMultiImageSelection(ls, list); } m_imageLabel->setText(QString()); m_imageFilePattern->setText(QString()); m_firstDescription = m_editList[0].description(); const bool allTextEqual = std::all_of(m_editList.begin(), m_editList.end(), [=](const DB::ImageInfo &item) -> bool { return item.description() == m_firstDescription; }); if (!allTextEqual) m_firstDescription = m_conflictText; m_description->setPlainText(m_firstDescription); } showHelpDialog(oneAtATime ? InputSingleImageConfigMode : InputMultiImageConfigMode); return exec(); } DB::ImageSearchInfo AnnotationDialog::Dialog::search(DB::ImageSearchInfo *search) { ShowHideSearch(true); #ifdef HAVE_MARBLE m_mapIsPopulated = false; m_annotationMap->clear(); #endif m_setup = SearchMode; if (search) m_oldSearch = *search; setup(); m_preview->setImage(QStandardPaths::locate(QStandardPaths::DataLocation, QString::fromLatin1("pics/search.jpg"))); m_ratingChanged = false; showHelpDialog(SearchMode); int ok = exec(); if (ok == QDialog::Accepted) { const QDate start = m_startDate->date(); const QDate end = m_endDate->date(); m_oldSearch = DB::ImageSearchInfo(DB::ImageDate(start, end), m_imageLabel->text(), m_description->toPlainText(), m_imageFilePattern->text()); for (const ListSelect *ls : qAsConst(m_optionList)) { m_oldSearch.setCategoryMatchText(ls->category(), ls->text()); } //FIXME: for the user to search for 0-rated images, he must first change the rating to anything > 0 //then change back to 0 . if (m_ratingChanged) m_oldSearch.setRating(m_rating->rating()); m_ratingChanged = false; m_oldSearch.setSearchMode(m_ratingSearchMode->currentIndex()); m_oldSearch.setMegaPixel(m_megapixel->value()); m_oldSearch.setMaxMegaPixel(m_max_megapixel->value()); m_oldSearch.setSearchRAW(m_searchRAW->isChecked()); #ifdef HAVE_MARBLE const Map::GeoCoordinates::LatLonBox regionSelection = m_annotationMap->getRegionSelection(); m_oldSearch.setRegionSelection(regionSelection); #endif return m_oldSearch; } else return DB::ImageSearchInfo(); } void AnnotationDialog::Dialog::setup() { // Repopulate the listboxes in case data has changed // An group might for example have been renamed. for (ListSelect *ls : qAsConst(m_optionList)) { ls->populate(); } if (m_setup == SearchMode) { KGuiItem::assign(m_okBut, KGuiItem(i18nc("@action:button", "&Search"), QString::fromLatin1("find"))); m_continueLaterBut->hide(); m_revertBut->hide(); m_clearBut->show(); m_preview->setSearchMode(true); setWindowTitle(i18nc("@title:window title of the 'find images' window", "Search")); loadInfo(m_oldSearch); } else { m_okBut->setText(i18n("Done")); m_continueLaterBut->show(); m_revertBut->setEnabled(m_setup == InputSingleImageConfigMode); m_clearBut->hide(); m_revertBut->show(); m_preview->setSearchMode(false); m_preview->setToggleFullscreenPreviewEnabled(m_setup == InputSingleImageConfigMode); setWindowTitle(i18nc("@title:window", "Annotations")); } for (ListSelect *ls : qAsConst(m_optionList)) { ls->setMode(m_setup); } } void AnnotationDialog::Dialog::slotClear() { loadInfo(DB::ImageSearchInfo()); } void AnnotationDialog::Dialog::loadInfo(const DB::ImageSearchInfo &info) { m_startDate->setDate(info.date().start().date()); m_endDate->setDate(info.date().end().date()); for (ListSelect *ls : qAsConst(m_optionList)) { ls->setText(info.categoryMatchText(ls->category())); } m_imageLabel->setText(info.label()); m_description->setText(info.description()); } void AnnotationDialog::Dialog::slotOptions() { // create menu entries for dock windows QMenu *menu = new QMenu(this); QMenu *dockMenu = m_dockWindow->createPopupMenu(); menu->addMenu(dockMenu) ->setText(i18n("Configure Window Layout...")); QAction *saveCurrent = dockMenu->addAction(i18n("Save Current Window Setup")); QAction *reset = dockMenu->addAction(i18n("Reset layout")); // create SortType entries menu->addSeparator(); QActionGroup *sortTypes = new QActionGroup(menu); QAction *alphaTreeSort = new QAction( smallIcon(QString::fromLatin1("view-list-tree")), i18n("Sort Alphabetically (Tree)"), sortTypes); QAction *alphaFlatSort = new QAction( smallIcon(QString::fromLatin1("draw-text")), i18n("Sort Alphabetically (Flat)"), sortTypes); QAction *dateSort = new QAction( smallIcon(QString::fromLatin1("x-office-calendar")), i18n("Sort by Date"), sortTypes); alphaTreeSort->setCheckable(true); alphaFlatSort->setCheckable(true); dateSort->setCheckable(true); alphaTreeSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree); alphaFlatSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaFlat); dateSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortLastUse); menu->addActions(sortTypes->actions()); connect(dateSort, &QAction::triggered, m_optionList.at(0), &ListSelect::slotSortDate); connect(alphaTreeSort, &QAction::triggered, m_optionList.at(0), &ListSelect::slotSortAlphaTree); connect(alphaFlatSort, &QAction::triggered, m_optionList.at(0), &ListSelect::slotSortAlphaFlat); // create MatchType entries menu->addSeparator(); QActionGroup *matchTypes = new QActionGroup(menu); QAction *matchFromBeginning = new QAction(i18n("Match Tags from the First Character"), matchTypes); QAction *matchFromWordStart = new QAction(i18n("Match Tags from Word Boundaries"), matchTypes); QAction *matchAnywhere = new QAction(i18n("Match Tags Anywhere"), matchTypes); matchFromBeginning->setCheckable(true); matchFromWordStart->setCheckable(true); matchAnywhere->setCheckable(true); // TODO add StatusTip text? // set current state: matchFromBeginning->setChecked(Settings::SettingsData::instance()->matchType() == AnnotationDialog::MatchFromBeginning); matchFromWordStart->setChecked(Settings::SettingsData::instance()->matchType() == AnnotationDialog::MatchFromWordStart); matchAnywhere->setChecked(Settings::SettingsData::instance()->matchType() == AnnotationDialog::MatchAnywhere); // add MatchType actions to menu: menu->addActions(matchTypes->actions()); // create toggle-show-selected entry# if (m_setup != SearchMode) { menu->addSeparator(); QAction *showSelectedOnly = new QAction( smallIcon(QString::fromLatin1("view-filter")), i18n("Show Only Selected Ctrl+S"), menu); showSelectedOnly->setCheckable(true); showSelectedOnly->setChecked(ShowSelectionOnlyManager::instance().selectionIsLimited()); menu->addAction(showSelectedOnly); connect(showSelectedOnly, &QAction::triggered, &ShowSelectionOnlyManager::instance(), &ShowSelectionOnlyManager::toggle); } // execute menu & handle response: QAction *res = menu->exec(QCursor::pos()); if (res == saveCurrent) slotSaveWindowSetup(); else if (res == reset) slotResetLayout(); else if (res == matchFromBeginning) Settings::SettingsData::instance()->setMatchType(AnnotationDialog::MatchFromBeginning); else if (res == matchFromWordStart) Settings::SettingsData::instance()->setMatchType(AnnotationDialog::MatchFromWordStart); else if (res == matchAnywhere) Settings::SettingsData::instance()->setMatchType(AnnotationDialog::MatchAnywhere); } int AnnotationDialog::Dialog::exec() { m_stack->setCurrentWidget(m_dockWindow); showTornOfWindows(); this->setFocus(); // Set temporary focus before show() is called so that extra cursor is not shown on any "random" input widget show(); // We need to call show before we call setupFocus() otherwise the widget will not yet all have been moved in place. setupFocus(); const int ret = QDialog::exec(); hideTornOfWindows(); return ret; } void AnnotationDialog::Dialog::slotSaveWindowSetup() { const QByteArray data = m_dockWindow->saveState(); QFile file(QString::fromLatin1("%1/layout.dat").arg(Settings::SettingsData::instance()->imageDirectory())); if (!file.open(QIODevice::WriteOnly)) { KMessageBox::sorry(this, i18n("

Could not save the window layout.

" "File %1 could not be opened because of the following error: %2", file.fileName(), file.errorString())); } else if (!(file.write(data) && file.flush())) { KMessageBox::sorry(this, i18n("

Could not save the window layout.

" "File %1 could not be written because of the following error: %2", file.fileName(), file.errorString())); } file.close(); } void AnnotationDialog::Dialog::closeEvent(QCloseEvent *e) { e->ignore(); reject(); } void AnnotationDialog::Dialog::hideTornOfWindows() { for (QDockWidget *dock : m_dockWidgets) { if (dock->isFloating()) { qCDebug(AnnotationDialogLog) << "Hiding dock: " << dock->objectName(); dock->hide(); } } } void AnnotationDialog::Dialog::showTornOfWindows() { for (QDockWidget *dock : m_dockWidgets) { if (dock->isFloating()) { qCDebug(AnnotationDialogLog) << "Showing dock: " << dock->objectName(); dock->show(); } } } AnnotationDialog::ListSelect *AnnotationDialog::Dialog::createListSel(const DB::CategoryPtr &category) { ListSelect *sel = new ListSelect(category, m_dockWindow); m_optionList.append(sel); connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::itemRemoved, this, &Dialog::slotDeleteOption); connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::itemRenamed, this, &Dialog::slotRenameOption); return sel; } void AnnotationDialog::Dialog::slotDeleteOption(DB::Category *category, const QString &value) { for (QList::Iterator it = m_editList.begin(); it != m_editList.end(); ++it) { (*it).removeCategoryInfo(category->name(), value); } } void AnnotationDialog::Dialog::slotRenameOption(DB::Category *category, const QString &oldValue, const QString &newValue) { for (QList::Iterator it = m_editList.begin(); it != m_editList.end(); ++it) { (*it).renameItem(category->name(), oldValue, newValue); } } void AnnotationDialog::Dialog::reject() { if (m_stack->currentWidget() == m_fullScreenPreview) { togglePreview(); return; } m_fullScreenPreview->stopPlayback(); if (hasChanges()) { int code = KMessageBox::questionYesNo(this, i18n("

Some changes are made to annotations. Do you really want to cancel all recent changes for each affected file?

")); if (code == KMessageBox::No) return; } closeDialog(); } void AnnotationDialog::Dialog::closeDialog() { tidyAreas(); m_accept = QDialog::Rejected; QDialog::reject(); } StringSet AnnotationDialog::Dialog::changedOptions(const ListSelect *ls) { StringSet on, partialOn, off, changes; std::tie(on, partialOn, off) = selectionForMultiSelect(ls, m_origList); changes += (ls->itemsOn() - on); changes += (on - ls->itemsOn()); changes += (ls->itemsOff() - off); changes += (off - ls->itemsOff()); return changes; } bool AnnotationDialog::Dialog::hasChanges() { if (m_setup == InputSingleImageConfigMode) { writeToInfo(); if (m_areasChanged) return true; for (int i = 0; i < m_editList.count(); ++i) { if (*(m_origList[i]) != m_editList[i]) return true; } } else if (m_setup == InputMultiImageConfigMode) { if ((!m_startDate->date().isNull()) || (!m_endDate->date().isNull()) || (!m_imageLabel->text().isEmpty()) || (m_description->toPlainText() != m_firstDescription) || m_ratingChanged) return true; for (const ListSelect *ls : qAsConst(m_optionList)) { if (!(changedOptions(ls).isEmpty())) return true; } } return false; } void AnnotationDialog::Dialog::rotate(int angle) { if (m_setup == InputMultiImageConfigMode) { // In doneTagging the preview will be queried for its angle. } else { DB::ImageInfo &info = m_editList[m_current]; info.rotate(angle, DB::RotateImageInfoOnly); emit imageRotated(info.fileName()); } } void AnnotationDialog::Dialog::slotSetFuzzyDate() { if (m_isFuzzyDate->isChecked()) { m_time->hide(); m_timeLabel->hide(); m_endDate->show(); m_endDateLabel->show(); } else { m_time->show(); m_timeLabel->show(); m_endDate->hide(); m_endDateLabel->hide(); } } void AnnotationDialog::Dialog::slotDeleteImage() { // CTRL+Del is a common key combination when editing text // TODO: The word right of cursor should be deleted as expected also in date and category fields if (m_setup == SearchMode) return; if (m_setup == InputMultiImageConfigMode) //TODO: probably delete here should mean remove from selection return; DB::ImageInfoPtr info = m_origList[m_current]; m_origList.remove(info); m_editList.removeAll(m_editList.at(m_current)); MainWindow::DirtyIndicator::markDirty(); if (m_origList.count() == 0) { doneTagging(); return; } if (m_current == (int)m_origList.count()) // we deleted the last image m_current--; load(); } void AnnotationDialog::Dialog::showHelpDialog(UsageMode type) { QString doNotShowKey; QString txt; if (type == SearchMode) { doNotShowKey = QString::fromLatin1("image_config_search_show_help"); txt = i18n("

You have just opened the advanced search dialog; to get the most out of it, " "it is suggested that you read the section in the manual on " "advanced searching.

" "

This dialog is also used for typing in information about images; you can find " "extra tips on its usage by reading about " "typing in.

"); } else { doNotShowKey = QString::fromLatin1("image_config_typein_show_help"); txt = i18n("

You have just opened one of the most important windows in KPhotoAlbum; " "it contains lots of functionality which has been optimized for fast usage.

" "

It is strongly recommended that you take 5 minutes to read the " "documentation for this " "dialog

"); } KMessageBox::information(this, txt, QString(), doNotShowKey, KMessageBox::AllowLink); } void AnnotationDialog::Dialog::resizeEvent(QResizeEvent *) { Settings::SettingsData::instance()->setWindowGeometry(Settings::AnnotationDialog, geometry()); } void AnnotationDialog::Dialog::moveEvent(QMoveEvent *) { Settings::SettingsData::instance()->setWindowGeometry(Settings::AnnotationDialog, geometry()); } void AnnotationDialog::Dialog::setupFocus() { QList list = findChildren(); QList orderedList; // Iterate through all widgets in our dialog. for (QObject *obj : list) { QWidget *current = static_cast(obj); if (!current->property("WantsFocus").isValid() || !current->isVisible()) continue; int cx = current->mapToGlobal(QPoint(0, 0)).x(); int cy = current->mapToGlobal(QPoint(0, 0)).y(); bool inserted = false; // Iterate through the ordered list of widgets, and insert the current one, so it is in the right position in the tab chain. for (QList::iterator orderedIt = orderedList.begin(); orderedIt != orderedList.end(); ++orderedIt) { const QWidget *w = *orderedIt; int wx = w->mapToGlobal(QPoint(0, 0)).x(); int wy = w->mapToGlobal(QPoint(0, 0)).y(); if (wy > cy || (wy == cy && wx >= cx)) { orderedList.insert(orderedIt, current); inserted = true; break; } } if (!inserted) orderedList.append(current); } // now setup tab order. QWidget *prev = nullptr; QWidget *first = nullptr; for (QWidget *widget : qAsConst(orderedList)) { if (prev) { setTabOrder(prev, widget); } else { first = widget; } prev = widget; } if (first) { setTabOrder(prev, first); } // Finally set focus on the first list select for (QWidget *widget : qAsConst(orderedList)) { if (widget->property("FocusCandidate").isValid() && widget->isVisible()) { widget->setFocus(); break; } } } void AnnotationDialog::Dialog::slotResetLayout() { m_dockWindow->restoreState(m_dockWindowCleanState); } void AnnotationDialog::Dialog::slotStartDateChanged(const DB::ImageDate &date) { if (date.start() == date.end()) m_endDate->setDate(QDate()); else m_endDate->setDate(date.end().date()); } void AnnotationDialog::Dialog::loadWindowLayout() { QString fileName = QString::fromLatin1("%1/layout.dat").arg(Settings::SettingsData::instance()->imageDirectory()); if (!QFileInfo(fileName).exists()) { // create default layout // label/date/rating in a visual block with description: m_dockWindow->splitDockWidget(m_generalDock, m_descriptionDock, Qt::Vertical); // more space for description: m_dockWindow->resizeDocks({ m_generalDock, m_descriptionDock }, { 60, 100 }, Qt::Vertical); // more space for preview: m_dockWindow->resizeDocks({ m_generalDock, m_descriptionDock, m_previewDock }, { 200, 200, 800 }, Qt::Horizontal); #ifdef HAVE_MARBLE // group the map with the preview m_dockWindow->tabifyDockWidget(m_previewDock, m_mapDock); // make sure the preview tab is active: m_previewDock->raise(); #endif return; } QFile file(fileName); file.open(QIODevice::ReadOnly); QByteArray data = file.readAll(); m_dockWindow->restoreState(data); } void AnnotationDialog::Dialog::setupActions() { QAction *action = nullptr; action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-alphatree"), m_optionList.at(0), &ListSelect::slotSortAlphaTree); action->setText(i18n("Sort Alphabetically (Tree)")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_F4); action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-alphaflat"), m_optionList.at(0), &ListSelect::slotSortAlphaFlat); action->setText(i18n("Sort Alphabetically (Flat)")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-MRU"), m_optionList.at(0), &ListSelect::slotSortDate); action->setText(i18n("Sort Most Recently Used")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-sort"), m_optionList.at(0), &ListSelect::toggleSortType); action->setText(i18n("Toggle Sorting")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_T); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-showing-selected-only"), &ShowSelectionOnlyManager::instance(), &ShowSelectionOnlyManager::toggle); action->setText(i18n("Toggle Showing Selected Items Only")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_S); action = m_actions->addAction(QString::fromLatin1("annotationdialog-next-image"), m_preview, &ImagePreviewWidget::slotNext); action->setText(i18n("Annotate Next")); m_actions->setDefaultShortcut(action, Qt::Key_PageDown); action = m_actions->addAction(QString::fromLatin1("annotationdialog-prev-image"), m_preview, &ImagePreviewWidget::slotPrev); action->setText(i18n("Annotate Previous")); m_actions->setDefaultShortcut(action, Qt::Key_PageUp); action = m_actions->addAction(QString::fromLatin1("annotationdialog-OK-dialog"), this, &Dialog::doneTagging); action->setText(i18n("OK dialog")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Return); action = m_actions->addAction(QString::fromLatin1("annotationdialog-delete-image"), this, &Dialog::slotDeleteImage); action->setText(i18n("Delete")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete); action = m_actions->addAction(QString::fromLatin1("annotationdialog-copy-previous"), this, &Dialog::slotCopyPrevious); action->setText(i18n("Copy tags from previous image")); m_actions->setDefaultShortcut(action, Qt::ALT + Qt::Key_Insert); action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-left"), m_preview, &ImagePreviewWidget::rotateLeft); action->setText(i18n("Rotate counterclockwise")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-right"), m_preview, &ImagePreviewWidget::rotateRight); action->setText(i18n("Rotate clockwise")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-viewer"), this, &Dialog::togglePreview); action->setText(i18n("Toggle fullscreen preview")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Space); const auto allActions = m_actions->actions(); for (QAction *action : allActions) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } // the annotation dialog is created when it's first used; // therefore, its actions are registered well after the MainWindow sets up its actionCollection, // and it has to read the shortcuts here, after they are set up: m_actions->readSettings(); } KActionCollection *AnnotationDialog::Dialog::actions() { return m_actions; } void AnnotationDialog::Dialog::setUpCategoryListBoxForMultiImageSelection(ListSelect *listSel, const DB::ImageInfoList &images) { StringSet on, partialOn, off; std::tie(on, partialOn, off) = selectionForMultiSelect(listSel, images); listSel->setSelection(on, partialOn); } std::tuple AnnotationDialog::Dialog::selectionForMultiSelect(const ListSelect *listSel, const DB::ImageInfoList &images) { const QString category = listSel->category(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const auto itemsInclCategories = DB::ImageDB::instance()->categoryCollection()->categoryForName(category)->itemsInclCategories(); + const StringSet allItems(itemsInclCategories.begin(), itemsInclCategories.end()); +#else const StringSet allItems = DB::ImageDB::instance()->categoryCollection()->categoryForName(category)->itemsInclCategories().toSet(); +#endif StringSet itemsOnSomeImages; StringSet itemsOnAllImages; bool firstImage = true; for (DB::ImageInfoList::ConstIterator imageIt = images.begin(); imageIt != images.end(); ++imageIt) { const StringSet itemsOnThisImage = (*imageIt)->itemsOfCategory(category); if (firstImage) { itemsOnAllImages = itemsOnThisImage; firstImage = false; } else { for (const QString &item : itemsOnThisImage) { if (!itemsOnAllImages.contains(item) && !itemsOnSomeImages.contains(item)) { itemsOnSomeImages += item; } } itemsOnAllImages = itemsOnAllImages.intersect(itemsOnThisImage); } } const StringSet itemsOnNoImages = allItems - itemsOnSomeImages - itemsOnAllImages; return std::make_tuple(itemsOnAllImages, itemsOnSomeImages, itemsOnNoImages); } void AnnotationDialog::Dialog::slotRatingChanged(unsigned int) { m_ratingChanged = true; } void AnnotationDialog::Dialog::continueLater() { saveAndClose(); } void AnnotationDialog::Dialog::saveAndClose() { tidyAreas(); m_fullScreenPreview->stopPlayback(); if (m_origList.isEmpty()) { // all images are deleted. QDialog::accept(); return; } // I need to check for the changes first, as the case for m_setup // == InputSingleImageConfigMode, saves to the m_origList, and we // can thus not check for changes anymore bool anyChanges = hasChanges(); if (m_setup == InputSingleImageConfigMode) { writeToInfo(); for (int i = 0; i < m_editList.count(); ++i) { *(m_origList[i]) = m_editList[i]; } } else if (m_setup == InputMultiImageConfigMode) { for (ListSelect *ls : qAsConst(m_optionList)) { ls->slotReturn(); } for (const ListSelect *ls : qAsConst(m_optionList)) { StringSet changes = changedOptions(ls); if (!(changes.isEmpty())) { anyChanges = true; StringSet newItemsOn = ls->itemsOn() & changes; StringSet newItemsOff = ls->itemsOff() & changes; for (DB::ImageInfoListConstIterator it = m_origList.constBegin(); it != m_origList.constEnd(); ++it) { DB::ImageInfoPtr info = *it; info->addCategoryInfo(ls->category(), newItemsOn); info->removeCategoryInfo(ls->category(), newItemsOff); } } } for (DB::ImageInfoListConstIterator it = m_origList.constBegin(); it != m_origList.constEnd(); ++it) { DB::ImageInfoPtr info = *it; if (!m_startDate->date().isNull()) info->setDate(DB::ImageDate(m_startDate->date(), m_endDate->date(), m_time->time())); if (!m_imageLabel->text().isEmpty()) { info->setLabel(m_imageLabel->text()); } if (!m_description->toPlainText().isEmpty() && m_description->toPlainText().compare(m_conflictText)) { info->setDescription(m_description->toPlainText()); } if (m_ratingChanged) { info->setRating(m_rating->rating()); } } m_ratingChanged = false; } m_accept = QDialog::Accepted; if (anyChanges) { MainWindow::DirtyIndicator::markDirty(); } QDialog::accept(); } AnnotationDialog::Dialog::~Dialog() { qDeleteAll(m_optionList); m_optionList.clear(); } void AnnotationDialog::Dialog::togglePreview() { if (m_setup == InputSingleImageConfigMode) { if (m_stack->currentWidget() == m_fullScreenPreview) { m_stack->setCurrentWidget(m_dockWindow); m_fullScreenPreview->stopPlayback(); } else { DB::ImageInfo currentInfo = m_editList[m_current]; m_stack->setCurrentWidget(m_fullScreenPreview); m_fullScreenPreview->load(DB::FileNameList() << currentInfo.fileName()); // compute altered tags by removing existing tags from full set: const DB::TaggedAreas existingAreas = currentInfo.taggedAreas(); DB::TaggedAreas alteredAreas = taggedAreas(); for (auto catIt = existingAreas.constBegin(); catIt != existingAreas.constEnd(); ++catIt) { const QString &categoryName = catIt.key(); const DB::PositionTags &tags = catIt.value(); for (auto tagIt = tags.cbegin(); tagIt != tags.constEnd(); ++tagIt) { const QString &tagName = tagIt.key(); const QRect &area = tagIt.value(); // remove unchanged areas if (area == alteredAreas[categoryName][tagName]) { alteredAreas[categoryName].remove(tagName); if (alteredAreas[categoryName].empty()) alteredAreas.remove(categoryName); } } } m_fullScreenPreview->addAdditionalTaggedAreas(alteredAreas); } } } void AnnotationDialog::Dialog::tidyAreas() { // Remove all areas marked on the preview image const auto allAreas = areas(); for (ResizableFrame *area : allAreas) { area->markTidied(); area->deleteLater(); } } void AnnotationDialog::Dialog::slotNewArea(ResizableFrame *area) { area->setDialog(this); } void AnnotationDialog::Dialog::positionableTagSelected(QString category, QString tag) { // Be sure not to propose an already-associated tag QPair tagData = qMakePair(category, tag); const auto allAreas = areas(); for (ResizableFrame *area : allAreas) { if (area->tagData() == tagData) { return; } } // Set the selected tag as the last selected positionable tag m_lastSelectedPositionableTag = tagData; // Add the tag to the positionable tag candidate list addTagToCandidateList(category, tag); } void AnnotationDialog::Dialog::positionableTagDeselected(QString category, QString tag) { // Remove the tag from the candidate list removeTagFromCandidateList(category, tag); // Search for areas linked against the tag on this image if (m_setup == InputSingleImageConfigMode) { QPair deselectedTag = QPair(category, tag); const auto allAreas = areas(); for (ResizableFrame *area : allAreas) { if (area->tagData() == deselectedTag) { area->removeTagData(); m_areasChanged = true; // Only one area can be associated with the tag, so we can return here return; } } } // Removal of tagged areas in InputMultiImageConfigMode is done in DB::ImageInfo::removeCategoryInfo } void AnnotationDialog::Dialog::addTagToCandidateList(QString category, QString tag) { m_positionableTagCandidates << QPair(category, tag); } void AnnotationDialog::Dialog::removeTagFromCandidateList(QString category, QString tag) { // Is the deselected tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == tag) { m_lastSelectedPositionableTag = QPair(); } // Remove the tag from the candidate list m_positionableTagCandidates.removeAll(QPair(category, tag)); // When a positionable tag is entered via the AreaTagSelectDialog, it's added to this // list twice, so we use removeAll here to be sure to also wipe duplicate entries. } QPair AnnotationDialog::Dialog::lastSelectedPositionableTag() const { return m_lastSelectedPositionableTag; } QList> AnnotationDialog::Dialog::positionableTagCandidates() const { return m_positionableTagCandidates; } void AnnotationDialog::Dialog::slotShowAreas(bool showAreas) { const auto allAreas = areas(); for (ResizableFrame *area : allAreas) { area->setVisible(showAreas); } } void AnnotationDialog::Dialog::positionableTagRenamed(QString category, QString oldTag, QString newTag) { // Is the renamed tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == oldTag) { m_lastSelectedPositionableTag.second = newTag; } // Check the candidate list for the tag QPair oldTagData = QPair(category, oldTag); if (m_positionableTagCandidates.contains(oldTagData)) { // The tag is in the list, so update it m_positionableTagCandidates.removeAt(m_positionableTagCandidates.indexOf(oldTagData)); m_positionableTagCandidates << QPair(category, newTag); } // Check if an area on the current image contains the changed or proposed tag const auto allAreas = areas(); for (ResizableFrame *area : allAreas) { if (area->tagData() == oldTagData) { area->setTagData(category, newTag); } } } void AnnotationDialog::Dialog::descriptionPageUpDownPressed(QKeyEvent *event) { if (event->key() == Qt::Key_PageUp) { m_actions->action(QString::fromLatin1("annotationdialog-prev-image"))->trigger(); } else if (event->key() == Qt::Key_PageDown) { m_actions->action(QString::fromLatin1("annotationdialog-next-image"))->trigger(); } } void AnnotationDialog::Dialog::checkProposedTagData( QPair tagData, ResizableFrame *areaToExclude) const { const auto allAreas = areas(); for (ResizableFrame *area : allAreas) { if (area != areaToExclude && area->proposedTagData() == tagData && area->tagData().first.isEmpty()) { area->removeProposedTagData(); } } } void AnnotationDialog::Dialog::areaChanged() { m_areasChanged = true; } /** * @brief positionableTagValid checks whether a given tag can still be associated to an area. * This checks for empty and duplicate tags. * @return */ bool AnnotationDialog::Dialog::positionableTagAvailable(const QString &category, const QString &tag) const { if (category.isEmpty() || tag.isEmpty()) return false; // does any area already have that tag? const auto allAreas = areas(); for (const ResizableFrame *area : allAreas) { const auto tagData = area->tagData(); if (tagData.first == category && tagData.second == tag) return false; } return true; } /** * @brief Generates a set of positionable tags currently used on the image * @param category * @return */ QSet AnnotationDialog::Dialog::positionedTags(const QString &category) const { QSet tags; const auto allAreas = areas(); for (const ResizableFrame *area : allAreas) { const auto tagData = area->tagData(); if (tagData.first == category) tags += tagData.second; } return tags; } AnnotationDialog::ListSelect *AnnotationDialog::Dialog::listSelectForCategory(const QString &category) { return m_listSelectList.value(category, nullptr); } #ifdef HAVE_MARBLE void AnnotationDialog::Dialog::updateMapForCurrentImage() { if (m_setup != InputSingleImageConfigMode) { return; } // we can use the coordinates of the original images here, because the are never changed by the annotation dialog if (m_origList[m_current]->coordinates().hasCoordinates()) { m_annotationMap->setCenter(m_origList[m_current]); m_annotationMap->displayStatus(Map::MapStatus::ImageHasCoordinates); } else { m_annotationMap->displayStatus(Map::MapStatus::ImageHasNoCoordinates); } } void AnnotationDialog::Dialog::annotationMapVisibilityChanged(bool visible) { // This populates the map if it's added when the dialog is already open if (visible) { // when the map dockwidget is already visible on show(), the call to // annotationMapVisibilityChanged is executed in the GUI thread. // This ensures that populateMap() doesn't block the GUI in this case: QTimer::singleShot(0, this, &Dialog::populateMap); } else { m_cancelMapLoading = true; } } void AnnotationDialog::Dialog::populateMap() { // populateMap is called every time the map widget gets visible if (m_mapIsPopulated) { return; } m_annotationMap->displayStatus(Map::MapStatus::Loading); m_cancelMapLoading = false; m_mapLoadingProgress->setMaximum(m_origList.count()); m_mapLoadingProgress->show(); m_cancelMapLoadingButton->show(); int processedImages = 0; int imagesWithCoordinates = 0; // we can use the coordinates of the original images here, because the are never changed by the annotation dialog for (const DB::ImageInfoPtr info : qAsConst(m_origList)) { processedImages++; m_mapLoadingProgress->setValue(processedImages); // keep things responsive by processing events manually: QApplication::processEvents(); if (m_annotationMap->addImage(info)) { imagesWithCoordinates++; } // m_cancelMapLoading is set to true by clicking the "Cancel" button if (m_cancelMapLoading) { m_annotationMap->clear(); break; } } m_annotationMap->buildImageClusters(); // at this point either we canceled loading or the map is populated: m_mapIsPopulated = !m_cancelMapLoading; mapLoadingFinished(imagesWithCoordinates > 0, imagesWithCoordinates == processedImages); } void AnnotationDialog::Dialog::setCancelMapLoading() { m_cancelMapLoading = true; } void AnnotationDialog::Dialog::mapLoadingFinished(bool mapHasImages, bool allImagesHaveCoordinates) { m_mapLoadingProgress->hide(); m_cancelMapLoadingButton->hide(); if (m_setup == InputSingleImageConfigMode) { m_annotationMap->displayStatus(Map::MapStatus::ImageHasNoCoordinates); } else { if (m_setup == SearchMode) { m_annotationMap->displayStatus(Map::MapStatus::SearchCoordinates); } else { if (mapHasImages) { if (!allImagesHaveCoordinates) { m_annotationMap->displayStatus(Map::MapStatus::SomeImagesHaveNoCoordinates); } else { m_annotationMap->displayStatus(Map::MapStatus::ImageHasCoordinates); } } else { m_annotationMap->displayStatus(Map::MapStatus::NoImagesHaveNoCoordinates); } } } if (m_setup != SearchMode) { m_annotationMap->zoomToMarkers(); updateMapForCurrentImage(); } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageInfo.cpp b/DB/ImageInfo.cpp index 3bcb2a10..553480d2 100644 --- a/DB/ImageInfo.cpp +++ b/DB/ImageInfo.cpp @@ -1,818 +1,826 @@ /* 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. */ #include "ImageInfo.h" #include "CategoryCollection.h" #include "FileInfo.h" #include "ImageDB.h" #include "Logging.h" #include "MemberMap.h" #include #include #include #include #include #include #include #include #include using namespace DB; ImageInfo::ImageInfo() : m_null(true) , m_rating(-1) , m_stackId(0) , m_stackOrder(0) , m_videoLength(-1) , m_isMatched(false) , m_matchGeneration(-1) , m_locked(false) , m_dirty(false) { } ImageInfo::ImageInfo(const DB::FileName &fileName, MediaType type, bool readExifInfo, bool storeExifInfo) : m_imageOnDisk(YesOnDisk) , m_null(false) , m_size(-1, -1) , m_type(type) , m_rating(-1) , m_stackId(0) , m_stackOrder(0) , m_videoLength(-1) , m_isMatched(false) , m_matchGeneration(-1) , m_locked(false) { QFileInfo fi(fileName.absolute()); m_label = fi.completeBaseName(); m_angle = 0; setFileName(fileName); // Read Exif information if (readExifInfo) { ExifMode mode = EXIFMODE_INIT; if (!storeExifInfo) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName, mode); } m_dirty = false; } ImageInfo::ImageInfo(const ImageInfo &other) : QSharedData(other) { *this = other; } void ImageInfo::setIsMatched(bool isMatched) { m_isMatched = isMatched; } bool ImageInfo::isMatched() const { return m_isMatched; } void ImageInfo::setMatchGeneration(int matchGeneration) { m_matchGeneration = matchGeneration; } int ImageInfo::matchGeneration() const { return m_matchGeneration; } void ImageInfo::setLabel(const QString &desc) { if (desc != m_label) markDirty(); m_label = desc; } QString ImageInfo::label() const { return m_label; } void ImageInfo::setDescription(const QString &desc) { if (desc != m_description) markDirty(); m_description = desc.trimmed(); } QString ImageInfo::description() const { return m_description; } void ImageInfo::setCategoryInfo(const QString &key, const StringSet &value) { // Don't check if really changed, because it's too slow. markDirty(); m_categoryInfomation[key] = value; } bool ImageInfo::hasCategoryInfo(const QString &key, const QString &value) const { return m_categoryInfomation[key].contains(value); } bool DB::ImageInfo::hasCategoryInfo(const QString &key, const StringSet &values) const { return values.intersects(m_categoryInfomation[key]); } StringSet ImageInfo::itemsOfCategory(const QString &key) const { return m_categoryInfomation[key]; } void ImageInfo::renameItem(const QString &category, const QString &oldValue, const QString &newValue) { if (m_taggedAreas.contains(category)) { if (m_taggedAreas[category].contains(oldValue)) { m_taggedAreas[category][newValue] = m_taggedAreas[category][oldValue]; m_taggedAreas[category].remove(oldValue); } } StringSet &set = m_categoryInfomation[category]; StringSet::iterator it = set.find(oldValue); if (it != set.end()) { markDirty(); set.erase(it); set.insert(newValue); } } DB::FileName ImageInfo::fileName() const { return m_fileName; } void ImageInfo::setFileName(const DB::FileName &fileName) { if (fileName != m_fileName) markDirty(); m_fileName = fileName; m_imageOnDisk = Unchecked; DB::CategoryPtr folderCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::FolderCategory); if (folderCategory) { DB::MemberMap &map = DB::ImageDB::instance()->memberMap(); createFolderCategoryItem(folderCategory, map); //ImageDB::instance()->setMemberMap( map ); } } void ImageInfo::rotate(int degrees, RotationMode mode) { // ensure positive degrees: degrees += 360; degrees = degrees % 360; if (degrees == 0) return; markDirty(); m_angle = (m_angle + degrees) % 360; if (degrees == 90 || degrees == 270) { m_size.transpose(); } // the AnnotationDialog manages this by itself and sets RotateImageInfoOnly: if (mode == RotateImageInfoAndAreas) { for (auto &areasOfCategory : m_taggedAreas) { for (auto &area : areasOfCategory) { QRect rotatedArea; // parameter order for QRect::setCoords: // setCoords( left, top, right, bottom ) // keep in mind that _size is already transposed switch (degrees) { case 90: rotatedArea.setCoords( m_size.width() - area.bottom(), area.left(), m_size.width() - area.top(), area.right()); break; case 180: rotatedArea.setCoords( m_size.width() - area.right(), m_size.height() - area.bottom(), m_size.width() - area.left(), m_size.height() - area.top()); break; case 270: rotatedArea.setCoords( area.top(), m_size.height() - area.right(), area.bottom(), m_size.height() - area.left()); break; default: // degrees==0; "odd" values won't happen. rotatedArea = area; break; } // update _taggedAreas[category][tag]: area = rotatedArea; } } } } int ImageInfo::angle() const { return m_angle; } void ImageInfo::setAngle(int angle) { if (angle != m_angle) markDirty(); m_angle = angle; } short ImageInfo::rating() const { return m_rating; } void ImageInfo::setRating(short rating) { Q_ASSERT((rating >= 0 && rating <= 10) || rating == -1); if (rating > 10) rating = 10; if (rating < -1) rating = -1; if (m_rating != rating) markDirty(); m_rating = rating; } DB::StackID ImageInfo::stackId() const { return m_stackId; } void ImageInfo::setStackId(const DB::StackID stackId) { if (stackId != m_stackId) markDirty(); m_stackId = stackId; } unsigned int ImageInfo::stackOrder() const { return m_stackOrder; } void ImageInfo::setStackOrder(const unsigned int stackOrder) { if (stackOrder != m_stackOrder) markDirty(); m_stackOrder = stackOrder; } void ImageInfo::setVideoLength(int length) { if (m_videoLength != length) markDirty(); m_videoLength = length; } int ImageInfo::videoLength() const { return m_videoLength; } void ImageInfo::setDate(const ImageDate &date) { if (date != m_date) markDirty(); m_date = date; } ImageDate &ImageInfo::date() { return m_date; } ImageDate ImageInfo::date() const { return m_date; } bool ImageInfo::operator!=(const ImageInfo &other) const { return !(*this == other); } bool ImageInfo::operator==(const ImageInfo &other) const { bool changed = (m_fileName != other.m_fileName || m_label != other.m_label || (!m_description.isEmpty() && !other.m_description.isEmpty() && m_description != other.m_description) || // one might be isNull. m_date != other.m_date || m_angle != other.m_angle || m_rating != other.m_rating || (m_stackId != other.m_stackId || !((m_stackId == 0) ? true : (m_stackOrder == other.m_stackOrder)))); if (!changed) { QStringList keys = DB::ImageDB::instance()->categoryCollection()->categoryNames(); for (QStringList::ConstIterator it = keys.constBegin(); it != keys.constEnd(); ++it) changed |= m_categoryInfomation[*it] != other.m_categoryInfomation[*it]; } return !changed; } void ImageInfo::renameCategory(const QString &oldName, const QString &newName) { markDirty(); m_categoryInfomation[newName] = m_categoryInfomation[oldName]; m_categoryInfomation.remove(oldName); m_taggedAreas[newName] = m_taggedAreas[oldName]; m_taggedAreas.remove(oldName); } void ImageInfo::setMD5Sum(const MD5 &sum, bool storeEXIF) { if (sum != m_md5sum) { // if we make a QObject derived class out of imageinfo, we might invalidate thumbnails from here // file changed -> reload/invalidate metadata: ExifMode mode = EXIFMODE_ORIENTATION | EXIFMODE_DATABASE_UPDATE; // fuzzy dates are usually set for a reason if (!m_date.isFuzzy()) mode |= EXIFMODE_DATE; // FIXME (ZaJ): the "right" thing to do would be to update the description // - if it is currently empty (done.) // - if it has been set from the exif info and not been changed (TODO) if (m_description.isEmpty()) mode |= EXIFMODE_DESCRIPTION; if (!storeEXIF) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName(), mode); if (storeEXIF) { // Size isn't really EXIF, but this is the most obvious // place to extract it QImageReader reader(m_fileName.absolute()); if (reader.canRead()) { m_size = reader.size(); if (m_angle == 90 || m_angle == 270) m_size.transpose(); } } // FIXME (ZaJ): it *should* make sense to set the ImageDB::md5Map() from here, but I want // to make sure I fully understand everything first... // this could also be done as signal md5Changed(old,new) // image size is invalidated by the thumbnail builder, if needed markDirty(); } m_md5sum = sum; } void ImageInfo::setLocked(bool locked) { m_locked = locked; } bool ImageInfo::isLocked() const { return m_locked; } void ImageInfo::readExif(const DB::FileName &fullPath, DB::ExifMode mode) { DB::FileInfo exifInfo = DB::FileInfo::read(fullPath, mode); // Date if (updateDateInformation(mode)) { const ImageDate newDate(exifInfo.dateTime()); setDate(newDate); } // Orientation if ((mode & EXIFMODE_ORIENTATION) && Settings::SettingsData::instance()->useEXIFRotate()) { setAngle(exifInfo.angle()); } // Description if ((mode & EXIFMODE_DESCRIPTION) && Settings::SettingsData::instance()->useEXIFComments()) { bool doSetDescription = true; QString desc = exifInfo.description(); if (Settings::SettingsData::instance()->stripEXIFComments()) { for (const auto &ignoredComment : Settings::SettingsData::instance()->EXIFCommentsToStrip()) { if (desc == ignoredComment) { doSetDescription = false; break; } } } if (doSetDescription) { setDescription(desc); } } // Database update if (mode & EXIFMODE_DATABASE_UPDATE) { Exif::Database::instance()->add(exifInfo); #ifdef HAVE_MARBLE // GPS coords might have changed... m_coordsIsSet = false; #endif } } QStringList ImageInfo::availableCategories() const { return m_categoryInfomation.keys(); } QSize ImageInfo::size() const { return m_size; } void ImageInfo::setSize(const QSize &size) { if (size != m_size) markDirty(); m_size = size; } bool ImageInfo::imageOnDisk(const DB::FileName &fileName) { return fileName.exists(); } ImageInfo::ImageInfo(const DB::FileName &fileName, const QString &label, const QString &description, const ImageDate &date, int angle, const MD5 &md5sum, const QSize &size, MediaType type, short rating, unsigned int stackId, unsigned int stackOrder) { m_fileName = fileName; m_label = label; m_description = description; m_date = date; m_angle = angle; m_md5sum = md5sum; m_size = size; m_imageOnDisk = Unchecked; m_locked = false; m_null = false; m_type = type; markDirty(); if (rating > 10) rating = 10; if (rating < -1) rating = -1; m_rating = rating; m_stackId = stackId; m_stackOrder = stackOrder; m_videoLength = -1; m_matchGeneration = -1; } // Note: we need this operator because the base class QSharedData hides // its copy operator to make exclude the reference counting from being // copied. ImageInfo &ImageInfo::operator=(const ImageInfo &other) { m_fileName = other.m_fileName; m_label = other.m_label; m_description = other.m_description; m_date = other.m_date; m_categoryInfomation = other.m_categoryInfomation; m_taggedAreas = other.m_taggedAreas; m_angle = other.m_angle; m_imageOnDisk = other.m_imageOnDisk; m_md5sum = other.m_md5sum; m_null = other.m_null; m_size = other.m_size; m_type = other.m_type; m_rating = other.m_rating; m_stackId = other.m_stackId; m_stackOrder = other.m_stackOrder; m_videoLength = other.m_videoLength; m_isMatched = other.m_isMatched; m_matchGeneration = other.m_matchGeneration; #ifdef HAVE_KGEOMAP m_coordinates = other.m_coordinates; m_coordsIsSet = other.m_coordsIsSet; #endif m_locked = other.m_locked; m_dirty = other.m_dirty; return *this; } MediaType DB::ImageInfo::mediaType() const { return m_type; } bool ImageInfo::isVideo() const { return m_type == Video; } void DB::ImageInfo::createFolderCategoryItem(DB::CategoryPtr folderCategory, DB::MemberMap &memberMap) { QString folderName = Utilities::relativeFolderName(m_fileName.relative()); if (folderName.isEmpty()) return; if (!memberMap.contains(folderCategory->name(), folderName)) { QStringList directories = folderName.split(QString::fromLatin1("/")); QString curPath; for (QStringList::ConstIterator directoryIt = directories.constBegin(); directoryIt != directories.constEnd(); ++directoryIt) { if (curPath.isEmpty()) curPath = *directoryIt; else { QString oldPath = curPath; curPath = curPath + QString::fromLatin1("/") + *directoryIt; memberMap.addMemberToGroup(folderCategory->name(), oldPath, curPath); } } folderCategory->addItem(folderName); } m_categoryInfomation.insert(folderCategory->name(), StringSet() << folderName); } void DB::ImageInfo::copyExtraData(const DB::ImageInfo &from, bool copyAngle) { m_categoryInfomation = from.m_categoryInfomation; m_description = from.m_description; // Hmm... what should the date be? orig or modified? // _date = from._date; if (copyAngle) m_angle = from.m_angle; m_rating = from.m_rating; } void DB::ImageInfo::removeExtraData() { m_categoryInfomation.clear(); m_description.clear(); m_rating = -1; } void ImageInfo::merge(const ImageInfo &other) { // Merge date if (other.date() != m_date) { // a fuzzy date has been set by the user and therefore "wins" over an exact date. // two fuzzy dates can be merged // two exact dates should ideally be cross-checked with Exif information in the file. // Nevertheless, we merge them into a fuzzy date to avoid the complexity of checking the file. if (other.date().isFuzzy()) { if (m_date.isFuzzy()) m_date.extendTo(other.date()); else m_date = other.date(); } else if (!m_date.isFuzzy()) { m_date.extendTo(other.date()); } // else: keep m_date } // Merge description if (!other.description().isEmpty()) { if (m_description.isEmpty()) m_description = other.description(); else if (m_description != other.description()) m_description += QString::fromUtf8("\n-----------\n") + other.m_description; } // Clear untagged tag if only one of the images was untagged const QString untaggedCategory = Settings::SettingsData::instance()->untaggedCategory(); const QString untaggedTag = Settings::SettingsData::instance()->untaggedTag(); const bool isCompleted = !m_categoryInfomation[untaggedCategory].contains(untaggedTag) || !other.m_categoryInfomation[untaggedCategory].contains(untaggedTag); // Merge tags +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const auto categoryInfomationKeys = m_categoryInfomation.keys(); + QSet keys(categoryInfomationKeys.begin(), categoryInfomationKeys.end()); + const auto otherCategoryInfomationKeys = other.m_categoryInfomation.keys(); + const QSet otherCategoryInfomationKeysSet(otherCategoryInfomationKeys.begin(), otherCategoryInfomationKeys.end()); +#else QSet keys = QSet::fromList(m_categoryInfomation.keys()); - keys.unite(QSet::fromList(other.m_categoryInfomation.keys())); + const auto otherCategoryInfomationKeysSet = QSet::fromList(other.m_categoryInfomation.keys()); +#endif + keys.unite(otherCategoryInfomationKeysSet); for (const QString &key : keys) { m_categoryInfomation[key].unite(other.m_categoryInfomation[key]); } // Clear untagged tag if only one of the images was untagged if (isCompleted) m_categoryInfomation[untaggedCategory].remove(untaggedTag); // merge stacks: if (isStacked() || other.isStacked()) { DB::FileNameList stackImages; if (!isStacked()) stackImages.append(fileName()); else stackImages.append(DB::ImageDB::instance()->getStackFor(fileName())); stackImages.append(DB::ImageDB::instance()->getStackFor(other.fileName())); DB::ImageDB::instance()->unstack(stackImages); if (!DB::ImageDB::instance()->stack(stackImages)) qCWarning(DBLog, "Could not merge stacks!"); } } void DB::ImageInfo::addCategoryInfo(const QString &category, const StringSet &values) { for (StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt) { if (!m_categoryInfomation[category].contains(*valueIt)) { markDirty(); m_categoryInfomation[category].insert(*valueIt); } } } void DB::ImageInfo::clearAllCategoryInfo() { m_categoryInfomation.clear(); m_taggedAreas.clear(); } void DB::ImageInfo::removeCategoryInfo(const QString &category, const StringSet &values) { for (StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt) { if (m_categoryInfomation[category].contains(*valueIt)) { markDirty(); m_categoryInfomation[category].remove(*valueIt); m_taggedAreas[category].remove(*valueIt); } } } void DB::ImageInfo::addCategoryInfo(const QString &category, const QString &value, const QRect &area) { if (!m_categoryInfomation[category].contains(value)) { markDirty(); m_categoryInfomation[category].insert(value); if (area.isValid()) { m_taggedAreas[category][value] = area; } } } void DB::ImageInfo::removeCategoryInfo(const QString &category, const QString &value) { if (m_categoryInfomation[category].contains(value)) { markDirty(); m_categoryInfomation[category].remove(value); m_taggedAreas[category].remove(value); } } void DB::ImageInfo::setPositionedTags(const QString &category, const PositionTags &positionedTags) { markDirty(); m_taggedAreas[category] = positionedTags; } bool DB::ImageInfo::updateDateInformation(int mode) const { if ((mode & EXIFMODE_DATE) == 0) return false; if ((mode & EXIFMODE_FORCE) != 0) return true; return true; } TaggedAreas DB::ImageInfo::taggedAreas() const { return m_taggedAreas; } QRect DB::ImageInfo::areaForTag(QString category, QString tag) const { // QMap::value returns a default constructed value if the key is not found: return m_taggedAreas.value(category).value(tag); } #ifdef HAVE_MARBLE Map::GeoCoordinates DB::ImageInfo::coordinates() const { if (m_coordsIsSet) { return m_coordinates; } static const int EXIF_GPS_VERSIONID = 0; static const int EXIF_GPS_LATREF = 1; static const int EXIF_GPS_LAT = 2; static const int EXIF_GPS_LONREF = 3; static const int EXIF_GPS_LON = 4; static const int EXIF_GPS_ALTREF = 5; static const int EXIF_GPS_ALT = 6; static const QString S = QString::fromUtf8("S"); static const QString W = QString::fromUtf8("W"); static QList fields; if (fields.isEmpty()) { // the order here matters! we use the named int constants afterwards to refer to them: fields.append(new Exif::IntExifElement("Exif.GPSInfo.GPSVersionID")); // actually a byte value fields.append(new Exif::StringExifElement("Exif.GPSInfo.GPSLatitudeRef")); fields.append(new Exif::RationalExifElement("Exif.GPSInfo.GPSLatitude")); fields.append(new Exif::StringExifElement("Exif.GPSInfo.GPSLongitudeRef")); fields.append(new Exif::RationalExifElement("Exif.GPSInfo.GPSLongitude")); fields.append(new Exif::IntExifElement("Exif.GPSInfo.GPSAltitudeRef")); // actually a byte value fields.append(new Exif::RationalExifElement("Exif.GPSInfo.GPSAltitude")); } // read field values from database: bool foundIt = Exif::Database::instance()->readFields(m_fileName, fields); // if the Database query result doesn't contain exif GPS info (-> upgraded exifdb from DBVersion < 2), it is null // if the result is int 0, then there's no exif gps information in the image // otherwise we can proceed to parse the information if (foundIt && fields[EXIF_GPS_VERSIONID]->value().isNull()) { // update exif DB and repeat the search: Exif::Database::instance()->remove(fileName()); Exif::Database::instance()->add(fileName()); Exif::Database::instance()->readFields(m_fileName, fields); Q_ASSERT(!fields[EXIF_GPS_VERSIONID]->value().isNull()); } Map::GeoCoordinates coords; // gps info set? // don't use the versionid field here, because some cameras use 0 as its value if (foundIt && fields[EXIF_GPS_LAT]->value().toInt() != -1.0 && fields[EXIF_GPS_LON]->value().toInt() != -1.0) { // lat/lon/alt reference determines sign of float: double latr = (fields[EXIF_GPS_LATREF]->value().toString() == S) ? -1.0 : 1.0; double lat = fields[EXIF_GPS_LAT]->value().toFloat(); double lonr = (fields[EXIF_GPS_LONREF]->value().toString() == W) ? -1.0 : 1.0; double lon = fields[EXIF_GPS_LON]->value().toFloat(); double altr = (fields[EXIF_GPS_ALTREF]->value().toInt() == 1) ? -1.0 : 1.0; double alt = fields[EXIF_GPS_ALT]->value().toFloat(); if (lat != -1.0 && lon != -1.0) { coords.setLatLon(latr * lat, lonr * lon); if (alt != 0.0f) { coords.setAlt(altr * alt); } } } m_coordinates = coords; m_coordsIsSet = true; return m_coordinates; } #endif void ImageInfo::markDirty() { m_dirty = true; m_matchGeneration = -1; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/MemberMap.cpp b/DB/MemberMap.cpp index 3a44c325..ad9c3738 100644 --- a/DB/MemberMap.cpp +++ b/DB/MemberMap.cpp @@ -1,363 +1,375 @@ -/* Copyright (C) 2003-2019 The KPhotoAlbum Development Team +/* 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. */ #include "MemberMap.h" #include "Category.h" #include "Logging.h" using namespace DB; MemberMap::MemberMap() : QObject(nullptr) , m_dirty(true) , m_loading(false) { } /** returns the groups directly available from category (non closure that is) */ QStringList MemberMap::groups(const QString &category) const { return QStringList(m_members[category].keys()); } bool MemberMap::contains(const QString &category, const QString &item) const { return m_flatMembers[category].contains(item); } void MemberMap::markDirty(const QString &category) { if (m_loading) regenerateFlatList(category); else emit dirty(); } void MemberMap::deleteGroup(const QString &category, const QString &name) { m_members[category].remove(name); m_dirty = true; markDirty(category); } /** return all the members of memberGroup */ QStringList MemberMap::members(const QString &category, const QString &memberGroup, bool closure) const { if (closure) { - if (m_dirty) + if (m_dirty) { calculate(); + } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const auto &members = m_closureMembers[category][memberGroup]; + return QStringList(members.begin(), members.end()); +#else return m_closureMembers[category][memberGroup].toList(); - } else +#endif + } else { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const auto &members = m_members[category][memberGroup]; + return QStringList(members.begin(), members.end()); +#else return m_members[category][memberGroup].toList(); +#endif + } } void MemberMap::setMembers(const QString &category, const QString &memberGroup, const QStringList &members) { StringSet allowedMembers = members.toSet(); for (QStringList::const_iterator i = members.begin(); i != members.end(); ++i) if (!canAddMemberToGroup(category, memberGroup, *i)) allowedMembers.remove(*i); m_members[category][memberGroup] = allowedMembers; m_dirty = true; markDirty(category); } bool MemberMap::isEmpty() const { return m_members.empty(); } /** returns true if item is a group for category. */ bool MemberMap::isGroup(const QString &category, const QString &item) const { return m_members[category].find(item) != m_members[category].end(); } /** return a map from groupName to list of items for category example: { USA |-> [Chicago, Grand Canyon, Santa Clara], Denmark |-> [Esbjerg, Odense] } */ QMap MemberMap::groupMap(const QString &category) const { if (m_dirty) calculate(); return m_closureMembers[category]; } /** Calculates the closure for group, that is finds all members for group. Imagine there is a group called USA, and that this groups has a group inside it called Califonia, Califonia consists of members San Fransisco and Los Angeless. This function then maps USA to include Califonia, San Fransisco and Los Angeless. */ QStringList MemberMap::calculateClosure(QMap &resultSoFar, const QString &category, const QString &group) const { resultSoFar[group] = StringSet(); // Prevent against cykles. StringSet members = m_members[category][group]; StringSet result = members; for (StringSet::const_iterator it = members.begin(); it != members.end(); ++it) { if (resultSoFar.contains(*it)) { result += resultSoFar[*it]; } else if (isGroup(category, *it)) { result += calculateClosure(resultSoFar, category, *it).toSet(); } } resultSoFar[group] = result; return result.toList(); } /** This methods create the map _closureMembers from _members This is simply to avoid finding the closure each and every time it is needed. */ void MemberMap::calculate() const { m_closureMembers.clear(); // run through all categories for (QMap>::ConstIterator categoryIt = m_members.begin(); categoryIt != m_members.end(); ++categoryIt) { QString category = categoryIt.key(); QMap groupMap = categoryIt.value(); // Run through each of the groups for the given categories for (QMap::const_iterator groupIt = groupMap.constBegin(); groupIt != groupMap.constEnd(); ++groupIt) { QString group = groupIt.key(); if (m_closureMembers[category].find(group) == m_closureMembers[category].end()) { (void)calculateClosure(m_closureMembers[category], category, group); } } } m_dirty = false; } void MemberMap::renameGroup(const QString &category, const QString &oldName, const QString &newName) { // Don't allow overwriting to avoid creating cycles if (m_members[category].contains(newName)) return; m_dirty = true; markDirty(category); QMap &groupMap = m_members[category]; groupMap.insert(newName, m_members[category][oldName]); groupMap.remove(oldName); for (StringSet &set : groupMap) { if (set.contains(oldName)) { set.remove(oldName); set.insert(newName); } } } MemberMap::MemberMap(const MemberMap &other) : QObject(nullptr) , m_members(other.memberMap()) , m_dirty(true) , m_loading(false) { } void MemberMap::deleteItem(DB::Category *category, const QString &name) { QMap &groupMap = m_members[category->name()]; for (StringSet &items : groupMap) { items.remove(name); } m_members[category->name()].remove(name); m_dirty = true; markDirty(category->name()); } void MemberMap::renameItem(DB::Category *category, const QString &oldName, const QString &newName) { if (oldName == newName) return; QMap &groupMap = m_members[category->name()]; for (StringSet &items : groupMap) { if (items.contains(oldName)) { items.remove(oldName); items.insert(newName); } } if (groupMap.contains(oldName)) { groupMap[newName] = groupMap[oldName]; groupMap.remove(oldName); } m_dirty = true; markDirty(category->name()); } MemberMap &MemberMap::operator=(const MemberMap &other) { if (this != &other) { m_members = other.memberMap(); m_dirty = true; } return *this; } void MemberMap::regenerateFlatList(const QString &category) { m_flatMembers[category].clear(); for (QMap::const_iterator i = m_members[category].constBegin(); i != m_members[category].constEnd(); i++) { for (StringSet::const_iterator j = i.value().constBegin(); j != i.value().constEnd(); j++) { m_flatMembers[category].insert(*j); } } } void MemberMap::addMemberToGroup(const QString &category, const QString &group, const QString &item) { // Only test for cycles after database is already loaded if (!m_loading && !canAddMemberToGroup(category, group, item)) { qCWarning(DBLog, "Inserting item %s into group %s/%s would create a cycle. Ignoring...", qPrintable(item), qPrintable(category), qPrintable(group)); return; } if (item.isEmpty()) { qCWarning(DBLog, "Tried to insert null item into group %s/%s. Ignoring...", qPrintable(category), qPrintable(group)); return; } m_members[category][group].insert(item); m_flatMembers[category].insert(item); if (m_loading) { m_dirty = true; } else if (!m_dirty) { // Update _closureMembers to avoid marking it dirty QMap &categoryClosure = m_closureMembers[category]; categoryClosure[group].insert(item); QMap::const_iterator closureOfItem = categoryClosure.constFind(item); const StringSet *closureOfItemPtr(nullptr); if (closureOfItem != categoryClosure.constEnd()) { closureOfItemPtr = &(*closureOfItem); categoryClosure[group] += *closureOfItem; } for (QMap::iterator i = categoryClosure.begin(); i != categoryClosure.end(); ++i) if ((*i).contains(group)) { (*i).insert(item); if (closureOfItemPtr) (*i) += *closureOfItemPtr; } } // If we are loading, we do *not* want to regenerate the list! if (!m_loading) emit dirty(); } void MemberMap::removeMemberFromGroup(const QString &category, const QString &group, const QString &item) { Q_ASSERT(m_members.contains(category)); if (m_members[category].contains(group)) { m_members[category][group].remove(item); // We shouldn't be doing this very often, so just regenerate // the flat list regenerateFlatList(category); emit dirty(); } } void MemberMap::addGroup(const QString &category, const QString &group) { if (!m_members[category].contains(group)) { m_members[category].insert(group, StringSet()); } markDirty(category); } void MemberMap::renameCategory(const QString &oldName, const QString &newName) { if (oldName == newName) return; m_members[newName] = m_members[oldName]; m_members.remove(oldName); m_closureMembers[newName] = m_closureMembers[oldName]; m_closureMembers.remove(oldName); if (!m_loading) emit dirty(); } void MemberMap::deleteCategory(const QString &category) { m_members.remove(category); m_closureMembers.remove(category); markDirty(category); } QMap DB::MemberMap::inverseMap(const QString &category) const { QMap res; const QMap &map = m_members[category]; for (QMap::ConstIterator mapIt = map.begin(); mapIt != map.end(); ++mapIt) { QString group = mapIt.key(); StringSet members = mapIt.value(); for (StringSet::const_iterator memberIt = members.begin(); memberIt != members.end(); ++memberIt) { res[*memberIt].insert(group); } } return res; } bool DB::MemberMap::hasPath(const QString &category, const QString &from, const QString &to) const { if (from == to) return true; else if (!m_members[category].contains(from)) // Try to avoid calculate(), which is quite time consuming. return false; else { // return members(category, from, true).contains(to); if (m_dirty) calculate(); return m_closureMembers[category][from].contains(to); } } void DB::MemberMap::setLoading(bool b) { if (m_loading && !b) { // TODO: Remove possible loaded cycles. } m_loading = b; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ValueCategoryMatcher.cpp b/DB/ValueCategoryMatcher.cpp index 27f4f8f7..0e70105d 100644 --- a/DB/ValueCategoryMatcher.cpp +++ b/DB/ValueCategoryMatcher.cpp @@ -1,59 +1,65 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* 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. + 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) version 3 or any later version + accepted by the membership of KDE e. V. (or its successor approved + by the membership of KDE e. V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. 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. + 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. + along with this program. If not, see . */ + #include "ValueCategoryMatcher.h" #include "ImageDB.h" #include "Logging.h" #include "MemberMap.h" void DB::ValueCategoryMatcher::debug(int level) const { qCDebug(DBCategoryMatcherLog, "%s%s: %s", qPrintable(spaces(level)), qPrintable(m_category), qPrintable(m_option)); } DB::ValueCategoryMatcher::ValueCategoryMatcher(const QString &category, const QString &value) { // Unescape doubled "&"s and restore the original value QString unEscapedValue = value; unEscapedValue.replace(QString::fromUtf8("&&"), QString::fromUtf8("&")); m_category = category; m_option = unEscapedValue; const MemberMap &map = DB::ImageDB::instance()->memberMap(); const QStringList members = map.members(m_category, m_option, true); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + m_members = StringSet(members.begin(), members.end()); +#else m_members = members.toSet(); +#endif } bool DB::ValueCategoryMatcher::eval(ImageInfoPtr info, QMap &alreadyMatched) { // Only add the tag _option to the alreadyMatched tags, // and omit the tags in _members if (m_shouldPrepareMatchedSet) alreadyMatched[m_category].insert(m_option); if (info->hasCategoryInfo(m_category, m_option)) { return true; } if (info->hasCategoryInfo(m_category, m_members)) return true; return false; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/TreeView.cpp b/Exif/TreeView.cpp index a0bf261a..035d3ec1 100644 --- a/Exif/TreeView.cpp +++ b/Exif/TreeView.cpp @@ -1,104 +1,109 @@ /* 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. */ #include "TreeView.h" #include "Info.h" #include #include #include using Utilities::StringSet; Exif::TreeView::TreeView(const QString &title, QWidget *parent) : QTreeWidget(parent) { setHeaderLabel(title); reload(); connect(this, &TreeView::itemClicked, this, &TreeView::toggleChildren); } void Exif::TreeView::toggleChildren(QTreeWidgetItem *parent) { if (!parent) return; bool on = parent->checkState(0) == Qt::Checked; for (int index = 0; index < parent->childCount(); ++index) { parent->child(index)->setCheckState(0, on ? Qt::Checked : Qt::Unchecked); toggleChildren(parent->child(index)); } } StringSet Exif::TreeView::selected() { StringSet result; for (QTreeWidgetItemIterator it(this); *it; ++it) { if ((*it)->checkState(0) == Qt::Checked) result.insert((*it)->text(1)); } return result; } void Exif::TreeView::setSelectedExif(const StringSet &selected) { for (QTreeWidgetItemIterator it(this); *it; ++it) { bool on = selected.contains((*it)->text(1)); (*it)->setCheckState(0, on ? Qt::Checked : Qt::Unchecked); } } void Exif::TreeView::reload() { clear(); setRootIsDecorated(true); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const auto availableKeys = Exif::Info::instance()->availableKeys(); + QStringList keys(availableKeys.begin(), availableKeys.end()); +#else QStringList keys = Exif::Info::instance()->availableKeys().toList(); +#endif keys.sort(); QMap tree; for (QStringList::const_iterator keysIt = keys.constBegin(); keysIt != keys.constEnd(); ++keysIt) { const QStringList subKeys = (*keysIt).split(QLatin1String(".")); QTreeWidgetItem *parent = nullptr; QString path; for (const QString &subKey : subKeys) { if (!path.isEmpty()) path += QString::fromLatin1("."); path += subKey; if (tree.contains(path)) parent = tree[path]; else { if (parent == nullptr) parent = new QTreeWidgetItem(this, QStringList(subKey)); else parent = new QTreeWidgetItem(parent, QStringList(subKey)); parent->setText(1, path); // This is simply to make the implementation of selected easier. parent->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); parent->setCheckState(0, Qt::Unchecked); tree.insert(path, parent); } } } if (QTreeWidgetItem *item = topLevelItem(0)) item->setExpanded(true); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/HTMLGenerator/Generator.cpp b/HTMLGenerator/Generator.cpp index 634187de..35da4d0f 100644 --- a/HTMLGenerator/Generator.cpp +++ b/HTMLGenerator/Generator.cpp @@ -1,796 +1,805 @@ /* 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. */ #include "Generator.h" #include "ImageSizeCheckBox.h" #include "Logging.h" #include "Setup.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 namespace { QString 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 HTMLGenerator::Generator::Generator(const Setup &setup, QWidget *parent) : QProgressDialog(parent) , m_tempDirHandle() , m_tempDir(m_tempDirHandle.path()) , m_hasEnteredLoop(false) { setLabelText(i18n("Generating images for HTML page ")); m_setup = setup; m_eventLoop = new QEventLoop; m_avconv = QStandardPaths::findExecutable(QString::fromUtf8("avconv")); if (m_avconv.isNull()) m_avconv = QStandardPaths::findExecutable(QString::fromUtf8("ffmpeg")); m_tempDirHandle.setAutoRemove(true); } HTMLGenerator::Generator::~Generator() { delete m_eventLoop; } void HTMLGenerator::Generator::generate() { qCDebug(HTMLGeneratorLog) << "Generating gallery" << m_setup.title() << "containing" << m_setup.imageList().size() << "entries in" << m_setup.baseDir(); // Generate .kim file if (m_setup.generateKimFile()) { qCDebug(HTMLGeneratorLog) << "Generating .kim file..."; bool ok; QString destURL = m_setup.destURL(); ImportExport::Export exp(m_setup.imageList(), kimFileName(false), false, -1, ImportExport::ManualCopy, destURL + QDir::separator() + m_setup.outputDir(), true, &ok); if (!ok) { qCDebug(HTMLGeneratorLog) << ".kim file failed!"; return; } } // prepare the progress dialog m_total = m_waitCounter = calculateSteps(); setMaximum(m_total); setValue(0); connect(this, &QProgressDialog::canceled, this, &Generator::slotCancelGenerate); m_filenameMapper.reset(); qCDebug(HTMLGeneratorLog) << "Generating content pages..."; // Iterate over each of the image sizes needed. for (QList::ConstIterator sizeIt = m_setup.activeResolutions().begin(); sizeIt != m_setup.activeResolutions().end(); ++sizeIt) { bool ok = generateIndexPage((*sizeIt)->width(), (*sizeIt)->height()); if (!ok) return; const DB::FileNameList imageList = m_setup.imageList(); for (int index = 0; index < imageList.size(); ++index) { DB::FileName current = imageList.at(index); DB::FileName prev; DB::FileName next; if (index != 0) prev = imageList.at(index - 1); if (index != imageList.size() - 1) next = imageList.at(index + 1); ok = generateContentPage((*sizeIt)->width(), (*sizeIt)->height(), prev, current, next); if (!ok) return; } } // Now generate the thumbnail images qCDebug(HTMLGeneratorLog) << "Generating thumbnail images..."; for (const DB::FileName &fileName : m_setup.imageList()) { if (wasCanceled()) return; createImage(fileName, m_setup.thumbSize()); } if (wasCanceled()) return; if (m_waitCounter > 0) { m_hasEnteredLoop = true; m_eventLoop->exec(); } if (wasCanceled()) return; qCDebug(HTMLGeneratorLog) << "Linking image file..."; bool ok = linkIndexFile(); if (!ok) return; qCDebug(HTMLGeneratorLog) << "Copying theme files..."; // Copy over the mainpage.css, imagepage.css QString themeDir, themeAuthor, themeName; getThemeInfo(&themeDir, &themeName, &themeAuthor); QDir dir(themeDir); QStringList files = dir.entryList(QDir::Files); if (files.count() < 1) qCWarning(HTMLGeneratorLog) << QString::fromLatin1("theme '%1' doesn't have enough files to be a theme").arg(themeDir); for (QStringList::Iterator it = files.begin(); it != files.end(); ++it) { if (*it == QString::fromLatin1("kphotoalbum.theme") || *it == QString::fromLatin1("mainpage.html") || *it == QString::fromLatin1("imagepage.html")) continue; QString from = QString::fromLatin1("%1%2").arg(themeDir).arg(*it); QString to = m_tempDir.filePath(*it); ok = Utilities::copyOrOverwrite(from, to); if (!ok) { KMessageBox::error(this, i18n("Error copying %1 to %2", from, to)); return; } } // Copy files over to destination. QString outputDir = m_setup.baseDir() + QString::fromLatin1("/") + m_setup.outputDir(); qCDebug(HTMLGeneratorLog) << "Copying files from" << m_tempDir.path() << "to final location" << outputDir << "..."; KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(m_tempDir.path()), QUrl::fromUserInput(outputDir)); connect(job, &KIO::CopyJob::result, this, &Generator::showBrowser); m_eventLoop->exec(); return; } bool HTMLGenerator::Generator::generateIndexPage(int width, int height) { QString themeDir, themeAuthor, themeName; getThemeInfo(&themeDir, &themeName, &themeAuthor); QString content = readFile(QString::fromLatin1("%1mainpage.html").arg(themeDir)); if (content.isEmpty()) return false; // Adding the copyright comment after DOCTYPE not before (HTML standard requires the DOCTYPE to be first within the document) QRegExp rx(QString::fromLatin1("^(]*>)")); int position; rx.setCaseSensitivity(Qt::CaseInsensitive); position = rx.indexIn(content); if ((position += rx.matchedLength()) < 0) content = QString::fromLatin1("\n").arg(themeName).arg(themeAuthor) + content; else content.insert(position, QString::fromLatin1("\n\n").arg(themeName).arg(themeAuthor)); content.replace(QString::fromLatin1("**DESCRIPTION**"), m_setup.description()); content.replace(QString::fromLatin1("**TITLE**"), m_setup.title()); QString copyright; if (!m_setup.copyright().isEmpty()) copyright = QString::fromLatin1("© %1").arg(m_setup.copyright()); else copyright = QString::fromLatin1(" "); content.replace(QString::fromLatin1("**COPYRIGHT**"), copyright); QString kimLink = QString::fromLatin1("Share and Enjoy KPhotoAlbum export file").arg(kimFileName(true)); if (m_setup.generateKimFile()) content.replace(QString::fromLatin1("**KIMFILE**"), kimLink); else content.remove(QString::fromLatin1("**KIMFILE**")); QDomDocument doc; QDomElement elm; QDomElement col; // -------------------------------------------------- Thumbnails // Initially all of the HTML generation was done using QDom, but it turned out in the end // to be much less code simply concatenating strings. This part, however, is easier using QDom // so we keep it using QDom. int count = 0; int cols = m_setup.numOfCols(); int minWidth = 0; int minHeight = 0; int enableVideo = 0; QString first, last, images; images += QString::fromLatin1("var gallery=new Array()\nvar width=%1\nvar height=%2\nvar tsize=%3\nvar inlineVideo=%4\nvar generatedVideo=%5\n").arg(width).arg(height).arg(m_setup.thumbSize()).arg(m_setup.inlineMovies()).arg(m_setup.html5VideoGenerate()); minImageSize(minWidth, minHeight); if (minWidth == 0 && minHeight == 0) { // full size only images += QString::fromLatin1("var minPage=\"index-fullsize.html\"\n"); } else { images += QString::fromLatin1("var minPage=\"index-%1x%2.html\"\n").arg(minWidth).arg(minHeight); } QDomElement row; for (const DB::FileName &fileName : m_setup.imageList()) { const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); if (wasCanceled()) return false; if (count % cols == 0) { row = doc.createElement(QString::fromLatin1("tr")); row.setAttribute(QString::fromLatin1("class"), QString::fromLatin1("thumbnail-row")); doc.appendChild(row); count = 0; } col = doc.createElement(QString::fromLatin1("td")); col.setAttribute(QString::fromLatin1("class"), QString::fromLatin1("thumbnail-col")); row.appendChild(col); if (first.isEmpty()) first = namePage(width, height, fileName); else last = namePage(width, height, fileName); if (!Utilities::isVideo(fileName)) { QMimeDatabase db; images += QString::fromLatin1("gallery.push([\"%1\", \"%2\", \"%3\", \"%4\", \"") .arg(nameImage(fileName, width)) .arg(nameImage(fileName, m_setup.thumbSize())) .arg(nameImage(fileName, maxImageSize())) .arg(db.mimeTypeForFile(nameImage(fileName, maxImageSize())).name()); } else { QMimeDatabase db; images += QString::fromLatin1("gallery.push([\"%1\", \"%2\", \"%3\"") .arg(nameImage(fileName, m_setup.thumbSize())) .arg(nameImage(fileName, m_setup.thumbSize())) .arg(nameImage(fileName, maxImageSize())); if (m_setup.html5VideoGenerate()) { images += QString::fromLatin1(", \"%1\", \"") .arg(QString::fromLatin1("video/ogg")); } else { images += QString::fromLatin1(", \"%1\", \"") .arg(db.mimeTypeForFile(fileName.relative(), QMimeDatabase::MatchExtension).name()); } enableVideo = 1; } // -------------------------------------------------- Description if (!info->description().isEmpty() && m_setup.includeCategory(QString::fromLatin1("**DESCRIPTION**"))) { images += QString::fromLatin1("%1\", \"") .arg(info->description() .replace(QString::fromLatin1("\n$"), QString::fromLatin1("")) .replace(QString::fromLatin1("\n"), QString::fromLatin1(" ")) .replace(QString::fromLatin1("\""), QString::fromLatin1("\\\""))); } else { images += QString::fromLatin1("\", \""); } QString description = populateDescription(DB::ImageDB::instance()->categoryCollection()->categories(), info); if (!description.isEmpty()) { description = QString::fromLatin1("
    %1
").arg(description); } else { description = QString::fromLatin1(""); } description.replace(QString::fromLatin1("\n$"), QString::fromLatin1("")); description.replace(QString::fromLatin1("\n"), QString::fromLatin1(" ")); description.replace(QString::fromLatin1("\""), QString::fromLatin1("\\\"")); images += description; images += QString::fromLatin1("\"]);\n"); QDomElement href = doc.createElement(QString::fromLatin1("a")); href.setAttribute(QString::fromLatin1("href"), namePage(width, height, fileName)); col.appendChild(href); QDomElement img = doc.createElement(QString::fromLatin1("img")); img.setAttribute(QString::fromLatin1("src"), nameImage(fileName, m_setup.thumbSize())); img.setAttribute(QString::fromLatin1("alt"), nameImage(fileName, m_setup.thumbSize())); href.appendChild(img); ++count; } // Adding TD elements to match the selected column amount for valid HTML if (count % cols != 0) { for (int i = count; i % cols != 0; ++i) { col = doc.createElement(QString::fromLatin1("td")); col.setAttribute(QString::fromLatin1("class"), QString::fromLatin1("thumbnail-col")); QDomText sp = doc.createTextNode(QString::fromLatin1(" ")); col.appendChild(sp); row.appendChild(col); } } content.replace(QString::fromLatin1("**THUMBNAIL-TABLE**"), doc.toString()); images += QString::fromLatin1("var enableVideo=%1\n").arg(enableVideo ? 1 : 0); content.replace(QString::fromLatin1("**JSIMAGES**"), images); if (!first.isEmpty()) content.replace(QString::fromLatin1("**FIRST**"), first); if (!last.isEmpty()) content.replace(QString::fromLatin1("**LAST**"), last); // -------------------------------------------------- Resolutions QString resolutions; QList actRes = m_setup.activeResolutions(); std::sort(actRes.begin(), actRes.end()); if (actRes.count() > 1) { resolutions += QString::fromLatin1("Resolutions: "); for (QList::ConstIterator sizeIt = actRes.constBegin(); sizeIt != actRes.constEnd(); ++sizeIt) { int w = (*sizeIt)->width(); int h = (*sizeIt)->height(); QString page = QString::fromLatin1("index-%1.html").arg(ImageSizeCheckBox::text(w, h, true)); QString text = (*sizeIt)->text(false); resolutions += QString::fromLatin1(" "); if (width == w && height == h) { resolutions += text; } else { resolutions += QString::fromLatin1("%2").arg(page).arg(text); } } } content.replace(QString::fromLatin1("**RESOLUTIONS**"), resolutions); if (wasCanceled()) return false; // -------------------------------------------------- write to file QString fileName = m_tempDir.filePath( QString::fromLatin1("index-%1.html") .arg(ImageSizeCheckBox::text(width, height, true))); bool ok = writeToFile(fileName, content); if (!ok) return false; return true; } bool HTMLGenerator::Generator::generateContentPage(int width, int height, const DB::FileName &prev, const DB::FileName ¤t, const DB::FileName &next) { QString themeDir, themeAuthor, themeName; getThemeInfo(&themeDir, &themeName, &themeAuthor); QString content = readFile(QString::fromLatin1("%1imagepage.html").arg(themeDir)); if (content.isEmpty()) return false; const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(current); // Note(jzarl): is there any reason why currentFile could be different from current? const DB::FileName currentFile = info->fileName(); // Adding the copyright comment after DOCTYPE not before (HTML standard requires the DOCTYPE to be first within the document) QRegExp rx(QString::fromLatin1("^(]*>)")); int position; rx.setCaseSensitivity(Qt::CaseInsensitive); position = rx.indexIn(content); if ((position += rx.matchedLength()) < 0) content = QString::fromLatin1("\n").arg(themeName).arg(themeAuthor) + content; else content.insert(position, QString::fromLatin1("\n\n").arg(themeName).arg(themeAuthor)); // TODO: Hardcoded non-standard category names is not good practice QString title = QString::fromLatin1(""); QString name = QString::fromLatin1("Common Name"); - if (!info->itemsOfCategory(name).empty()) { - title += QStringList(info->itemsOfCategory(name).toList()).join(QString::fromLatin1(" - ")); + const auto itemsOfCategory = info->itemsOfCategory(name); + if (!itemsOfCategory.empty()) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + title += QStringList(itemsOfCategory.begin(), itemsOfCategory.end()).join(QLatin1String(" - ")); +#else + title += QStringList(itemsOfCategory.toList()).join(QString::fromLatin1(" - ")); +#endif } else { name = QString::fromLatin1("Latin Name"); - if (!info->itemsOfCategory(name).empty()) { - title += QStringList(info->itemsOfCategory(name).toList()).join(QString::fromLatin1(" - ")); + if (!itemsOfCategory.empty()) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + title += QStringList(itemsOfCategory.begin(), itemsOfCategory.end()).join(QString::fromLatin1(" - ")); +#else + title += QStringList(itemsOfCategory.toList()).join(QString::fromLatin1(" - ")); +#endif } else { title = info->label(); } } content.replace(QString::fromLatin1("**TITLE**"), title); // Image or video content if (Utilities::isVideo(currentFile)) { QString videoFile = createVideo(currentFile); QString videoBase = videoFile.replace(QRegExp(QString::fromLatin1("\\..*")), QString::fromLatin1("")); if (m_setup.inlineMovies()) if (m_setup.html5Video()) content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("").arg(QString::fromLatin1("%1.mp4").arg(videoBase)).arg(createImage(current, 256)).arg(QString::fromLatin1("%1.mp4").arg(videoBase)).arg(QString::fromLatin1("%1.mp4").arg(videoBase)).arg(QString::fromLatin1("%1.ogg").arg(videoBase))); else content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("" "") .arg(videoFile) .arg(createImage(current, 256)) .arg(videoFile)); else content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("" "") .arg(videoFile) .arg(createImage(current, 256))); } else content.replace(QString::fromLatin1("**IMAGE_OR_VIDEO**"), QString::fromLatin1("\"%1\"/") .arg(createImage(current, width))); // -------------------------------------------------- Links QString link; // prev link if (!prev.isNull()) link = i18n("prev", namePage(width, height, prev)); else link = i18n("prev"); content.replace(QString::fromLatin1("**PREV**"), link); // PENDING(blackie) These next 5 line also exists exactly like that in HTMLGenerator::Generator::generateIndexPage. Please refactor. // prevfile if (!prev.isNull()) link = namePage(width, height, prev); else link = i18n("prev"); content.replace(QString::fromLatin1("**PREVFILE**"), link); // index link link = i18n("index", ImageSizeCheckBox::text(width, height, true)); content.replace(QString::fromLatin1("**INDEX**"), link); // indexfile link = QString::fromLatin1("index-%1.html").arg(ImageSizeCheckBox::text(width, height, true)); content.replace(QString::fromLatin1("**INDEXFILE**"), link); // Next Link if (!next.isNull()) link = i18n("next", namePage(width, height, next)); else link = i18n("next"); content.replace(QString::fromLatin1("**NEXT**"), link); // Nextfile if (!next.isNull()) link = namePage(width, height, next); else link = i18n("next"); content.replace(QString::fromLatin1("**NEXTFILE**"), link); if (!next.isNull()) link = namePage(width, height, next); else link = QString::fromLatin1("index-%1.html").arg(ImageSizeCheckBox::text(width, height, true)); content.replace(QString::fromLatin1("**NEXTPAGE**"), link); // -------------------------------------------------- Resolutions QString resolutions; const QList &actRes = m_setup.activeResolutions(); if (actRes.count() > 1) { for (QList::ConstIterator sizeIt = actRes.begin(); sizeIt != actRes.end(); ++sizeIt) { int w = (*sizeIt)->width(); int h = (*sizeIt)->height(); QString page = namePage(w, h, currentFile); QString text = (*sizeIt)->text(false); resolutions += QString::fromLatin1(" "); if (width == w && height == h) resolutions += text; else resolutions += QString::fromLatin1("%2").arg(page).arg(text); } } content.replace(QString::fromLatin1("**RESOLUTIONS**"), resolutions); // -------------------------------------------------- Copyright QString copyright; if (!m_setup.copyright().isEmpty()) copyright = QString::fromLatin1("© %1").arg(m_setup.copyright()); else copyright = QString::fromLatin1(" "); content.replace(QString::fromLatin1("**COPYRIGHT**"), QString::fromLatin1("%1").arg(copyright)); // -------------------------------------------------- Description QString description = populateDescription(DB::ImageDB::instance()->categoryCollection()->categories(), info); if (!description.isEmpty()) content.replace(QString::fromLatin1("**DESCRIPTION**"), QString::fromLatin1("
    \n%1\n
").arg(description)); else content.replace(QString::fromLatin1("**DESCRIPTION**"), QString::fromLatin1("")); // -------------------------------------------------- write to file QString fileName = m_tempDir.filePath(namePage(width, height, currentFile)); bool ok = writeToFile(fileName, content); if (!ok) return false; return true; } QString HTMLGenerator::Generator::namePage(int width, int height, const DB::FileName &fileName) { QString name = m_filenameMapper.uniqNameFor(fileName); QString base = QFileInfo(name).completeBaseName(); return QString::fromLatin1("%1-%2.html").arg(base).arg(ImageSizeCheckBox::text(width, height, true)); } QString HTMLGenerator::Generator::nameImage(const DB::FileName &fileName, int size) { QString name = m_filenameMapper.uniqNameFor(fileName); QString base = QFileInfo(name).completeBaseName(); if (size == maxImageSize() && !Utilities::isVideo(fileName)) { if (name.endsWith(QString::fromLatin1(".jpg"), Qt::CaseSensitive) || name.endsWith(QString::fromLatin1(".jpeg"), Qt::CaseSensitive)) return name; else return base + QString::fromLatin1(".jpg"); } else if (size == maxImageSize() && Utilities::isVideo(fileName)) { return name; } else return QString::fromLatin1("%1-%2.jpg").arg(base).arg(size); } QString HTMLGenerator::Generator::createImage(const DB::FileName &fileName, int size) { const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); if (m_generatedFiles.contains(qMakePair(fileName, size))) { m_waitCounter--; } else { ImageManager::ImageRequest *request = new ImageManager::ImageRequest(fileName, QSize(size, size), info->angle(), this); request->setPriority(ImageManager::BatchTask); ImageManager::AsyncLoader::instance()->load(request); m_generatedFiles.insert(qMakePair(fileName, size)); } return nameImage(fileName, size); } QString HTMLGenerator::Generator::createVideo(const DB::FileName &fileName) { setValue(m_total - m_waitCounter); qApp->processEvents(); QString baseName = nameImage(fileName, maxImageSize()); QString destName = m_tempDir.filePath(baseName); if (!m_copiedVideos.contains(fileName)) { if (m_setup.html5VideoGenerate()) { // TODO: shouldn't we use avconv library directly instead of KRun // TODO: should check that the avconv (ffmpeg takes the same parameters on older systems) and ffmpeg2theora exist // TODO: Figure out avconv parameters to get rid of ffmpeg2theora KRun::runCommand(QString::fromLatin1("%1 -y -i %2 -vcodec libx264 -b 250k -bt 50k -acodec libfaac -ab 56k -ac 2 -s %3 %4") .arg(m_avconv) .arg(fileName.absolute()) .arg(QString::fromLatin1("320x240")) .arg(destName.replace(QRegExp(QString::fromLatin1("\\..*")), QString::fromLatin1(".mp4"))), MainWindow::Window::theMainWindow()); KRun::runCommand(QString::fromLatin1("ffmpeg2theora -v 7 -o %1 -x %2 %3") .arg(destName.replace(QRegExp(QString::fromLatin1("\\..*")), QString::fromLatin1(".ogg"))) .arg(QString::fromLatin1("320")) .arg(fileName.absolute()), MainWindow::Window::theMainWindow()); } else Utilities::copyOrOverwrite(fileName.absolute(), destName); m_copiedVideos.insert(fileName); } return baseName; } QString HTMLGenerator::Generator::kimFileName(bool relative) { if (relative) return QString::fromLatin1("%2.kim").arg(m_setup.outputDir()); else return m_tempDir.filePath(QString::fromLatin1("%2.kim").arg(m_setup.outputDir())); } bool HTMLGenerator::Generator::writeToFile(const QString &fileName, const QString &str) { QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { KMessageBox::error(this, i18n("Could not create file '%1'.", fileName), i18n("Could Not Create File")); return false; } QByteArray data = translateToHTML(str).toUtf8(); file.write(data); file.close(); return true; } QString HTMLGenerator::Generator::translateToHTML(const QString &str) { QString res; for (int i = 0; i < str.length(); ++i) { if (str[i].unicode() < 128) res.append(str[i]); else { res.append(QString().sprintf("&#%u;", (unsigned int)str[i].unicode())); } } return res; } bool HTMLGenerator::Generator::linkIndexFile() { ImageSizeCheckBox *resolution = m_setup.activeResolutions()[0]; QString fromFile = QString::fromLatin1("index-%1.html") .arg(resolution->text(true)); fromFile = m_tempDir.filePath(fromFile); QString destFile = m_tempDir.filePath(QString::fromLatin1("index.html")); bool ok = Utilities::copyOrOverwrite(fromFile, destFile); if (!ok) { KMessageBox::error(this, i18n("

Unable to copy %1 to %2

", fromFile, destFile)); return false; } return ok; } void HTMLGenerator::Generator::slotCancelGenerate() { ImageManager::AsyncLoader::instance()->stop(this); m_waitCounter = 0; if (m_hasEnteredLoop) m_eventLoop->exit(); } void HTMLGenerator::Generator::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) { const DB::FileName fileName = request->databaseFileName(); const QSize imgSize = request->size(); const bool loadedOK = request->loadedOK(); setValue(m_total - m_waitCounter); m_waitCounter--; int size = imgSize.width(); QString file = m_tempDir.filePath(nameImage(fileName, size)); bool success = loadedOK && image.save(file, "JPEG"); if (!success) { // We better stop the imageloading. In case this is a full disk, we will just get all images loaded, while this // error box is showing, resulting in a bunch of error messages, and memory running out due to all the hanging // pixmapLoaded methods. slotCancelGenerate(); KMessageBox::error(this, i18n("Unable to write image '%1'.", file)); } if (!Utilities::isVideo(fileName)) { try { Exif::Info::instance()->writeInfoToFile(fileName, file); } catch (...) { } } if (m_waitCounter == 0 && m_hasEnteredLoop) { m_eventLoop->exit(); } } int HTMLGenerator::Generator::calculateSteps() { int count = m_setup.activeResolutions().count(); return m_setup.imageList().size() * (1 + count); // 1 thumbnail + 1 real image } void HTMLGenerator::Generator::getThemeInfo(QString *baseDir, QString *name, QString *author) { *baseDir = m_setup.themePath(); KConfig themeconfig(QString::fromLatin1("%1/kphotoalbum.theme").arg(*baseDir), KConfig::SimpleConfig); KConfigGroup config = themeconfig.group("theme"); *name = config.readEntry("Name"); *author = config.readEntry("Author"); } int HTMLGenerator::Generator::maxImageSize() { int res = 0; for (QList::ConstIterator sizeIt = m_setup.activeResolutions().begin(); sizeIt != m_setup.activeResolutions().end(); ++sizeIt) { res = qMax(res, (*sizeIt)->width()); } return res; } void HTMLGenerator::Generator::minImageSize(int &width, int &height) { width = height = 0; for (QList::ConstIterator sizeIt = m_setup.activeResolutions().begin(); sizeIt != m_setup.activeResolutions().end(); ++sizeIt) { if ((width == 0) && ((*sizeIt)->width() > 0)) { width = (*sizeIt)->width(); height = (*sizeIt)->height(); } else if ((*sizeIt)->width() > 0) { width = qMin(width, (*sizeIt)->width()); height = qMin(height, (*sizeIt)->height()); } } } void HTMLGenerator::Generator::showBrowser() { if (m_setup.generateKimFile()) ImportExport::Export::showUsageDialog(); if (!m_setup.baseURL().isEmpty()) new KRun(QUrl::fromUserInput(QString::fromLatin1("%1/%2/index.html").arg(m_setup.baseURL()).arg(m_setup.outputDir())), MainWindow::Window::theMainWindow()); m_eventLoop->exit(); } QString HTMLGenerator::Generator::populateDescription(QList categories, const DB::ImageInfoPtr info) { QString description; if (m_setup.includeCategory(QString::fromLatin1("**DATE**"))) description += QString::fromLatin1("
  • %1 %2
  • ").arg(i18n("Date")).arg(info->date().toString()); for (QList::Iterator it = categories.begin(); it != categories.end(); ++it) { if ((*it)->isSpecialCategory()) continue; QString name = (*it)->name(); if (!info->itemsOfCategory(name).empty() && m_setup.includeCategory(name)) { QString val = QStringList(info->itemsOfCategory(name).toList()).join(QString::fromLatin1(", ")); description += QString::fromLatin1("
  • %1: %2
  • ").arg(name).arg(val); } } if (!info->description().isEmpty() && m_setup.includeCategory(QString::fromLatin1("**DESCRIPTION**"))) { description += QString::fromLatin1("
  • Description: %1
  • ").arg(info->description()); } return description; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ExtractOneVideoFrame.cpp b/ImageManager/ExtractOneVideoFrame.cpp index 402cbd7a..c4bb0a5d 100644 --- a/ImageManager/ExtractOneVideoFrame.cpp +++ b/ImageManager/ExtractOneVideoFrame.cpp @@ -1,159 +1,164 @@ /* Copyright 2012-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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ExtractOneVideoFrame.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace ImageManager { QString ExtractOneVideoFrame::s_tokenForShortVideos; #define STR(x) QString::fromUtf8(x) void ExtractOneVideoFrame::extract(const DB::FileName &fileName, double offset, QObject *receiver, const char *slot) { if (MainWindow::FeatureDialog::hasVideoThumbnailer()) new ExtractOneVideoFrame(fileName, offset, receiver, slot); } ExtractOneVideoFrame::ExtractOneVideoFrame(const DB::FileName &fileName, double offset, QObject *receiver, const char *slot) { m_fileName = fileName; const QString tmpPath = STR("%1/KPA-XXXXXX").arg(QDir::tempPath()); m_workingDirectory = new QTemporaryDir(tmpPath); if (!m_workingDirectory->isValid()) qCWarning(ImageManagerLog) << "Failed to create temporary directory!"; m_process = new Utilities::Process(this); m_process->setWorkingDirectory(m_workingDirectory->path()); connect(m_process, SIGNAL(finished(int)), this, SLOT(frameFetched())); connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(handleError(QProcess::ProcessError))); connect(this, SIGNAL(result(QImage)), receiver, slot); Q_ASSERT(MainWindow::FeatureDialog::hasVideoThumbnailer()); QStringList arguments; // analyzeduration is for videos where the videostream starts later than the sound arguments << STR("-ss") << QString::number(offset, 'f', 4) << STR("-analyzeduration") << STR("200M") << STR("-i") << fileName.absolute() << STR("-vf") << STR("thumbnail") << STR("-vframes") << STR("20") << m_workingDirectory->filePath(STR("000000%02d.png")); qCDebug(ImageManagerLog, "%s %s", qPrintable(MainWindow::FeatureDialog::ffmpegBinary()), qPrintable(arguments.join(QString::fromLatin1(" ")))); m_process->start(MainWindow::FeatureDialog::ffmpegBinary(), arguments); } void ExtractOneVideoFrame::frameFetched() { if (!QFile::exists(m_workingDirectory->filePath(STR("00000020.png")))) markShortVideo(m_fileName); QString name; for (int i = 20; i > 0; --i) { name = m_workingDirectory->filePath(STR("000000%1.png").arg(i, 2, 10, QChar::fromLatin1('0'))); if (QFile::exists(name)) { qCDebug(ImageManagerLog) << "Using video frame " << i; break; } } QImage image(name); emit result(image); delete m_workingDirectory; deleteLater(); } void ExtractOneVideoFrame::handleError(QProcess::ProcessError error) { QString message; switch (error) { case QProcess::FailedToStart: message = i18n("Failed to start"); break; case QProcess::Crashed: message = i18n("Crashed"); break; case QProcess::Timedout: message = i18n("Timedout"); break; case QProcess::ReadError: message = i18n("Read error"); break; case QProcess::WriteError: message = i18n("Write error"); break; case QProcess::UnknownError: message = i18n("Unknown error"); break; } KMessageBox::information(MainWindow::Window::theMainWindow(), i18n("

    Error when extracting video thumbnails.
    Error was: %1

    ", message), QString(), QLatin1String("errorWhenRunningQProcessFromExtractOneVideoFrame")); emit result(QImage()); deleteLater(); } void ExtractOneVideoFrame::markShortVideo(const DB::FileName &fileName) { if (s_tokenForShortVideos.isNull()) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const auto tokensInUse = MainWindow::TokenEditor::tokensInUse(); + Utilities::StringSet usedTokens(tokensInUse.begin(), tokensInUse.end()); +#else Utilities::StringSet usedTokens = MainWindow::TokenEditor::tokensInUse().toSet(); +#endif for (int ch = 'A'; ch <= 'Z'; ++ch) { QString token = QChar::fromLatin1((char)ch); if (!usedTokens.contains(token)) { s_tokenForShortVideos = token; break; } } if (s_tokenForShortVideos.isNull()) { // Hmmm, no free token. OK lets just skip setting tokens. return; } KMessageBox::information(MainWindow::Window::theMainWindow(), i18n("Unable to extract video thumbnails from some files. " "Either the file is damaged in some way, or the video is ultra short. " "For your convenience, the token '%1' " "has been set on those videos.\n\n" "(You might need to wait till the video extraction led in your status bar has stopped blinking, " "to see all affected videos.)", s_tokenForShortVideos)); } DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); info->addCategoryInfo(tokensCategory->name(), s_tokenForShortVideos); MainWindow::DirtyIndicator::markDirty(); } } // namespace ImageManager // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportDialog.cpp b/ImportExport/ImportDialog.cpp index 0d5fcab5..1c94bfe0 100644 --- a/ImportExport/ImportDialog.cpp +++ b/ImportExport/ImportDialog.cpp @@ -1,397 +1,402 @@ /* 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. */ #include "ImportDialog.h" #include "ImageRow.h" #include "ImportMatcher.h" #include "KimFileReader.h" #include "MD5CheckPage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Utilities::StringSet; class QPushButton; using namespace ImportExport; ImportDialog::ImportDialog(QWidget *parent) : KAssistantDialog(parent) , m_hasFilled(false) , m_md5CheckPage(nullptr) { } bool ImportDialog::exec(KimFileReader *kimFileReader, const QUrl &kimFileURL) { m_kimFileReader = kimFileReader; if (kimFileURL.isLocalFile()) { QDir cwd; // convert relative local path to absolute m_kimFile = QUrl::fromLocalFile(cwd.absoluteFilePath(kimFileURL.toLocalFile())) .adjusted(QUrl::NormalizePathSegments); } else { m_kimFile = kimFileURL; } QByteArray indexXML = m_kimFileReader->indexXML(); if (indexXML.isNull()) return false; bool ok = readFile(indexXML); if (!ok) return false; setupPages(); return KAssistantDialog::exec(); } bool ImportDialog::readFile(const QByteArray &data) { XMLDB::ReaderPtr reader = XMLDB::ReaderPtr(new XMLDB::XmlReader(DB::ImageDB::instance()->uiDelegate(), m_kimFile.toDisplayString())); reader->addData(data); XMLDB::ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KimDaBa-export")); if (!info.isStartToken) reader->complainStartElementExpected(QString::fromUtf8("KimDaBa-export")); // Read source QString source = reader->attribute(QString::fromUtf8("location")).toLower(); if (source != QString::fromLatin1("inline") && source != QString::fromLatin1("external")) { KMessageBox::error(this, i18n("

    XML file did not specify the source of the images, " "this is a strong indication that the file is corrupted

    ")); return false; } m_externalSource = (source == QString::fromLatin1("external")); // Read base url m_baseUrl = QUrl::fromUserInput(reader->attribute(QString::fromLatin1("baseurl"))); while (reader->readNextStartOrStopElement(QString::fromUtf8("image")).isStartToken) { const DB::FileName fileName = DB::FileName::fromRelativePath(reader->attribute(QString::fromUtf8("file"))); DB::ImageInfoPtr info = XMLDB::Database::createImageInfo(fileName, reader); m_images.append(info); } // the while loop already read the end element, so we tell readEndElement to not read the next token: reader->readEndElement(false); return true; } void ImportDialog::setupPages() { createIntroduction(); createImagesPage(); createDestination(); createCategoryPages(); connect(this, &ImportDialog::currentPageChanged, this, &ImportDialog::updateNextButtonState); QPushButton *helpButton = buttonBox()->button(QDialogButtonBox::Help); connect(helpButton, &QPushButton::clicked, this, &ImportDialog::slotHelp); } void ImportDialog::createIntroduction() { QString txt = i18n("

    Welcome to KPhotoAlbum Import

    " "This wizard will take you through the steps of an import operation. The steps are: " "
    1. First you must select which images you want to import from the export file. " "You do so by selecting the checkbox next to the image.
    2. " "
    3. Next you must tell KPhotoAlbum in which directory to put the images. This directory must " "of course be below the directory root KPhotoAlbum uses for images. " "KPhotoAlbum will take care to avoid name clashes
    4. " "
    5. The next step is to specify which categories you want to import (People, Places, ... ) " "and also tell KPhotoAlbum how to match the categories from the file to your categories. " "Imagine you load from a file, where a category is called Blomst (which is the " "Danish word for flower), then you would likely want to match this with your category, which might be " "called Blume (which is the German word for flower) - of course given you are German.
    6. " "
    7. The final steps, is matching the individual tokens from the categories. I may call myself Jesper " "in my image database, while you want to call me by my full name, namely Jesper K. Pedersen. " "In this step non matches will be highlighted in red, so you can see which tokens was not found in your " "database, or which tokens was only a partial match.
    "); QLabel *intro = new QLabel(txt, this); intro->setWordWrap(true); addPage(intro, i18nc("@title:tab introduction page", "Introduction")); } void ImportDialog::createImagesPage() { QScrollArea *top = new QScrollArea; top->setWidgetResizable(true); QWidget *container = new QWidget; QVBoxLayout *lay1 = new QVBoxLayout(container); top->setWidget(container); // Select all and Deselect All buttons QHBoxLayout *lay2 = new QHBoxLayout; lay1->addLayout(lay2); QPushButton *selectAll = new QPushButton(i18n("Select All"), container); lay2->addWidget(selectAll); QPushButton *selectNone = new QPushButton(i18n("Deselect All"), container); lay2->addWidget(selectNone); lay2->addStretch(1); connect(selectAll, &QPushButton::clicked, this, &ImportDialog::slotSelectAll); connect(selectNone, &QPushButton::clicked, this, &ImportDialog::slotSelectNone); QGridLayout *lay3 = new QGridLayout; lay1->addLayout(lay3); lay3->setColumnStretch(2, 1); int row = 0; for (DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it, ++row) { DB::ImageInfoPtr info = *it; ImageRow *ir = new ImageRow(info, this, m_kimFileReader, container); lay3->addWidget(ir->m_checkbox, row, 0); QPixmap pixmap = m_kimFileReader->loadThumbnail(info->fileName().relative()); if (!pixmap.isNull()) { QPushButton *but = new QPushButton(container); but->setIcon(pixmap); but->setIconSize(pixmap.size()); lay3->addWidget(but, row, 1); connect(but, &QPushButton::clicked, ir, &ImageRow::showImage); } else { QLabel *label = new QLabel(info->label()); lay3->addWidget(label, row, 1); } QLabel *label = new QLabel(QString::fromLatin1("

    %1

    ").arg(info->description())); lay3->addWidget(label, row, 2); m_imagesSelect.append(ir); } addPage(top, i18n("Select Which Images to Import")); } void ImportDialog::createDestination() { QWidget *top = new QWidget(this); QVBoxLayout *topLay = new QVBoxLayout(top); QHBoxLayout *lay = new QHBoxLayout; topLay->addLayout(lay); topLay->addStretch(1); QLabel *label = new QLabel(i18n("Destination of images: "), top); lay->addWidget(label); m_destinationEdit = new QLineEdit(top); lay->addWidget(m_destinationEdit, 1); QPushButton *but = new QPushButton(QString::fromLatin1("..."), top); but->setFixedWidth(30); lay->addWidget(but); m_destinationEdit->setText(Settings::SettingsData::instance()->imageDirectory()); connect(but, &QPushButton::clicked, this, &ImportDialog::slotEditDestination); connect(m_destinationEdit, &QLineEdit::textChanged, this, &ImportDialog::updateNextButtonState); m_destinationPage = addPage(top, i18n("Destination of Images")); } void ImportDialog::slotEditDestination() { QString file = QFileDialog::getExistingDirectory(this, QString(), m_destinationEdit->text()); if (!file.isNull()) { if (!QFileInfo(file).absoluteFilePath().startsWith(QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath())) { KMessageBox::error(this, i18n("The directory must be a subdirectory of %1", Settings::SettingsData::instance()->imageDirectory())); } else if (QFileInfo(file).absoluteFilePath().startsWith( QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath() + QString::fromLatin1("CategoryImages"))) { KMessageBox::error(this, i18n("This directory is reserved for category images.")); } else { m_destinationEdit->setText(file); updateNextButtonState(); } } } void ImportDialog::updateNextButtonState() { bool enabled = true; if (currentPage() == m_destinationPage) { QString dest = m_destinationEdit->text(); if (QFileInfo(dest).isFile()) enabled = false; else if (!QFileInfo(dest).absoluteFilePath().startsWith(QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath())) enabled = false; } setValid(currentPage(), enabled); } void ImportDialog::createCategoryPages() { QStringList categories; const DB::ImageInfoList images = selectedImages(); for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) { const DB::ImageInfoPtr info = *it; const QStringList categoriesForImage = info->availableCategories(); for (const QString &category : categoriesForImage) { auto catPtr = DB::ImageDB::instance()->categoryCollection()->categoryForName(category); if (!categories.contains(category) && !(catPtr && catPtr->isSpecialCategory())) { categories.append(category); } } } if (!categories.isEmpty()) { m_categoryMatcher = new ImportMatcher(QString(), QString(), categories, DB::ImageDB::instance()->categoryCollection()->categoryNames(DB::CategoryCollection::IncludeSpecialCategories::No), false, this); m_categoryMatcherPage = addPage(m_categoryMatcher, i18n("Match Categories")); QWidget *dummy = new QWidget; m_dummy = addPage(dummy, QString()); } else { m_categoryMatcherPage = nullptr; possiblyAddMD5CheckPage(); } } ImportMatcher *ImportDialog::createCategoryPage(const QString &myCategory, const QString &otherCategory) { StringSet otherItems; DB::ImageInfoList images = selectedImages(); for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) { otherItems += (*it)->itemsOfCategory(otherCategory); } QStringList myItems = DB::ImageDB::instance()->categoryCollection()->categoryForName(myCategory)->itemsInclCategories(); myItems.sort(); - ImportMatcher *matcher = new ImportMatcher(otherCategory, myCategory, otherItems.toList(), myItems, true, this); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const QStringList otherItemsList(otherItems.begin(), otherItems.end()); +#else + const QStringList otherItemsList = otherItems.toList(); +#endif + ImportMatcher *matcher = new ImportMatcher(otherCategory, myCategory, otherItemsList, myItems, true, this); addPage(matcher, myCategory); return matcher; } void ImportDialog::next() { if (currentPage() == m_destinationPage) { QString dir = m_destinationEdit->text(); if (!QFileInfo(dir).exists()) { int answer = KMessageBox::questionYesNo(this, i18n("Directory %1 does not exist. Should it be created?", dir)); if (answer == KMessageBox::Yes) { bool ok = QDir().mkpath(dir); if (!ok) { KMessageBox::error(this, i18n("Error creating directory %1", dir)); return; } } else return; } } if (!m_hasFilled && currentPage() == m_categoryMatcherPage) { m_hasFilled = true; m_categoryMatcher->setEnabled(false); removePage(m_dummy); ImportMatcher *matcher = nullptr; const auto matchers = m_categoryMatcher->m_matchers; for (const CategoryMatch *match : matchers) { if (match->m_checkbox->isChecked()) { matcher = createCategoryPage(match->m_combobox->currentText(), match->m_text); m_matchers.append(matcher); } } possiblyAddMD5CheckPage(); } KAssistantDialog::next(); } void ImportDialog::slotSelectAll() { selectImage(true); } void ImportDialog::slotSelectNone() { selectImage(false); } void ImportDialog::selectImage(bool on) { for (ImageRow *row : qAsConst(m_imagesSelect)) { row->m_checkbox->setChecked(on); } } DB::ImageInfoList ImportDialog::selectedImages() const { DB::ImageInfoList res; for (QList::ConstIterator it = m_imagesSelect.begin(); it != m_imagesSelect.end(); ++it) { if ((*it)->m_checkbox->isChecked()) res.append((*it)->m_info); } return res; } void ImportDialog::slotHelp() { KHelpClient::invokeHelp(QString::fromLatin1("chp-importExport")); } ImportSettings ImportExport::ImportDialog::settings() { ImportSettings settings; settings.setSelectedImages(selectedImages()); settings.setDestination(m_destinationEdit->text()); settings.setExternalSource(m_externalSource); settings.setKimFile(m_kimFile); settings.setBaseURL(m_baseUrl); if (m_md5CheckPage) { settings.setImportActions(m_md5CheckPage->settings()); } for (ImportMatcher *match : m_matchers) settings.addCategoryMatchSetting(match->settings()); return settings; } void ImportExport::ImportDialog::possiblyAddMD5CheckPage() { if (MD5CheckPage::pageNeeded(settings())) { m_md5CheckPage = new MD5CheckPage(settings()); addPage(m_md5CheckPage, i18n("How to resolve clashes")); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/CategoryImageConfig.cpp b/Viewer/CategoryImageConfig.cpp index d1f8769e..965ab7d6 100644 --- a/Viewer/CategoryImageConfig.cpp +++ b/Viewer/CategoryImageConfig.cpp @@ -1,190 +1,194 @@ -/* Copyright (C) 2003-2019 The KPhotoAlbum Development Team +/* 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. */ #include "CategoryImageConfig.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Utilities::StringSet; Viewer::CategoryImageConfig *Viewer::CategoryImageConfig::s_instance = nullptr; Viewer::CategoryImageConfig::CategoryImageConfig() : m_image(QImage()) { setWindowTitle(i18nc("@title:window", "Configure Category Image")); QWidget *top = new QWidget; QVBoxLayout *lay1 = new QVBoxLayout(top); setLayout(lay1); QGridLayout *lay2 = new QGridLayout; lay1->addLayout(lay2); // Group QLabel *label = new QLabel(i18nc("@label:listbox As in 'select the tag category'", "Category:"), top); lay2->addWidget(label, 0, 0); m_group = new KComboBox(top); lay2->addWidget(m_group, 0, 1); connect(m_group, static_cast(&QComboBox::activated), this, &CategoryImageConfig::groupChanged); // Member label = new QLabel(i18nc("@label:listbox As in 'select a tag'", "Tag:"), top); lay2->addWidget(label, 1, 0); m_member = new KComboBox(top); lay2->addWidget(m_member, 1, 1); connect(m_member, static_cast(&QComboBox::activated), this, &CategoryImageConfig::memberChanged); // Current Value QGridLayout *lay3 = new QGridLayout; lay1->addLayout(lay3); label = new QLabel(i18nc("@label The current category image", "Current image:"), top); lay3->addWidget(label, 0, 0); m_current = new QLabel(top); m_current->setFixedSize(128, 128); lay3->addWidget(m_current, 0, 1); // New Value m_imageLabel = new QLabel(i18nc("@label Preview of the new category imape", "New image:"), top); lay3->addWidget(m_imageLabel, 1, 0); m_imageLabel = new QLabel(top); m_imageLabel->setFixedSize(128, 128); lay3->addWidget(m_imageLabel, 1, 1); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); QPushButton *user1Button = new QPushButton; user1Button->setText(i18nc("@action:button As in 'Set the category image'", "Set")); buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole); connect(user1Button, &QPushButton::clicked, this, &CategoryImageConfig::slotSet); connect(buttonBox, &QDialogButtonBox::accepted, this, &CategoryImageConfig::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &CategoryImageConfig::reject); lay1->addWidget(buttonBox); } void Viewer::CategoryImageConfig::groupChanged() { QString categoryName = currentGroup(); if (categoryName.isNull()) return; QString currentText = m_member->currentText(); m_member->clear(); StringSet directMembers = m_info->itemsOfCategory(categoryName); StringSet set = directMembers; QMap map = DB::ImageDB::instance()->memberMap().inverseMap(categoryName); for (StringSet::const_iterator directMembersIt = directMembers.begin(); directMembersIt != directMembers.end(); ++directMembersIt) { set += map[*directMembersIt]; } +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QStringList list(set.begin(), set.end()); +#else QStringList list = set.toList(); +#endif list.sort(); m_member->addItems(list); int index = list.indexOf(currentText); if (index != -1) m_member->setCurrentIndex(index); memberChanged(); } void Viewer::CategoryImageConfig::memberChanged() { QString categoryName = currentGroup(); if (categoryName.isNull()) return; QPixmap pix = DB::ImageDB::instance()->categoryCollection()->categoryForName(categoryName)->categoryImage(categoryName, m_member->currentText(), 128, 128); m_current->setPixmap(pix); } void Viewer::CategoryImageConfig::slotSet() { QString categoryName = currentGroup(); if (categoryName.isNull()) return; DB::ImageDB::instance()->categoryCollection()->categoryForName(categoryName)->setCategoryImage(categoryName, m_member->currentText(), m_image); memberChanged(); } QString Viewer::CategoryImageConfig::currentGroup() { int index = m_group->currentIndex(); if (index == -1) return QString(); return m_categoryNames[index]; } void Viewer::CategoryImageConfig::setCurrentImage(const QImage &image, const DB::ImageInfoPtr &info) { m_image = image; m_imageLabel->setPixmap(QPixmap::fromImage(image)); m_info = info; groupChanged(); } Viewer::CategoryImageConfig *Viewer::CategoryImageConfig::instance() { if (!s_instance) s_instance = new CategoryImageConfig(); return s_instance; } void Viewer::CategoryImageConfig::show() { QString currentCategory = m_group->currentText(); m_group->clear(); m_categoryNames.clear(); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); int index = 0; int currentIndex = -1; for (QList::ConstIterator categoryIt = categories.constBegin(); categoryIt != categories.constEnd(); ++categoryIt) { if (!(*categoryIt)->isSpecialCategory()) { m_group->addItem((*categoryIt)->name()); m_categoryNames.push_back((*categoryIt)->name()); if ((*categoryIt)->name() == currentCategory) currentIndex = index; ++index; } } if (currentIndex != -1) m_group->setCurrentIndex(currentIndex); groupChanged(); QDialog::show(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileWriter.cpp b/XMLDB/FileWriter.cpp index 731fcae7..c12e79d6 100644 --- a/XMLDB/FileWriter.cpp +++ b/XMLDB/FileWriter.cpp @@ -1,497 +1,501 @@ /* 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. */ #include "FileWriter.h" #include "CompressFileInfo.h" #include "Database.h" #include "ElementWriter.h" #include "Logging.h" #include "NumberedBackup.h" #include "XMLCategory.h" #include #include #include #include #include #include #include #include #include // // // // +++++++++++++++++++++++++++++++ REMEMBER ++++++++++++++++++++++++++++++++ // // // // // Update XMLDB::Database::fileVersion every time you update the file format! // // // // // // // // // (sorry for the noise, but it is really important :-) using Utilities::StringSet; void XMLDB::FileWriter::save(const QString &fileName, bool isAutoSave) { setUseCompressedFileFormat(Settings::SettingsData::instance()->useCompressedIndexXML()); if (!isAutoSave) NumberedBackup(m_db->uiDelegate()).makeNumberedBackup(); // prepare XML document for saving: m_db->m_categoryCollection.initIdMap(); QFile out(fileName + QString::fromLatin1(".tmp")); if (!out.open(QIODevice::WriteOnly | QIODevice::Text)) { m_db->uiDelegate().sorry( QString::fromUtf8("Error saving to file '%1': %2").arg(out.fileName()).arg(out.errorString()), i18n("

    Could not save the image database to XML.

    " "File %1 could not be opened because of the following error: %2", out.fileName(), out.errorString()), i18n("Error while saving...")); return; } QElapsedTimer timer; if (TimingLog().isDebugEnabled()) timer.start(); QXmlStreamWriter writer(&out); writer.setAutoFormatting(true); writer.writeStartDocument(); { ElementWriter dummy(writer, QString::fromLatin1("KPhotoAlbum")); writer.writeAttribute(QString::fromLatin1("version"), QString::number(Database::fileVersion())); writer.writeAttribute(QString::fromLatin1("compressed"), QString::number(useCompressedFileFormat())); saveCategories(writer); saveImages(writer); saveBlockList(writer); saveMemberGroups(writer); //saveSettings(writer); } writer.writeEndDocument(); qCDebug(TimingLog) << "XMLDB::FileWriter::save(): Saving took" << timer.elapsed() << "ms"; // State: index.xml has previous DB version, index.xml.tmp has the current version. // original file can be safely deleted if ((!QFile::remove(fileName)) && QFile::exists(fileName)) { m_db->uiDelegate().sorry( QString::fromUtf8("Removal of file '%1' failed.").arg(fileName), i18n("

    Failed to remove old version of image database.

    " "

    Please try again or replace the file %1 with file %2 manually!

    ", fileName, out.fileName()), i18n("Error while saving...")); return; } // State: index.xml doesn't exist, index.xml.tmp has the current version. if (!out.rename(fileName)) { m_db->uiDelegate().sorry( QString::fromUtf8("Renaming index.xml to '%1' failed.").arg(out.fileName()), i18n("

    Failed to move temporary XML file to permanent location.

    " "

    Please try again or rename file %1 to %2 manually!

    ", out.fileName(), fileName), i18n("Error while saving...")); // State: index.xml.tmp has the current version. return; } // State: index.xml has the current version. } void XMLDB::FileWriter::saveCategories(QXmlStreamWriter &writer) { QStringList categories = DB::ImageDB::instance()->categoryCollection()->categoryNames(); ElementWriter dummy(writer, QString::fromLatin1("Categories")); DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); for (QString name : categories) { DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(name); if (!shouldSaveCategory(name)) { continue; } ElementWriter dummy(writer, QString::fromUtf8("Category")); writer.writeAttribute(QString::fromUtf8("name"), name); writer.writeAttribute(QString::fromUtf8("icon"), category->iconName()); writer.writeAttribute(QString::fromUtf8("show"), QString::number(category->doShow())); writer.writeAttribute(QString::fromUtf8("viewtype"), QString::number(category->viewType())); writer.writeAttribute(QString::fromUtf8("thumbnailsize"), QString::number(category->thumbnailSize())); writer.writeAttribute(QString::fromUtf8("positionable"), QString::number(category->positionable())); if (category == tokensCategory) { writer.writeAttribute(QString::fromUtf8("meta"), QString::fromUtf8("tokens")); } // FIXME (l3u): // Correct me if I'm wrong, but we don't need this, as the tags used as groups are // added to the respective category anyway when they're created, so there's no need to // re-add them here. Apart from this, adding an empty group (one without members) does // add an empty tag ("") doing so. /* QStringList list = Utilities::mergeListsUniqly(category->items(), m_db->_members.groups(name)); */ const auto categoryItems = category->items(); for (const QString &tagName : categoryItems) { ElementWriter dummy(writer, QString::fromLatin1("value")); writer.writeAttribute(QString::fromLatin1("value"), tagName); writer.writeAttribute(QString::fromLatin1("id"), QString::number(static_cast(category.data())->idForName(tagName))); QDate birthDate = category->birthDate(tagName); if (!birthDate.isNull()) writer.writeAttribute(QString::fromUtf8("birthDate"), birthDate.toString(Qt::ISODate)); } } } void XMLDB::FileWriter::saveImages(QXmlStreamWriter &writer) { DB::ImageInfoList list = m_db->m_images; // Copy files from clipboard to end of overview, so we don't loose them const auto clipBoardImages = m_db->m_clipboard; for (const DB::ImageInfoPtr &infoPtr : clipBoardImages) { list.append(infoPtr); } { ElementWriter dummy(writer, QString::fromLatin1("images")); for (const DB::ImageInfoPtr &infoPtr : qAsConst(list)) { save(writer, infoPtr); } } } void XMLDB::FileWriter::saveBlockList(QXmlStreamWriter &writer) { ElementWriter dummy(writer, QString::fromLatin1("blocklist")); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QList blockList(m_db->m_blockList.begin(), m_db->m_blockList.end()); +#else QList blockList = m_db->m_blockList.toList(); +#endif // sort blocklist to get diffable files std::sort(blockList.begin(), blockList.end()); for (const DB::FileName &block : qAsConst(blockList)) { ElementWriter dummy(writer, QString::fromLatin1("block")); writer.writeAttribute(QString::fromLatin1("file"), block.relative()); } } void XMLDB::FileWriter::saveMemberGroups(QXmlStreamWriter &writer) { if (m_db->m_members.isEmpty()) return; ElementWriter dummy(writer, QString::fromLatin1("member-groups")); for (QMap>::ConstIterator memberMapIt = m_db->m_members.memberMap().constBegin(); memberMapIt != m_db->m_members.memberMap().constEnd(); ++memberMapIt) { const QString categoryName = memberMapIt.key(); // FIXME (l3u): This can happen when an empty sub-category (group) is present. // Would be fine to fix the reason why this happens in the first place. if (categoryName.isEmpty()) { continue; } if (!shouldSaveCategory(categoryName)) continue; QMap groupMap = memberMapIt.value(); for (QMap::ConstIterator groupMapIt = groupMap.constBegin(); groupMapIt != groupMap.constEnd(); ++groupMapIt) { // FIXME (l3u): This can happen when an empty sub-category (group) is present. // Would be fine to fix the reason why this happens in the first place. if (groupMapIt.key().isEmpty()) { continue; } if (useCompressedFileFormat()) { const StringSet members = groupMapIt.value(); ElementWriter dummy(writer, QString::fromLatin1("member")); writer.writeAttribute(QString::fromLatin1("category"), categoryName); writer.writeAttribute(QString::fromLatin1("group-name"), groupMapIt.key()); QStringList idList; for (const QString &member : members) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName(categoryName); XMLCategory *category = static_cast(catPtr.data()); if (category->idForName(member) == 0) qCWarning(XMLDBLog) << "Member" << member << "in group" << categoryName << "->" << groupMapIt.key() << "has no id!"; idList.append(QString::number(category->idForName(member))); } std::sort(idList.begin(), idList.end()); writer.writeAttribute(QString::fromLatin1("members"), idList.join(QString::fromLatin1(","))); } else { QStringList members = groupMapIt.value().toList(); std::sort(members.begin(), members.end()); for (const QString &member : qAsConst(members)) { ElementWriter dummy(writer, QString::fromLatin1("member")); writer.writeAttribute(QString::fromLatin1("category"), memberMapIt.key()); writer.writeAttribute(QString::fromLatin1("group-name"), groupMapIt.key()); writer.writeAttribute(QString::fromLatin1("member"), member); } // Add an entry even if the group is empty // (this is not necessary for the compressed format) if (members.size() == 0) { ElementWriter dummy(writer, QString::fromLatin1("member")); writer.writeAttribute(QString::fromLatin1("category"), memberMapIt.key()); writer.writeAttribute(QString::fromLatin1("group-name"), groupMapIt.key()); } } } } } /* Perhaps, we may need this later ;-) void XMLDB::FileWriter::saveSettings(QXmlStreamWriter& writer) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementWriter dummy(writer, settingsString); QMap settings; // For testing settings.insert(QString::fromUtf8("tokensCategory"), QString::fromUtf8("Tokens")); settings.insert(QString::fromUtf8("untaggedCategory"), QString::fromUtf8("Events")); settings.insert(QString::fromUtf8("untaggedTag"), QString::fromUtf8("untagged")); QMapIterator settingsIterator(settings); while (settingsIterator.hasNext()) { ElementWriter dummy(writer, settingString); settingsIterator.next(); writer.writeAttribute(keyString, escape(settingsIterator.key())); writer.writeAttribute(valueString, escape(settingsIterator.value())); } } */ void XMLDB::FileWriter::save(QXmlStreamWriter &writer, const DB::ImageInfoPtr &info) { ElementWriter dummy(writer, QString::fromLatin1("image")); writer.writeAttribute(QString::fromLatin1("file"), info->fileName().relative()); if (info->label() != QFileInfo(info->fileName().relative()).completeBaseName()) writer.writeAttribute(QString::fromLatin1("label"), info->label()); if (!info->description().isEmpty()) writer.writeAttribute(QString::fromLatin1("description"), info->description()); DB::ImageDate date = info->date(); QDateTime start = date.start(); QDateTime end = date.end(); writer.writeAttribute(QString::fromLatin1("startDate"), start.toString(Qt::ISODate)); if (start != end) writer.writeAttribute(QString::fromLatin1("endDate"), end.toString(Qt::ISODate)); if (info->angle() != 0) writer.writeAttribute(QString::fromLatin1("angle"), QString::number(info->angle())); writer.writeAttribute(QString::fromLatin1("md5sum"), info->MD5Sum().toHexString()); writer.writeAttribute(QString::fromLatin1("width"), QString::number(info->size().width())); writer.writeAttribute(QString::fromLatin1("height"), QString::number(info->size().height())); if (info->rating() != -1) { writer.writeAttribute(QString::fromLatin1("rating"), QString::number(info->rating())); } if (info->stackId()) { writer.writeAttribute(QString::fromLatin1("stackId"), QString::number(info->stackId())); writer.writeAttribute(QString::fromLatin1("stackOrder"), QString::number(info->stackOrder())); } if (info->isVideo()) writer.writeAttribute(QLatin1String("videoLength"), QString::number(info->videoLength())); if (useCompressedFileFormat()) writeCategoriesCompressed(writer, info); else writeCategories(writer, info); } QString XMLDB::FileWriter::areaToString(QRect area) const { QStringList areaString; areaString.append(QString::number(area.x())); areaString.append(QString::number(area.y())); areaString.append(QString::number(area.width())); areaString.append(QString::number(area.height())); return areaString.join(QString::fromLatin1(" ")); } void XMLDB::FileWriter::writeCategories(QXmlStreamWriter &writer, const DB::ImageInfoPtr &info) { ElementWriter topElm(writer, QString::fromLatin1("options"), false); const QStringList grps = info->availableCategories(); for (const QString &name : grps) { if (!shouldSaveCategory(name)) continue; ElementWriter categoryElm(writer, QString::fromLatin1("option"), false); QStringList items = info->itemsOfCategory(name).toList(); std::sort(items.begin(), items.end()); if (!items.isEmpty()) { topElm.writeStartElement(); categoryElm.writeStartElement(); writer.writeAttribute(QString::fromLatin1("name"), name); } for (const QString &itemValue : qAsConst(items)) { ElementWriter dummy(writer, QString::fromLatin1("value")); writer.writeAttribute(QString::fromLatin1("value"), itemValue); QRect area = info->areaForTag(name, itemValue); if (!area.isNull()) { writer.writeAttribute(QString::fromLatin1("area"), areaToString(area)); } } } } void XMLDB::FileWriter::writeCategoriesCompressed(QXmlStreamWriter &writer, const DB::ImageInfoPtr &info) { QMap>> positionedTags; const QList categoryList = DB::ImageDB::instance()->categoryCollection()->categories(); for (const DB::CategoryPtr &category : categoryList) { QString categoryName = category->name(); if (!shouldSaveCategory(categoryName)) continue; const StringSet items = info->itemsOfCategory(categoryName); if (!items.empty()) { QStringList idList; for (const QString &itemValue : items) { QRect area = info->areaForTag(categoryName, itemValue); if (area.isValid()) { // Positioned tags can't be stored in the "fast" format // so we have to handle them separately positionedTags[categoryName] << QPair(itemValue, area); } else { int id = static_cast(category.data())->idForName(itemValue); idList.append(QString::number(id)); } } // Possibly all ids of a category have area information, so only // write the category attribute if there are actually ids to write if (!idList.isEmpty()) { std::sort(idList.begin(), idList.end()); writer.writeAttribute(escape(categoryName), idList.join(QString::fromLatin1(","))); } } } // Add a "readable" sub-element for the positioned tags // FIXME: can this be merged with the code in writeCategories()? if (!positionedTags.isEmpty()) { ElementWriter topElm(writer, QString::fromLatin1("options"), false); topElm.writeStartElement(); QMapIterator>> categoryWithAreas(positionedTags); while (categoryWithAreas.hasNext()) { categoryWithAreas.next(); ElementWriter categoryElm(writer, QString::fromLatin1("option"), false); categoryElm.writeStartElement(); writer.writeAttribute(QString::fromLatin1("name"), categoryWithAreas.key()); QList> areas = categoryWithAreas.value(); std::sort(areas.begin(), areas.end(), [](QPair a, QPair b) { return a.first < b.first; }); for (const auto &positionedTag : qAsConst(areas)) { ElementWriter dummy(writer, QString::fromLatin1("value")); writer.writeAttribute(QString::fromLatin1("value"), positionedTag.first); writer.writeAttribute(QString::fromLatin1("area"), areaToString(positionedTag.second)); } } } } bool XMLDB::FileWriter::shouldSaveCategory(const QString &categoryName) const { // Profiling indicated that this function was a hotspot, so this cache improved saving speed with 25% static QHash cache; if (cache.contains(categoryName)) return cache[categoryName]; // A few bugs has shown up, where an invalid category name has crashed KPA. It therefore checks for such invalid names here. if (!m_db->m_categoryCollection.categoryForName(categoryName)) { qCWarning(XMLDBLog, "Invalid category name: %s", qPrintable(categoryName)); cache.insert(categoryName, false); return false; } const bool shouldSave = dynamic_cast(m_db->m_categoryCollection.categoryForName(categoryName).data())->shouldSave(); cache.insert(categoryName, shouldSave); return shouldSave; } /** * @brief Escape problematic characters in a string that forms an XML attribute name. * * N.B.: Attribute values do not need to be escaped! * @see XMLDB::FileReader::unescape * * @param str the string to be escaped * @return the escaped string */ QString XMLDB::FileWriter::escape(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); // Regex to match characters that are not allowed to start XML attribute names const QRegExp rx(QString::fromLatin1("([^a-zA-Z0-9:_])")); int pos = 0; // Encoding special characters if compressed XML is selected if (useCompressedFileFormat()) { while ((pos = rx.indexIn(tmp, pos)) != -1) { QString before = rx.cap(1); QString after; after.sprintf("_.%0X", rx.cap(1).data()->toLatin1()); 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: