File indexing completed on 2024-04-28 15:39:44

0001 // SPDX-FileCopyrightText: 2003 David Faure <faure@kde.org>
0002 // SPDX-FileCopyrightText: 2003 Lukáš Tinkl <lukas@kde.org>
0003 // SPDX-FileCopyrightText: 2003-2005 Stephan Binner <binner@kde.org>
0004 // SPDX-FileCopyrightText: 2003-2013, 2019, 2022 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0005 // SPDX-FileCopyrightText: 2004 Andrew Coles <andrew.i.coles@googlemail.com>
0006 // SPDX-FileCopyrightText: 2005, 2007 Dirk Mueller <mueller@kde.org>
0007 // SPDX-FileCopyrightText: 2006-2008, 2010 Tuomas Suutari <tuomas@nepnep.net>
0008 // SPDX-FileCopyrightText: 2007, 2009 Laurent Montel <montel@kde.org>
0009 // SPDX-FileCopyrightText: 2007-2010 Jan Kundrát <jkt@flaska.net>
0010 // SPDX-FileCopyrightText: 2008 Henner Zeller <h.zeller@acm.org>
0011 // SPDX-FileCopyrightText: 2009-2010 Hassan Ibraheem <hasan.ibraheem@gmail.com>
0012 // SPDX-FileCopyrightText: 2010-2012 Miika Turkia <miika.turkia@gmail.com>
0013 // SPDX-FileCopyrightText: 2012 Andreas Neustifter <andreas.neustifter@gmail.com>
0014 // SPDX-FileCopyrightText: 2012-2024 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0015 // SPDX-FileCopyrightText: 2014 David Edmundson <kde@davidedmundson.co.uk>
0016 // SPDX-FileCopyrightText: 2014-2020 Tobias Leupold <tl@stonemx.de>
0017 // SPDX-FileCopyrightText: 2017 Raymond Wooninck <tittiatcoke@gmail.com>
0018 // SPDX-FileCopyrightText: 2017, 2019-2020 Robert Krawitz <rlk@alum.mit.edu>
0019 // SPDX-FileCopyrightText: 2018 Antoni Bella Pérez <antonibella5@yahoo.com>
0020 // SPDX-FileCopyrightText: 2022 Friedrich W. H. Kossebau <kossebau@kde.org>
0021 //
0022 // SPDX-License-Identifier: GPL-2.0-or-later
0023 
0024 #include "Dialog.h"
0025 
0026 #include "DateEdit.h"
0027 #include "DescriptionEdit.h"
0028 #include "ImagePreviewWidget.h"
0029 #include "ListSelect.h"
0030 #include "Logging.h"
0031 #include "ResizableFrame.h"
0032 #include "ShortCutManager.h"
0033 #include "ShowSelectionOnlyManager.h"
0034 
0035 #include <DB/CategoryCollection.h>
0036 #include <DB/ImageDB.h>
0037 #include <DB/ImageInfo.h>
0038 #include <MainWindow/DirtyIndicator.h>
0039 #include <Utilities/ShowBusyCursor.h>
0040 #include <Viewer/ViewerWidget.h>
0041 #include <kpabase/SettingsData.h>
0042 #include <kpabase/enums.h>
0043 
0044 #include <KAcceleratorManager>
0045 #include <KActionCollection>
0046 #include <KComboBox>
0047 #include <KConfigGroup>
0048 #include <KGuiItem>
0049 #include <KLineEdit>
0050 #include <KLocalizedString>
0051 #include <KMessageBox>
0052 #include <KRatingWidget>
0053 #include <KTextEdit>
0054 #include <QAction>
0055 #include <QApplication>
0056 #include <QCloseEvent>
0057 #include <QCursor>
0058 #include <QDialogButtonBox>
0059 #include <QDir>
0060 #include <QDockWidget>
0061 #include <QFile>
0062 #include <QFileInfo>
0063 #include <QHBoxLayout>
0064 #include <QLabel>
0065 #include <QList>
0066 #include <QMainWindow>
0067 #include <QMenu>
0068 #include <QPoint>
0069 #include <QPushButton>
0070 #include <QSpinBox>
0071 #include <QStackedWidget>
0072 #include <QStandardPaths>
0073 #include <QTimeEdit>
0074 #include <QVBoxLayout>
0075 #include <QtGlobal>
0076 #include <algorithm>
0077 #include <kwidgetsaddons_version.h>
0078 #include <tuple>
0079 
0080 #ifdef HAVE_MARBLE
0081 #include "Map/GeoCoordinates.h"
0082 #include <Map/MapView.h>
0083 #include <QProgressBar>
0084 #include <QTimer>
0085 #endif
0086 
0087 namespace
0088 {
0089 inline QPixmap smallIcon(const QString &iconName)
0090 {
0091     return QIcon::fromTheme(iconName).pixmap(KIconLoader::StdSizes::SizeSmall);
0092 }
0093 }
0094 
0095 using Utilities::StringSet;
0096 
0097 /**
0098  * \class AnnotationDialog::Dialog
0099  * \brief QDialog subclass used for tagging images
0100  */
0101 
0102 AnnotationDialog::Dialog::Dialog(QWidget *parent)
0103     : QDialog(parent)
0104     , m_ratingChanged(false)
0105     , m_conflictText(i18n("(You have differing descriptions on individual images, setting text here will override them all)"))
0106 {
0107     Utilities::ShowBusyCursor dummy;
0108     ShortCutManager shortCutManager;
0109 
0110     m_actions = new KActionCollection(this);
0111 
0112     // The widget stack
0113     QWidget *mainWidget = new QWidget(this);
0114     QVBoxLayout *layout = new QVBoxLayout(mainWidget);
0115     setLayout(layout);
0116     layout->addWidget(mainWidget);
0117     m_stack = new QStackedWidget(mainWidget);
0118     layout->addWidget(m_stack);
0119 
0120     // The Viewer
0121     m_fullScreenPreview = new Viewer::ViewerWidget(Viewer::ViewerWidget::UsageType::FullsizePreview);
0122     m_stack->addWidget(m_fullScreenPreview);
0123 
0124     // The dock widget
0125     m_dockWindow = new QMainWindow;
0126     m_stack->addWidget(m_dockWindow);
0127     m_dockWindow->setDockNestingEnabled(true);
0128 
0129     // -------------------------------------------------- Dock widgets
0130     m_generalDock = createDock(i18n("Label and Dates"), QString::fromLatin1("Label and Dates"), Qt::TopDockWidgetArea, createDateWidget(shortCutManager));
0131 
0132     m_previewDock = createDock(i18n("Image Preview"), QString::fromLatin1("Image Preview"), Qt::TopDockWidgetArea, createPreviewWidget());
0133 
0134     m_description = new DescriptionEdit(this);
0135     m_description->setWhatsThis(i18nc("@info:whatsthis",
0136                                       "<para>A descriptive text of the image.</para>"
0137                                       "<para>If <emphasis>Use Exif description</emphasis> is enabled under "
0138                                       "<interface>Settings|Configure KPhotoAlbum...|General</interface>, a description "
0139                                       "embedded in the image Exif information is imported to this field if available.</para>"));
0140 
0141     m_descriptionDock = createDock(i18n("Description"), QString::fromLatin1("description"), Qt::LeftDockWidgetArea, m_description);
0142     shortCutManager.addDock(m_descriptionDock, m_description);
0143 
0144     connect(m_description, &DescriptionEdit::pageUpDownPressed, this, &Dialog::descriptionPageUpDownPressed);
0145 
0146 #ifdef HAVE_MARBLE
0147     // -------------------------------------------------- Map representation
0148 
0149     m_annotationMapContainer = new QWidget(this);
0150     QVBoxLayout *annotationMapContainerLayout = new QVBoxLayout(m_annotationMapContainer);
0151 
0152     m_annotationMap = new Map::MapView(this, Map::UsageType::InlineMapView);
0153     annotationMapContainerLayout->addWidget(m_annotationMap);
0154 
0155     QHBoxLayout *mapLoadingProgressLayout = new QHBoxLayout();
0156     annotationMapContainerLayout->addLayout(mapLoadingProgressLayout);
0157 
0158     m_mapLoadingProgress = new QProgressBar(this);
0159     mapLoadingProgressLayout->addWidget(m_mapLoadingProgress);
0160     m_mapLoadingProgress->hide();
0161 
0162     m_cancelMapLoadingButton = new QPushButton(i18n("Cancel"));
0163     mapLoadingProgressLayout->addWidget(m_cancelMapLoadingButton);
0164     m_cancelMapLoadingButton->hide();
0165     connect(m_cancelMapLoadingButton, &QPushButton::clicked, this, &Dialog::setCancelMapLoading);
0166 
0167     m_annotationMapContainer->setObjectName(i18n("Map"));
0168     m_mapDock = createDock(
0169         i18n("Map"),
0170         QString::fromLatin1("map"),
0171         Qt::LeftDockWidgetArea,
0172         m_annotationMapContainer);
0173     shortCutManager.addDock(m_mapDock, m_annotationMapContainer);
0174     connect(m_mapDock, &QDockWidget::visibilityChanged, this, &Dialog::annotationMapVisibilityChanged);
0175     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."));
0176 #endif
0177 
0178     // -------------------------------------------------- Categories
0179     QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories();
0180 
0181     // Let's first assume we don't have positionable categories
0182     m_positionableCategories = false;
0183 
0184     for (QList<DB::CategoryPtr>::ConstIterator categoryIt = categories.constBegin(); categoryIt != categories.constEnd(); ++categoryIt) {
0185         ListSelect *sel = createListSel(*categoryIt);
0186 
0187         // Create a QMap of all ListSelect instances, so that we can easily
0188         // check if a specific (positioned) tag is (still) selected later
0189         m_listSelectList[(*categoryIt)->name()] = sel;
0190 
0191         QDockWidget *dock = createDock((*categoryIt)->name(),
0192                                        (*categoryIt)->name(),
0193                                        Qt::BottomDockWidgetArea,
0194                                        sel);
0195         shortCutManager.addDock(dock, sel->lineEdit());
0196 
0197         if ((*categoryIt)->isSpecialCategory())
0198             dock->hide();
0199 
0200         // Pass the positionable selection to the object
0201         sel->setPositionable((*categoryIt)->positionable());
0202 
0203         if (sel->positionable()) {
0204             connect(sel, &ListSelect::positionableTagSelected, this, &Dialog::positionableTagSelected);
0205             connect(sel, &ListSelect::positionableTagDeselected, this, &Dialog::positionableTagDeselected);
0206             connect(sel, &ListSelect::positionableTagRenamed, this, &Dialog::positionableTagRenamed);
0207 
0208             connect(m_preview->preview(), &ImagePreview::proposedTagSelected, sel, &ListSelect::ensureTagIsSelected);
0209 
0210             // We have at least one positionable category
0211             m_positionableCategories = true;
0212         }
0213     }
0214 
0215     // -------------------------------------------------- The buttons.
0216     // don't use default buttons (Ok, Cancel):
0217     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::NoButton);
0218     connect(buttonBox, &QDialogButtonBox::accepted, this, &Dialog::accept);
0219     connect(buttonBox, &QDialogButtonBox::rejected, this, &Dialog::reject);
0220     QHBoxLayout *lay1 = new QHBoxLayout;
0221     layout->addLayout(lay1);
0222 
0223     m_revertBut = new QPushButton(i18n("Revert This Item"));
0224     KAcceleratorManager::setNoAccel(m_revertBut);
0225     lay1->addWidget(m_revertBut);
0226 
0227     m_clearBut = new QPushButton();
0228     KGuiItem::assign(m_clearBut,
0229                      KGuiItem(i18n("Clear Form"), QApplication::isRightToLeft() ? QString::fromLatin1("clear_left") : QString::fromLatin1("locationbar_erase")));
0230     KAcceleratorManager::setNoAccel(m_clearBut);
0231     lay1->addWidget(m_clearBut);
0232 
0233     QPushButton *optionsBut = new QPushButton(i18n("Options..."));
0234     KAcceleratorManager::setNoAccel(optionsBut);
0235     lay1->addWidget(optionsBut);
0236 
0237     lay1->addStretch(1);
0238 
0239     m_okBut = new QPushButton(i18n("&Done"));
0240     lay1->addWidget(m_okBut);
0241 
0242     m_continueLaterBut = new QPushButton(i18n("Continue &Later"));
0243     lay1->addWidget(m_continueLaterBut);
0244 
0245     QPushButton *cancelBut = new QPushButton();
0246     KGuiItem::assign(cancelBut, KStandardGuiItem::cancel());
0247     lay1->addWidget(cancelBut);
0248 
0249     // It is unfortunately not possible to ask KAcceleratorManager not to setup the OK and cancel keys.
0250     shortCutManager.addTaken(i18nc("@action:button", "&Search"));
0251     shortCutManager.addTaken(m_okBut->text());
0252     shortCutManager.addTaken(m_continueLaterBut->text());
0253     shortCutManager.addTaken(cancelBut->text());
0254 
0255     connect(m_revertBut, &QPushButton::clicked, this, &Dialog::slotRevert);
0256     connect(m_okBut, &QPushButton::clicked, this, &Dialog::doneTagging);
0257     connect(m_continueLaterBut, &QPushButton::clicked, this, &Dialog::continueLater);
0258     connect(cancelBut, &QPushButton::clicked, this, &Dialog::reject);
0259     connect(m_clearBut, &QPushButton::clicked, this, &Dialog::slotClearSearchForm);
0260     connect(optionsBut, &QPushButton::clicked, this, &Dialog::slotOptions);
0261 
0262     connect(m_preview, &ImagePreviewWidget::imageRotated, this, &Dialog::rotate);
0263     connect(m_preview, &ImagePreviewWidget::indexChanged, this, &Dialog::slotIndexChanged);
0264     connect(m_preview, &ImagePreviewWidget::copyPrevClicked, this, &Dialog::slotCopyPrevious);
0265     connect(m_preview, &ImagePreviewWidget::areaVisibilityChanged, this, &Dialog::slotShowAreas);
0266     connect(m_preview->preview(), &ImagePreview::areaCreated, this, &Dialog::slotNewArea);
0267 
0268     // Disable so no button accept return (which would break with the line edits)
0269     m_revertBut->setAutoDefault(false);
0270     m_okBut->setAutoDefault(false);
0271     m_continueLaterBut->setAutoDefault(false);
0272     cancelBut->setAutoDefault(false);
0273     m_clearBut->setAutoDefault(false);
0274     optionsBut->setAutoDefault(false);
0275 
0276     m_dockWindowCleanState = m_dockWindow->saveState();
0277 
0278     loadWindowLayout();
0279 
0280     m_current = -1;
0281 
0282     setGeometry(Settings::SettingsData::instance()->windowGeometry(Settings::AnnotationDialog));
0283 
0284     setupActions();
0285     shortCutManager.setupShortCuts();
0286 
0287     layout->addWidget(buttonBox);
0288 
0289     connect(DB::ImageDB::instance(), &DB::ImageDB::imagesDeleted, this, &Dialog::slotDiscardFiles);
0290 }
0291 
0292 QDockWidget *AnnotationDialog::Dialog::createDock(const QString &title, const QString &name,
0293                                                   Qt::DockWidgetArea location, QWidget *widget)
0294 {
0295     qCDebug(AnnotationDialogLog) << "Creating dock widget. Title:" << title << ", name:" << name << ", location:" << location;
0296     QDockWidget *dock = new QDockWidget(title);
0297     // make sure that no accelerator is set up now - this is done by ShortCutManager instead:
0298     KAcceleratorManager::setNoAccel(dock);
0299     dock->setObjectName(name);
0300     dock->setAllowedAreas(Qt::AllDockWidgetAreas);
0301     dock->setWidget(widget);
0302     m_dockWindow->addDockWidget(location, dock);
0303     m_dockWidgets.append(dock);
0304     return dock;
0305 }
0306 
0307 QWidget *AnnotationDialog::Dialog::createDateWidget(ShortCutManager &shortCutManager)
0308 {
0309     QWidget *top = new QWidget;
0310     QVBoxLayout *lay2 = new QVBoxLayout(top);
0311 
0312     // Image Label
0313     QHBoxLayout *lay3 = new QHBoxLayout;
0314     lay2->addLayout(lay3);
0315 
0316     QLabel *label = new QLabel(i18n("Label: "));
0317     lay3->addWidget(label);
0318     m_imageLabel = new KLineEdit;
0319     m_imageLabel->setProperty("WantsFocus", true);
0320     m_imageLabel->setObjectName(i18n("Label"));
0321     lay3->addWidget(m_imageLabel);
0322     shortCutManager.addLabel(label);
0323     label->setBuddy(m_imageLabel);
0324 
0325     // Date
0326     QHBoxLayout *lay4 = new QHBoxLayout;
0327     lay2->addLayout(lay4);
0328 
0329     label = new QLabel(i18n("Date: "));
0330     lay4->addWidget(label);
0331 
0332     m_startDate = new ::AnnotationDialog::DateEdit(true);
0333     lay4->addWidget(m_startDate, 1);
0334     connect(m_startDate, QOverload<const DB::ImageDate &>::of(&DateEdit::dateChanged), this, &Dialog::slotStartDateChanged);
0335     shortCutManager.addLabel(label);
0336     label->setBuddy(m_startDate);
0337 
0338     m_endDateLabel = new QLabel(QString::fromLatin1("-"));
0339     lay4->addWidget(m_endDateLabel);
0340 
0341     m_endDate = new ::AnnotationDialog::DateEdit(false);
0342     lay4->addWidget(m_endDate, 1);
0343 
0344     // Time
0345     m_timeLabel = new QLabel(i18n("Time: "));
0346     lay4->addWidget(m_timeLabel);
0347 
0348     m_time = new QTimeEdit;
0349     lay4->addWidget(m_time);
0350 
0351     m_isFuzzyDate = new QCheckBox(i18n("Use Fuzzy Date"));
0352     m_isFuzzyDate->setWhatsThis(i18nc("@info",
0353                                       "<para>In KPhotoAlbum, images can either have an exact date and time"
0354                                       ", or a <emphasis>fuzzy</emphasis> date which happened any time during"
0355                                       " a specified time interval. Images produced by digital cameras"
0356                                       " do normally have an exact date.</para>"
0357                                       "<para>If you don't know exactly when a photo was taken"
0358                                       " (e.g. if the photo comes from an analog camera), then you should set"
0359                                       " <interface>Use Fuzzy Date</interface>.</para>"));
0360     m_isFuzzyDate->setToolTip(m_isFuzzyDate->whatsThis());
0361     lay4->addWidget(m_isFuzzyDate);
0362     lay4->addStretch(1);
0363     connect(m_isFuzzyDate, &QCheckBox::stateChanged, this, &Dialog::slotSetFuzzyDate);
0364 
0365     QHBoxLayout *lay8 = new QHBoxLayout;
0366     lay2->addLayout(lay8);
0367 
0368     m_megapixelLabel = new QLabel(i18n("Minimum megapixels:"));
0369     lay8->addWidget(m_megapixelLabel);
0370 
0371     m_megapixel = new QSpinBox;
0372     m_megapixel->setRange(0, 99);
0373     m_megapixel->setSingleStep(1);
0374     m_megapixelLabel->setBuddy(m_megapixel);
0375     lay8->addWidget(m_megapixel);
0376     lay8->addStretch(1);
0377 
0378     m_max_megapixelLabel = new QLabel(i18n("Maximum megapixels:"));
0379     lay8->addWidget(m_max_megapixelLabel);
0380 
0381     m_max_megapixel = new QSpinBox;
0382     m_max_megapixel->setRange(0, 99);
0383     m_max_megapixel->setSingleStep(1);
0384     m_max_megapixelLabel->setBuddy(m_max_megapixel);
0385     lay8->addWidget(m_max_megapixel);
0386     lay8->addStretch(1);
0387 
0388     QHBoxLayout *lay9 = new QHBoxLayout;
0389     lay2->addLayout(lay9);
0390 
0391     label = new QLabel(i18n("Rating:"));
0392     lay9->addWidget(label);
0393     m_rating = new KRatingWidget;
0394     m_rating->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0395     lay9->addWidget(m_rating, 0, Qt::AlignCenter);
0396     // cast for versions of KRatingWidget where ratingChanged(uint) is still a thing:
0397     connect(m_rating, static_cast<void (KRatingWidget::*)(int)>(&KRatingWidget::ratingChanged), this, &Dialog::slotRatingChanged);
0398 
0399     m_ratingSearchLabel = new QLabel(i18n("Rating search mode:"));
0400     lay9->addWidget(m_ratingSearchLabel);
0401 
0402     m_ratingSearchMode = new KComboBox(lay9);
0403     m_ratingSearchMode->addItems(QStringList() << i18n("==") << i18n(">=") << i18n("<=") << i18n("!="));
0404     m_ratingSearchLabel->setBuddy(m_ratingSearchMode);
0405     lay9->addWidget(m_ratingSearchMode);
0406 
0407     // File name search pattern
0408     QHBoxLayout *lay10 = new QHBoxLayout;
0409     lay2->addLayout(lay10);
0410 
0411     m_imageFilePatternLabel = new QLabel(i18n("File Name Pattern: "));
0412     lay10->addWidget(m_imageFilePatternLabel);
0413     m_imageFilePattern = new KLineEdit;
0414     m_imageFilePattern->setObjectName(i18n("File Name Pattern"));
0415     lay10->addWidget(m_imageFilePattern);
0416     shortCutManager.addLabel(m_imageFilePatternLabel);
0417     m_imageFilePatternLabel->setBuddy(m_imageFilePattern);
0418 
0419     m_searchRAW = new QCheckBox(i18n("Search only for RAW files"));
0420     lay2->addWidget(m_searchRAW);
0421 
0422     lay9->addStretch(1);
0423     lay2->addStretch(1);
0424 
0425     return top;
0426 }
0427 
0428 QWidget *AnnotationDialog::Dialog::createPreviewWidget()
0429 {
0430     m_preview = new ImagePreviewWidget(m_actions);
0431     connect(m_preview, &ImagePreviewWidget::togglePreview, this, &Dialog::togglePreview);
0432     return m_preview;
0433 }
0434 
0435 void AnnotationDialog::Dialog::slotRevert()
0436 {
0437     if (m_setup == InputSingleImageConfigMode)
0438         load();
0439 }
0440 
0441 void AnnotationDialog::Dialog::slotIndexChanged(int index)
0442 {
0443     if (m_setup != InputSingleImageConfigMode)
0444         return;
0445 
0446     if (m_current >= 0)
0447         writeToInfo();
0448 
0449     m_current = index;
0450 
0451     load();
0452 }
0453 
0454 void AnnotationDialog::Dialog::doneTagging()
0455 {
0456     saveAndClose();
0457     if (DB::ImageDB::instance()->untaggedCategoryFeatureConfigured()) {
0458         for (DB::ImageInfoListIterator it = m_origList.begin(); it != m_origList.end(); ++it) {
0459             (*it)->removeCategoryInfo(Settings::SettingsData::instance()->untaggedCategory(),
0460                                       Settings::SettingsData::instance()->untaggedTag());
0461         }
0462     }
0463 }
0464 
0465 /*
0466  * Copy tags (only tags/categories, not description/label/...) from previous image to the currently showed one
0467  */
0468 void AnnotationDialog::Dialog::slotCopyPrevious()
0469 {
0470     if (m_setup != InputSingleImageConfigMode)
0471         return;
0472     if (m_current < 1)
0473         return;
0474 
0475     // (jzarl 2020-07-26): defining the "previous image" as the one before this is the behaviour of the least surprise:
0476     DB::ImageInfo &old_info = m_editList[m_current - 1];
0477 
0478     m_positionableTagCandidates.clear();
0479     m_lastSelectedPositionableTag.first = QString();
0480     m_lastSelectedPositionableTag.second = QString();
0481 
0482     for (ListSelect *ls : qAsConst(m_optionList)) {
0483         ls->setSelection(old_info.itemsOfCategory(ls->category()));
0484 
0485         // Also set all positionable tag candidates
0486 
0487         if (ls->positionable()) {
0488             const QString category = ls->category();
0489             const QSet<QString> selectedTags = old_info.itemsOfCategory(category);
0490             const QSet<QString> positionedTagSet = positionedTags(category);
0491 
0492             // Add the tag to the positionable candiate list, if no area is already associated with it
0493             for (const auto &tag : selectedTags) {
0494                 if (!positionedTagSet.contains(tag)) {
0495                     addTagToCandidateList(category, tag);
0496                 }
0497             }
0498 
0499             // Check all areas for a linked tag in this category that is probably not selected anymore
0500             const auto allAreas = areas();
0501             for (ResizableFrame *area : allAreas) {
0502                 QPair<QString, QString> tagData = area->tagData();
0503 
0504                 if (tagData.first == category) {
0505                     if (!selectedTags.contains(tagData.second)) {
0506                         // The linked tag is not selected anymore, so remove it
0507                         area->removeTagData();
0508                     }
0509                 }
0510             }
0511         }
0512     }
0513 }
0514 
0515 void AnnotationDialog::Dialog::load()
0516 {
0517     if (m_current < 0)
0518         return;
0519 
0520     // Remove all areas
0521     tidyAreas();
0522 
0523     // Empty the positionable tag candidate list and the last selected positionable tag
0524     m_positionableTagCandidates.clear();
0525     m_lastSelectedPositionableTag = QPair<QString, QString>();
0526 
0527     DB::ImageInfo &info = m_editList[m_current];
0528     m_startDate->setDate(info.date().start().date());
0529 
0530     if (info.date().hasValidTime()) {
0531         m_time->show();
0532         m_time->setTime(info.date().start().time());
0533         m_isFuzzyDate->setChecked(false);
0534     } else {
0535         m_time->hide();
0536         m_isFuzzyDate->setChecked(true);
0537     }
0538 
0539     if (info.date().start().date() == info.date().end().date())
0540         m_endDate->setDate(QDate());
0541     else
0542         m_endDate->setDate(info.date().end().date());
0543 
0544     m_imageLabel->setText(info.label());
0545     m_description->setDescription(info.description());
0546 
0547     if (m_setup == InputSingleImageConfigMode)
0548         m_rating->setRating(qMax(static_cast<short int>(0), info.rating()));
0549     m_ratingChanged = false;
0550 
0551     // A category areas have been linked against could have been deleted
0552     // or un-marked as positionable in the meantime, so ...
0553     QMap<QString, bool> categoryIsPositionable;
0554 
0555     QList<QString> positionableCategories;
0556 
0557     for (ListSelect *ls : qAsConst(m_optionList)) {
0558         ls->setSelection(info.itemsOfCategory(ls->category()));
0559         ls->rePopulate();
0560 
0561         // Get all selected positionable tags and add them to the candidate list
0562         if (ls->positionable()) {
0563             const QSet<QString> selectedTags = ls->itemsOn();
0564 
0565             for (const QString &tagName : selectedTags) {
0566                 addTagToCandidateList(ls->category(), tagName);
0567             }
0568         }
0569 
0570         // ... create a list of all categories and their positionability ...
0571         categoryIsPositionable[ls->category()] = ls->positionable();
0572 
0573         if (ls->positionable()) {
0574             positionableCategories << ls->category();
0575         }
0576     }
0577 
0578     // Create all tagged areas
0579 
0580     DB::TaggedAreas taggedAreas = info.taggedAreas();
0581     DB::TaggedAreasIterator areasInCategory(taggedAreas);
0582 
0583     while (areasInCategory.hasNext()) {
0584         areasInCategory.next();
0585         QString category = areasInCategory.key();
0586 
0587         // ... and check if the respective category is actually there yet and still positionable
0588         // (operator[] will insert an empty item if the category has been deleted
0589         // and is thus missing in the QMap, but the respective key won't be true)
0590         if (categoryIsPositionable[category]) {
0591             DB::PositionTagsIterator areaData(areasInCategory.value());
0592             while (areaData.hasNext()) {
0593                 areaData.next();
0594                 QString tag = areaData.key();
0595 
0596                 // Be sure that the corresponding tag is still checked. The category could have
0597                 // been un-marked as positionable in the meantime and the tag could have been
0598                 // deselected, without triggering positionableTagDeselected and the area thus
0599                 // still remaining. If the category is then re-marked as positionable, the area would
0600                 // show up without the tag being selected.
0601                 if (m_listSelectList[category]->tagIsChecked(tag)) {
0602                     m_preview->preview()->createTaggedArea(category, tag, areaData.value(), m_preview->showAreas());
0603                 }
0604             }
0605         }
0606     }
0607 
0608     if (m_setup == InputSingleImageConfigMode) {
0609         setWindowTitle(i18nc("@title:window image %1 of %2 images", "Annotations (%1/%2)",
0610                              m_current + 1,
0611                              m_origList.count()));
0612         m_preview->canCreateAreas(
0613             m_setup == InputSingleImageConfigMode && !info.isVideo() && m_positionableCategories);
0614 #ifdef HAVE_MARBLE
0615         updateMapForCurrentImage();
0616 #endif
0617     }
0618 
0619     m_preview->updatePositionableCategories(positionableCategories);
0620 }
0621 
0622 void AnnotationDialog::Dialog::writeToInfo()
0623 {
0624     if (m_current + 1 > m_editList.size())
0625         return;
0626 
0627     for (ListSelect *ls : qAsConst(m_optionList)) {
0628         ls->slotReturn();
0629     }
0630 
0631     DB::ImageInfo &info = m_editList[m_current];
0632 
0633     if (!info.size().isValid()) {
0634         // The actual image size has been fetched by ImagePreview, so we can add it to
0635         // the database silenty, so that it's saved if the database will be saved.
0636         info.setSize(m_preview->preview()->getActualImageSize());
0637     }
0638 
0639     if (m_time->isHidden()) {
0640         if (m_endDate->date().isValid())
0641             info.setDate(DB::ImageDate(Utilities::FastDateTime(m_startDate->date(), QTime(0, 0, 0)),
0642                                        Utilities::FastDateTime(m_endDate->date(), QTime(23, 59, 59))));
0643         else
0644             info.setDate(DB::ImageDate(Utilities::FastDateTime(m_startDate->date(), QTime(0, 0, 0)),
0645                                        Utilities::FastDateTime(m_startDate->date(), QTime(23, 59, 59))));
0646     } else
0647         info.setDate(DB::ImageDate(Utilities::FastDateTime(m_startDate->date(), m_time->time())));
0648 
0649     // Generate a list of all tagged areas
0650 
0651     DB::TaggedAreas areas = taggedAreas();
0652 
0653     info.setLabel(m_imageLabel->text());
0654     info.setDescription(m_description->description());
0655 
0656     for (const ListSelect *ls : qAsConst(m_optionList)) {
0657         info.setCategoryInfo(ls->category(), ls->itemsOn());
0658         if (ls->positionable()) {
0659             info.setPositionedTags(ls->category(), areas[ls->category()]);
0660         }
0661     }
0662 
0663     if (m_ratingChanged) {
0664         info.setRating(m_rating->rating());
0665         m_ratingChanged = false;
0666     }
0667 }
0668 
0669 void AnnotationDialog::Dialog::ShowHideSearch(bool show)
0670 {
0671     m_megapixel->setVisible(show);
0672     m_megapixelLabel->setVisible(show);
0673     m_max_megapixel->setVisible(show);
0674     m_max_megapixelLabel->setVisible(show);
0675     m_searchRAW->setVisible(show);
0676     m_imageFilePatternLabel->setVisible(show);
0677     m_imageFilePattern->setVisible(show);
0678     m_isFuzzyDate->setChecked(show);
0679     m_isFuzzyDate->setVisible(!show);
0680     slotSetFuzzyDate();
0681     m_ratingSearchMode->setVisible(show);
0682     m_ratingSearchLabel->setVisible(show);
0683 }
0684 
0685 #ifdef HAVE_MARBLE
0686 void AnnotationDialog::Dialog::clearMapData()
0687 {
0688     m_annotationMap->clear();
0689     m_mapIsPopulated = false;
0690 }
0691 #endif
0692 
0693 QList<AnnotationDialog::ResizableFrame *> AnnotationDialog::Dialog::areas() const
0694 {
0695     return m_preview->preview()->findChildren<ResizableFrame *>();
0696 }
0697 
0698 DB::TaggedAreas AnnotationDialog::Dialog::taggedAreas() const
0699 {
0700     DB::TaggedAreas taggedAreas;
0701     const auto allAreas = areas();
0702     for (ResizableFrame *area : allAreas) {
0703         QPair<QString, QString> tagData = area->tagData();
0704         if (!tagData.first.isEmpty()) {
0705             taggedAreas[tagData.first][tagData.second] = area->actualCoordinates();
0706         }
0707     }
0708     return taggedAreas;
0709 }
0710 
0711 int AnnotationDialog::Dialog::configure(DB::ImageInfoList list, bool oneAtATime)
0712 {
0713     Q_ASSERT(!list.isEmpty());
0714     ShowHideSearch(false);
0715 
0716     if (oneAtATime) {
0717         m_setup = InputSingleImageConfigMode;
0718     } else {
0719         m_setup = InputMultiImageConfigMode;
0720         // Hide the default positionable category selector
0721         m_preview->updatePositionableCategories();
0722     }
0723 
0724 #ifdef HAVE_MARBLE
0725     clearMapData();
0726 #endif
0727     m_origList = list;
0728     m_editList.clear();
0729 
0730     for (DB::ImageInfoListConstIterator it = list.constBegin(); it != list.constEnd(); ++it) {
0731         m_editList.append(*(*it));
0732     }
0733 
0734     setup();
0735 
0736     if (oneAtATime) {
0737         m_current = 0;
0738         m_preview->configure(&m_editList, true);
0739         load();
0740     } else {
0741         m_preview->configure(&m_editList, false);
0742         m_preview->canCreateAreas(false);
0743         m_startDate->setDate(QDate());
0744         m_endDate->setDate(QDate());
0745         m_time->hide();
0746         m_rating->setRating(0);
0747         m_ratingChanged = false;
0748         m_areasChanged = false;
0749 
0750         for (ListSelect *ls : qAsConst(m_optionList)) {
0751             setUpCategoryListBoxForMultiImageSelection(ls, list);
0752         }
0753 
0754         m_imageLabel->setText(QString());
0755         m_imageFilePattern->setText(QString());
0756         const QString &firstDescription = m_editList[0].description();
0757 
0758         const bool allTextEqual = std::all_of(m_editList.begin(), m_editList.end(),
0759                                               [=](const DB::ImageInfo &item) -> bool {
0760                                                   return item.description() == firstDescription;
0761                                               });
0762 
0763         if (!allTextEqual)
0764             m_description->setConflictWarning(m_conflictText);
0765         else
0766             m_description->setDescription(firstDescription);
0767     }
0768 
0769     showHelpDialog(oneAtATime ? InputSingleImageConfigMode : InputMultiImageConfigMode);
0770 
0771     return exec();
0772 }
0773 
0774 DB::ImageSearchInfo AnnotationDialog::Dialog::search(DB::ImageSearchInfo *search)
0775 {
0776     ShowHideSearch(true);
0777 
0778 #ifdef HAVE_MARBLE
0779     clearMapData();
0780 #endif
0781     m_setup = SearchMode;
0782     if (search)
0783         m_oldSearch = *search;
0784 
0785     setup();
0786 
0787     m_preview->setImage(QStandardPaths::locate(QStandardPaths::DataLocation, QString::fromLatin1("pics/search.jpg")));
0788 
0789     m_ratingChanged = false;
0790     showHelpDialog(SearchMode);
0791     int ok = exec();
0792     if (ok == QDialog::Accepted) {
0793         const QDate start = m_startDate->date();
0794         const QDate end = m_endDate->date();
0795         m_oldSearch = DB::ImageSearchInfo(DB::ImageDate(start, end),
0796                                           m_imageLabel->text(), m_description->description(),
0797                                           m_imageFilePattern->text());
0798 
0799         for (const ListSelect *ls : qAsConst(m_optionList)) {
0800             m_oldSearch.setCategoryMatchText(ls->category(), ls->text());
0801         }
0802         // FIXME: for the user to search for 0-rated images, he must first change the rating to anything > 0
0803         // then change back to 0 .
0804         if (m_ratingChanged)
0805             m_oldSearch.setRating(m_rating->rating());
0806 
0807         m_ratingChanged = false;
0808         m_oldSearch.setSearchMode(m_ratingSearchMode->currentIndex());
0809         m_oldSearch.setMegaPixel(m_megapixel->value());
0810         m_oldSearch.setMaxMegaPixel(m_max_megapixel->value());
0811         m_oldSearch.setSearchRAW(m_searchRAW->isChecked());
0812 #ifdef HAVE_MARBLE
0813         const Map::GeoCoordinates::LatLonBox regionSelection = m_annotationMap->getRegionSelection();
0814         m_oldSearch.setRegionSelection(regionSelection);
0815 #endif
0816         return m_oldSearch;
0817     } else
0818         return DB::ImageSearchInfo();
0819 }
0820 
0821 void AnnotationDialog::Dialog::setup()
0822 {
0823     // Repopulate the listboxes in case data has changed
0824     // An group might for example have been renamed.
0825     for (ListSelect *ls : qAsConst(m_optionList)) {
0826         ls->populate();
0827     }
0828 
0829     if (m_setup == SearchMode) {
0830         KGuiItem::assign(m_okBut, KGuiItem(i18nc("@action:button", "&Search"), QString::fromLatin1("find")));
0831         m_continueLaterBut->hide();
0832         m_revertBut->hide();
0833         m_clearBut->show();
0834         m_preview->setSearchMode(true);
0835         setWindowTitle(i18nc("@title:window title of the 'find images' window", "Search"));
0836         loadInfo(m_oldSearch);
0837     } else {
0838         m_okBut->setText(i18n("Done"));
0839         m_continueLaterBut->show();
0840         m_revertBut->setEnabled(m_setup == InputSingleImageConfigMode);
0841         m_clearBut->hide();
0842         m_revertBut->show();
0843         m_preview->setSearchMode(false);
0844         m_preview->setToggleFullscreenPreviewEnabled(m_setup == InputSingleImageConfigMode);
0845         setWindowTitle(i18nc("@title:window", "Annotations"));
0846     }
0847 
0848     for (ListSelect *ls : qAsConst(m_optionList)) {
0849         ls->setMode(m_setup);
0850     }
0851 }
0852 
0853 void AnnotationDialog::Dialog::slotClearSearchForm()
0854 {
0855     loadInfo(DB::ImageSearchInfo());
0856 }
0857 
0858 void AnnotationDialog::Dialog::loadInfo(const DB::ImageSearchInfo &info)
0859 {
0860     m_startDate->setDate(info.date().start().date());
0861     m_endDate->setDate(info.date().end().date());
0862 
0863     for (ListSelect *ls : qAsConst(m_optionList)) {
0864         ls->setText(info.categoryMatchText(ls->category()));
0865     }
0866 
0867     m_imageLabel->setText(info.label());
0868     m_description->setDescription(info.description());
0869 }
0870 
0871 void AnnotationDialog::Dialog::slotOptions()
0872 {
0873     // create menu entries for dock windows
0874     QMenu *menu = new QMenu(this);
0875     QMenu *dockMenu = m_dockWindow->createPopupMenu();
0876     menu->addMenu(dockMenu)
0877         ->setText(i18n("Configure Window Layout..."));
0878     QAction *saveCurrent = dockMenu->addAction(i18n("Save Current Window Setup"));
0879     QAction *reset = dockMenu->addAction(i18n("Reset layout"));
0880 
0881     // create SortType entries
0882     menu->addSeparator();
0883     QActionGroup *sortTypes = new QActionGroup(menu);
0884     QAction *alphaTreeSort = new QAction(
0885         smallIcon(QString::fromLatin1("view-list-tree")),
0886         i18n("Sort Alphabetically (Tree)"),
0887         sortTypes);
0888     QAction *alphaFlatSort = new QAction(
0889         smallIcon(QString::fromLatin1("draw-text")),
0890         i18n("Sort Alphabetically (Flat)"),
0891         sortTypes);
0892     QAction *dateSort = new QAction(
0893         smallIcon(QString::fromLatin1("x-office-calendar")),
0894         i18n("Sort by Date"),
0895         sortTypes);
0896     alphaTreeSort->setCheckable(true);
0897     alphaFlatSort->setCheckable(true);
0898     dateSort->setCheckable(true);
0899     alphaTreeSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree);
0900     alphaFlatSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaFlat);
0901     dateSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortLastUse);
0902     menu->addActions(sortTypes->actions());
0903     connect(dateSort, &QAction::triggered, m_optionList.at(0), &ListSelect::slotSortDate);
0904     connect(alphaTreeSort, &QAction::triggered, m_optionList.at(0), &ListSelect::slotSortAlphaTree);
0905     connect(alphaFlatSort, &QAction::triggered, m_optionList.at(0), &ListSelect::slotSortAlphaFlat);
0906 
0907     // create MatchType entries
0908     menu->addSeparator();
0909     QActionGroup *matchTypes = new QActionGroup(menu);
0910     QAction *matchFromBeginning = new QAction(i18n("Match Tags from the First Character"), matchTypes);
0911     QAction *matchFromWordStart = new QAction(i18n("Match Tags from Word Boundaries"), matchTypes);
0912     QAction *matchAnywhere = new QAction(i18n("Match Tags Anywhere"), matchTypes);
0913     matchFromBeginning->setCheckable(true);
0914     matchFromWordStart->setCheckable(true);
0915     matchAnywhere->setCheckable(true);
0916     // TODO add StatusTip text?
0917     // set current state:
0918     matchFromBeginning->setChecked(Settings::SettingsData::instance()->matchType() == AnnotationDialog::MatchFromBeginning);
0919     matchFromWordStart->setChecked(Settings::SettingsData::instance()->matchType() == AnnotationDialog::MatchFromWordStart);
0920     matchAnywhere->setChecked(Settings::SettingsData::instance()->matchType() == AnnotationDialog::MatchAnywhere);
0921     // add MatchType actions to menu:
0922     menu->addActions(matchTypes->actions());
0923 
0924     // create toggle-show-selected entry#
0925     if (m_setup != SearchMode) {
0926         menu->addSeparator();
0927         QAction *showSelectedOnly = new QAction(
0928             smallIcon(QString::fromLatin1("view-filter")),
0929             i18n("Show Only Selected Ctrl+S"),
0930             menu);
0931         showSelectedOnly->setCheckable(true);
0932         showSelectedOnly->setChecked(ShowSelectionOnlyManager::instance().selectionIsLimited());
0933         menu->addAction(showSelectedOnly);
0934 
0935         connect(showSelectedOnly, &QAction::triggered, &ShowSelectionOnlyManager::instance(), &ShowSelectionOnlyManager::toggle);
0936     }
0937 
0938     // execute menu & handle response:
0939     QAction *res = menu->exec(QCursor::pos());
0940     if (res == saveCurrent)
0941         slotSaveWindowSetup();
0942     else if (res == reset)
0943         slotResetLayout();
0944     else if (res == matchFromBeginning)
0945         Settings::SettingsData::instance()->setMatchType(AnnotationDialog::MatchFromBeginning);
0946     else if (res == matchFromWordStart)
0947         Settings::SettingsData::instance()->setMatchType(AnnotationDialog::MatchFromWordStart);
0948     else if (res == matchAnywhere)
0949         Settings::SettingsData::instance()->setMatchType(AnnotationDialog::MatchAnywhere);
0950 }
0951 
0952 int AnnotationDialog::Dialog::exec()
0953 {
0954     m_stack->setCurrentWidget(m_dockWindow);
0955     this->setFocus(); // Set temporary focus before show() is called so that extra cursor is not shown on any "random" input widget
0956     show(); // We need to call show before we call setupFocus() otherwise the widget will not yet all have been moved in place.
0957     setupFocus();
0958 
0959     const int ret = QDialog::exec();
0960     // don't do cleanup here! the dialog may even be deleted already at this point!
0961     return ret;
0962 }
0963 
0964 void AnnotationDialog::Dialog::slotSaveWindowSetup()
0965 {
0966     const QByteArray data = m_dockWindow->saveState();
0967 
0968     const auto fileName = QString::fromLatin1("%1/layout.dat").arg(Settings::SettingsData::instance()->imageDirectory());
0969     qCDebug(AnnotationDialogLog) << "Saving window layout to file:" << fileName;
0970 
0971     QFile file(fileName);
0972     if (!file.open(QIODevice::WriteOnly)) {
0973         KMessageBox::error(this,
0974                            i18n("<p>Could not save the window layout.</p>"
0975                                 "File %1 could not be opened because of the following error: %2",
0976                                 file.fileName(), file.errorString()));
0977     } else if (!(file.write(data) && file.flush())) {
0978         KMessageBox::error(this,
0979                            i18n("<p>Could not save the window layout.</p>"
0980                                 "File %1 could not be written because of the following error: %2",
0981                                 file.fileName(), file.errorString()));
0982     }
0983     file.close();
0984 }
0985 
0986 void AnnotationDialog::Dialog::closeEvent(QCloseEvent *e)
0987 {
0988     e->ignore();
0989     reject();
0990 }
0991 
0992 void AnnotationDialog::Dialog::hideFloatingWindows()
0993 {
0994     for (QDockWidget *dock : qAsConst(m_dockWidgets)) {
0995         if (dock->isFloating()) {
0996             qCDebug(AnnotationDialogLog) << "Hiding dock: " << dock->objectName();
0997             dock->hide();
0998         }
0999     }
1000 }
1001 
1002 void AnnotationDialog::Dialog::showFloatingWindows()
1003 {
1004     for (QDockWidget *dock : qAsConst(m_dockWidgets)) {
1005         if (dock->isFloating()) {
1006             qCDebug(AnnotationDialogLog) << "Showing dock: " << dock->objectName();
1007             dock->show();
1008         }
1009     }
1010 }
1011 
1012 AnnotationDialog::ListSelect *AnnotationDialog::Dialog::createListSel(const DB::CategoryPtr &category)
1013 {
1014     ListSelect *sel = new ListSelect(category, m_dockWindow);
1015     m_optionList.append(sel);
1016     connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::itemRemoved,
1017             this, &Dialog::slotDeleteOption);
1018     connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::itemRenamed,
1019             this, &Dialog::slotRenameOption);
1020 
1021     return sel;
1022 }
1023 
1024 void AnnotationDialog::Dialog::slotDeleteOption(DB::Category *category, const QString &value)
1025 {
1026     for (QList<DB::ImageInfo>::Iterator it = m_editList.begin(); it != m_editList.end(); ++it) {
1027         (*it).removeCategoryInfo(category->name(), value);
1028     }
1029 }
1030 
1031 void AnnotationDialog::Dialog::slotRenameOption(DB::Category *category, const QString &oldValue, const QString &newValue)
1032 {
1033     for (QList<DB::ImageInfo>::Iterator it = m_editList.begin(); it != m_editList.end(); ++it) {
1034         (*it).renameItem(category->name(), oldValue, newValue);
1035     }
1036 }
1037 
1038 void AnnotationDialog::Dialog::reject()
1039 {
1040     if (m_stack->currentWidget() == m_fullScreenPreview) {
1041         togglePreview();
1042         if (!m_origList.empty())
1043             return;
1044     }
1045 
1046     if (hasChanges()) {
1047         const QString question = i18n("<p>Some changes are made to annotations. Do you really want to discard all recent changes for each affected file?</p>");
1048         const QString title = i18nc("@title", "Discard changes?");
1049 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
1050         const auto answer = KMessageBox::questionTwoActions(this,
1051                                                             question,
1052                                                             title,
1053                                                             KStandardGuiItem::discard(),
1054                                                             KStandardGuiItem::cancel());
1055         if (answer != KMessageBox::ButtonCode::PrimaryAction)
1056             return;
1057 #else
1058         int code = KMessageBox::questionYesNo(this, question, title);
1059         if (code == KMessageBox::No)
1060             return;
1061 #endif
1062     }
1063     closeDialog();
1064 }
1065 
1066 void AnnotationDialog::Dialog::closeDialog()
1067 {
1068     // the dialog is usually reused, so clear residual data upon closing it...
1069     loadInfo({});
1070 #ifdef HAVE_MARBLE
1071     clearMapData();
1072 #endif
1073     m_origList.clear();
1074     m_editList.clear();
1075     m_current = -1;
1076     tidyAreas();
1077 
1078     m_accept = QDialog::Rejected;
1079     QDialog::reject();
1080 }
1081 
1082 StringSet AnnotationDialog::Dialog::changedOptions(const ListSelect *ls)
1083 {
1084     StringSet on, partialOn, off, changes;
1085     std::tie(on, partialOn, off) = selectionForMultiSelect(ls, m_origList);
1086     changes += (ls->itemsOn() - on);
1087     changes += (on - ls->itemsOn());
1088     changes += (ls->itemsOff() - off);
1089     changes += (off - ls->itemsOff());
1090     return changes;
1091 }
1092 
1093 bool AnnotationDialog::Dialog::hasChanges()
1094 {
1095     if (m_current < 0)
1096         return false;
1097 
1098     if (m_setup == InputSingleImageConfigMode) {
1099         writeToInfo();
1100         if (m_areasChanged)
1101             return true;
1102         for (int i = 0; i < m_editList.count(); ++i) {
1103             if (*(m_origList[i]) != m_editList[i])
1104                 return true;
1105         }
1106     } else if (m_setup == InputMultiImageConfigMode) {
1107         if ((!m_startDate->date().isNull()) || (!m_endDate->date().isNull()) || (!m_imageLabel->text().isEmpty()) || m_description->changed() || m_ratingChanged)
1108             return true;
1109         for (const ListSelect *ls : qAsConst(m_optionList)) {
1110             if (!(changedOptions(ls).isEmpty()))
1111                 return true;
1112         }
1113     }
1114     return false;
1115 }
1116 
1117 void AnnotationDialog::Dialog::rotate(int angle)
1118 {
1119     if (m_setup == InputMultiImageConfigMode) {
1120         // In doneTagging the preview will be queried for its angle.
1121     } else {
1122         DB::ImageInfo &info = m_editList[m_current];
1123         info.rotate(angle, DB::RotateImageInfoOnly);
1124         Q_EMIT imageRotated(info.fileName());
1125     }
1126 }
1127 
1128 void AnnotationDialog::Dialog::slotSetFuzzyDate()
1129 {
1130     if (m_isFuzzyDate->isChecked()) {
1131         m_time->hide();
1132         m_timeLabel->hide();
1133         m_endDate->show();
1134         m_endDateLabel->show();
1135     } else {
1136         m_time->show();
1137         m_timeLabel->show();
1138         m_endDate->hide();
1139         m_endDateLabel->hide();
1140     }
1141 }
1142 
1143 void AnnotationDialog::Dialog::showHelpDialog(UsageMode type)
1144 {
1145     QString doNotShowKey;
1146     QString txt;
1147     if (type == SearchMode) {
1148         doNotShowKey = QString::fromLatin1("image_config_search_show_help");
1149         txt = i18n("<p>You have just opened the advanced search dialog; to get the most out of it, "
1150                    "it is suggested that you read the section in the manual on <a href=\"help:/kphotoalbum/sect-general-image-searches.html\">"
1151                    "advanced searching</a>.</p>"
1152                    "<p>This dialog is also used for typing in information about images; you can find "
1153                    "extra tips on its usage by reading about "
1154                    "<a href=\"help:/kphotoalbum/chp-typingIn.html\">typing in</a>.</p>");
1155     } else {
1156         doNotShowKey = QString::fromLatin1("image_config_typein_show_help");
1157         txt = i18n("<p>You have just opened one of the most important windows in KPhotoAlbum; "
1158                    "it contains lots of functionality which has been optimized for fast usage.</p>"
1159                    "<p>It is strongly recommended that you take 5 minutes to read the "
1160                    "<a href=\"help:/kphotoalbum/chp-typingIn.html\">documentation for this "
1161                    "dialog</a></p>");
1162     }
1163 
1164     KMessageBox::information(this, txt, QString(), doNotShowKey, KMessageBox::AllowLink);
1165 }
1166 
1167 void AnnotationDialog::Dialog::resizeEvent(QResizeEvent *)
1168 {
1169     Settings::SettingsData::instance()->setWindowGeometry(Settings::AnnotationDialog, geometry());
1170 }
1171 
1172 void AnnotationDialog::Dialog::moveEvent(QMoveEvent *)
1173 {
1174     Settings::SettingsData::instance()->setWindowGeometry(Settings::AnnotationDialog, geometry());
1175 }
1176 
1177 void AnnotationDialog::Dialog::setupFocus()
1178 {
1179     QList<QWidget *> list = findChildren<QWidget *>();
1180     QList<QWidget *> orderedList;
1181 
1182     // Iterate through all widgets in our dialog.
1183     for (QObject *obj : list) {
1184         QWidget *current = static_cast<QWidget *>(obj);
1185         if (!current->property("WantsFocus").isValid() || !current->isVisible())
1186             continue;
1187 
1188         int cx = current->mapToGlobal(QPoint(0, 0)).x();
1189         int cy = current->mapToGlobal(QPoint(0, 0)).y();
1190 
1191         bool inserted = false;
1192         // Iterate through the ordered list of widgets, and insert the current one, so it is in the right position in the tab chain.
1193         for (QList<QWidget *>::iterator orderedIt = orderedList.begin(); orderedIt != orderedList.end(); ++orderedIt) {
1194             const QWidget *w = *orderedIt;
1195             int wx = w->mapToGlobal(QPoint(0, 0)).x();
1196             int wy = w->mapToGlobal(QPoint(0, 0)).y();
1197 
1198             if (wy > cy || (wy == cy && wx >= cx)) {
1199                 orderedList.insert(orderedIt, current);
1200                 inserted = true;
1201                 break;
1202             }
1203         }
1204         if (!inserted)
1205             orderedList.append(current);
1206     }
1207 
1208     // now setup tab order.
1209     QWidget *prev = nullptr;
1210     QWidget *first = nullptr;
1211     for (QWidget *widget : qAsConst(orderedList)) {
1212         if (prev) {
1213             setTabOrder(prev, widget);
1214         } else {
1215             first = widget;
1216         }
1217         prev = widget;
1218     }
1219 
1220     if (first) {
1221         setTabOrder(prev, first);
1222     }
1223 
1224     // Finally set focus on the first list select
1225     for (QWidget *widget : qAsConst(orderedList)) {
1226         if (widget->property("FocusCandidate").isValid() && widget->isVisible()) {
1227             widget->setFocus();
1228             break;
1229         }
1230     }
1231 }
1232 
1233 void AnnotationDialog::Dialog::slotResetLayout()
1234 {
1235     m_dockWindow->restoreState(m_dockWindowCleanState);
1236 }
1237 
1238 void AnnotationDialog::Dialog::slotStartDateChanged(const DB::ImageDate &date)
1239 {
1240     if (date.start() == date.end())
1241         m_endDate->setDate(QDate());
1242     else
1243         m_endDate->setDate(date.end().date());
1244 }
1245 
1246 void AnnotationDialog::Dialog::loadWindowLayout()
1247 {
1248     QString fileName = QString::fromLatin1("%1/layout.dat").arg(Settings::SettingsData::instance()->imageDirectory());
1249     qCDebug(AnnotationDialogLog) << "Loading window layout from file:" << fileName;
1250     bool layoutLoaded = false;
1251 
1252     if (QFileInfo::exists(fileName)) {
1253         QFile file(fileName);
1254         if (file.open(QIODevice::ReadOnly)) {
1255             QByteArray data = file.readAll();
1256             layoutLoaded = m_dockWindow->restoreState(data);
1257         } else {
1258             qCWarning(AnnotationDialogLog) << "Window layout file" << fileName << "exists but could not be opened!";
1259         }
1260     }
1261 
1262     if (!layoutLoaded) {
1263         // create default layout
1264         // label/date/rating in a visual block with description:
1265         m_dockWindow->splitDockWidget(m_generalDock, m_descriptionDock, Qt::Vertical);
1266 
1267         // more space for description:
1268         m_dockWindow->resizeDocks({ m_generalDock, m_descriptionDock }, { 60, 100 }, Qt::Vertical);
1269         // more space for preview:
1270         m_dockWindow->resizeDocks({ m_generalDock, m_descriptionDock, m_previewDock }, { 200, 200, 800 }, Qt::Horizontal);
1271 #ifdef HAVE_MARBLE
1272         // group the map with the preview
1273         m_dockWindow->tabifyDockWidget(m_previewDock, m_mapDock);
1274         // make sure the preview tab is active:
1275         m_previewDock->raise();
1276 #endif
1277         return;
1278     }
1279 }
1280 
1281 void AnnotationDialog::Dialog::setupActions()
1282 {
1283     QAction *action = nullptr;
1284     action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-alphatree"), m_optionList.at(0), &ListSelect::slotSortAlphaTree);
1285     action->setText(i18n("Sort Alphabetically (Tree)"));
1286     m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_F4);
1287 
1288     action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-alphaflat"), m_optionList.at(0), &ListSelect::slotSortAlphaFlat);
1289     action->setText(i18n("Sort Alphabetically (Flat)"));
1290 
1291     action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-MRU"), m_optionList.at(0), &ListSelect::slotSortDate);
1292     action->setText(i18n("Sort Most Recently Used"));
1293 
1294     action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-sort"), m_optionList.at(0), &ListSelect::toggleSortType);
1295     action->setText(i18n("Toggle Sorting"));
1296     m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_T);
1297 
1298     action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-showing-selected-only"),
1299                                   &ShowSelectionOnlyManager::instance(), &ShowSelectionOnlyManager::toggle);
1300     action->setText(i18n("Toggle Showing Selected Items Only"));
1301     m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_S);
1302 
1303     action = m_actions->addAction(QString::fromLatin1("annotationdialog-next-image"), m_preview, &ImagePreviewWidget::slotNext);
1304     action->setText(i18n("Annotate Next"));
1305     m_actions->setDefaultShortcut(action, Qt::Key_PageDown);
1306 
1307     action = m_actions->addAction(QString::fromLatin1("annotationdialog-prev-image"), m_preview, &ImagePreviewWidget::slotPrev);
1308     action->setText(i18n("Annotate Previous"));
1309     m_actions->setDefaultShortcut(action, Qt::Key_PageUp);
1310 
1311     action = m_actions->addAction(QString::fromLatin1("annotationdialog-OK-dialog"), this, &Dialog::doneTagging);
1312     action->setText(i18n("OK dialog"));
1313     m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Return);
1314 
1315     action = m_actions->addAction(QString::fromLatin1("annotationdialog-copy-previous"), this, &Dialog::slotCopyPrevious);
1316     action->setText(i18n("Copy tags from previous image"));
1317     m_actions->setDefaultShortcut(action, Qt::ALT + Qt::Key_Insert);
1318 
1319     action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-left"), m_preview, &ImagePreviewWidget::rotateLeft);
1320     action->setText(i18n("Rotate counterclockwise"));
1321 
1322     action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-right"), m_preview, &ImagePreviewWidget::rotateRight);
1323     action->setText(i18n("Rotate clockwise"));
1324 
1325     action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-viewer"), this, &Dialog::togglePreview);
1326     action->setText(i18n("Toggle fullscreen preview"));
1327     m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Space);
1328 
1329     const auto allActions = m_actions->actions();
1330     for (QAction *action : allActions) {
1331         action->setShortcutContext(Qt::WindowShortcut);
1332         addAction(action);
1333     }
1334 
1335     // the annotation dialog is created when it's first used;
1336     // therefore, its actions are registered well after the MainWindow sets up its actionCollection,
1337     // and it has to read the shortcuts here, after they are set up:
1338     m_actions->readSettings();
1339 }
1340 
1341 KActionCollection *AnnotationDialog::Dialog::actions()
1342 {
1343     return m_actions;
1344 }
1345 
1346 void AnnotationDialog::Dialog::setUpCategoryListBoxForMultiImageSelection(ListSelect *listSel, const DB::ImageInfoList &images)
1347 {
1348     StringSet on, partialOn, off;
1349     std::tie(on, partialOn, off) = selectionForMultiSelect(listSel, images);
1350     listSel->setSelection(on, partialOn);
1351 }
1352 
1353 std::tuple<StringSet, StringSet, StringSet> AnnotationDialog::Dialog::selectionForMultiSelect(const ListSelect *listSel, const DB::ImageInfoList &images)
1354 {
1355     const QString category = listSel->category();
1356     const auto itemsInclCategories = DB::ImageDB::instance()->categoryCollection()->categoryForName(category)->itemsInclCategories();
1357     const StringSet allItems(itemsInclCategories.begin(), itemsInclCategories.end());
1358     StringSet itemsOnSomeImages;
1359     StringSet itemsOnAllImages;
1360     bool firstImage = true;
1361 
1362     for (DB::ImageInfoList::ConstIterator imageIt = images.begin(); imageIt != images.end(); ++imageIt) {
1363         const StringSet itemsOnThisImage = (*imageIt)->itemsOfCategory(category);
1364         if (firstImage) {
1365             itemsOnAllImages = itemsOnThisImage;
1366             firstImage = false;
1367         } else {
1368             for (const QString &item : itemsOnThisImage) {
1369                 if (!itemsOnAllImages.contains(item) && !itemsOnSomeImages.contains(item)) {
1370                     itemsOnSomeImages += item;
1371                 }
1372             }
1373             itemsOnAllImages = itemsOnAllImages.intersect(itemsOnThisImage);
1374         }
1375     }
1376     const StringSet itemsOnNoImages = allItems - itemsOnSomeImages - itemsOnAllImages;
1377 
1378     return std::make_tuple(itemsOnAllImages, itemsOnSomeImages, itemsOnNoImages);
1379 }
1380 
1381 void AnnotationDialog::Dialog::slotRatingChanged(int)
1382 {
1383     m_ratingChanged = true;
1384 }
1385 
1386 void AnnotationDialog::Dialog::continueLater()
1387 {
1388     saveAndClose();
1389 }
1390 
1391 void AnnotationDialog::Dialog::saveAndClose()
1392 {
1393     tidyAreas();
1394 
1395     m_fullScreenPreview->stopPlayback();
1396 
1397     if (m_origList.isEmpty()) {
1398         // all images are deleted.
1399         QDialog::accept();
1400         return;
1401     }
1402 
1403     // I need to check for the changes first, as the case for m_setup
1404     // == InputSingleImageConfigMode, saves to the m_origList, and we
1405     // can thus not check for changes anymore
1406     bool anyChanges = hasChanges();
1407 
1408     if (m_setup == InputSingleImageConfigMode) {
1409         writeToInfo();
1410         for (int i = 0; i < m_editList.count(); ++i) {
1411             *(m_origList[i]) = m_editList[i];
1412         }
1413     } else if (m_setup == InputMultiImageConfigMode) {
1414         for (ListSelect *ls : qAsConst(m_optionList)) {
1415             ls->slotReturn();
1416         }
1417 
1418         for (const ListSelect *ls : qAsConst(m_optionList)) {
1419             StringSet changes = changedOptions(ls);
1420             if (!(changes.isEmpty())) {
1421                 anyChanges = true;
1422                 StringSet newItemsOn = ls->itemsOn() & changes;
1423                 StringSet newItemsOff = ls->itemsOff() & changes;
1424                 for (DB::ImageInfoListConstIterator it = m_origList.constBegin(); it != m_origList.constEnd(); ++it) {
1425                     DB::ImageInfoPtr info = *it;
1426                     info->addCategoryInfo(ls->category(), newItemsOn);
1427                     info->removeCategoryInfo(ls->category(), newItemsOff);
1428                 }
1429             }
1430         }
1431         for (DB::ImageInfoListConstIterator it = m_origList.constBegin(); it != m_origList.constEnd(); ++it) {
1432             DB::ImageInfoPtr info = *it;
1433             if (!m_startDate->date().isNull())
1434                 info->setDate(DB::ImageDate(m_startDate->date(), m_endDate->date(), m_time->time()));
1435 
1436             if (!m_imageLabel->text().isEmpty()) {
1437                 info->setLabel(m_imageLabel->text());
1438             }
1439 
1440             if (!m_description->isEmpty()) {
1441                 info->setDescription(m_description->description());
1442             }
1443 
1444             if (m_ratingChanged) {
1445                 info->setRating(m_rating->rating());
1446             }
1447         }
1448         m_ratingChanged = false;
1449     }
1450     m_accept = QDialog::Accepted;
1451 
1452     if (anyChanges) {
1453         MainWindow::DirtyIndicator::markDirty();
1454     }
1455     QDialog::accept();
1456 }
1457 
1458 AnnotationDialog::Dialog::~Dialog()
1459 {
1460     qDeleteAll(m_optionList);
1461     m_optionList.clear();
1462 }
1463 
1464 void AnnotationDialog::Dialog::togglePreview()
1465 {
1466     if (m_setup == InputSingleImageConfigMode) {
1467         if (m_stack->currentWidget() == m_fullScreenPreview) {
1468             m_stack->setCurrentWidget(m_dockWindow);
1469             m_fullScreenPreview->stopPlayback();
1470         } else {
1471             DB::ImageInfo currentInfo = m_editList[m_current];
1472             m_stack->setCurrentWidget(m_fullScreenPreview);
1473             m_fullScreenPreview->load(DB::FileNameList() << currentInfo.fileName());
1474 
1475             // compute altered tags by removing existing tags from full set:
1476             const DB::TaggedAreas existingAreas = currentInfo.taggedAreas();
1477             DB::TaggedAreas alteredAreas = taggedAreas();
1478             for (auto catIt = existingAreas.constBegin(); catIt != existingAreas.constEnd(); ++catIt) {
1479                 const QString &categoryName = catIt.key();
1480                 const DB::PositionTags &tags = catIt.value();
1481                 for (auto tagIt = tags.cbegin(); tagIt != tags.constEnd(); ++tagIt) {
1482                     const QString &tagName = tagIt.key();
1483                     const QRect &area = tagIt.value();
1484 
1485                     // remove unchanged areas
1486                     if (area == alteredAreas[categoryName][tagName]) {
1487                         alteredAreas[categoryName].remove(tagName);
1488                         if (alteredAreas[categoryName].empty())
1489                             alteredAreas.remove(categoryName);
1490                     }
1491                 }
1492             }
1493             m_fullScreenPreview->addAdditionalTaggedAreas(alteredAreas);
1494         }
1495     }
1496 }
1497 
1498 void AnnotationDialog::Dialog::tidyAreas()
1499 {
1500     // Remove all areas marked on the preview image
1501     const auto allAreas = areas();
1502     for (ResizableFrame *area : allAreas) {
1503         area->markTidied();
1504         area->deleteLater();
1505     }
1506 
1507     // No areas have been changed
1508     m_areasChanged = false;
1509 }
1510 
1511 void AnnotationDialog::Dialog::slotNewArea(AnnotationDialog::ResizableFrame *area)
1512 {
1513     area->setDialog(this);
1514 }
1515 
1516 void AnnotationDialog::Dialog::positionableTagSelected(QString category, QString tag)
1517 {
1518     // Be sure not to propose an already-associated tag
1519     QPair<QString, QString> tagData = qMakePair(category, tag);
1520     const auto allAreas = areas();
1521     for (ResizableFrame *area : allAreas) {
1522         if (area->tagData() == tagData) {
1523             return;
1524         }
1525     }
1526 
1527     // Set the selected tag as the last selected positionable tag
1528     m_lastSelectedPositionableTag = tagData;
1529 
1530     // Add the tag to the positionable tag candidate list
1531     addTagToCandidateList(category, tag);
1532 }
1533 
1534 void AnnotationDialog::Dialog::positionableTagDeselected(QString category, QString tag)
1535 {
1536     // Remove the tag from the candidate list
1537     removeTagFromCandidateList(category, tag);
1538 
1539     // Search for areas linked against the tag on this image
1540     if (m_setup == InputSingleImageConfigMode) {
1541         QPair<QString, QString> deselectedTag = QPair<QString, QString>(category, tag);
1542 
1543         const auto allAreas = areas();
1544         for (ResizableFrame *area : allAreas) {
1545             if (area->tagData() == deselectedTag) {
1546                 area->removeTagData();
1547                 m_areasChanged = true;
1548                 // Only one area can be associated with the tag, so we can return here
1549                 return;
1550             }
1551         }
1552     }
1553     // Removal of tagged areas in InputMultiImageConfigMode is done in DB::ImageInfo::removeCategoryInfo
1554 }
1555 
1556 void AnnotationDialog::Dialog::addTagToCandidateList(QString category, QString tag)
1557 {
1558     m_positionableTagCandidates << QPair<QString, QString>(category, tag);
1559 }
1560 
1561 void AnnotationDialog::Dialog::removeTagFromCandidateList(QString category, QString tag)
1562 {
1563     // Is the deselected tag the last selected positionable tag?
1564     if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == tag) {
1565         m_lastSelectedPositionableTag = QPair<QString, QString>();
1566     }
1567 
1568     // Remove the tag from the candidate list
1569     m_positionableTagCandidates.removeAll(QPair<QString, QString>(category, tag));
1570     // When a positionable tag is entered via the AreaTagSelectDialog, it's added to this
1571     // list twice, so we use removeAll here to be sure to also wipe duplicate entries.
1572 }
1573 
1574 QPair<QString, QString> AnnotationDialog::Dialog::lastSelectedPositionableTag() const
1575 {
1576     return m_lastSelectedPositionableTag;
1577 }
1578 
1579 QList<QPair<QString, QString>> AnnotationDialog::Dialog::positionableTagCandidates() const
1580 {
1581     return m_positionableTagCandidates;
1582 }
1583 
1584 void AnnotationDialog::Dialog::slotShowAreas(bool showAreas)
1585 {
1586     const auto allAreas = areas();
1587     for (ResizableFrame *area : allAreas) {
1588         area->setVisible(showAreas);
1589     }
1590 }
1591 
1592 void AnnotationDialog::Dialog::positionableTagRenamed(QString category, QString oldTag, QString newTag)
1593 {
1594     // Is the renamed tag the last selected positionable tag?
1595     if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == oldTag) {
1596         m_lastSelectedPositionableTag.second = newTag;
1597     }
1598 
1599     // Check the candidate list for the tag
1600     QPair<QString, QString> oldTagData = QPair<QString, QString>(category, oldTag);
1601     if (m_positionableTagCandidates.contains(oldTagData)) {
1602         // The tag is in the list, so update it
1603         m_positionableTagCandidates.removeAt(m_positionableTagCandidates.indexOf(oldTagData));
1604         m_positionableTagCandidates << QPair<QString, QString>(category, newTag);
1605     }
1606 
1607     // Check if an area on the current image contains the changed or proposed tag
1608     const auto allAreas = areas();
1609     for (ResizableFrame *area : allAreas) {
1610         if (area->tagData() == oldTagData) {
1611             area->setTagData(category, newTag);
1612         }
1613     }
1614 }
1615 
1616 void AnnotationDialog::Dialog::descriptionPageUpDownPressed(QKeyEvent *event)
1617 {
1618     if (event->key() == Qt::Key_PageUp) {
1619         m_actions->action(QString::fromLatin1("annotationdialog-prev-image"))->trigger();
1620     } else if (event->key() == Qt::Key_PageDown) {
1621         m_actions->action(QString::fromLatin1("annotationdialog-next-image"))->trigger();
1622     }
1623 }
1624 
1625 void AnnotationDialog::Dialog::checkProposedTagData(
1626     QPair<QString, QString> tagData,
1627     ResizableFrame *areaToExclude) const
1628 {
1629     const auto allAreas = areas();
1630     for (ResizableFrame *area : allAreas) {
1631         if (area != areaToExclude
1632             && area->proposedTagData() == tagData
1633             && area->tagData().first.isEmpty()) {
1634             area->removeProposedTagData();
1635         }
1636     }
1637 }
1638 
1639 void AnnotationDialog::Dialog::areaChanged()
1640 {
1641     m_areasChanged = true;
1642 }
1643 
1644 /**
1645  * @brief positionableTagValid checks whether a given tag can still be associated to an area.
1646  * This checks for empty and duplicate tags.
1647  * @return
1648  */
1649 bool AnnotationDialog::Dialog::positionableTagAvailable(const QString &category, const QString &tag) const
1650 {
1651     if (category.isEmpty() || tag.isEmpty())
1652         return false;
1653 
1654     // does any area already have that tag?
1655     const auto allAreas = areas();
1656     for (const ResizableFrame *area : allAreas) {
1657         const auto tagData = area->tagData();
1658         if (tagData.first == category && tagData.second == tag)
1659             return false;
1660     }
1661 
1662     return true;
1663 }
1664 
1665 /**
1666  * @brief Generates a set of positionable tags currently used on the image
1667  * @param category
1668  * @return
1669  */
1670 QSet<QString> AnnotationDialog::Dialog::positionedTags(const QString &category) const
1671 {
1672     QSet<QString> tags;
1673     const auto allAreas = areas();
1674     for (const ResizableFrame *area : allAreas) {
1675         const auto tagData = area->tagData();
1676         if (tagData.first == category)
1677             tags += tagData.second;
1678     }
1679     return tags;
1680 }
1681 
1682 AnnotationDialog::ListSelect *AnnotationDialog::Dialog::listSelectForCategory(const QString &category)
1683 {
1684     return m_listSelectList.value(category, nullptr);
1685 }
1686 
1687 void AnnotationDialog::Dialog::showEvent(QShowEvent *event)
1688 {
1689     showFloatingWindows();
1690     event->accept();
1691 }
1692 
1693 void AnnotationDialog::Dialog::hideEvent(QHideEvent *event)
1694 {
1695     hideFloatingWindows();
1696     event->accept();
1697 }
1698 
1699 void AnnotationDialog::Dialog::slotDiscardFiles(const DB::FileNameList &files)
1700 {
1701 #ifdef HAVE_MARBLE
1702     clearMapData();
1703 #endif
1704     // we can't directly compare ImageInfos with the ones in the database, so we work on filenames:
1705     auto origFilenames = m_origList.files();
1706     for (const auto &filename : files) {
1707         const int index = origFilenames.indexOf(filename);
1708         if (index >= 0) {
1709             qCDebug(AnnotationDialogLog) << "Discarding file" << filename.relative() << "from annotation dialog";
1710             origFilenames.removeAt(index);
1711             m_origList.removeAt(index);
1712             m_editList.removeAt(index);
1713             if (0 < index && index <= m_current)
1714                 m_current--;
1715         }
1716     }
1717     if (m_origList.count() == 0) {
1718         m_current = -1;
1719         reject();
1720         return;
1721     }
1722 
1723     m_preview->reconfigure(&m_editList, m_current);
1724     load();
1725 #ifdef HAVE_MARBLE
1726     // trigger repopulating the map
1727     if (m_annotationMap->isVisible())
1728         annotationMapVisibilityChanged(true);
1729 #endif
1730 }
1731 
1732 #ifdef HAVE_MARBLE
1733 void AnnotationDialog::Dialog::updateMapForCurrentImage()
1734 {
1735     if (m_setup != InputSingleImageConfigMode) {
1736         return;
1737     }
1738 
1739     // we can use the coordinates of the original images here, because the are never changed by the annotation dialog
1740     if (m_origList[m_current]->coordinates().hasCoordinates()) {
1741         m_annotationMap->setCenter(m_origList[m_current]);
1742         m_annotationMap->displayStatus(Map::MapStatus::ImageHasCoordinates);
1743     } else {
1744         m_annotationMap->displayStatus(Map::MapStatus::ImageHasNoCoordinates);
1745     }
1746 }
1747 #endif
1748 #ifdef HAVE_MARBLE
1749 void AnnotationDialog::Dialog::annotationMapVisibilityChanged(bool visible)
1750 {
1751     // This populates the map if it's added when the dialog is already open
1752     if (visible) {
1753         // when the map dockwidget is already visible on show(), the call to
1754         // annotationMapVisibilityChanged  is executed in the GUI thread.
1755         // This ensures that populateMap() doesn't block the GUI in this case:
1756         QTimer::singleShot(0, this, &Dialog::populateMap);
1757     } else {
1758         m_cancelMapLoading = true;
1759     }
1760 }
1761 #endif
1762 #ifdef HAVE_MARBLE
1763 void AnnotationDialog::Dialog::populateMap()
1764 {
1765     // populateMap is called every time the map widget gets visible
1766     if (m_mapIsPopulated) {
1767         return;
1768     }
1769     m_annotationMap->displayStatus(Map::MapStatus::Loading);
1770     m_cancelMapLoading = false;
1771     m_mapLoadingProgress->setMaximum(m_origList.count());
1772     m_mapLoadingProgress->show();
1773     m_cancelMapLoadingButton->show();
1774 
1775     int processedImages = 0;
1776     int imagesWithCoordinates = 0;
1777 
1778     // we can use the coordinates of the original images here, because the are never changed by the annotation dialog
1779     for (const DB::ImageInfoPtr &info : qAsConst(m_origList)) {
1780         processedImages++;
1781         m_mapLoadingProgress->setValue(processedImages);
1782         // keep things responsive by processing events manually:
1783         QApplication::processEvents();
1784 
1785         if (m_annotationMap->addImage(info)) {
1786             imagesWithCoordinates++;
1787         }
1788 
1789         // m_cancelMapLoading is set to true by clicking the "Cancel" button
1790         if (m_cancelMapLoading) {
1791             m_annotationMap->clear();
1792             break;
1793         }
1794     }
1795     m_annotationMap->buildImageClusters();
1796     // at this point either we canceled loading or the map is populated:
1797     m_mapIsPopulated = !m_cancelMapLoading;
1798     mapLoadingFinished(imagesWithCoordinates > 0, imagesWithCoordinates == processedImages);
1799 }
1800 #endif
1801 #ifdef HAVE_MARBLE
1802 void AnnotationDialog::Dialog::setCancelMapLoading()
1803 {
1804     m_cancelMapLoading = true;
1805 }
1806 #endif
1807 #ifdef HAVE_MARBLE
1808 void AnnotationDialog::Dialog::mapLoadingFinished(bool mapHasImages, bool allImagesHaveCoordinates)
1809 {
1810     m_mapLoadingProgress->hide();
1811     m_cancelMapLoadingButton->hide();
1812 
1813     if (m_setup == InputSingleImageConfigMode) {
1814         m_annotationMap->displayStatus(Map::MapStatus::ImageHasNoCoordinates);
1815     } else {
1816         if (m_setup == SearchMode) {
1817             m_annotationMap->displayStatus(Map::MapStatus::SearchCoordinates);
1818         } else {
1819             if (mapHasImages) {
1820                 if (!allImagesHaveCoordinates) {
1821                     m_annotationMap->displayStatus(Map::MapStatus::SomeImagesHaveNoCoordinates);
1822                 } else {
1823                     m_annotationMap->displayStatus(Map::MapStatus::ImageHasCoordinates);
1824                 }
1825             } else {
1826                 m_annotationMap->displayStatus(Map::MapStatus::NoImagesHaveNoCoordinates);
1827             }
1828         }
1829     }
1830 
1831     if (m_setup != SearchMode) {
1832         m_annotationMap->zoomToMarkers();
1833         updateMapForCurrentImage();
1834     }
1835 }
1836 #endif
1837 
1838 // vi:expandtab:tabstop=4 shiftwidth=4:
1839 
1840 #include "moc_Dialog.cpp"