File indexing completed on 2024-04-28 04:20:50
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"