diff --git a/AnnotationDialog/Dialog.cpp b/AnnotationDialog/Dialog.cpp index 921073ab..62b28a97 100644 --- a/AnnotationDialog/Dialog.cpp +++ b/AnnotationDialog/Dialog.cpp @@ -1,1768 +1,1763 @@ -/* 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. + 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 "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_KGEOMAP #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_KGEOMAP // -------------------------------------------------- Map representation m_annotationMapContainer = new QWidget(this); QVBoxLayout *annotationMapContainerLayout = new QVBoxLayout(m_annotationMapContainer); m_annotationMap = new Map::MapView(this); 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(), SIGNAL(proposedTagSelected(QString, QString)), sel, SLOT(ensureTagIsSelected(QString, QString))); // 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(), SIGNAL(areaCreated(ResizableFrame *)), this, SLOT(slotNewArea(ResizableFrame *))); // 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 (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()) { 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(); - Q_FOREACH (ListSelect *ls, m_optionList) { + for (ListSelect *ls : m_optionList) { ls->setSelection(old_info.itemsOfCategory(ls->category())); // Also set all positionable tag candidates if (ls->positionable()) { QString category = ls->category(); QSet selectedTags = old_info.itemsOfCategory(category); QSet positionedTagSet = positionedTags(category); // Add the tag to the positionable candiate list, if no area is already associated with it - Q_FOREACH (const auto &tag, selectedTags) { + 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 for (ResizableFrame *area : areas()) { 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; - Q_FOREACH (ListSelect *ls, m_optionList) { + for (ListSelect *ls : 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()) { QSet selectedTags = ls->itemsOn(); - Q_FOREACH (const QString &tagName, selectedTags) { + 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 QMap> taggedAreas = info.taggedAreas(); QMapIterator> 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]) { QMapIterator 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_KGEOMAP updateMapForCurrentImage(); #endif } m_preview->updatePositionableCategories(positionableCategories); } void AnnotationDialog::Dialog::writeToInfo() { - Q_FOREACH (ListSelect *ls, m_optionList) { + for (ListSelect *ls : 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 QMap> areas = taggedAreas(); info.setLabel(m_imageLabel->text()); info.setDescription(m_description->toPlainText()); - Q_FOREACH (ListSelect *ls, m_optionList) { + for (ListSelect *ls : 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(); } QMap> AnnotationDialog::Dialog::taggedAreas() const { QMap> taggedAreas; foreach (ResizableFrame *area, areas()) { 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 (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()) { 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_KGEOMAP 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; - Q_FOREACH (ListSelect *ls, m_optionList) { + for (ListSelect *ls : 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_KGEOMAP 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()); - Q_FOREACH (const ListSelect *ls, m_optionList) { + for (const ListSelect *ls : 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_KGEOMAP const KGeoMap::GeoCoordinates::Pair 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. - Q_FOREACH (ListSelect *ls, m_optionList) { + for (ListSelect *ls : 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")); } - Q_FOREACH (ListSelect *ls, m_optionList) { + for (ListSelect *ls : 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()); - Q_FOREACH (ListSelect *ls, m_optionList) { + for (ListSelect *ls : 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, SIGNAL(triggered()), m_optionList.at(0), SLOT(slotSortDate())); connect(alphaTreeSort, SIGNAL(triggered()), m_optionList.at(0), SLOT(slotSortAlphaTree())); connect(alphaFlatSort, SIGNAL(triggered()), m_optionList.at(0), SLOT(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, SIGNAL(triggered()), &ShowSelectionOnlyManager::instance(), SLOT(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(), SIGNAL(itemRemoved(DB::Category *, QString)), this, SLOT(slotDeleteOption(DB::Category *, QString))); connect(DB::ImageDB::instance()->categoryCollection(), SIGNAL(itemRenamed(DB::Category *, QString, QString)), this, SLOT(slotRenameOption(DB::Category *, QString, QString))); 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(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(bool checkOptions) { if (m_setup == InputSingleImageConfigMode) { writeToInfo(); for (int i = 0; i < m_editList.count(); ++i) { if (*(m_origList[i]) != m_editList[i]) return true; } return m_areasChanged; } else if (m_setup == InputMultiImageConfigMode) { - bool changed = (!m_startDate->date().isNull()) || - (!m_endDate->date().isNull()) || - (!m_imageLabel->text().isEmpty()) || - (m_description->toPlainText() != m_firstDescription) || - m_ratingChanged; + bool changed = (!m_startDate->date().isNull()) || (!m_endDate->date().isNull()) || (!m_imageLabel->text().isEmpty()) || (m_description->toPlainText() != m_firstDescription) || m_ratingChanged; if (checkOptions) { - Q_FOREACH (ListSelect *ls, m_optionList) { + for (ListSelect *ls : m_optionList) { if (!(changedOptions(ls).isEmpty())) return true; } } return changed; } 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; - Q_FOREACH (QWidget *widget, orderedList) { + for (QWidget *widget : orderedList) { if (prev) { setTabOrder(prev, widget); } else { first = widget; } prev = widget; } if (first) { setTabOrder(prev, first); } // Finally set focus on the first list select - Q_FOREACH (QWidget *widget, orderedList) { + for (QWidget *widget : 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_KGEOMAP // 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), SLOT(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), SLOT(slotSortAlphaFlat())); action->setText(i18n("Sort Alphabetically (Flat)")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-MRU"), m_optionList.at(0), SLOT(slotSortDate())); action->setText(i18n("Sort Most Recently Used")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-sort"), m_optionList.at(0), SLOT(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(), SLOT(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, SLOT(slotNext())); action->setText(i18n("Annotate Next")); m_actions->setDefaultShortcut(action, Qt::Key_PageDown); action = m_actions->addAction(QString::fromLatin1("annotationdialog-prev-image"), m_preview, SLOT(slotPrev())); action->setText(i18n("Annotate Previous")); m_actions->setDefaultShortcut(action, Qt::Key_PageUp); action = m_actions->addAction(QString::fromLatin1("annotationdialog-OK-dialog"), this, SLOT(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, SLOT(slotDeleteImage())); action->setText(i18n("Delete")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete); action = m_actions->addAction(QString::fromLatin1("annotationdialog-copy-previous"), this, SLOT(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, SLOT(rotateLeft())); action->setText(i18n("Rotate counterclockwise")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-right"), m_preview, SLOT(rotateRight())); action->setText(i18n("Rotate clockwise")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-viewer"), this, SLOT(togglePreview())); action->setText(i18n("Toggle fullscreen preview")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Space); foreach (QAction *action, m_actions->actions()) { 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(ListSelect *listSel, const DB::ImageInfoList &images) { const QString category = listSel->category(); const StringSet allItems = DB::ImageDB::instance()->categoryCollection()->categoryForName(category)->itemsInclCategories().toSet(); 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 { foreach (const QString &item, itemsOnThisImage) { - if (! itemsOnAllImages.contains(item) && - ! itemsOnSomeImages.contains(item)) { + if (!itemsOnAllImages.contains(item) && !itemsOnSomeImages.contains(item)) { itemsOnSomeImages += item; } } foreach (const QString &item, itemsOnAllImages) { - if (! itemsOnThisImage.contains(item)) { + if (!itemsOnThisImage.contains(item)) { itemsOnAllImages -= item; } } } } 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(m_setup == InputSingleImageConfigMode); 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) { - Q_FOREACH (ListSelect *ls, m_optionList) { + for (ListSelect *ls : m_optionList) { ls->slotReturn(); } - Q_FOREACH(ListSelect *ls, m_optionList) { + for (ListSelect *ls : 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 QMap> existingAreas = currentInfo.taggedAreas(); QMap> alteredAreas = taggedAreas(); for (auto catIt = existingAreas.constBegin(); catIt != existingAreas.constEnd(); ++catIt) { const QString &categoryName = catIt.key(); const QMap &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 foreach (ResizableFrame *area, areas()) { 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); foreach (ResizableFrame *area, areas()) { 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); foreach (ResizableFrame *area, areas()) { 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) { foreach (ResizableFrame *area, areas()) { 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 foreach (ResizableFrame *area, areas()) { 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 { foreach (ResizableFrame *area, areas()) { 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? foreach (const ResizableFrame *area, areas()) { 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; foreach (const ResizableFrame *area, areas()) { 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_KGEOMAP void AnnotationDialog::Dialog::updateMapForCurrentImage() { if (m_setup != InputSingleImageConfigMode) { return; } if (m_editList[m_current].coordinates().hasCoordinates()) { m_annotationMap->setCenter(m_editList[m_current]); m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::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, SLOT(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::MapView::MapStatus::Loading); m_cancelMapLoading = false; m_mapLoadingProgress->setMaximum(m_editList.count()); m_mapLoadingProgress->show(); m_cancelMapLoadingButton->show(); int processedImages = 0; int imagesWithCoordinates = 0; foreach (DB::ImageInfo info, m_editList) { processedImages++; m_mapLoadingProgress->setValue(processedImages); // keep things responsive by processing events manually: QApplication::processEvents(); if (info.coordinates().hasCoordinates()) { m_annotationMap->addImage(info); imagesWithCoordinates++; } // m_cancelMapLoading is set to true by clicking the "Cancel" button if (m_cancelMapLoading) { m_annotationMap->clear(); break; } } // 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::MapView::MapStatus::ImageHasNoCoordinates); } else { if (m_setup == SearchMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SearchCoordinates); } else { if (mapHasImages) { if (!allImagesHaveCoordinates) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SomeImagesHaveNoCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::NoImagesHaveNoCoordinates); } } } if (m_setup != SearchMode) { m_annotationMap->zoomToMarkers(); updateMapForCurrentImage(); } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ListViewItemHider.cpp b/AnnotationDialog/ListViewItemHider.cpp index 2e1c06b8..1111e613 100644 --- a/AnnotationDialog/ListViewItemHider.cpp +++ b/AnnotationDialog/ListViewItemHider.cpp @@ -1,127 +1,128 @@ -/* Copyright (C) 2003-2015 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 . */ // Qt includes #include #include #include // Local includes #include "ListSelect.h" #include "ListViewItemHider.h" using namespace Utilities; /** * \class AnnotationDialog::ListViewItemHider * \brief Helper class, used to hide/show listview items * * This is a helper class that is used to hide items in a listview. A leaf * will be hidden if then subclass implemented method \ref * shouldItemBeShown returns true for the given item. A parent node is * hidden if none of the children are shown, and \ref shouldItemBeShown * also returns false for the parent itself. */ /** * \class AnnotationDialog::ListViewTextMatchHider * \brief Helper class for showing items matching a given text string. */ /** * \class AnnotationDialog::ListViewCheckedHider * \brief Helper class for only showing items that are selected. */ bool AnnotationDialog::ListViewItemHider::setItemsVisible(QTreeWidgetItem *parentItem) { bool anyChildrenVisible = false; for (int i = 0; i < parentItem->childCount(); ++i) { QTreeWidgetItem *item = parentItem->child(i); bool anySubChildrenVisible = setItemsVisible(item); bool itemVisible = anySubChildrenVisible || shouldItemBeShown(item); item->setHidden(!itemVisible); anyChildrenVisible |= itemVisible; } return anyChildrenVisible; } AnnotationDialog::ListViewTextMatchHider::ListViewTextMatchHider(const QString &text, const AnnotationDialog::MatchType mt, QTreeWidget *listView) : m_text(text) , m_matchType(mt) { setItemsVisible(listView->invisibleRootItem()); } bool AnnotationDialog::ListViewTextMatchHider::shouldItemBeShown(QTreeWidgetItem *item) { // Be sure not to display the "untagged image" tag if configured if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && !Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (Settings::SettingsData::instance()->untaggedCategory() == dynamic_cast(item->treeWidget()->parent())->category()) { if (item->text(0) == Settings::SettingsData::instance()->untaggedTag()) { return false; } } } switch (m_matchType) { case AnnotationDialog::MatchFromBeginning: return item->text(0).toLower().startsWith(m_text.toLower()); case AnnotationDialog::MatchFromWordStart: { QStringList itemWords = item->text(0).toLower().split(QRegExp(QString::fromUtf8("\\W+")), QString::SkipEmptyParts); QStringList searchWords = m_text.toLower().split(QRegExp(QString::fromUtf8("\\W+")), QString::SkipEmptyParts); // all search words ... - Q_FOREACH (const auto searchWord, searchWords) { + for (const auto searchWord : searchWords) { bool found = false; // ... must match at least one word of the item - Q_FOREACH (const auto itemWord, itemWords) { + for (const auto itemWord : itemWords) { if (itemWord.startsWith(searchWord)) { found = true; break; } } if (!found) { return false; } } return true; } case AnnotationDialog::MatchAnywhere: return item->text(0).toLower().contains(m_text.toLower()); } // gcc believes this could be reached Q_ASSERT(false); return false; } bool AnnotationDialog::ListViewCheckedHider::shouldItemBeShown(QTreeWidgetItem *item) { return item->checkState(0) != Qt::Unchecked; } AnnotationDialog::ListViewCheckedHider::ListViewCheckedHider(QTreeWidget *listView) { setItemsVisible(listView->invisibleRootItem()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ResizableFrame.cpp b/AnnotationDialog/ResizableFrame.cpp index 040847d8..e4c4e7b4 100644 --- a/AnnotationDialog/ResizableFrame.cpp +++ b/AnnotationDialog/ResizableFrame.cpp @@ -1,548 +1,549 @@ /* Copyright (C) 2014-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 . */ // The basic resizable QFrame has been shamelessly stolen from // http://qt-project.org/forums/viewthread/24104 // Big thanks to Mr. kripton :-) #include "ResizableFrame.h" // Local includes #include "AreaTagSelectDialog.h" #include "CompletableLineEdit.h" #include "ImagePreview.h" #include "ImagePreviewWidget.h" // Qt includes #include #include #include #include #include #include // KDE includes #include #include namespace { constexpr int SCALE_TOP = 0b00000001; constexpr int SCALE_BOTTOM = 0b00000010; constexpr int SCALE_RIGHT = 0b00000100; constexpr int SCALE_LEFT = 0b00001000; constexpr int MOVE = 0b10000000; const QString STYLE_UNASSOCIATED = QString::fromUtf8( "AnnotationDialog--ResizableFrame { color: rgb(255,0,0); }" "AnnotationDialog--ResizableFrame:hover { background-color: rgb(255,255,255,30); }"); const QString STYLE_PROPOSED = QString::fromUtf8( "AnnotationDialog--ResizableFrame { color: rgb(255,255,0); }" "AnnotationDialog--ResizableFrame:hover { background-color: rgb(255,255,255,30); }"); const QString STYLE_ASSOCIATED = QString::fromUtf8( "AnnotationDialog--ResizableFrame { color: rgb(0,255,0); }" "AnnotationDialog--ResizableFrame:hover { background-color: rgb(255,255,255,30); }"); } AnnotationDialog::ResizableFrame::ResizableFrame(QWidget *parent) : QFrame(parent) { m_preview = dynamic_cast(parent); m_previewWidget = dynamic_cast(m_preview->parentWidget()); setFrameShape(QFrame::Box); setMouseTracking(true); setStyleSheet(STYLE_UNASSOCIATED); m_removeAct = new QAction( i18nc("area of an image; rectangle that is overlayed upon the image", "Remove area"), this); connect(m_removeAct, &QAction::triggered, this, &ResizableFrame::remove); m_removeTagAct = new QAction(this); connect(m_removeTagAct, &QAction::triggered, this, &ResizableFrame::removeTag); } AnnotationDialog::ResizableFrame::~ResizableFrame() { } void AnnotationDialog::ResizableFrame::setActualCoordinates(QRect actualCoordinates) { m_actualCoordinates = actualCoordinates; } QRect AnnotationDialog::ResizableFrame::actualCoordinates() const { return m_actualCoordinates; } void AnnotationDialog::ResizableFrame::getMinMaxCoordinates() { // Get the maximal area to drag or resize the frame m_minMaxCoordinates = m_preview->minMaxAreaPreview(); // Add one pixel (width of the frame) m_minMaxCoordinates.setWidth(m_minMaxCoordinates.width() + 1); m_minMaxCoordinates.setHeight(m_minMaxCoordinates.height() + 1); } void AnnotationDialog::ResizableFrame::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { m_dragStartPosition = event->pos(); m_dragStartGeometry = geometry(); // Just in case this will be a drag/resize and not just a click getMinMaxCoordinates(); } } void AnnotationDialog::ResizableFrame::mouseMoveEvent(QMouseEvent *event) { static int moveAction = 0; if (!(event->buttons() & Qt::LeftButton)) { // No drag, just change the cursor and return if (event->x() <= 3 && event->y() <= 3) { moveAction = SCALE_TOP | SCALE_LEFT; setCursor(Qt::SizeFDiagCursor); } else if (event->x() <= 3 && event->y() >= height() - 3) { moveAction = SCALE_BOTTOM | SCALE_LEFT; setCursor(Qt::SizeBDiagCursor); } else if (event->x() >= width() - 3 && event->y() <= 3) { moveAction = SCALE_TOP | SCALE_RIGHT; setCursor(Qt::SizeBDiagCursor); } else if (event->x() >= width() - 3 && event->y() >= height() - 3) { moveAction = SCALE_BOTTOM | SCALE_RIGHT; setCursor(Qt::SizeFDiagCursor); } else if (event->x() <= 3) { moveAction = SCALE_LEFT; setCursor(Qt::SizeHorCursor); } else if (event->x() >= width() - 3) { moveAction = SCALE_RIGHT; setCursor(Qt::SizeHorCursor); } else if (event->y() <= 3) { moveAction = SCALE_TOP; setCursor(Qt::SizeVerCursor); } else if (event->y() >= height() - 3) { moveAction = SCALE_BOTTOM; setCursor(Qt::SizeVerCursor); } else { moveAction = MOVE; setCursor(Qt::SizeAllCursor); } return; } int x; int y; int w; int h; h = height(); if (moveAction & MOVE) { x = m_dragStartGeometry.left() - (m_dragStartPosition.x() - event->x()); y = m_dragStartGeometry.top() - (m_dragStartPosition.y() - event->y()); w = width(); // Be sure not to move out of the preview if (x < m_minMaxCoordinates.left()) { x = m_minMaxCoordinates.left(); } if (y < m_minMaxCoordinates.top()) { y = m_minMaxCoordinates.top(); } if (x + w > m_minMaxCoordinates.width()) { x = m_minMaxCoordinates.width() - w; } if (y + h > m_minMaxCoordinates.height()) { y = m_minMaxCoordinates.height() - h; } } else { // initialize with the "missing" values when only one direction is manipulated: x = m_dragStartGeometry.left(); y = m_dragStartGeometry.top(); w = m_dragStartGeometry.width(); if (moveAction & SCALE_TOP) { y = m_dragStartGeometry.top() - (m_dragStartPosition.y() - event->y()); if (y >= geometry().y() + geometry().height()) { y = m_dragStartGeometry.top() + m_dragStartGeometry.height(); moveAction ^= SCALE_BOTTOM | SCALE_TOP; } if (y < m_minMaxCoordinates.top()) { y = m_minMaxCoordinates.top(); h = m_dragStartGeometry.top() + m_dragStartGeometry.height() - m_minMaxCoordinates.y(); } else { h = height() + (m_dragStartPosition.y() - event->y()); } } else if (moveAction & SCALE_BOTTOM) { y = m_dragStartGeometry.top(); h = event->y(); if (h <= 0) { h = 0; m_dragStartPosition.setY(0); moveAction ^= SCALE_BOTTOM | SCALE_TOP; } if (y + h > m_minMaxCoordinates.height()) { h = m_minMaxCoordinates.height() - y; } } if (moveAction & SCALE_RIGHT) { x = m_dragStartGeometry.left(); w = event->x(); if (w <= 0) { w = 0; m_dragStartPosition.setX(0); moveAction ^= SCALE_RIGHT | SCALE_LEFT; } if (x + w > m_minMaxCoordinates.width()) { w = m_minMaxCoordinates.width() - x; } } else if (moveAction & SCALE_LEFT) { x = m_dragStartGeometry.left() - (m_dragStartPosition.x() - event->x()); if (x >= geometry().left() + geometry().width()) { x = m_dragStartGeometry.left() + m_dragStartGeometry.width(); moveAction ^= SCALE_RIGHT | SCALE_LEFT; } if (x < m_minMaxCoordinates.left()) { x = m_minMaxCoordinates.left(); w = m_dragStartGeometry.x() + m_dragStartGeometry.width() - m_minMaxCoordinates.x(); } else { w = m_dragStartGeometry.width() + (m_dragStartPosition.x() - event->x()); } } } setGeometry(x, y, w, h); m_dragStartGeometry = geometry(); if (!m_tagData.first.isEmpty()) { // If we change an area with an associated tag: // tell the Dialog we made a change that should be saved (set the dirty marker) m_dialog->areaChanged(); } } void AnnotationDialog::ResizableFrame::checkGeometry() { // If this is called when the area is created, we don't have it yet getMinMaxCoordinates(); // First cache the current geometry int x; int y; int w; int h; x = geometry().x(); y = geometry().y(); w = geometry().width(); h = geometry().height(); // Be sure no non-visible area is created by resizing to a height or width of 0 // A height and width of 3 is the minimum to have more than a line if (geometry().height() < 3) { y = y - 1; h = 3; } if (geometry().width() < 3) { x = x - 1; w = 3; } // Probably, the above tweaking moved the area out of the preview area if (x < m_minMaxCoordinates.left()) { x = m_minMaxCoordinates.left(); } if (y < m_minMaxCoordinates.top()) { y = m_minMaxCoordinates.top(); } if (x + w > m_minMaxCoordinates.width()) { x = m_minMaxCoordinates.width() - w; } if (y + h > m_minMaxCoordinates.height()) { y = m_minMaxCoordinates.height() - h; } // If anything has been changed, set the updated geometry if (geometry().x() != x || geometry().y() != y || geometry().width() != w || geometry().height() != h) { setGeometry(x, y, w, h); } } void AnnotationDialog::ResizableFrame::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { checkGeometry(); setActualCoordinates(m_preview->areaPreviewToActual(geometry())); } } void AnnotationDialog::ResizableFrame::contextMenuEvent(QContextMenuEvent *) { showContextMenu(); } QAction *AnnotationDialog::ResizableFrame::createAssociateTagAction( const QPair &tag, QString prefix) { QString actionText; if (!prefix.isEmpty()) { actionText = i18nc("%1 is a prefix like 'Associate with', " "%2 is the tag name and %3 is the tag's category", "%1 %2 (%3)", prefix, tag.second, tag.first); } else { actionText = i18nc("%1 is the tag name and %2 is the tag's category", "%1 (%2)", tag.second, tag.first); } QAction *action = new QAction(actionText, this); QStringList data; data << tag.first << tag.second; action->setData(data); return action; } void AnnotationDialog::ResizableFrame::associateTag() { QAction *action = qobject_cast(sender()); Q_ASSERT(action != nullptr); associateTag(action); } void AnnotationDialog::ResizableFrame::associateTag(QAction *action) { setTagData(action->data().toStringList()[0], action->data().toStringList()[1]); } void AnnotationDialog::ResizableFrame::setTagData(QString category, QString tag, ChangeOrigin changeOrigin) { QPair selectedData = QPair(category, tag); // check existing areas for consistency - Q_FOREACH (ResizableFrame *area, m_dialog->areas()) { + for (ResizableFrame *area : m_dialog->areas()) { if (area->isTidied()) { continue; } if (area->tagData() == selectedData) { if (KMessageBox::Cancel == KMessageBox::warningContinueCancel(m_preview, i18n("

%1 has already been tagged in another area on this image.

" "

If you continue, the previous tag will be removed...

", tag), i18n("Replace existing area?"))) { // don't execute setTagData return; } // replace existing tag area->removeTagData(); } } // Add the data to this area m_tagData = selectedData; // Update the tool tip setToolTip(tag + QString::fromUtf8(" (") + category + QString::fromUtf8(")")); // Set the color to "associated" setStyleSheet(STYLE_ASSOCIATED); // Remove the associated tag from the tag candidate list m_dialog->removeTagFromCandidateList(m_tagData.first, m_tagData.second); if (changeOrigin != AutomatedChange) { // Tell the dialog an area has been changed m_dialog->areaChanged(); } } void AnnotationDialog::ResizableFrame::removeTag() { // Deselect the tag m_dialog->listSelectForCategory(m_tagData.first)->deselectTag(m_tagData.second); // Delete the tag data from this area removeTagData(); } void AnnotationDialog::ResizableFrame::removeTagData() { // Delete the data m_tagData.first.clear(); m_tagData.second.clear(); setToolTip(QString()); // Set the color to "un-associated" or "proposed" if (m_proposedTagData.first.isEmpty()) { setStyleSheet(STYLE_UNASSOCIATED); } else { setStyleSheet(STYLE_PROPOSED); } // Tell the dialog an area has been changed m_dialog->areaChanged(); } void AnnotationDialog::ResizableFrame::remove() { if (!m_tagData.first.isEmpty()) { // Deselect the tag m_dialog->listSelectForCategory(m_tagData.first)->deselectTag(m_tagData.second); } // Delete the area this->deleteLater(); } void AnnotationDialog::ResizableFrame::showContextMenu() { // Display a dialog where a tag can be selected directly QString category = m_previewWidget->defaultPositionableCategory(); // this is not a memory leak: AreaTagSelectDialog is a regular parented dialog AreaTagSelectDialog *tagMenu = new AreaTagSelectDialog( this, m_dialog->listSelectForCategory(category), m_preview->grabAreaImage(geometry()), m_dialog); tagMenu->show(); tagMenu->moveToArea(mapToGlobal(QPoint(0, 0))); tagMenu->exec(); } void AnnotationDialog::ResizableFrame::setDialog(Dialog *dialog) { m_dialog = dialog; } QPair AnnotationDialog::ResizableFrame::tagData() const { return m_tagData; } void AnnotationDialog::ResizableFrame::setProposedTagData(QPair tagData) { m_proposedTagData = tagData; setStyleSheet(STYLE_PROPOSED); } QPair AnnotationDialog::ResizableFrame::proposedTagData() const { return m_proposedTagData; } void AnnotationDialog::ResizableFrame::removeProposedTagData() { m_proposedTagData = QPair(); setStyleSheet(STYLE_UNASSOCIATED); setToolTip(QString()); } void AnnotationDialog::ResizableFrame::addTagActions(QMenu *menu) { // Let's see if we already have an associated tag if (!m_tagData.first.isEmpty()) { m_removeTagAct->setText( i18nc("As in: remove tag %1 in category %2 [from this marked area of the image]", "Remove tag %1 (%2)", m_tagData.second, m_tagData.first)); menu->addAction(m_removeTagAct); } else { // Handle the last selected positionable tag (if we have one) QPair lastSelectedPositionableTag = m_dialog->lastSelectedPositionableTag(); if (!lastSelectedPositionableTag.first.isEmpty()) { QAction *associateLastSelectedTagAction = createAssociateTagAction( lastSelectedPositionableTag, i18n("Associate with")); connect(associateLastSelectedTagAction, &QAction::triggered, this, QOverload<>::of(&ResizableFrame::associateTag)); menu->addAction(associateLastSelectedTagAction); } // Handle all positionable tag candidates QList> positionableTagCandidates = m_dialog->positionableTagCandidates(); // If we have a last selected positionable tag: remove it positionableTagCandidates.removeAt(positionableTagCandidates.indexOf(lastSelectedPositionableTag)); // If we still have candidates: if (positionableTagCandidates.length() > 0) { if (positionableTagCandidates.length() == 1 && lastSelectedPositionableTag.first.isEmpty()) { // Add a single action QAction *associateOnlyCandidateAction = createAssociateTagAction( positionableTagCandidates[0], i18nc("As in: associate [this marked area of the image] with one of the " "following choices/menu items", "Associate with")); connect(associateOnlyCandidateAction, &QAction::triggered, this, QOverload<>::of(&ResizableFrame::associateTag)); menu->addAction(associateOnlyCandidateAction); } else { // Create a new menu for all other tags QMenu *submenu = menu->addMenu( i18nc("As in: associate [this marked area of the image] with one of the " "following choices/menu items", "Associate with")); for (const QPair &tag : positionableTagCandidates) { submenu->addAction(createAssociateTagAction(tag)); } connect(submenu, &QMenu::triggered, this, QOverload::of(&ResizableFrame::associateTag)); } } } QAction *sep = menu->addSeparator(); // clicking the separator should not dismiss the menu: sep->setEnabled(false); // Append the "Remove area" action menu->addAction(m_removeAct); } void AnnotationDialog::ResizableFrame::markTidied() { m_tidied = true; } bool AnnotationDialog::ResizableFrame::isTidied() const { return m_tidied; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/FlatCategoryModel.cpp b/Browser/FlatCategoryModel.cpp index 0d7ad9a6..889726ad 100644 --- a/Browser/FlatCategoryModel.cpp +++ b/Browser/FlatCategoryModel.cpp @@ -1,74 +1,75 @@ -/* 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 "FlatCategoryModel.h" -#include +#include "DB/ImageDB.h" #include Browser::FlatCategoryModel::FlatCategoryModel(const DB::CategoryPtr &category, const DB::ImageSearchInfo &info) : AbstractCategoryModel(category, info) { if (hasNoneEntry()) m_items.append(DB::ImageDB::NONE()); QStringList items = m_category->itemsInclCategories(); items.sort(); - Q_FOREACH (const QString &name, items) { + for (const QString &name : items) { const int imageCount = m_images.contains(name) ? m_images[name].count : 0; const int videoCount = m_videos.contains(name) ? m_videos[name].count : 0; if (imageCount + videoCount > 0) m_items.append(name); } } int Browser::FlatCategoryModel::rowCount(const QModelIndex &index) const { if (!index.isValid()) return m_items.count(); else return 0; } int Browser::FlatCategoryModel::columnCount(const QModelIndex &) const { return 1; } QModelIndex Browser::FlatCategoryModel::index(int row, int column, const QModelIndex &parent) const { if (row >= 0 && row < rowCount(parent) && column >= 0 && column < columnCount(parent)) return createIndex(row, column); else return QModelIndex(); } QModelIndex Browser::FlatCategoryModel::parent(const QModelIndex &) const { return QModelIndex(); } QString Browser::FlatCategoryModel::indexToName(const QModelIndex &index) const { return m_items[index.row()]; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Copyright_Header_Template.txt b/Copyright_Header_Template.txt index db00d223..04c76c67 100644 --- a/Copyright_Header_Template.txt +++ b/Copyright_Header_Template.txt @@ -1,18 +1,18 @@ -/* Copyright (C) 2019 The KPhotoAlbum Development Team +/* Copyright (C) 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 . */ diff --git a/DB/AndCategoryMatcher.cpp b/DB/AndCategoryMatcher.cpp index a7941487..95f41609 100644 --- a/DB/AndCategoryMatcher.cpp +++ b/DB/AndCategoryMatcher.cpp @@ -1,38 +1,39 @@ -/* 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 "AndCategoryMatcher.h" +#include "AndCategoryMatcher.h" #include "ImageInfo.h" #include "Logging.h" bool DB::AndCategoryMatcher::eval(ImageInfoPtr info, QMap &alreadyMatched) { - Q_FOREACH (CategoryMatcher *subMatcher, mp_elements) { + for (CategoryMatcher *subMatcher : mp_elements) { if (!subMatcher->eval(info, alreadyMatched)) return false; } return true; } void DB::AndCategoryMatcher::debug(int level) const { qCDebug(DBCategoryMatcherLog, "%sAND:", qPrintable(spaces(level))); ContainerCategoryMatcher::debug(level + 1); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/CategoryItem.cpp b/DB/CategoryItem.cpp index d85faebb..0511179d 100644 --- a/DB/CategoryItem.cpp +++ b/DB/CategoryItem.cpp @@ -1,64 +1,66 @@ -/* 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 "CategoryItem.h" #include DB::CategoryItem::~CategoryItem() { for (QList::ConstIterator it = mp_subcategories.constBegin(); it != mp_subcategories.constEnd(); ++it) { delete *it; } } DB::CategoryItem *DB::CategoryItem::clone() const { CategoryItem *result = new CategoryItem(mp_name); for (QList::ConstIterator it = mp_subcategories.constBegin(); it != mp_subcategories.constEnd(); ++it) { result->mp_subcategories.append((*it)->clone()); } return result; } bool DB::CategoryItem::isDescendentOf(const QString &child, const QString &parent) const { for (QList::ConstIterator it = mp_subcategories.begin(); it != mp_subcategories.end(); ++it) { if (mp_name == parent) { if ((*it)->hasChild(child)) return true; } else { if ((*it)->isDescendentOf(child, parent)) return true; } } return false; } bool DB::CategoryItem::hasChild(const QString &child) const { if (mp_name == child) return true; - Q_FOREACH (const CategoryItem *subcategory, mp_subcategories) { + for (const CategoryItem *subcategory : mp_subcategories) { if (subcategory->hasChild(child)) return true; } return false; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/GroupCounter.cpp b/DB/GroupCounter.cpp index 53c0d0ff..cee1474b 100644 --- a/DB/GroupCounter.cpp +++ b/DB/GroupCounter.cpp @@ -1,114 +1,115 @@ -/* 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. + 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 "GroupCounter.h" - #include "ImageDB.h" #include "MemberMap.h" -#include +#include "Utilities/StringSet.h" + using namespace DB; /** * \class DB::GroupCounter * \brief Utility class to help counting matches for member groups. * * This class is used to count the member group matches when * categorizing. The class is instantiating with the category we currently * are counting items for. * * The class builds the inverse member map, that is a map pointing from items * to parent. * * As an example, imagine we have the following member map (stored in the * variable groupToMemberMap in the code): * \code * { USA |-> [Chicago, Santa Clara], * California |-> [Santa Clara, Los Angeles] } * \endcode * * The inverse map (stored in m_memberToGroup in the code ) will then look * like this: * \code * { Chicago |-> [USA], * Sanata Clara |-> [ USA, California ], * Los Angeless |-> [ California ] } * \endcode */ GroupCounter::GroupCounter(const QString &category) { const MemberMap map = DB::ImageDB::instance()->memberMap(); QMap groupToMemberMap = map.groupMap(category); m_memberToGroup.reserve(2729 /* A large prime */); m_groupCount.reserve(2729 /* A large prime */); // Populate the m_memberToGroup map for (QMap::Iterator groupToMemberIt = groupToMemberMap.begin(); groupToMemberIt != groupToMemberMap.end(); ++groupToMemberIt) { StringSet members = groupToMemberIt.value(); QString group = groupToMemberIt.key(); - Q_FOREACH (const auto &member, members) { + for (const auto &member : members) { m_memberToGroup[member].append(group); } m_groupCount.insert(group, CountWithRange()); } } /** * categories is the selected categories for one image, members may be Las Vegas, Chicago, and Los Angeles if the * category in question is Places. * This function then increases m_groupCount with 1 for each of the groups the relavant items belongs to * Las Vegas might increase the m_groupCount[Nevada] by one. * The tricky part is to avoid increasing it by more than 1 per image, that is what the countedGroupDict is * used for. */ void GroupCounter::count(const StringSet &categories, const ImageDate &date) { static StringSet countedGroupDict; countedGroupDict.clear(); for (StringSet::const_iterator categoryIt = categories.begin(); categoryIt != categories.end(); ++categoryIt) { if (m_memberToGroup.contains(*categoryIt)) { const QStringList groups = m_memberToGroup[*categoryIt]; for (const QString &group : groups) { if (!countedGroupDict.contains(group)) { countedGroupDict.insert(group); m_groupCount[group].add(date); } } } // The item Nevada should itself go into the group Nevada. if (!countedGroupDict.contains(*categoryIt) && m_groupCount.contains(*categoryIt)) { countedGroupDict.insert(*categoryIt); m_groupCount[*categoryIt].add(date); } } } QMap GroupCounter::result() { QMap res; for (QHash::const_iterator it = m_groupCount.constBegin(); it != m_groupCount.constEnd(); ++it) { if (it.value().count != 0) res.insert(it.key(), it.value()); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageSearchInfo.cpp b/DB/ImageSearchInfo.cpp index 368e628c..ae9ed30d 100644 --- a/DB/ImageSearchInfo.cpp +++ b/DB/ImageSearchInfo.cpp @@ -1,632 +1,633 @@ /* 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 "ImageSearchInfo.h" #include "AndCategoryMatcher.h" #include "CategoryMatcher.h" #include "ContainerCategoryMatcher.h" #include "ExactCategoryMatcher.h" #include "ImageDB.h" #include "Logging.h" #include "NegationCategoryMatcher.h" #include "NoTagCategoryMatcher.h" #include "OrCategoryMatcher.h" #include "ValueCategoryMatcher.h" #include #include #include #include #include #include #include using namespace DB; static QAtomicInt s_matchGeneration; static int nextGeneration() { return s_matchGeneration++; } ImageSearchInfo::ImageSearchInfo() : m_matchGeneration(nextGeneration()) { } ImageSearchInfo::ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description) : m_date(date) , m_label(label) , m_description(description) , m_isNull(false) , m_matchGeneration(nextGeneration()) { } ImageSearchInfo::ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description, const QString &fnPattern) : m_date(date) , m_label(label) , m_description(description) , m_fnPattern(fnPattern) , m_isNull(false) , m_matchGeneration(nextGeneration()) { } QString ImageSearchInfo::label() const { return m_label; } QRegExp ImageSearchInfo::fnPattern() const { return m_fnPattern; } QString ImageSearchInfo::description() const { return m_description; } void ImageSearchInfo::checkIfNull() { if (m_compiled.valid || isNull()) return; if (m_date.isNull() && m_label.isEmpty() && m_description.isEmpty() && m_rating == -1 && m_megapixel == 0 && m_exifSearchInfo.isNull() && m_categoryMatchText.isEmpty() #ifdef HAVE_KGEOMAP && !m_regionSelection.first.hasCoordinates() && !m_regionSelection.second.hasCoordinates() #endif ) { m_isNull = true; } } bool ImageSearchInfo::isNull() const { return m_isNull; } bool ImageSearchInfo::isCacheable() const { return m_isCacheable; } void ImageSearchInfo::setCacheable(bool cacheable) { m_isCacheable = cacheable; } bool ImageSearchInfo::match(ImageInfoPtr info) const { if (m_isNull) return true; if (m_isCacheable && info->matchGeneration() == m_matchGeneration) return info->isMatched(); bool ok = doMatch(info); if (m_isCacheable) { info->setMatchGeneration(m_matchGeneration); info->setIsMatched(ok); } return ok; } bool ImageSearchInfo::doMatch(ImageInfoPtr info) const { if (!m_compiled.valid) compile(); // -------------------------------------------------- Rating //ok = ok && (_rating == -1 ) || ( _rating == info->rating() ); if (m_rating != -1) { switch (m_ratingSearchMode) { case 1: // Image rating at least selected if (m_rating > info->rating()) return false; break; case 2: // Image rating less than selected if (m_rating < info->rating()) return false; break; case 3: // Image rating not equal if (m_rating == info->rating()) return false; break; default: if (m_rating != info->rating()) return false; break; } } // -------------------------------------------------- Resolution if (m_megapixel && (m_megapixel * 1000000 > info->size().width() * info->size().height())) return false; if (m_max_megapixel && m_max_megapixel < m_megapixel && (m_max_megapixel * 1000000 < info->size().width() * info->size().height())) return false; // -------------------------------------------------- Date QDateTime actualStart = info->date().start(); QDateTime actualEnd = info->date().end(); if (m_date.start().isValid()) { if (actualEnd < m_date.start() || (m_date.end().isValid() && actualStart > m_date.end())) return false; } else if (m_date.end().isValid() && actualStart > m_date.end()) { return false; } // -------------------------------------------------- Label if (m_label.isEmpty() && info->label().indexOf(m_label) == -1) return false; // -------------------------------------------------- RAW if (m_searchRAW && !ImageManager::RAWImageDecoder::isRAW(info->fileName())) return false; #ifdef HAVE_KGEOMAP // Search for GPS Position if (m_usingRegionSelection) { if (!info->coordinates().hasCoordinates()) return false; float infoLat = info->coordinates().lat(); if (m_regionSelectionMinLat > infoLat || m_regionSelectionMaxLat < infoLat) return false; float infoLon = info->coordinates().lon(); if (m_regionSelectionMinLon > infoLon || m_regionSelectionMaxLon < infoLon) return false; } #endif // -------------------------------------------------- File name pattern if (!m_fnPattern.isEmpty() && m_fnPattern.indexIn(info->fileName().relative()) == -1) return false; // -------------------------------------------------- Options // alreadyMatched map is used to make it possible to search for // Jesper & None QMap alreadyMatched; for (CategoryMatcher *optionMatcher : m_compiled.categoryMatchers) { if (!optionMatcher->eval(info, alreadyMatched)) return false; } // -------------------------------------------------- Text if (!m_description.isEmpty()) { const QString &txt(info->description()); QStringList list = m_description.split(QChar::fromLatin1(' '), QString::SkipEmptyParts); - Q_FOREACH (const QString &word, list) { + for (const QString &word : list) { if (txt.indexOf(word, 0, Qt::CaseInsensitive) == -1) return false; } } // -------------------------------------------------- EXIF if (!m_exifSearchInfo.matches(info->fileName())) return false; return true; } QString ImageSearchInfo::categoryMatchText(const QString &name) const { return m_categoryMatchText[name]; } void ImageSearchInfo::setCategoryMatchText(const QString &name, const QString &value) { if (value.isEmpty()) { m_categoryMatchText.remove(name); } else { m_categoryMatchText[name] = value; } m_isNull = false; m_compiled.valid = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::addAnd(const QString &category, const QString &value) { // Escape literal "&"s in value by doubling it QString escapedValue = value; escapedValue.replace(QString::fromUtf8("&"), QString::fromUtf8("&&")); QString val = categoryMatchText(category); if (!val.isEmpty()) val += QString::fromLatin1(" & ") + escapedValue; else val = escapedValue; setCategoryMatchText(category, val); m_isNull = false; m_compiled.valid = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setRating(short rating) { m_rating = rating; m_isNull = false; m_compiled.valid = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setMegaPixel(short megapixel) { m_megapixel = megapixel; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setMaxMegaPixel(short max_megapixel) { m_max_megapixel = max_megapixel; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setSearchMode(int index) { m_ratingSearchMode = index; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setSearchRAW(bool searchRAW) { m_searchRAW = searchRAW; m_matchGeneration = nextGeneration(); } QString ImageSearchInfo::toString() const { QString res; bool first = true; for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { if (!it.value().isEmpty()) { if (first) first = false; else res += QString::fromLatin1(" / "); QString txt = it.value(); if (txt == ImageDB::NONE()) txt = i18nc("As in No persons, no locations etc. I do realize that translators may have problem with this, " "but I need some how to indicate the category, and users may create their own categories, so this is " "the best I can do - Jesper.", "No %1", it.key()); if (txt.contains(QString::fromLatin1("|"))) txt.replace(QString::fromLatin1("&"), QString::fromLatin1(" %1 ").arg(i18n("and"))); else txt.replace(QString::fromLatin1("&"), QString::fromLatin1(" / ")); txt.replace(QString::fromLatin1("|"), QString::fromLatin1(" %1 ").arg(i18n("or"))); txt.replace(QString::fromLatin1("!"), QString::fromLatin1(" %1 ").arg(i18n("not"))); txt.replace(ImageDB::NONE(), i18nc("As in no other persons, or no other locations. " "I do realize that translators may have problem with this, " "but I need some how to indicate the category, and users may create their own categories, so this is " "the best I can do - Jesper.", "No other %1", it.key())); res += txt.simplified(); } } return res; } void ImageSearchInfo::debug() { for (QMap::Iterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { qCDebug(DBCategoryMatcherLog) << it.key() << ", " << it.value(); } } // PENDING(blackie) move this into the Options class instead of having it here. void ImageSearchInfo::saveLock() const { KConfigGroup config = KSharedConfig::openConfig()->group(Settings::SettingsData::instance()->groupForDatabase("Privacy Settings")); config.writeEntry(QString::fromLatin1("label"), m_label); config.writeEntry(QString::fromLatin1("description"), m_description); config.writeEntry(QString::fromLatin1("categories"), m_categoryMatchText.keys()); for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { config.writeEntry(it.key(), it.value()); } config.sync(); } ImageSearchInfo ImageSearchInfo::loadLock() { KConfigGroup config = KSharedConfig::openConfig()->group(Settings::SettingsData::instance()->groupForDatabase("Privacy Settings")); ImageSearchInfo info; info.m_label = config.readEntry("label"); info.m_description = config.readEntry("description"); QStringList categories = config.readEntry(QString::fromLatin1("categories"), QStringList()); for (QStringList::ConstIterator it = categories.constBegin(); it != categories.constEnd(); ++it) { info.setCategoryMatchText(*it, config.readEntry(*it, QString())); } return info; } void ImageSearchInfo::compile() const { m_exifSearchInfo.search(); #ifdef HAVE_KGEOMAP // Prepare Search for GPS Position m_usingRegionSelection = m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates(); if (m_usingRegionSelection) { using std::max; using std::min; m_regionSelectionMinLat = min(m_regionSelection.first.lat(), m_regionSelection.second.lat()); m_regionSelectionMaxLat = max(m_regionSelection.first.lat(), m_regionSelection.second.lat()); m_regionSelectionMinLon = min(m_regionSelection.first.lon(), m_regionSelection.second.lon()); m_regionSelectionMaxLon = max(m_regionSelection.first.lon(), m_regionSelection.second.lon()); } #endif CompiledDataPrivate compiledData; for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { QString category = it.key(); QString matchText = it.value(); QStringList orParts = matchText.split(QString::fromLatin1("|"), QString::SkipEmptyParts); DB::ContainerCategoryMatcher *orMatcher = new DB::OrCategoryMatcher; - Q_FOREACH (QString orPart, orParts) { + for (QString orPart : orParts) { // Split by " & ", not only by "&", so that the doubled "&"s won't be used as a split point QStringList andParts = orPart.split(QString::fromLatin1(" & "), QString::SkipEmptyParts); DB::ContainerCategoryMatcher *andMatcher; bool exactMatch = false; bool negate = false; andMatcher = new DB::AndCategoryMatcher; - Q_FOREACH (QString str, andParts) { + for (QString str : andParts) { static QRegExp regexp(QString::fromLatin1("^\\s*!\\s*(.*)$")); if (regexp.exactMatch(str)) { // str is preceded with NOT negate = true; str = regexp.cap(1); } str = str.trimmed(); CategoryMatcher *valueMatcher; if (str == ImageDB::NONE()) { // mark AND-group as containing a "No other" condition exactMatch = true; continue; } else { valueMatcher = new DB::ValueCategoryMatcher(category, str); if (negate) valueMatcher = new DB::NegationCategoryMatcher(valueMatcher); } andMatcher->addElement(valueMatcher); } if (exactMatch) { DB::CategoryMatcher *exactMatcher = nullptr; // if andMatcher has exactMatch set, but no CategoryMatchers, then // matching "category / None" is what we want: if (andMatcher->mp_elements.count() == 0) { exactMatcher = new DB::NoTagCategoryMatcher(category); } else { ExactCategoryMatcher *noOtherMatcher = new ExactCategoryMatcher(category); if (andMatcher->mp_elements.count() == 1) noOtherMatcher->setMatcher(andMatcher->mp_elements[0]); else noOtherMatcher->setMatcher(andMatcher); exactMatcher = noOtherMatcher; } if (negate) exactMatcher = new DB::NegationCategoryMatcher(exactMatcher); orMatcher->addElement(exactMatcher); } else if (andMatcher->mp_elements.count() == 1) orMatcher->addElement(andMatcher->mp_elements[0]); else if (andMatcher->mp_elements.count() > 1) orMatcher->addElement(andMatcher); } CategoryMatcher *matcher = nullptr; if (orMatcher->mp_elements.count() == 1) matcher = orMatcher->mp_elements[0]; else if (orMatcher->mp_elements.count() > 1) matcher = orMatcher; if (matcher) { compiledData.categoryMatchers.append(matcher); if (DBCategoryMatcherLog().isDebugEnabled()) { qCDebug(DBCategoryMatcherLog) << "Matching text '" << matchText << "' in category " << category << ":"; matcher->debug(0); qCDebug(DBCategoryMatcherLog) << "."; } } } compiledData.valid = true; std::swap(m_compiled, compiledData); } void ImageSearchInfo::debugMatcher() const { if (!m_compiled.valid) compile(); qCDebug(DBCategoryMatcherLog, "And:"); for (CategoryMatcher *optionMatcher : m_compiled.categoryMatchers) { optionMatcher->debug(1); } } QList> ImageSearchInfo::query() const { if (!m_compiled.valid) compile(); // Combine _optionMachers to one list of lists in Disjunctive // Normal Form and return it. QList::Iterator it = m_compiled.categoryMatchers.begin(); QList> result; if (it == m_compiled.categoryMatchers.end()) return result; result = convertMatcher(*it); ++it; for (; it != m_compiled.categoryMatchers.end(); ++it) { QList> current = convertMatcher(*it); QList> oldResult = result; result.clear(); for (QList resultIt : oldResult) { for (QList currentIt : current) { QList tmp; tmp += resultIt; tmp += currentIt; result.append(tmp); } } } return result; } Utilities::StringSet ImageSearchInfo::findAlreadyMatched(const QString &group) const { Utilities::StringSet result; QString str = categoryMatchText(group); if (str.contains(QString::fromLatin1("|"))) { return result; } QStringList list = str.split(QString::fromLatin1("&"), QString::SkipEmptyParts); - Q_FOREACH (QString part, list) { + for (QString part : list) { QString nm = part.trimmed(); if (!nm.contains(QString::fromLatin1("!"))) result.insert(nm); } return result; } QList ImageSearchInfo::extractAndMatcher(CategoryMatcher *matcher) const { QList result; AndCategoryMatcher *andMatcher; SimpleCategoryMatcher *simpleMatcher; if ((andMatcher = dynamic_cast(matcher))) { for (CategoryMatcher *child : andMatcher->mp_elements) { SimpleCategoryMatcher *simpleMatcher = dynamic_cast(child); Q_ASSERT(simpleMatcher); result.append(simpleMatcher); } } else if ((simpleMatcher = dynamic_cast(matcher))) result.append(simpleMatcher); else Q_ASSERT(false); return result; } /** Convert matcher to Disjunctive Normal Form. * * @return OR-list of AND-lists. (e.g. OR(AND(a,b),AND(c,d))) */ QList> ImageSearchInfo::convertMatcher(CategoryMatcher *item) const { QList> result; OrCategoryMatcher *orMacther; if ((orMacther = dynamic_cast(item))) { for (CategoryMatcher *child : orMacther->mp_elements) { result.append(extractAndMatcher(child)); } } else result.append(extractAndMatcher(item)); return result; } short ImageSearchInfo::rating() const { return m_rating; } ImageDate ImageSearchInfo::date() const { return m_date; } void ImageSearchInfo::addExifSearchInfo(const Exif::SearchInfo info) { m_exifSearchInfo = info; m_isNull = false; m_matchGeneration = nextGeneration(); } void DB::ImageSearchInfo::renameCategory(const QString &oldName, const QString &newName) { m_categoryMatchText[newName] = m_categoryMatchText[oldName]; m_categoryMatchText.remove(oldName); m_compiled.valid = false; m_matchGeneration = nextGeneration(); } #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates::Pair ImageSearchInfo::regionSelection() const { return m_regionSelection; } void ImageSearchInfo::setRegionSelection(const KGeoMap::GeoCoordinates::Pair &actRegionSelection) { m_regionSelection = actRegionSelection; m_compiled.valid = false; if (m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates()) { m_isNull = false; } m_matchGeneration = nextGeneration(); } #endif ImageSearchInfo::CompiledDataPrivate::CompiledDataPrivate(const ImageSearchInfo::CompiledDataPrivate &) { // copying invalidates the compiled data valid = false; } ImageSearchInfo::CompiledDataPrivate::~CompiledDataPrivate() { qDeleteAll(categoryMatchers); categoryMatchers.clear(); } ImageSearchInfo::CompiledDataPrivate &ImageSearchInfo::CompiledDataPrivate::operator=(const ImageSearchInfo::CompiledDataPrivate &) { // copying invalidates the compiled data valid = false; return *this; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/OrCategoryMatcher.cpp b/DB/OrCategoryMatcher.cpp index 6622fd7d..283c1a75 100644 --- a/DB/OrCategoryMatcher.cpp +++ b/DB/OrCategoryMatcher.cpp @@ -1,38 +1,40 @@ -/* 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 "OrCategoryMatcher.h" #include "ImageInfo.h" #include "Logging.h" bool DB::OrCategoryMatcher::eval(ImageInfoPtr info, QMap &alreadyMatched) { - Q_FOREACH (CategoryMatcher *subMatcher, mp_elements) { + for (CategoryMatcher *subMatcher : mp_elements) { if (subMatcher->eval(info, alreadyMatched)) return true; } return false; } void DB::OrCategoryMatcher::debug(int level) const { qCDebug(DBCategoryMatcherLog, "%sOR:", qPrintable(spaces(level))); ContainerCategoryMatcher::debug(level + 1); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/Database.cpp b/Exif/Database.cpp index 94f608ed..6724466b 100644 --- a/Exif/Database.cpp +++ b/Exif/Database.cpp @@ -1,652 +1,654 @@ -/* 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 "Database.h" #include "DatabaseElement.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Exif; namespace { // schema version; bump it up whenever the database schema changes constexpr int DB_VERSION = 3; const Database::ElementList elements(int since = 0) { static Database::ElementList elms; static int sinceDBVersion[DB_VERSION] {}; if (elms.count() == 0) { elms.append(new RationalExifElement("Exif.Photo.FocalLength")); elms.append(new RationalExifElement("Exif.Photo.ExposureTime")); elms.append(new RationalExifElement("Exif.Photo.ApertureValue")); elms.append(new RationalExifElement("Exif.Photo.FNumber")); //elms.append( new RationalExifElement( "Exif.Photo.FlashEnergy" ) ); elms.append(new IntExifElement("Exif.Photo.Flash")); elms.append(new IntExifElement("Exif.Photo.Contrast")); elms.append(new IntExifElement("Exif.Photo.Sharpness")); elms.append(new IntExifElement("Exif.Photo.Saturation")); elms.append(new IntExifElement("Exif.Image.Orientation")); elms.append(new IntExifElement("Exif.Photo.MeteringMode")); elms.append(new IntExifElement("Exif.Photo.ISOSpeedRatings")); elms.append(new IntExifElement("Exif.Photo.ExposureProgram")); elms.append(new StringExifElement("Exif.Image.Make")); elms.append(new StringExifElement("Exif.Image.Model")); // gps info has been added in database schema version 2: sinceDBVersion[1] = elms.size(); elms.append(new IntExifElement("Exif.GPSInfo.GPSVersionID")); // actually a byte value elms.append(new RationalExifElement("Exif.GPSInfo.GPSAltitude")); elms.append(new IntExifElement("Exif.GPSInfo.GPSAltitudeRef")); // actually a byte value elms.append(new StringExifElement("Exif.GPSInfo.GPSMeasureMode")); elms.append(new RationalExifElement("Exif.GPSInfo.GPSDOP")); elms.append(new RationalExifElement("Exif.GPSInfo.GPSImgDirection")); elms.append(new RationalExifElement("Exif.GPSInfo.GPSLatitude")); elms.append(new StringExifElement("Exif.GPSInfo.GPSLatitudeRef")); elms.append(new RationalExifElement("Exif.GPSInfo.GPSLongitude")); elms.append(new StringExifElement("Exif.GPSInfo.GPSLongitudeRef")); elms.append(new RationalExifElement("Exif.GPSInfo.GPSTimeStamp")); // lens info has been added in database schema version 3: sinceDBVersion[2] = elms.size(); elms.append(new LensExifElement()); } // query only for the newly added stuff: if (since > 0) return elms.mid(sinceDBVersion[since]); return elms; } } Exif::Database *Exif::Database::s_instance = nullptr; /** * @brief show and error message for the failed \p query and disable the Exif database. * The database is closed because at this point we can not trust the data inside. * @param query */ void Database::showErrorAndFail(QSqlQuery &query) const { const QString txt = i18n("

There was an error while accessing the Exif search database. " "The error is likely due to a broken database file.

" "

To fix this problem run Maintenance->Recreate Exif Search database.

" "
" "

For debugging: the command that was attempted to be executed was:
%1

" "

The error message obtained was:
%2

", query.lastQuery(), query.lastError().text()); const QString technicalInfo = QString::fromUtf8("Error running query: %s\n Error was: %s") .arg(query.lastQuery(), query.lastError().text()); showErrorAndFail(txt, technicalInfo); } void Database::showErrorAndFail(const QString &errorMessage, const QString &technicalInfo) const { KMessageBox::information(MainWindow::Window::theMainWindow(), errorMessage, i18n("Error in Exif database"), QString::fromLatin1("sql_error_in_exif_DB")); qCWarning(ExifLog) << technicalInfo; // disable exif db for now: m_isFailed = true; } Exif::Database::Database() : m_isOpen(false) , m_isFailed(false) { m_db = QSqlDatabase::addDatabase(QString::fromLatin1("QSQLITE"), QString::fromLatin1("exif")); } void Exif::Database::openDatabase() { m_db.setDatabaseName(exifDBFile()); m_isOpen = m_db.open(); if (!m_isOpen) { const QString txt = i18n("

There was an error while opening the Exif search database.

" "

To fix this problem run Maintenance->Recreate Exif Search database.

" "
" "

The error message obtained was:
%1

", m_db.lastError().text()); const QString logMsg = QString::fromUtf8("Could not open Exif search database! " "Error was: %s") .arg(m_db.lastError().text()); showErrorAndFail(txt, logMsg); return; } // If SQLite in Qt has Unicode feature, it will convert queries to // UTF-8 automatically. Otherwise we should do the conversion to // be able to store any Unicode character. m_doUTF8Conversion = !m_db.driver()->hasFeature(QSqlDriver::Unicode); } Exif::Database::~Database() { // We have to close the database before destroying the QSqlDatabase object, // otherwise Qt screams and kittens might die (see QSqlDatabase's // documentation) if (m_db.isOpen()) m_db.close(); } bool Exif::Database::isOpen() const { return m_isOpen && !m_isFailed; } void Exif::Database::populateDatabase() { createMetadataTable(SchemaAndDataChanged); QStringList attributes; - Q_FOREACH (DatabaseElement *element, elements()) { + for (DatabaseElement *element : elements()) { attributes.append(element->createString()); } QSqlQuery query(QString::fromLatin1("create table if not exists exif (filename string PRIMARY KEY, %1 )") .arg(attributes.join(QString::fromLatin1(", "))), m_db); if (!query.exec()) showErrorAndFail(query); } void Exif::Database::updateDatabase() { if (m_db.tables().isEmpty()) { const QString txt = i18n("

The Exif search database is corrupted and has no data.

" "

To fix this problem run Maintenance->Recreate Exif Search database.

"); const QString logMsg = QString::fromUtf8("Database open but empty!"); showErrorAndFail(txt, logMsg); return; } const int version = DBFileVersion(); if (m_isFailed) return; if (version < DBVersion()) { // on the next update, we can just query the DB Version createMetadataTable(SchemaChanged); } // update schema if (version < DBVersion()) { QSqlQuery query(m_db); for (const DatabaseElement *e : elements(version)) { query.prepare(QString::fromLatin1("alter table exif add column %1") .arg(e->createString())); if (!query.exec()) showErrorAndFail(query); } } } void Exif::Database::createMetadataTable(DBSchemaChangeType change) { QSqlQuery query(m_db); query.prepare(QString::fromLatin1("create table if not exists settings (keyword TEXT PRIMARY KEY, value TEXT) without rowid")); if (!query.exec()) { showErrorAndFail(query); return; } query.prepare(QString::fromLatin1("insert or replace into settings (keyword, value) values('DBVersion','%1')").arg(Database::DBVersion())); if (!query.exec()) { showErrorAndFail(query); return; } if (change == SchemaAndDataChanged) { query.prepare(QString::fromLatin1("insert or replace into settings (keyword, value) values('GuaranteedDataVersion','%1')").arg(Database::DBVersion())); if (!query.exec()) showErrorAndFail(query); } } bool Exif::Database::add(const DB::FileName &fileName) { if (!isUsable()) return false; try { Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data()); Q_ASSERT(image.get() != nullptr); image->readMetadata(); Exiv2::ExifData &exifData = image->exifData(); return insert(fileName, exifData); } catch (...) { qCWarning(ExifLog, "Error while reading exif information from %s", qPrintable(fileName.absolute())); return false; } } bool Exif::Database::add(DB::FileInfo &fileInfo) { if (!isUsable()) return false; return insert(fileInfo.getFileName(), fileInfo.getExifData()); } bool Exif::Database::add(const DB::FileNameList &list) { if (!isUsable()) return false; QList map; - Q_FOREACH (const DB::FileName &fileName, list) { + for (const DB::FileName &fileName : list) { try { Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data()); Q_ASSERT(image.get() != nullptr); image->readMetadata(); map << DBExifInfo(fileName, image->exifData()); } catch (...) { qWarning("Error while reading exif information from %s", qPrintable(fileName.absolute())); } } insert(map); return true; } void Exif::Database::remove(const DB::FileName &fileName) { if (!isUsable()) return; QSqlQuery query(m_db); query.prepare(QString::fromLatin1("DELETE FROM exif WHERE fileName=?")); query.bindValue(0, fileName.absolute()); if (!query.exec()) showErrorAndFail(query); } void Exif::Database::remove(const DB::FileNameList &list) { if (!isUsable()) return; m_db.transaction(); QSqlQuery query(m_db); query.prepare(QString::fromLatin1("DELETE FROM exif WHERE fileName=?")); - Q_FOREACH (const DB::FileName &fileName, list) { + for (const DB::FileName &fileName : list) { query.bindValue(0, fileName.absolute()); if (!query.exec()) { m_db.rollback(); showErrorAndFail(query); return; } } m_db.commit(); } QSqlQuery *Exif::Database::getInsertQuery() { if (!isUsable()) return nullptr; if (m_insertTransaction) return m_insertTransaction; if (m_queryString.isEmpty()) { QStringList formalList; Database::ElementList elms = elements(); for (const DatabaseElement *e : elms) { formalList.append(e->queryString()); } m_queryString = QString::fromLatin1("INSERT OR REPLACE into exif values (?, %1) ").arg(formalList.join(QString::fromLatin1(", "))); } QSqlQuery *query = new QSqlQuery(m_db); if (query) query->prepare(m_queryString); return query; } void Exif::Database::concludeInsertQuery(QSqlQuery *query) { if (m_insertTransaction) return; m_db.commit(); delete query; } bool Exif::Database::startInsertTransaction() { Q_ASSERT(m_insertTransaction == nullptr); m_insertTransaction = getInsertQuery(); m_db.transaction(); return (m_insertTransaction != nullptr); } bool Exif::Database::commitInsertTransaction() { if (m_insertTransaction) { m_db.commit(); delete m_insertTransaction; m_insertTransaction = nullptr; } else qCWarning(ExifLog, "Trying to commit transaction, but no transaction is active!"); return true; } bool Exif::Database::abortInsertTransaction() { if (m_insertTransaction) { m_db.rollback(); delete m_insertTransaction; m_insertTransaction = nullptr; } else qCWarning(ExifLog, "Trying to abort transaction, but no transaction is active!"); return true; } bool Exif::Database::insert(const DB::FileName &filename, Exiv2::ExifData data) { if (!isUsable()) return false; QSqlQuery *query = getInsertQuery(); query->bindValue(0, filename.absolute()); int i = 1; for (const DatabaseElement *e : elements()) { query->bindValue(i++, e->valueFromExif(data)); } bool status = query->exec(); if (!status) showErrorAndFail(*query); concludeInsertQuery(query); return status; } bool Exif::Database::insert(QList map) { if (!isUsable()) return false; QSqlQuery *query = getInsertQuery(); // not a const reference because DatabaseElement::valueFromExif uses operator[] on the exif datum - Q_FOREACH (DBExifInfo elt, map) { + for (DBExifInfo elt : map) { query->bindValue(0, elt.first.absolute()); int i = 1; for (const DatabaseElement *e : elements()) { query->bindValue(i++, e->valueFromExif(elt.second)); } if (!query->exec()) { showErrorAndFail(*query); } } concludeInsertQuery(query); return true; } Exif::Database *Exif::Database::instance() { if (!s_instance) { qCInfo(ExifLog) << "initializing Exif database..."; s_instance = new Exif::Database(); s_instance->init(); } return s_instance; } void Exif::Database::deleteInstance() { delete s_instance; s_instance = nullptr; } bool Exif::Database::isAvailable() { #ifdef QT_NO_SQL return false; #else return QSqlDatabase::isDriverAvailable(QString::fromLatin1("QSQLITE")); #endif } int Exif::Database::DBFileVersion() const { // previous to KPA 4.6, there was no metadata table: if (!m_db.tables().contains(QString::fromLatin1("settings"))) return 1; QSqlQuery query(QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'DBVersion'"), m_db); if (!query.exec()) showErrorAndFail(query); if (query.first()) { return query.value(0).toInt(); } return 0; } int Exif::Database::DBFileVersionGuaranteed() const { // previous to KPA 4.6, there was no metadata table: if (!m_db.tables().contains(QString::fromLatin1("settings"))) return 0; QSqlQuery query(QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'GuaranteedDataVersion'"), m_db); if (!query.exec()) showErrorAndFail(query); if (query.first()) { return query.value(0).toInt(); } return 0; } constexpr int Exif::Database::DBVersion() { return DB_VERSION; } bool Exif::Database::isUsable() const { return (isAvailable() && isOpen()); } QString Exif::Database::exifDBFile() { return ::Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("/exif-info.db"); } bool Exif::Database::readFields(const DB::FileName &fileName, ElementList &fields) const { if (!isUsable()) return false; bool foundIt = false; QStringList fieldList; for (const DatabaseElement *e : fields) { fieldList.append(e->columnName()); } QSqlQuery query(m_db); // the query returns a single value, so we don't need the overhead for random access: query.setForwardOnly(true); query.prepare(QString::fromLatin1("select %1 from exif where filename=?") .arg(fieldList.join(QString::fromLatin1(", ")))); query.bindValue(0, fileName.absolute()); if (!query.exec()) { showErrorAndFail(query); } if (query.next()) { // file in exif db -> write back results int i = 0; for (DatabaseElement *e : fields) { e->setValue(query.value(i++)); } foundIt = true; } return foundIt; } DB::FileNameSet Exif::Database::filesMatchingQuery(const QString &queryStr) const { if (!isUsable()) return DB::FileNameSet(); DB::FileNameSet result; QSqlQuery query(queryStr, m_db); if (!query.exec()) showErrorAndFail(query); else { if (m_doUTF8Conversion) while (query.next()) result.insert(DB::FileName::fromAbsolutePath(QString::fromUtf8(query.value(0).toByteArray()))); else while (query.next()) result.insert(DB::FileName::fromAbsolutePath(query.value(0).toString())); } return result; } QList> Exif::Database::cameras() const { QList> result; if (!isUsable()) return result; QSqlQuery query(QString::fromLatin1("SELECT DISTINCT Exif_Image_Make, Exif_Image_Model FROM exif"), m_db); if (!query.exec()) { showErrorAndFail(query); } else { while (query.next()) { QString make = query.value(0).toString(); QString model = query.value(1).toString(); if (!make.isEmpty() && !model.isEmpty()) result.append(qMakePair(make, model)); } } return result; } QList Exif::Database::lenses() const { QList result; if (!isUsable()) return result; QSqlQuery query(QString::fromLatin1("SELECT DISTINCT Exif_Photo_LensModel FROM exif"), m_db); if (!query.exec()) { showErrorAndFail(query); } else { while (query.next()) { QString lens = query.value(0).toString(); if (!lens.isEmpty()) result.append(lens); } } return result; } void Exif::Database::init() { if (!isAvailable()) return; m_isFailed = false; m_insertTransaction = nullptr; bool dbExists = QFile::exists(exifDBFile()); openDatabase(); if (!isOpen()) return; if (!dbExists) populateDatabase(); else updateDatabase(); } void Exif::Database::recreate() { // We create a backup of the current database in case // the user presse 'cancel' or there is any error. In that case // we want to go back to the original DB. const QString origBackup = exifDBFile() + QLatin1String(".bak"); m_db.close(); QDir().remove(origBackup); QDir().rename(exifDBFile(), origBackup); init(); const DB::FileNameList allImages = DB::ImageDB::instance()->images(); QProgressDialog dialog; dialog.setModal(true); dialog.setLabelText(i18n("Rereading Exif information from all images")); dialog.setMaximum(allImages.size()); // using a transaction here removes a *huge* overhead on the insert statements startInsertTransaction(); int i = 0; for (const DB::FileName &fileName : allImages) { const DB::ImageInfoPtr info = fileName.info(); dialog.setValue(i++); if (info->mediaType() == DB::Image) { add(fileName); } if (i % 10) qApp->processEvents(); if (dialog.wasCanceled()) break; } // PENDING(blackie) We should count the amount of files that did not succeeded and warn the user. if (dialog.wasCanceled()) { abortInsertTransaction(); m_db.close(); QDir().remove(exifDBFile()); QDir().rename(origBackup, exifDBFile()); init(); } else { commitInsertTransaction(); QDir().remove(origBackup); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/SearchInfo.cpp b/Exif/SearchInfo.cpp index cc21bf75..fc8fc869 100644 --- a/Exif/SearchInfo.cpp +++ b/Exif/SearchInfo.cpp @@ -1,207 +1,208 @@ -/* 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. + 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 "SearchInfo.h" #include "Database.h" #include #include /** * \class Exif::SearchInfo * This class represents a search for Exif information. It is similar in functionality for category searches which is in the * class \ref DB::ImageSearchInfo. * * The search is build, from \ref Exif::SearchDialog, using the functions addRangeKey(), addSearchKey(), and addCamara(). * The search is stored in an instance of \ref DB::ImageSearchInfo, and may later be executed using search(). * Once a search has been executed, the application may ask if a given image is in the search result using matches() */ void Exif::SearchInfo::addSearchKey(const QString &key, const IntList &values) { m_intKeys.append(qMakePair(key, values)); } QStringList Exif::SearchInfo::buildIntKeyQuery() const { QStringList andArgs; for (IntKeyList::ConstIterator intIt = m_intKeys.begin(); intIt != m_intKeys.end(); ++intIt) { QStringList orArgs; QString key = (*intIt).first; IntList values = (*intIt).second; - Q_FOREACH (int value, values) { + for (int value : values) { orArgs << QString::fromLatin1("(%1 == %2)").arg(key).arg(value); } if (orArgs.count() != 0) andArgs << QString::fromLatin1("(%1)").arg(orArgs.join(QString::fromLatin1(" or "))); } return andArgs; } void Exif::SearchInfo::addRangeKey(const Range &range) { m_rangeKeys.append(range); } Exif::SearchInfo::Range::Range(const QString &key) : isLowerMin(false) , isLowerMax(false) , isUpperMin(false) , isUpperMax(false) , key(key) { } QString Exif::SearchInfo::buildQuery() const { QStringList subQueries; subQueries += buildIntKeyQuery(); subQueries += buildRangeQuery(); QString cameraQuery = buildCameraSearchQuery(); if (!cameraQuery.isEmpty()) subQueries.append(cameraQuery); QString lensQuery = buildLensSearchQuery(); if (!lensQuery.isEmpty()) subQueries.append(lensQuery); if (subQueries.empty()) return QString(); else return QString::fromLatin1("SELECT filename from exif WHERE %1") .arg(subQueries.join(QString::fromLatin1(" and "))); } QStringList Exif::SearchInfo::buildRangeQuery() const { QStringList result; for (QList::ConstIterator it = m_rangeKeys.begin(); it != m_rangeKeys.end(); ++it) { QString str = sqlForOneRangeItem(*it); if (!str.isEmpty()) result.append(str); } return result; } QString Exif::SearchInfo::sqlForOneRangeItem(const Range &range) const { // Notice I multiplied factors on each value to ensure that we do not fail due to rounding errors for say 1/3 if (range.isLowerMin) { // Min to Min means < x if (range.isUpperMin) return QString::fromLatin1("%1 < %2 and %3 > 0").arg(range.key).arg(range.min * 1.01).arg(range.key); // Min to Max means all images if (range.isUpperMax) return QString(); // Min to y means <= y return QString::fromLatin1("%1 <= %2 and %3 > 0").arg(range.key).arg(range.max * 1.01).arg(range.key); } // MAX to MAX means >= y if (range.isLowerMax) return QString::fromLatin1("%1 > %2").arg(range.key).arg(range.max * 0.99); // x to Max means >= x if (range.isUpperMax) return QString::fromLatin1("%1 >= %2").arg(range.key).arg(range.min * 0.99); // x to y means >=x and <=y return QString::fromLatin1("(%1 <= %2 and %3 <= %4)") .arg(range.min * 0.99) .arg(range.key) .arg(range.key) .arg(range.max * 1.01); } void Exif::SearchInfo::search() const { QString queryStr = buildQuery(); m_emptyQuery = queryStr.isEmpty(); // ensure to do SQL queries as little as possible. static QString lastQuery; if (queryStr == lastQuery) return; lastQuery = queryStr; m_matches.clear(); if (m_emptyQuery) return; m_matches = Exif::Database::instance()->filesMatchingQuery(queryStr); } bool Exif::SearchInfo::matches(const DB::FileName &fileName) const { if (m_emptyQuery) return true; return m_matches.contains(fileName); } bool Exif::SearchInfo::isNull() const { return buildQuery().isEmpty(); } void Exif::SearchInfo::addCamera(const CameraList &list) { m_cameras = list; } void Exif::SearchInfo::addLens(const LensList &list) { m_lenses = list; } QString Exif::SearchInfo::buildCameraSearchQuery() const { QStringList subResults; for (CameraList::ConstIterator cameraIt = m_cameras.begin(); cameraIt != m_cameras.end(); ++cameraIt) { subResults.append(QString::fromUtf8("(Exif_Image_Make='%1' and Exif_Image_Model='%2')") .arg((*cameraIt).first) .arg((*cameraIt).second)); } if (subResults.count() != 0) return QString::fromUtf8("(%1)").arg(subResults.join(QString::fromLatin1(" or "))); else return QString(); } QString Exif::SearchInfo::buildLensSearchQuery() const { QStringList subResults; for (LensList::ConstIterator lensIt = m_lenses.begin(); lensIt != m_lenses.end(); ++lensIt) { if (*lensIt == i18nc("As in No persons, no locations etc.", "None")) // compare to null (=entry from old db schema) and empty string (=entry w/o exif lens info) subResults.append(QString::fromUtf8("(nullif(Exif_Photo_LensModel,'') is null)")); else subResults.append(QString::fromUtf8("(Exif_Photo_LensModel='%1')") .arg(*lensIt)); } if (subResults.count() != 0) return QString::fromUtf8("(%1)").arg(subResults.join(QString::fromLatin1(" or "))); else return QString(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/TreeView.cpp b/Exif/TreeView.cpp index 90527098..0825e87a 100644 --- a/Exif/TreeView.cpp +++ b/Exif/TreeView.cpp @@ -1,104 +1,104 @@ -/* 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. + 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 "TreeView.h" - #include "Info.h" #include -#include -#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); QStringList keys = Exif::Info::instance()->availableKeys().toList(); keys.sort(); QMap tree; for (QStringList::const_iterator keysIt = keys.constBegin(); keysIt != keys.constEnd(); ++keysIt) { QStringList subKeys = (*keysIt).split(QLatin1String(".")); QTreeWidgetItem *parent = nullptr; QString path; - Q_FOREACH (const QString &subKey, subKeys) { + 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/ImageManager/ThumbnailCache.cpp b/ImageManager/ThumbnailCache.cpp index cdedeff4..1f2c3c6c 100644 --- a/ImageManager/ThumbnailCache.cpp +++ b/ImageManager/ThumbnailCache.cpp @@ -1,480 +1,482 @@ -/* 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. + 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 "ThumbnailCache.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { // We split the thumbnails into chunks to avoid a huge file changing over and over again, with a bad hit for backups constexpr int MAX_FILE_SIZE = 32 * 1024 * 1024; constexpr int THUMBNAIL_FILE_VERSION = 4; // We map some thumbnail files into memory and manage them in a least-recently-used fashion constexpr size_t LRU_SIZE = 2; constexpr int THUMBNAIL_CACHE_SAVE_INTERNAL_MS = (5 * 1000); } namespace ImageManager { /** * The ThumbnailMapping wraps the memory-mapped data of a QFile. * Upon initialization with a file name, the corresponding file is opened * and its contents mapped into memory (as a QByteArray). * * Deleting the ThumbnailMapping unmaps the memory and closes the file. */ class ThumbnailMapping { public: ThumbnailMapping(const QString &filename) : file(filename) , map(nullptr) { if (!file.open(QIODevice::ReadOnly)) qCWarning(ImageManagerLog, "Failed to open thumbnail file"); uchar *data = file.map(0, file.size()); if (!data || QFile::NoError != file.error()) { qCWarning(ImageManagerLog, "Failed to map thumbnail file"); } else { map = QByteArray::fromRawData(reinterpret_cast(data), file.size()); } } bool isValid() { return !map.isEmpty(); } // we need to keep the file around to keep the data mapped: QFile file; QByteArray map; }; } ImageManager::ThumbnailCache *ImageManager::ThumbnailCache::s_instance = nullptr; ImageManager::ThumbnailCache::ThumbnailCache() : m_currentFile(0) , m_currentOffset(0) , m_timer(new QTimer) , m_needsFullSave(true) , m_isDirty(false) , m_memcache(new QCache(LRU_SIZE)) , m_currentWriter(nullptr) { const QString dir = thumbnailPath(QString()); if (!QFile::exists(dir)) QDir().mkpath(dir); load(); connect(this, &ImageManager::ThumbnailCache::doSave, this, &ImageManager::ThumbnailCache::saveImpl); connect(m_timer, &QTimer::timeout, this, &ImageManager::ThumbnailCache::saveImpl); m_timer->setInterval(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); m_timer->setSingleShot(true); m_timer->start(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); } ImageManager::ThumbnailCache::~ThumbnailCache() { m_needsFullSave = true; saveInternal(); delete m_memcache; delete m_timer; if (m_currentWriter) delete m_currentWriter; } void ImageManager::ThumbnailCache::insert(const DB::FileName &name, const QImage &image) { QMutexLocker thumbnailLocker(&m_thumbnailWriterLock); if (!m_currentWriter) { m_currentWriter = new QFile(fileNameForIndex(m_currentFile)); if (!m_currentWriter->open(QIODevice::ReadWrite)) { qCWarning(ImageManagerLog, "Failed to open thumbnail file for inserting"); return; } } if (!m_currentWriter->seek(m_currentOffset)) { qCWarning(ImageManagerLog, "Failed to seek in thumbnail file"); return; } QMutexLocker dataLocker(&m_dataLock); // purge in-memory cache for the current file: m_memcache->remove(m_currentFile); QByteArray data; QBuffer buffer(&data); bool OK = buffer.open(QIODevice::WriteOnly); Q_ASSERT(OK); Q_UNUSED(OK); OK = image.save(&buffer, "JPG"); Q_ASSERT(OK); const int size = data.size(); if (!(m_currentWriter->write(data.data(), size) == size && m_currentWriter->flush())) { qCWarning(ImageManagerLog, "Failed to write image data to thumbnail file"); return; } if (m_currentOffset + size > MAX_FILE_SIZE) { delete m_currentWriter; m_currentWriter = nullptr; } thumbnailLocker.unlock(); if (m_hash.contains(name)) { CacheFileInfo info = m_hash[name]; if (info.fileIndex == m_currentFile && info.offset == m_currentOffset && info.size == size) { qCDebug(ImageManagerLog) << "Found duplicate thumbnail " << name.relative() << "but no change in information"; dataLocker.unlock(); return; } else { // File has moved; incremental save does no good. qCDebug(ImageManagerLog) << "Found duplicate thumbnail " << name.relative() << " at new location, need full save! "; m_saveLock.lock(); m_needsFullSave = true; m_saveLock.unlock(); } } m_hash.insert(name, CacheFileInfo(m_currentFile, m_currentOffset, size)); m_isDirty = true; m_unsavedHash.insert(name, CacheFileInfo(m_currentFile, m_currentOffset, size)); // Update offset m_currentOffset += size; if (m_currentOffset > MAX_FILE_SIZE) { m_currentFile++; m_currentOffset = 0; } int unsaved = m_unsavedHash.count(); dataLocker.unlock(); // Thumbnail building is a lot faster now. Even on an HDD this corresponds to less // than 1 minute of work. // // We need to call the internal version that does not interact with the timer. // We can't simply signal from here because if we're in the middle of loading new // images the signal won't get invoked until we return to the main application loop. if (unsaved >= 100) { saveInternal(); } } QString ImageManager::ThumbnailCache::fileNameForIndex(int index, const QString dir) const { return thumbnailPath(QString::fromLatin1("thumb-") + QString::number(index), dir); } QPixmap ImageManager::ThumbnailCache::lookup(const DB::FileName &name) const { m_dataLock.lock(); CacheFileInfo info = m_hash[name]; m_dataLock.unlock(); ThumbnailMapping *t = m_memcache->object(info.fileIndex); if (!t || !t->isValid()) { t = new ThumbnailMapping(fileNameForIndex(info.fileIndex)); if (!t->isValid()) { qCWarning(ImageManagerLog, "Failed to map thumbnail file"); return QPixmap(); } m_memcache->insert(info.fileIndex, t); } QByteArray array(t->map.mid(info.offset, info.size)); QBuffer buffer(&array); buffer.open(QIODevice::ReadOnly); QImage image; image.load(&buffer, "JPG"); // Notice the above image is sharing the bits with the file, so I can't just return it as it then will be invalid when the file goes out of scope. // PENDING(blackie) Is that still true? return QPixmap::fromImage(image); } QByteArray ImageManager::ThumbnailCache::lookupRawData(const DB::FileName &name) const { m_dataLock.lock(); CacheFileInfo info = m_hash[name]; m_dataLock.unlock(); ThumbnailMapping *t = m_memcache->object(info.fileIndex); if (!t || !t->isValid()) { t = new ThumbnailMapping(fileNameForIndex(info.fileIndex)); if (!t->isValid()) { qCWarning(ImageManagerLog, "Failed to map thumbnail file"); return QByteArray(); } m_memcache->insert(info.fileIndex, t); } QByteArray array(t->map.mid(info.offset, info.size)); return array; } void ImageManager::ThumbnailCache::saveFull() const { // First ensure that any dirty thumbnails are written to disk m_thumbnailWriterLock.lock(); if (m_currentWriter) { delete m_currentWriter; m_currentWriter = nullptr; } m_thumbnailWriterLock.unlock(); QMutexLocker dataLocker(&m_dataLock); if (!m_isDirty) { return; } QTemporaryFile file; if (!file.open()) { qCWarning(ImageManagerLog, "Failed to create temporary file"); return; } QHash tempHash = m_hash; m_unsavedHash.clear(); m_needsFullSave = false; // Clear the dirty flag early so that we can allow further work to proceed. // If the save fails, we'll set the dirty flag again. m_isDirty = false; dataLocker.unlock(); QDataStream stream(&file); stream << THUMBNAIL_FILE_VERSION << m_currentFile << m_currentOffset << m_hash.count(); for (auto it = tempHash.constBegin(); it != tempHash.constEnd(); ++it) { const CacheFileInfo &cacheInfo = it.value(); stream << it.key().relative() << cacheInfo.fileIndex << cacheInfo.offset << cacheInfo.size; } file.close(); const QString realFileName = thumbnailPath(QString::fromLatin1("thumbnailindex")); QFile::remove(realFileName); if (!file.copy(realFileName)) { qCWarning(ImageManagerLog, "Failed to copy the temporary file %s to %s", qPrintable(file.fileName()), qPrintable(realFileName)); dataLocker.relock(); m_isDirty = true; m_needsFullSave = true; } else { QFile realFile(realFileName); realFile.open(QIODevice::ReadOnly); realFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::WriteGroup | QFile::ReadOther); realFile.close(); } } // Incremental save does *not* clear the dirty flag. We always want to do a full // save eventually. void ImageManager::ThumbnailCache::saveIncremental() const { m_thumbnailWriterLock.lock(); if (m_currentWriter) { delete m_currentWriter; m_currentWriter = nullptr; } m_thumbnailWriterLock.unlock(); QMutexLocker dataLocker(&m_dataLock); if (m_unsavedHash.count() == 0) { return; } QHash tempUnsavedHash = m_unsavedHash; m_unsavedHash.clear(); m_isDirty = true; const QString realFileName = thumbnailPath(QString::fromLatin1("thumbnailindex")); QFile file(realFileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) { qCWarning(ImageManagerLog, "Failed to open thumbnail cache for appending"); m_needsFullSave = true; return; } QDataStream stream(&file); for (auto it = tempUnsavedHash.constBegin(); it != tempUnsavedHash.constEnd(); ++it) { const CacheFileInfo &cacheInfo = it.value(); stream << it.key().relative() << cacheInfo.fileIndex << cacheInfo.offset << cacheInfo.size; } file.close(); } void ImageManager::ThumbnailCache::saveInternal() const { m_saveLock.lock(); const QString realFileName = thumbnailPath(QString::fromLatin1("thumbnailindex")); // If something has asked for a full save, do it! if (m_needsFullSave || !QFile(realFileName).exists()) { saveFull(); } else { saveIncremental(); } m_saveLock.unlock(); } void ImageManager::ThumbnailCache::saveImpl() const { m_timer->stop(); saveInternal(); m_timer->setInterval(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); m_timer->setSingleShot(true); m_timer->start(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); } void ImageManager::ThumbnailCache::save() const { m_saveLock.lock(); m_needsFullSave = true; m_saveLock.unlock(); emit doSave(); } void ImageManager::ThumbnailCache::load() { QFile file(thumbnailPath(QString::fromLatin1("thumbnailindex"))); if (!file.exists()) return; QElapsedTimer timer; timer.start(); file.open(QIODevice::ReadOnly); QDataStream stream(&file); int version; stream >> version; if (version != THUMBNAIL_FILE_VERSION) return; //Discard cache // We can't allow anything to modify the structure while we're doing this. QMutexLocker dataLocker(&m_dataLock); int count = 0; stream >> m_currentFile >> m_currentOffset >> count; while (!stream.atEnd()) { QString name; int fileIndex; int offset; int size; stream >> name >> fileIndex >> offset >> size; m_hash.insert(DB::FileName::fromRelativePath(name), CacheFileInfo(fileIndex, offset, size)); if (fileIndex > m_currentFile) { m_currentFile = fileIndex; m_currentOffset = offset + size; } else if (fileIndex == m_currentFile && offset + size > m_currentOffset) { m_currentOffset = offset + size; } if (m_currentOffset > MAX_FILE_SIZE) { m_currentFile++; m_currentOffset = 0; } count++; } qCDebug(TimingLog) << "Loaded thumbnails in " << timer.elapsed() / 1000.0 << " seconds"; } bool ImageManager::ThumbnailCache::contains(const DB::FileName &name) const { QMutexLocker dataLocker(&m_dataLock); bool answer = m_hash.contains(name); return answer; } QString ImageManager::ThumbnailCache::thumbnailPath(const QString &file, const QString dir) const { QString base = QDir(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath(dir); return base + file; } ImageManager::ThumbnailCache *ImageManager::ThumbnailCache::instance() { if (!s_instance) { s_instance = new ThumbnailCache; } return s_instance; } void ImageManager::ThumbnailCache::deleteInstance() { delete s_instance; s_instance = nullptr; } void ImageManager::ThumbnailCache::flush() { QMutexLocker dataLocker(&m_dataLock); for (int i = 0; i <= m_currentFile; ++i) QFile::remove(fileNameForIndex(i)); m_currentFile = 0; m_currentOffset = 0; m_isDirty = true; m_hash.clear(); m_unsavedHash.clear(); m_memcache->clear(); dataLocker.unlock(); save(); } void ImageManager::ThumbnailCache::removeThumbnail(const DB::FileName &fileName) { QMutexLocker dataLocker(&m_dataLock); m_isDirty = true; m_hash.remove(fileName); dataLocker.unlock(); save(); } void ImageManager::ThumbnailCache::removeThumbnails(const DB::FileNameList &files) { QMutexLocker dataLocker(&m_dataLock); m_isDirty = true; - Q_FOREACH (const DB::FileName &fileName, files) { + for (const DB::FileName &fileName : files) { m_hash.remove(fileName); } dataLocker.unlock(); save(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportDialog.cpp b/ImportExport/ImportDialog.cpp index bafdcc30..1192452e 100644 --- a/ImportExport/ImportDialog.cpp +++ b/ImportExport/ImportDialog.cpp @@ -1,394 +1,395 @@ -/* 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. + 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 "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; DB::ImageInfoList images = selectedImages(); for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) { DB::ImageInfoPtr info = *it; QStringList categoriesForImage = info->availableCategories(); - Q_FOREACH (const QString &category, categoriesForImage) { + for (const QString &category : categoriesForImage) { if (!categories.contains(category) && category != i18n("Folder") && category != i18n("Tokens") && category != i18n("Media Type")) categories.append(category); } } if (!categories.isEmpty()) { m_categoryMatcher = new ImportMatcher(QString(), QString(), categories, DB::ImageDB::instance()->categoryCollection()->categoryNames(), 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); 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; - Q_FOREACH (const CategoryMatch *match, m_categoryMatcher->m_matchers) { + for (const CategoryMatch *match : m_categoryMatcher->m_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) { - Q_FOREACH (ImageRow *row, m_imagesSelect) { + for (ImageRow *row : 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/ImportExport/ImportHandler.cpp b/ImportExport/ImportHandler.cpp index 91beb323..b773f60c 100644 --- a/ImportExport/ImportHandler.cpp +++ b/ImportExport/ImportHandler.cpp @@ -1,349 +1,351 @@ -/* 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 "ImportHandler.h" #include "ImportSettings.h" #include "KimFileReader.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ImportExport; ImportExport::ImportHandler::ImportHandler() : m_fileMapper(nullptr) , m_finishedPressed(false) , m_progress(0) , m_reportUnreadableFiles(true) , m_eventLoop(new QEventLoop) { } ImportHandler::~ImportHandler() { delete m_fileMapper; delete m_eventLoop; } bool ImportExport::ImportHandler::exec(const ImportSettings &settings, KimFileReader *kimFileReader) { m_settings = settings; m_kimFileReader = kimFileReader; m_finishedPressed = true; delete m_fileMapper; m_fileMapper = new Utilities::UniqFilenameMapper(m_settings.destination()); bool ok; // copy images if (m_settings.externalSource()) { copyFromExternal(); // If none of the images were to be copied, then we flushed the loop before we got started, in that case, don't start the loop. qCDebug(ImportExportLog) << "Copying" << m_pendingCopies.count() << "files from external source..."; if (m_pendingCopies.count() > 0) ok = m_eventLoop->exec(); else ok = false; } else { ok = copyFilesFromZipFile(); if (ok) updateDB(); } if (m_progress) delete m_progress; return ok; } void ImportExport::ImportHandler::copyFromExternal() { m_pendingCopies = m_settings.selectedImages(); m_totalCopied = 0; m_progress = new QProgressDialog(MainWindow::Window::theMainWindow()); m_progress->setWindowTitle(i18nc("@title:window", "Copying Images")); m_progress->setMinimum(0); m_progress->setMaximum(2 * m_pendingCopies.count()); m_progress->show(); connect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages); copyNextFromExternal(); } void ImportExport::ImportHandler::copyNextFromExternal() { DB::ImageInfoPtr info = m_pendingCopies[0]; if (isImageAlreadyInDB(info)) { qCDebug(ImportExportLog) << info->fileName().relative() << "is already in database."; aCopyJobCompleted(0); return; } const DB::FileName fileName = info->fileName(); bool succeeded = false; QStringList tried; // First search for images next to the .kim file // Second search for images base on the image root as specified in the .kim file QList searchUrls { m_settings.kimFile().adjusted(QUrl::RemoveFilename), m_settings.baseURL().adjusted(QUrl::RemoveFilename) }; - Q_FOREACH (const QUrl &url, searchUrls) { + for (const QUrl &url : searchUrls) { QUrl src(url); src.setPath(src.path() + fileName.relative()); std::unique_ptr statJob { KIO::stat(src, KIO::StatJob::SourceSide, 0 /* just query for existence */) }; KJobWidgets::setWindow(statJob.get(), MainWindow::Window::theMainWindow()); if (statJob->exec()) { QUrl dest = QUrl::fromLocalFile(m_fileMapper->uniqNameFor(fileName)); m_job = KIO::file_copy(src, dest, -1, KIO::HideProgressInfo); connect(m_job, &KIO::FileCopyJob::result, this, &ImportHandler::aCopyJobCompleted); succeeded = true; qCDebug(ImportExportLog) << "Copying" << src << "to" << dest; break; } else tried << src.toDisplayString(); } if (!succeeded) aCopyFailed(tried); } bool ImportExport::ImportHandler::copyFilesFromZipFile() { DB::ImageInfoList images = m_settings.selectedImages(); m_totalCopied = 0; m_progress = new QProgressDialog(MainWindow::Window::theMainWindow()); m_progress->setWindowTitle(i18nc("@title:window", "Copying Images")); m_progress->setMinimum(0); m_progress->setMaximum(2 * m_pendingCopies.count()); m_progress->show(); for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) { if (!isImageAlreadyInDB(*it)) { const DB::FileName fileName = (*it)->fileName(); QByteArray data = m_kimFileReader->loadImage(fileName.relative()); if (data.isNull()) return false; QString newName = m_fileMapper->uniqNameFor(fileName); QFile out(newName); if (!out.open(QIODevice::WriteOnly)) { KMessageBox::error(MainWindow::Window::theMainWindow(), i18n("Error when writing image %1", newName)); return false; } out.write(data.constData(), data.size()); out.close(); } qApp->processEvents(); m_progress->setValue(++m_totalCopied); if (m_progress->wasCanceled()) { return false; } } return true; } void ImportExport::ImportHandler::updateDB() { disconnect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages); m_progress->setLabelText(i18n("Updating Database")); int len = Settings::SettingsData::instance()->imageDirectory().length(); // image directory is always a prefix of destination if (len == m_settings.destination().length()) len = 0; else qCDebug(ImportExportLog) << "Re-rooting of ImageInfos from " << Settings::SettingsData::instance()->imageDirectory() << " to " << m_settings.destination(); // Run though all images DB::ImageInfoList images = m_settings.selectedImages(); for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) { DB::ImageInfoPtr info = *it; if (len != 0) { // exchange prefix: QString name = m_settings.destination() + info->fileName().absolute().mid(len); qCDebug(ImportExportLog) << info->fileName().absolute() << " -> " << name; info->setFileName(DB::FileName::fromAbsolutePath(name)); } if (isImageAlreadyInDB(info)) { qCDebug(ImportExportLog) << "Updating ImageInfo for " << info->fileName().absolute(); updateInfo(matchingInfoFromDB(info), info); } else { qCDebug(ImportExportLog) << "Adding ImageInfo for " << info->fileName().absolute(); addNewRecord(info); } m_progress->setValue(++m_totalCopied); if (m_progress->wasCanceled()) break; } Browser::BrowserWidget::instance()->home(); } void ImportExport::ImportHandler::stopCopyingImages() { m_job->kill(); } void ImportExport::ImportHandler::aCopyFailed(QStringList files) { int result = m_reportUnreadableFiles ? KMessageBox::warningYesNoCancelList(m_progress, i18n("Cannot copy from any of the following locations:"), files, QString(), KStandardGuiItem::cont(), KGuiItem(i18n("Continue without Asking"))) : KMessageBox::Yes; switch (result) { case KMessageBox::Cancel: // This might be late -- if we managed to copy some files, we will // just throw away any changes to the DB, but some new image files // might be in the image directory... m_eventLoop->exit(false); m_pendingCopies.pop_front(); break; case KMessageBox::No: m_reportUnreadableFiles = false; // fall through default: aCopyJobCompleted(0); } } void ImportExport::ImportHandler::aCopyJobCompleted(KJob *job) { qCDebug(ImportExportLog) << "CopyJob" << job << "completed."; m_pendingCopies.pop_front(); if (job && job->error()) { job->uiDelegate()->showErrorMessage(); m_eventLoop->exit(false); } else if (m_pendingCopies.count() == 0) { updateDB(); m_eventLoop->exit(true); } else if (m_progress->wasCanceled()) { m_eventLoop->exit(false); } else { m_progress->setValue(++m_totalCopied); copyNextFromExternal(); } } bool ImportExport::ImportHandler::isImageAlreadyInDB(const DB::ImageInfoPtr &info) { return DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum()); } DB::ImageInfoPtr ImportExport::ImportHandler::matchingInfoFromDB(const DB::ImageInfoPtr &info) { const DB::FileName name = DB::ImageDB::instance()->md5Map()->lookup(info->MD5Sum()); return DB::ImageDB::instance()->info(name); } /** * Merge the ImageInfo data from the kim file into the existing ImageInfo. */ void ImportExport::ImportHandler::updateInfo(DB::ImageInfoPtr dbInfo, DB::ImageInfoPtr newInfo) { if (dbInfo->label() != newInfo->label() && m_settings.importAction(QString::fromLatin1("*Label*")) == ImportSettings::Replace) dbInfo->setLabel(newInfo->label()); if (dbInfo->description().simplified() != newInfo->description().simplified()) { if (m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Replace) dbInfo->setDescription(newInfo->description()); else if (m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Merge) dbInfo->setDescription(dbInfo->description() + QString::fromLatin1("

") + newInfo->description()); } if (dbInfo->angle() != newInfo->angle() && m_settings.importAction(QString::fromLatin1("*Orientation*")) == ImportSettings::Replace) dbInfo->setAngle(newInfo->angle()); if (dbInfo->date() != newInfo->date() && m_settings.importAction(QString::fromLatin1("*Date*")) == ImportSettings::Replace) dbInfo->setDate(newInfo->date()); updateCategories(newInfo, dbInfo, false); } void ImportExport::ImportHandler::addNewRecord(DB::ImageInfoPtr info) { const DB::FileName importName = info->fileName(); DB::ImageInfoPtr updateInfo(new DB::ImageInfo(importName, info->mediaType(), false /*don't read exif */)); updateInfo->setLabel(info->label()); updateInfo->setDescription(info->description()); updateInfo->setDate(info->date()); updateInfo->setAngle(info->angle()); updateInfo->setMD5Sum(DB::MD5Sum(updateInfo->fileName())); DB::ImageInfoList list; list.append(updateInfo); DB::ImageDB::instance()->addImages(list); updateCategories(info, updateInfo, true); } void ImportExport::ImportHandler::updateCategories(DB::ImageInfoPtr XMLInfo, DB::ImageInfoPtr DBInfo, bool forceReplace) { // Run though the categories const QList matches = m_settings.categoryMatchSetting(); for (const CategoryMatchSetting &match : matches) { QString XMLCategoryName = match.XMLCategoryName(); QString DBCategoryName = match.DBCategoryName(); ImportSettings::ImportAction action = m_settings.importAction(DBCategoryName); const Utilities::StringSet items = XMLInfo->itemsOfCategory(XMLCategoryName); DB::CategoryPtr DBCategoryPtr = DB::ImageDB::instance()->categoryCollection()->categoryForName(DBCategoryName); if (!forceReplace && action == ImportSettings::Replace) DBInfo->setCategoryInfo(DBCategoryName, Utilities::StringSet()); if (action == ImportSettings::Merge || action == ImportSettings::Replace || forceReplace) { for (const QString &item : items) { if (match.XMLtoDB().contains(item)) { DBInfo->addCategoryInfo(DBCategoryName, match.XMLtoDB()[item]); DBCategoryPtr->addItem(match.XMLtoDB()[item]); } } } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/MD5CheckPage.cpp b/ImportExport/MD5CheckPage.cpp index c1c7600f..3d524a0c 100644 --- a/ImportExport/MD5CheckPage.cpp +++ b/ImportExport/MD5CheckPage.cpp @@ -1,199 +1,201 @@ -/* 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 "MD5CheckPage.h" #include #include #include #include #include #include #include #include ImportExport::ClashInfo::ClashInfo(const QStringList &categories) : label(false) , description(false) , orientation(false) , date(false) { for (const QString &category : categories) this->categories[category] = false; } bool ImportExport::MD5CheckPage::pageNeeded(const ImportSettings &settings) { if (countOfMD5Matches(settings) != 0 && clashes(settings).anyClashes()) return true; return false; } ImportExport::MD5CheckPage::MD5CheckPage(const ImportSettings &settings) { QVBoxLayout *vlay = new QVBoxLayout(this); const QString txt = i18np("One image from the import file, has the same MD5 sum as an image in the Database, how should that be resolved?", "%1 images from the import file, have the same MD5 sum as images in the Database, how should that be resolved?", countOfMD5Matches(settings)); QLabel *label = new QLabel(txt); label->setWordWrap(true); vlay->addWidget(label); QGridLayout *grid = new QGridLayout; grid->setHorizontalSpacing(0); vlay->addLayout(grid); int row = -1; // Titles label = new QLabel(i18n("Use data from\nImport File")); grid->addWidget(label, ++row, 1); label = new QLabel(i18n("Use data from\nDatabase")); grid->addWidget(label, row, 2); label = new QLabel(i18n("Merge data")); grid->addWidget(label, row, 3); ClashInfo clashes = this->clashes(settings); createRow(grid, row, QString::fromLatin1("*Label*"), i18n("Label"), clashes.label, false); createRow(grid, row, QString::fromLatin1("*Description*"), i18n("Description"), clashes.description, true); createRow(grid, row, QString::fromLatin1("*Orientation*"), i18n("Orientation"), clashes.orientation, false); createRow(grid, row, QString::fromLatin1("*Date*"), i18n("Date and Time"), clashes.date, false); for (QMap::const_iterator it = clashes.categories.constBegin(); it != clashes.categories.constEnd(); ++it) { createRow(grid, row, it.key(), it.key(), *it, true); } vlay->addStretch(1); } /** * Return the number of images in the import set which has the same MD5 sum as those from the DB. */ int ImportExport::MD5CheckPage::countOfMD5Matches(const ImportSettings &settings) { int count = 0; DB::ImageInfoList list = settings.selectedImages(); for (DB::ImageInfoPtr info : list) { if (DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum())) ++count; } return count; } ImportExport::ClashInfo ImportExport::MD5CheckPage::clashes(const ImportSettings &settings) { QStringList myCategories; - Q_FOREACH (const CategoryMatchSetting &matcher, settings.categoryMatchSetting()) { + for (const CategoryMatchSetting &matcher : settings.categoryMatchSetting()) { myCategories.append(matcher.DBCategoryName()); } ClashInfo res(myCategories); DB::ImageInfoList list = settings.selectedImages(); - Q_FOREACH (DB::ImageInfoPtr info, list) { + for (DB::ImageInfoPtr info : list) { if (!DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum())) continue; const DB::FileName name = DB::ImageDB::instance()->md5Map()->lookup(info->MD5Sum()); DB::ImageInfoPtr other = DB::ImageDB::instance()->info(name); if (info->label() != other->label()) res.label = true; if (info->description() != other->description()) res.description = true; if (info->angle() != other->angle()) res.orientation = true; if (info->date() != other->date()) res.date = true; - Q_FOREACH (const CategoryMatchSetting &matcher, settings.categoryMatchSetting()) { + for (const CategoryMatchSetting &matcher : settings.categoryMatchSetting()) { const QString XMLFileCategory = matcher.XMLCategoryName(); const QString DBCategory = matcher.DBCategoryName(); if (mapCategoriesToDB(matcher, info->itemsOfCategory(XMLFileCategory)) != other->itemsOfCategory(DBCategory)) res.categories[DBCategory] = true; } } return res; } bool ImportExport::ClashInfo::anyClashes() { if (label || description || orientation || date) return true; for (QMap::ConstIterator categoryIt = categories.constBegin(); categoryIt != categories.constEnd(); ++categoryIt) { if (categoryIt.value()) return true; } return false; } void ImportExport::MD5CheckPage::createRow(QGridLayout *layout, int &row, const QString &name, const QString &title, bool anyClashes, bool allowMerge) { if (row % 3 == 0) { QFrame *line = new QFrame; line->setFrameShape(QFrame::HLine); layout->addWidget(line, ++row, 0, 1, 4); } QLabel *label = new QLabel(title); label->setEnabled(anyClashes); layout->addWidget(label, ++row, 0); QButtonGroup *group = new QButtonGroup(this); m_groups[name] = group; for (int i = 1; i < 4; ++i) { if (i == 3 && !allowMerge) continue; QRadioButton *rb = new QRadioButton; layout->addWidget(rb, row, i); group->addButton(rb, i); rb->setEnabled(anyClashes); if (i == 1) rb->setChecked(true); } } Utilities::StringSet ImportExport::MD5CheckPage::mapCategoriesToDB(const CategoryMatchSetting &matcher, const Utilities::StringSet &items) { Utilities::StringSet res; - Q_FOREACH (const QString &item, items) { + for (const QString &item : items) { if (matcher.XMLtoDB().contains(item)) res.insert(matcher.XMLtoDB()[item]); } return res; } QMap ImportExport::MD5CheckPage::settings() { QMap res; for (QMap::iterator it = m_groups.begin(); it != m_groups.end(); ++it) { res.insert(it.key(), static_cast(it.value()->checkedId())); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/XMLHandler.cpp b/ImportExport/XMLHandler.cpp index 5b3c6cb9..af180648 100644 --- a/ImportExport/XMLHandler.cpp +++ b/ImportExport/XMLHandler.cpp @@ -1,121 +1,123 @@ -/* 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. + 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 "XMLHandler.h" #include #include using Utilities::StringSet; /** * \class ImportExport::XMLHandler * \brief Helper class for * reading and writing the index.xml file located in exported .kim file. * This class is a simple helper class which encapsulate the code needed for generating an index.xml for the export file. * There should never be a need to keep any instances around of this class, simply create one on the stack, and call * thee method \ref createIndexXML(). * * Notice, you will find a lot of duplicated code inhere from the XML database, there are two reasons for this * (1) In the long run the XML database ought to be an optional part (users might instead use, say an SQL database) * (2) To ensure that the .kim files are compatible both forth and back between versions, I'd rather keep that code * separate from the normal index.xml file, which might change with KPhotoAlbum versions to e.g. support compression. */ QByteArray ImportExport::XMLHandler::createIndexXML( const DB::FileNameList &images, const QString &baseUrl, ImageFileLocation location, Utilities::UniqFilenameMapper *nameMap) { QDomDocument doc; doc.appendChild(doc.createProcessingInstruction(QString::fromLatin1("xml"), QString::fromLatin1("version=\"1.0\" encoding=\"UTF-8\""))); QDomElement top = doc.createElement(QString::fromLatin1("KimDaBa-export")); // Don't change, as this will make the files unreadable for KimDaBa 2.1 and back. top.setAttribute(QString::fromLatin1("location"), location == Inline ? QString::fromLatin1("inline") : QString::fromLatin1("external")); if (!baseUrl.isEmpty()) top.setAttribute(QString::fromLatin1("baseurl"), baseUrl); doc.appendChild(top); - Q_FOREACH (const DB::FileName &fileName, images) { + for (const DB::FileName &fileName : images) { const QString mappedFile = nameMap->uniqNameFor(fileName); QDomElement elm = save(doc, fileName.info()); elm.setAttribute(QString::fromLatin1("file"), mappedFile); top.appendChild(elm); } return doc.toByteArray(); } QDomElement ImportExport::XMLHandler::save(QDomDocument doc, const DB::ImageInfoPtr &info) { QDomElement elm = doc.createElement(QString::fromLatin1("image")); elm.setAttribute(QString::fromLatin1("label"), info->label()); elm.setAttribute(QString::fromLatin1("description"), info->description()); DB::ImageDate date = info->date(); QDateTime start = date.start(); QDateTime end = date.end(); elm.setAttribute(QString::fromLatin1("yearFrom"), start.date().year()); elm.setAttribute(QString::fromLatin1("monthFrom"), start.date().month()); elm.setAttribute(QString::fromLatin1("dayFrom"), start.date().day()); elm.setAttribute(QString::fromLatin1("hourFrom"), start.time().hour()); elm.setAttribute(QString::fromLatin1("minuteFrom"), start.time().minute()); elm.setAttribute(QString::fromLatin1("secondFrom"), start.time().second()); elm.setAttribute(QString::fromLatin1("yearTo"), end.date().year()); elm.setAttribute(QString::fromLatin1("monthTo"), end.date().month()); elm.setAttribute(QString::fromLatin1("dayTo"), end.date().day()); elm.setAttribute(QString::fromLatin1("width"), info->size().width()); elm.setAttribute(QString::fromLatin1("height"), info->size().height()); elm.setAttribute(QString::fromLatin1("md5sum"), info->MD5Sum().toHexString()); elm.setAttribute(QString::fromLatin1("angle"), info->angle()); writeCategories(doc, elm, info); return elm; } void ImportExport::XMLHandler::writeCategories(QDomDocument doc, QDomElement root, const DB::ImageInfoPtr &info) { QDomElement elm = doc.createElement(QString::fromLatin1("options")); bool anyAtAll = false; QStringList grps = info->availableCategories(); - Q_FOREACH (const QString &name, grps) { + for (const QString &name : grps) { QDomElement opt = doc.createElement(QString::fromLatin1("option")); opt.setAttribute(QString::fromLatin1("name"), name); StringSet items = info->itemsOfCategory(name); bool any = false; - Q_FOREACH (const QString &item, items) { + for (const QString &item : items) { QDomElement val = doc.createElement(QString::fromLatin1("value")); val.setAttribute(QString::fromLatin1("value"), item); opt.appendChild(val); any = true; anyAtAll = true; } if (any) elm.appendChild(opt); } if (anyAtAll) root.appendChild(elm); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/AutoStackImages.cpp b/MainWindow/AutoStackImages.cpp index 02cb7624..40d53a3e 100644 --- a/MainWindow/AutoStackImages.cpp +++ b/MainWindow/AutoStackImages.cpp @@ -1,326 +1,327 @@ -/* Copyright (C) 2010-2019 The KPhotoAlbum Development Team +/* Copyright (C) 2010-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 "AutoStackImages.h" #include "Window.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MainWindow; AutoStackImages::AutoStackImages(QWidget *parent, const DB::FileNameList &list) : QDialog(parent) , m_list(list) { setWindowTitle(i18nc("@title:window", "Automatically Stack Images")); QWidget *top = new QWidget; QVBoxLayout *lay1 = new QVBoxLayout(top); setLayout(lay1); QWidget *containerMd5 = new QWidget(this); lay1->addWidget(containerMd5); QHBoxLayout *hlayMd5 = new QHBoxLayout(containerMd5); m_matchingMD5 = new QCheckBox(i18n("Stack images with identical MD5 sum")); m_matchingMD5->setChecked(false); hlayMd5->addWidget(m_matchingMD5); QWidget *containerFile = new QWidget(this); lay1->addWidget(containerFile); QHBoxLayout *hlayFile = new QHBoxLayout(containerFile); m_matchingFile = new QCheckBox(i18n("Stack images based on file version detection")); m_matchingFile->setChecked(true); hlayFile->addWidget(m_matchingFile); m_origTop = new QCheckBox(i18n("Original to top")); m_origTop->setChecked(false); hlayFile->addWidget(m_origTop); QWidget *containerContinuous = new QWidget(this); lay1->addWidget(containerContinuous); QHBoxLayout *hlayContinuous = new QHBoxLayout(containerContinuous); //FIXME: This is hard to translate because of the split sentence. It is better //to use a single sentence here like "Stack images that are (were?) shot //within this time:" and use the spin method setSuffix() to set the "seconds". //Also: Would minutes not be a more sane time unit here? (schwarzer) m_continuousShooting = new QCheckBox(i18nc("The whole sentence should read: *Stack images that are shot within x seconds of each other*. So images that are shot in one burst are automatically stacked together. (This sentence is before the x.)", "Stack images that are shot within")); m_continuousShooting->setChecked(false); hlayContinuous->addWidget(m_continuousShooting); m_continuousThreshold = new QSpinBox; m_continuousThreshold->setRange(1, 999); m_continuousThreshold->setSingleStep(1); m_continuousThreshold->setValue(2); hlayContinuous->addWidget(m_continuousThreshold); QLabel *sec = new QLabel(i18nc("The whole sentence should read: *Stack images that are shot within x seconds of each other*. (This being the text after x.)", "seconds"), containerContinuous); hlayContinuous->addWidget(sec); QGroupBox *grpOptions = new QGroupBox(i18n("AutoStacking Options")); QVBoxLayout *grpLayOptions = new QVBoxLayout(grpOptions); lay1->addWidget(grpOptions); m_autostackDefault = new QRadioButton(i18n("Include matching image to appropriate stack (if one exists)")); m_autostackDefault->setChecked(true); grpLayOptions->addWidget(m_autostackDefault); m_autostackUnstack = new QRadioButton(i18n("Unstack images from their current stack and create new one for the matches")); m_autostackUnstack->setChecked(false); grpLayOptions->addWidget(m_autostackUnstack); m_autostackSkip = new QRadioButton(i18n("Skip images that are already in a stack")); m_autostackSkip->setChecked(false); grpLayOptions->addWidget(m_autostackSkip); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &AutoStackImages::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &AutoStackImages::reject); lay1->addWidget(buttonBox); } /* * This function searches for images with matching MD5 sums * Matches are automatically stacked */ void AutoStackImages::matchingMD5(DB::FileNameList &toBeShown) { QMap tostack; DB::FileNameList showIfStacked; // Stacking all images that have the same MD5 sum // First make a map of MD5 sums with corresponding images - Q_FOREACH (const DB::FileName &fileName, m_list) { + for (const DB::FileName &fileName : m_list) { DB::MD5 sum = fileName.info()->MD5Sum(); if (DB::ImageDB::instance()->md5Map()->contains(sum)) { if (tostack[sum].isEmpty()) tostack.insert(sum, DB::FileNameList() << fileName); else tostack[sum].append(fileName); } } // Then add images to stack (depending on configuration options) for (QMap::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it) { if (tostack[it.key()].count() > 1) { DB::FileNameList stack; for (int i = 0; i < tostack[it.key()].count(); ++i) { if (!DB::ImageDB::instance()->getStackFor(tostack[it.key()][i]).isEmpty()) { if (m_autostackUnstack->isChecked()) DB::ImageDB::instance()->unstack(DB::FileNameList() << tostack[it.key()][i]); else if (m_autostackSkip->isChecked()) continue; } showIfStacked.append(tostack[it.key()][i]); stack.append(tostack[it.key()][i]); } if (stack.size() > 1) { - Q_FOREACH (const DB::FileName &a, showIfStacked) { + for (const DB::FileName &a : showIfStacked) { if (!DB::ImageDB::instance()->getStackFor(a).isEmpty()) - Q_FOREACH (const DB::FileName &b, DB::ImageDB::instance()->getStackFor(a)) + for (const DB::FileName &b : DB::ImageDB::instance()->getStackFor(a)) toBeShown.append(b); else toBeShown.append(a); } DB::ImageDB::instance()->stack(stack); } showIfStacked.clear(); } } } /* * This function searches for images based on file version detection configuration. * Images that are detected to be versions of same file are stacked together. */ void AutoStackImages::matchingFile(DB::FileNameList &toBeShown) { QMap tostack; DB::FileNameList showIfStacked; QString modifiedFileCompString; QRegExp modifiedFileComponent; QStringList originalFileComponents; modifiedFileCompString = Settings::SettingsData::instance()->modifiedFileComponent(); modifiedFileComponent = QRegExp(modifiedFileCompString); originalFileComponents << Settings::SettingsData::instance()->originalFileComponent(); originalFileComponents = originalFileComponents.at(0).split(QString::fromLatin1(";")); // Stacking all images based on file version detection // First round prepares the stacking - Q_FOREACH (const DB::FileName &fileName, m_list) { + for (const DB::FileName &fileName : m_list) { if (modifiedFileCompString.length() >= 0 && fileName.relative().contains(modifiedFileComponent)) { for (QStringList::const_iterator it = originalFileComponents.constBegin(); it != originalFileComponents.constEnd(); ++it) { QString tmp = fileName.relative(); tmp.replace(modifiedFileComponent, (*it)); DB::FileName originalFileName = DB::FileName::fromRelativePath(tmp); if (originalFileName != fileName && m_list.contains(originalFileName)) { DB::MD5 sum = originalFileName.info()->MD5Sum(); if (tostack[sum].isEmpty()) { if (m_origTop->isChecked()) { tostack.insert(sum, DB::FileNameList() << originalFileName); tostack[sum].append(fileName); } else { tostack.insert(sum, DB::FileNameList() << fileName); tostack[sum].append(originalFileName); } } else tostack[sum].append(fileName); break; } } } } // Then add images to stack (depending on configuration options) for (QMap::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it) { if (tostack[it.key()].count() > 1) { DB::FileNameList stack; for (int i = 0; i < tostack[it.key()].count(); ++i) { if (!DB::ImageDB::instance()->getStackFor(tostack[it.key()][i]).isEmpty()) { if (m_autostackUnstack->isChecked()) DB::ImageDB::instance()->unstack(DB::FileNameList() << tostack[it.key()][i]); else if (m_autostackSkip->isChecked()) continue; } showIfStacked.append(tostack[it.key()][i]); stack.append(tostack[it.key()][i]); } if (stack.size() > 1) { - Q_FOREACH (const DB::FileName &a, showIfStacked) { + for (const DB::FileName &a : showIfStacked) { if (!DB::ImageDB::instance()->getStackFor(a).isEmpty()) - Q_FOREACH (const DB::FileName &b, DB::ImageDB::instance()->getStackFor(a)) + for (const DB::FileName &b : DB::ImageDB::instance()->getStackFor(a)) toBeShown.append(b); else toBeShown.append(a); } DB::ImageDB::instance()->stack(stack); } showIfStacked.clear(); } } } /* * This function searches for images that are shot within specified time frame */ void AutoStackImages::continuousShooting(DB::FileNameList &toBeShown) { DB::ImageInfoPtr prev; - Q_FOREACH (const DB::FileName &fileName, m_list) { + for (const DB::FileName &fileName : m_list) { DB::ImageInfoPtr info = fileName.info(); // Skipping images that do not have exact time stamp if (info->date().start() != info->date().end()) continue; if (prev && (prev->date().start().secsTo(info->date().start()) < m_continuousThreshold->value())) { DB::FileNameList stack; if (!DB::ImageDB::instance()->getStackFor(prev->fileName()).isEmpty()) { if (m_autostackUnstack->isChecked()) DB::ImageDB::instance()->unstack(DB::FileNameList() << prev->fileName()); else if (m_autostackSkip->isChecked()) continue; } if (!DB::ImageDB::instance()->getStackFor(fileName).isEmpty()) { if (m_autostackUnstack->isChecked()) DB::ImageDB::instance()->unstack(DB::FileNameList() << fileName); else if (m_autostackSkip->isChecked()) continue; } stack.append(prev->fileName()); stack.append(info->fileName()); if (!toBeShown.isEmpty()) { if (toBeShown.at(toBeShown.size() - 1).info()->fileName() != prev->fileName()) toBeShown.append(prev->fileName()); } else { // if this is first insert, we have to include also the stacked images from previuous image if (!DB::ImageDB::instance()->getStackFor(info->fileName()).isEmpty()) - Q_FOREACH (const DB::FileName &a, DB::ImageDB::instance()->getStackFor(prev->fileName())) + for (const DB::FileName &a : DB::ImageDB::instance()->getStackFor(prev->fileName())) toBeShown.append(a); else toBeShown.append(prev->fileName()); } // Inserting stacked images from the current image if (!DB::ImageDB::instance()->getStackFor(info->fileName()).isEmpty()) - Q_FOREACH (const DB::FileName &a, DB::ImageDB::instance()->getStackFor(fileName)) + for (const DB::FileName &a : DB::ImageDB::instance()->getStackFor(fileName)) toBeShown.append(a); else toBeShown.append(info->fileName()); DB::ImageDB::instance()->stack(stack); } prev = info; } } void AutoStackImages::accept() { QDialog::accept(); Utilities::ShowBusyCursor dummy; DB::FileNameList toBeShown; if (m_matchingMD5->isChecked()) matchingMD5(toBeShown); if (m_matchingFile->isChecked()) matchingFile(toBeShown); if (m_continuousShooting->isChecked()) continuousShooting(toBeShown); MainWindow::Window::theMainWindow()->showThumbNails(toBeShown); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/CategoryImagePopup.cpp b/MainWindow/CategoryImagePopup.cpp index 73199d05..01c806d2 100644 --- a/MainWindow/CategoryImagePopup.cpp +++ b/MainWindow/CategoryImagePopup.cpp @@ -1,87 +1,88 @@ -/* Copyright (C) 2003-2018 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 "CategoryImagePopup.h" #include "Window.h" #include #include #include #include #include void MainWindow::CategoryImagePopup::populate(const QImage &image, const DB::FileName &imageName) { clear(); m_image = image; m_imageInfo = DB::ImageDB::instance()->info(imageName); // add the categories QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); - Q_FOREACH (const DB::CategoryPtr category, categories) { + for (const DB::CategoryPtr category : categories) { if (!category->isSpecialCategory()) { bool categoryMenuEnabled = false; const QString categoryName = category->name(); QMenu *categoryMenu = new QMenu(this); categoryMenu->setTitle(category->name()); // add category members Utilities::StringSet members = m_imageInfo->itemsOfCategory(categoryName); - Q_FOREACH (const QString &member, members) { + for (const QString &member : members) { QAction *action = categoryMenu->addAction(member); action->setObjectName(categoryName); action->setData(member); categoryMenuEnabled = true; } categoryMenu->setEnabled(categoryMenuEnabled); addMenu(categoryMenu); } } // Add the Category Editor menu item QAction *action = addAction(QString::fromLatin1("viewer-show-category-editor"), this, SLOT(makeCategoryImage())); action->setText(i18n("Show Category Editor")); } void MainWindow::CategoryImagePopup::slotExecuteService(QAction *action) { QString categoryName = action->objectName(); QString memberName = action->data().toString(); if (categoryName.isNull()) return; DB::ImageDB::instance()->categoryCollection()->categoryForName(categoryName)->setCategoryImage(categoryName, memberName, m_image); } void MainWindow::CategoryImagePopup::makeCategoryImage() { Viewer::CategoryImageConfig::instance()->setCurrentImage(m_image, m_imageInfo); Viewer::CategoryImageConfig::instance()->show(); } MainWindow::CategoryImagePopup::CategoryImagePopup(QWidget *parent) : QMenu(parent) { setTitle(i18n("Make Category Image")); connect(this, &CategoryImagePopup::triggered, this, &CategoryImagePopup::slotExecuteService); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DeleteDialog.cpp b/MainWindow/DeleteDialog.cpp index 03cff093..1d569e2a 100644 --- a/MainWindow/DeleteDialog.cpp +++ b/MainWindow/DeleteDialog.cpp @@ -1,109 +1,110 @@ -/* Copyright (C) 2003-2018 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 "DeleteDialog.h" #include #include #include #include #include #include #include #include using namespace MainWindow; DeleteDialog::DeleteDialog(QWidget *parent) : QDialog(parent) , m_list() { setWindowTitle(i18nc("@title:window", "Removing Items")); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); QWidget *top = new QWidget; QVBoxLayout *lay1 = new QVBoxLayout(top); mainLayout->addWidget(top); m_label = new QLabel; lay1->addWidget(m_label); m_useTrash = new QRadioButton; lay1->addWidget(m_useTrash); m_deleteFile = new QRadioButton; lay1->addWidget(m_deleteFile); m_deleteFromDb = new QRadioButton; lay1->addWidget(m_deleteFromDb); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &DeleteDialog::deleteImages); connect(buttonBox, &QDialogButtonBox::rejected, this, &DeleteDialog::reject); mainLayout->addWidget(buttonBox); } int DeleteDialog::exec(const DB::FileNameList &list) { if (!list.size()) return 0; bool someFileExists = false; - Q_FOREACH (const DB::FileName &file, list) { + for (const DB::FileName &file : list) { if (file.exists()) { someFileExists = true; break; } } const QString msg1 = i18np("Removing 1 item", "Removing %1 items", list.size()); const QString msg2 = i18np("Selected item will be removed from the database.
What do you want to do with the file on disk?", "Selected %1 items will be removed from the database.
What do you want to do with the files on disk?", list.size()); const QString txt = QString::fromLatin1("

%1
%2

").arg(msg1).arg(msg2); m_useTrash->setText(i18np("Move file to Trash", "Move %1 files to Trash", list.size())); m_deleteFile->setText(i18np("Delete file from disk", "Delete %1 files from disk", list.size())); m_deleteFromDb->setText(i18np("Only remove the item from database", "Only remove %1 items from database", list.size())); m_label->setText(txt); m_list = list; // disable trash/delete options if files don't exist m_useTrash->setChecked(someFileExists); m_useTrash->setEnabled(someFileExists); m_deleteFile->setEnabled(someFileExists); m_deleteFromDb->setChecked(!someFileExists); return QDialog::exec(); } void DeleteDialog::deleteImages() { bool anyDeleted = Utilities::DeleteFiles::deleteFiles(m_list, m_deleteFile->isChecked() ? Utilities::DeleteFromDisk : m_useTrash->isChecked() ? Utilities::MoveToTrash : Utilities::BlockFromDatabase); if (anyDeleted) accept(); else reject(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DuplicateMerger/DuplicateMatch.cpp b/MainWindow/DuplicateMerger/DuplicateMatch.cpp index a187bfbb..acfeb161 100644 --- a/MainWindow/DuplicateMerger/DuplicateMatch.cpp +++ b/MainWindow/DuplicateMerger/DuplicateMatch.cpp @@ -1,163 +1,163 @@ -/* Copyright (C) 2012-2019 The KPhotoAlbum Development Team - +/* Copyright (C) 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 + 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 + 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 . + along with this program. If not, see . */ #include "DuplicateMatch.h" #include "MergeToolTip.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace MainWindow { DuplicateMatch::DuplicateMatch(const DB::FileNameList &files) { QVBoxLayout *topLayout = new QVBoxLayout(this); QHBoxLayout *horizontalLayout = new QHBoxLayout; topLayout->addLayout(horizontalLayout); m_image = new QLabel; horizontalLayout->addWidget(m_image); QVBoxLayout *rightSideLayout = new QVBoxLayout; horizontalLayout->addSpacing(20); horizontalLayout->addLayout(rightSideLayout); horizontalLayout->addStretch(1); rightSideLayout->addStretch(1); m_merge = new QCheckBox(i18n("Merge these images")); rightSideLayout->addWidget(m_merge); m_merge->setChecked(false); connect(m_merge, &QCheckBox::toggled, this, &DuplicateMatch::selectionChanged); QWidget *options = new QWidget; rightSideLayout->addWidget(options); QVBoxLayout *optionsLayout = new QVBoxLayout(options); connect(m_merge, &QCheckBox::toggled, options, &QWidget::setEnabled); QLabel *label = new QLabel(i18n("Select target:")); optionsLayout->addWidget(label); bool first = true; - Q_FOREACH (const DB::FileName &fileName, files) { + for (const DB::FileName &fileName : files) { QHBoxLayout *lay = new QHBoxLayout; optionsLayout->addLayout(lay); QRadioButton *button = new QRadioButton(fileName.relative()); button->setProperty("data", QVariant::fromValue(fileName)); lay->addWidget(button); if (first) { button->setChecked(true); first = false; } QToolButton *details = new QToolButton; details->setText(i18nc("i for info", "i")); details->installEventFilter(this); details->setProperty("data", QVariant::fromValue(fileName)); lay->addWidget(details); m_buttons.append(button); } rightSideLayout->addStretch(1); QFrame *line = new QFrame; line->setFrameStyle(QFrame::HLine); topLayout->addWidget(line); const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(files.first()); const int angle = info->angle(); ImageManager::ImageRequest *request = new ImageManager::ImageRequest(files.first(), QSize(300, 300), angle, this); ImageManager::AsyncLoader::instance()->load(request); } void DuplicateMatch::pixmapLoaded(ImageManager::ImageRequest * /*request*/, const QImage &image) { m_image->setPixmap(QPixmap::fromImage(image)); } void DuplicateMatch::setSelected(bool b) { m_merge->setChecked(b); } bool DuplicateMatch::selected() const { return m_merge->isChecked(); } void DuplicateMatch::execute(Utilities::DeleteMethod method) { if (!m_merge->isChecked()) return; DB::FileName destination; - Q_FOREACH (QRadioButton *button, m_buttons) { + for (QRadioButton *button : m_buttons) { if (button->isChecked()) { destination = button->property("data").value(); break; } } DB::FileNameList deleteList, dupList; - Q_FOREACH (QRadioButton *button, m_buttons) { + for (QRadioButton *button : m_buttons) { if (button->isChecked()) continue; DB::FileName fileName = button->property("data").value(); DB::ImageDB::instance()->copyData(fileName, destination); // can we safely delete the file? if (fileName != destination) deleteList.append(fileName); else dupList.append(fileName); } Utilities::DeleteFiles::deleteFiles(deleteList, method); // remove duplicate DB-entries without removing or blocking the file: DB::ImageDB::instance()->deleteList(dupList); } bool DuplicateMatch::eventFilter(QObject *obj, QEvent *event) { if (event->type() != QEvent::Enter) return false; QToolButton *but; if (!(but = qobject_cast(obj))) return false; const DB::FileName fileName = but->property("data").value(); MergeToolTip::instance()->requestToolTip(fileName); return false; } } // namespace MainWindow // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DuplicateMerger/DuplicateMerger.cpp b/MainWindow/DuplicateMerger/DuplicateMerger.cpp index c2350d18..2bfb85b4 100644 --- a/MainWindow/DuplicateMerger/DuplicateMerger.cpp +++ b/MainWindow/DuplicateMerger/DuplicateMerger.cpp @@ -1,207 +1,207 @@ -/* Copyright 2012-2019 The KPhotoAlbum Development Team +/* Copyright (C) 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 + 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 + 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 . + along with this program. If not, see . */ #include "DuplicateMerger.h" #include "DuplicateMatch.h" #include "MergeToolTip.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace MainWindow { DuplicateMerger::DuplicateMerger(QWidget *parent) : QDialog(parent) { setAttribute(Qt::WA_DeleteOnClose); resize(800, 600); QWidget *top = new QWidget(this); QVBoxLayout *topLayout = new QVBoxLayout(top); setLayout(topLayout); topLayout->addWidget(top); QString txt = i18n("

Below is a list of all images that are duplicate in your database.
" "Select which you want merged, and which of the duplicates should be kept.
" "The tag and description from the deleted images will be transferred to the kept image

"); QLabel *label = new QLabel(txt); QFont fnt = font(); fnt.setPixelSize(18); label->setFont(fnt); topLayout->addWidget(label); m_trash = new QRadioButton(i18n("Move to &trash")); m_deleteFromDisk = new QRadioButton(i18n("&Delete from disk")); QRadioButton *blockFromDB = new QRadioButton(i18n("&Block from database")); m_trash->setChecked(true); topLayout->addSpacing(10); topLayout->addWidget(m_trash); topLayout->addWidget(m_deleteFromDisk); topLayout->addWidget(blockFromDB); topLayout->addSpacing(10); QScrollArea *scrollArea = new QScrollArea; topLayout->addWidget(scrollArea); scrollArea->setWidgetResizable(true); m_container = new QWidget(scrollArea); m_scrollLayout = new QVBoxLayout(m_container); scrollArea->setWidget(m_container); m_selectionCount = new QLabel; topLayout->addWidget(m_selectionCount); QDialogButtonBox *buttonBox = new QDialogButtonBox(); m_selectAllButton = buttonBox->addButton(i18n("Select &All"), QDialogButtonBox::YesRole); m_selectNoneButton = buttonBox->addButton(i18n("Select &None"), QDialogButtonBox::NoRole); m_okButton = buttonBox->addButton(QDialogButtonBox::Ok); m_cancelButton = buttonBox->addButton(QDialogButtonBox::Cancel); connect(m_selectAllButton, &QPushButton::clicked, this, QOverload::of(&DuplicateMerger::selectAll)); connect(m_selectNoneButton, &QPushButton::clicked, this, &DuplicateMerger::selectNone); connect(m_okButton, &QPushButton::clicked, this, &DuplicateMerger::go); connect(m_cancelButton, &QPushButton::clicked, this, &DuplicateMerger::reject); topLayout->addWidget(buttonBox); findDuplicates(); } MainWindow::DuplicateMerger::~DuplicateMerger() { MergeToolTip::destroy(); } void DuplicateMerger::selectAll() { selectAll(true); } void DuplicateMerger::selectNone() { selectAll(false); } void DuplicateMerger::go() { Utilities::DeleteMethod method = Utilities::BlockFromDatabase; if (m_trash->isChecked()) { method = Utilities::MoveToTrash; } else if (m_deleteFromDisk->isChecked()) { method = Utilities::DeleteFromDisk; } - Q_FOREACH (DuplicateMatch *selector, m_selectors) { + for (DuplicateMatch *selector : m_selectors) { selector->execute(method); } accept(); } void DuplicateMerger::updateSelectionCount() { int total = 0; int selected = 0; - Q_FOREACH (DuplicateMatch *selector, m_selectors) { + for (DuplicateMatch *selector : m_selectors) { ++total; if (selector->selected()) ++selected; } m_selectionCount->setText(i18n("%1 of %2 selected", selected, total)); m_okButton->setEnabled(selected > 0); } void DuplicateMerger::findDuplicates() { Utilities::ShowBusyCursor dummy; - Q_FOREACH (const DB::FileName &fileName, DB::ImageDB::instance()->images()) { + for (const DB::FileName &fileName : DB::ImageDB::instance()->images()) { const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); const DB::MD5 md5 = info->MD5Sum(); m_matches[md5].append(fileName); } bool anyFound = false; for (QMap::const_iterator it = m_matches.constBegin(); it != m_matches.constEnd(); ++it) { if (it.value().count() > 1) { addRow(it.key()); anyFound = true; } } if (!anyFound) { tellThatNoDuplicatesWereFound(); } updateSelectionCount(); } void DuplicateMerger::addRow(const DB::MD5 &md5) { DuplicateMatch *match = new DuplicateMatch(m_matches[md5]); connect(match, &DuplicateMatch::selectionChanged, this, &DuplicateMerger::updateSelectionCount); m_scrollLayout->addWidget(match); m_selectors.append(match); } void DuplicateMerger::selectAll(bool b) { - Q_FOREACH (DuplicateMatch *selector, m_selectors) { + for (DuplicateMatch *selector : m_selectors) { selector->setSelected(b); } } void DuplicateMerger::tellThatNoDuplicatesWereFound() { QLabel *label = new QLabel(i18n("No duplicates found")); QFont fnt = font(); fnt.setPixelSize(30); label->setFont(fnt); m_scrollLayout->addWidget(label); m_selectAllButton->setEnabled(false); m_selectNoneButton->setEnabled(false); m_okButton->setEnabled(false); } } // namespace MainWindow // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/ExternalPopup.cpp b/MainWindow/ExternalPopup.cpp index 61ba320d..44bcf6e8 100644 --- a/MainWindow/ExternalPopup.cpp +++ b/MainWindow/ExternalPopup.cpp @@ -1,203 +1,204 @@ -/* Copyright (C) 2003-2018 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 "ExternalPopup.h" #include "Logging.h" #include "RunDialog.h" #include "Window.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include void MainWindow::ExternalPopup::populate(DB::ImageInfoPtr current, const DB::FileNameList &imageList) { m_list = imageList; m_currentInfo = current; clear(); QAction *action; QStringList list = QStringList() << i18n("Current Item") << i18n("All Selected Items") << i18n("Copy and Open"); for (int which = 0; which < 3; ++which) { if (which == 0 && !current) continue; const bool multiple = (m_list.count() > 1); const bool enabled = (which != 1 && m_currentInfo) || (which == 1 && multiple); // Submenu QMenu *submenu = addMenu(list[which]); submenu->setEnabled(enabled); // Fetch set of offers OfferType offers; if (which == 0) offers = appInfos(DB::FileNameList() << current->fileName()); else offers = appInfos(imageList); for (OfferType::const_iterator offerIt = offers.begin(); offerIt != offers.end(); ++offerIt) { action = submenu->addAction((*offerIt).first); action->setObjectName((*offerIt).first); // Notice this is needed to find the application later! action->setIcon(QIcon::fromTheme((*offerIt).second)); action->setData(which); action->setEnabled(enabled); } // A personal command action = submenu->addAction(i18n("Open With...")); action->setObjectName(i18n("Open With...")); // Notice this is needed to find the application later! // XXX: action->setIcon( QIcon::fromTheme((*offerIt).second) ); action->setData(which); action->setEnabled(enabled); // A personal command // XXX: see kdialog.h for simple usage action = submenu->addAction(i18n("Your Command Line")); action->setObjectName(i18n("Your Command Line")); // Notice this is needed to find the application later! // XXX: action->setIcon( QIcon::fromTheme((*offerIt).second) ); action->setData(which); action->setEnabled(enabled); } } void MainWindow::ExternalPopup::slotExecuteService(QAction *action) { QString name = action->objectName(); const StringSet apps = m_appToMimeTypeMap[name]; // get the list of arguments QList lst; if (action->data() == -1) { return; //user clicked the title entry. (i.e: "All Selected Items") } else if (action->data() == 1) { - Q_FOREACH (const DB::FileName &file, m_list) { + for (const DB::FileName &file : m_list) { if (m_appToMimeTypeMap[name].contains(mimeType(file))) lst.append(QUrl(file.absolute())); } } else if (action->data() == 2) { QString origFile = m_currentInfo->fileName().absolute(); QString newFile = origFile; QString origRegexpString = Settings::SettingsData::instance()->copyFileComponent(); QRegExp origRegexp = QRegExp(origRegexpString); QString copyFileReplacement = Settings::SettingsData::instance()->copyFileReplacementComponent(); if (origRegexpString.length() > 0) { newFile.replace(origRegexp, copyFileReplacement); QFile::copy(origFile, newFile); lst.append(QUrl::fromLocalFile(newFile)); } else { qCWarning(MainWindowLog, "No settings were appropriate for modifying the file name (you must fill in the regexp field; Opening the original instead"); lst.append(QUrl::fromLocalFile(origFile)); } } else { lst.append(QUrl(m_currentInfo->fileName().absolute())); } // get the program to run // check for the special entry for self-defined if (name == i18n("Your Command Line")) { static RunDialog *dialog = new RunDialog(MainWindow::Window::theMainWindow()); dialog->setImageList(m_list); dialog->show(); return; } // check for the special entry for self-defined if (name == i18n("Open With...")) { KRun::displayOpenWithDialog(lst, MainWindow::Window::theMainWindow()); return; } KService::List offers = KMimeTypeTrader::self()->query(*(apps.begin()), QString::fromLatin1("Application"), QString::fromLatin1("Name == '%1'").arg(name)); Q_ASSERT(offers.count() >= 1); KService::Ptr ptr = offers.first(); KRun::runService(*ptr, lst, MainWindow::Window::theMainWindow()); } MainWindow::ExternalPopup::ExternalPopup(QWidget *parent) : QMenu(parent) { setTitle(i18n("Invoke External Program")); connect(this, &ExternalPopup::triggered, this, &ExternalPopup::slotExecuteService); } QString MainWindow::ExternalPopup::mimeType(const DB::FileName &file) { QMimeDatabase db; return db.mimeTypeForFile(file.absolute(), QMimeDatabase::MatchExtension).name(); } Utilities::StringSet MainWindow::ExternalPopup::mimeTypes(const DB::FileNameList &files) { StringSet res; StringSet extensions; - Q_FOREACH (const DB::FileName &file, files) { + for (const DB::FileName &file : files) { const DB::FileName baseFileName = file; const int extStart = baseFileName.relative().lastIndexOf(QChar::fromLatin1('.')); const QString ext = baseFileName.relative().mid(extStart); if (!extensions.contains(ext)) { res.insert(mimeType(file)); extensions.insert(ext); } } return res; } MainWindow::OfferType MainWindow::ExternalPopup::appInfos(const DB::FileNameList &files) { StringSet types = mimeTypes(files); OfferType res; - Q_FOREACH (const QString &type, types) { + for (const QString &type : types) { KService::List offers = KMimeTypeTrader::self()->query(type, QLatin1String("Application")); - Q_FOREACH (const KService::Ptr offer, offers) { + for (const KService::Ptr offer : offers) { res.insert(qMakePair(offer->name(), offer->icon())); m_appToMimeTypeMap[offer->name()].insert(type); } } return res; } bool operator<(const QPair &a, const QPair &b) { return a.first < b.first; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/InvalidDateFinder.cpp b/MainWindow/InvalidDateFinder.cpp index 3a3f172f..cbfdc932 100644 --- a/MainWindow/InvalidDateFinder.cpp +++ b/MainWindow/InvalidDateFinder.cpp @@ -1,149 +1,150 @@ -/* Copyright (C) 2003-2018 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 "InvalidDateFinder.h" #include "Window.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MainWindow; InvalidDateFinder::InvalidDateFinder(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Search for Images and Videos with Missing Dates")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QGroupBox *grp = new QGroupBox(i18n("Which Images and Videos to Display")); QVBoxLayout *grpLay = new QVBoxLayout(grp); mainLayout->addWidget(grp); m_dateNotTime = new QRadioButton(i18n("Search for images and videos with a valid date but an invalid time stamp")); m_missingDate = new QRadioButton(i18n("Search for images and videos missing date and time")); m_partialDate = new QRadioButton(i18n("Search for images and videos with only partial dates (like 1971 vs. 11/7-1971)")); m_dateNotTime->setChecked(true); grpLay->addWidget(m_dateNotTime); grpLay->addWidget(m_missingDate); grpLay->addWidget(m_partialDate); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &InvalidDateFinder::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &InvalidDateFinder::reject); mainLayout->addWidget(buttonBox); } void InvalidDateFinder::accept() { QDialog::accept(); Utilities::ShowBusyCursor dummy; // create the info dialog QDialog *info = new QDialog; QVBoxLayout *mainLayout = new QVBoxLayout; info->setLayout(mainLayout); info->setWindowTitle(i18nc("@title:window", "Image Info")); KTextEdit *edit = new KTextEdit(info); mainLayout->addWidget(edit); edit->setText(i18n("

Here you may see the date changes for the displayed items.

")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); info->connect(buttonBox, &QDialogButtonBox::accepted, info, &QDialog::accept); info->connect(buttonBox, &QDialogButtonBox::rejected, info, &QDialog::reject); mainLayout->addWidget(buttonBox); // Now search for the images. const DB::FileNameList list = DB::ImageDB::instance()->images(); DB::FileNameList toBeShown; QProgressDialog dialog(nullptr); dialog.setWindowTitle(i18nc("@title:window", "Reading File Properties")); dialog.setMaximum(list.size()); dialog.setValue(0); int progress = 0; - Q_FOREACH (const DB::FileName &fileName, list) { + for (const DB::FileName &fileName : list) { dialog.setValue(++progress); qApp->processEvents(QEventLoop::AllEvents); if (dialog.wasCanceled()) break; if (fileName.info()->isNull()) continue; DB::ImageDate date = fileName.info()->date(); bool show = false; if (m_dateNotTime->isChecked()) { DB::FileInfo fi = DB::FileInfo::read(fileName, DB::EXIFMODE_DATE); if (fi.dateTime().date() == date.start().date()) show = (fi.dateTime().time() != date.start().time()); if (show) { edit->append(QString::fromLatin1("%1:
existing = %2
new..... = %3") .arg(fileName.relative()) .arg(date.start().toString()) .arg(fi.dateTime().toString())); } } else if (m_missingDate->isChecked()) { show = !date.start().isValid(); } else if (m_partialDate->isChecked()) { show = (date.start() != date.end()); } if (show) toBeShown.append(fileName); } if (m_dateNotTime->isChecked()) { info->resize(800, 600); edit->setReadOnly(true); QFont f = edit->font(); f.setFamily(QString::fromLatin1("fixed")); edit->setFont(f); info->show(); } else delete info; Window::theMainWindow()->showThumbNails(toBeShown); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/RunDialog.cpp b/MainWindow/RunDialog.cpp index b29d0d15..280a8e55 100644 --- a/MainWindow/RunDialog.cpp +++ b/MainWindow/RunDialog.cpp @@ -1,107 +1,108 @@ -/* Copyright (C) 2009-2010 Wes Hardaker +/* Copyright (C) 2009-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 "RunDialog.h" #include "Window.h" #include #include #include #include #include #include #include #include #include MainWindow::RunDialog::RunDialog(QWidget *parent) : QDialog(parent) { QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); // xgettext: no-c-format QString txt = i18n("

Enter your command to run below:

" "

%all will be replaced with a file list

"); QLabel *label = new QLabel(txt); mainLayout->addWidget(label); m_cmd = new QLineEdit(); mainLayout->addWidget(m_cmd); m_cmd->setMinimumWidth(400); // xgettext: no-c-format txt = i18n("

Enter the command you want to run on your image file(s). " "KPhotoAlbum will run your command and replace any '%all' tokens " "with a list of your files. For example, if you entered:

" "
  • cp %all /tmp
" "

Then the files you selected would be copied to the /tmp " "directory

" "

You can also use %each to have a command be run once per " "file.

"); m_cmd->setWhatsThis(txt); label->setWhatsThis(txt); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); mainLayout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(this, &QDialog::accepted, this, &RunDialog::slotMarkGo); } void MainWindow::RunDialog::setImageList(const DB::FileNameList &fileList) { m_fileList = fileList; } void MainWindow::RunDialog::slotMarkGo() { QString cmdString = m_cmd->text(); // xgettext: no-c-format QRegExp replaceall = QRegExp(i18nc("As in 'Execute a command and replace any occurrence of %all with the filenames of all selected files'", "%all")); // xgettext: no-c-format QRegExp replaceeach = QRegExp(i18nc("As in 'Execute a command for each selected file in turn and replace any occurrence of %each with the filename ", "%each")); // Replace the %all argument first QStringList fileList; - Q_FOREACH (const DB::FileName &fileName, m_fileList) + for (const DB::FileName &fileName : m_fileList) fileList.append(fileName.absolute()); cmdString.replace(replaceall, KShell::joinArgs(fileList)); if (cmdString.contains(replaceeach)) { // cmdString should be run multiple times, once per "each" QString cmdOnce; - Q_FOREACH (const DB::FileName &filename, m_fileList) { + for (const DB::FileName &filename : m_fileList) { cmdOnce = cmdString; cmdOnce.replace(replaceeach, filename.absolute()); KRun::runCommand(cmdOnce, MainWindow::Window::theMainWindow()); } } else { KRun::runCommand(cmdString, MainWindow::Window::theMainWindow()); } } void MainWindow::RunDialog::show() { QDialog::show(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/StatisticsDialog.cpp b/MainWindow/StatisticsDialog.cpp index 498a56ba..b5896262 100644 --- a/MainWindow/StatisticsDialog.cpp +++ b/MainWindow/StatisticsDialog.cpp @@ -1,225 +1,227 @@ -/* 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. + 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 "StatisticsDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MainWindow; StatisticsDialog::StatisticsDialog(QWidget *parent) : QDialog(parent) { QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); QString txt = i18n("

Description

" "" "" "" "" "
# of ItemsThis is the number of different items in the category
Tags TotalThis is a count of how many tags was made,
i.e. a simple counting though all the images
Tags Per PictureThis tells you how many tags are on each picture on average


" "Do not get too attached to this dialog, it has the problem that it counts categories AND subcategories,
" "so if an image has been taken in Las Vegas, Nevada, USA, then 3 tags are counted for that image,
" "while it should only be one.
" "I am not really sure if it is worth fixing that bug (as it is pretty hard to fix),
" "so maybe the dialog will simply go away again"); QLabel *label = new QLabel(txt); layout->addWidget(label); layout->addWidget(createAnnotatedGroupBox()); label = new QLabel(i18n("

Statistics

")); layout->addWidget(label); m_treeWidget = new QTreeWidget; layout->addWidget(m_treeWidget); QStringList labels; labels << i18n("Category") << i18n("# of Items") << i18n("Tags Totals") << i18n("Tags Per Picture") << QString(); m_treeWidget->setHeaderLabels(labels); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); layout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); } void StatisticsDialog::show() { populate(); QDialog::show(); } QSize MainWindow::StatisticsDialog::sizeHint() const { return QSize(800, 800); } QTreeWidgetItem *MainWindow::StatisticsDialog::addRow(const QString &title, int noOfTags, int tagCount, int imageCount, QTreeWidgetItem *parent) { QStringList list; list << title << QString::number(noOfTags) << QString::number(tagCount) << QString::number((double)tagCount / imageCount, 'F', 2); QTreeWidgetItem *item = new QTreeWidgetItem(parent, list); for (int col = 1; col < 4; ++col) item->setTextAlignment(col, Qt::AlignRight); return item; } void MainWindow::StatisticsDialog::highlightTotalRow(QTreeWidgetItem *item) { for (int col = 0; col < 5; ++col) { QFont font = item->data(col, Qt::FontRole).value(); font.setWeight(QFont::Bold); item->setData(col, Qt::FontRole, font); } } QGroupBox *MainWindow::StatisticsDialog::createAnnotatedGroupBox() { QGroupBox *box = new QGroupBox(i18n("Tag indication completed annotation")); m_boxLayout = new QGridLayout(box); m_boxLayout->setColumnStretch(2, 1); int row = -1; QLabel *label = new QLabel(i18n("If you use a specific tag to indicate that an image has been tagged, then specify it here.")); label->setWordWrap(true); m_boxLayout->addWidget(label, ++row, 0, 1, 3); label = new QLabel(i18n("Category:")); m_boxLayout->addWidget(label, ++row, 0); m_category = new KComboBox; m_boxLayout->addWidget(m_category, row, 1); m_tagLabel = new QLabel(i18n("Tag:")); m_boxLayout->addWidget(m_tagLabel, ++row, 0); m_tag = new KComboBox; m_tag->setSizeAdjustPolicy(KComboBox::AdjustToContents); m_boxLayout->addWidget(m_tag, row, 1); m_category->addItem(i18nc("@item:inlistbox meaning 'no category'", "None")); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); - Q_FOREACH (const DB::CategoryPtr &category, categories) { + for (const DB::CategoryPtr &category : categories) { if (category->type() == DB::Category::MediaTypeCategory || category->type() == DB::Category::FolderCategory) { continue; } m_category->addItem(category->name(), category->name()); } connect(m_category, static_cast(&KComboBox::activated), this, &StatisticsDialog::categoryChanged); connect(m_tag, static_cast(&KComboBox::activated), this, &StatisticsDialog::populate); m_tagLabel->setEnabled(false); m_tag->setEnabled(false); return box; } void MainWindow::StatisticsDialog::categoryChanged(int index) { const bool enabled = (index != 0); m_tagLabel->setEnabled(enabled); m_tag->setEnabled(enabled); m_tag->clear(); if (enabled) { const QString name = m_category->itemData(index).value(); DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(name); m_tag->addItems(category->items()); } } void MainWindow::StatisticsDialog::populate() { Utilities::ShowBusyCursor dummy; m_treeWidget->clear(); const int imageCount = DB::ImageDB::instance()->totalCount(); QTreeWidgetItem *top = new QTreeWidgetItem(m_treeWidget, QStringList() << i18nc("As in 'all images'", "All") << QString::number(imageCount)); top->setTextAlignment(1, Qt::AlignRight); populateSubTree(DB::ImageSearchInfo(), imageCount, top); if (m_category->currentIndex() != 0) { const QString category = m_category->itemData(m_category->currentIndex()).value(); const QString tag = m_tag->currentText(); DB::ImageSearchInfo info; info.setCategoryMatchText(category, tag); const int imageCount = DB::ImageDB::instance()->count(info).total(); QTreeWidgetItem *item = new QTreeWidgetItem(m_treeWidget, QStringList() << QString::fromLatin1("%1: %2").arg(category).arg(tag) << QString::number(imageCount)); item->setTextAlignment(1, Qt::AlignRight); populateSubTree(info, imageCount, item); } m_treeWidget->header()->resizeSections(QHeaderView::ResizeToContents); } void MainWindow::StatisticsDialog::populateSubTree(const DB::ImageSearchInfo &info, int imageCount, QTreeWidgetItem *top) { top->setExpanded(true); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); int tagsTotal = 0; int grantTotal = 0; - Q_FOREACH (const DB::CategoryPtr &category, categories) { + for (const DB::CategoryPtr &category : categories) { if (category->type() == DB::Category::MediaTypeCategory || category->type() == DB::Category::FolderCategory) { continue; } const QMap tags = DB::ImageDB::instance()->classify(info, category->name(), DB::anyMediaType); int total = 0; for (auto tagIt = tags.constBegin(); tagIt != tags.constEnd(); ++tagIt) { // Don't count the NONE tag, and the OK tag if (tagIt.key() != DB::ImageDB::NONE() && (category->name() != m_category->currentText() || tagIt.key() != m_tag->currentText())) total += tagIt.value().count; } addRow(category->name(), tags.count() - 1, total, imageCount, top); tagsTotal += tags.count() - 1; grantTotal += total; } QTreeWidgetItem *totalRow = addRow(i18n("Total"), tagsTotal, grantTotal, imageCount, top); highlightTotalRow(totalRow); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/TokenEditor.cpp b/MainWindow/TokenEditor.cpp index a6648792..0af6de82 100644 --- a/MainWindow/TokenEditor.cpp +++ b/MainWindow/TokenEditor.cpp @@ -1,138 +1,139 @@ -/* 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. + 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 "TokenEditor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MainWindow; TokenEditor::TokenEditor(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Remove Tokens")); QVBoxLayout *dialogLayout = new QVBoxLayout(this); QWidget *mainContents = new QWidget; QVBoxLayout *vlay = new QVBoxLayout(mainContents); QLabel *label = new QLabel(i18n("Select tokens to remove from all images and videos:")); vlay->addWidget(label); QGridLayout *grid = new QGridLayout; vlay->addLayout(grid); int index = 0; for (int ch = 'A'; ch <= 'Z'; ch++, index++) { QChar token = QChar::fromLatin1((char)ch); QCheckBox *box = new QCheckBox(token); grid->addWidget(box, index / 5, index % 5); m_checkBoxes.append(box); } QHBoxLayout *hlay = new QHBoxLayout; vlay->addLayout(hlay); hlay->addStretch(1); QPushButton *selectAll = new QPushButton(i18n("Select All")); QPushButton *selectNone = new QPushButton(i18n("Select None")); hlay->addWidget(selectAll); hlay->addWidget(selectNone); connect(selectAll, &QPushButton::clicked, this, &TokenEditor::selectAll); connect(selectNone, &QPushButton::clicked, this, &TokenEditor::selectNone); dialogLayout->addWidget(mainContents); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &TokenEditor::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &TokenEditor::reject); dialogLayout->addWidget(buttonBox); } void TokenEditor::show() { QStringList tokens = tokensInUse(); - Q_FOREACH (QCheckBox *box, m_checkBoxes) { + for (QCheckBox *box : m_checkBoxes) { box->setChecked(false); QString txt = box->text().remove(QString::fromLatin1("&")); box->setEnabled(tokens.contains(txt)); } QDialog::show(); } void TokenEditor::selectAll() { - Q_FOREACH (QCheckBox *box, m_checkBoxes) { + for (QCheckBox *box : m_checkBoxes) { box->setChecked(true); } } void TokenEditor::selectNone() { - Q_FOREACH (QCheckBox *box, m_checkBoxes) { + for (QCheckBox *box : m_checkBoxes) { box->setChecked(false); } } /** I would love to use Settings::optionValue, but that method does not forget about an item once it has seen it, which is really what it should do anyway, otherwise it would be way to expensive in use. */ QStringList TokenEditor::tokensInUse() { QStringList res; DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); QMap map = DB::ImageDB::instance()->classify(DB::ImageSearchInfo(), tokensCategory->name(), DB::anyMediaType); for (auto it = map.constBegin(); it != map.constEnd(); ++it) { if (it.value().count > 0) res.append(it.key()); } return res; } void TokenEditor::accept() { DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); - Q_FOREACH (const QCheckBox *box, m_checkBoxes) { + for (const QCheckBox *box : m_checkBoxes) { if (box->isChecked() && box->isEnabled()) { QString txt = box->text().remove(QString::fromLatin1("&")); tokensCategory->removeItem(txt); } } QDialog::accept(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/UpdateVideoThumbnail.cpp b/MainWindow/UpdateVideoThumbnail.cpp index df9bb93c..b6992a9b 100644 --- a/MainWindow/UpdateVideoThumbnail.cpp +++ b/MainWindow/UpdateVideoThumbnail.cpp @@ -1,89 +1,89 @@ -/* Copyright (C) 2012-2019 The KPhotoAlbum Development Team +/* Copyright (C) 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 + 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 + 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 . + along with this program. If not, see . */ #include "UpdateVideoThumbnail.h" #include "Window.h" #include #include #include #include #include namespace MainWindow { void UpdateVideoThumbnail::useNext(const DB::FileNameList &list) { update(list, +1); } void UpdateVideoThumbnail::usePrevious(const DB::FileNameList &list) { update(list, -1); } void UpdateVideoThumbnail::update(const DB::FileNameList &list, int direction) { - Q_FOREACH (const DB::FileName &fileName, list) { + for (const DB::FileName &fileName : list) { if (Utilities::isVideo(fileName)) update(fileName, direction); } } void UpdateVideoThumbnail::update(const DB::FileName &fileName, int direction) { const DB::FileName baseImageName = BackgroundJobs::HandleVideoThumbnailRequestJob::pathForRequest(fileName); QImage baseImage(baseImageName.absolute()); int frame = 0; for (; frame < 10; ++frame) { const DB::FileName frameFile = BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(fileName, frame); QImage frameImage(frameFile.absolute()); if (frameImage.isNull()) continue; if (baseImage == frameImage) { break; } } const DB::FileName newImageName = nextExistingImage(fileName, frame, direction); Utilities::copyOrOverwrite(newImageName.absolute(), baseImageName.absolute()); QImage image = QImage(newImageName.absolute()).scaled(ThumbnailView::CellGeometry::preferredIconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation); ImageManager::ThumbnailCache::instance()->insert(fileName, image); MainWindow::Window::theMainWindow()->reloadThumbnails(); } DB::FileName UpdateVideoThumbnail::nextExistingImage(const DB::FileName &fileName, int frame, int direction) { for (int i = 1; i < 10; ++i) { const int nextIndex = (frame + 10 + direction * i) % 10; const DB::FileName file = BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(fileName, nextIndex); if (file.exists()) return file; } Q_ASSERT(false && "We should always find at least the current frame"); return DB::FileName(); } } // namespace MainWindow // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Window.cpp b/MainWindow/Window.cpp index 901f728e..4e3c9970 100644 --- a/MainWindow/Window.cpp +++ b/MainWindow/Window.cpp @@ -1,1998 +1,1999 @@ /* 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 "Window.h" #include #include #ifdef HAVE_STDLIB_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for #if KIO_VERSION... #include #ifdef HASKIPI #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HASKIPI #include #endif #ifdef KF5Purpose_FOUND #include #endif #include "AutoStackImages.h" #include "BreadcrumbViewer.h" #include "CopyPopup.h" #include "DeleteDialog.h" #include "DirtyIndicator.h" #include "DuplicateMerger/DuplicateMerger.h" #include "ExternalPopup.h" #include "FeatureDialog.h" #include "ImageCounter.h" #include "InvalidDateFinder.h" #include "Logging.h" #include "Options.h" #include "SearchBar.h" #include "SplashScreen.h" #include "StatisticsDialog.h" #include "StatusBar.h" #include "TokenEditor.h" #include "UpdateVideoThumbnail.h" #include "WelcomeDialog.h" #ifdef KPA_ENABLE_REMOTECONTROL #include #endif #include #include #include #include #include #include #include #include #include #include #include using namespace DB; MainWindow::Window *MainWindow::Window::s_instance = nullptr; MainWindow::Window::Window(QWidget *parent) : KXmlGuiWindow(parent) , m_annotationDialog(nullptr) , m_deleteDialog(nullptr) , m_htmlDialog(nullptr) , m_tokenEditor(nullptr) { #ifdef HAVE_KGEOMAP m_positionBrowser = 0; #endif qCDebug(MainWindowLog) << "Using icon theme: " << QIcon::themeName(); qCDebug(MainWindowLog) << "Icon search paths: " << QIcon::themeSearchPaths(); QElapsedTimer timer; timer.start(); SplashScreen::instance()->message(i18n("Loading Database")); s_instance = this; bool gotConfigFile = load(); if (!gotConfigFile) throw 0; qCInfo(TimingLog) << "MainWindow: Loading Database: " << timer.restart() << "ms."; SplashScreen::instance()->message(i18n("Loading Main Window")); QWidget *top = new QWidget(this); QVBoxLayout *lay = new QVBoxLayout(top); lay->setSpacing(2); lay->setContentsMargins(2, 2, 2, 2); setCentralWidget(top); m_stack = new QStackedWidget(top); lay->addWidget(m_stack, 1); m_dateBar = new DateBar::DateBarWidget(top); lay->addWidget(m_dateBar); m_dateBarLine = new QFrame(top); m_dateBarLine->setFrameStyle(QFrame::HLine | QFrame::Plain); m_dateBarLine->setLineWidth(0); m_dateBarLine->setMidLineWidth(0); QPalette pal = m_dateBarLine->palette(); pal.setColor(QPalette::WindowText, QColor("#c4c1bd")); m_dateBarLine->setPalette(pal); lay->addWidget(m_dateBarLine); setHistogramVisibilty(Settings::SettingsData::instance()->showHistogram()); m_browser = new Browser::BrowserWidget(m_stack); m_thumbnailView = new ThumbnailView::ThumbnailFacade(); m_stack->addWidget(m_browser); m_stack->addWidget(m_thumbnailView->gui()); m_stack->setCurrentWidget(m_browser); m_settingsDialog = nullptr; qCInfo(TimingLog) << "MainWindow: Loading MainWindow: " << timer.restart() << "ms."; setupMenuBar(); qCInfo(TimingLog) << "MainWindow: setupMenuBar: " << timer.restart() << "ms."; createSearchBar(); qCInfo(TimingLog) << "MainWindow: createSearchBar: " << timer.restart() << "ms."; setupStatusBar(); qCInfo(TimingLog) << "MainWindow: setupStatusBar: " << timer.restart() << "ms."; // Misc m_autoSaveTimer = new QTimer(this); connect(m_autoSaveTimer, &QTimer::timeout, this, &Window::slotAutoSave); startAutoSaveTimer(); connect(m_browser, &Browser::BrowserWidget::showingOverview, this, &Window::showBrowser); connect(m_browser, &Browser::BrowserWidget::pathChanged, m_statusBar->mp_pathIndicator, &BreadcrumbViewer::setBreadcrumbs); connect(m_statusBar->mp_pathIndicator, &BreadcrumbViewer::widenToBreadcrumb, m_browser, &Browser::BrowserWidget::widenToBreadcrumb); connect(m_browser, &Browser::BrowserWidget::pathChanged, this, QOverload::of(&Window::updateDateBar)); connect(m_dateBar, &DateBar::DateBarWidget::dateSelected, m_thumbnailView, &ThumbnailView::ThumbnailFacade::gotoDate); connect(m_dateBar, &DateBar::DateBarWidget::toolTipInfo, this, &Window::showDateBarTip); connect(Settings::SettingsData::instance(), &Settings::SettingsData::histogramSizeChanged, m_dateBar, &DateBar::DateBarWidget::setHistogramBarSize); connect(Settings::SettingsData::instance(), &Settings::SettingsData::actualThumbnailSizeChanged, this, &Window::slotThumbnailSizeChanged); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeChange, this, &Window::setDateRange); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeCleared, this, &Window::clearDateRange); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::currentDateChanged, m_dateBar, &DateBar::DateBarWidget::setDate); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::showImage, this, &Window::showImage); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::showSelection, this, QOverload<>::of(&Window::slotView)); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::fileIdUnderCursorChanged, this, &Window::slotSetFileName); connect(DB::ImageDB::instance(), &DB::ImageDB::totalChanged, this, QOverload<>::of(&Window::updateDateBar)); connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::categoryCollectionChanged, this, &Window::slotOptionGroupChanged); connect(m_browser, &Browser::BrowserWidget::imageCount, m_statusBar->mp_partial, &ImageCounter::showBrowserMatches); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, this, &Window::updateContextMenuFromSelectionSize); checkIfVideoThumbnailerIsInstalled(); executeStartupActions(); qCInfo(TimingLog) << "MainWindow: executeStartupActions " << timer.restart() << "ms."; QTimer::singleShot(0, this, SLOT(delayedInit())); updateContextMenuFromSelectionSize(0); // Automatically save toolbar settings setAutoSaveSettings(); qCInfo(TimingLog) << "MainWindow: misc setup time: " << timer.restart() << "ms."; } MainWindow::Window::~Window() { DB::ImageDB::deleteInstance(); ImageManager::ThumbnailCache::deleteInstance(); Exif::Database::deleteInstance(); } void MainWindow::Window::delayedInit() { QElapsedTimer timer; timer.start(); SplashScreen *splash = SplashScreen::instance(); setupPluginMenu(); qCInfo(TimingLog) << "MainWindow: setupPluginMenu: " << timer.restart() << "ms."; if (Settings::SettingsData::instance()->searchForImagesOnStart() || Options::the()->searchForImagesOnStart()) { splash->message(i18n("Searching for New Files")); qApp->processEvents(); DB::ImageDB::instance()->slotRescan(); qCInfo(TimingLog) << "MainWindow: Search for New Files: " << timer.restart() << "ms."; } if (!Settings::SettingsData::instance()->delayLoadingPlugins()) { splash->message(i18n("Loading Plug-ins")); loadKipiPlugins(); qCInfo(TimingLog) << "MainWindow: Loading Plug-ins: " << timer.restart() << "ms."; } splash->done(); show(); updateDateBar(); qCInfo(TimingLog) << "MainWindow: MainWindow.show():" << timer.restart() << "ms."; QUrl importUrl = Options::the()->importFile(); if (importUrl.isValid()) { // I need to do this in delayed init to get the import window on top of the normal window ImportExport::Import::imageImport(importUrl); qCInfo(TimingLog) << "MainWindow: imageImport:" << timer.restart() << "ms."; } else { // I need to postpone this otherwise the tip dialog will not get focus on start up KTipDialog::showTip(this); } Exif::Database::instance(); // Load the database qCInfo(TimingLog) << "MainWindow: Loading Exif DB:" << timer.restart() << "ms."; #ifdef KPA_ENABLE_REMOTECONTROL if (!Options::the()->listen().isNull()) RemoteControl::RemoteInterface::instance().listen(Options::the()->listen()); else if (Settings::SettingsData::instance()->listenForAndroidDevicesOnStartup()) RemoteControl::RemoteInterface::instance().listen(); #endif } bool MainWindow::Window::slotExit() { if (Options::the()->demoMode()) { QString txt = i18n("

Delete Your Temporary Demo Database

" "

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

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

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

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

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

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

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

" "

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

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

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

" "

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

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

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

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

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

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

"), i18n("Internal Error")); m_hasLoadedKipiPlugins = true; return; // This is no good, but lets try and continue. } #ifdef KF5Purpose_FOUND Plugins::PurposeMenu *purposeMenu = new Plugins::PurposeMenu(menu); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, purposeMenu, &Plugins::PurposeMenu::slotSelectionChanged); connect(purposeMenu, &Plugins::PurposeMenu::imageShared, [this](QUrl shareLocation) { QString message; if (shareLocation.isValid()) { message = i18n("Successfully shared image(s). Copying location to clipboard..."); QGuiApplication::clipboard()->setText(shareLocation.toString()); } else { message = i18n("Successfully shared image(s)."); } m_statusBar->showMessage(message); }); connect(purposeMenu, &Plugins::PurposeMenu::imageSharingFailed, [this](QString errorMessage) { QString message = i18n("Image sharing failed with message: %1", errorMessage); m_statusBar->showMessage(message); }); #endif #ifdef HASKIPI connect(menu, &QMenu::aboutToShow, this, &Window::loadKipiPlugins); m_hasLoadedKipiPlugins = false; #else setPluginMenuState("kipiplugins", {}); #ifndef KF5Purpose_FOUND menu->setEnabled(false); #endif m_hasLoadedKipiPlugins = true; #endif } void MainWindow::Window::loadKipiPlugins() { #ifdef HASKIPI Utilities::ShowBusyCursor dummy; if (m_hasLoadedKipiPlugins) return; m_pluginInterface = new Plugins::Interface(this, QString::fromLatin1("KPhotoAlbum kipi interface")); connect(m_pluginInterface, &Plugins::Interface::imagesChanged, this, &Window::slotImagesChanged); QStringList ignores; ignores << QString::fromLatin1("CommentsEditor") << QString::fromLatin1("HelloWorld"); m_pluginLoader = new KIPI::PluginLoader(); m_pluginLoader->setIgnoredPluginsList(ignores); m_pluginLoader->setInterface(m_pluginInterface); m_pluginLoader->init(); connect(m_pluginLoader, &KIPI::PluginLoader::replug, this, &Window::plug); m_pluginLoader->loadPlugins(); // Setup signals connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, this, &Window::slotSelectionChanged); m_hasLoadedKipiPlugins = true; // Make sure selection is updated also when plugin loading is // delayed. This is needed, because selection might already be // non-empty when loading the plugins. slotSelectionChanged(selected().size()); #endif // HASKIPI } void MainWindow::Window::plug() { #ifdef HASKIPI unplugActionList(QString::fromLatin1("kipi_actions")); QList kipiActions; KIPI::PluginLoader::PluginList list = m_pluginLoader->pluginList(); - Q_FOREACH (const KIPI::PluginLoader::Info *pluginInfo, list) { + for (const KIPI::PluginLoader::Info *pluginInfo : list) { KIPI::Plugin *plugin = pluginInfo->plugin(); if (!plugin || !pluginInfo->shouldLoad()) continue; plugin->setup(this); QList actions = plugin->actions(); - Q_FOREACH (QAction *action, actions) { + for (QAction *action : actions) { kipiActions.append(action); } KConfigGroup group = KSharedConfig::openConfig()->group(QString::fromLatin1("Shortcuts")); plugin->actionCollection()->importGlobalShortcuts(&group); } setPluginMenuState("kipiplugins", kipiActions); // For this to work I need to pass false as second arg for createGUI plugActionList(QString::fromLatin1("kipi_actions"), kipiActions); #endif } void MainWindow::Window::setPluginMenuState(const char *name, const QList &actions) { QMenu *menu = findChild(QString::fromLatin1(name)); if (menu) menu->setEnabled(actions.count() != 0); } void MainWindow::Window::slotImagesChanged(const QList &urls) { for (QList::ConstIterator it = urls.begin(); it != urls.end(); ++it) { DB::FileName fileName = DB::FileName::fromAbsolutePath((*it).path()); if (!fileName.isNull()) { // Plugins may report images outsite of the photodatabase // This seems to be the case with the border image plugin, which reports the destination image ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); // update MD5sum: MD5 md5sum = MD5Sum(fileName); fileName.info()->setMD5Sum(md5sum); } } m_statusBar->mp_dirtyIndicator->markDirty(); reloadThumbnails(ThumbnailView::MaintainSelection); } DB::ImageSearchInfo MainWindow::Window::currentContext() { return m_browser->currentContext(); } QString MainWindow::Window::currentBrowseCategory() const { return m_browser->currentCategory(); } void MainWindow::Window::slotSelectionChanged(int count) { #ifdef HASKIPI m_pluginInterface->slotSelectionChanged(count != 0); #else Q_UNUSED(count) #endif } void MainWindow::Window::resizeEvent(QResizeEvent *) { if (Settings::SettingsData::ready() && isVisible()) Settings::SettingsData::instance()->setWindowGeometry(Settings::MainWindow, geometry()); } void MainWindow::Window::moveEvent(QMoveEvent *) { if (Settings::SettingsData::ready() && isVisible()) Settings::SettingsData::instance()->setWindowGeometry(Settings::MainWindow, geometry()); } void MainWindow::Window::slotRemoveTokens() { if (!m_tokenEditor) m_tokenEditor = new TokenEditor(this); m_tokenEditor->show(); connect(m_tokenEditor, &TokenEditor::finished, m_browser, &Browser::BrowserWidget::go); } void MainWindow::Window::slotShowListOfFiles() { QStringList list = QInputDialog::getMultiLineText(this, i18n("Open List of Files"), i18n("You can open a set of files from KPhotoAlbum's image root by listing the files here.")) .split(QChar::fromLatin1('\n'), QString::SkipEmptyParts); if (list.isEmpty()) return; DB::FileNameList out; for (QStringList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) { QString fileNameStr = Utilities::imageFileNameToAbsolute(*it); if (fileNameStr.isNull()) continue; const DB::FileName fileName = DB::FileName::fromAbsolutePath(fileNameStr); if (!fileName.isNull()) out.append(fileName); } if (out.isEmpty()) KMessageBox::sorry(this, i18n("No images matching your input were found."), i18n("No Matches")); else showThumbNails(out); } void MainWindow::Window::updateDateBar(const Browser::BreadcrumbList &path) { static QString lastPath = QString::fromLatin1("ThisStringShouldNeverBeSeenSoWeUseItAsInitialContent"); if (path.toString() != lastPath) updateDateBar(); lastPath = path.toString(); } void MainWindow::Window::updateDateBar() { m_dateBar->setImageDateCollection(DB::ImageDB::instance()->rangeCollection()); } void MainWindow::Window::slotShowImagesWithInvalidDate() { QPointer finder = new InvalidDateFinder(this); if (finder->exec() == QDialog::Accepted) showThumbNails(); delete finder; } void MainWindow::Window::showDateBarTip(const QString &msg) { m_statusBar->showMessage(msg, 3000); } void MainWindow::Window::slotJumpToContext() { const DB::FileName fileName = m_thumbnailView->currentItem(); if (!fileName.isNull()) { m_browser->addImageView(fileName); } } void MainWindow::Window::setDateRange(const DB::ImageDate &range) { DB::ImageDB::instance()->setDateRange(range, m_dateBar->includeFuzzyCounts()); m_statusBar->mp_partial->showBrowserMatches(this->selected().size()); m_browser->reload(); reloadThumbnails(ThumbnailView::MaintainSelection); } void MainWindow::Window::clearDateRange() { DB::ImageDB::instance()->clearDateRange(); m_browser->reload(); reloadThumbnails(ThumbnailView::MaintainSelection); } void MainWindow::Window::showThumbNails(const DB::FileNameList &items) { m_thumbnailView->setImageList(items); m_statusBar->mp_partial->setMatchCount(items.size()); showThumbNails(); } void MainWindow::Window::slotRecalcCheckSums() { DB::ImageDB::instance()->slotRecalcCheckSums(selected()); } void MainWindow::Window::slotShowExifInfo() { DB::FileNameList items = selectedOnDisk(); if (!items.isEmpty()) { Exif::InfoDialog *exifDialog = new Exif::InfoDialog(items.at(0), this); exifDialog->show(); } } void MainWindow::Window::showFeatures() { FeatureDialog dialog(this); dialog.exec(); } void MainWindow::Window::showImage(const DB::FileName &fileName) { launchViewer(DB::FileNameList() << fileName, true, false, false); } void MainWindow::Window::slotBuildThumbnails() { ImageManager::ThumbnailBuilder::instance()->buildAll(ImageManager::StartNow); } void MainWindow::Window::slotBuildThumbnailsIfWanted() { ImageManager::ThumbnailCache::instance()->flush(); if (!Settings::SettingsData::instance()->incrementalThumbnails()) ImageManager::ThumbnailBuilder::instance()->buildAll(ImageManager::StartDelayed); } void MainWindow::Window::slotOrderIncr() { m_thumbnailView->setSortDirection(ThumbnailView::OldestFirst); } void MainWindow::Window::slotOrderDecr() { m_thumbnailView->setSortDirection(ThumbnailView::NewestFirst); } void MainWindow::Window::showVideos() { QDesktopServices::openUrl(QUrl( QStringLiteral("https://www.kphotoalbum.org/documentation/videos/"))); } void MainWindow::Window::slotStatistics() { static StatisticsDialog *dialog = new StatisticsDialog(this); dialog->show(); } void MainWindow::Window::slotMarkUntagged() { if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()) { for (const DB::FileName &newFile : selected()) { newFile.info()->addCategoryInfo(Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag()); } DirtyIndicator::markDirty(); } else { // Note: the same dialog text is used in // Browser::OverviewPage::activateUntaggedImagesAction(), // so if it is changed, be sure to also change it there! KMessageBox::information(this, i18n("

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

" "

Please follow these steps to do so:" "

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

"), i18n("Feature has not been configured")); } } void MainWindow::Window::setupStatusBar() { m_statusBar = new MainWindow::StatusBar; setStatusBar(m_statusBar); setLocked(Settings::SettingsData::instance()->locked(), true, false); connect(m_statusBar, &StatusBar::thumbnailSettingsRequested, [this]() { this->slotOptions(); m_settingsDialog->activatePage(Settings::SettingsPage::ThumbnailsPage); }); } void MainWindow::Window::slotRecreateExifDB() { Exif::Database::instance()->recreate(); } void MainWindow::Window::useNextVideoThumbnail() { UpdateVideoThumbnail::useNext(selected()); } void MainWindow::Window::usePreviousVideoThumbnail() { UpdateVideoThumbnail::usePrevious(selected()); } void MainWindow::Window::mergeDuplicates() { DuplicateMerger *merger = new DuplicateMerger; merger->show(); } void MainWindow::Window::slotThumbnailSizeChanged() { QString thumbnailSizeMsg = i18nc("@info:status", //xgettext:no-c-format "Thumbnail width: %1px (storage size: %2px)", Settings::SettingsData::instance()->actualThumbnailSize(), Settings::SettingsData::instance()->thumbnailSize()); m_statusBar->showMessage(thumbnailSizeMsg, 4000); } void MainWindow::Window::createSearchBar() { // Set up the search tool bar SearchBar *searchBar = new SearchBar(this); searchBar->setLineEditEnabled(false); searchBar->setObjectName(QString::fromUtf8("searchBar")); connect(searchBar, &SearchBar::textChanged, m_browser, &Browser::BrowserWidget::slotLimitToMatch); connect(searchBar, &SearchBar::returnPressed, m_browser, &Browser::BrowserWidget::slotInvokeSeleted); connect(searchBar, &SearchBar::keyPressed, m_browser, &Browser::BrowserWidget::scrollKeyPressed); connect(m_browser, &Browser::BrowserWidget::viewChanged, searchBar, &SearchBar::reset); connect(m_browser, &Browser::BrowserWidget::isSearchable, searchBar, &SearchBar::setLineEditEnabled); QAction *toggleSearchBar = actionCollection()->action(QString::fromLatin1("toggleSearchBar")); Q_ASSERT(toggleSearchBar); connect(toggleSearchBar, &QAction::triggered, searchBar, &SearchBar::setVisible); connect(searchBar, &SearchBar::visibilityChanged, toggleSearchBar, &QAction::setChecked); auto filterWidget = m_thumbnailView->filterWidget(); addToolBar(filterWidget); filterWidget->setObjectName(QString::fromUtf8("filterBar")); connect(m_browser, &Browser::BrowserWidget::viewChanged, ThumbnailView::ThumbnailFacade::instance(), &ThumbnailView::ThumbnailFacade::clearFilter); connect(m_browser, &Browser::BrowserWidget::isFilterable, filterWidget, &ThumbnailView::FilterWidget::setEnabled); QAction *toggleFilterToolbar = actionCollection()->action(QString::fromLatin1("toggleFilterToolbar")); Q_ASSERT(toggleFilterToolbar); connect(toggleFilterToolbar, &QAction::triggered, filterWidget, &ThumbnailView::FilterWidget::setVisible); connect(filterWidget, &ThumbnailView::FilterWidget::visibilityChanged, toggleFilterToolbar, &QAction::setChecked); } void MainWindow::Window::executeStartupActions() { new ImageManager::ThumbnailBuilder(m_statusBar, this); if (!Settings::SettingsData::instance()->incrementalThumbnails()) ImageManager::ThumbnailBuilder::instance()->buildMissing(); connect(Settings::SettingsData::instance(), &Settings::SettingsData::thumbnailSizeChanged, this, &Window::slotBuildThumbnailsIfWanted); if (!FeatureDialog::hasVideoThumbnailer()) { BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::SearchForVideosWithoutLengthInfo); BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::SearchForVideosWithoutVideoThumbnailsJob); } } void MainWindow::Window::checkIfVideoThumbnailerIsInstalled() { if (Options::the()->demoMode()) return; if (!FeatureDialog::hasVideoThumbnailer()) { KMessageBox::information(this, i18n("

Unable to find ffmpeg on the system.

" "

Without it, KPhotoAlbum will not be able to display video thumbnails and video lengths. " "Please install the ffmpeg package

"), i18n("Video thumbnails are not available"), QString::fromLatin1("VideoThumbnailerNotInstalled")); } } bool MainWindow::Window::anyVideosSelected() const { - Q_FOREACH (const DB::FileName &fileName, selected()) { + for (const DB::FileName &fileName : selected()) { if (Utilities::isVideo(fileName)) return true; } return false; } void MainWindow::Window::setHistogramVisibilty(bool visible) const { if (visible) { m_dateBar->show(); m_dateBarLine->show(); } else { m_dateBar->hide(); m_dateBarLine->hide(); } } void MainWindow::Window::slotImageRotated(const DB::FileName &fileName) { // An image has been rotated by the annotation dialog or the viewer. // We have to reload the respective thumbnail to get it in the right angle ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } bool MainWindow::Window::dbIsDirty() const { return m_statusBar->mp_dirtyIndicator->isSaveDirty(); } #ifdef HAVE_KGEOMAP void MainWindow::Window::showPositionBrowser() { Browser::PositionBrowserWidget *positionBrowser = positionBrowserWidget(); m_stack->setCurrentWidget(positionBrowser); updateStates(false); } Browser::PositionBrowserWidget *MainWindow::Window::positionBrowserWidget() { if (m_positionBrowser == 0) { m_positionBrowser = createPositionBrowser(); } return m_positionBrowser; } Browser::PositionBrowserWidget *MainWindow::Window::createPositionBrowser() { Browser::PositionBrowserWidget *widget = new Browser::PositionBrowserWidget(m_stack); m_stack->addWidget(widget); return widget; } #endif UserFeedback MainWindow::Window::askWarningContinueCancel(const QString &msg, const QString &title, const QString &dialogId) { auto answer = KMessageBox::warningContinueCancel(this, msg, title, KStandardGuiItem::cont(), KStandardGuiItem::cancel(), dialogId); return (answer == KMessageBox::Continue) ? UserFeedback::Confirm : UserFeedback::Deny; } UserFeedback MainWindow::Window::askQuestionYesNo(const QString &msg, const QString &title, const QString &dialogId) { auto answer = KMessageBox::questionYesNo(this, msg, title, KStandardGuiItem::yes(), KStandardGuiItem::no(), dialogId); return (answer == KMessageBox::Yes) ? UserFeedback::Confirm : UserFeedback::Deny; } void MainWindow::Window::showInformation(const QString &msg, const QString &title, const QString &dialogId) { KMessageBox::information(this, msg, title, dialogId); } void MainWindow::Window::showSorry(const QString &msg, const QString &title, const QString &) { KMessageBox::sorry(this, msg, title); } void MainWindow::Window::showError(const QString &msg, const QString &title, const QString &) { KMessageBox::error(this, msg, title); } bool MainWindow::Window::isDialogDisabled(const QString &dialogId) { // Note(jzarl): there are different methods for different kinds of dialogs. // However, all these methods share exactly the same code in KMessageBox. // If that ever changes, we can still update our implementation - until then I won't just copy a stupid API... return !KMessageBox::shouldBeShownContinue(dialogId); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/ImageInfo.cpp b/Plugins/ImageInfo.cpp index dd42bf35..b5e137cd 100644 --- a/Plugins/ImageInfo.cpp +++ b/Plugins/ImageInfo.cpp @@ -1,367 +1,368 @@ -/* 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 "ImageInfo.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #define KEXIV_ORIENTATION_UNSPECIFIED 0 #define KEXIV_ORIENTATION_NORMAL 1 #define KEXIV_ORIENTATION_HFLIP 2 #define KEXIV_ORIENTATION_ROT_180 3 #define KEXIV_ORIENTATION_VFLIP 4 #define KEXIV_ORIENTATION_ROT_90_HFLIP 5 #define KEXIV_ORIENTATION_ROT_90 6 #define KEXIV_ORIENTATION_ROT_90_VFLIP 7 #define KEXIV_ORIENTATION_ROT_270 8 /** * Convert a rotation in degrees to a KExiv2::ImageOrientation value. */ static int deg2KexivOrientation(int deg) { deg = (deg + 360) % 360; ; switch (deg) { case 0: return KEXIV_ORIENTATION_NORMAL; case 90: return KEXIV_ORIENTATION_ROT_90; case 180: return KEXIV_ORIENTATION_ROT_180; case 270: return KEXIV_ORIENTATION_ROT_270; default: qCWarning(PluginsLog) << "Rotation of " << deg << "degrees can't be mapped to KExiv2::ImageOrientation value."; return KEXIV_ORIENTATION_UNSPECIFIED; } } /** * Convert a KExiv2::ImageOrientation value into a degrees angle. */ static int kexivOrientation2deg(int orient) { switch (orient) { case KEXIV_ORIENTATION_NORMAL: return 0; case KEXIV_ORIENTATION_ROT_90: return 90; case KEXIV_ORIENTATION_ROT_180: return 280; case KEXIV_ORIENTATION_ROT_270: return 270; default: qCWarning(PluginsLog) << "KExiv2::ImageOrientation value " << orient << " not a pure rotation. Discarding orientation info."; return 0; } } Plugins::ImageInfo::ImageInfo(KIPI::Interface *interface, const QUrl &url) : KIPI::ImageInfoShared(interface, url) { m_info = DB::ImageDB::instance()->info(DB::FileName::fromAbsolutePath(_url.path())); } QMap Plugins::ImageInfo::attributes() { if (m_info == nullptr) { // This can happen if we're trying to access an image that // has been deleted on-disc, but not yet the database return QMap(); } Q_ASSERT(m_info); QMap res; res.insert(QString::fromLatin1("name"), QFileInfo(m_info->fileName().absolute()).baseName()); res.insert(QString::fromLatin1("comment"), m_info->description()); res.insert(QLatin1String("date"), m_info->date().start()); res.insert(QLatin1String("dateto"), m_info->date().end()); res.insert(QLatin1String("isexactdate"), m_info->date().start() == m_info->date().end()); res.insert(QString::fromLatin1("orientation"), deg2KexivOrientation(m_info->angle())); res.insert(QString::fromLatin1("angle"), deg2KexivOrientation(m_info->angle())); // for compatibility with older versions. Now called orientation. res.insert(QString::fromLatin1("title"), m_info->label()); res.insert(QString::fromLatin1("rating"), m_info->rating()); // not supported: //res.insert(QString::fromLatin1("colorlabel"), xxx ); //res.insert(QString::fromLatin1("picklabel"), xxx ); #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates position = m_info->coordinates(); if (position.hasCoordinates()) { res.insert(QString::fromLatin1("longitude"), QVariant(position.lon())); res.insert(QString::fromLatin1("latitude"), QVariant(position.lat())); if (position.hasAltitude()) res.insert(QString::fromLatin1("altitude"), QVariant(position.alt())); } #endif // Flickr plug-in expects the item tags, so we better give them. QString text; QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); QStringList tags; QStringList tagspath; const QLatin1String sep("/"); - Q_FOREACH (const DB::CategoryPtr category, categories) { + for (const DB::CategoryPtr category : categories) { QString categoryName = category->name(); if (category->isSpecialCategory()) continue; // I don't know why any categories except the above should be excluded //if ( category->doShow() ) { Utilities::StringSet items = m_info->itemsOfCategory(categoryName); - Q_FOREACH (const QString &tag, items) { + for (const QString &tag : items) { tags.append(tag); // digikam compatible tag path: // note: this produces a semi-flattened hierarchy. // instead of "Places/France/Paris" this will yield "Places/Paris" tagspath.append(categoryName + sep + tag); } //} } res.insert(QString::fromLatin1("tagspath"), tagspath); res.insert(QString::fromLatin1("keywords"), tags); res.insert(QString::fromLatin1("tags"), tags); // for compatibility with older versions. Now called keywords. // TODO: implement this: //res.insert(QString::fromLatin1( "filesize" ), xxx ); // not supported: //res.insert(QString::fromLatin1( "creators" ), xxx ); //res.insert(QString::fromLatin1( "credit" ), xxx ); //res.insert(QString::fromLatin1( "rights" ), xxx ); //res.insert(QString::fromLatin1( "source" ), xxx ); return res; } void Plugins::ImageInfo::clearAttributes() { if (m_info) { // official behaviour is to delete all officially supported attributes: QStringList attr; attr.append(QString::fromLatin1("comment")); attr.append(QString::fromLatin1("date")); attr.append(QString::fromLatin1("title")); attr.append(QString::fromLatin1("orientation")); attr.append(QString::fromLatin1("tagspath")); attr.append(QString::fromLatin1("rating")); attr.append(QString::fromLatin1("colorlabel")); attr.append(QString::fromLatin1("picklabel")); attr.append(QString::fromLatin1("gpslocation")); attr.append(QString::fromLatin1("copyrights")); delAttributes(attr); } } void Plugins::ImageInfo::addAttributes(const QMap &amap) { if (m_info && !amap.empty()) { QMap map = amap; if (map.contains(QLatin1String("name"))) { // plugin renamed the item // TODO: implement this qCWarning(PluginsLog, "File renaming by kipi-plugin not supported."); //map.remove(QLatin1String("name")); } if (map.contains(QLatin1String("comment"))) { // is it save to do that? digikam seems to allow multiple comments on a single image // if a plugin assumes that it is adding a comment, not setting it, things might go badly... m_info->setDescription(map[QLatin1String("comment")].toString()); map.remove(QLatin1String("comment")); } // note: this probably won't work as expected because according to the spec, // "isexactdate" is supposed to be readonly and therefore never set here: if (map.contains(QLatin1String("isexactdate")) && map.contains(QLatin1String("date"))) { m_info->setDate(DB::ImageDate(map[QLatin1String("date")].toDateTime())); map.remove(QLatin1String("date")); } else if (map.contains(QLatin1String("date")) && map.contains(QLatin1String("dateto"))) { m_info->setDate(DB::ImageDate(map[QLatin1String("date")].toDateTime(), map[QLatin1String("dateto")].toDateTime())); map.remove(QLatin1String("date")); map.remove(QLatin1String("dateto")); } else if (map.contains(QLatin1String("date"))) { m_info->setDate(DB::ImageDate(map[QLatin1String("date")].toDateTime())); map.remove(QLatin1String("date")); } if (map.contains(QLatin1String("angle"))) { qCWarning(PluginsLog, "Kipi-plugin uses deprecated attribute \"angle\"."); m_info->setAngle(kexivOrientation2deg(map[QLatin1String("angle")].toInt())); map.remove(QLatin1String("angle")); } if (map.contains(QLatin1String("orientation"))) { m_info->setAngle(kexivOrientation2deg(map[QLatin1String("orientation")].toInt())); map.remove(QLatin1String("orientation")); } if (map.contains(QLatin1String("title"))) { m_info->setLabel(map[QLatin1String("title")].toString()); map.remove(QLatin1String("title")); } if (map.contains(QLatin1String("rating"))) { m_info->setRating(map[QLatin1String("rating")].toInt()); map.remove(QLatin1String("rating")); } if (map.contains(QLatin1String("tagspath"))) { const QStringList tagspaths = map[QLatin1String("tagspath")].toStringList(); const DB::CategoryCollection *categories = DB::ImageDB::instance()->categoryCollection(); DB::MemberMap &memberMap = DB::ImageDB::instance()->memberMap(); - Q_FOREACH (const QString &path, tagspaths) { + for (const QString &path : tagspaths) { qCDebug(PluginsLog) << "Adding tags: " << path; QStringList tagpath = path.split(QLatin1String("/"), QString::SkipEmptyParts); // Note: maybe tagspaths with only one component or with unknown first component // should be added to the "keywords"/"Events" category? if (tagpath.size() < 2) { qCWarning(PluginsLog) << "Ignoring incompatible tag: " << path; continue; } // first component is the category, const QString categoryName = tagpath.takeFirst(); DB::CategoryPtr cat = categories->categoryForName(categoryName); if (cat) { QString previousTag; // last component is the tag: // others define hierarchy: - Q_FOREACH (const QString ¤tTag, tagpath) { + for (const QString ¤tTag : tagpath) { if (!cat->items().contains(currentTag)) { qCDebug(PluginsLog) << "Adding tag " << currentTag << " to category " << categoryName; // before we can use a tag, we have to add it cat->addItem(currentTag); } if (!previousTag.isNull()) { if (!memberMap.isGroup(categoryName, previousTag)) { // create a group for the parent tag, so we can add a sub-category memberMap.addGroup(categoryName, previousTag); } if (memberMap.canAddMemberToGroup(categoryName, previousTag, currentTag)) { // make currentTag a member of the previousTag group memberMap.addMemberToGroup(categoryName, previousTag, currentTag); } else { qCWarning(PluginsLog) << "Cannot make " << currentTag << " a subcategory of " << categoryName << "/" << previousTag << "!"; } } previousTag = currentTag; } qCDebug(PluginsLog) << "Adding tag " << previousTag << " in category " << categoryName << " to image " << m_info->label(); // previousTag must be a valid category (see addItem() above...) m_info->addCategoryInfo(categoryName, previousTag); } else { qCWarning(PluginsLog) << "Unknown category: " << categoryName; } } map.remove(QLatin1String("tagspath")); } // remove read-only keywords: map.remove(QLatin1String("filesize")); map.remove(QLatin1String("isexactdate")); map.remove(QLatin1String("keywords")); map.remove(QLatin1String("tags")); map.remove(QLatin1String("altitude")); map.remove(QLatin1String("longitude")); map.remove(QLatin1String("latitude")); // colorlabel // picklabel // creators // credit // rights // source MainWindow::DirtyIndicator::markDirty(); if (!map.isEmpty()) { qCWarning(PluginsLog) << "The following attributes are not (yet) supported by the KPhotoAlbum KIPI interface:" << map; } } } void Plugins::ImageInfo::delAttributes(const QStringList &attrs) { if (m_info && !attrs.empty()) { QStringList delAttrs = attrs; if (delAttrs.contains(QLatin1String("comment"))) { m_info->setDescription(QString()); delAttrs.removeAll(QLatin1String("comment")); } // not supported: date if (delAttrs.contains(QLatin1String("orientation")) || delAttrs.contains(QLatin1String("angle"))) { m_info->setAngle(0); delAttrs.removeAll(QLatin1String("orientation")); delAttrs.removeAll(QLatin1String("angle")); } if (delAttrs.contains(QLatin1String("rating"))) { m_info->setRating(-1); delAttrs.removeAll(QLatin1String("rating")); } if (delAttrs.contains(QLatin1String("title"))) { m_info->setLabel(QString()); delAttrs.removeAll(QLatin1String("title")); } // TODO: // (colorlabel) // (picklabel) // copyrights // not supported: gpslocation if (delAttrs.contains(QLatin1String("tags")) || delAttrs.contains(QLatin1String("tagspath"))) { m_info->clearAllCategoryInfo(); delAttrs.removeAll(QLatin1String("tags")); delAttrs.removeAll(QLatin1String("tagspath")); } MainWindow::DirtyIndicator::markDirty(); if (!delAttrs.isEmpty()) { qCWarning(PluginsLog) << "The following attributes are not (yet) supported by the KPhotoAlbum KIPI interface:" << delAttrs; } } } void Plugins::ImageInfo::cloneData(ImageInfoShared *const other) { ImageInfoShared::cloneData(other); if (m_info) { Plugins::ImageInfo *inf = static_cast(other); m_info->setDate(inf->m_info->date()); MainWindow::DirtyIndicator::markDirty(); } } bool Plugins::ImageInfo::isPositionAttribute(const QString &key) { return (key == QString::fromLatin1("longitude") || key == QString::fromLatin1("latitude") || key == QString::fromLatin1("altitude") || key == QString::fromLatin1("positionPrecision")); } bool Plugins::ImageInfo::isCategoryAttribute(const QString &key) { return (key != QString::fromLatin1("tags") && !isPositionAttribute(key)); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/CategoryPage.cpp b/Settings/CategoryPage.cpp index 76fd4d19..d86598fd 100644 --- a/Settings/CategoryPage.cpp +++ b/Settings/CategoryPage.cpp @@ -1,536 +1,537 @@ /* 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 "CategoryPage.h" // Qt includes #include #include #include #include #include #include #include #include #include #include // KDE includes #include #include #include // Local includes #include "CategoryItem.h" #include "SettingsDialog.h" #include "UntaggedGroupBox.h" #include #include #include #include #include Settings::CategoryPage::CategoryPage(QWidget *parent) : QWidget(parent) { QVBoxLayout *mainLayout = new QVBoxLayout(this); // The category settings QGroupBox *categoryGroupBox = new QGroupBox; mainLayout->addWidget(categoryGroupBox); categoryGroupBox->setTitle(i18n("Category Settings")); QHBoxLayout *categoryLayout = new QHBoxLayout(categoryGroupBox); // Category list QVBoxLayout *categorySideLayout = new QVBoxLayout; categoryLayout->addLayout(categorySideLayout); m_categoriesListWidget = new QListWidget; connect(m_categoriesListWidget, &QListWidget::itemClicked, this, &CategoryPage::editCategory); connect(m_categoriesListWidget, &QListWidget::itemSelectionChanged, this, &CategoryPage::editSelectedCategory); connect(m_categoriesListWidget, &QListWidget::itemChanged, this, &CategoryPage::categoryNameChanged); categorySideLayout->addWidget(m_categoriesListWidget); // New, Delete, and buttons QHBoxLayout *newDeleteRenameLayout = new QHBoxLayout; categorySideLayout->addLayout(newDeleteRenameLayout); m_newCategoryButton = new QPushButton(i18n("New")); connect(m_newCategoryButton, &QPushButton::clicked, this, &CategoryPage::newCategory); newDeleteRenameLayout->addWidget(m_newCategoryButton); m_delItem = new QPushButton(i18n("Delete")); connect(m_delItem, &QPushButton::clicked, this, &CategoryPage::deleteCurrentCategory); newDeleteRenameLayout->addWidget(m_delItem); m_renameItem = new QPushButton(i18n("Rename")); connect(m_renameItem, &QPushButton::clicked, this, &CategoryPage::renameCurrentCategory); newDeleteRenameLayout->addWidget(m_renameItem); // Category settings QVBoxLayout *rightSideLayout = new QVBoxLayout; categoryLayout->addLayout(rightSideLayout); // Header m_categoryLabel = new QLabel; m_categoryLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); rightSideLayout->addWidget(m_categoryLabel); // Pending rename label m_renameLabel = new QLabel; m_renameLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); rightSideLayout->addWidget(m_renameLabel); QDialog *parentDialog = qobject_cast(parent); connect(parentDialog, &QDialog::rejected, m_renameLabel, &QLabel::clear); // Some space looks better here :-) QLabel *spacer = new QLabel; rightSideLayout->addWidget(spacer); // Here we start with the actual settings QGridLayout *settingsLayout = new QGridLayout; rightSideLayout->addLayout(settingsLayout); int row = 0; // Positionable m_positionableLabel = new QLabel(i18n("Positionable tags:")); settingsLayout->addWidget(m_positionableLabel, row, 0); m_positionable = new QCheckBox(i18n("Tags in this category can be associated with areas within images")); settingsLayout->addWidget(m_positionable, row, 1); connect(m_positionable, &QCheckBox::clicked, this, &CategoryPage::positionableChanged); row++; // Icon m_iconLabel = new QLabel(i18n("Icon:")); settingsLayout->addWidget(m_iconLabel, row, 0); m_icon = new KIconButton; settingsLayout->addWidget(m_icon, row, 1); m_icon->setIconSize(32); m_icon->setIcon(QString::fromUtf8("personsIcon")); connect(m_icon, &KIconButton::iconChanged, this, &CategoryPage::iconChanged); row++; // Thumbnail size m_thumbnailSizeInCategoryLabel = new QLabel(i18n("Thumbnail size:")); settingsLayout->addWidget(m_thumbnailSizeInCategoryLabel, row, 0); m_thumbnailSizeInCategory = new QSpinBox; m_thumbnailSizeInCategory->setRange(32, 512); m_thumbnailSizeInCategory->setSingleStep(32); settingsLayout->addWidget(m_thumbnailSizeInCategory, row, 1); connect(m_thumbnailSizeInCategory, static_cast(&QSpinBox::valueChanged), this, &CategoryPage::thumbnailSizeChanged); row++; // Preferred View m_preferredViewLabel = new QLabel(i18n("Preferred view:")); settingsLayout->addWidget(m_preferredViewLabel, row, 0); m_preferredView = new QComboBox; settingsLayout->addWidget(m_preferredView, row, 1); m_preferredView->addItems(QStringList() << i18n("List View") << i18n("List View with Custom Thumbnails") << i18n("Icon View") << i18n("Icon View with Custom Thumbnails")); connect(m_preferredView, static_cast(&QComboBox::activated), this, &CategoryPage::preferredViewChanged); rightSideLayout->addStretch(); // Info about the database not being saved QHBoxLayout *dbNotSavedLayout = new QHBoxLayout; mainLayout->addLayout(dbNotSavedLayout); m_dbNotSavedLabel = new QLabel(i18n("" "The database has unsaved changes. As long as those are " "not saved,
the names of categories can't be changed " "and new ones can't be added." "
")); m_dbNotSavedLabel->setWordWrap(true); dbNotSavedLayout->addWidget(m_dbNotSavedLabel); m_saveDbNowButton = new QPushButton(i18n("Save the DB now")); m_saveDbNowButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum); connect(m_saveDbNowButton, &QPushButton::clicked, this, &CategoryPage::saveDbNow); dbNotSavedLayout->addWidget(m_saveDbNowButton); resetInterface(); // Untagged images m_untaggedBox = new UntaggedGroupBox; mainLayout->addWidget(m_untaggedBox); m_currentCategory = nullptr; m_categoryNamesChanged = false; } void Settings::CategoryPage::resetInterface() { enableDisable(false); if (m_categoriesListWidget->currentItem()) m_categoriesListWidget->currentItem()->setSelected(false); resetCategoryLabel(); m_renameLabel->hide(); } void Settings::CategoryPage::editSelectedCategory() { editCategory(m_categoriesListWidget->currentItem()); } void Settings::CategoryPage::editCategory(QListWidgetItem *i) { if (i == nullptr) { return; } m_categoryNameBeforeEdit = i->text(); Settings::CategoryItem *item = static_cast(i); m_currentCategory = item; m_categoryLabel->setText(i18n("Settings for category %1", item->originalName())); if (m_currentCategory->originalName() != m_categoryNameBeforeEdit) { m_renameLabel->setText(i18n("Pending change: rename to \"%1\"", m_categoryNameBeforeEdit)); m_renameLabel->show(); } else { m_renameLabel->clear(); m_renameLabel->hide(); } m_positionable->setChecked(item->positionable()); m_icon->setIcon(item->icon()); m_thumbnailSizeInCategory->setValue(item->thumbnailSize()); m_preferredView->setCurrentIndex(static_cast(item->viewType())); enableDisable(true); if (item->originalName() == DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name()) { m_delItem->setEnabled(false); m_positionableLabel->setEnabled(false); m_positionable->setEnabled(false); m_thumbnailSizeInCategoryLabel->setEnabled(false); m_thumbnailSizeInCategory->setEnabled(false); m_preferredViewLabel->setEnabled(false); m_preferredView->setEnabled(false); } } void Settings::CategoryPage::categoryNameChanged(QListWidgetItem *item) { QString newCategoryName = item->text().simplified(); m_categoriesListWidget->blockSignals(true); item->setText(QString()); m_categoriesListWidget->blockSignals(false); // Now let's check if the new name is valid :-) // If it's empty, we're done here. The new name can't be empty. if (newCategoryName.isEmpty()) { resetCategory(item); return; } // We don't want to have special category names. // We do have to search both for the localized version and the C locale version, because a user // could start KPA e. g. with a German locale and create a "Folder" category (which would not // be caught by i18n("Folder")), and then start KPA with the C locale, which would produce a // doubled "Folder" category. if (newCategoryName == i18n("Folder") || newCategoryName == QString::fromUtf8("Folder") || newCategoryName == i18n("Media Type") || newCategoryName == QString::fromUtf8("Media Type")) { resetCategory(item); KMessageBox::sorry(this, i18n("

Can't change the name of category \"%1\" to \"%2\":

" "

\"%2\" is a special category name which is reserved and can't " "be used for a normal category.

", m_currentCategory->text(), newCategoryName), i18n("Invalid category name")); return; } // Let's see if we already have a category with this name. if (m_categoriesListWidget->findItems(newCategoryName, Qt::MatchExactly).size() > 0) { resetCategory(item); KMessageBox::sorry(this, i18n("

Can't change the name of category \"%1\" to \"%2\":

" "

A category with this name already exists.

", m_currentCategory->text(), newCategoryName), i18n("Invalid category name")); return; } // Let's see if we have any pending name changes that would cause collisions. for (int i = 0; i < m_categoriesListWidget->count(); i++) { Settings::CategoryItem *cat = static_cast(m_categoriesListWidget->item(i)); if (cat == m_currentCategory) { continue; } if (newCategoryName == cat->originalName()) { resetCategory(item); KMessageBox::sorry(this, i18n("

Can't change the name of category \"%1\" to \"%2\":

" "

There's a pending rename action on the category \"%2\". " "Please save this change first.

", m_currentCategory->text(), newCategoryName), i18n("Unsaved pending renaming action")); return; } } m_categoriesListWidget->blockSignals(true); item->setText(newCategoryName); m_categoriesListWidget->blockSignals(false); emit categoryChangesPending(); m_untaggedBox->categoryRenamed(m_categoryNameBeforeEdit, newCategoryName); m_currentCategory->setLabel(newCategoryName); editCategory(m_currentCategory); m_categoryNamesChanged = true; } void Settings::CategoryPage::resetCategory(QListWidgetItem *item) { m_categoriesListWidget->blockSignals(true); item->setText(m_categoryNameBeforeEdit); m_categoriesListWidget->blockSignals(false); } void Settings::CategoryPage::positionableChanged(bool positionable) { if (!m_currentCategory) { return; } if (!positionable) { int answer = KMessageBox::questionYesNo(this, i18n("

Do you really want to make \"%1\" " "non-positionable?

" "

All areas linked against this category " "will be deleted!

", m_currentCategory->text())); if (answer == KMessageBox::No) { m_positionable->setCheckState(Qt::Checked); return; } } m_currentCategory->setPositionable(positionable); } void Settings::CategoryPage::iconChanged(const QString &icon) { if (m_currentCategory) { m_currentCategory->setIcon(icon); } } void Settings::CategoryPage::thumbnailSizeChanged(int size) { if (m_currentCategory) { m_currentCategory->setThumbnailSize(size); } } void Settings::CategoryPage::preferredViewChanged(int i) { if (m_currentCategory) { m_currentCategory->setViewType(static_cast(i)); } } void Settings::CategoryPage::newCategory() { // Here starts the real function QString newCategory = i18n("New category"); QString checkedCategory = newCategory; int i = 1; while (m_categoriesListWidget->findItems(checkedCategory, Qt::MatchExactly).size() > 0) { i++; checkedCategory = QString::fromUtf8("%1 %2").arg(newCategory).arg(i); } m_categoriesListWidget->blockSignals(true); m_currentCategory = new Settings::CategoryItem(checkedCategory, QString(), DB::Category::TreeView, 64, m_categoriesListWidget); m_currentCategory->markAsNewCategory(); emit categoryChangesPending(); m_currentCategory->setLabel(checkedCategory); m_currentCategory->setSelected(true); m_categoriesListWidget->blockSignals(false); m_positionable->setChecked(false); m_icon->setIcon(QIcon()); m_thumbnailSizeInCategory->setValue(64); enableDisable(true); editCategory(m_currentCategory); m_categoriesListWidget->editItem(m_currentCategory); } void Settings::CategoryPage::deleteCurrentCategory() { int answer = KMessageBox::questionYesNo(this, i18n("

Really delete category \"%1\"?

", m_currentCategory->text())); if (answer == KMessageBox::No) { return; } m_untaggedBox->categoryDeleted(m_currentCategory->text()); m_deletedCategories.append(m_currentCategory); m_categoriesListWidget->takeItem(m_categoriesListWidget->row(m_currentCategory)); m_currentCategory = nullptr; m_positionable->setChecked(false); m_icon->setIcon(QIcon()); m_thumbnailSizeInCategory->setValue(64); enableDisable(false); resetCategoryLabel(); editCategory(m_categoriesListWidget->currentItem()); emit categoryChangesPending(); } void Settings::CategoryPage::renameCurrentCategory() { m_categoriesListWidget->editItem(m_currentCategory); } void Settings::CategoryPage::enableDisable(bool b) { m_delItem->setEnabled(b); m_positionableLabel->setEnabled(b); m_positionable->setEnabled(b); m_icon->setEnabled(b); m_iconLabel->setEnabled(b); m_thumbnailSizeInCategoryLabel->setEnabled(b); m_thumbnailSizeInCategory->setEnabled(b); m_preferredViewLabel->setEnabled(b); m_preferredView->setEnabled(b); m_categoriesListWidget->blockSignals(true); if (MainWindow::Window::theMainWindow()->dbIsDirty()) { m_dbNotSavedLabel->show(); m_saveDbNowButton->show(); m_renameItem->setEnabled(false); m_newCategoryButton->setEnabled(false); for (int i = 0; i < m_categoriesListWidget->count(); i++) { QListWidgetItem *currentItem = m_categoriesListWidget->item(i); currentItem->setFlags(currentItem->flags() & ~Qt::ItemIsEditable); } } else { m_dbNotSavedLabel->hide(); m_saveDbNowButton->hide(); m_renameItem->setEnabled(b); m_newCategoryButton->setEnabled(true); for (int i = 0; i < m_categoriesListWidget->count(); i++) { QListWidgetItem *currentItem = m_categoriesListWidget->item(i); currentItem->setFlags(currentItem->flags() | Qt::ItemIsEditable); } } m_categoriesListWidget->blockSignals(false); } void Settings::CategoryPage::saveSettings(Settings::SettingsData *opt, DB::MemberMap *memberMap) { // Delete items - Q_FOREACH (CategoryItem *item, m_deletedCategories) { + for (CategoryItem *item : m_deletedCategories) { item->removeFromDatabase(); } // Created or Modified items for (int i = 0; i < m_categoriesListWidget->count(); ++i) { CategoryItem *item = static_cast(m_categoriesListWidget->item(i)); item->submit(memberMap); } DB::ImageDB::instance()->memberMap() = *memberMap; m_untaggedBox->saveSettings(opt); if (m_categoryNamesChanged) { // Probably, one or more category names have been edited. Save the database so that // all thumbnails are referenced with the correct name. MainWindow::Window::theMainWindow()->slotSave(); m_categoryNamesChanged = false; } } void Settings::CategoryPage::loadSettings(Settings::SettingsData *opt) { m_categoriesListWidget->blockSignals(true); m_categoriesListWidget->clear(); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); - Q_FOREACH (const DB::CategoryPtr category, categories) { + for (const DB::CategoryPtr category : categories) { if (category->type() == DB::Category::PlainCategory || category->type() == DB::Category::TokensCategory) { Settings::CategoryItem *item = new CategoryItem(category->name(), category->iconName(), category->viewType(), category->thumbnailSize(), m_categoriesListWidget, category->positionable()); Q_UNUSED(item); } } m_categoriesListWidget->blockSignals(false); m_untaggedBox->loadSettings(opt); } void Settings::CategoryPage::resetCategoryLabel() { m_categoryLabel->setText(i18n("Choose a category to edit it")); } void Settings::CategoryPage::saveDbNow() { MainWindow::Window::theMainWindow()->slotSave(); resetInterface(); enableDisable(false); } void Settings::CategoryPage::resetCategoryNamesChanged() { m_categoryNamesChanged = false; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/GeneralPage.cpp b/Settings/GeneralPage.cpp index bcc01fa5..b39cfea9 100644 --- a/Settings/GeneralPage.cpp +++ b/Settings/GeneralPage.cpp @@ -1,326 +1,328 @@ -/* 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. + 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 "GeneralPage.h" #include "SettingsData.h" #include #include #include #include #include #include #include #include #include #include #include #include #include Settings::GeneralPage::GeneralPage(QWidget *parent) : QWidget(parent) { QVBoxLayout *lay1 = new QVBoxLayout(this); QGroupBox *box = new QGroupBox(i18n("Loading New Images"), this); lay1->addWidget(box); QGridLayout *lay = new QGridLayout(box); lay->setSpacing(6); int row = 0; // Thrust time stamps QLabel *timeStampLabel = new QLabel(i18n("Trust image dates:"), box); m_trustTimeStamps = new KComboBox(box); m_trustTimeStamps->addItems(QStringList() << i18nc("As in 'always trust image dates'", "Always") << i18nc("As in 'ask whether to trust image dates'", "Ask") << i18nc("As in 'never trust image dates'", "Never")); timeStampLabel->setBuddy(m_trustTimeStamps); lay->addWidget(timeStampLabel, row, 0); lay->addWidget(m_trustTimeStamps, row, 1, 1, 3); // Do Exif rotate row++; m_useEXIFRotate = new QCheckBox(i18n("Use Exif orientation information"), box); lay->addWidget(m_useEXIFRotate, row, 0, 1, 4); // Use Exif description row++; m_useEXIFComments = new QCheckBox(i18n("Use Exif description"), box); lay->addWidget(m_useEXIFComments, row, 0, 1, 4); connect(m_useEXIFComments, &QCheckBox::stateChanged, this, &GeneralPage::useEXIFCommentsChanged); m_stripEXIFComments = new QCheckBox(i18n("Strip out camera generated default descriptions"), box); connect(m_stripEXIFComments, &QCheckBox::stateChanged, this, &GeneralPage::stripEXIFCommentsChanged); lay->addWidget(m_stripEXIFComments, row, 1, 1, 4); row++; m_commentsToStrip = new QTextEdit(); m_commentsToStrip->setMaximumHeight(60); m_commentsToStrip->setEnabled(false); lay->addWidget(m_commentsToStrip, row, 1, 1, 4); // Use embedded thumbnail row++; m_useRawThumbnail = new QCheckBox(i18n("Use the embedded thumbnail in RAW file or halfsized RAW"), box); lay->addWidget(m_useRawThumbnail, row, 0); row++; QLabel *label = new QLabel(i18n("Required size for the thumbnail:"), box); m_useRawThumbnailWidth = new QSpinBox(box); m_useRawThumbnailWidth->setRange(100, 5000); m_useRawThumbnailWidth->setSingleStep(64); lay->addWidget(label, row, 0); lay->addWidget(m_useRawThumbnailWidth, row, 1); label = new QLabel(QString::fromLatin1("x"), box); m_useRawThumbnailHeight = new QSpinBox(box); m_useRawThumbnailHeight->setRange(100, 5000); m_useRawThumbnailHeight->setSingleStep(64); lay->addWidget(label, row, 2); lay->addWidget(m_useRawThumbnailHeight, row, 3); box = new QGroupBox(i18n("Histogram"), this); lay1->addWidget(box); lay = new QGridLayout(box); lay->setSpacing(6); row = 0; m_showHistogram = new QCheckBox(i18n("Show histogram"), box); lay->addWidget(m_showHistogram, row, 0); row++; connect(m_showHistogram, &QCheckBox::stateChanged, this, &GeneralPage::showHistogramChanged); m_histogramUseLinearScale = new QCheckBox(i18n("Use linear scale for histogram")); lay->addWidget(m_histogramUseLinearScale, row, 0); row++; label = new QLabel(i18n("Size of histogram columns in date bar:"), box); m_barWidth = new QSpinBox; m_barWidth->setRange(1, 100); m_barWidth->setSingleStep(1); lay->addWidget(label, row, 0); lay->addWidget(m_barWidth, row, 1); label = new QLabel(QString::fromLatin1("x"), box); m_barHeight = new QSpinBox; m_barHeight->setRange(15, 100); lay->addWidget(label, row, 2); lay->addWidget(m_barHeight, row, 3); box = new QGroupBox(i18n("Miscellaneous"), this); lay1->addWidget(box); lay = new QGridLayout(box); lay->setSpacing(6); row = 0; // Show splash screen m_showSplashScreen = new QCheckBox(i18n("Show splash screen"), box); lay->addWidget(m_showSplashScreen, row, 0); // Album Category row++; QLabel *albumCategoryLabel = new QLabel(i18n("Category for virtual albums:"), box); m_albumCategory = new QComboBox; lay->addWidget(albumCategoryLabel, row, 0); lay->addWidget(m_albumCategory, row, 1); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); - Q_FOREACH (const DB::CategoryPtr category, categories) { + for (const DB::CategoryPtr category : categories) { m_albumCategory->addItem(category->name()); } #ifdef KPA_ENABLE_REMOTECONTROL m_listenForAndroidDevicesOnStartup = new QCheckBox(i18n("Listen for Android devices on startup")); lay->addWidget(m_listenForAndroidDevicesOnStartup); #endif lay1->addStretch(1); // Whats This QString txt; txt = i18n("

KPhotoAlbum will try to read the image date from Exif information in the image. " "If that fails it will try to get the date from the file's time stamp.

" "

However, this information will be wrong if the image was scanned in (you want the date the image " "was taken, not the date of the scan).

" "

If you only scan images, in contrast to sometimes using " "a digital camera, you should reply no. If you never scan images, you should reply yes, " "otherwise reply ask. This will allow you to decide whether the images are from " "the scanner or the camera, from session to session.

"); timeStampLabel->setWhatsThis(txt); m_trustTimeStamps->setWhatsThis(txt); txt = i18n("

JPEG images may contain information about rotation. " "If you have a reason for not using this information to get a default rotation of " "your images, uncheck this check box.

" "

Note: Your digital camera may not write this information into the images at all.

"); m_useEXIFRotate->setWhatsThis(txt); txt = i18n("

JPEG images may contain a description. " "Check this checkbox to specify if you want to use this as a " "default description for your images.

"); m_useEXIFComments->setWhatsThis(txt); txt = i18n("

KPhotoAlbum shares plugins with other imaging applications, some of which have the concept of albums. " "KPhotoAlbum does not have this concept; nevertheless, for certain plugins to function, KPhotoAlbum behaves " "to the plugin system as if it did.

" "

KPhotoAlbum does this by defining the current album to be the current view - that is, all the images the " "browser offers to display.

" "

In addition to the current album, KPhotoAlbum must also be able to give a list of all albums; " "the list of all albums is defined in the following way:" "

  • When KPhotoAlbum's browser displays the content of a category, say all People, then each item in this category " "will look like an album to the plugin.
  • " "
  • Otherwise, the category you specify using this option will be used; e.g. if you specify People " "with this option, then KPhotoAlbum will act as if you had just chosen to display people and then invoke " "the plugin which needs to know about all albums.

" "

Most users would probably want to specify Events here.

"); albumCategoryLabel->setWhatsThis(txt); m_albumCategory->setWhatsThis(txt); txt = i18n("Show the KPhotoAlbum splash screen on start up"); m_showSplashScreen->setWhatsThis(txt); #ifdef KPA_ENABLE_REMOTECONTROL txt = i18n("

KPhotoAlbum is capable of showing your images on android devices. KPhotoAlbum will automatically pair with the app from " "android. This, however, requires that KPhotoAlbum on your desktop is listening for multicast messages. " "Checking this checkbox will make KPhotoAlbum do so automatically on start up. " "Alternatively, you can click the connection icon in the status bar to start listening."); m_listenForAndroidDevicesOnStartup->setWhatsThis(txt); #endif txt = i18n("

Some cameras automatically store generic comments in each image. " "These comments can be ignored automatically.

" "

Enter the comments that you want to ignore in the input field, one per line. " "Be sure to add the exact comment, including all whitespace.

"); m_stripEXIFComments->setWhatsThis(txt); m_commentsToStrip->setWhatsThis(txt); } void Settings::GeneralPage::loadSettings(Settings::SettingsData *opt) { m_trustTimeStamps->setCurrentIndex(opt->tTimeStamps()); m_useEXIFRotate->setChecked(opt->useEXIFRotate()); m_useEXIFComments->setChecked(opt->useEXIFComments()); m_stripEXIFComments->setChecked(opt->stripEXIFComments()); m_stripEXIFComments->setEnabled(opt->useEXIFComments()); QStringList commentsToStrip = opt->EXIFCommentsToStrip(); QString commentsToStripStr; for (int i = 0; i < commentsToStrip.size(); ++i) { if (commentsToStripStr.size() > 0) { commentsToStripStr += QString::fromLatin1("\n"); } commentsToStripStr += commentsToStrip.at(i); } m_commentsToStrip->setPlainText(commentsToStripStr); m_commentsToStrip->setEnabled(opt->stripEXIFComments()); m_useRawThumbnail->setChecked(opt->useRawThumbnail()); setUseRawThumbnailSize(QSize(opt->useRawThumbnailSize().width(), opt->useRawThumbnailSize().height())); m_barWidth->setValue(opt->histogramSize().width()); m_barHeight->setValue(opt->histogramSize().height()); m_showHistogram->setChecked(opt->showHistogram()); m_histogramUseLinearScale->setChecked(opt->histogramUseLinearScale()); m_showSplashScreen->setChecked(opt->showSplashScreen()); #ifdef KPA_ENABLE_REMOTECONTROL m_listenForAndroidDevicesOnStartup->setChecked(opt->listenForAndroidDevicesOnStartup()); #endif DB::CategoryPtr cat = DB::ImageDB::instance()->categoryCollection()->categoryForName(opt->albumCategory()); if (!cat) cat = DB::ImageDB::instance()->categoryCollection()->categories()[0]; m_albumCategory->setEditText(cat->name()); } void Settings::GeneralPage::saveSettings(Settings::SettingsData *opt) { opt->setTTimeStamps((TimeStampTrust)m_trustTimeStamps->currentIndex()); opt->setUseEXIFRotate(m_useEXIFRotate->isChecked()); opt->setUseEXIFComments(m_useEXIFComments->isChecked()); opt->setStripEXIFComments(m_stripEXIFComments->isChecked()); QStringList commentsToStrip = m_commentsToStrip->toPlainText().split(QString::fromLatin1("\n")); // Put the processable list to opt opt->setEXIFCommentsToStrip(commentsToStrip); QString commentsToStripString; for (QString comment : commentsToStrip) { // separate comments with "-,-" and escape existing commas by doubling if (!comment.isEmpty()) commentsToStripString += comment.replace(QString::fromLatin1(","), QString::fromLatin1(",,")) + QString::fromLatin1("-,-"); } // Put the storable list to opt opt->setCommentsToStrip(commentsToStripString); opt->setUseRawThumbnail(m_useRawThumbnail->isChecked()); opt->setUseRawThumbnailSize(QSize(useRawThumbnailSize())); opt->setShowHistogram(m_showHistogram->isChecked()); opt->setHistogramUseLinearScale(m_histogramUseLinearScale->isChecked()); opt->setShowSplashScreen(m_showSplashScreen->isChecked()); #ifdef KPA_ENABLE_REMOTECONTROL opt->setListenForAndroidDevicesOnStartup(m_listenForAndroidDevicesOnStartup->isChecked()); #endif QString name = m_albumCategory->currentText(); if (name.isNull()) { name = DB::ImageDB::instance()->categoryCollection()->categoryNames()[0]; } opt->setAlbumCategory(name); opt->setHistogramSize(QSize(m_barWidth->value(), m_barHeight->value())); } void Settings::GeneralPage::setUseRawThumbnailSize(const QSize &size) { m_useRawThumbnailWidth->setValue(size.width()); m_useRawThumbnailHeight->setValue(size.height()); } QSize Settings::GeneralPage::useRawThumbnailSize() { return QSize(m_useRawThumbnailWidth->value(), m_useRawThumbnailHeight->value()); } void Settings::GeneralPage::showHistogramChanged(int state) const { const bool checked = state == Qt::Checked; m_histogramUseLinearScale->setChecked(checked); m_barHeight->setEnabled(checked); m_barWidth->setEnabled(checked); MainWindow::Window::theMainWindow()->setHistogramVisibilty(checked); } void Settings::GeneralPage::useEXIFCommentsChanged(int state) { m_stripEXIFComments->setEnabled(state); m_commentsToStrip->setEnabled(state && m_stripEXIFComments->isChecked()); } void Settings::GeneralPage::stripEXIFCommentsChanged(int state) { m_commentsToStrip->setEnabled(state); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/TagGroupsPage.cpp b/Settings/TagGroupsPage.cpp index 9a22d203..48bacba3 100644 --- a/Settings/TagGroupsPage.cpp +++ b/Settings/TagGroupsPage.cpp @@ -1,797 +1,798 @@ /* 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 "TagGroupsPage.h" // Qt includes #include #include #include #include #include #include #include #include #include #include #include // KDE includes #include #include // Local includes #include "CategoriesGroupsWidget.h" #include "SettingsData.h" #include #include Settings::TagGroupsPage::TagGroupsPage(QWidget *parent) : QWidget(parent) { QGridLayout *layout = new QGridLayout(this); // The category and group tree layout->addWidget(new QLabel(i18nc("@label", "Categories and groups:")), 0, 0); m_categoryTreeWidget = new CategoriesGroupsWidget(this); m_categoryTreeWidget->header()->hide(); m_categoryTreeWidget->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_categoryTreeWidget, 1, 0); connect(m_categoryTreeWidget, &CategoriesGroupsWidget::customContextMenuRequested, this, &TagGroupsPage::showTreeContextMenu); connect(m_categoryTreeWidget, &CategoriesGroupsWidget::itemClicked, this, &TagGroupsPage::slotGroupSelected); // The member list m_selectGroupToAddTags = i18nc("@label/rich", "Select a group on the left side to add tags to it"); m_tagsInGroupLabel = new QLabel(m_selectGroupToAddTags); layout->addWidget(m_tagsInGroupLabel, 0, 1); m_membersListWidget = new QListWidget; m_membersListWidget->setEnabled(false); m_membersListWidget->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_membersListWidget, 1, 1); connect(m_membersListWidget, &QListWidget::itemChanged, this, &TagGroupsPage::checkItemSelection); connect(m_membersListWidget, &QListWidget::customContextMenuRequested, this, &TagGroupsPage::showMembersContextMenu); // The "pending rename actions" label m_pendingChangesLabel = new QLabel(i18nc("@label/rich", "There are pending changes on the categories page. " "Please save the changes before working on tag groups.")); m_pendingChangesLabel->hide(); layout->addWidget(m_pendingChangesLabel, 2, 0, 1, 2); QDialog *parentDialog = qobject_cast(parent); connect(parentDialog, &QDialog::rejected, this, &TagGroupsPage::discardChanges); // Context menu actions m_newGroupAction = new QAction(i18nc("@action:inmenu", "Add group ..."), this); connect(m_newGroupAction, &QAction::triggered, this, &TagGroupsPage::slotAddGroup); m_renameAction = new QAction(this); connect(m_renameAction, &QAction::triggered, this, &TagGroupsPage::slotRenameGroup); m_deleteAction = new QAction(this); connect(m_deleteAction, &QAction::triggered, this, &TagGroupsPage::slotDeleteGroup); m_deleteMemberAction = new QAction(this); connect(m_deleteMemberAction, &QAction::triggered, this, &TagGroupsPage::slotDeleteMember); m_renameMemberAction = new QAction(this); connect(m_renameMemberAction, &QAction::triggered, this, &TagGroupsPage::slotRenameMember); m_memberMap = DB::ImageDB::instance()->memberMap(); connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::itemRemoved, &m_memberMap, &DB::MemberMap::deleteItem); connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::itemRenamed, &m_memberMap, &DB::MemberMap::renameItem); connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::categoryRemoved, &m_memberMap, &DB::MemberMap::deleteCategory); m_dataChanged = false; } void Settings::TagGroupsPage::updateCategoryTree() { // Store all expanded items so that they can be expanded after reload QList> expandedItems = QList>(); for (QTreeWidgetItemIterator it { m_categoryTreeWidget }; *it; ++it) { if ((*it)->isExpanded()) { QString parentName; if ((*it)->parent() != nullptr) { parentName = (*it)->parent()->text(0); } expandedItems.append(QPair((*it)->text(0), parentName)); } } m_categoryTreeWidget->clear(); // Create a tree view of all groups and their sub-groups QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); - Q_FOREACH (const DB::CategoryPtr category, categories) { + for (const DB::CategoryPtr category : categories) { if (category->isSpecialCategory()) { continue; } // Add the real categories as top-level items QTreeWidgetItem *topLevelItem = new QTreeWidgetItem; topLevelItem->setText(0, category->name()); topLevelItem->setFlags(topLevelItem->flags() & Qt::ItemIsEnabled); QFont font = topLevelItem->font(0); font.setWeight(QFont::Bold); topLevelItem->setFont(0, font); m_categoryTreeWidget->addTopLevelItem(topLevelItem); // Build a map with all members for each group QMap membersForGroup; const QStringList allGroups = m_memberMap.groups(category->name()); foreach (const QString &group, allGroups) { // FIXME: Why does the member map return an empty category?! if (group.isEmpty()) { continue; } QStringList allMembers = m_memberMap.members(category->name(), group, true); membersForGroup[group] = allMembers; } // Add all groups (their sub-groups will be added recursively) addSubCategories(topLevelItem, membersForGroup, allGroups); } // Order the items alphabetically m_categoryTreeWidget->sortItems(0, Qt::AscendingOrder); // Re-expand all previously expanded items for (QTreeWidgetItemIterator it { m_categoryTreeWidget }; *it; ++it) { QString parentName; if ((*it)->parent() != nullptr) { parentName = (*it)->parent()->text(0); } if (expandedItems.contains(QPair((*it)->text(0), parentName))) { (*it)->setExpanded(true); } } } void Settings::TagGroupsPage::addSubCategories(QTreeWidgetItem *superCategory, const QMap &membersForGroup, const QStringList &allGroups) { // Process all group members for (auto memIt = membersForGroup.constBegin(); memIt != membersForGroup.constEnd(); ++memIt) { const QString &group = memIt.key(); bool isSubGroup = false; // Search for a membership in another group for the current group for (const QStringList &members : membersForGroup) { if (members.contains(group)) { isSubGroup = true; break; } } // Add the group if it's not member of another group if (!isSubGroup) { QTreeWidgetItem *groupItem = new QTreeWidgetItem; groupItem->setText(0, group); superCategory->addChild(groupItem); // Search the member list for other groups QMap subGroups; foreach (const QString &groupName, allGroups) { if (membersForGroup[group].contains(groupName)) { subGroups[groupName] = membersForGroup[groupName]; } } // If the list contains other groups, add them recursively if (subGroups.count() > 0) { addSubCategories(groupItem, subGroups, allGroups); } } } } QString Settings::TagGroupsPage::getCategory(QTreeWidgetItem *currentItem) { while (currentItem->parent() != nullptr) { currentItem = currentItem->parent(); } return currentItem->text(0); } void Settings::TagGroupsPage::showTreeContextMenu(QPoint point) { QTreeWidgetItem *currentItem = m_categoryTreeWidget->currentItem(); if (currentItem == nullptr) { return; } m_currentSubCategory = currentItem->text(0); if (currentItem->parent() == nullptr) { // It's a top-level, "real" category m_currentSuperCategory.clear(); } else { // It's a normal sub-category that belongs to another one m_currentSuperCategory = currentItem->parent()->text(0); } m_currentCategory = getCategory(currentItem); QMenu *menu = new QMenu; menu->addAction(m_newGroupAction); // "Real" top-level categories have to processed on the category page. if (!m_currentSuperCategory.isEmpty()) { menu->addSeparator(); m_renameAction->setText(i18nc("@action:inmenu", "Rename group \"%1\"", m_currentSubCategory)); menu->addAction(m_renameAction); m_deleteAction->setText(i18nc("@action:inmenu", "Delete group \"%1\"", m_currentSubCategory)); menu->addAction(m_deleteAction); } menu->exec(m_categoryTreeWidget->mapToGlobal(point)); delete menu; } void Settings::TagGroupsPage::categoryChanged(const QString &name) { if (name.isEmpty()) { return; } m_membersListWidget->blockSignals(true); m_membersListWidget->clear(); QStringList list = getCategoryObject(name)->items(); list += m_memberMap.groups(name); QStringList alreadyAdded; - Q_FOREACH (const QString &member, list) { + for (const QString &member : list) { if (member.isEmpty()) { // This can happen if we add group that currently has no members. continue; } if (!alreadyAdded.contains(member)) { alreadyAdded << member; if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && !Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (name == Settings::SettingsData::instance()->untaggedCategory()) { if (member == Settings::SettingsData::instance()->untaggedTag()) { continue; } } } QListWidgetItem *newItem = new QListWidgetItem(member, m_membersListWidget); newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); newItem->setCheckState(Qt::Unchecked); } } m_currentGroup.clear(); m_membersListWidget->clearSelection(); m_membersListWidget->sortItems(); m_membersListWidget->setEnabled(false); m_membersListWidget->blockSignals(false); } void Settings::TagGroupsPage::slotGroupSelected(QTreeWidgetItem *item) { // When something else than a "real" category has been selected before, // we have to save it's members. if (!m_currentGroup.isEmpty()) { saveOldGroup(); } if (item->parent() == nullptr) { // A "real" category has been selected, not a group m_currentCategory.clear(); m_currentGroup.clear(); m_membersListWidget->setEnabled(false); categoryChanged(item->text(0)); m_tagsInGroupLabel->setText(m_selectGroupToAddTags); return; } // Let's see if the category changed QString itemCategory = getCategory(item); if (m_currentCategory != itemCategory) { m_currentCategory = itemCategory; categoryChanged(m_currentCategory); } m_currentGroup = item->text(0); selectMembers(m_currentGroup); m_tagsInGroupLabel->setText(i18nc("@label", "Tags in group \"%1\" of category \"%2\"", m_currentGroup, m_currentCategory)); } void Settings::TagGroupsPage::slotAddGroup() { bool ok; DB::CategoryPtr category = getCategoryObject(m_currentCategory); QStringList groups = m_memberMap.groups(m_currentCategory); QStringList tags; for (QString tag : category->items()) { if (!groups.contains(tag)) { tags << tag; } } //// reject existing group names: //KStringListValidator validator(groups); //QString newSubCategory = KInputDialog::getText(i18nc("@title:window","New Group"), // i18nc("@label:textbox","Group name:"), // QString() /*value*/, // &ok, // this /*parent*/, // &validator, // QString() /*mask*/, // QString() /*WhatsThis*/, // tags /*completion*/ // ); // FIXME: KF5-port: QInputDialog does not accept a validator, // and KInputDialog was removed in KF5. -> Reimplement input validation using other stuff QString newSubCategory = QInputDialog::getText(this, i18nc("@title:window", "New Group"), i18nc("@label:textbox", "Group name:"), QLineEdit::Normal, QString(), &ok); if (groups.contains(newSubCategory)) return; // only a workaround until GUI-support for validation is restored if (!ok) { return; } // Let's see if we already have this group if (groups.contains(newSubCategory)) { // (with the validator working correctly, we should not get to this point) KMessageBox::sorry(this, i18nc("@info", "

The group \"%1\" already exists.

", newSubCategory), i18nc("@title:window", "Cannot add group")); return; } // Add the group as a new tag to the respective category MainWindow::DirtyIndicator::suppressMarkDirty(true); category->addItem(newSubCategory); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Add] = newSubCategory; m_categoryChanges.append(categoryChange); // Add the group m_memberMap.addGroup(m_currentCategory, newSubCategory); // Display the new group categoryChanged(m_currentCategory); // Display the new item QTreeWidgetItem *parentItem = m_categoryTreeWidget->currentItem(); addNewSubItem(newSubCategory, parentItem); // Check if we also have to update some other group (in case this is not a top-level group) if (!m_currentSuperCategory.isEmpty()) { m_memberMap.addMemberToGroup(m_currentCategory, parentItem->text(0), newSubCategory); slotGroupSelected(parentItem); } m_dataChanged = true; } void Settings::TagGroupsPage::addNewSubItem(QString &name, QTreeWidgetItem *parentItem) { QTreeWidgetItem *newItem = new QTreeWidgetItem; newItem->setText(0, name); parentItem->addChild(newItem); if (!parentItem->isExpanded()) { parentItem->setExpanded(true); } } QTreeWidgetItem *Settings::TagGroupsPage::findCategoryItem(QString category) { QTreeWidgetItem *categoryItem = nullptr; for (int i = 0; i < m_categoryTreeWidget->topLevelItemCount(); ++i) { categoryItem = m_categoryTreeWidget->topLevelItem(i); if (categoryItem->text(0) == category) { break; } } return categoryItem; } void Settings::TagGroupsPage::checkItemSelection(QListWidgetItem *) { m_dataChanged = true; saveOldGroup(); updateCategoryTree(); } void Settings::TagGroupsPage::slotRenameGroup() { bool ok; DB::CategoryPtr category = getCategoryObject(m_currentCategory); QStringList groups = m_memberMap.groups(m_currentCategory); QStringList tags; for (QString tag : category->items()) { if (!groups.contains(tag)) { tags << tag; } } // FIXME: reject existing group names QString newSubCategoryName = QInputDialog::getText(this, i18nc("@title:window", "Rename Group"), i18nc("@label:textbox", "New group name:"), QLineEdit::Normal, m_currentSubCategory, &ok); if (!ok || m_currentSubCategory == newSubCategoryName) { return; } if (groups.contains(newSubCategoryName)) { // (with the validator working correctly, we should not get to this point) KMessageBox::sorry(this, xi18nc("@info", "Cannot rename group \"%1\" to \"%2\": " "\"%2\" already exists in category \"%3\"", m_currentSubCategory, newSubCategoryName, m_currentCategory), i18nc("@title:window", "Rename Group")); return; } QTreeWidgetItem *selectedGroup = m_categoryTreeWidget->currentItem(); saveOldGroup(); // Update the group m_memberMap.renameGroup(m_currentCategory, m_currentSubCategory, newSubCategoryName); // Update the tag in the respective category MainWindow::DirtyIndicator::suppressMarkDirty(true); category->renameItem(m_currentSubCategory, newSubCategoryName); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Rename] = m_currentSubCategory; categoryChange[CategoryEdit::NewName] = newSubCategoryName; m_categoryChanges.append(categoryChange); m_dataChanged = true; // Search for all possible sub-category items in this category that have to be renamed QTreeWidgetItem *categoryItem = findCategoryItem(m_currentCategory); for (int i = 0; i < categoryItem->childCount(); ++i) { renameAllSubCategories(categoryItem->child(i), m_currentSubCategory, newSubCategoryName); } // Update the displayed items categoryChanged(m_currentCategory); slotGroupSelected(selectedGroup); m_dataChanged = true; } void Settings::TagGroupsPage::renameAllSubCategories(QTreeWidgetItem *categoryItem, QString oldName, QString newName) { // Probably, it item itself has to be renamed if (categoryItem->text(0) == oldName) { categoryItem->setText(0, newName); } // Also check all sub-categories recursively for (int i = 0; i < categoryItem->childCount(); ++i) { renameAllSubCategories(categoryItem->child(i), oldName, newName); } } void Settings::TagGroupsPage::slotDeleteGroup() { QTreeWidgetItem *currentItem = m_categoryTreeWidget->currentItem(); QString message; QString title; if (currentItem->childCount() > 0) { message = xi18nc("@info", "Really delete group \"%1\"?" "Sub-categories of this group will be moved to the super category of \"%1\" (\"%2\"). " "All other memberships of the sub-categories will stay intact.", m_currentSubCategory, m_currentSuperCategory); } else { message = xi18nc("@info", "Really delete group \"%1\"?", m_currentSubCategory); } int res = KMessageBox::warningContinueCancel(this, message, i18nc("@title:window", "Delete Group"), KGuiItem(i18n("&Delete"), QString::fromUtf8("editdelete"))); if (res == KMessageBox::Cancel) { return; } // Delete the group m_memberMap.deleteGroup(m_currentCategory, m_currentSubCategory); // Delete the tag MainWindow::DirtyIndicator::suppressMarkDirty(true); getCategoryObject(m_currentCategory)->removeItem(m_currentSubCategory); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Remove] = m_currentSubCategory; m_categoryChanges.append(categoryChange); m_dataChanged = true; slotPageChange(); m_dataChanged = true; } void Settings::TagGroupsPage::saveOldGroup() { QStringList list; for (int i = 0; i < m_membersListWidget->count(); ++i) { QListWidgetItem *item = m_membersListWidget->item(i); if (item->checkState() == Qt::Checked) { list << item->text(); } } m_memberMap.setMembers(m_currentCategory, m_currentGroup, list); } void Settings::TagGroupsPage::selectMembers(const QString &group) { m_membersListWidget->blockSignals(true); m_membersListWidget->setEnabled(false); m_currentGroup = group; QStringList memberList = m_memberMap.members(m_currentCategory, group, false); for (int i = 0; i < m_membersListWidget->count(); ++i) { QListWidgetItem *item = m_membersListWidget->item(i); item->setCheckState(Qt::Unchecked); if (!m_memberMap.canAddMemberToGroup(m_currentCategory, group, item->text())) { item->setFlags(item->flags() & ~Qt::ItemIsSelectable & ~Qt::ItemIsEnabled); } else { item->setFlags(item->flags() | Qt::ItemIsSelectable | Qt::ItemIsEnabled); if (memberList.contains(item->text())) { item->setCheckState(Qt::Checked); } } } m_membersListWidget->setEnabled(true); m_membersListWidget->blockSignals(false); } void Settings::TagGroupsPage::slotPageChange() { m_tagsInGroupLabel->setText(m_selectGroupToAddTags); m_membersListWidget->setEnabled(false); m_membersListWidget->clear(); m_currentCategory.clear(); updateCategoryTree(); } void Settings::TagGroupsPage::saveSettings() { saveOldGroup(); slotPageChange(); DB::ImageDB::instance()->memberMap() = m_memberMap; m_categoryChanges.clear(); if (m_dataChanged) { m_dataChanged = false; MainWindow::DirtyIndicator::markDirty(); } m_categoryTreeWidget->setEnabled(true); m_membersListWidget->setEnabled(true); m_pendingChangesLabel->hide(); } void Settings::TagGroupsPage::discardChanges() { m_memberMap = DB::ImageDB::instance()->memberMap(); slotPageChange(); m_dataChanged = false; // Revert all changes to the "real" category objects MainWindow::DirtyIndicator::suppressMarkDirty(true); for (int i = m_categoryChanges.size() - 1; i >= 0; i--) { DB::CategoryPtr category = getCategoryObject(m_categoryChanges.at(i)[CategoryEdit::Category]); if (m_categoryChanges.at(i).contains(CategoryEdit::Add)) { // Remove added tags category->removeItem(m_categoryChanges.at(i)[CategoryEdit::Add]); } else if (m_categoryChanges.at(i).contains(CategoryEdit::Remove)) { // Add removed tags category->addItem(m_categoryChanges.at(i)[CategoryEdit::Add]); } else if (m_categoryChanges.at(i).contains(CategoryEdit::Rename)) { // Re-rename tags to their old name category->renameItem(m_categoryChanges.at(i)[CategoryEdit::NewName], m_categoryChanges.at(i)[Rename]); } } MainWindow::DirtyIndicator::suppressMarkDirty(false); m_categoryChanges.clear(); m_categoryTreeWidget->setEnabled(true); m_membersListWidget->setEnabled(true); m_pendingChangesLabel->hide(); } void Settings::TagGroupsPage::loadSettings() { categoryChanged(m_currentCategory); updateCategoryTree(); } void Settings::TagGroupsPage::categoryChangesPending() { m_categoryTreeWidget->setEnabled(false); m_membersListWidget->setEnabled(false); m_pendingChangesLabel->show(); } DB::MemberMap *Settings::TagGroupsPage::memberMap() { return &m_memberMap; } void Settings::TagGroupsPage::processDrop(QTreeWidgetItem *draggedItem, QTreeWidgetItem *targetItem) { if (targetItem->parent() != nullptr) { // Dropped on a group // Select the group m_categoryTreeWidget->setCurrentItem(targetItem); slotGroupSelected(targetItem); // Check the dragged group on the member side to make it a sub-group of the target group m_membersListWidget->findItems(draggedItem->text(0), Qt::MatchExactly)[0]->setCheckState(Qt::Checked); } else { // Dropped on a top-level category // Check if it's already a direct child of the category. // If so, we don't need to do anything. QTreeWidgetItem *parent = draggedItem->parent(); if (parent->parent() == nullptr) { return; } // Select the former super group m_categoryTreeWidget->setCurrentItem(parent); slotGroupSelected(parent); // Deselect the dragged group (this will bring it to the top level) m_membersListWidget->findItems(draggedItem->text(0), Qt::MatchExactly)[0]->setCheckState(Qt::Unchecked); } } void Settings::TagGroupsPage::showMembersContextMenu(QPoint point) { if (m_membersListWidget->currentItem() == nullptr) { return; } QMenu *menu = new QMenu; m_renameMemberAction->setText(i18nc("@action:inmenu", "Rename \"%1\"", m_membersListWidget->currentItem()->text())); menu->addAction(m_renameMemberAction); m_deleteMemberAction->setText(i18nc("@action:inmenu", "Delete \"%1\"", m_membersListWidget->currentItem()->text())); menu->addAction(m_deleteMemberAction); menu->exec(m_membersListWidget->mapToGlobal(point)); delete menu; } void Settings::TagGroupsPage::slotRenameMember() { bool ok; QString newTagName = QInputDialog::getText(this, i18nc("@title:window", "New Tag Name"), i18nc("@label:textbox", "Tag name:"), QLineEdit::Normal, m_membersListWidget->currentItem()->text(), &ok); if (!ok || newTagName == m_membersListWidget->currentItem()->text()) { return; } // Update the tag name in the database MainWindow::DirtyIndicator::suppressMarkDirty(true); getCategoryObject(m_currentCategory)->renameItem(m_membersListWidget->currentItem()->text(), newTagName); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Rename] = m_membersListWidget->currentItem()->text(); categoryChange[CategoryEdit::NewName] = newTagName; m_categoryChanges.append(categoryChange); // Update the displayed tag name m_membersListWidget->currentItem()->setText(newTagName); // Re-order the tags, as their alphabetial order may have changed m_membersListWidget->sortItems(); } void Settings::TagGroupsPage::slotDeleteMember() { QString memberToDelete = m_membersListWidget->currentItem()->text(); if (m_memberMap.groups(m_currentCategory).contains(memberToDelete)) { // The item to delete is a group // Find the tag in the tree view and select it ... QTreeWidgetItemIterator it(m_categoryTreeWidget); while (*it) { if ((*it)->text(0) == memberToDelete && getCategory((*it)) == m_currentCategory) { m_categoryTreeWidget->setCurrentItem((*it)); m_currentSubCategory = (*it)->text(0); m_currentSuperCategory = (*it)->parent()->text(0); break; } ++it; } // ... then delete it like it had been requested by the TreeWidget's context menu slotDeleteGroup(); } else { // The item to delete is a normal tag int res = KMessageBox::warningContinueCancel(this, xi18nc("@info", "Do you really want to delete \"%1\"?" "Deleting the item will remove any information " "about it from any image containing the item.", memberToDelete), i18nc("@title:window", "Really delete %1?", memberToDelete), KGuiItem(i18n("&Delete"), QString::fromUtf8("editdelete"))); if (res != KMessageBox::Continue) { return; } // Delete the tag as if it had been deleted from the annotation dialog. getCategoryObject(m_currentCategory)->removeItem(memberToDelete); slotPageChange(); } } DB::CategoryPtr Settings::TagGroupsPage::getCategoryObject(QString category) const { return DB::ImageDB::instance()->categoryCollection()->categoryForName(category); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/UntaggedGroupBox.cpp b/Settings/UntaggedGroupBox.cpp index f8863741..b264892f 100644 --- a/Settings/UntaggedGroupBox.cpp +++ b/Settings/UntaggedGroupBox.cpp @@ -1,159 +1,161 @@ -/* 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 "UntaggedGroupBox.h" #include "SettingsData.h" #include #include #include #include #include #include #include #include Settings::UntaggedGroupBox::UntaggedGroupBox(QWidget *parent) : QGroupBox(i18n("Untagged Images"), parent) { setWhatsThis(i18n("If a tag is selected here, it will be added to new (untagged) images " "automatically, so that they can be easily found. It will be removed as " "soon as the image has been annotated.")); QGridLayout *grid = new QGridLayout(this); int row = -1; QLabel *label = new QLabel(i18n("Category:")); grid->addWidget(label, ++row, 0); m_category = new QComboBox; grid->addWidget(m_category, row, 1); connect(m_category, static_cast(&QComboBox::currentIndexChanged), this, &UntaggedGroupBox::populateTagsCombo); label = new QLabel(i18n("Tag:")); grid->addWidget(label, ++row, 0); m_tag = new QComboBox; grid->addWidget(m_tag, row, 1); m_tag->setEditable(true); m_showUntaggedImagesTag = new QCheckBox(i18n("Show the untagged images tag as a normal tag")); grid->addWidget(m_showUntaggedImagesTag, ++row, 0, 1, 2); grid->setColumnStretch(1, 1); } void Settings::UntaggedGroupBox::populateCategoryComboBox() { m_category->clear(); m_category->addItem(i18n("None Selected")); - Q_FOREACH (DB::CategoryPtr category, DB::ImageDB::instance()->categoryCollection()->categories()) { + for (DB::CategoryPtr category : DB::ImageDB::instance()->categoryCollection()->categories()) { if (!category->isSpecialCategory()) m_category->addItem(category->name(), category->name()); } } void Settings::UntaggedGroupBox::populateTagsCombo() { m_tag->clear(); const QString currentCategory = m_category->itemData(m_category->currentIndex()).value(); if (currentCategory.isEmpty()) m_tag->setEnabled(false); else { m_tag->setEnabled(true); const QStringList items = DB::ImageDB::instance()->categoryCollection()->categoryForName(currentCategory)->items(); m_tag->addItems(items); } } void Settings::UntaggedGroupBox::loadSettings(Settings::SettingsData *opt) { populateCategoryComboBox(); const QString category = opt->untaggedCategory(); const QString tag = opt->untaggedTag(); int categoryIndex = m_category->findData(category); if (categoryIndex == -1) categoryIndex = 0; m_category->setCurrentIndex(categoryIndex); populateTagsCombo(); if (categoryIndex != 0) { int tagIndex = m_tag->findText(tag); if (tagIndex == -1) { m_tag->addItem(tag); tagIndex = m_tag->findText(tag); Q_ASSERT(tagIndex != -1); } m_tag->setCurrentIndex(tagIndex); } m_showUntaggedImagesTag->setChecked(opt->untaggedImagesTagVisible()); } void Settings::UntaggedGroupBox::saveSettings(Settings::SettingsData *opt) { const QString category = m_category->itemData(m_category->currentIndex()).value(); QString untaggedTag = m_tag->currentText().simplified(); if (!category.isEmpty()) { // Add a new tag if the entered one is not in the DB yet DB::CategoryPtr categoryPointer = DB::ImageDB::instance()->categoryCollection()->categoryForName(category); if (!categoryPointer->items().contains(untaggedTag)) { categoryPointer->addItem(untaggedTag); QMessageBox::information(this, i18n("New tag added"), i18n("

The new tag \"%1\" has been added to the category \"%2\" and will be used " "for untagged images now.

" "

Please save now, so that this tag will be stored in the database. " "Otherwise, it will be lost, and you will get an error about this tag being " "not present on the next start.

", untaggedTag, category)); } opt->setUntaggedCategory(category); opt->setUntaggedTag(untaggedTag); } else { // If no untagged images tag is selected, remove the setting by using an empty string opt->setUntaggedCategory(QString()); opt->setUntaggedTag(QString()); } opt->setUntaggedImagesTagVisible(m_showUntaggedImagesTag->isChecked()); } void Settings::UntaggedGroupBox::categoryDeleted(QString categoryName) { if (categoryName == m_category->itemData(m_category->currentIndex()).value()) { m_category->setCurrentIndex(0); } m_category->removeItem(m_category->findText(categoryName)); } void Settings::UntaggedGroupBox::categoryRenamed(QString oldCategoryName, QString newCategoryName) { const int index = m_category->findText(oldCategoryName); m_category->setItemText(index, newCategoryName); m_category->setItemData(index, newCategoryName); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/CellGeometry.cpp b/ThumbnailView/CellGeometry.cpp index 7ab6c157..8ea0ad4c 100644 --- a/ThumbnailView/CellGeometry.cpp +++ b/ThumbnailView/CellGeometry.cpp @@ -1,153 +1,155 @@ -/* 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 "CellGeometry.h" #include "ThumbnailModel.h" #include "ThumbnailWidget.h" #include #include using Utilities::StringSet; ThumbnailView::CellGeometry::CellGeometry(ThumbnailFactory *factory) : ThumbnailComponent(factory) , m_cacheInitialized(false) { } /** * Return desired size of the pixmap */ QSize ThumbnailView::CellGeometry::preferredIconSize() { int width = Settings::SettingsData::instance()->actualThumbnailSize(); int height = width * Settings::SettingsData::instance()->getThumbnailAspectRatio(); return QSize(width, height); } /** * Return base size of the pixmap. * I.e. the unscaled thumbnail size, as it is set in the settings page. */ QSize ThumbnailView::CellGeometry::baseIconSize() { int width = Settings::SettingsData::instance()->thumbnailSize(); int height = width * Settings::SettingsData::instance()->getThumbnailAspectRatio(); return QSize(width, height); } /** * Return the geometry for the icon in the cell. The coordinates are relative to the cell. */ QRect ThumbnailView::CellGeometry::iconGeometry(const QPixmap &pixmap) const { const QSize cellSize = preferredIconSize(); const int space = Settings::SettingsData::instance()->thumbnailSpace() + 5; /* 5 pixels for 3d effect */ int width = cellSize.width() - space; int xoff = space / 2 + qMax(0, (width - pixmap.width()) / 2); int yoff = space / 2 + cellSize.height() - pixmap.height(); return QRect(QPoint(xoff, yoff), pixmap.size()); } /** * return the number of categories with values in for the given image. */ static int noOfCategoriesForImage(const DB::FileName &image) { static const QString folder(i18n("Folder")); DB::ImageInfoPtr info = image.info(); int grps = info->availableCategories().length(); if (info->itemsOfCategory(folder).empty()) return grps - 1; else return grps - 2; // Exclude folder and media type } /** * Return the height of the text under the thumbnails. */ int ThumbnailView::CellGeometry::textHeight() const { if (!m_cacheInitialized) const_cast(this)->flushCache(); return m_textHeight; } QSize ThumbnailView::CellGeometry::cellSize() const { if (!m_cacheInitialized) const_cast(this)->flushCache(); return m_cellSize; } QRect ThumbnailView::CellGeometry::cellTextGeometry() const { if (!m_cacheInitialized) const_cast(this)->flushCache(); return m_cellTextGeometry; } void ThumbnailView::CellGeometry::flushCache() { m_cacheInitialized = true; calculateTextHeight(); calculateCellSize(); calculateCellTextGeometry(); } void ThumbnailView::CellGeometry::calculateTextHeight() { m_textHeight = 0; const int charHeight = QFontMetrics(widget()->font()).height(); if (Settings::SettingsData::instance()->displayLabels()) m_textHeight += charHeight + 2; if (Settings::SettingsData::instance()->displayCategories()) { int maxCatsInText = 0; - Q_FOREACH (const DB::FileName &fileName, model()->imageList(ViewOrder)) { + for (const DB::FileName &fileName : model()->imageList(ViewOrder)) { maxCatsInText = qMax(noOfCategoriesForImage(fileName), maxCatsInText); } m_textHeight += charHeight * maxCatsInText + 5; } } void ThumbnailView::CellGeometry::calculateCellSize() { const QSize iconSize = preferredIconSize(); const int height = iconSize.height() + 2 + m_textHeight; const int space = Settings::SettingsData::instance()->thumbnailSpace() + 5; /* 5 pixels for 3d effect */ m_cellSize = QSize(iconSize.width() + space, height + space); } void ThumbnailView::CellGeometry::calculateCellTextGeometry() { if (!Settings::SettingsData::instance()->displayLabels() && !Settings::SettingsData::instance()->displayCategories()) m_cellTextGeometry = QRect(); else { const int h = m_textHeight; m_cellTextGeometry = QRect(1, m_cellSize.height() - h - 1, m_cellSize.width() - 2, h); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/KeyboardEventHandler.cpp b/ThumbnailView/KeyboardEventHandler.cpp index 661387cd..8aed919d 100644 --- a/ThumbnailView/KeyboardEventHandler.cpp +++ b/ThumbnailView/KeyboardEventHandler.cpp @@ -1,129 +1,131 @@ -/* 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. + 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 "KeyboardEventHandler.h" #include "CellGeometry.h" #include "ThumbnailModel.h" #include "ThumbnailWidget.h" #include "VideoThumbnailCycler.h" #include "enums.h" #include #include #include #include ThumbnailView::KeyboardEventHandler::KeyboardEventHandler(ThumbnailFactory *factory) : ThumbnailComponent(factory) { } bool ThumbnailView::KeyboardEventHandler::keyPressEvent(QKeyEvent *event) { if (event->modifiers() == Qt::NoModifier && event->key() == Qt::Key_Escape) { if (model()->isFiltered()) { model()->clearFilter(); return true; } } // tokens if (event->key() >= Qt::Key_A && event->key() <= Qt::Key_Z) { const QString token = event->text().toUpper().left(1); if (event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::ShiftModifier) { // toggle tokens bool mustRemoveToken = false; bool hadHit = false; const DB::FileNameList selection = widget()->selection(event->modifiers() == Qt::NoModifier ? NoExpandCollapsedStacks : IncludeAllStacks); DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); - Q_FOREACH (const DB::FileName &fileName, selection) { + for (const DB::FileName &fileName : selection) { DB::ImageInfoPtr info = fileName.info(); if (!hadHit) { mustRemoveToken = info->hasCategoryInfo(tokensCategory->name(), token); hadHit = true; } if (mustRemoveToken) info->removeCategoryInfo(tokensCategory->name(), token); else info->addCategoryInfo(tokensCategory->name(), token); model()->updateCell(fileName); } tokensCategory->addItem(token); MainWindow::DirtyIndicator::markDirty(); return true; } if (event->modifiers() == (Qt::AltModifier | Qt::ShiftModifier)) { // filter view const QString tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name(); model()->toggleCategoryFilter(tokensCategory, token); return true; } } // rating if (event->key() >= Qt::Key_0 && event->key() <= Qt::Key_5) { bool ok; const short rating = 2 * event->text().left(1).toShort(&ok, 10); if (ok) { if (event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::ShiftModifier) { // set rating const DB::FileNameList selection = widget()->selection(event->modifiers() == Qt::NoModifier ? NoExpandCollapsedStacks : IncludeAllStacks); - Q_FOREACH (const DB::FileName &fileName, selection) { + for (const DB::FileName &fileName : selection) { DB::ImageInfoPtr info = fileName.info(); info->setRating(rating); } MainWindow::DirtyIndicator::markDirty(); return true; } } } if (event->key() == Qt::Key_Control && widget()->isItemUnderCursorSelected()) VideoThumbnailCycler::instance()->stopCycle(); if (event->key() == Qt::Key_Return) { emit showSelection(); return true; } return false; } /** Handle key release event. \return true if the event should propagate */ bool ThumbnailView::KeyboardEventHandler::keyReleaseEvent(QKeyEvent *event) { if (widget()->m_wheelResizing && event->key() == Qt::Key_Control) { widget()->m_gridResizeInteraction.leaveGridResizingMode(); widget()->m_wheelResizing = false; return false; // Don't propagate the event - I'm not sure why. } if (event->key() == Qt::Key_Control) VideoThumbnailCycler::instance()->setActive(widget()->mediaIdUnderCursor()); return true; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/SelectionInteraction.cpp b/ThumbnailView/SelectionInteraction.cpp index ab43e18b..75456044 100644 --- a/ThumbnailView/SelectionInteraction.cpp +++ b/ThumbnailView/SelectionInteraction.cpp @@ -1,82 +1,83 @@ -/* Copyright (C) 2003-2018 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 "SelectionInteraction.h" #include "CellGeometry.h" #include "ThumbnailFactory.h" #include "ThumbnailModel.h" #include "ThumbnailWidget.h" #include #include #include #include #include #include #include ThumbnailView::SelectionInteraction::SelectionInteraction(ThumbnailFactory *factory) : ThumbnailComponent(factory) , m_dragInProgress(false) { } bool ThumbnailView::SelectionInteraction::mousePressEvent(QMouseEvent *event) { m_mousePressPos = event->pos(); const DB::FileName fileName = widget()->mediaIdUnderCursor(); m_isMouseDragOperation = widget()->isSelected(fileName) && !event->modifiers(); return m_isMouseDragOperation; } bool ThumbnailView::SelectionInteraction::mouseMoveEvent(QMouseEvent *event) { if (m_isMouseDragOperation) { if ((m_mousePressPos - event->pos()).manhattanLength() > QApplication::startDragDistance()) startDrag(); return true; } return false; } void ThumbnailView::SelectionInteraction::startDrag() { m_dragInProgress = true; QList urls; - Q_FOREACH (const DB::FileName &fileName, widget()->selection(NoExpandCollapsedStacks)) { + for (const DB::FileName &fileName : widget()->selection(NoExpandCollapsedStacks)) { urls.append(QUrl::fromLocalFile(fileName.absolute())); } QDrag *drag = new QDrag(MainWindow::Window::theMainWindow()); QMimeData *data = new QMimeData; data->setUrls(urls); drag->setMimeData(data); drag->exec(Qt::ActionMask); widget()->m_mouseHandler = &(widget()->m_mouseTrackingHandler); m_dragInProgress = false; } bool ThumbnailView::SelectionInteraction::isDragging() const { return m_dragInProgress; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailFacade.cpp b/ThumbnailView/ThumbnailFacade.cpp index e8091d4e..2af814bc 100644 --- a/ThumbnailView/ThumbnailFacade.cpp +++ b/ThumbnailView/ThumbnailFacade.cpp @@ -1,199 +1,201 @@ -/* 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. + 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 "ThumbnailFacade.h" #include "CellGeometry.h" #include "FilterWidget.h" #include "GridResizeSlider.h" #include "ThumbnailModel.h" #include "ThumbnailToolTip.h" #include "ThumbnailWidget.h" #include #include #include ThumbnailView::ThumbnailFacade *ThumbnailView::ThumbnailFacade::s_instance = nullptr; ThumbnailView::ThumbnailFacade::ThumbnailFacade() : m_cellGeometry(nullptr) , m_model(nullptr) , m_widget(nullptr) , m_toolTip(nullptr) { // To avoid one of the components references one of the other before it has been initialized, we first construct them all with null. m_cellGeometry = new CellGeometry(this); m_model = new ThumbnailModel(this); m_widget = new ThumbnailWidget(this); m_toolTip = new ThumbnailToolTip(m_widget); connect(m_widget, &ThumbnailWidget::showImage, this, &ThumbnailFacade::showImage); connect(m_widget, &ThumbnailWidget::showSelection, this, &ThumbnailFacade::showSelection); connect(m_widget, &ThumbnailWidget::fileIdUnderCursorChanged, this, &ThumbnailFacade::fileIdUnderCursorChanged); connect(m_widget, &ThumbnailWidget::currentDateChanged, this, &ThumbnailFacade::currentDateChanged); connect(m_widget, &ThumbnailWidget::selectionCountChanged, this, &ThumbnailFacade::selectionChanged); connect(m_model, &ThumbnailModel::collapseAllStacksEnabled, this, &ThumbnailFacade::collapseAllStacksEnabled); connect(m_model, &ThumbnailModel::expandAllStacksEnabled, this, &ThumbnailFacade::expandAllStacksEnabled); s_instance = this; } QWidget *ThumbnailView::ThumbnailFacade::gui() { return m_widget; } void ThumbnailView::ThumbnailFacade::gotoDate(const DB::ImageDate &date, bool b) { m_widget->gotoDate(date, b); } void ThumbnailView::ThumbnailFacade::setCurrentItem(const DB::FileName &fileName) { widget()->setCurrentItem(fileName); } void ThumbnailView::ThumbnailFacade::reload(SelectionUpdateMethod method) { m_widget->reload(method); } DB::FileNameList ThumbnailView::ThumbnailFacade::selection(ThumbnailView::SelectionMode mode) const { return m_widget->selection(mode); } DB::FileNameList ThumbnailView::ThumbnailFacade::imageList(Order order) const { return m_model->imageList(order); } DB::FileName ThumbnailView::ThumbnailFacade::mediaIdUnderCursor() const { return m_widget->mediaIdUnderCursor(); } DB::FileName ThumbnailView::ThumbnailFacade::currentItem() const { return m_model->imageAt(m_widget->currentIndex().row()); } void ThumbnailView::ThumbnailFacade::setImageList(const DB::FileNameList &list) { m_model->setImageList(list); } void ThumbnailView::ThumbnailFacade::setSortDirection(SortDirection direction) { m_model->setSortDirection(direction); } QSlider *ThumbnailView::ThumbnailFacade::createResizeSlider() { return new GridResizeSlider(this); } ThumbnailView::FilterWidget *ThumbnailView::ThumbnailFacade::filterWidget() { return model()->filterWidget(); } KActionCollection *ThumbnailView::ThumbnailFacade::actions() { return filterWidget()->actions(); } void ThumbnailView::ThumbnailFacade::selectAll() { m_widget->selectAll(); } void ThumbnailView::ThumbnailFacade::clearSelection() { m_widget->clearSelection(); } void ThumbnailView::ThumbnailFacade::showToolTipsOnImages(bool on) { m_toolTip->setActive(on); } void ThumbnailView::ThumbnailFacade::toggleStackExpansion(const DB::FileName &fileName) { m_model->toggleStackExpansion(fileName); } void ThumbnailView::ThumbnailFacade::collapseAllStacks() { m_model->collapseAllStacks(); } void ThumbnailView::ThumbnailFacade::expandAllStacks() { m_model->expandAllStacks(); } void ThumbnailView::ThumbnailFacade::updateDisplayModel() { m_model->updateDisplayModel(); } void ThumbnailView::ThumbnailFacade::changeSingleSelection(const DB::FileName &fileName) { m_widget->changeSingleSelection(fileName); } ThumbnailView::ThumbnailModel *ThumbnailView::ThumbnailFacade::model() { Q_ASSERT(m_model); return m_model; } ThumbnailView::CellGeometry *ThumbnailView::ThumbnailFacade::cellGeometry() { Q_ASSERT(m_cellGeometry); return m_cellGeometry; } ThumbnailView::ThumbnailWidget *ThumbnailView::ThumbnailFacade::widget() { Q_ASSERT(m_widget); return m_widget; } ThumbnailView::ThumbnailFacade *ThumbnailView::ThumbnailFacade::instance() { Q_ASSERT(s_instance); return s_instance; } void ThumbnailView::ThumbnailFacade::slotRecreateThumbnail() { - Q_FOREACH (const DB::FileName &fileName, widget()->selection(NoExpandCollapsedStacks)) { + for (const DB::FileName &fileName : widget()->selection(NoExpandCollapsedStacks)) { ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); BackgroundJobs::HandleVideoThumbnailRequestJob::removeFullScaleFrame(fileName); m_model->updateCell(fileName); } } void ThumbnailView::ThumbnailFacade::clearFilter() { Q_ASSERT(m_model); m_model->clearFilter(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailModel.cpp b/ThumbnailView/ThumbnailModel.cpp index f040f7b5..02f76730 100644 --- a/ThumbnailView/ThumbnailModel.cpp +++ b/ThumbnailView/ThumbnailModel.cpp @@ -1,559 +1,561 @@ -/* 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. + 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 "ThumbnailModel.h" #include "CellGeometry.h" #include "FilterWidget.h" #include "Logging.h" #include "SelectionMaintainer.h" #include "ThumbnailRequest.h" #include "ThumbnailWidget.h" #include #include #include #include #include #include #include #include #include ThumbnailView::ThumbnailModel::ThumbnailModel(ThumbnailFactory *factory) : ThumbnailComponent(factory) , m_sortDirection(Settings::SettingsData::instance()->showNewestThumbnailFirst() ? NewestFirst : OldestFirst) , m_firstVisibleRow(-1) , m_lastVisibleRow(-1) { connect(DB::ImageDB::instance(), SIGNAL(imagesDeleted(DB::FileNameList)), this, SLOT(imagesDeletedFromDB(DB::FileNameList))); m_ImagePlaceholder = QIcon::fromTheme(QLatin1String("image-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize()); m_VideoPlaceholder = QIcon::fromTheme(QLatin1String("video-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize()); m_filter.setSearchMode(0); connect(this, &ThumbnailModel::filterChanged, this, &ThumbnailModel::updateDisplayModel); m_filterWidget = new FilterWidget; connect(this, &ThumbnailModel::filterChanged, m_filterWidget, &FilterWidget::setFilter); connect(m_filterWidget, &FilterWidget::ratingChanged, this, &ThumbnailModel::filterByRating); connect(m_filterWidget, &FilterWidget::filterToggled, this, &ThumbnailModel::toggleFilter); } static bool stackOrderComparator(const DB::FileName &a, const DB::FileName &b) { return a.info()->stackOrder() < b.info()->stackOrder(); } void ThumbnailView::ThumbnailModel::updateDisplayModel() { beginResetModel(); ImageManager::AsyncLoader::instance()->stop(model(), ImageManager::StopOnlyNonPriorityLoads); // Note, this can be simplified, if we make the database backend already // return things in the right order. Then we only need one pass while now // we need to go through the list two times. /* Extract all stacks we have first. Different stackid's might be * intermingled in the result so we need to know this ahead before * creating the display list. */ typedef QList StackList; typedef QMap StackMap; StackMap stackContents; - Q_FOREACH (const DB::FileName &fileName, m_imageList) { + for (const DB::FileName &fileName : m_imageList) { DB::ImageInfoPtr imageInfo = fileName.info(); if (imageInfo && imageInfo->isStacked()) { DB::StackID stackid = imageInfo->stackId(); stackContents[stackid].append(fileName); } } /* * All stacks need to be ordered in their stack order. We don't rely that * the images actually came in the order necessary. */ for (StackMap::iterator it = stackContents.begin(); it != stackContents.end(); ++it) { std::stable_sort(it->begin(), it->end(), stackOrderComparator); } /* Build the final list to be displayed. That is basically the sequence * we got from the original, but the stacks shown with all images together * in the right sequence or collapsed showing only the top image. */ m_displayList = DB::FileNameList(); QSet alreadyShownStacks; - Q_FOREACH (const DB::FileName &fileName, m_imageList) { + for (const DB::FileName &fileName : m_imageList) { DB::ImageInfoPtr imageInfo = fileName.info(); if (!m_filter.match(imageInfo)) continue; if (imageInfo && imageInfo->isStacked()) { DB::StackID stackid = imageInfo->stackId(); if (alreadyShownStacks.contains(stackid)) continue; StackMap::iterator found = stackContents.find(stackid); Q_ASSERT(found != stackContents.end()); const StackList &orderedStack = *found; if (m_expandedStacks.contains(stackid)) { - Q_FOREACH (const DB::FileName &fileName, orderedStack) { + for (const DB::FileName &fileName : orderedStack) { m_displayList.append(fileName); } } else { m_displayList.append(orderedStack.at(0)); } alreadyShownStacks.insert(stackid); } else { m_displayList.append(fileName); } } if (m_sortDirection != OldestFirst) m_displayList = m_displayList.reversed(); updateIndexCache(); emit collapseAllStacksEnabled(m_expandedStacks.size() > 0); emit expandAllStacksEnabled(m_allStacks.size() != model()->m_expandedStacks.size()); endResetModel(); } void ThumbnailView::ThumbnailModel::toggleStackExpansion(const DB::FileName &fileName) { DB::ImageInfoPtr imageInfo = fileName.info(); if (imageInfo) { DB::StackID stackid = imageInfo->stackId(); model()->beginResetModel(); if (m_expandedStacks.contains(stackid)) m_expandedStacks.remove(stackid); else m_expandedStacks.insert(stackid); updateDisplayModel(); model()->endResetModel(); } } void ThumbnailView::ThumbnailModel::collapseAllStacks() { m_expandedStacks.clear(); updateDisplayModel(); } void ThumbnailView::ThumbnailModel::expandAllStacks() { m_expandedStacks = m_allStacks; updateDisplayModel(); } void ThumbnailView::ThumbnailModel::setImageList(const DB::FileNameList &items) { m_imageList = items; m_allStacks.clear(); - Q_FOREACH (const DB::FileName &fileName, items) { + for (const DB::FileName &fileName : items) { DB::ImageInfoPtr info = fileName.info(); if (info && info->isStacked()) m_allStacks << info->stackId(); } updateDisplayModel(); preloadThumbnails(); } // TODO(hzeller) figure out if this should return the m_imageList or m_displayList. DB::FileNameList ThumbnailView::ThumbnailModel::imageList(Order order) const { if (order == SortedOrder && m_sortDirection == NewestFirst) return m_displayList.reversed(); else return m_displayList; } void ThumbnailView::ThumbnailModel::imagesDeletedFromDB(const DB::FileNameList &list) { SelectionMaintainer dummy(widget(), model()); - Q_FOREACH (const DB::FileName &fileName, list) { + for (const DB::FileName &fileName : list) { m_displayList.removeAll(fileName); m_imageList.removeAll(fileName); } updateDisplayModel(); } int ThumbnailView::ThumbnailModel::indexOf(const DB::FileName &fileName) { Q_ASSERT(!fileName.isNull()); if (!m_fileNameToIndex.contains(fileName)) m_fileNameToIndex.insert(fileName, m_displayList.indexOf(fileName)); return m_fileNameToIndex[fileName]; } int ThumbnailView::ThumbnailModel::indexOf(const DB::FileName &fileName) const { Q_ASSERT(!fileName.isNull()); if (!m_fileNameToIndex.contains(fileName)) return -1; return m_fileNameToIndex[fileName]; } void ThumbnailView::ThumbnailModel::updateIndexCache() { m_fileNameToIndex.clear(); int index = 0; - Q_FOREACH (const DB::FileName &fileName, m_displayList) { + for (const DB::FileName &fileName : m_displayList) { m_fileNameToIndex[fileName] = index; ++index; } } DB::FileName ThumbnailView::ThumbnailModel::rightDropItem() const { return m_rightDrop; } void ThumbnailView::ThumbnailModel::setRightDropItem(const DB::FileName &item) { m_rightDrop = item; } DB::FileName ThumbnailView::ThumbnailModel::leftDropItem() const { return m_leftDrop; } void ThumbnailView::ThumbnailModel::setLeftDropItem(const DB::FileName &item) { m_leftDrop = item; } void ThumbnailView::ThumbnailModel::setSortDirection(SortDirection direction) { if (direction == m_sortDirection) return; Settings::SettingsData::instance()->setShowNewestFirst(direction == NewestFirst); m_displayList = m_displayList.reversed(); updateIndexCache(); m_sortDirection = direction; } bool ThumbnailView::ThumbnailModel::isItemInExpandedStack(const DB::StackID &id) const { return m_expandedStacks.contains(id); } int ThumbnailView::ThumbnailModel::imageCount() const { return m_displayList.size(); } void ThumbnailView::ThumbnailModel::setOverrideImage(const DB::FileName &fileName, const QPixmap &pixmap) { if (pixmap.isNull()) m_overrideFileName = DB::FileName(); else { m_overrideFileName = fileName; m_overrideImage = pixmap; } emit dataChanged(fileNameToIndex(fileName), fileNameToIndex(fileName)); } DB::FileName ThumbnailView::ThumbnailModel::imageAt(int index) const { Q_ASSERT(index >= 0 && index < imageCount()); return m_displayList.at(index); } int ThumbnailView::ThumbnailModel::rowCount(const QModelIndex &) const { return imageCount(); } QVariant ThumbnailView::ThumbnailModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= m_displayList.size()) return QVariant(); if (role == Qt::DecorationRole) { const DB::FileName fileName = m_displayList.at(index.row()); return pixmap(fileName); } if (role == Qt::DisplayRole) return thumbnailText(index); return QVariant(); } void ThumbnailView::ThumbnailModel::requestThumbnail(const DB::FileName &fileName, const ImageManager::Priority priority) { DB::ImageInfoPtr imageInfo = fileName.info(); if (!imageInfo) return; // request the thumbnail in the size that is set in the settings, not in the current grid size: const QSize cellSize = cellGeometryInfo()->baseIconSize(); const int angle = imageInfo->angle(); const int row = indexOf(fileName); ThumbnailRequest *request = new ThumbnailRequest(row, fileName, cellSize, angle, this); request->setPriority(priority); ImageManager::AsyncLoader::instance()->load(request); } void ThumbnailView::ThumbnailModel::pixmapLoaded(ImageManager::ImageRequest *request, const QImage & /*image*/) { const DB::FileName fileName = request->databaseFileName(); const QSize fullSize = request->fullSize(); // As a result of the image being loaded, we emit the dataChanged signal, which in turn asks the delegate to paint the cell // The delegate now fetches the newly loaded image from the cache. DB::ImageInfoPtr imageInfo = fileName.info(); // TODO(hzeller): figure out, why the size is set here. We do an implicit // write here to the database. if (fullSize.isValid() && imageInfo) { imageInfo->setSize(fullSize); } emit dataChanged(fileNameToIndex(fileName), fileNameToIndex(fileName)); } QString ThumbnailView::ThumbnailModel::thumbnailText(const QModelIndex &index) const { const DB::FileName fileName = imageAt(index.row()); QString text; const QSize cellSize = cellGeometryInfo()->preferredIconSize(); const int thumbnailHeight = cellSize.height() - 2 * Settings::SettingsData::instance()->thumbnailSpace(); const int thumbnailWidth = cellSize.width(); // no subtracting here const int maxCharacters = thumbnailHeight / QFontMetrics(widget()->font()).maxWidth() * 2; if (Settings::SettingsData::instance()->displayLabels()) { QString line = fileName.info()->label(); if (stringWidth(line) > thumbnailWidth) { line = line.left(maxCharacters); line += QLatin1String(" ..."); } text += line + QLatin1String("\n"); } if (Settings::SettingsData::instance()->displayCategories()) { QStringList grps = fileName.info()->availableCategories(); for (QStringList::const_iterator it = grps.constBegin(); it != grps.constEnd(); ++it) { QString category = *it; if (category != i18n("Folder") && category != i18n("Media Type")) { Utilities::StringSet items = fileName.info()->itemsOfCategory(category); if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && !Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (category == Settings::SettingsData::instance()->untaggedCategory()) { if (items.contains(Settings::SettingsData::instance()->untaggedTag())) { items.remove(Settings::SettingsData::instance()->untaggedTag()); } } } if (!items.empty()) { QString line; bool first = true; for (Utilities::StringSet::const_iterator it2 = items.begin(); it2 != items.end(); ++it2) { QString item = *it2; if (first) first = false; else line += QLatin1String(", "); line += item; } if (stringWidth(line) > thumbnailWidth) { line = line.left(maxCharacters); line += QLatin1String(" ..."); } text += line + QLatin1String("\n"); } } } } return text.trimmed(); } void ThumbnailView::ThumbnailModel::updateCell(int row) { updateCell(index(row, 0)); } void ThumbnailView::ThumbnailModel::updateCell(const QModelIndex &index) { emit dataChanged(index, index); } void ThumbnailView::ThumbnailModel::updateCell(const DB::FileName &fileName) { updateCell(indexOf(fileName)); } QModelIndex ThumbnailView::ThumbnailModel::fileNameToIndex(const DB::FileName &fileName) const { if (fileName.isNull()) return QModelIndex(); else return index(indexOf(fileName), 0); } QPixmap ThumbnailView::ThumbnailModel::pixmap(const DB::FileName &fileName) const { if (m_overrideFileName == fileName) return m_overrideImage; const DB::ImageInfoPtr imageInfo = fileName.info(); if (imageInfo == DB::ImageInfoPtr(nullptr)) return QPixmap(); if (ImageManager::ThumbnailCache::instance()->contains(fileName)) { // the cached thumbnail needs to be scaled to the actual thumbnail size: return ImageManager::ThumbnailCache::instance()->lookup(fileName).scaled(cellGeometryInfo()->preferredIconSize(), Qt::KeepAspectRatio); } const_cast(this)->requestThumbnail(fileName, ImageManager::ThumbnailVisible); if (imageInfo->isVideo()) return m_VideoPlaceholder; else return m_ImagePlaceholder; } bool ThumbnailView::ThumbnailModel::isFiltered() const { return !m_filter.isNull(); } ThumbnailView::FilterWidget *ThumbnailView::ThumbnailModel::filterWidget() { return m_filterWidget; } bool ThumbnailView::ThumbnailModel::thumbnailStillNeeded(int row) const { return (row >= m_firstVisibleRow && row <= m_lastVisibleRow); } void ThumbnailView::ThumbnailModel::updateVisibleRowInfo() { m_firstVisibleRow = widget()->indexAt(QPoint(0, 0)).row(); const int columns = widget()->width() / cellGeometryInfo()->cellSize().width(); const int rows = widget()->height() / cellGeometryInfo()->cellSize().height(); m_lastVisibleRow = qMin(m_firstVisibleRow + columns * (rows + 1), rowCount(QModelIndex())); // the cellGeometry has changed -> update placeholders m_ImagePlaceholder = QIcon::fromTheme(QLatin1String("image-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize()); m_VideoPlaceholder = QIcon::fromTheme(QLatin1String("video-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize()); } void ThumbnailView::ThumbnailModel::toggleFilter(bool enable) { if (!enable) clearFilter(); else if (m_filter.isNull()) { std::swap(m_filter, m_previousFilter); emit filterChanged(m_filter); } } void ThumbnailView::ThumbnailModel::clearFilter() { if (!m_filter.isNull()) { qCDebug(ThumbnailViewLog) << "Filter cleared."; m_previousFilter = m_filter; m_filter = DB::ImageSearchInfo(); emit filterChanged(m_filter); } } void ThumbnailView::ThumbnailModel::filterByRating(short rating) { Q_ASSERT(-1 <= rating && rating <= 10); qCDebug(ThumbnailViewLog) << "Filter set: rating(" << rating << ")"; m_filter.setRating(rating); emit filterChanged(m_filter); } void ThumbnailView::ThumbnailModel::toggleRatingFilter(short rating) { if (m_filter.rating() == rating) { filterByRating(rating); } else { filterByRating(-1); qCDebug(ThumbnailViewLog) << "Filter removed: rating"; m_filter.setRating(-1); m_filter.checkIfNull(); emit filterChanged(m_filter); } } void ThumbnailView::ThumbnailModel::filterByCategory(const QString &category, const QString &tag) { qCDebug(ThumbnailViewLog) << "Filter added: category(" << category << "," << tag << ")"; m_filter.addAnd(category, tag); emit filterChanged(m_filter); } void ThumbnailView::ThumbnailModel::toggleCategoryFilter(const QString &category, const QString &tag) { auto tags = m_filter.categoryMatchText(category).split(QLatin1String("&"), QString::SkipEmptyParts); for (const auto &existingTag : tags) { if (tag == existingTag.trimmed()) { qCDebug(ThumbnailViewLog) << "Filter removed: category(" << category << "," << tag << ")"; tags.removeAll(existingTag); m_filter.setCategoryMatchText(category, tags.join(QLatin1String(" & "))); m_filter.checkIfNull(); emit filterChanged(m_filter); return; } } filterByCategory(category, tag); } void ThumbnailView::ThumbnailModel::preloadThumbnails() { // FIXME: it would make a lot of sense to merge preloadThumbnails() with pixmap() // and maybe also move the caching stuff into the ImageManager - Q_FOREACH (const DB::FileName &fileName, m_displayList) { + for (const DB::FileName &fileName : m_displayList) { if (fileName.isNull()) continue; if (ImageManager::ThumbnailCache::instance()->contains(fileName)) continue; const_cast(this)->requestThumbnail(fileName, ImageManager::ThumbnailInvisible); } } int ThumbnailView::ThumbnailModel::stringWidth(const QString &text) const { // This is a workaround for the deprecation warnings emerged with Qt 5.13. // QFontMetrics::horizontalAdvance wasn't introduced until Qt 5.11. As soon as we drop support // for Qt versions before 5.11, this can be removed in favor of calling horizontalAdvance // directly. #if (QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) return QFontMetrics(widget()->font()).width(text); #else return QFontMetrics(widget()->font()).horizontalAdvance(text); #endif } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailWidget.cpp b/ThumbnailView/ThumbnailWidget.cpp index 6ad0a696..0d0b824b 100644 --- a/ThumbnailView/ThumbnailWidget.cpp +++ b/ThumbnailView/ThumbnailWidget.cpp @@ -1,452 +1,453 @@ -/* 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. + 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 "ThumbnailWidget.h" #include "CellGeometry.h" #include "Delegate.h" #include "KeyboardEventHandler.h" #include "SelectionMaintainer.h" #include "ThumbnailDND.h" #include "ThumbnailFactory.h" #include "ThumbnailModel.h" #include #include #include #include #include -#include -#include #include #include +#include +#include #include #include #include #include /** * \class ThumbnailView::ThumbnailWidget * This is the widget which shows the thumbnails. * * In previous versions this was implemented using a QIconView, but there * simply was too many problems, so after years of tears and pains I * rewrote it. */ ThumbnailView::ThumbnailWidget::ThumbnailWidget(ThumbnailFactory *factory) : QListView() , ThumbnailComponent(factory) , m_isSettingDate(false) , m_gridResizeInteraction(factory) , m_wheelResizing(false) , m_externallyResizing(false) , m_selectionInteraction(factory) , m_mouseTrackingHandler(factory) , m_mouseHandler(&m_mouseTrackingHandler) , m_dndHandler(new ThumbnailDND(factory)) , m_pressOnStackIndicator(false) , m_keyboardHandler(new KeyboardEventHandler(factory)) , m_videoThumbnailCycler(new VideoThumbnailCycler(model())) { setModel(ThumbnailComponent::model()); setResizeMode(QListView::Adjust); setViewMode(QListView::IconMode); setUniformItemSizes(true); setSelectionMode(QAbstractItemView::ExtendedSelection); // It beats me why I need to set mouse tracking on both, but without it doesn't work. viewport()->setMouseTracking(true); setMouseTracking(true); connect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(scheduleDateChangeSignal())); viewport()->setAcceptDrops(true); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); connect(&m_mouseTrackingHandler, &MouseTrackingInteraction::fileIdUnderCursorChanged, this, &ThumbnailWidget::fileIdUnderCursorChanged); connect(m_keyboardHandler, &KeyboardEventHandler::showSelection, this, &ThumbnailWidget::showSelection); updatePalette(); setItemDelegate(new Delegate(factory, this)); connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(emitSelectionChangedSignal())); setDragEnabled(false); // We run our own dragging, so disable QListView's version. connect(verticalScrollBar(), SIGNAL(valueChanged(int)), model(), SLOT(updateVisibleRowInfo())); setupDateChangeTimer(); } bool ThumbnailView::ThumbnailWidget::isGridResizing() const { return m_mouseHandler->isResizingGrid() || m_wheelResizing || m_externallyResizing; } void ThumbnailView::ThumbnailWidget::keyPressEvent(QKeyEvent *event) { if (!m_keyboardHandler->keyPressEvent(event)) QListView::keyPressEvent(event); } void ThumbnailView::ThumbnailWidget::keyReleaseEvent(QKeyEvent *event) { const bool propagate = m_keyboardHandler->keyReleaseEvent(event); if (propagate) QListView::keyReleaseEvent(event); } bool ThumbnailView::ThumbnailWidget::isMouseOverStackIndicator(const QPoint &point) { // first check if image is stack, if not return. DB::ImageInfoPtr imageInfo = mediaIdUnderCursor().info(); if (!imageInfo) return false; if (!imageInfo->isStacked()) return false; const QModelIndex index = indexUnderCursor(); const QRect itemRect = visualRect(index); const QPixmap pixmap = index.data(Qt::DecorationRole).value(); if (pixmap.isNull()) return false; const QRect pixmapRect = cellGeometryInfo()->iconGeometry(pixmap).translated(itemRect.topLeft()); const QRect blackOutRect = pixmapRect.adjusted(0, 0, -10, -10); return pixmapRect.contains(point) && !blackOutRect.contains(point); } static bool isMouseResizeGesture(QMouseEvent *event) { return (event->button() & Qt::MidButton) || ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::AltModifier)); } void ThumbnailView::ThumbnailWidget::mousePressEvent(QMouseEvent *event) { if ((!(event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier))) && isMouseOverStackIndicator(event->pos())) { model()->toggleStackExpansion(mediaIdUnderCursor()); m_pressOnStackIndicator = true; return; } if (isMouseResizeGesture(event)) m_mouseHandler = &m_gridResizeInteraction; else m_mouseHandler = &m_selectionInteraction; if (!m_mouseHandler->mousePressEvent(event)) QListView::mousePressEvent(event); if (event->button() & Qt::RightButton) //get out of selection mode if this is a right click m_mouseHandler = &m_mouseTrackingHandler; } void ThumbnailView::ThumbnailWidget::mouseMoveEvent(QMouseEvent *event) { if (m_pressOnStackIndicator) return; if (!m_mouseHandler->mouseMoveEvent(event)) QListView::mouseMoveEvent(event); } void ThumbnailView::ThumbnailWidget::mouseReleaseEvent(QMouseEvent *event) { if (m_pressOnStackIndicator) { m_pressOnStackIndicator = false; return; } if (!m_mouseHandler->mouseReleaseEvent(event)) QListView::mouseReleaseEvent(event); m_mouseHandler = &m_mouseTrackingHandler; } void ThumbnailView::ThumbnailWidget::mouseDoubleClickEvent(QMouseEvent *event) { if (isMouseOverStackIndicator(event->pos())) { model()->toggleStackExpansion(mediaIdUnderCursor()); m_pressOnStackIndicator = true; } else if (!(event->modifiers() & Qt::ControlModifier)) { DB::FileName id = mediaIdUnderCursor(); if (!id.isNull()) emit showImage(id); } } void ThumbnailView::ThumbnailWidget::wheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::ControlModifier) { event->setAccepted(true); if (!m_wheelResizing) m_gridResizeInteraction.enterGridResizingMode(); m_wheelResizing = true; model()->beginResetModel(); const int delta = -event->delta() / 20; static int _minimum_ = Settings::SettingsData::instance()->minimumThumbnailSize(); Settings::SettingsData::instance()->setActualThumbnailSize(qMax(_minimum_, Settings::SettingsData::instance()->actualThumbnailSize() + delta)); cellGeometryInfo()->calculateCellSize(); model()->endResetModel(); } else { int delta = event->delta() / 5; QWheelEvent newevent = QWheelEvent(event->pos(), delta, event->buttons(), nullptr); QListView::wheelEvent(&newevent); } } void ThumbnailView::ThumbnailWidget::emitDateChange() { if (m_isSettingDate) return; int row = currentIndex().row(); if (row == -1) return; DB::FileName fileName = model()->imageAt(row); if (fileName.isNull()) return; static QDateTime lastDate; QDateTime date = fileName.info()->date().start(); if (date != lastDate) { lastDate = date; if (date.date().year() != 1900) emit currentDateChanged(date); } } /** * scroll to the date specified with the parameter date. * The boolean includeRanges tells whether we accept range matches or not. */ void ThumbnailView::ThumbnailWidget::gotoDate(const DB::ImageDate &date, bool includeRanges) { m_isSettingDate = true; DB::FileName candidate = DB::ImageDB::instance() ->findFirstItemInRange(model()->imageList(ViewOrder), date, includeRanges); if (!candidate.isNull()) setCurrentItem(candidate); m_isSettingDate = false; } void ThumbnailView::ThumbnailWidget::setExternallyResizing(bool state) { m_externallyResizing = state; } void ThumbnailView::ThumbnailWidget::reload(SelectionUpdateMethod method) { SelectionMaintainer maintainer(this, model()); ThumbnailComponent::model()->beginResetModel(); cellGeometryInfo()->flushCache(); updatePalette(); ThumbnailComponent::model()->endResetModel(); if (method == ClearSelection) maintainer.disable(); } DB::FileName ThumbnailView::ThumbnailWidget::mediaIdUnderCursor() const { const QModelIndex index = indexUnderCursor(); if (index.isValid()) return model()->imageAt(index.row()); else return DB::FileName(); } QModelIndex ThumbnailView::ThumbnailWidget::indexUnderCursor() const { return indexAt(mapFromGlobal(QCursor::pos())); } void ThumbnailView::ThumbnailWidget::dragMoveEvent(QDragMoveEvent *event) { m_dndHandler->contentsDragMoveEvent(event); } void ThumbnailView::ThumbnailWidget::dragLeaveEvent(QDragLeaveEvent *event) { m_dndHandler->contentsDragLeaveEvent(event); } void ThumbnailView::ThumbnailWidget::dropEvent(QDropEvent *event) { m_dndHandler->contentsDropEvent(event); } void ThumbnailView::ThumbnailWidget::dragEnterEvent(QDragEnterEvent *event) { m_dndHandler->contentsDragEnterEvent(event); } void ThumbnailView::ThumbnailWidget::setCurrentItem(const DB::FileName &fileName) { if (fileName.isNull()) return; const int row = model()->indexOf(fileName); setCurrentIndex(QListView::model()->index(row, 0)); } DB::FileName ThumbnailView::ThumbnailWidget::currentItem() const { if (!currentIndex().isValid()) return DB::FileName(); return model()->imageAt(currentIndex().row()); } void ThumbnailView::ThumbnailWidget::updatePalette() { QPalette pal = palette(); pal.setBrush(QPalette::Base, QColor(Settings::SettingsData::instance()->backgroundColor())); pal.setBrush(QPalette::Text, contrastColor(QColor(Settings::SettingsData::instance()->backgroundColor()))); setPalette(pal); } int ThumbnailView::ThumbnailWidget::cellWidth() const { return visualRect(QListView::model()->index(0, 0)).size().width(); } void ThumbnailView::ThumbnailWidget::emitSelectionChangedSignal() { emit selectionCountChanged(selection(ExpandCollapsedStacks).size()); } void ThumbnailView::ThumbnailWidget::scheduleDateChangeSignal() { m_dateChangedTimer->start(200); } /** * During profiling, I found that emitting the dateChanged signal was * rather expensive, so now I delay that signal, so it is only emitted 200 * msec after the scroll, which means it will not be emitted when the user * holds down, say the page down key for scrolling. */ void ThumbnailView::ThumbnailWidget::setupDateChangeTimer() { m_dateChangedTimer = new QTimer(this); m_dateChangedTimer->setSingleShot(true); connect(m_dateChangedTimer, &QTimer::timeout, this, &ThumbnailWidget::emitDateChange); } void ThumbnailView::ThumbnailWidget::showEvent(QShowEvent *event) { model()->updateVisibleRowInfo(); QListView::showEvent(event); } DB::FileNameList ThumbnailView::ThumbnailWidget::selection(ThumbnailView::SelectionMode mode) const { DB::FileNameList res; - Q_FOREACH (const QModelIndex &index, selectedIndexes()) { + for (const QModelIndex &index : selectedIndexes()) { DB::FileName currFileName = model()->imageAt(index.row()); bool includeAllStacks = false; switch (mode) { case IncludeAllStacks: includeAllStacks = true; /* FALLTHROUGH */ case ExpandCollapsedStacks: { // if the selected image belongs to a collapsed thread, // imply that all images in the stack are selected: DB::ImageInfoPtr imageInfo = currFileName.info(); if (imageInfo && imageInfo->isStacked() && (includeAllStacks || !model()->isItemInExpandedStack(imageInfo->stackId()))) { // add all images in the same stack res.append(DB::ImageDB::instance()->getStackFor(currFileName)); } else res.append(currFileName); } break; case NoExpandCollapsedStacks: res.append(currFileName); break; } } return res; } bool ThumbnailView::ThumbnailWidget::isSelected(const DB::FileName &fileName) const { return selection(NoExpandCollapsedStacks).indexOf(fileName) != -1; } /** This very specific method will make the item specified by id selected, if there only are one item selected. This is used from the Viewer when you start it without a selection, and are going forward or backward. */ void ThumbnailView::ThumbnailWidget::changeSingleSelection(const DB::FileName &fileName) { if (selection(NoExpandCollapsedStacks).size() == 1) { QItemSelectionModel *selection = selectionModel(); selection->select(model()->fileNameToIndex(fileName), QItemSelectionModel::ClearAndSelect); setCurrentItem(fileName); } } void ThumbnailView::ThumbnailWidget::select(const DB::FileNameList &items) { QItemSelection selection; QModelIndex start; QModelIndex end; int count = 0; - Q_FOREACH (const DB::FileName &fileName, items) { + for (const DB::FileName &fileName : items) { QModelIndex index = model()->fileNameToIndex(fileName); if (count == 0) { start = index; end = index; } else if (index.row() == end.row() + 1) { end = index; } else { selection.merge(QItemSelection(start, end), QItemSelectionModel::Select); start = index; end = index; } count++; } if (count > 0) { selection.merge(QItemSelection(start, end), QItemSelectionModel::Select); } selectionModel()->select(selection, QItemSelectionModel::Select); } bool ThumbnailView::ThumbnailWidget::isItemUnderCursorSelected() const { return widget()->selection(ExpandCollapsedStacks).contains(mediaIdUnderCursor()); } QColor ThumbnailView::contrastColor(const QColor &color) { if (color.red() < 127 && color.green() < 127 && color.blue() < 127) return Qt::white; else return Qt::black; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/DeleteFiles.cpp b/Utilities/DeleteFiles.cpp index b2151a89..20951fd4 100644 --- a/Utilities/DeleteFiles.cpp +++ b/Utilities/DeleteFiles.cpp @@ -1,97 +1,97 @@ -/* Copyright (C) 2012-2019 The KPhotoAlbum Development Team +/* Copyright (C) 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 + 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 + 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 . + along with this program. If not, see . */ #include "DeleteFiles.h" #include "ShowBusyCursor.h" #include #include #include #include #include #include #include #include #include #include namespace Utilities { DeleteFiles *DeleteFiles::s_instance; bool DeleteFiles::deleteFiles(const DB::FileNameList &files, DeleteMethod method) { if (!s_instance) s_instance = new DeleteFiles; return s_instance->deleteFilesPrivate(files, method); } void DeleteFiles::slotKIOJobCompleted(KJob *job) { if (job->error()) KMessageBox::error(MainWindow::Window::theMainWindow(), job->errorString(), i18n("Error Deleting Files")); } bool DeleteFiles::deleteFilesPrivate(const DB::FileNameList &files, DeleteMethod method) { Utilities::ShowBusyCursor dummy; DB::FileNameList filenamesToRemove; QList filesToDelete; - Q_FOREACH (const DB::FileName &fileName, files) { + for (const DB::FileName &fileName : files) { if (DB::ImageInfo::imageOnDisk(fileName)) { if (method == DeleteFromDisk || method == MoveToTrash) { filesToDelete.append(QUrl::fromLocalFile(fileName.absolute())); filenamesToRemove.append(fileName); } else { filenamesToRemove.append(fileName); } } else filenamesToRemove.append(fileName); } ImageManager::ThumbnailCache::instance()->removeThumbnails(files); if (method == DeleteFromDisk || method == MoveToTrash) { KJob *job; if (method == MoveToTrash) job = KIO::trash(filesToDelete); else job = KIO::del(filesToDelete); connect(job, &KJob::result, this, &DeleteFiles::slotKIOJobCompleted); } if (!filenamesToRemove.isEmpty()) { if (method == MoveToTrash || method == DeleteFromDisk) DB::ImageDB::instance()->deleteList(filenamesToRemove); else DB::ImageDB::instance()->addToBlockList(filenamesToRemove); MainWindow::DirtyIndicator::markDirty(); return true; } else return false; } } // namespace Utilities // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/DemoUtil.cpp b/Utilities/DemoUtil.cpp index dcb1b3e1..1edb04be 100644 --- a/Utilities/DemoUtil.cpp +++ b/Utilities/DemoUtil.cpp @@ -1,126 +1,127 @@ -/* 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. + 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 "DemoUtil.h" #include "FileUtil.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #include namespace { void copyList(const QStringList &from, const QString &directoryTo) { for (QStringList::ConstIterator it = from.constBegin(); it != from.constEnd(); ++it) { const QString destFile = directoryTo + QString::fromLatin1("/") + QFileInfo(*it).fileName(); if (!QFileInfo(destFile).exists()) { const bool ok = Utilities::copyOrOverwrite(*it, destFile); if (!ok) { KMessageBox::error(nullptr, i18n("Unable to copy '%1' to '%2'.", *it, destFile), i18n("Error Running Demo")); exit(-1); } } } } } // vi:expandtab:tabstop=4 shiftwidth=4: QString Utilities::setupDemo() { const QString demoDir = QString::fromLatin1("%1/kphotoalbum-demo-%2").arg(QDir::tempPath()).arg(QString::fromLocal8Bit(qgetenv("LOGNAME"))); QFileInfo fi(demoDir); if (!fi.exists()) { bool ok = QDir().mkdir(demoDir); if (!ok) { KMessageBox::error(nullptr, i18n("Unable to create directory '%1' needed for demo.", demoDir), i18n("Error Running Demo")); exit(-1); } } // index.xml const QString demoDB = QStandardPaths::locate(QStandardPaths::DataLocation, QString::fromLatin1("demo/index.xml")); if (demoDB.isEmpty()) { qCDebug(UtilitiesLog) << "No demo database in standard locations:" << QStandardPaths::standardLocations(QStandardPaths::DataLocation); exit(-1); } const QString configFile = demoDir + QString::fromLatin1("/index.xml"); copyOrOverwrite(demoDB, configFile); // Images const QStringList kpaDemoDirs = QStandardPaths::locateAll( QStandardPaths::DataLocation, QString::fromLatin1("demo"), QStandardPaths::LocateDirectory); QStringList images; - Q_FOREACH (const QString &dir, kpaDemoDirs) { + for (const QString &dir : kpaDemoDirs) { QDirIterator it(dir, QStringList() << QStringLiteral("*.jpg") << QStringLiteral("*.avi")); while (it.hasNext()) { images.append(it.next()); } } copyList(images, demoDir); // CategoryImages QString catDir = demoDir + QString::fromLatin1("/CategoryImages"); fi = QFileInfo(catDir); if (!fi.exists()) { bool ok = QDir().mkdir(catDir); if (!ok) { KMessageBox::error(nullptr, i18n("Unable to create directory '%1' needed for demo.", catDir), i18n("Error Running Demo")); exit(-1); } } const QStringList kpaDemoCatDirs = QStandardPaths::locateAll( QStandardPaths::DataLocation, QString::fromLatin1("demo/CategoryImages"), QStandardPaths::LocateDirectory); QStringList catImages; - Q_FOREACH (const QString &dir, kpaDemoCatDirs) { + for (const QString &dir : kpaDemoCatDirs) { QDirIterator it(dir, QStringList() << QStringLiteral("*.jpg")); while (it.hasNext()) { catImages.append(it.next()); } } copyList(catImages, catDir); return configFile; } void Utilities::deleteDemo() { QString dir = QString::fromLatin1("%1/kphotoalbum-demo-%2").arg(QDir::tempPath()).arg(QString::fromLocal8Bit(qgetenv("LOGNAME"))); QUrl demoUrl = QUrl::fromLocalFile(dir); KJob *delDemoJob = KIO::del(demoUrl); KJobWidgets::setWindow(delDemoJob, MainWindow::Window::theMainWindow()); delDemoJob->exec(); } diff --git a/Utilities/DescriptionUtil.cpp b/Utilities/DescriptionUtil.cpp index bc5c1c23..0d00db79 100644 --- a/Utilities/DescriptionUtil.cpp +++ b/Utilities/DescriptionUtil.cpp @@ -1,400 +1,401 @@ -/* 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. + 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 . */ // Local includes #include "DescriptionUtil.h" #include "DB/CategoryCollection.h" #include "DB/ImageDB.h" #include "Exif/Info.h" #include "Logging.h" #include "Settings/SettingsData.h" // KDE includes #include // Qt includes #include #include #include namespace { const QLatin1String LINE_BREAK("
"); } /** * Add a line label + info text to the result text if info is not empty. * If the result already contains something, a HTML newline is added first. * To be used in createInfoText(). */ static void AddNonEmptyInfo(const QString &label, const QString &infoText, QString *result) { if (infoText.isEmpty()) { return; } if (!result->isEmpty()) { *result += LINE_BREAK; } result->append(label).append(infoText); } /** * Given an ImageInfoPtr this function will create an HTML blob about the * image. The blob is used in the viewer and in the tool tip box from the * thumbnail view. * * As the HTML text is created, the parameter linkMap is filled with * information about hyperlinks. The map maps from an index to a pair of * (categoryName, categoryItem). This linkMap is used when the user selects * one of the hyberlinks. */ QString Utilities::createInfoText(DB::ImageInfoPtr info, QMap> *linkMap) { Q_ASSERT(info); QString result; if (Settings::SettingsData::instance()->showFilename()) { AddNonEmptyInfo(i18n("File Name: "), info->fileName().relative(), &result); } if (Settings::SettingsData::instance()->showDate()) { QString dateString = info->date().toString(Settings::SettingsData::instance()->showTime() ? true : false); dateString.append(timeAgo(info)); AddNonEmptyInfo(i18n("Date: "), dateString, &result); } /* XXX */ if (Settings::SettingsData::instance()->showImageSize() && info->mediaType() == DB::Image) { const QSize imageSize = info->size(); // Do not add -1 x -1 text if (imageSize.width() >= 0 && imageSize.height() >= 0) { const double megapix = imageSize.width() * imageSize.height() / 1000000.0; QString infoText = i18nc("width x height", "%1x%2", QString::number(imageSize.width()), QString::number(imageSize.height())); if (megapix > 0.05) { infoText += i18nc("short for: x megapixels", " (%1MP)", QString::number(megapix, 'f', 1)); } const double aspect = (double)imageSize.width() / (double)imageSize.height(); // 0.995 - 1.005 can still be considered quadratic if (aspect > 1.005) infoText += i18nc("aspect ratio", " (%1:1)", QLocale::system().toString(aspect, 'f', 2)); else if (aspect >= 0.995) infoText += i18nc("aspect ratio", " (1:1)"); else infoText += i18nc("aspect ratio", " (1:%1)", QLocale::system().toString(1.0 / aspect, 'f', 2)); AddNonEmptyInfo(i18n("Image Size: "), infoText, &result); } } if (Settings::SettingsData::instance()->showRating()) { if (info->rating() != -1) { if (!result.isEmpty()) result += QString::fromLatin1("
"); QUrl rating; rating.setScheme(QString::fromLatin1("kratingwidget")); // we don't use the host part, but if we don't set it, we can't use port: rating.setHost(QString::fromLatin1("int")); rating.setPort(qMin(qMax(static_cast(0), info->rating()), static_cast(10))); result += QString::fromLatin1("").arg(rating.toString(QUrl::None)); } } QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); int link = 0; - Q_FOREACH (const DB::CategoryPtr category, categories) { + for (const DB::CategoryPtr category : categories) { const QString categoryName = category->name(); if (category->doShow()) { StringSet items = info->itemsOfCategory(categoryName); if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && !Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (categoryName == Settings::SettingsData::instance()->untaggedCategory()) { if (items.contains(Settings::SettingsData::instance()->untaggedTag())) { items.remove(Settings::SettingsData::instance()->untaggedTag()); } } } if (!items.empty()) { QString title = QString::fromUtf8("%1: ").arg(category->name()); QString infoText; bool first = true; - Q_FOREACH (const QString &item, items) { + for (const QString &item : items) { if (first) first = false; else infoText += QString::fromLatin1(", "); if (linkMap) { ++link; (*linkMap)[link] = QPair(categoryName, item); infoText += QString::fromLatin1("%2").arg(link).arg(item); infoText += formatAge(category, item, info); } else infoText += item; } AddNonEmptyInfo(title, infoText, &result); } } } if (Settings::SettingsData::instance()->showLabel()) { AddNonEmptyInfo(i18n("Label: "), info->label(), &result); } if (Settings::SettingsData::instance()->showDescription() && !info->description().trimmed().isEmpty()) { AddNonEmptyInfo(i18n("Description: "), info->description(), &result); } QString exifText; if (Settings::SettingsData::instance()->showEXIF()) { typedef QMap ExifMap; typedef ExifMap::const_iterator ExifMapIterator; ExifMap exifMap = Exif::Info::instance()->infoForViewer(info->fileName(), Settings::SettingsData::instance()->iptcCharset()); for (ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt) { if (exifIt.key().startsWith(QString::fromLatin1("Exif."))) for (QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt) { QString exifName = exifIt.key().split(QChar::fromLatin1('.')).last(); AddNonEmptyInfo(QString::fromLatin1("%1: ").arg(exifName), *valuesIt, &exifText); } } QString iptcText; for (ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt) { if (!exifIt.key().startsWith(QString::fromLatin1("Exif."))) for (QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt) { QString iptcName = exifIt.key().split(QChar::fromLatin1('.')).last(); AddNonEmptyInfo(QString::fromLatin1("%1: ").arg(iptcName), *valuesIt, &iptcText); } } if (!iptcText.isEmpty()) { if (exifText.isEmpty()) exifText = iptcText; else exifText += QString::fromLatin1("
") + iptcText; } } if (!result.isEmpty() && !exifText.isEmpty()) result += QString::fromLatin1("
"); result += exifText; return result; } namespace { enum class TimeUnit { /** Denotes a negative age. */ Invalid, Days, Months, Years }; class AgeSpec { public: /** * @brief The I18nContext enum determines how an age is displayed. */ enum class I18nContext { /// For birthdays, e.g. "Jesper was 30 years in this image". Birthday, /// For ages of events, e.g. "This image was taken 30 years ago". Anniversary }; int age; ///< The number of \c units, e.g. the "5" in "5 days" TimeUnit unit; AgeSpec(); AgeSpec(int age, TimeUnit unit); /** * @brief format * @param context the context where the formatted age is used. * @return a localized string describing the time range. */ QString format(I18nContext context) const; /** * @brief isValid * @return \c true, if the AgeSpec contains a valid age that is not negative. \c false otherwise. */ bool isValid() const; bool operator==(const AgeSpec &other) const; }; AgeSpec::AgeSpec() : age(70) , unit(TimeUnit::Invalid) { } AgeSpec::AgeSpec(int age, TimeUnit unit) : age(age) , unit(unit) { } QString AgeSpec::format(I18nContext context) const { switch (unit) { case TimeUnit::Invalid: return {}; case TimeUnit::Days: if (context == I18nContext::Birthday) return i18ncp("As in 'The baby is 1 day old'", "1 day", "%1 days", age); else return i18ncp("As in 'This happened 1 day ago'", "1 day ago", "%1 days ago", age); case TimeUnit::Months: if (context == I18nContext::Birthday) return i18ncp("As in 'The baby is 1 month old'", "1 month", "%1 months", age); else return i18ncp("As in 'This happened 1 month ago'", "1 month ago", "%1 months ago", age); case TimeUnit::Years: if (context == I18nContext::Birthday) return i18ncp("As in 'The baby is 1 year old'", "1 year", "%1 years", age); else return i18ncp("As in 'This happened 1 year ago'", "1 year ago", "%1 years ago", age); } Q_ASSERT(false); return {}; } bool AgeSpec::isValid() const { return unit != TimeUnit::Invalid; } bool AgeSpec::operator==(const AgeSpec &other) const { return (age == other.age && unit == other.unit); } /** * @brief dateDifference computes the difference between two dates with an appropriate unit. * It can be used to generate human readable date differences, * e.g. "6 months" instead of "0.5 years". * * @param priorDate * @param laterDate * @return a DateSpec with appropriate scale. */ AgeSpec dateDifference(const QDate &priorDate, const QDate &laterDate) { const int priorDay = priorDate.day(); const int laterDay = laterDate.day(); const int priorMonth = priorDate.month(); const int laterMonth = laterDate.month(); const int priorYear = priorDate.year(); const int laterYear = laterDate.year(); // Image before birth const int days = priorDate.daysTo(laterDate); if (days < 0) return {}; if (days < 31) return { days, TimeUnit::Days }; int months = (laterYear - priorYear) * 12; months += (laterMonth - priorMonth); months += (laterDay >= priorDay) ? 0 : -1; if (months < 24) return { months, TimeUnit::Months }; else return { months / 12, TimeUnit::Years }; } #ifdef TEST_DATEDIFF void testDateDifference() { using namespace Utilities; Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 7, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("0 days")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 10)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("30 days")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("1 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 8, 12)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("1 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 9, 10)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("1 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1971, 9, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("2 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 10)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("10 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("11 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 6, 12)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("11 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 10)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("11 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("12 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 7, 12)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("12 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1972, 12, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("17 month")); Q_ASSERT(dateDifference(QDate(1971, 7, 11), QDate(1973, 7, 11)).format(AgeSpec::I18nContext::Birthday) == QString::fromLatin1("2 years")); qDebug() << "Tested dateDifference without problems."; } #endif } QString Utilities::formatAge(DB::CategoryPtr category, const QString &item, DB::ImageInfoPtr info) { #ifdef TEST_DATEDIFF testDateDifference(); // I wish I could get my act together to set up a test suite. #endif const QDate birthDate = category->birthDate(item); const QDate start = info->date().start().date(); const QDate end = info->date().end().date(); if (birthDate.isNull() || !info->date().isValid()) return {}; const AgeSpec minAge = dateDifference(birthDate, start); const AgeSpec maxAge = dateDifference(birthDate, end); if (minAge == maxAge) return i18n(" (%1)", minAge.format(AgeSpec::I18nContext::Birthday)); else if (!minAge.isValid()) return i18n(" (< %1)", maxAge.format(AgeSpec::I18nContext::Birthday)); else { if (minAge.unit == maxAge.unit) return i18nc("E.g. ' (1-2 years)'", " (%1-%2)", minAge.age, maxAge.format(AgeSpec::I18nContext::Birthday)); else return i18nc("E.g. ' (7 months-1 year)'", " (%1-%2)", minAge.format(AgeSpec::I18nContext::Birthday), maxAge.format(AgeSpec::I18nContext::Birthday)); } } QString Utilities::timeAgo(const DB::ImageInfoPtr info) { const QDate startDate = info->date().start().date(); const QDate endDate = info->date().end().date(); const QDate today = QDate::currentDate(); if (!info->date().isValid()) return {}; const AgeSpec minTimeAgo = dateDifference(endDate, today); const AgeSpec maxTimeAgo = dateDifference(startDate, today); if (!minTimeAgo.isValid()) { return {}; } if (minTimeAgo == maxTimeAgo) { return i18n(" (%1)", minTimeAgo.format(AgeSpec::I18nContext::Anniversary)); } else { if (minTimeAgo.unit == maxTimeAgo.unit) return i18nc("E.g. ' (1-2 years ago)'", " (%1-%2)", minTimeAgo.age, maxTimeAgo.format(AgeSpec::I18nContext::Anniversary)); else return i18nc("E.g. '(7 months ago-1 year ago)'", " (%1-%2)", minTimeAgo.format(AgeSpec::I18nContext::Anniversary), maxTimeAgo.format(AgeSpec::I18nContext::Anniversary)); } } diff --git a/Utilities/List.cpp b/Utilities/List.cpp index 837a3946..a6677271 100644 --- a/Utilities/List.cpp +++ b/Utilities/List.cpp @@ -1,103 +1,103 @@ -/* - Copyright (C) 2006-2010 Tuomas Suutari - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program (see the file COPYING); if not, write to the - Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, - MA 02110-1301 USA. +/* Copyright (C) 2006-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 "List.h" #include #include #include #include #include #include // std::swap #include // rand template QList Utilities::mergeListsUniqly(const QList &l1, const QList &l2) { QList r = l1; - Q_FOREACH (const T &x, l2) + for (const T &x : l2) if (!r.contains(x)) r.append(x); return r; } namespace { template class AutoDeletedArray { public: AutoDeletedArray(uint size) : m_ptr(new T[size]) { } operator T *() const { return m_ptr; } ~AutoDeletedArray() { delete[] m_ptr; } private: T *m_ptr; }; } template QList Utilities::shuffleList(const QList &list) { static bool init = false; if (!init) { QTime midnight(0, 0, 0); srand(midnight.secsTo(QTime::currentTime())); init = true; } // Take pointers from input list to an array for shuffling uint N = list.size(); AutoDeletedArray deck(N); const T **p = deck; for (typename QList::const_iterator i = list.begin(); i != list.end(); ++i) { *p = &(*i); ++p; } // Shuffle the array of pointers for (uint i = 0; i < N; i++) { uint r = i + static_cast(static_cast(N - i) * rand() / static_cast(RAND_MAX)); std::swap(deck[r], deck[i]); } // Create new list from the array QList result; const T **const onePastLast = deck + N; for (p = deck; p != onePastLast; ++p) result.push_back(**p); return result; } #define INSTANTIATE_MERGELISTSUNIQLY(T) \ template QList Utilities::mergeListsUniqly(const QList &l1, const QList &l2) #define INSTANTIATE_SHUFFLELIST(T) \ template QList Utilities::shuffleList(const QList &list) INSTANTIATE_MERGELISTSUNIQLY(DB::RawId); INSTANTIATE_MERGELISTSUNIQLY(QString); INSTANTIATE_SHUFFLELIST(DB::FileName); // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ImageDisplay.cpp b/Viewer/ImageDisplay.cpp index 8f4f9cec..cf8ca829 100644 --- a/Viewer/ImageDisplay.cpp +++ b/Viewer/ImageDisplay.cpp @@ -1,757 +1,758 @@ -/* Copyright (C) 2003-2018 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 "ImageDisplay.h" #include "Logging.h" #include "ViewHandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include /** Area displaying the actual image in the viewer. The purpose of this class is to display the actual image in the viewer. This involves controlling zooming and drawing on the images. This class is quite complicated as it had to both be fast and memory efficient. The following are dead end tried: 1) Initially QPainter::setWindow was used for zooming the images, but this had the effect that if you zoom to 100x100 from a 2300x1700 image on a 800x600 display, then Qt would internally create a pixmap with the size (2300/100)*800, (1700/100)*600, which takes up 1.4Gb of memory! 2) I tried doing all scaling and cropping using QPixmap's as that would allow me to keep all transformations on the X Server site (making resizing fast - or I beleived so). Unfortunately it showed up that this was much slower than doing it using QImage, and the result was thus that the looking at a series of images was slow. The process is as follows: - The image loaded from disk is rotated and stored in _loadedImage. Initially this image is as large as the view, until the user starts zooming, at which time the image is reloaded to the size as it is on disk. - Then _loadedImage is cropped and scaled to _croppedAndScaledImg. This image is the size of the display. Resizing the window thus needs to redo step. - Finally in paintEvent _croppedAndScaledImg is drawn to the screen. The above might very likely be simplified. Back in the old days it needed to be that complex to allow drawing on images. To propagate the cache, we need to know which direction the images are viewed in, which is the job of the instance variable _forward. */ Viewer::ImageDisplay::ImageDisplay(QWidget *parent) : AbstractDisplay(parent) , m_reloadImageInProgress(false) , m_forward(true) , m_curIndex(0) , m_busy(false) , m_cursorHiding(true) { m_viewHandler = new ViewHandler(this); setMouseTracking(true); m_cursorTimer = new QTimer(this); m_cursorTimer->setSingleShot(true); connect(m_cursorTimer, &QTimer::timeout, this, &ImageDisplay::hideCursor); showCursor(); } /** * If mouse cursor hiding is enabled, hide the cursor right now */ void Viewer::ImageDisplay::hideCursor() { if (m_cursorHiding) setCursor(Qt::BlankCursor); } /** * If mouse cursor hiding is enabled, show normal cursor and start a timer that will hide it later */ void Viewer::ImageDisplay::showCursor() { if (m_cursorHiding) { unsetCursor(); m_cursorTimer->start(1500); } } /** * Prevent hideCursor() and showCursor() from altering cursor state */ void Viewer::ImageDisplay::disableCursorHiding() { m_cursorHiding = false; } /** * Enable automatic mouse cursor hiding */ void Viewer::ImageDisplay::enableCursorHiding() { m_cursorHiding = true; } void Viewer::ImageDisplay::mousePressEvent(QMouseEvent *event) { // disable cursor hiding till button release disableCursorHiding(); QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers()); double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size()); bool block = m_viewHandler->mousePressEvent(&e, event->pos(), ratio); if (!block) QWidget::mousePressEvent(event); update(); } void Viewer::ImageDisplay::mouseMoveEvent(QMouseEvent *event) { // just reset the timer showCursor(); QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers()); double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size()); bool block = m_viewHandler->mouseMoveEvent(&e, event->pos(), ratio); if (!block) QWidget::mouseMoveEvent(event); update(); } void Viewer::ImageDisplay::mouseReleaseEvent(QMouseEvent *event) { // enable cursor hiding and reset timer enableCursorHiding(); showCursor(); m_cache.remove(m_curIndex); QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers()); double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size()); bool block = m_viewHandler->mouseReleaseEvent(&e, event->pos(), ratio); if (!block) { QWidget::mouseReleaseEvent(event); } emit possibleChange(); update(); } bool Viewer::ImageDisplay::setImage(DB::ImageInfoPtr info, bool forward) { qCDebug(ViewerLog) << "setImage(" << info->fileName().relative() << "," << forward << ")"; m_info = info; m_loadedImage = QImage(); // Find the index of the current image m_curIndex = 0; - Q_FOREACH (const DB::FileName &filename, m_imageList) { + for (const DB::FileName &filename : m_imageList) { if (filename == info->fileName()) break; ++m_curIndex; } if (m_cache.contains(m_curIndex) && m_cache[m_curIndex].angle == info->angle()) { const ViewPreloadInfo &found = m_cache[m_curIndex]; m_loadedImage = found.img; updateZoomPoints(Settings::SettingsData::instance()->viewerStandardSize(), found.img.size()); cropAndScale(); info->setSize(found.size); emit imageReady(); } else { requestImage(info, true); busy(); } m_forward = forward; updatePreload(); return true; } void Viewer::ImageDisplay::resizeEvent(QResizeEvent *event) { ImageManager::AsyncLoader::instance()->stop(this, ImageManager::StopOnlyNonPriorityLoads); m_cache.clear(); if (m_info) { cropAndScale(); if (event->size().width() > 1.5 * this->m_loadedImage.size().width() || event->size().height() > 1.5 * this->m_loadedImage.size().height()) potentiallyLoadFullSize(); // Only do if we scale much bigger. } updatePreload(); } void Viewer::ImageDisplay::paintEvent(QPaintEvent *) { int x = (width() - m_croppedAndScaledImg.width()) / 2; int y = (height() - m_croppedAndScaledImg.height()) / 2; QPainter painter(this); painter.fillRect(0, 0, width(), height(), Qt::black); painter.drawImage(x, y, m_croppedAndScaledImg); } QPoint Viewer::ImageDisplay::offset(int logicalWidth, int logicalHeight, int physicalWidth, int physicalHeight, double *ratio) { double rat = sizeRatio(QSize(logicalWidth, logicalHeight), QSize(physicalWidth, physicalHeight)); int ox = (int)(physicalWidth - logicalWidth * rat) / 2; int oy = (int)(physicalHeight - logicalHeight * rat) / 2; if (ratio) *ratio = rat; return QPoint(ox, oy); } void Viewer::ImageDisplay::zoom(QPoint p1, QPoint p2) { qCDebug(ViewerLog, "zoom(%d,%d, %d,%d)", p1.x(), p1.y(), p2.x(), p2.y()); m_cache.remove(m_curIndex); normalize(p1, p2); double ratio; QPoint off = offset((p2 - p1).x(), (p2 - p1).y(), width(), height(), &ratio); off = off / ratio; p1.setX(p1.x() - off.x()); p1.setY(p1.y() - off.y()); p2.setX(p2.x() + off.x()); p2.setY(p2.y() + off.y()); m_zStart = p1; m_zEnd = p2; potentiallyLoadFullSize(); cropAndScale(); } QPoint Viewer::ImageDisplay::mapPos(QPoint p) { QPoint off = offset(qAbs(m_zEnd.x() - m_zStart.x()), qAbs(m_zEnd.y() - m_zStart.y()), width(), height(), 0); p -= off; int x = (int)(m_zStart.x() + (m_zEnd.x() - m_zStart.x()) * ((double)p.x() / (width() - 2 * off.x()))); int y = (int)(m_zStart.y() + (m_zEnd.y() - m_zStart.y()) * ((double)p.y() / (height() - 2 * off.y()))); return QPoint(x, y); } void Viewer::ImageDisplay::xformPainter(QPainter *p) { QPoint off = offset(qAbs(m_zEnd.x() - m_zStart.x()), qAbs(m_zEnd.y() - m_zStart.y()), width(), height(), 0); double s = (width() - 2 * off.x()) / qAbs((double)m_zEnd.x() - m_zStart.x()); p->scale(s, s); p->translate(-m_zStart.x(), -m_zStart.y()); } void Viewer::ImageDisplay::zoomIn() { qCDebug(ViewerLog, "zoomIn()"); QPoint size = (m_zEnd - m_zStart); QPoint p1 = m_zStart + size * (0.2 / 2); QPoint p2 = m_zEnd - size * (0.2 / 2); zoom(p1, p2); } void Viewer::ImageDisplay::zoomOut() { qCDebug(ViewerLog, "zoomOut()"); QPoint size = (m_zEnd - m_zStart); //Bug 150971, Qt tries to render bigger and bigger images (10000x10000), hence running out of memory. if ((size.x() * size.y() > 25 * 1024 * 1024)) return; QPoint p1 = m_zStart - size * (0.25 / 2); QPoint p2 = m_zEnd + size * (0.25 / 2); zoom(p1, p2); } void Viewer::ImageDisplay::zoomFull() { qCDebug(ViewerLog, "zoomFull()"); m_zStart = QPoint(0, 0); m_zEnd = QPoint(m_loadedImage.width(), m_loadedImage.height()); zoom(QPoint(0, 0), QPoint(m_loadedImage.width(), m_loadedImage.height())); } void Viewer::ImageDisplay::normalize(QPoint &p1, QPoint &p2) { int minx = qMin(p1.x(), p2.x()); int miny = qMin(p1.y(), p2.y()); int maxx = qMax(p1.x(), p2.x()); int maxy = qMax(p1.y(), p2.y()); p1 = QPoint(minx, miny); p2 = QPoint(maxx, maxy); } void Viewer::ImageDisplay::pan(const QPoint &point) { m_zStart += point; m_zEnd += point; cropAndScale(); } void Viewer::ImageDisplay::cropAndScale() { if (m_loadedImage.isNull()) { return; } if (m_zStart != QPoint(0, 0) || m_zEnd != QPoint(m_loadedImage.width(), m_loadedImage.height())) { qCDebug(ViewerLog) << "cropAndScale(): using cropped image" << m_zStart << "-" << m_zEnd; m_croppedAndScaledImg = m_loadedImage.copy(m_zStart.x(), m_zStart.y(), m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()); } else { qCDebug(ViewerLog) << "cropAndScale(): using full image."; m_croppedAndScaledImg = m_loadedImage; } updateZoomCaption(); if (!m_croppedAndScaledImg.isNull()) // I don't know how this can happen, but it seems not to be dangerous. { qCDebug(ViewerLog) << "cropAndScale(): scaling image to" << width() << "x" << height(); m_croppedAndScaledImg = m_croppedAndScaledImg.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); } else { qCDebug(ViewerLog) << "cropAndScale(): image is null."; } update(); emit viewGeometryChanged(m_croppedAndScaledImg.size(), QRect(m_zStart, m_zEnd), sizeRatio(m_loadedImage.size(), m_info->size())); } void Viewer::ImageDisplay::filterNone() { cropAndScale(); update(); } bool Viewer::ImageDisplay::filterMono() { m_croppedAndScaledImg = m_croppedAndScaledImg.convertToFormat(m_croppedAndScaledImg.Format_Mono); update(); return true; } // I can't believe there isn't a standard conversion for this??? -- WH bool Viewer::ImageDisplay::filterBW() { if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error(this, i18n("Insufficient color depth for this filter")); return false; } for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int gray = qGray(pixel); int alpha = qAlpha(pixel); m_croppedAndScaledImg.setPixel(x, y, qRgba(gray, gray, gray, alpha)); } } update(); return true; } bool Viewer::ImageDisplay::filterContrastStretch() { int redMin, redMax, greenMin, greenMax, blueMin, blueMax; redMin = greenMin = blueMin = 255; redMax = greenMax = blueMax = 0; if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error(this, i18n("Insufficient color depth for this filter")); return false; } // Look for minimum and maximum intensities within each color channel for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int red = qRed(pixel); int green = qGreen(pixel); int blue = qBlue(pixel); redMin = redMin < red ? redMin : red; redMax = redMax > red ? redMax : red; greenMin = greenMin < green ? greenMin : green; greenMax = greenMax > green ? greenMax : green; blueMin = blueMin < blue ? blueMin : blue; blueMax = blueMax > blue ? blueMax : blue; } } // Calculate factor for stretching each color intensity throughout the // whole range float redFactor, greenFactor, blueFactor; redFactor = ((float)(255) / (float)(redMax - redMin)); greenFactor = ((float)(255) / (float)(greenMax - greenMin)); blueFactor = ((float)(255) / (float)(blueMax - blueMin)); // Perform the contrast stretching for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int red = qRed(pixel); int green = qGreen(pixel); int blue = qBlue(pixel); int alpha = qAlpha(pixel); red = (red - redMin) * redFactor; red = red < 255 ? red : 255; red = red > 0 ? red : 0; green = (green - greenMin) * greenFactor; green = green < 255 ? green : 255; green = green > 0 ? green : 0; blue = (blue - blueMin) * blueFactor; blue = blue < 255 ? blue : 255; blue = blue > 0 ? blue : 0; m_croppedAndScaledImg.setPixel(x, y, qRgba(red, green, blue, alpha)); } } update(); return true; } bool Viewer::ImageDisplay::filterHistogramEqualization() { int width, height; float R_histogram[256]; float G_histogram[256]; float B_histogram[256]; float d; if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error(this, i18n("Insufficient color depth for this filter")); return false; } memset(R_histogram, 0, sizeof(R_histogram)); memset(G_histogram, 0, sizeof(G_histogram)); memset(B_histogram, 0, sizeof(B_histogram)); width = m_croppedAndScaledImg.width(); height = m_croppedAndScaledImg.height(); d = 1.0 / width / height; // Populate histogram for each color channel for (int y = 0; y < height; ++y) { for (int x = 1; x < width; ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); R_histogram[qRed(pixel)] += d; G_histogram[qGreen(pixel)] += d; B_histogram[qBlue(pixel)] += d; } } // Transfer histogram table to cumulative distribution table float R_sum = 0.0; float G_sum = 0.0; float B_sum = 0.0; for (int i = 0; i < 256; ++i) { R_sum += R_histogram[i]; G_sum += G_histogram[i]; B_sum += B_histogram[i]; R_histogram[i] = R_sum * 255 + 0.5; G_histogram[i] = G_sum * 255 + 0.5; B_histogram[i] = B_sum * 255 + 0.5; } // Equalize the image for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); m_croppedAndScaledImg.setPixel( x, y, qRgba(R_histogram[qRed(pixel)], G_histogram[qGreen(pixel)], B_histogram[qBlue(pixel)], qAlpha(pixel))); } } update(); return true; } void Viewer::ImageDisplay::updateZoomCaption() { const QSize imgSize = m_loadedImage.size(); // similar to sizeRatio(), but we take the _highest_ factor. double ratio = ((double)imgSize.width()) / (m_zEnd.x() - m_zStart.x()); if (ratio * (m_zEnd.y() - m_zStart.y()) < imgSize.height()) { ratio = ((double)imgSize.height()) / (m_zEnd.y() - m_zStart.y()); } emit setCaptionInfo((ratio > 1.05) ? ki18n("[ zoom x%1 ]").subs(ratio, 0, 'f', 1).toString() : QString()); } QImage Viewer::ImageDisplay::currentViewAsThumbnail() const { if (m_croppedAndScaledImg.isNull()) return QImage(); else return m_croppedAndScaledImg.scaled(512, 512, Qt::KeepAspectRatio, Qt::SmoothTransformation); } bool Viewer::ImageDisplay::isImageZoomed(const Settings::StandardViewSize type, const QSize &imgSize) { if (type == Settings::FullSize) return true; if (type == Settings::NaturalSizeIfFits) return !(imgSize.width() < width() && imgSize.height() < height()); return false; } void Viewer::ImageDisplay::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) { const DB::FileName fileName = request->databaseFileName(); const QSize imgSize = request->size(); const QSize fullSize = request->fullSize(); const int angle = request->angle(); const bool loadedOK = request->loadedOK(); if (loadedOK && fileName == m_info->fileName()) { if (fullSize.isValid() && !m_info->size().isValid()) m_info->setSize(fullSize); if (!m_reloadImageInProgress) updateZoomPoints(Settings::SettingsData::instance()->viewerStandardSize(), image.size()); else { // See documentation for zoomPixelForPixel for details. // We just loaded a likely much larger image, so the zoom points // need to be scaled. Notice m_loadedImage is the size of the // old image. // when using raw images, the decoded image may be a preview // and have a size different from m_info->size(). Therefore, use fullSize here: double ratio = sizeRatio(m_loadedImage.size(), fullSize); qCDebug(ViewerLog) << "Old size:" << m_loadedImage.size() << "; new size:" << m_info->size(); qCDebug(ViewerLog) << "Req size:" << imgSize << "fullsize:" << fullSize; qCDebug(ViewerLog) << "pixmapLoaded(): Zoom region was" << m_zStart << "-" << m_zEnd; m_zStart *= ratio; m_zEnd *= ratio; qCDebug(ViewerLog) << "pixmapLoaded(): Zoom region changed to" << m_zStart << "-" << m_zEnd; m_reloadImageInProgress = false; } m_loadedImage = image; cropAndScale(); emit imageReady(); } else { if (imgSize != size()) return; // Might be an old preload version, or a loaded version that never made it in time ViewPreloadInfo info(image, fullSize, angle); m_cache.insert(indexOf(fileName), info); updatePreload(); } unbusy(); emit possibleChange(); } void Viewer::ImageDisplay::setImageList(const DB::FileNameList &list) { m_imageList = list; m_cache.clear(); } void Viewer::ImageDisplay::updatePreload() { // cacheSize: number of images at current window dimensions (at 4 byte per pixel) const int cacheSize = (int)((long long)(Settings::SettingsData::instance()->viewerCacheSize() * 1024LL * 1024LL) / (width() * height() * 4)); bool cacheFull = (m_cache.count() > cacheSize); int incr = (m_forward ? 1 : -1); int nextOnesInCache = 0; // Iterate from the current image in the direction of the viewing for (int i = m_curIndex + incr; cacheSize; i += incr) { if (m_forward ? (i >= (int)m_imageList.count()) : (i < 0)) break; DB::ImageInfoPtr info = DB::ImageDB::instance()->info(m_imageList[i]); if (!info) { qCWarning(ViewerLog, "Info was null for index %d!", i); return; } if (m_cache.contains(i)) { nextOnesInCache++; if (nextOnesInCache >= ceil(cacheSize / 2.0) && cacheFull) { // Ok enough images in cache return; } } else { requestImage(info); if (cacheFull) { // The cache was full, we need to delete an item from the cache. // First try to find an item from the direction we came from for (int j = (m_forward ? 0 : m_imageList.count() - 1); j != m_curIndex; j += (m_forward ? 1 : -1)) { if (m_cache.contains(j)) { m_cache.remove(j); return; } } // OK We found no item in the direction we came from (think of home/end keys) for (int j = (m_forward ? m_imageList.count() - 1 : 0); j != m_curIndex; j += (m_forward ? -1 : 1)) { if (m_cache.contains(j)) { m_cache.remove(j); return; } } Q_ASSERT(false); // We should never get here. } return; } } } int Viewer::ImageDisplay::indexOf(const DB::FileName &fileName) { int i = 0; - Q_FOREACH (const DB::FileName &name, m_imageList) { + for (const DB::FileName &name : m_imageList) { if (name == fileName) break; ++i; } return i; } void Viewer::ImageDisplay::busy() { if (!m_busy) qApp->setOverrideCursor(Qt::WaitCursor); m_busy = true; } void Viewer::ImageDisplay::unbusy() { if (m_busy) qApp->restoreOverrideCursor(); m_busy = false; } void Viewer::ImageDisplay::zoomPixelForPixel() { qCDebug(ViewerLog, "zoomPixelForPixel()"); // This is rather tricky. // We want to zoom to a pixel level for the real image, which we might // or might not have loaded yet. // // First we ask for zoom points as they would look like had we had the // real image loaded now. (We need to ask for them, for the real image, // otherwise we would just zoom to the pixel level of the view size // image) updateZoomPoints(Settings::NaturalSize, m_info->size()); // The points now, however might not match the current visible image - // as this image might be be only view size large. We therefore need // to scale the coordinates. double ratio = sizeRatio(m_loadedImage.size(), m_info->size()); qCDebug(ViewerLog) << "zoomPixelForPixel(): Zoom region was" << m_zStart << "-" << m_zEnd; m_zStart /= ratio; m_zEnd /= ratio; qCDebug(ViewerLog) << "zoomPixelForPixel(): Zoom region changed to" << m_zStart << "-" << m_zEnd; cropAndScale(); potentiallyLoadFullSize(); } void Viewer::ImageDisplay::updateZoomPoints(const Settings::StandardViewSize type, const QSize &imgSize) { const int iw = imgSize.width(); const int ih = imgSize.height(); if (isImageZoomed(type, imgSize)) { m_zStart = QPoint(0, 0); m_zEnd = QPoint(iw, ih); qCDebug(ViewerLog) << "updateZoomPoints(): Zoom region reset to" << m_zStart << "-" << m_zEnd; } else { m_zStart = QPoint(-(width() - iw) / 2, -(height() - ih) / 2); m_zEnd = QPoint(iw + (width() - iw) / 2, ih + (height() - ih) / 2); qCDebug(ViewerLog) << "updateZoomPoints(): Zoom region set to" << m_zStart << "-" << m_zEnd; } } void Viewer::ImageDisplay::potentiallyLoadFullSize() { if (m_info->size() != m_loadedImage.size()) { qCDebug(ViewerLog) << "Loading full size image for " << m_info->fileName().relative(); ImageManager::ImageRequest *request = new ImageManager::ImageRequest(m_info->fileName(), QSize(-1, -1), m_info->angle(), this); request->setPriority(ImageManager::Viewer); ImageManager::AsyncLoader::instance()->load(request); busy(); m_reloadImageInProgress = true; } } /** * return the ratio of the two sizes. That is newSize/baseSize. */ double Viewer::ImageDisplay::sizeRatio(const QSize &baseSize, const QSize &newSize) const { double res = ((double)newSize.width()) / baseSize.width(); if (res * baseSize.height() > newSize.height()) { res = ((double)newSize.height()) / baseSize.height(); } return res; } void Viewer::ImageDisplay::requestImage(const DB::ImageInfoPtr &info, bool priority) { Settings::StandardViewSize viewSize = Settings::SettingsData::instance()->viewerStandardSize(); QSize s = size(); if (viewSize == Settings::NaturalSize) s = QSize(-1, -1); ImageManager::ImageRequest *request = new ImageManager::ImageRequest(info->fileName(), s, info->angle(), this); request->setUpScale(viewSize == Settings::FullSize); request->setPriority(priority ? ImageManager::Viewer : ImageManager::ViewerPreload); ImageManager::AsyncLoader::instance()->load(request); } void Viewer::ImageDisplay::hideEvent(QHideEvent *) { m_viewHandler->hideEvent(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewerWidget.cpp b/Viewer/ViewerWidget.cpp index 9db36cc9..a06b112d 100644 --- a/Viewer/ViewerWidget.cpp +++ b/Viewer/ViewerWidget.cpp @@ -1,1442 +1,1443 @@ -/* 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. + 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 "ViewerWidget.h" #include "CategoryImageConfig.h" #include "ImageDisplay.h" #include "InfoBox.h" #include "SpeedDisplay.h" #include "TaggedArea.h" #include "TextDisplay.h" #include "VideoDisplay.h" #include "VideoShooter.h" #include "VisibleOptionsMenu.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 Viewer::ViewerWidget *Viewer::ViewerWidget::s_latest = nullptr; Viewer::ViewerWidget *Viewer::ViewerWidget::latest() { return s_latest; } // Notice the parent is zero to allow other windows to come on top of it. Viewer::ViewerWidget::ViewerWidget(UsageType type, QMap> *macroStore) : QStackedWidget(nullptr) , m_current(0) , m_popup(nullptr) , m_showingFullScreen(false) , m_forward(true) , m_isRunningSlideShow(false) , m_videoPlayerStoppedManually(false) , m_type(type) , m_currentCategory(DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name()) , m_inputMacros(macroStore) , m_myInputMacros(nullptr) { if (type == ViewerWindow) { setWindowFlags(Qt::Window); setAttribute(Qt::WA_DeleteOnClose); s_latest = this; } if (!m_inputMacros) { m_myInputMacros = m_inputMacros = new QMap>; } m_screenSaverCookie = -1; m_currentInputMode = InACategory; m_display = m_imageDisplay = new ImageDisplay(this); addWidget(m_imageDisplay); m_textDisplay = new TextDisplay(this); addWidget(m_textDisplay); createVideoViewer(); connect(m_imageDisplay, &ImageDisplay::possibleChange, this, &ViewerWidget::updateCategoryConfig); connect(m_imageDisplay, &ImageDisplay::imageReady, this, &ViewerWidget::updateInfoBox); connect(m_imageDisplay, &ImageDisplay::setCaptionInfo, this, &ViewerWidget::setCaptionWithDetail); connect(m_imageDisplay, &ImageDisplay::viewGeometryChanged, this, &ViewerWidget::remapAreas); // This must not be added to the layout, as it is standing on top of // the ImageDisplay m_infoBox = new InfoBox(this); m_infoBox->hide(); setupContextMenu(); m_slideShowTimer = new QTimer(this); m_slideShowTimer->setSingleShot(true); m_slideShowPause = Settings::SettingsData::instance()->slideShowInterval() * 1000; connect(m_slideShowTimer, &QTimer::timeout, this, &ViewerWidget::slotSlideShowNextFromTimer); m_speedDisplay = new SpeedDisplay(this); m_speedDisplay->hide(); setFocusPolicy(Qt::StrongFocus); QTimer::singleShot(2000, this, SLOT(test())); } void Viewer::ViewerWidget::setupContextMenu() { m_popup = new QMenu(this); m_actions = new KActionCollection(this); createSlideShowMenu(); createZoomMenu(); createRotateMenu(); createSkipMenu(); createShowContextMenu(); createInvokeExternalMenu(); createVideoMenu(); createCategoryImageMenu(); createFilterMenu(); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-edit-image-properties"), this, SLOT(editImage())); action->setText(i18nc("@action:inmenu", "Annotate...")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_1); m_popup->addAction(action); m_setStackHead = m_actions->addAction(QString::fromLatin1("viewer-set-stack-head"), this, SLOT(slotSetStackHead())); m_setStackHead->setText(i18nc("@action:inmenu", "Set as First Image in Stack")); m_actions->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4); m_popup->addAction(m_setStackHead); m_showExifViewer = m_actions->addAction(QString::fromLatin1("viewer-show-exif-viewer"), this, SLOT(showExifViewer())); m_showExifViewer->setText(i18nc("@action:inmenu", "Show Exif Viewer")); m_popup->addAction(m_showExifViewer); m_copyTo = m_actions->addAction(QString::fromLatin1("viewer-copy-to"), this, SLOT(copyTo())); m_copyTo->setText(i18nc("@action:inmenu", "Copy Image to...")); m_actions->setDefaultShortcut(m_copyTo, Qt::Key_F7); m_popup->addAction(m_copyTo); if (m_type == ViewerWindow) { action = m_actions->addAction(QString::fromLatin1("viewer-close"), this, SLOT(close())); action->setText(i18nc("@action:inmenu", "Close")); action->setShortcut(Qt::Key_Escape); m_actions->setShortcutsConfigurable(action, false); } m_popup->addAction(action); m_actions->readSettings(); - Q_FOREACH (QAction *action, m_actions->actions()) { + for (QAction *action : m_actions->actions()) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } } void Viewer::ViewerWidget::createShowContextMenu() { VisibleOptionsMenu *menu = new VisibleOptionsMenu(this, m_actions); connect(menu, &VisibleOptionsMenu::visibleOptionsChanged, this, &ViewerWidget::updateInfoBox); m_popup->addMenu(menu); } void Viewer::ViewerWidget::inhibitScreenSaver(bool inhibit) { QDBusMessage message; if (inhibit) { message = QDBusMessage::createMethodCall(QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("Inhibit")); message << QString(QString::fromLatin1("KPhotoAlbum")); message << QString(QString::fromLatin1("Giving a slideshow")); QDBusMessage reply = QDBusConnection::sessionBus().call(message); if (reply.type() == QDBusMessage::ReplyMessage) m_screenSaverCookie = reply.arguments().first().toInt(); } else { if (m_screenSaverCookie != -1) { message = QDBusMessage::createMethodCall(QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("UnInhibit")); message << (uint)m_screenSaverCookie; QDBusConnection::sessionBus().send(message); m_screenSaverCookie = -1; } } } void Viewer::ViewerWidget::createInvokeExternalMenu() { m_externalPopup = new MainWindow::ExternalPopup(m_popup); m_popup->addMenu(m_externalPopup); connect(m_externalPopup, &MainWindow::ExternalPopup::aboutToShow, this, &ViewerWidget::populateExternalPopup); } void Viewer::ViewerWidget::createRotateMenu() { m_rotateMenu = new QMenu(m_popup); m_rotateMenu->setTitle(i18nc("@title:inmenu", "Rotate")); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-rotate90"), this, SLOT(rotate90())); action->setText(i18nc("@action:inmenu", "Rotate clockwise")); action->setShortcut(Qt::Key_9); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-rotate180"), this, SLOT(rotate180())); action->setText(i18nc("@action:inmenu", "Flip Over")); action->setShortcut(Qt::Key_8); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-rotare270"), this, SLOT(rotate270())); // ^ this is a typo, isn't it?! action->setText(i18nc("@action:inmenu", "Rotate counterclockwise")); action->setShortcut(Qt::Key_7); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); m_popup->addMenu(m_rotateMenu); } void Viewer::ViewerWidget::createSkipMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@title:inmenu As in 'skip 2 images'", "Skip")); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-home"), this, SLOT(showFirst())); action->setText(i18nc("@action:inmenu Go to first image", "First")); action->setShortcut(Qt::Key_Home); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-end"), this, SLOT(showLast())); action->setText(i18nc("@action:inmenu Go to last image", "Last")); action->setShortcut(Qt::Key_End); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next"), this, SLOT(showNext())); action->setText(i18nc("@action:inmenu", "Show Next")); action->setShortcuts(QList() << Qt::Key_PageDown << Qt::Key_Space); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-10"), this, SLOT(showNext10())); action->setText(i18nc("@action:inmenu", "Skip 10 Forward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-100"), this, SLOT(showNext100())); action->setText(i18nc("@action:inmenu", "Skip 100 Forward")); m_actions->setDefaultShortcut(action, Qt::SHIFT + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-1000"), this, SLOT(showNext1000())); action->setText(i18nc("@action:inmenu", "Skip 1000 Forward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev"), this, SLOT(showPrev())); action->setText(i18nc("@action:inmenu", "Show Previous")); action->setShortcuts(QList() << Qt::Key_PageUp << Qt::Key_Backspace); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-10"), this, SLOT(showPrev10())); action->setText(i18nc("@action:inmenu", "Skip 10 Backward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-100"), this, SLOT(showPrev100())); action->setText(i18nc("@action:inmenu", "Skip 100 Backward")); m_actions->setDefaultShortcut(action, Qt::SHIFT + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-1000"), this, SLOT(showPrev1000())); action->setText(i18nc("@action:inmenu", "Skip 1000 Backward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-delete-current"), this, SLOT(deleteCurrent())); action->setText(i18nc("@action:inmenu", "Delete Image")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-remove-current"), this, SLOT(removeCurrent())); action->setText(i18nc("@action:inmenu", "Remove Image from Display List")); action->setShortcut(Qt::Key_Delete); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_popup->addMenu(popup); } void Viewer::ViewerWidget::createZoomMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@action:inmenu", "Zoom")); // PENDING(blackie) Only for image display? QAction *action = m_actions->addAction(QString::fromLatin1("viewer-zoom-in"), this, SLOT(zoomIn())); action->setText(i18nc("@action:inmenu", "Zoom In")); action->setShortcut(Qt::Key_Plus); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-out"), this, SLOT(zoomOut())); action->setText(i18nc("@action:inmenu", "Zoom Out")); action->setShortcut(Qt::Key_Minus); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-full"), this, SLOT(zoomFull())); action->setText(i18nc("@action:inmenu", "Full View")); action->setShortcut(Qt::Key_Period); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-pixel"), this, SLOT(zoomPixelForPixel())); action->setText(i18nc("@action:inmenu", "Pixel for Pixel View")); action->setShortcut(Qt::Key_Equal); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-toggle-fullscreen"), this, SLOT(toggleFullScreen())); action->setText(i18nc("@action:inmenu", "Toggle Full Screen")); action->setShortcuts(QList() << Qt::Key_F11 << Qt::Key_Return); popup->addAction(action); m_popup->addMenu(popup); } void Viewer::ViewerWidget::createSlideShowMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@title:inmenu", "Slideshow")); m_startStopSlideShow = m_actions->addAction(QString::fromLatin1("viewer-start-stop-slideshow"), this, SLOT(slotStartStopSlideShow())); m_startStopSlideShow->setText(i18nc("@action:inmenu", "Run Slideshow")); m_actions->setDefaultShortcut(m_startStopSlideShow, Qt::CTRL + Qt::Key_R); popup->addAction(m_startStopSlideShow); m_slideShowRunFaster = m_actions->addAction(QString::fromLatin1("viewer-run-faster"), this, SLOT(slotSlideShowFaster())); m_slideShowRunFaster->setText(i18nc("@action:inmenu", "Run Faster")); m_actions->setDefaultShortcut(m_slideShowRunFaster, Qt::CTRL + Qt::Key_Plus); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction(m_slideShowRunFaster); m_slideShowRunSlower = m_actions->addAction(QString::fromLatin1("viewer-run-slower"), this, SLOT(slotSlideShowSlower())); m_slideShowRunSlower->setText(i18nc("@action:inmenu", "Run Slower")); m_actions->setDefaultShortcut(m_slideShowRunSlower, Qt::CTRL + Qt::Key_Minus); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction(m_slideShowRunSlower); m_popup->addMenu(popup); } void Viewer::ViewerWidget::load(const DB::FileNameList &list, int index) { m_list = list; m_imageDisplay->setImageList(list); m_current = index; load(); bool on = (list.count() > 1); m_startStopSlideShow->setEnabled(on); m_slideShowRunFaster->setEnabled(on); m_slideShowRunSlower->setEnabled(on); } void Viewer::ViewerWidget::load() { const bool isReadable = QFileInfo(currentInfo()->fileName().absolute()).isReadable(); const bool isVideo = isReadable && Utilities::isVideo(currentInfo()->fileName()); if (isReadable) { if (isVideo) { m_display = m_videoDisplay; } else m_display = m_imageDisplay; } else { m_display = m_textDisplay; m_textDisplay->setText(i18n("File not available")); updateInfoBox(); } setCurrentWidget(m_display); m_infoBox->raise(); m_rotateMenu->setEnabled(!isVideo); m_categoryImagePopup->setEnabled(!isVideo); m_filterMenu->setEnabled(!isVideo); m_showExifViewer->setEnabled(!isVideo); if (m_exifViewer) m_exifViewer->setImage(currentInfo()->fileName()); - Q_FOREACH (QAction *videoAction, m_videoActions) { + for (QAction *videoAction : m_videoActions) { videoAction->setVisible(isVideo); } emit soughtTo(m_list[m_current]); bool ok = m_display->setImage(currentInfo(), m_forward); if (!ok) { close(false); return; } setCaptionWithDetail(QString()); // PENDING(blackie) This needs to be improved, so that it shows the actions only if there are that many images to jump. for (QList::const_iterator it = m_forwardActions.constBegin(); it != m_forwardActions.constEnd(); ++it) (*it)->setEnabled(m_current + 1 < (int)m_list.count()); for (QList::const_iterator it = m_backwardActions.constBegin(); it != m_backwardActions.constEnd(); ++it) (*it)->setEnabled(m_current > 0); m_setStackHead->setEnabled(currentInfo()->isStacked()); if (isVideo) updateCategoryConfig(); if (m_isRunningSlideShow) m_slideShowTimer->start(m_slideShowPause); if (m_display == m_textDisplay) updateInfoBox(); // Add all tagged areas setTaggedAreasFromImage(); } void Viewer::ViewerWidget::setCaptionWithDetail(const QString &detail) { setWindowTitle(i18nc("@title:window %1 is the filename, %2 its detail info", "%1 %2", currentInfo()->fileName().absolute(), detail)); } void Viewer::ViewerWidget::contextMenuEvent(QContextMenuEvent *e) { if (m_videoDisplay) { if (m_videoDisplay->isPaused()) m_playPause->setText(i18nc("@action:inmenu Start video playback", "Play")); else m_playPause->setText(i18nc("@action:inmenu Pause video playback", "Pause")); m_stop->setEnabled(m_videoDisplay->isPlaying()); } m_popup->exec(e->globalPos()); e->setAccepted(true); } void Viewer::ViewerWidget::showNextN(int n) { filterNone(); if (m_display == m_videoDisplay) { m_videoPlayerStoppedManually = true; m_videoDisplay->stop(); } if (m_current + n < (int)m_list.count()) { m_current += n; if (m_current >= (int)m_list.count()) m_current = (int)m_list.count() - 1; m_forward = true; load(); } } void Viewer::ViewerWidget::showNext() { showNextN(1); } void Viewer::ViewerWidget::removeCurrent() { removeOrDeleteCurrent(OnlyRemoveFromViewer); } void Viewer::ViewerWidget::deleteCurrent() { removeOrDeleteCurrent(RemoveImageFromDatabase); } void Viewer::ViewerWidget::removeOrDeleteCurrent(RemoveAction action) { const DB::FileName fileName = m_list[m_current]; if (action == RemoveImageFromDatabase) m_removed.append(fileName); m_list.removeAll(fileName); if (m_list.isEmpty()) close(); if (m_current == m_list.count()) showPrev(); else showNextN(0); } void Viewer::ViewerWidget::showNext10() { showNextN(10); } void Viewer::ViewerWidget::showNext100() { showNextN(100); } void Viewer::ViewerWidget::showNext1000() { showNextN(1000); } void Viewer::ViewerWidget::showPrevN(int n) { if (m_display == m_videoDisplay) m_videoDisplay->stop(); if (m_current > 0) { m_current -= n; if (m_current < 0) m_current = 0; m_forward = false; load(); } } void Viewer::ViewerWidget::showPrev() { showPrevN(1); } void Viewer::ViewerWidget::showPrev10() { showPrevN(10); } void Viewer::ViewerWidget::showPrev100() { showPrevN(100); } void Viewer::ViewerWidget::showPrev1000() { showPrevN(1000); } void Viewer::ViewerWidget::rotate90() { currentInfo()->rotate(90); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::rotate180() { currentInfo()->rotate(180); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::rotate270() { currentInfo()->rotate(270); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::showFirst() { showPrevN(m_list.count()); } void Viewer::ViewerWidget::showLast() { showNextN(m_list.count()); } bool Viewer::ViewerWidget::close(bool alsoDelete) { if (!m_removed.isEmpty()) { MainWindow::DeleteDialog dialog(this); dialog.exec(m_removed); } m_slideShowTimer->stop(); m_isRunningSlideShow = false; return QWidget::close(); if (alsoDelete) deleteLater(); } DB::ImageInfoPtr Viewer::ViewerWidget::currentInfo() const { return DB::ImageDB::instance()->info(m_list[m_current]); // PENDING(blackie) can we postpone this lookup? } void Viewer::ViewerWidget::infoBoxMove() { QPoint p = mapFromGlobal(QCursor::pos()); Settings::Position oldPos = Settings::SettingsData::instance()->infoBoxPosition(); Settings::Position pos = oldPos; int x = m_display->mapFromParent(p).x(); int y = m_display->mapFromParent(p).y(); int w = m_display->width(); int h = m_display->height(); if (x < w / 3) { if (y < h / 3) pos = Settings::TopLeft; else if (y > h * 2 / 3) pos = Settings::BottomLeft; else pos = Settings::Left; } else if (x > w * 2 / 3) { if (y < h / 3) pos = Settings::TopRight; else if (y > h * 2 / 3) pos = Settings::BottomRight; else pos = Settings::Right; } else { if (y < h / 3) pos = Settings::Top; else if (y > h * 2 / 3) pos = Settings::Bottom; } if (pos != oldPos) { Settings::SettingsData::instance()->setInfoBoxPosition(pos); updateInfoBox(); } } void Viewer::ViewerWidget::moveInfoBox() { m_infoBox->setSize(); Settings::Position pos = Settings::SettingsData::instance()->infoBoxPosition(); int lx = m_display->pos().x(); int ly = m_display->pos().y(); int lw = m_display->width(); int lh = m_display->height(); int bw = m_infoBox->width(); int bh = m_infoBox->height(); int bx, by; // x-coordinate if (pos == Settings::TopRight || pos == Settings::BottomRight || pos == Settings::Right) bx = lx + lw - 5 - bw; else if (pos == Settings::TopLeft || pos == Settings::BottomLeft || pos == Settings::Left) bx = lx + 5; else bx = lx + lw / 2 - bw / 2; // Y-coordinate if (pos == Settings::TopLeft || pos == Settings::TopRight || pos == Settings::Top) by = ly + 5; else if (pos == Settings::BottomLeft || pos == Settings::BottomRight || pos == Settings::Bottom) by = ly + lh - 5 - bh; else by = ly + lh / 2 - bh / 2; m_infoBox->move(bx, by); } void Viewer::ViewerWidget::resizeEvent(QResizeEvent *e) { moveInfoBox(); QWidget::resizeEvent(e); } void Viewer::ViewerWidget::updateInfoBox() { QString tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name(); if (currentInfo() || !m_currentInput.isEmpty() || (!m_currentCategory.isEmpty() && m_currentCategory != tokensCategory)) { QMap> map; QString text = Utilities::createInfoText(currentInfo(), &map); QString selecttext = QString::fromLatin1(""); if (m_currentCategory.isEmpty()) { selecttext = i18nc("Basically 'enter a category name'", "Setting Category: ") + m_currentInput; if (m_currentInputList.length() > 0) { selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}"); } } else if ((!m_currentInput.isEmpty() && m_currentCategory != tokensCategory)) { selecttext = i18nc("Basically 'enter a tag name'", "Assigning: ") + m_currentCategory + QString::fromLatin1("/") + m_currentInput; if (m_currentInputList.length() > 0) { selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}"); } } else if (!m_currentInput.isEmpty() && m_currentCategory == tokensCategory) { m_currentInput = QString::fromLatin1(""); } if (!selecttext.isEmpty()) text = selecttext + QString::fromLatin1("
") + text; if (Settings::SettingsData::instance()->showInfoBox() && !text.isNull() && (m_type != InlineViewer)) { m_infoBox->setInfo(text, map); m_infoBox->show(); } else m_infoBox->hide(); moveInfoBox(); } } Viewer::ViewerWidget::~ViewerWidget() { inhibitScreenSaver(false); if (s_latest == this) s_latest = nullptr; if (m_myInputMacros) delete m_myInputMacros; } void Viewer::ViewerWidget::toggleFullScreen() { setShowFullScreen(!m_showingFullScreen); } void Viewer::ViewerWidget::slotStartStopSlideShow() { bool wasRunningSlideShow = m_isRunningSlideShow; m_isRunningSlideShow = !m_isRunningSlideShow && m_list.count() != 1; if (wasRunningSlideShow) { m_startStopSlideShow->setText(i18nc("@action:inmenu", "Run Slideshow")); m_slideShowTimer->stop(); if (m_list.count() != 1) m_speedDisplay->end(); inhibitScreenSaver(false); } else { m_startStopSlideShow->setText(i18nc("@action:inmenu", "Stop Slideshow")); if (currentInfo()->mediaType() != DB::Video) m_slideShowTimer->start(m_slideShowPause); m_speedDisplay->start(); inhibitScreenSaver(true); } } void Viewer::ViewerWidget::slotSlideShowNextFromTimer() { // Load the next images. QTime timer; timer.start(); if (m_display == m_imageDisplay) slotSlideShowNext(); // ensure that there is a few milliseconds pause, so that an end slideshow keypress // can get through immediately, we don't want it to queue up behind a bunch of timer events, // which loaded a number of new images before the slideshow stops int ms = qMax(200, m_slideShowPause - timer.elapsed()); m_slideShowTimer->start(ms); } void Viewer::ViewerWidget::slotSlideShowNext() { m_forward = true; if (m_current + 1 < (int)m_list.count()) m_current++; else m_current = 0; load(); } void Viewer::ViewerWidget::slotSlideShowFaster() { changeSlideShowInterval(-500); } void Viewer::ViewerWidget::slotSlideShowSlower() { changeSlideShowInterval(+500); } void Viewer::ViewerWidget::changeSlideShowInterval(int delta) { if (m_list.count() == 1) return; m_slideShowPause += delta; m_slideShowPause = qMax(m_slideShowPause, 500); m_speedDisplay->display(m_slideShowPause); if (m_slideShowTimer->isActive()) m_slideShowTimer->start(m_slideShowPause); } void Viewer::ViewerWidget::editImage() { DB::ImageInfoList list; list.append(currentInfo()); MainWindow::Window::configureImages(list, true); } void Viewer::ViewerWidget::filterNone() { if (m_display == m_imageDisplay) { m_imageDisplay->filterNone(); m_filterMono->setChecked(false); m_filterBW->setChecked(false); m_filterContrastStretch->setChecked(false); m_filterHistogramEqualization->setChecked(false); } } void Viewer::ViewerWidget::filterSelected() { // The filters that drop bit depth below 32 should be the last ones // so that filters requiring more bit depth are processed first if (m_display == m_imageDisplay) { m_imageDisplay->filterNone(); if (m_filterBW->isChecked()) m_imageDisplay->filterBW(); if (m_filterContrastStretch->isChecked()) m_imageDisplay->filterContrastStretch(); if (m_filterHistogramEqualization->isChecked()) m_imageDisplay->filterHistogramEqualization(); if (m_filterMono->isChecked()) m_imageDisplay->filterMono(); } } void Viewer::ViewerWidget::filterBW() { if (m_display == m_imageDisplay) { if (m_filterBW->isChecked()) m_filterBW->setChecked(m_imageDisplay->filterBW()); else filterSelected(); } } void Viewer::ViewerWidget::filterContrastStretch() { if (m_display == m_imageDisplay) { if (m_filterContrastStretch->isChecked()) m_filterContrastStretch->setChecked(m_imageDisplay->filterContrastStretch()); else filterSelected(); } } void Viewer::ViewerWidget::filterHistogramEqualization() { if (m_display == m_imageDisplay) { if (m_filterHistogramEqualization->isChecked()) m_filterHistogramEqualization->setChecked(m_imageDisplay->filterHistogramEqualization()); else filterSelected(); } } void Viewer::ViewerWidget::filterMono() { if (m_display == m_imageDisplay) { if (m_filterMono->isChecked()) m_filterMono->setChecked(m_imageDisplay->filterMono()); else filterSelected(); } } void Viewer::ViewerWidget::slotSetStackHead() { MainWindow::Window::theMainWindow()->setStackHead(m_list[m_current]); } bool Viewer::ViewerWidget::showingFullScreen() const { return m_showingFullScreen; } void Viewer::ViewerWidget::setShowFullScreen(bool on) { if (on) { setWindowState(windowState() | Qt::WindowFullScreen); // set moveInfoBox(); } else { // We need to size the image when going out of full screen, in case we started directly in full screen // setWindowState(windowState() & ~Qt::WindowFullScreen); // reset resize(Settings::SettingsData::instance()->viewerSize()); } m_showingFullScreen = on; } void Viewer::ViewerWidget::updateCategoryConfig() { if (!CategoryImageConfig::instance()->isVisible()) return; CategoryImageConfig::instance()->setCurrentImage(m_imageDisplay->currentViewAsThumbnail(), currentInfo()); } void Viewer::ViewerWidget::populateExternalPopup() { m_externalPopup->populate(currentInfo(), m_list); } void Viewer::ViewerWidget::populateCategoryImagePopup() { m_categoryImagePopup->populate(m_imageDisplay->currentViewAsThumbnail(), m_list[m_current]); } void Viewer::ViewerWidget::show(bool slideShow) { QSize size; bool fullScreen; if (slideShow) { fullScreen = Settings::SettingsData::instance()->launchSlideShowFullScreen(); size = Settings::SettingsData::instance()->slideShowSize(); } else { fullScreen = Settings::SettingsData::instance()->launchViewerFullScreen(); size = Settings::SettingsData::instance()->viewerSize(); } if (fullScreen) setShowFullScreen(true); else resize(size); QWidget::show(); if (slideShow != m_isRunningSlideShow) { // The info dialog will show up at the wrong place if we call this function directly // don't ask me why - 4 Sep. 2004 15:13 -- Jesper K. Pedersen QTimer::singleShot(0, this, SLOT(slotStartStopSlideShow())); } } KActionCollection *Viewer::ViewerWidget::actions() { return m_actions; } int Viewer::ViewerWidget::find_tag_in_list(const QStringList &list, QString &namefound) { int found = 0; m_currentInputList = QString::fromLatin1(""); for (QStringList::ConstIterator listIter = list.constBegin(); listIter != list.constEnd(); ++listIter) { if (listIter->startsWith(m_currentInput, Qt::CaseInsensitive)) { found++; if (m_currentInputList.length() > 0) m_currentInputList = m_currentInputList + QString::fromLatin1(","); m_currentInputList = m_currentInputList + listIter->right(listIter->length() - m_currentInput.length()); if (found > 1 && m_currentInputList.length() > 20) { // already found more than we want to display // bail here for now // XXX: non-ideal? display more? certainly config 20 return found; } else { namefound = *listIter; } } } return found; } void Viewer::ViewerWidget::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Backspace) { // remove stuff from the current input string m_currentInput.remove(m_currentInput.length() - 1, 1); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); m_currentInputList = QString::fromLatin1(""); // } else if (event->modifier & (Qt::AltModifier | Qt::MetaModifier) && // event->key() == Qt::Key_Enter) { return; // we've handled it } else if (event->key() == Qt::Key_Comma) { // force set the "new" token if (!m_currentCategory.isEmpty()) { if (m_currentInput.left(1) == QString::fromLatin1("\"") || // allow a starting ' or " to signal a brand new category // this bypasses the auto-selection of matching characters m_currentInput.left(1) == QString::fromLatin1("\'")) { m_currentInput = m_currentInput.right(m_currentInput.length() - 1); } if (m_currentInput.isEmpty()) return; currentInfo()->addCategoryInfo(m_currentCategory, m_currentInput); DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_currentCategory); category->addItem(m_currentInput); } m_currentInput = QString::fromLatin1(""); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); return; // we've handled it } else if (event->modifiers() == 0 && event->key() >= Qt::Key_0 && event->key() <= Qt::Key_5) { bool ok; short rating = event->text().left(1).toShort(&ok, 10); if (ok) { currentInfo()->setRating(rating * 2); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); } } else if (event->modifiers() == 0 || event->modifiers() == Qt::ShiftModifier) { // search the category for matches QString namefound; QString incomingKey = event->text().left(1); // start searching for a new category name if (incomingKey == QString::fromLatin1("/")) { if (m_currentInput.isEmpty() && m_currentCategory.isEmpty()) { if (m_currentInputMode == InACategory) { m_currentInputMode = AlwaysStartWithCategory; } else { m_currentInputMode = InACategory; } } else { // reset the category to search through m_currentInput = QString::fromLatin1(""); m_currentCategory = QString::fromLatin1(""); } // use an assigned key or map to a given key for future reference } else if (m_currentInput.isEmpty() && // can map to function keys event->key() >= Qt::Key_F1 && event->key() <= Qt::Key_F35) { // we have a request to assign a macro key or use one Qt::Key key = (Qt::Key)event->key(); if (m_inputMacros->contains(key)) { // Use the requested toggle if (event->modifiers() == Qt::ShiftModifier) { if (currentInfo()->hasCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second)) { currentInfo()->removeCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second); } } else { currentInfo()->addCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second); } } else { (*m_inputMacros)[key] = qMakePair(m_lastCategory, m_lastFound); } updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); // handled it return; } else if (m_currentCategory.isEmpty()) { // still searching for a category to lock to m_currentInput += incomingKey; QStringList categorynames = DB::ImageDB::instance()->categoryCollection()->categoryTexts(); if (find_tag_in_list(categorynames, namefound) == 1) { // yay, we have exactly one! m_currentCategory = namefound; m_currentInput = QString::fromLatin1(""); m_currentInputList = QString::fromLatin1(""); } } else { m_currentInput += incomingKey; DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_currentCategory); QStringList items = category->items(); if (find_tag_in_list(items, namefound) == 1) { // yay, we have exactly one! if (currentInfo()->hasCategoryInfo(category->name(), namefound)) currentInfo()->removeCategoryInfo(category->name(), namefound); else currentInfo()->addCategoryInfo(category->name(), namefound); m_lastFound = namefound; m_lastCategory = m_currentCategory; m_currentInput = QString::fromLatin1(""); m_currentInputList = QString::fromLatin1(""); if (m_currentInputMode == AlwaysStartWithCategory) m_currentCategory = QString::fromLatin1(""); } } updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); } QWidget::keyPressEvent(event); return; } void Viewer::ViewerWidget::videoStopped() { if (!m_videoPlayerStoppedManually && m_isRunningSlideShow) slotSlideShowNext(); m_videoPlayerStoppedManually = false; } void Viewer::ViewerWidget::wheelEvent(QWheelEvent *event) { if (event->delta() < 0) { showNext(); } else { showPrev(); } } void Viewer::ViewerWidget::showExifViewer() { m_exifViewer = new Exif::InfoDialog(currentInfo()->fileName(), this); m_exifViewer->show(); } void Viewer::ViewerWidget::zoomIn() { m_display->zoomIn(); } void Viewer::ViewerWidget::zoomOut() { m_display->zoomOut(); } void Viewer::ViewerWidget::zoomFull() { m_display->zoomFull(); } void Viewer::ViewerWidget::zoomPixelForPixel() { m_display->zoomPixelForPixel(); } void Viewer::ViewerWidget::makeThumbnailImage() { VideoShooter::go(currentInfo(), this); } struct SeekInfo { SeekInfo(const QString &title, const char *name, int value, const QKeySequence &key) : title(title) , name(name) , value(value) , key(key) { } QString title; const char *name; int value; QKeySequence key; }; void Viewer::ViewerWidget::createVideoMenu() { QMenu *menu = new QMenu(m_popup); menu->setTitle(i18nc("@title:inmenu", "Seek")); m_videoActions.append(m_popup->addMenu(menu)); QList list; list << SeekInfo(i18nc("@action:inmenu", "10 minutes backward"), "seek-10-minute", -600000, QKeySequence(QString::fromLatin1("Ctrl+Left"))) << SeekInfo(i18nc("@action:inmenu", "1 minute backward"), "seek-1-minute", -60000, QKeySequence(QString::fromLatin1("Shift+Left"))) << SeekInfo(i18nc("@action:inmenu", "10 seconds backward"), "seek-10-second", -10000, QKeySequence(QString::fromLatin1("Left"))) << SeekInfo(i18nc("@action:inmenu", "1 seconds backward"), "seek-1-second", -1000, QKeySequence(QString::fromLatin1("Up"))) << SeekInfo(i18nc("@action:inmenu", "100 milliseconds backward"), "seek-100-millisecond", -100, QKeySequence(QString::fromLatin1("Shift+Up"))) << SeekInfo(i18nc("@action:inmenu", "100 milliseconds forward"), "seek+100-millisecond", 100, QKeySequence(QString::fromLatin1("Shift+Down"))) << SeekInfo(i18nc("@action:inmenu", "1 seconds forward"), "seek+1-second", 1000, QKeySequence(QString::fromLatin1("Down"))) << SeekInfo(i18nc("@action:inmenu", "10 seconds forward"), "seek+10-second", 10000, QKeySequence(QString::fromLatin1("Right"))) << SeekInfo(i18nc("@action:inmenu", "1 minute forward"), "seek+1-minute", 60000, QKeySequence(QString::fromLatin1("Shift+Right"))) << SeekInfo(i18nc("@action:inmenu", "10 minutes forward"), "seek+10-minute", 600000, QKeySequence(QString::fromLatin1("Ctrl+Right"))); int count = 0; - Q_FOREACH (const SeekInfo &info, list) { + for (const SeekInfo &info : list) { if (count++ == 5) { QAction *sep = new QAction(menu); sep->setSeparator(true); menu->addAction(sep); } QAction *seek = m_actions->addAction(QString::fromLatin1(info.name), m_videoDisplay, SLOT(seek())); seek->setText(info.title); seek->setData(info.value); seek->setShortcut(info.key); m_actions->setShortcutsConfigurable(seek, false); menu->addAction(seek); } QAction *sep = new QAction(m_popup); sep->setSeparator(true); m_popup->addAction(sep); m_videoActions.append(sep); m_stop = m_actions->addAction(QString::fromLatin1("viewer-video-stop"), m_videoDisplay, SLOT(stop())); m_stop->setText(i18nc("@action:inmenu Stop video playback", "Stop")); m_popup->addAction(m_stop); m_videoActions.append(m_stop); m_playPause = m_actions->addAction(QString::fromLatin1("viewer-video-pause"), m_videoDisplay, SLOT(playPause())); // text set in contextMenuEvent() m_playPause->setShortcut(Qt::Key_P); m_actions->setShortcutsConfigurable(m_playPause, false); m_popup->addAction(m_playPause); m_videoActions.append(m_playPause); m_makeThumbnailImage = m_actions->addAction(QString::fromLatin1("make-thumbnail-image"), this, SLOT(makeThumbnailImage())); m_actions->setDefaultShortcut(m_makeThumbnailImage, Qt::ControlModifier + Qt::Key_S); m_makeThumbnailImage->setText(i18nc("@action:inmenu", "Use current frame in thumbnail view")); m_popup->addAction(m_makeThumbnailImage); m_videoActions.append(m_makeThumbnailImage); QAction *restart = m_actions->addAction(QString::fromLatin1("viewer-video-restart"), m_videoDisplay, SLOT(restart())); restart->setText(i18nc("@action:inmenu Restart video playback.", "Restart")); m_popup->addAction(restart); m_videoActions.append(restart); } void Viewer::ViewerWidget::createCategoryImageMenu() { m_categoryImagePopup = new MainWindow::CategoryImagePopup(m_popup); m_popup->addMenu(m_categoryImagePopup); connect(m_categoryImagePopup, &MainWindow::CategoryImagePopup::aboutToShow, this, &ViewerWidget::populateCategoryImagePopup); } void Viewer::ViewerWidget::createFilterMenu() { m_filterMenu = new QMenu(m_popup); m_filterMenu->setTitle(i18nc("@title:inmenu", "Filters")); m_filterNone = m_actions->addAction(QString::fromLatin1("filter-empty"), this, SLOT(filterNone())); m_filterNone->setText(i18nc("@action:inmenu", "Remove All Filters")); m_filterMenu->addAction(m_filterNone); m_filterBW = m_actions->addAction(QString::fromLatin1("filter-bw"), this, SLOT(filterBW())); m_filterBW->setText(i18nc("@action:inmenu", "Apply Grayscale Filter")); m_filterBW->setCheckable(true); m_filterMenu->addAction(m_filterBW); m_filterContrastStretch = m_actions->addAction(QString::fromLatin1("filter-cs"), this, SLOT(filterContrastStretch())); m_filterContrastStretch->setText(i18nc("@action:inmenu", "Apply Contrast Stretching Filter")); m_filterContrastStretch->setCheckable(true); m_filterMenu->addAction(m_filterContrastStretch); m_filterHistogramEqualization = m_actions->addAction(QString::fromLatin1("filter-he"), this, SLOT(filterHistogramEqualization())); m_filterHistogramEqualization->setText(i18nc("@action:inmenu", "Apply Histogram Equalization Filter")); m_filterHistogramEqualization->setCheckable(true); m_filterMenu->addAction(m_filterHistogramEqualization); m_filterMono = m_actions->addAction(QString::fromLatin1("filter-mono"), this, SLOT(filterMono())); m_filterMono->setText(i18nc("@action:inmenu", "Apply Monochrome Filter")); m_filterMono->setCheckable(true); m_filterMenu->addAction(m_filterMono); m_popup->addMenu(m_filterMenu); } void Viewer::ViewerWidget::test() { #ifdef TESTING QTimeLine *timeline = new QTimeLine; timeline->setStartFrame(_infoBox->y()); timeline->setEndFrame(height()); connect(timeline, &QTimeLine::frameChanged, this, &ViewerWidget::moveInfoBox); timeline->start(); #endif // TESTING } void Viewer::ViewerWidget::moveInfoBox(int y) { m_infoBox->move(m_infoBox->x(), y); } void Viewer::ViewerWidget::createVideoViewer() { m_videoDisplay = new VideoDisplay(this); addWidget(m_videoDisplay); connect(m_videoDisplay, &VideoDisplay::stopped, this, &ViewerWidget::videoStopped); } void Viewer::ViewerWidget::stopPlayback() { m_videoDisplay->stop(); } void Viewer::ViewerWidget::invalidateThumbnail() const { ImageManager::ThumbnailCache::instance()->removeThumbnail(currentInfo()->fileName()); } void Viewer::ViewerWidget::setTaggedAreasFromImage() { // Clean all areas we probably already have foreach (TaggedArea *area, findChildren()) { area->deleteLater(); } QMap> taggedAreas = currentInfo()->taggedAreas(); addTaggedAreas(taggedAreas, AreaType::Standard); } void Viewer::ViewerWidget::addAdditionalTaggedAreas(QMap> taggedAreas) { addTaggedAreas(taggedAreas, AreaType::Highlighted); } void Viewer::ViewerWidget::addTaggedAreas(QMap> taggedAreas, AreaType type) { QMapIterator> areasInCategory(taggedAreas); QString category; QString tag; while (areasInCategory.hasNext()) { areasInCategory.next(); category = areasInCategory.key(); QMapIterator areaData(areasInCategory.value()); while (areaData.hasNext()) { areaData.next(); tag = areaData.key(); // Add a new frame for the area TaggedArea *newArea = new TaggedArea(this); newArea->setTagInfo(category, category, tag); newArea->setActualGeometry(areaData.value()); newArea->setHighlighted(type == AreaType::Highlighted); newArea->show(); connect(m_infoBox, &InfoBox::tagHovered, newArea, &TaggedArea::checkIsSelected); connect(m_infoBox, &InfoBox::noTagHovered, newArea, &TaggedArea::deselect); } } // Be sure to display the areas, as viewGeometryChanged is not always emitted on load QSize imageSize = currentInfo()->size(); QSize windowSize = this->size(); // On load, the image is never zoomed, so it's a bit easier ;-) double scaleWidth = double(imageSize.width()) / windowSize.width(); double scaleHeight = double(imageSize.height()) / windowSize.height(); int offsetTop = 0; int offsetLeft = 0; if (scaleWidth > scaleHeight) { offsetTop = (windowSize.height() - imageSize.height() / scaleWidth); } else { offsetLeft = (windowSize.width() - imageSize.width() / scaleHeight); } remapAreas( QSize(windowSize.width() - offsetLeft, windowSize.height() - offsetTop), QRect(QPoint(0, 0), QPoint(imageSize.width(), imageSize.height())), 1); } void Viewer::ViewerWidget::remapAreas(QSize viewSize, QRect zoomWindow, double sizeRatio) { QSize currentWindowSize = this->size(); int outerOffsetLeft = (currentWindowSize.width() - viewSize.width()) / 2; int outerOffsetTop = (currentWindowSize.height() - viewSize.height()) / 2; if (sizeRatio != 1) { zoomWindow = QRect( QPoint( double(zoomWindow.left()) * sizeRatio, double(zoomWindow.top()) * sizeRatio), QPoint( double(zoomWindow.left() + zoomWindow.width()) * sizeRatio, double(zoomWindow.top() + zoomWindow.height()) * sizeRatio)); } double scaleHeight = double(viewSize.height()) / zoomWindow.height(); double scaleWidth = double(viewSize.width()) / zoomWindow.width(); int innerOffsetLeft = -zoomWindow.left() * scaleWidth; int innerOffsetTop = -zoomWindow.top() * scaleHeight; - Q_FOREACH (TaggedArea *area, findChildren()) { + for (TaggedArea *area : findChildren()) { QRect actualGeometry = area->actualGeometry(); QRect screenGeometry; screenGeometry.setWidth(actualGeometry.width() * scaleWidth); screenGeometry.setHeight(actualGeometry.height() * scaleHeight); screenGeometry.moveTo( actualGeometry.left() * scaleWidth + outerOffsetLeft + innerOffsetLeft, actualGeometry.top() * scaleHeight + outerOffsetTop + innerOffsetTop); area->setGeometry(screenGeometry); } } void Viewer::ViewerWidget::copyTo() { QUrl src = QUrl::fromLocalFile(currentInfo()->fileName().absolute()); if (m_lastCopyToTarget.isNull()) { // get directory of src file m_lastCopyToTarget = QFileInfo(src.path()).path(); } QFileDialog dialog(this); dialog.setWindowTitle(i18nc("@title:window", "Copy Image to...")); // use directory of src as start-location: dialog.setDirectory(m_lastCopyToTarget); dialog.selectFile(src.fileName()); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setLabelText(QFileDialog::Accept, i18nc("@action:button", "Copy")); if (dialog.exec()) { QUrl dst = dialog.selectedUrls().first(); KIO::CopyJob *job = KIO::copy(src, dst); connect(job, &KIO::CopyJob::finished, job, &QObject::deleteLater); // get directory of dst file m_lastCopyToTarget = QFileInfo(dst.path()).path(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/VisibleOptionsMenu.cpp b/Viewer/VisibleOptionsMenu.cpp index 0eb1ca1e..260d5308 100644 --- a/Viewer/VisibleOptionsMenu.cpp +++ b/Viewer/VisibleOptionsMenu.cpp @@ -1,176 +1,178 @@ -/* 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 "VisibleOptionsMenu.h" #include #include #include #include #include #include #include #include #include Viewer::VisibleOptionsMenu::VisibleOptionsMenu(QWidget *parent, KActionCollection *actions) : QMenu(i18n("Show..."), parent) { setTearOffEnabled(true); setTitle(i18n("Show")); connect(this, &VisibleOptionsMenu::aboutToShow, this, &VisibleOptionsMenu::updateState); m_showInfoBox = actions->add(QString::fromLatin1("viewer-show-infobox")); m_showInfoBox->setText(i18n("Show Info Box")); actions->setDefaultShortcut(m_showInfoBox, Qt::CTRL + Qt::Key_I); m_showInfoBox->setChecked(Settings::SettingsData::instance()->showInfoBox()); connect(m_showInfoBox, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowInfoBox); addAction(m_showInfoBox); m_showLabel = actions->add(QString::fromLatin1("viewer-show-label")); m_showLabel->setText(i18n("Show Label")); connect(m_showLabel, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowLabel); addAction(m_showLabel); m_showDescription = actions->add(QString::fromLatin1("viewer-show-description")); m_showDescription->setText(i18n("Show Description")); connect(m_showDescription, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowDescription); addAction(m_showDescription); m_showDate = actions->add(QString::fromLatin1("viewer-show-date")); m_showDate->setText(i18n("Show Date")); connect(m_showDate, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowDate); addAction(m_showDate); m_showTime = actions->add(QString::fromLatin1("viewer-show-time")); m_showTime->setText(i18n("Show Time")); connect(m_showTime, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowTime); addAction(m_showTime); m_showTime->setVisible(m_showDate->isChecked()); m_showFileName = actions->add(QString::fromLatin1("viewer-show-filename")); m_showFileName->setText(i18n("Show Filename")); connect(m_showFileName, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowFilename); addAction(m_showFileName); m_showExif = actions->add(QString::fromLatin1("viewer-show-exif")); m_showExif->setText(i18n("Show Exif")); connect(m_showExif, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowEXIF); addAction(m_showExif); m_showImageSize = actions->add(QString::fromLatin1("viewer-show-imagesize")); m_showImageSize->setText(i18n("Show Image Size")); connect(m_showImageSize, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowImageSize); addAction(m_showImageSize); m_showRating = actions->add(QString::fromLatin1("viewer-show-rating")); m_showRating->setText(i18n("Show Rating")); connect(m_showRating, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowRating); addAction(m_showRating); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); for (QList::Iterator it = categories.begin(); it != categories.end(); ++it) { KToggleAction *taction = actions->add((*it)->name()); m_actionList.append(taction); taction->setText((*it)->name()); taction->setData((*it)->name()); addAction(taction); connect(taction, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowCategory); } } void Viewer::VisibleOptionsMenu::toggleShowCategory(bool b) { QAction *action = qobject_cast(sender()); DB::ImageDB::instance()->categoryCollection()->categoryForName(action->data().value())->setDoShow(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowLabel(bool b) { Settings::SettingsData::instance()->setShowLabel(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowDescription(bool b) { Settings::SettingsData::instance()->setShowDescription(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowDate(bool b) { Settings::SettingsData::instance()->setShowDate(b); m_showTime->setVisible(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowFilename(bool b) { Settings::SettingsData::instance()->setShowFilename(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowTime(bool b) { Settings::SettingsData::instance()->setShowTime(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowEXIF(bool b) { Settings::SettingsData::instance()->setShowEXIF(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowImageSize(bool b) { Settings::SettingsData::instance()->setShowImageSize(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowRating(bool b) { Settings::SettingsData::instance()->setShowRating(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowInfoBox(bool b) { Settings::SettingsData::instance()->setShowInfoBox(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::updateState() { m_showInfoBox->setChecked(Settings::SettingsData::instance()->showInfoBox()); m_showLabel->setChecked(Settings::SettingsData::instance()->showLabel()); m_showDescription->setChecked(Settings::SettingsData::instance()->showDescription()); m_showDate->setChecked(Settings::SettingsData::instance()->showDate()); m_showTime->setChecked(Settings::SettingsData::instance()->showTime()); m_showFileName->setChecked(Settings::SettingsData::instance()->showFilename()); m_showExif->setChecked(Settings::SettingsData::instance()->showEXIF()); m_showImageSize->setChecked(Settings::SettingsData::instance()->showImageSize()); m_showRating->setChecked(Settings::SettingsData::instance()->showRating()); - Q_FOREACH (KToggleAction *action, m_actionList) { + for (KToggleAction *action : m_actionList) { action->setChecked(DB::ImageDB::instance()->categoryCollection()->categoryForName(action->data().value())->doShow()); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/Database.cpp b/XMLDB/Database.cpp index 394a7a58..408d24a2 100644 --- a/XMLDB/Database.cpp +++ b/XMLDB/Database.cpp @@ -1,796 +1,797 @@ -/* 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. + 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 "Database.h" #include "FileReader.h" #include "FileWriter.h" #include "Logging.h" #include "XMLCategory.h" #include "XMLImageDateCollection.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Utilities::StringSet; namespace { void checkForBackupFile(const QString &fileName, DB::UIDelegate &ui) { QString backupName = QFileInfo(fileName).absolutePath() + QString::fromLatin1("/.#") + QFileInfo(fileName).fileName(); QFileInfo backUpFile(backupName); QFileInfo indexFile(fileName); if (!backUpFile.exists() || indexFile.lastModified() > backUpFile.lastModified() || backUpFile.size() == 0) return; const long backupSizeKB = backUpFile.size() >> 10; const DB::UserFeedback choice = ui.questionYesNo( QString::fromUtf8("Autosave file found: '%1', %2KB.").arg(backupName).arg(backupSizeKB), i18n("Autosave file '%1' exists (size %3 KB) and is newer than '%2'. " "Should the autosave file be used?", backupName, fileName, backupSizeKB), i18n("Found Autosave File")); if (choice == DB::UserFeedback::Confirm) { qCInfo(XMLDBLog) << "Using autosave file:" << backupName; QFile in(backupName); if (in.open(QIODevice::ReadOnly)) { QFile out(fileName); if (out.open(QIODevice::WriteOnly)) { char data[1024]; int len; while ((len = in.read(data, 1024))) out.write(data, len); } } } } } // namespace bool XMLDB::Database::s_anyImageWithEmptySize = false; XMLDB::Database::Database(const QString &configFile, DB::UIDelegate &delegate) : ImageDB(delegate) , m_fileName(configFile) { checkForBackupFile(configFile, uiDelegate()); FileReader reader(this); reader.read(configFile); m_nextStackId = reader.nextStackId(); connect(categoryCollection(), &DB::CategoryCollection::itemRemoved, this, &Database::deleteItem); connect(categoryCollection(), &DB::CategoryCollection::itemRenamed, this, &Database::renameItem); connect(categoryCollection(), &DB::CategoryCollection::itemRemoved, &m_members, &DB::MemberMap::deleteItem); connect(categoryCollection(), &DB::CategoryCollection::itemRenamed, &m_members, &DB::MemberMap::renameItem); connect(categoryCollection(), &DB::CategoryCollection::categoryRemoved, &m_members, &DB::MemberMap::deleteCategory); } uint XMLDB::Database::totalCount() const { return m_images.count(); } /** * I was considering merging the two calls to this method (one for images, one for video), but then I * realized that all the work is really done after the check for whether the given * imageInfo is of the right type, and as a match can't be both, this really * would buy me nothing. */ QMap XMLDB::Database::classify(const DB::ImageSearchInfo &info, const QString &category, DB::MediaType typemask, DB::ClassificationMode mode) { QElapsedTimer timer; timer.start(); QMap map; DB::GroupCounter counter(category); Utilities::StringSet alreadyMatched = info.findAlreadyMatched(category); DB::ImageSearchInfo noMatchInfo = info; QString currentMatchTxt = noMatchInfo.categoryMatchText(category); if (currentMatchTxt.isEmpty()) noMatchInfo.setCategoryMatchText(category, DB::ImageDB::NONE()); else noMatchInfo.setCategoryMatchText(category, QString::fromLatin1("%1 & %2").arg(currentMatchTxt).arg(DB::ImageDB::NONE())); noMatchInfo.setCacheable(false); // Iterate through the whole database of images. for (const auto &imageInfo : m_images) { bool match = ((imageInfo)->mediaType() & typemask) && !(imageInfo)->isLocked() && info.match(imageInfo) && rangeInclude(imageInfo); if (match) { // If the given image is currently matched. // Now iterate through all the categories the current image // contains, and increase them in the map mapping from category // to count. StringSet items = (imageInfo)->itemsOfCategory(category); counter.count(items, imageInfo->date()); for (const auto &categoryName : items) { if (!alreadyMatched.contains(categoryName)) // We do not want to match "Jesper & Jesper" map[categoryName].add(imageInfo->date()); } // Find those with no other matches if (noMatchInfo.match(imageInfo)) map[DB::ImageDB::NONE()].count++; // this is a shortcut for the browser overview page, // where we are only interested whether there are sub-categories to a category if (mode == DB::ClassificationMode::PartialCount && map.size() > 1) { qCInfo(TimingLog) << "Database::classify(partial): " << timer.restart() << "ms."; return map; } } } QMap groups = counter.result(); for (QMap::iterator it = groups.begin(); it != groups.end(); ++it) { map[it.key()] = it.value(); } qCInfo(TimingLog) << "Database::classify(): " << timer.restart() << "ms."; return map; } void XMLDB::Database::renameCategory(const QString &oldName, const QString newName) { for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) { (*it)->renameCategory(oldName, newName); } } void XMLDB::Database::addToBlockList(const DB::FileNameList &list) { - Q_FOREACH (const DB::FileName &fileName, list) { + for (const DB::FileName &fileName : list) { m_blockList.insert(fileName); } deleteList(list); } void XMLDB::Database::deleteList(const DB::FileNameList &list) { - Q_FOREACH (const DB::FileName &fileName, list) { + for (const DB::FileName &fileName : list) { DB::ImageInfoPtr inf = fileName.info(); StackMap::iterator found = m_stackMap.find(inf->stackId()); if (inf->isStacked() && found != m_stackMap.end()) { const DB::FileNameList origCache = found.value(); DB::FileNameList newCache; - Q_FOREACH (const DB::FileName &cacheName, origCache) { + for (const DB::FileName &cacheName : origCache) { if (fileName != cacheName) newCache.append(cacheName); } if (newCache.size() <= 1) { // we're destroying a stack - Q_FOREACH (const DB::FileName &cacheName, newCache) { + for (const DB::FileName &cacheName : newCache) { DB::ImageInfoPtr cacheInf = cacheName.info(); cacheInf->setStackId(0); cacheInf->setStackOrder(0); } m_stackMap.remove(inf->stackId()); } else { m_stackMap.insert(inf->stackId(), newCache); } } m_imageCache.remove(inf->fileName().absolute()); m_images.remove(inf); } Exif::Database::instance()->remove(list); emit totalChanged(m_images.count()); emit imagesDeleted(list); emit dirty(); } void XMLDB::Database::renameItem(DB::Category *category, const QString &oldName, const QString &newName) { for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) { (*it)->renameItem(category->name(), oldName, newName); } } void XMLDB::Database::deleteItem(DB::Category *category, const QString &value) { for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) { (*it)->removeCategoryInfo(category->name(), value); } } void XMLDB::Database::lockDB(bool lock, bool exclude) { DB::ImageSearchInfo info = Settings::SettingsData::instance()->currentLock(); for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it) { if (lock) { bool match = info.match(*it); if (!exclude) match = !match; (*it)->setLocked(match); } else (*it)->setLocked(false); } } void XMLDB::Database::clearDelayedImages() { m_delayedCache.clear(); m_delayedUpdate.clear(); } void XMLDB::Database::forceUpdate(const DB::ImageInfoList &images) { // FIXME: merge stack information DB::ImageInfoList newImages = images.sort(); if (m_images.count() == 0) { // case 1: The existing imagelist is empty. - Q_FOREACH (const DB::ImageInfoPtr &imageInfo, newImages) + for (const DB::ImageInfoPtr &imageInfo : newImages) m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo); m_images = newImages; } else if (newImages.count() == 0) { // case 2: No images to merge in - that's easy ;-) return; } else if (newImages.first()->date().start() > m_images.last()->date().start()) { // case 2: The new list is later than the existsing - Q_FOREACH (const DB::ImageInfoPtr &imageInfo, newImages) + for (const DB::ImageInfoPtr &imageInfo : newImages) m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo); m_images.appendList(newImages); } else if (m_images.isSorted()) { // case 3: The lists overlaps, and the existsing list is sorted - Q_FOREACH (const DB::ImageInfoPtr &imageInfo, newImages) + for (const DB::ImageInfoPtr &imageInfo : newImages) m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo); m_images.mergeIn(newImages); } else { // case 4: The lists overlaps, and the existsing list is not sorted in the overlapping range. - Q_FOREACH (const DB::ImageInfoPtr &imageInfo, newImages) + for (const DB::ImageInfoPtr &imageInfo : newImages) m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo); m_images.appendList(newImages); } } void XMLDB::Database::addImages(const DB::ImageInfoList &images, bool doUpdate) { - Q_FOREACH (const DB::ImageInfoPtr &info, images) { + for (const DB::ImageInfoPtr &info : images) { info->addCategoryInfo(i18n("Media Type"), info->mediaType() == DB::Image ? i18n("Image") : i18n("Video")); m_delayedCache.insert(info->fileName().absolute(), info); m_delayedUpdate << info; } if (doUpdate) { commitDelayedImages(); } } void XMLDB::Database::commitDelayedImages() { uint imagesAdded = m_delayedUpdate.count(); if (imagesAdded > 0) { forceUpdate(m_delayedUpdate); m_delayedCache.clear(); m_delayedUpdate.clear(); // It's the responsibility of the caller to add the Exif information. // It's more efficient from an I/O perspective to minimize the number // of passes over the images, and with the ability to add the Exif // data in a transaction, there's no longer any need to read it here. emit totalChanged(m_images.count()); emit dirty(); } } void XMLDB::Database::renameImage(DB::ImageInfoPtr info, const DB::FileName &newName) { info->setFileName(newName); } DB::ImageInfoPtr XMLDB::Database::info(const DB::FileName &fileName) const { if (fileName.isNull()) return DB::ImageInfoPtr(); const QString name = fileName.absolute(); if (m_imageCache.contains(name)) return m_imageCache[name]; if (m_delayedCache.contains(name)) return m_delayedCache[name]; - Q_FOREACH (const DB::ImageInfoPtr &imageInfo, m_images) + for (const DB::ImageInfoPtr &imageInfo : m_images) m_imageCache.insert(imageInfo->fileName().absolute(), imageInfo); if (m_imageCache.contains(name)) { return m_imageCache[name]; } return DB::ImageInfoPtr(); } bool XMLDB::Database::rangeInclude(DB::ImageInfoPtr info) const { if (m_selectionRange.start().isNull()) return true; DB::ImageDate::MatchType tp = info->date().isIncludedIn(m_selectionRange); if (m_includeFuzzyCounts) return (tp == DB::ImageDate::ExactMatch || tp == DB::ImageDate::RangeMatch); else return (tp == DB::ImageDate::ExactMatch); } DB::MemberMap &XMLDB::Database::memberMap() { return m_members; } void XMLDB::Database::save(const QString &fileName, bool isAutoSave) { FileWriter saver(this); saver.save(fileName, isAutoSave); } DB::MD5Map *XMLDB::Database::md5Map() { return &m_md5map; } bool XMLDB::Database::isBlocking(const DB::FileName &fileName) { return m_blockList.contains(fileName); } DB::FileNameList XMLDB::Database::images() { return m_images.files(); } DB::FileNameList XMLDB::Database::search( const DB::ImageSearchInfo &info, bool requireOnDisk) const { return searchPrivate(info, requireOnDisk, true); } DB::FileNameList XMLDB::Database::searchPrivate( const DB::ImageSearchInfo &info, bool requireOnDisk, bool onlyItemsMatchingRange) const { // When searching for images counts for the datebar, we want matches outside the range too. // When searching for images for the thumbnail view, we only want matches inside the range. DB::FileNameList result; for (DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it) { bool match = !(*it)->isLocked() && info.match(*it) && (!onlyItemsMatchingRange || rangeInclude(*it)); match &= !requireOnDisk || DB::ImageInfo::imageOnDisk((*it)->fileName()); if (match) result.append((*it)->fileName()); } return result; } void XMLDB::Database::sortAndMergeBackIn(const DB::FileNameList &fileNameList) { DB::ImageInfoList infoList; - Q_FOREACH (const DB::FileName &fileName, fileNameList) + for (const DB::FileName &fileName : fileNameList) infoList.append(fileName.info()); m_images.sortAndMergeBackIn(infoList); } DB::CategoryCollection *XMLDB::Database::categoryCollection() { return &m_categoryCollection; } QExplicitlySharedDataPointer XMLDB::Database::rangeCollection() { return QExplicitlySharedDataPointer( new XMLImageDateCollection(searchPrivate(Browser::BrowserWidget::instance()->currentContext(), false, false))); } void XMLDB::Database::reorder( const DB::FileName &item, const DB::FileNameList &selection, bool after) { Q_ASSERT(!item.isNull()); DB::ImageInfoList list = takeImagesFromSelection(selection); insertList(item, list, after); } // Remove all the images from the database that match the given selection and // return that sublist. // This returns the selected and erased images in the order in which they appear // in the image list itself. DB::ImageInfoList XMLDB::Database::takeImagesFromSelection(const DB::FileNameList &selection) { DB::ImageInfoList result; if (selection.isEmpty()) return result; // iterate over all images (expensive!!) TODO: improve? for (DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); /**/) { const DB::FileName imagefile = (*it)->fileName(); DB::FileNameList::const_iterator si = selection.begin(); // for each image, iterate over selection, break on match for (/**/; si != selection.end(); ++si) { const DB::FileName file = *si; if (imagefile == file) { break; } } // if image is not in selection, simply advance to next, if not add to result and erase if (si == selection.end()) { ++it; } else { result << *it; m_imageCache.remove((*it)->fileName().absolute()); it = m_images.erase(it); } // if all images from selection are in result (size of lists is equal) break. if (result.size() == selection.size()) break; } return result; } void XMLDB::Database::insertList( const DB::FileName &fileName, const DB::ImageInfoList &list, bool after) { DB::ImageInfoListIterator imageIt = m_images.begin(); for (; imageIt != m_images.end(); ++imageIt) { if ((*imageIt)->fileName() == fileName) { break; } } // since insert() inserts before iterator increment when inserting AFTER image if (after) imageIt++; for (DB::ImageInfoListConstIterator it = list.begin(); it != list.end(); ++it) { // the call to insert() destroys the given iterator so use the new one after the call imageIt = m_images.insert(imageIt, *it); m_imageCache.insert((*it)->fileName().absolute(), *it); // increment always to retain order of selected images imageIt++; } emit dirty(); } bool XMLDB::Database::stack(const DB::FileNameList &items) { unsigned int changed = 0; QSet stacks; QList images; unsigned int stackOrder = 1; - Q_FOREACH (const DB::FileName &fileName, items) { + for (const DB::FileName &fileName : items) { DB::ImageInfoPtr imgInfo = fileName.info(); Q_ASSERT(imgInfo); if (imgInfo->isStacked()) { stacks << imgInfo->stackId(); stackOrder = qMax(stackOrder, imgInfo->stackOrder() + 1); } else { images << imgInfo; } } if (stacks.size() > 1) return false; // images already in different stacks -> can't stack DB::StackID stackId = (stacks.size() == 1) ? *(stacks.begin()) : m_nextStackId++; - Q_FOREACH (DB::ImageInfoPtr info, images) { + for (DB::ImageInfoPtr info : images) { info->setStackOrder(stackOrder); info->setStackId(stackId); m_stackMap[stackId].append(info->fileName()); ++changed; ++stackOrder; } if (changed) emit dirty(); return changed; } void XMLDB::Database::unstack(const DB::FileNameList &items) { - Q_FOREACH (const DB::FileName &fileName, items) { + for (const DB::FileName &fileName : items) { DB::FileNameList allInStack = getStackFor(fileName); if (allInStack.size() <= 2) { // we're destroying stack here - Q_FOREACH (const DB::FileName &stackFileName, allInStack) { + for (const DB::FileName &stackFileName : allInStack) { DB::ImageInfoPtr imgInfo = stackFileName.info(); Q_ASSERT(imgInfo); if (imgInfo->isStacked()) { m_stackMap.remove(imgInfo->stackId()); imgInfo->setStackId(0); imgInfo->setStackOrder(0); } } } else { DB::ImageInfoPtr imgInfo = fileName.info(); Q_ASSERT(imgInfo); if (imgInfo->isStacked()) { m_stackMap[imgInfo->stackId()].removeAll(fileName); imgInfo->setStackId(0); imgInfo->setStackOrder(0); } } } if (!items.isEmpty()) emit dirty(); } DB::FileNameList XMLDB::Database::getStackFor(const DB::FileName &referenceImg) const { DB::ImageInfoPtr imageInfo = info(referenceImg); if (!imageInfo || !imageInfo->isStacked()) return DB::FileNameList(); StackMap::iterator found = m_stackMap.find(imageInfo->stackId()); if (found != m_stackMap.end()) return found.value(); // it wasn't in the cache -> rebuild it m_stackMap.clear(); for (DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it) { if ((*it)->isStacked()) { DB::StackID stackid = (*it)->stackId(); m_stackMap[stackid].append((*it)->fileName()); } } found = m_stackMap.find(imageInfo->stackId()); if (found != m_stackMap.end()) return found.value(); else return DB::FileNameList(); } void XMLDB::Database::copyData(const DB::FileName &from, const DB::FileName &to) { (*info(to)).merge(*info(from)); } int XMLDB::Database::fileVersion() { // File format version, bump it up every time the format for the file changes. return 8; } // During profiling of loading, I found that a significant amount of time was spent in QDateTime::fromString. // Reviewing the code, I fount that it did a lot of extra checks we don't need (like checking if the string have // timezone information (which they won't in KPA), this function is a replacement that is faster than the original. QDateTime dateTimeFromString(const QString &str) { static QChar T = QChar::fromLatin1('T'); if (str[10] == T) return QDateTime(QDate::fromString(str.left(10), Qt::ISODate), QTime::fromString(str.mid(11), Qt::ISODate)); else return QDateTime::fromString(str, Qt::ISODate); } DB::ImageInfoPtr XMLDB::Database::createImageInfo(const DB::FileName &fileName, ReaderPtr reader, Database *db, const QMap *newToOldCategory) { static QString _label_ = QString::fromUtf8("label"); static QString _description_ = QString::fromUtf8("description"); static QString _startDate_ = QString::fromUtf8("startDate"); static QString _endDate_ = QString::fromUtf8("endDate"); static QString _yearFrom_ = QString::fromUtf8("yearFrom"); static QString _monthFrom_ = QString::fromUtf8("monthFrom"); static QString _dayFrom_ = QString::fromUtf8("dayFrom"); static QString _hourFrom_ = QString::fromUtf8("hourFrom"); static QString _minuteFrom_ = QString::fromUtf8("minuteFrom"); static QString _secondFrom_ = QString::fromUtf8("secondFrom"); static QString _yearTo_ = QString::fromUtf8("yearTo"); static QString _monthTo_ = QString::fromUtf8("monthTo"); static QString _dayTo_ = QString::fromUtf8("dayTo"); static QString _angle_ = QString::fromUtf8("angle"); static QString _md5sum_ = QString::fromUtf8("md5sum"); static QString _width_ = QString::fromUtf8("width"); static QString _height_ = QString::fromUtf8("height"); static QString _rating_ = QString::fromUtf8("rating"); static QString _stackId_ = QString::fromUtf8("stackId"); static QString _stackOrder_ = QString::fromUtf8("stackOrder"); static QString _videoLength_ = QString::fromUtf8("videoLength"); static QString _options_ = QString::fromUtf8("options"); static QString _0_ = QString::fromUtf8("0"); static QString _minus1_ = QString::fromUtf8("-1"); static QString _MediaType_ = i18n("Media Type"); static QString _Image_ = i18n("Image"); static QString _Video_ = i18n("Video"); QString label; if (reader->hasAttribute(_label_)) label = reader->attribute(_label_); else label = QFileInfo(fileName.relative()).completeBaseName(); QString description; if (reader->hasAttribute(_description_)) description = reader->attribute(_description_); DB::ImageDate date; if (reader->hasAttribute(_startDate_)) { QDateTime start; QString str = reader->attribute(_startDate_); if (!str.isEmpty()) start = dateTimeFromString(str); str = reader->attribute(_endDate_); if (!str.isEmpty()) date = DB::ImageDate(start, dateTimeFromString(str)); else date = DB::ImageDate(start); } else { int yearFrom = 0, monthFrom = 0, dayFrom = 0, yearTo = 0, monthTo = 0, dayTo = 0, hourFrom = -1, minuteFrom = -1, secondFrom = -1; yearFrom = reader->attribute(_yearFrom_, _0_).toInt(); monthFrom = reader->attribute(_monthFrom_, _0_).toInt(); dayFrom = reader->attribute(_dayFrom_, _0_).toInt(); hourFrom = reader->attribute(_hourFrom_, _minus1_).toInt(); minuteFrom = reader->attribute(_minuteFrom_, _minus1_).toInt(); secondFrom = reader->attribute(_secondFrom_, _minus1_).toInt(); yearTo = reader->attribute(_yearTo_, _0_).toInt(); monthTo = reader->attribute(_monthTo_, _0_).toInt(); dayTo = reader->attribute(_dayTo_, _0_).toInt(); date = DB::ImageDate(yearFrom, monthFrom, dayFrom, yearTo, monthTo, dayTo, hourFrom, minuteFrom, secondFrom); } int angle = reader->attribute(_angle_, _0_).toInt(); DB::MD5 md5sum(reader->attribute(_md5sum_)); s_anyImageWithEmptySize |= !reader->hasAttribute(_width_); int w = reader->attribute(_width_, _minus1_).toInt(); int h = reader->attribute(_height_, _minus1_).toInt(); QSize size = QSize(w, h); DB::MediaType mediaType = Utilities::isVideo(fileName) ? DB::Video : DB::Image; short rating = reader->attribute(_rating_, _minus1_).toShort(); DB::StackID stackId = reader->attribute(_stackId_, _0_).toULong(); unsigned int stackOrder = reader->attribute(_stackOrder_, _0_).toULong(); DB::ImageInfo *info = new DB::ImageInfo(fileName, label, description, date, angle, md5sum, size, mediaType, rating, stackId, stackOrder); if (reader->hasAttribute(_videoLength_)) info->setVideoLength(reader->attribute(_videoLength_).toInt()); DB::ImageInfoPtr result(info); possibleLoadCompressedCategories(reader, result, db, newToOldCategory); while (reader->readNextStartOrStopElement(_options_).isStartToken) { readOptions(result, reader, newToOldCategory); } info->addCategoryInfo(_MediaType_, info->mediaType() == DB::Image ? _Image_ : _Video_); return result; } void XMLDB::Database::readOptions(DB::ImageInfoPtr info, ReaderPtr reader, const QMap *newToOldCategory) { static QString _name_ = QString::fromUtf8("name"); static QString _value_ = QString::fromUtf8("value"); static QString _option_ = QString::fromUtf8("option"); static QString _area_ = QString::fromUtf8("area"); while (reader->readNextStartOrStopElement(_option_).isStartToken) { QString name = FileReader::unescape(reader->attribute(_name_)); // If the silent update to db version 6 has been done, use the updated category names. if (newToOldCategory) { name = newToOldCategory->key(name, name); } if (!name.isNull()) { // Read values while (reader->readNextStartOrStopElement(_value_).isStartToken) { QString value = reader->attribute(_value_); if (reader->hasAttribute(_area_)) { QStringList areaData = reader->attribute(_area_).split(QString::fromUtf8(" ")); int x = areaData[0].toInt(); int y = areaData[1].toInt(); int w = areaData[2].toInt(); int h = areaData[3].toInt(); QRect area = QRect(QPoint(x, y), QPoint(x + w - 1, y + h - 1)); if (!value.isNull()) { info->addCategoryInfo(name, value, area); } } else { if (!value.isNull()) { info->addCategoryInfo(name, value); } } reader->readEndElement(); } } } } void XMLDB::Database::possibleLoadCompressedCategories(ReaderPtr reader, DB::ImageInfoPtr info, Database *db, const QMap *newToOldCategory) { if (db == nullptr) return; - Q_FOREACH (const DB::CategoryPtr categoryPtr, db->m_categoryCollection.categories()) { + for (const DB::CategoryPtr categoryPtr : db->m_categoryCollection.categories()) { QString categoryName = categoryPtr->name(); QString oldCategoryName; if (newToOldCategory) { // translate to old categoryName, defaulting to the original name if not found: oldCategoryName = newToOldCategory->value(categoryName, categoryName); } else { oldCategoryName = categoryName; } QString str = reader->attribute(FileWriter::escape(oldCategoryName)); if (!str.isEmpty()) { QStringList list = str.split(QString::fromLatin1(","), QString::SkipEmptyParts); - Q_FOREACH (const QString &tagString, list) { + for (const QString &tagString : list) { int id = tagString.toInt(); if (id != 0 || categoryPtr->isSpecialCategory()) { const QString name = static_cast(categoryPtr.data())->nameForId(id); info->addCategoryInfo(categoryName, name); } else { QStringList tags = static_cast(categoryPtr.data())->namesForId(id); if (tags.size() == 1) { qCInfo(XMLDBLog) << "Fixing tag " << categoryName << "/" << tags[0] << "with id=0 for image" << info->fileName().relative(); } else { // insert marker category QString markerTag = i18n("KPhotoAlbum - manual repair needed (%1)", tags.join(i18nc("Separator in a list of tags", ", "))); categoryPtr->addItem(markerTag); info->addCategoryInfo(categoryName, markerTag); qCWarning(XMLDBLog) << "Manual fix required for image" << info->fileName().relative(); qCWarning(XMLDBLog) << "Image was marked with tag " << categoryName << "/" << markerTag; } for (const auto &name : tags) { info->addCategoryInfo(categoryName, name); } } } } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileReader.cpp b/XMLDB/FileReader.cpp index 59aed60b..44f2ddbc 100644 --- a/XMLDB/FileReader.cpp +++ b/XMLDB/FileReader.cpp @@ -1,599 +1,600 @@ /* 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 . */ // Local includes #include "FileReader.h" #include "CompressFileInfo.h" #include "Database.h" #include "Logging.h" #include "XMLCategory.h" #include #include // KDE includes #include // Qt includes #include #include #include #include #include #include #include void XMLDB::FileReader::read(const QString &configFile) { static QString versionString = QString::fromUtf8("version"); static QString compressedString = QString::fromUtf8("compressed"); ReaderPtr reader = readConfigFile(configFile); ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KPhotoAlbum")); if (!info.isStartToken) reader->complainStartElementExpected(QString::fromUtf8("KPhotoAlbum")); m_fileVersion = reader->attribute(versionString, QString::fromLatin1("1")).toInt(); if (m_fileVersion > Database::fileVersion()) { DB::UserFeedback ret = m_db->uiDelegate().warningContinueCancel( QString::fromLatin1("index.xml version %1 is newer than %2!").arg(m_fileVersion).arg(Database::fileVersion()), i18n("

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

" "

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

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

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

" "

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

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

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

" "

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

" "

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

" "

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

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

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

" "

Manual repair required!

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

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

" "

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

" "

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

" "

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

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

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

" "

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

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

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

" "

The following tags require manual inspection:" "

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

" "

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

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

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

" "

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

" "

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

" "

export KDEDIRS=/usr/local/kde

" "

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

"), i18n("No default setup file found")); } else { QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); QString str = stream.readAll(); // Replace the default setup's category and tag names with localized ones str = str.replace(QString::fromUtf8("People"), i18n("People")); str = str.replace(QString::fromUtf8("Places"), i18n("Places")); str = str.replace(QString::fromUtf8("Events"), i18n("Events")); str = str.replace(QString::fromUtf8("untagged"), i18n("untagged")); str = str.replace(QRegExp(QString::fromLatin1("imageDirectory=\"[^\"]*\"")), QString::fromLatin1("")); str = str.replace(QRegExp(QString::fromLatin1("htmlBaseDir=\"[^\"]*\"")), QString::fromLatin1("")); str = str.replace(QRegExp(QString::fromLatin1("htmlBaseURL=\"[^\"]*\"")), QString::fromLatin1("")); reader->addData(str); } } else { if (!file.open(QIODevice::ReadOnly)) { m_db->uiDelegate().error( QString::fromLatin1("Unable to open '%1' for reading").arg(configFile), i18n("Unable to open '%1' for reading", configFile), i18n("Error Running Demo")); exit(-1); } reader->addData(file.readAll()); #if 0 QString errMsg; int errLine; int errCol; if ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol )) { file.close(); // If parsing index.xml fails let's see if we could use a backup instead Utilities::checkForBackupFile( configFile, i18n( "line %1 column %2 in file %3: %4", errLine , errCol , configFile , errMsg ) ); if ( !file.open( QIODevice::ReadOnly ) || ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol ) ) ) { KMessageBox::error( messageParent(), i18n( "Failed to recover the backup: %1", errMsg ) ); exit(-1); } } #endif } // Now read the content of the file. #if 0 QDomElement top = doc.documentElement(); if ( top.isNull() ) { KMessageBox::error( messageParent(), i18n("Error in file %1: No elements found", configFile ) ); exit(-1); } if ( top.tagName().toLower() != QString::fromLatin1( "kphotoalbum" ) && top.tagName().toLower() != QString::fromLatin1( "kimdaba" ) ) { // KimDaBa compatibility KMessageBox::error( messageParent(), i18n("Error in file %1: expected 'KPhotoAlbum' as top element but found '%2'", configFile , top.tagName() ) ); exit(-1); } #endif file.close(); return reader; } /** * @brief Unescape a string used as an XML attribute name. * * @see XMLDB::FileWriter::escape * * @param str the string to be unescaped * @return the unescaped string */ QString XMLDB::FileReader::unescape(const QString &str) { static bool hashUsesCompressedFormat = useCompressedFileFormat(); static QHash s_cache; if (hashUsesCompressedFormat != useCompressedFileFormat()) s_cache.clear(); if (s_cache.contains(str)) return s_cache[str]; QString tmp(str); // Matches encoded characters in attribute names QRegExp rx(QString::fromLatin1("(_.)([0-9A-F]{2})")); int pos = 0; // Unencoding special characters if compressed XML is selected if (useCompressedFileFormat()) { while ((pos = rx.indexIn(tmp, pos)) != -1) { QString before = rx.cap(1) + rx.cap(2); QString after = QString::fromLatin1(QByteArray::fromHex(rx.cap(2).toLocal8Bit())); tmp.replace(pos, before.length(), after); pos += after.length(); } } else tmp.replace(QString::fromLatin1("_"), QString::fromLatin1(" ")); s_cache.insert(str, tmp); return tmp; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileWriter.cpp b/XMLDB/FileWriter.cpp index 01305e5f..4bbf039b 100644 --- a/XMLDB/FileWriter.cpp +++ b/XMLDB/FileWriter.cpp @@ -1,494 +1,496 @@ -/* 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. + 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 "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 // // // // +++++++++++++++++++++++++++++++ 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; } QTime t; if (TimingLog().isDebugEnabled()) t.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" << t.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)); */ - Q_FOREACH (const QString &tagName, category->items()) { + for (const QString &tagName : category->items()) { 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 - Q_FOREACH (const DB::ImageInfoPtr &infoPtr, m_db->m_clipboard) { + for (const DB::ImageInfoPtr &infoPtr : m_db->m_clipboard) { list.append(infoPtr); } { ElementWriter dummy(writer, QString::fromLatin1("images")); - Q_FOREACH (const DB::ImageInfoPtr &infoPtr, list) { + for (const DB::ImageInfoPtr &infoPtr : list) { save(writer, infoPtr); } } } void XMLDB::FileWriter::saveBlockList(QXmlStreamWriter &writer) { ElementWriter dummy(writer, QString::fromLatin1("blocklist")); QList blockList = m_db->m_blockList.toList(); // sort blocklist to get diffable files std::sort(blockList.begin(), blockList.end()); - Q_FOREACH (const DB::FileName &block, blockList) { + for (const DB::FileName &block : 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()) { 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; - Q_FOREACH (const QString &member, members) { + 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()); - Q_FOREACH (const QString &member, members) { + for (const QString &member : 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); QStringList grps = info->availableCategories(); - Q_FOREACH (const QString &name, grps) { + 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); } - Q_FOREACH (const QString &itemValue, items) { + for (const QString &itemValue : 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; QList categoryList = DB::ImageDB::instance()->categoryCollection()->categories(); - Q_FOREACH (const DB::CategoryPtr &category, categoryList) { + for (const DB::CategoryPtr &category : categoryList) { QString categoryName = category->name(); if (!shouldSaveCategory(categoryName)) continue; StringSet items = info->itemsOfCategory(categoryName); if (!items.empty()) { QStringList idList; - Q_FOREACH (const QString &itemValue, items) { + 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; }); - Q_FOREACH (const auto &positionedTag, areas) { + for (const auto &positionedTag : 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: diff --git a/XMLDB/XMLCategory.cpp b/XMLDB/XMLCategory.cpp index 64bb12ed..3a543a08 100644 --- a/XMLDB/XMLCategory.cpp +++ b/XMLDB/XMLCategory.cpp @@ -1,248 +1,250 @@ -/* 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. + 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 "XMLCategory.h" #include "Logging.h" #include #include #include XMLDB::XMLCategory::XMLCategory(const QString &name, const QString &icon, ViewType type, int thumbnailSize, bool show, bool positionable) : m_name(name) , m_icon(icon) , m_show(show) , m_type(type) , m_thumbnailSize(thumbnailSize) , m_positionable(positionable) , m_categoryType(DB::Category::PlainCategory) , m_shouldSave(true) { } QString XMLDB::XMLCategory::name() const { return m_name; } void XMLDB::XMLCategory::setName(const QString &name) { m_name = name; } void XMLDB::XMLCategory::setPositionable(bool positionable) { if (positionable != m_positionable) { m_positionable = positionable; emit changed(); } } bool XMLDB::XMLCategory::positionable() const { return m_positionable; } QString XMLDB::XMLCategory::iconName() const { return m_icon; } void XMLDB::XMLCategory::setIconName(const QString &name) { m_icon = name; emit changed(); } void XMLDB::XMLCategory::setViewType(ViewType type) { m_type = type; emit changed(); } XMLDB::XMLCategory::ViewType XMLDB::XMLCategory::viewType() const { return m_type; } void XMLDB::XMLCategory::setDoShow(bool b) { m_show = b; emit changed(); } bool XMLDB::XMLCategory::doShow() const { return m_show; } void XMLDB::XMLCategory::setType(DB::Category::CategoryType t) { m_categoryType = t; } DB::Category::CategoryType XMLDB::XMLCategory::type() const { return m_categoryType; } bool XMLDB::XMLCategory::isSpecialCategory() const { return m_categoryType != DB::Category::PlainCategory; } void XMLDB::XMLCategory::addOrReorderItems(const QStringList &items) { m_items = Utilities::mergeListsUniqly(items, m_items); } void XMLDB::XMLCategory::setItems(const QStringList &items) { m_items = items; } void XMLDB::XMLCategory::removeItem(const QString &item) { m_items.removeAll(item); m_nameMap.remove(idForName(item)); m_idMap.remove(item); emit itemRemoved(item); } void XMLDB::XMLCategory::renameItem(const QString &oldValue, const QString &newValue) { int id = idForName(oldValue); m_items.removeAll(oldValue); m_nameMap.remove(id); m_idMap.remove(oldValue); addItem(newValue); if (id > 0) setIdMapping(newValue, id); emit itemRenamed(oldValue, newValue); } void XMLDB::XMLCategory::addItem(const QString &item) { // for the "SortLastUsed" functionality in ListSelect we remove the item and insert it again: if (m_items.contains(item)) m_items.removeAll(item); m_items.prepend(item); } QStringList XMLDB::XMLCategory::items() const { return m_items; } int XMLDB::XMLCategory::idForName(const QString &name) const { Q_ASSERT(m_idMap.count(name) <= 1); return m_idMap[name]; } /** * @brief Make sure that the id/name mapping is a full mapping. */ void XMLDB::XMLCategory::initIdMap() { // find maximum id // obviously, this will leave gaps in numbering when tags are deleted // assuming that tags are seldomly removed this should not be a problem int i = 0; if (!m_nameMap.empty()) { i = m_nameMap.lastKey(); } - Q_FOREACH (const QString &tag, m_items) { + for (const QString &tag : m_items) { if (!m_idMap.contains(tag)) setIdMapping(tag, ++i); } const QStringList groups = DB::ImageDB::instance()->memberMap().groups(m_name); - Q_FOREACH (const QString &group, groups) { + for (const QString &group : groups) { if (!m_idMap.contains(group)) setIdMapping(group, ++i); } } void XMLDB::XMLCategory::setIdMapping(const QString &name, int id, IdMapping mode) { if (id <= 0) { if (mode == IdMapping::SafeMapping) { qCWarning(XMLDBLog, "XMLDB::XMLCategory::setIdMapping attempting to set id for %s to invalid value %d", qPrintable(name), id); } else { m_nameMap.insertMulti(id, name); m_idMap.insertMulti(name, id); } } else { m_nameMap.insert(id, name); m_idMap.insert(name, id); } } QString XMLDB::XMLCategory::nameForId(int id) const { Q_ASSERT(m_nameMap.count(id) <= 1); return m_nameMap[id]; } QStringList XMLDB::XMLCategory::namesForId(int id) const { return m_nameMap.values(id); } void XMLDB::XMLCategory::clearNullIds() { for (const auto &tag : namesForId(0)) { m_idMap.remove(tag); } m_nameMap.remove(0); } void XMLDB::XMLCategory::setThumbnailSize(int size) { m_thumbnailSize = size; emit changed(); } int XMLDB::XMLCategory::thumbnailSize() const { return m_thumbnailSize; } bool XMLDB::XMLCategory::shouldSave() { return m_shouldSave; } void XMLDB::XMLCategory::setShouldSave(bool b) { m_shouldSave = b; } void XMLDB::XMLCategory::setBirthDate(const QString &item, const QDate &birthDate) { m_birthDates.insert(item, birthDate); } QDate XMLDB::XMLCategory::birthDate(const QString &item) const { return m_birthDates[item]; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLCategoryCollection.cpp b/XMLDB/XMLCategoryCollection.cpp index 8a61513d..17d39659 100644 --- a/XMLDB/XMLCategoryCollection.cpp +++ b/XMLDB/XMLCategoryCollection.cpp @@ -1,109 +1,108 @@ -/* 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. + 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 "XMLCategoryCollection.h" - #include "XMLCategory.h" #include - #include DB::CategoryPtr XMLDB::XMLCategoryCollection::categoryForName(const QString &name) const { for (QList::ConstIterator it = m_categories.begin(); it != m_categories.end(); ++it) { if ((*it)->name() == name) return *it; } return DB::CategoryPtr(); } void XMLDB::XMLCategoryCollection::addCategory(DB::CategoryPtr category) { m_categories.append(category); if (category->isSpecialCategory()) { m_specialCategories[category->type()] = category; } connect(category.data(), &DB::Category::changed, this, &XMLCategoryCollection::categoryCollectionChanged); connect(category.data(), &DB::Category::itemRemoved, this, &XMLCategoryCollection::slotItemRemoved); connect(category.data(), &DB::Category::itemRenamed, this, &XMLCategoryCollection::slotItemRenamed); emit categoryCollectionChanged(); } QStringList XMLDB::XMLCategoryCollection::categoryNames() const { QStringList res; for (QList::ConstIterator it = m_categories.begin(); it != m_categories.end(); ++it) { res.append((*it)->name()); } return res; } QStringList XMLDB::XMLCategoryCollection::categoryTexts() const { QStringList res; for (QList::ConstIterator it = m_categories.begin(); it != m_categories.end(); ++it) { res.append((*it)->name()); } return res; } void XMLDB::XMLCategoryCollection::removeCategory(const QString &name) { for (QList::iterator it = m_categories.begin(); it != m_categories.end(); ++it) { if ((*it)->name() == name) { m_categories.erase(it); emit categoryRemoved(name); emit categoryCollectionChanged(); return; } } Q_ASSERT_X(false, "removeCategory", "trying to remove non-existing category"); } void XMLDB::XMLCategoryCollection::rename(const QString &oldName, const QString &newName) { categoryForName(oldName)->setName(newName); DB::ImageDB::instance()->renameCategory(oldName, newName); emit categoryCollectionChanged(); } QList XMLDB::XMLCategoryCollection::categories() const { return m_categories; } void XMLDB::XMLCategoryCollection::addCategory(const QString &text, const QString &icon, DB::Category::ViewType type, int thumbnailSize, bool show, bool positionable) { addCategory(DB::CategoryPtr(new XMLCategory(text, icon, type, thumbnailSize, show, positionable))); } DB::CategoryPtr XMLDB::XMLCategoryCollection::categoryForSpecial(const DB::Category::CategoryType type) const { return m_specialCategories[type]; } void XMLDB::XMLCategoryCollection::initIdMap() { - Q_FOREACH (DB::CategoryPtr categoryPtr, m_categories) { + for (DB::CategoryPtr categoryPtr : m_categories) { static_cast(categoryPtr.data())->initIdMap(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLImageDateCollection.cpp b/XMLDB/XMLImageDateCollection.cpp index f0f877de..8eb5951b 100644 --- a/XMLDB/XMLImageDateCollection.cpp +++ b/XMLDB/XMLImageDateCollection.cpp @@ -1,141 +1,142 @@ -/* 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 "XMLImageDateCollection.h" #include #include void XMLDB::XMLImageDateCollection::add(const DB::ImageDate &date) { m_startIndex.insertMulti(date.start(), date); } void XMLDB::XMLImageDateCollection::buildIndex() { StartIndexMap::ConstIterator startSearch = m_startIndex.constBegin(); QDateTime biggestEnd = QDateTime(QDate(1900, 1, 1)); for (StartIndexMap::ConstIterator it = m_startIndex.constBegin(); it != m_startIndex.constEnd(); ++it) { // We want a monotonic mapping end-date -> smallest-in-start-index. // Since we go through the start index sorted, lowest first, we just // have to keep the last pointer as long as we find smaller end-dates. // This should be rare as it only occurs if there are images that // actually represent a range not just a point in time. if (it.value().end() >= biggestEnd) { biggestEnd = it.value().end(); startSearch = it; } m_endIndex.insert(it.value().end(), startSearch); } } /** Previously, counting the elements was done by going through all elements and count the matches for a particular range, this unfortunately had O(n) complexity multiplied by m ranges we would get O(mn). Henner Zeller rewrote it to its current state. The main idea now is to have all dates sorted so that it is possible to only look at the requested range. Since it is not points in time, we can't have just a simple sorted list. So we have two sorted maps, the m_startIndex and m_endIndex. m_startIndex is sorted by the start time of all ImageDates (which are in fact ranges) If we would just look for Images that start _after_ the query-range, we would miscount, because there might be Image ranges starting before the query time but whose end time reaches into the query range this is what the m_endIndex is for: it is sortd by end-date; here we look for everything that is >= our query start. its value() part is basically a pointer to the position in the m_startIndex where we actually have to start looking. The rest is simple: we determine the interesting start in m_startIndex using the m_endIndex and iterate through it until the elements in that sorted list have a start time that is larger than the query-end-range .. there will no more elements coming. The above uses the fact that a QMap::constIterator iterates the map in sorted order. **/ DB::ImageCount XMLDB::XMLImageDateCollection::count(const DB::ImageDate &range) { if (m_cache.contains(range)) return m_cache[range]; int exact = 0, rangeMatch = 0; // We start searching in ranges that overlap our start search range, i.e. // where the end-date is higher than our search start. EndIndexMap::Iterator endSearch = m_endIndex.lowerBound(range.start()); if (endSearch != m_endIndex.end()) { for (StartIndexMap::ConstIterator it = endSearch.value(); it != m_startIndex.constEnd() && it.key() < range.end(); ++it) { DB::ImageDate::MatchType tp = it.value().isIncludedIn(range); switch (tp) { case DB::ImageDate::ExactMatch: exact++; break; case DB::ImageDate::RangeMatch: rangeMatch++; break; case DB::ImageDate::DontMatch: break; } } } DB::ImageCount res(exact, rangeMatch); m_cache.insert(range, res); // TODO(hzeller) this might go now return res; } QDateTime XMLDB::XMLImageDateCollection::lowerLimit() const { if (!m_startIndex.empty()) { // skip null dates: for (StartIndexMap::ConstIterator it = m_startIndex.constBegin(); it != m_startIndex.constEnd(); ++it) { if (it.key().isValid()) return it.key(); } } return QDateTime(QDate(1900, 1, 1)); } QDateTime XMLDB::XMLImageDateCollection::upperLimit() const { if (!m_endIndex.empty()) { EndIndexMap::ConstIterator highest = m_endIndex.constEnd(); --highest; return highest.key(); } return QDateTime(QDate(2100, 1, 1)); } XMLDB::XMLImageDateCollection::XMLImageDateCollection(const DB::FileNameList &list) { - Q_FOREACH (const DB::FileName &fileName, list) { + for (const DB::FileName &fileName : list) { add(fileName.info()->date()); } buildIndex(); } // vi:expandtab:tabstop=4 shiftwidth=4: