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

Could not save the window layout.

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

Could not save the window layout.

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

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

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

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

" "

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

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

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

" "

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

"); } KMessageBox::information(this, txt, QString(), doNotShowKey, KMessageBox::AllowLink); } void AnnotationDialog::Dialog::resizeEvent(QResizeEvent *) { Settings::SettingsData::instance()->setWindowGeometry(Settings::AnnotationDialog, geometry()); } void AnnotationDialog::Dialog::moveEvent(QMoveEvent *) { Settings::SettingsData::instance()->setWindowGeometry(Settings::AnnotationDialog, geometry()); } void AnnotationDialog::Dialog::setupFocus() { QList list = findChildren(); QList orderedList; // Iterate through all widgets in our dialog. for (QObject *obj : list) { QWidget *current = static_cast(obj); if (!current->property("WantsFocus").isValid() || !current->isVisible()) continue; int cx = current->mapToGlobal(QPoint(0, 0)).x(); int cy = current->mapToGlobal(QPoint(0, 0)).y(); bool inserted = false; // Iterate through the ordered list of widgets, and insert the current one, so it is in the right position in the tab chain. for (QList::iterator orderedIt = orderedList.begin(); orderedIt != orderedList.end(); ++orderedIt) { const QWidget *w = *orderedIt; int wx = w->mapToGlobal(QPoint(0, 0)).x(); int wy = w->mapToGlobal(QPoint(0, 0)).y(); if (wy > cy || (wy == cy && wx >= cx)) { orderedList.insert(orderedIt, current); inserted = true; break; } } if (!inserted) orderedList.append(current); } // now setup tab order. QWidget *prev = nullptr; QWidget *first = nullptr; for (QWidget *widget : qAsConst(orderedList)) { if (prev) { setTabOrder(prev, widget); } else { first = widget; } prev = widget; } if (first) { setTabOrder(prev, first); } // Finally set focus on the first list select for (QWidget *widget : qAsConst(orderedList)) { if (widget->property("FocusCandidate").isValid() && widget->isVisible()) { widget->setFocus(); break; } } } void AnnotationDialog::Dialog::slotResetLayout() { m_dockWindow->restoreState(m_dockWindowCleanState); } void AnnotationDialog::Dialog::slotStartDateChanged(const DB::ImageDate &date) { if (date.start() == date.end()) m_endDate->setDate(QDate()); else m_endDate->setDate(date.end().date()); } void AnnotationDialog::Dialog::loadWindowLayout() { QString fileName = QString::fromLatin1("%1/layout.dat").arg(Settings::SettingsData::instance()->imageDirectory()); if (!QFileInfo(fileName).exists()) { // create default layout // label/date/rating in a visual block with description: m_dockWindow->splitDockWidget(m_generalDock, m_descriptionDock, Qt::Vertical); // more space for description: m_dockWindow->resizeDocks({ m_generalDock, m_descriptionDock }, { 60, 100 }, Qt::Vertical); // more space for preview: m_dockWindow->resizeDocks({ m_generalDock, m_descriptionDock, m_previewDock }, { 200, 200, 800 }, Qt::Horizontal); #ifdef HAVE_MARBLE // group the map with the preview m_dockWindow->tabifyDockWidget(m_previewDock, m_mapDock); // make sure the preview tab is active: m_previewDock->raise(); #endif return; } QFile file(fileName); file.open(QIODevice::ReadOnly); QByteArray data = file.readAll(); m_dockWindow->restoreState(data); } void AnnotationDialog::Dialog::setupActions() { QAction *action = nullptr; action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-alphatree"), m_optionList.at(0), SLOT(slotSortAlphaTree())); action->setText(i18n("Sort Alphabetically (Tree)")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_F4); action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-alphaflat"), m_optionList.at(0), SLOT(slotSortAlphaFlat())); action->setText(i18n("Sort Alphabetically (Flat)")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-MRU"), m_optionList.at(0), SLOT(slotSortDate())); action->setText(i18n("Sort Most Recently Used")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-sort"), m_optionList.at(0), SLOT(toggleSortType())); action->setText(i18n("Toggle Sorting")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_T); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-showing-selected-only"), &ShowSelectionOnlyManager::instance(), SLOT(toggle())); action->setText(i18n("Toggle Showing Selected Items Only")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_S); action = m_actions->addAction(QString::fromLatin1("annotationdialog-next-image"), m_preview, SLOT(slotNext())); action->setText(i18n("Annotate Next")); m_actions->setDefaultShortcut(action, Qt::Key_PageDown); action = m_actions->addAction(QString::fromLatin1("annotationdialog-prev-image"), m_preview, SLOT(slotPrev())); action->setText(i18n("Annotate Previous")); m_actions->setDefaultShortcut(action, Qt::Key_PageUp); action = m_actions->addAction(QString::fromLatin1("annotationdialog-OK-dialog"), this, SLOT(doneTagging())); action->setText(i18n("OK dialog")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Return); action = m_actions->addAction(QString::fromLatin1("annotationdialog-delete-image"), this, SLOT(slotDeleteImage())); action->setText(i18n("Delete")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete); action = m_actions->addAction(QString::fromLatin1("annotationdialog-copy-previous"), this, SLOT(slotCopyPrevious())); action->setText(i18n("Copy tags from previous image")); m_actions->setDefaultShortcut(action, Qt::ALT + Qt::Key_Insert); action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-left"), m_preview, SLOT(rotateLeft())); action->setText(i18n("Rotate counterclockwise")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-right"), m_preview, SLOT(rotateRight())); action->setText(i18n("Rotate clockwise")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-viewer"), this, SLOT(togglePreview())); action->setText(i18n("Toggle fullscreen preview")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Space); - foreach (QAction *action, m_actions->actions()) { + const auto allActions = m_actions->actions(); + for (QAction *action : allActions) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } // the annotation dialog is created when it's first used; // therefore, its actions are registered well after the MainWindow sets up its actionCollection, // and it has to read the shortcuts here, after they are set up: m_actions->readSettings(); } KActionCollection *AnnotationDialog::Dialog::actions() { return m_actions; } void AnnotationDialog::Dialog::setUpCategoryListBoxForMultiImageSelection(ListSelect *listSel, const DB::ImageInfoList &images) { StringSet on, partialOn, off; std::tie(on, partialOn, off) = selectionForMultiSelect(listSel, images); listSel->setSelection(on, partialOn); } std::tuple AnnotationDialog::Dialog::selectionForMultiSelect(const ListSelect *listSel, const DB::ImageInfoList &images) { const QString category = listSel->category(); const StringSet allItems = DB::ImageDB::instance()->categoryCollection()->categoryForName(category)->itemsInclCategories().toSet(); StringSet itemsOnSomeImages; StringSet itemsOnAllImages; bool firstImage = true; for (DB::ImageInfoList::ConstIterator imageIt = images.begin(); imageIt != images.end(); ++imageIt) { const StringSet itemsOnThisImage = (*imageIt)->itemsOfCategory(category); if (firstImage) { itemsOnAllImages = itemsOnThisImage; firstImage = false; } else { for (const QString &item : itemsOnThisImage) { if (!itemsOnAllImages.contains(item) && !itemsOnSomeImages.contains(item)) { itemsOnSomeImages += item; } } itemsOnAllImages = itemsOnAllImages.intersect(itemsOnThisImage); } } const StringSet itemsOnNoImages = allItems - itemsOnSomeImages - itemsOnAllImages; return std::make_tuple(itemsOnAllImages, itemsOnSomeImages, itemsOnNoImages); } void AnnotationDialog::Dialog::slotRatingChanged(unsigned int) { m_ratingChanged = true; } void AnnotationDialog::Dialog::continueLater() { saveAndClose(); } void AnnotationDialog::Dialog::saveAndClose() { tidyAreas(); m_fullScreenPreview->stopPlayback(); if (m_origList.isEmpty()) { // all images are deleted. QDialog::accept(); return; } // I need to check for the changes first, as the case for m_setup // == InputSingleImageConfigMode, saves to the m_origList, and we // can thus not check for changes anymore bool anyChanges = hasChanges(); if (m_setup == InputSingleImageConfigMode) { writeToInfo(); for (int i = 0; i < m_editList.count(); ++i) { *(m_origList[i]) = m_editList[i]; } } else if (m_setup == InputMultiImageConfigMode) { for (ListSelect *ls : qAsConst(m_optionList)) { ls->slotReturn(); } for (const ListSelect *ls : qAsConst(m_optionList)) { StringSet changes = changedOptions(ls); if (!(changes.isEmpty())) { anyChanges = true; StringSet newItemsOn = ls->itemsOn() & changes; StringSet newItemsOff = ls->itemsOff() & changes; for (DB::ImageInfoListConstIterator it = m_origList.constBegin(); it != m_origList.constEnd(); ++it) { DB::ImageInfoPtr info = *it; info->addCategoryInfo(ls->category(), newItemsOn); info->removeCategoryInfo(ls->category(), newItemsOff); } } } for (DB::ImageInfoListConstIterator it = m_origList.constBegin(); it != m_origList.constEnd(); ++it) { DB::ImageInfoPtr info = *it; if (!m_startDate->date().isNull()) info->setDate(DB::ImageDate(m_startDate->date(), m_endDate->date(), m_time->time())); if (!m_imageLabel->text().isEmpty()) { info->setLabel(m_imageLabel->text()); } if (!m_description->toPlainText().isEmpty() && m_description->toPlainText().compare(m_conflictText)) { info->setDescription(m_description->toPlainText()); } if (m_ratingChanged) { info->setRating(m_rating->rating()); } } m_ratingChanged = false; } m_accept = QDialog::Accepted; if (anyChanges) { MainWindow::DirtyIndicator::markDirty(); } QDialog::accept(); } AnnotationDialog::Dialog::~Dialog() { qDeleteAll(m_optionList); m_optionList.clear(); } void AnnotationDialog::Dialog::togglePreview() { if (m_setup == InputSingleImageConfigMode) { if (m_stack->currentWidget() == m_fullScreenPreview) { m_stack->setCurrentWidget(m_dockWindow); m_fullScreenPreview->stopPlayback(); } else { DB::ImageInfo currentInfo = m_editList[m_current]; m_stack->setCurrentWidget(m_fullScreenPreview); m_fullScreenPreview->load(DB::FileNameList() << currentInfo.fileName()); // compute altered tags by removing existing tags from full set: const DB::TaggedAreas existingAreas = currentInfo.taggedAreas(); DB::TaggedAreas alteredAreas = taggedAreas(); for (auto catIt = existingAreas.constBegin(); catIt != existingAreas.constEnd(); ++catIt) { const QString &categoryName = catIt.key(); const DB::PositionTags &tags = catIt.value(); for (auto tagIt = tags.cbegin(); tagIt != tags.constEnd(); ++tagIt) { const QString &tagName = tagIt.key(); const QRect &area = tagIt.value(); // remove unchanged areas if (area == alteredAreas[categoryName][tagName]) { alteredAreas[categoryName].remove(tagName); if (alteredAreas[categoryName].empty()) alteredAreas.remove(categoryName); } } } m_fullScreenPreview->addAdditionalTaggedAreas(alteredAreas); } } } void AnnotationDialog::Dialog::tidyAreas() { // Remove all areas marked on the preview image - foreach (ResizableFrame *area, areas()) { + const auto allAreas = areas(); + for (ResizableFrame *area : allAreas) { area->markTidied(); area->deleteLater(); } } void AnnotationDialog::Dialog::slotNewArea(ResizableFrame *area) { area->setDialog(this); } void AnnotationDialog::Dialog::positionableTagSelected(QString category, QString tag) { // Be sure not to propose an already-associated tag QPair tagData = qMakePair(category, tag); - foreach (ResizableFrame *area, areas()) { + const auto allAreas = areas(); + for (ResizableFrame *area : allAreas) { if (area->tagData() == tagData) { return; } } // Set the selected tag as the last selected positionable tag m_lastSelectedPositionableTag = tagData; // Add the tag to the positionable tag candidate list addTagToCandidateList(category, tag); } void AnnotationDialog::Dialog::positionableTagDeselected(QString category, QString tag) { // Remove the tag from the candidate list removeTagFromCandidateList(category, tag); // Search for areas linked against the tag on this image if (m_setup == InputSingleImageConfigMode) { QPair deselectedTag = QPair(category, tag); - foreach (ResizableFrame *area, areas()) { + const auto allAreas = areas(); + for (ResizableFrame *area : allAreas) { if (area->tagData() == deselectedTag) { area->removeTagData(); m_areasChanged = true; // Only one area can be associated with the tag, so we can return here return; } } } // Removal of tagged areas in InputMultiImageConfigMode is done in DB::ImageInfo::removeCategoryInfo } void AnnotationDialog::Dialog::addTagToCandidateList(QString category, QString tag) { m_positionableTagCandidates << QPair(category, tag); } void AnnotationDialog::Dialog::removeTagFromCandidateList(QString category, QString tag) { // Is the deselected tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == tag) { m_lastSelectedPositionableTag = QPair(); } // Remove the tag from the candidate list m_positionableTagCandidates.removeAll(QPair(category, tag)); // When a positionable tag is entered via the AreaTagSelectDialog, it's added to this // list twice, so we use removeAll here to be sure to also wipe duplicate entries. } QPair AnnotationDialog::Dialog::lastSelectedPositionableTag() const { return m_lastSelectedPositionableTag; } QList> AnnotationDialog::Dialog::positionableTagCandidates() const { return m_positionableTagCandidates; } void AnnotationDialog::Dialog::slotShowAreas(bool showAreas) { - foreach (ResizableFrame *area, areas()) { + const auto allAreas = areas(); + for (ResizableFrame *area : allAreas) { area->setVisible(showAreas); } } void AnnotationDialog::Dialog::positionableTagRenamed(QString category, QString oldTag, QString newTag) { // Is the renamed tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == oldTag) { m_lastSelectedPositionableTag.second = newTag; } // Check the candidate list for the tag QPair oldTagData = QPair(category, oldTag); if (m_positionableTagCandidates.contains(oldTagData)) { // The tag is in the list, so update it m_positionableTagCandidates.removeAt(m_positionableTagCandidates.indexOf(oldTagData)); m_positionableTagCandidates << QPair(category, newTag); } // Check if an area on the current image contains the changed or proposed tag - foreach (ResizableFrame *area, areas()) { + const auto allAreas = areas(); + for (ResizableFrame *area : allAreas) { if (area->tagData() == oldTagData) { area->setTagData(category, newTag); } } } void AnnotationDialog::Dialog::descriptionPageUpDownPressed(QKeyEvent *event) { if (event->key() == Qt::Key_PageUp) { m_actions->action(QString::fromLatin1("annotationdialog-prev-image"))->trigger(); } else if (event->key() == Qt::Key_PageDown) { m_actions->action(QString::fromLatin1("annotationdialog-next-image"))->trigger(); } } void AnnotationDialog::Dialog::checkProposedTagData( QPair tagData, ResizableFrame *areaToExclude) const { - foreach (ResizableFrame *area, areas()) { + const auto allAreas = areas(); + for (ResizableFrame *area : allAreas) { if (area != areaToExclude && area->proposedTagData() == tagData && area->tagData().first.isEmpty()) { area->removeProposedTagData(); } } } void AnnotationDialog::Dialog::areaChanged() { m_areasChanged = true; } /** * @brief positionableTagValid checks whether a given tag can still be associated to an area. * This checks for empty and duplicate tags. * @return */ bool AnnotationDialog::Dialog::positionableTagAvailable(const QString &category, const QString &tag) const { if (category.isEmpty() || tag.isEmpty()) return false; // does any area already have that tag? - foreach (const ResizableFrame *area, areas()) { + const auto allAreas = areas(); + for (const ResizableFrame *area : allAreas) { const auto tagData = area->tagData(); if (tagData.first == category && tagData.second == tag) return false; } return true; } /** * @brief Generates a set of positionable tags currently used on the image * @param category * @return */ QSet AnnotationDialog::Dialog::positionedTags(const QString &category) const { QSet tags; - foreach (const ResizableFrame *area, areas()) { + const auto allAreas = areas(); + for (const ResizableFrame *area : allAreas) { const auto tagData = area->tagData(); if (tagData.first == category) tags += tagData.second; } return tags; } AnnotationDialog::ListSelect *AnnotationDialog::Dialog::listSelectForCategory(const QString &category) { return m_listSelectList.value(category, nullptr); } #ifdef HAVE_MARBLE void AnnotationDialog::Dialog::updateMapForCurrentImage() { if (m_setup != InputSingleImageConfigMode) { return; } // we can use the coordinates of the original images here, because the are never changed by the annotation dialog if (m_origList[m_current]->coordinates().hasCoordinates()) { m_annotationMap->setCenter(m_origList[m_current]); m_annotationMap->displayStatus(Map::MapStatus::ImageHasCoordinates); } else { m_annotationMap->displayStatus(Map::MapStatus::ImageHasNoCoordinates); } } void AnnotationDialog::Dialog::annotationMapVisibilityChanged(bool visible) { // This populates the map if it's added when the dialog is already open if (visible) { // when the map dockwidget is already visible on show(), the call to // annotationMapVisibilityChanged is executed in the GUI thread. // This ensures that populateMap() doesn't block the GUI in this case: QTimer::singleShot(0, this, SLOT(populateMap())); } else { m_cancelMapLoading = true; } } void AnnotationDialog::Dialog::populateMap() { // populateMap is called every time the map widget gets visible if (m_mapIsPopulated) { return; } m_annotationMap->displayStatus(Map::MapStatus::Loading); m_cancelMapLoading = false; m_mapLoadingProgress->setMaximum(m_origList.count()); m_mapLoadingProgress->show(); m_cancelMapLoadingButton->show(); int processedImages = 0; int imagesWithCoordinates = 0; // we can use the coordinates of the original images here, because the are never changed by the annotation dialog - foreach (const DB::ImageInfoPtr info, m_origList) { + for (const DB::ImageInfoPtr info : qAsConst(m_origList)) { processedImages++; m_mapLoadingProgress->setValue(processedImages); // keep things responsive by processing events manually: QApplication::processEvents(); if (m_annotationMap->addImage(info)) { imagesWithCoordinates++; } // m_cancelMapLoading is set to true by clicking the "Cancel" button if (m_cancelMapLoading) { m_annotationMap->clear(); break; } } m_annotationMap->buildImageClusters(); // at this point either we canceled loading or the map is populated: m_mapIsPopulated = !m_cancelMapLoading; mapLoadingFinished(imagesWithCoordinates > 0, imagesWithCoordinates == processedImages); } void AnnotationDialog::Dialog::setCancelMapLoading() { m_cancelMapLoading = true; } void AnnotationDialog::Dialog::mapLoadingFinished(bool mapHasImages, bool allImagesHaveCoordinates) { m_mapLoadingProgress->hide(); m_cancelMapLoadingButton->hide(); if (m_setup == InputSingleImageConfigMode) { m_annotationMap->displayStatus(Map::MapStatus::ImageHasNoCoordinates); } else { if (m_setup == SearchMode) { m_annotationMap->displayStatus(Map::MapStatus::SearchCoordinates); } else { if (mapHasImages) { if (!allImagesHaveCoordinates) { m_annotationMap->displayStatus(Map::MapStatus::SomeImagesHaveNoCoordinates); } else { m_annotationMap->displayStatus(Map::MapStatus::ImageHasCoordinates); } } else { m_annotationMap->displayStatus(Map::MapStatus::NoImagesHaveNoCoordinates); } } } if (m_setup != SearchMode) { m_annotationMap->zoomToMarkers(); updateMapForCurrentImage(); } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ImagePreview.cpp b/AnnotationDialog/ImagePreview.cpp index 7895ff49..d39ea59b 100644 --- a/AnnotationDialog/ImagePreview.cpp +++ b/AnnotationDialog/ImagePreview.cpp @@ -1,582 +1,577 @@ -/* Copyright (C) 2003-2018 Jesper K. Pedersen +/* Copyright (C) 2003-2020 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImagePreview.h" #include "Logging.h" #include "ResizableFrame.h" #include #include #include #include #include #include #include #include #include #include #include using namespace AnnotationDialog; ImagePreview::ImagePreview(QWidget *parent) : QLabel(parent) , m_selectionRect(0) , m_aspectRatio(1) , m_reloadTimer(new QTimer(this)) , m_areaCreationEnabled(false) { setAlignment(Qt::AlignCenter); setMinimumSize(64, 64); // "the widget can make use of extra space, so it should get as much space as possible" setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_reloadTimer->setSingleShot(true); connect(m_reloadTimer, &QTimer::timeout, this, &ImagePreview::resizeFinished); } void ImagePreview::resizeEvent(QResizeEvent *ev) { qCDebug(AnnotationDialogLog) << "Resizing from" << ev->oldSize() << "to" << ev->size(); // during resizing, a scaled image will do QImage scaledImage = m_currentImage.getImage().scaled(size(), Qt::KeepAspectRatio); setPixmap(QPixmap::fromImage(scaledImage)); updateScaleFactors(); // (re)start the timer to do a full reload m_reloadTimer->start(200); QLabel::resizeEvent(ev); } int ImagePreview::heightForWidth(int width) const { int height = width * m_aspectRatio; return height; } QSize ImagePreview::sizeHint() const { QSize hint = m_info.size(); qCDebug(AnnotationDialogLog) << "Preview size hint is" << hint; return hint; } void ImagePreview::rotate(int angle) { if (!m_info.isNull()) { m_currentImage.setAngle(m_info.angle()); m_info.rotate(angle, DB::RotateImageInfoOnly); } else { // Can this really happen? m_angle += angle; } m_preloader.cancelPreload(); m_lastImage.reset(); reload(); rotateAreas(angle); } void ImagePreview::setImage(const DB::ImageInfo &info) { m_info = info; reload(); } /** This method should only be used for the non-user images. Currently this includes two images: the search image and the configure several images at a time image. */ void ImagePreview::setImage(const QString &fileName) { m_fileName = fileName; m_info = DB::ImageInfo(); m_angle = 0; // Set the current angle that will be passed to m_lastImage m_currentImage.setAngle(m_info.angle()); reload(); } void ImagePreview::reload() { m_aspectRatio = 1; if (!m_info.isNull()) { if (m_preloader.has(m_info.fileName(), m_info.angle())) { qCDebug(AnnotationDialogLog) << "reload(): set preloader image"; setCurrentImage(m_preloader.getImage()); } else if (m_lastImage.has(m_info.fileName(), m_info.angle())) { qCDebug(AnnotationDialogLog) << "reload(): set last image"; //don't pass by reference, the additional constructor is needed here //see setCurrentImage for the reason (where m_lastImage is changed...) setCurrentImage(QImage(m_lastImage.getImage())); } else { if (!m_currentImage.has(m_info.fileName(), m_info.angle())) { // erase old image to prevent a laggy feel, // but only erase old image if it is a different image // (otherwise we get flicker when resizing) setPixmap(QPixmap()); } qCDebug(AnnotationDialogLog) << "reload(): set another image"; ImageManager::AsyncLoader::instance()->stop(this); ImageManager::ImageRequest *request = new ImageManager::ImageRequest(m_info.fileName(), size(), m_info.angle(), this); request->setPriority(ImageManager::Viewer); ImageManager::AsyncLoader::instance()->load(request); } } else { qCDebug(AnnotationDialogLog) << "reload(): set image from file"; QImage img(m_fileName); img = rotateAndScale(img, width(), height(), m_angle); setPixmap(QPixmap::fromImage(img)); } } int ImagePreview::angle() const { Q_ASSERT(!m_info.isNull()); return m_angle; } QSize ImagePreview::getActualImageSize() { if (!m_info.size().isValid()) { // We have to fetch the size from the image m_info.setSize(QImageReader(m_info.fileName().absolute()).size()); m_aspectRatio = m_info.size().height() / m_info.size().width(); } return m_info.size(); } void ImagePreview::setCurrentImage(const QImage &image) { // Cache the current image as the last image before changing it m_lastImage.set(m_currentImage); m_currentImage.set(m_info.fileName(), image, m_info.angle()); setPixmap(QPixmap::fromImage(image)); if (!m_anticipated.m_fileName.isNull()) m_preloader.preloadImage(m_anticipated.m_fileName, width(), height(), m_anticipated.m_angle); updateScaleFactors(); // Clear the full size image (if we have loaded one) m_fullSizeImage = QImage(); } void ImagePreview::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) { const DB::FileName fileName = request->databaseFileName(); const bool loadedOK = request->loadedOK(); if (loadedOK && !m_info.isNull()) { if (m_info.fileName() == fileName) setCurrentImage(image); } } void ImagePreview::anticipate(DB::ImageInfo &info1) { //We cannot call m_preloader.preloadImage right here: //this function is called before reload(), so if we preload here, //the preloader will always be loading the image after the next image. m_anticipated.set(info1.fileName(), info1.angle()); } ImagePreview::PreloadInfo::PreloadInfo() : m_angle(0) { } void ImagePreview::PreloadInfo::set(const DB::FileName &fileName, int angle) { m_fileName = fileName; m_angle = angle; } bool ImagePreview::PreviewImage::has(const DB::FileName &fileName, int angle) const { return fileName == m_fileName && !m_image.isNull() && angle == m_angle; } QImage &ImagePreview::PreviewImage::getImage() { return m_image; } void ImagePreview::PreviewImage::set(const DB::FileName &fileName, const QImage &image, int angle) { m_fileName = fileName; m_image = image; m_angle = angle; } void ImagePreview::PreviewImage::set(const PreviewImage &other) { m_fileName = other.m_fileName; m_image = other.m_image; m_angle = other.m_angle; } void ImagePreview::PreviewImage::setAngle(int angle) { m_angle = angle; } void ImagePreview::PreviewImage::reset() { m_fileName = DB::FileName(); m_image = QImage(); } void ImagePreview::PreviewLoader::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) { if (request->loadedOK()) { const DB::FileName fileName = request->databaseFileName(); set(fileName, image, request->angle()); } } void ImagePreview::PreviewLoader::preloadImage(const DB::FileName &fileName, int width, int height, int angle) { //no need to worry about concurrent access: everything happens in the event loop thread reset(); ImageManager::AsyncLoader::instance()->stop(this); ImageManager::ImageRequest *request = new ImageManager::ImageRequest(fileName, QSize(width, height), angle, this); request->setPriority(ImageManager::ViewerPreload); ImageManager::AsyncLoader::instance()->load(request); } void ImagePreview::PreviewLoader::cancelPreload() { reset(); ImageManager::AsyncLoader::instance()->stop(this); } QImage ImagePreview::rotateAndScale(QImage img, int width, int height, int angle) const { if (angle != 0) { QMatrix matrix; matrix.rotate(angle); img = img.transformed(matrix); } img = Utilities::scaleImage(img, QSize(width, height), Qt::KeepAspectRatio); return img; } void ImagePreview::updateScaleFactors() { if (m_info.isNull()) return; // search mode // Calculate a scale factor from the original image's size and it's current preview QSize actualSize = getActualImageSize(); QSize previewSize = pixmap()->size(); m_scaleWidth = double(actualSize.width()) / double(previewSize.width()); m_scaleHeight = double(actualSize.height()) / double(previewSize.height()); // Calculate the min and max coordinates inside the preview widget int previewWidth = previewSize.width(); int previewHeight = previewSize.height(); int widgetWidth = this->frameGeometry().width(); int widgetHeight = this->frameGeometry().height(); m_minX = (widgetWidth - previewWidth) / 2; m_maxX = m_minX + previewWidth - 1; m_minY = (widgetHeight - previewHeight) / 2; m_maxY = m_minY + previewHeight - 1; // Put all areas to their respective position on the preview remapAreas(); } void ImagePreview::mousePressEvent(QMouseEvent *event) { if (!m_areaCreationEnabled) { return; } if (event->button() & Qt::LeftButton) { if (!m_selectionRect) { m_selectionRect = new QRubberBand(QRubberBand::Rectangle, this); } m_areaStart = event->pos(); if (m_areaStart.x() < m_minX || m_areaStart.x() > m_maxX || m_areaStart.y() < m_minY || m_areaStart.y() > m_maxY) { // Dragging started outside of the preview image return; } m_selectionRect->setGeometry(QRect(m_areaStart, QSize())); m_selectionRect->show(); } } void ImagePreview::mouseMoveEvent(QMouseEvent *event) { if (!m_areaCreationEnabled) { return; } if (m_selectionRect && m_selectionRect->isVisible()) { m_currentPos = event->pos(); // Restrict the coordinates to the preview images's size if (m_currentPos.x() < m_minX) { m_currentPos.setX(m_minX); } if (m_currentPos.y() < m_minY) { m_currentPos.setY(m_minY); } if (m_currentPos.x() > m_maxX) { m_currentPos.setX(m_maxX); } if (m_currentPos.y() > m_maxY) { m_currentPos.setY(m_maxY); } m_selectionRect->setGeometry(QRect(m_areaStart, m_currentPos).normalized()); } } void ImagePreview::mouseReleaseEvent(QMouseEvent *event) { if (!m_areaCreationEnabled) { return; } if (event->button() & Qt::LeftButton && m_selectionRect->isVisible()) { m_areaEnd = event->pos(); processNewArea(); m_selectionRect->hide(); } } QPixmap ImagePreview::grabAreaImage(QRect area) { return QPixmap::fromImage(m_currentImage.getImage().copy(area.left() - m_minX, area.top() - m_minY, area.width(), area.height())); } QRect ImagePreview::areaPreviewToActual(QRect area) const { return QRect(QPoint(int(double(area.left() - m_minX) * m_scaleWidth), int(double(area.top() - m_minY) * m_scaleHeight)), QPoint(int(double(area.right() - m_minX) * m_scaleWidth), int(double(area.bottom() - m_minY) * m_scaleHeight))); } QRect ImagePreview::areaActualToPreview(QRect area) const { return QRect(QPoint(int(double(area.left() / m_scaleWidth)) + m_minX, int(double(area.top() / m_scaleHeight)) + m_minY), QPoint(int(double(area.right() / m_scaleWidth)) + m_minX, int(double(area.bottom() / m_scaleHeight)) + m_minY)); } void ImagePreview::createNewArea(QRect geometry, QRect actualGeometry) { // Create a ResizableFrame (cleaned up in Dialog::tidyAreas()) ResizableFrame *newArea = new ResizableFrame(this); newArea->setGeometry(geometry); // Be sure not to create an invisible area newArea->checkGeometry(); // In case the geometry has been changed by checkGeometry() actualGeometry = areaPreviewToActual(newArea->geometry()); // Store the coordinates on the real image (not on the preview) newArea->setActualCoordinates(actualGeometry); emit areaCreated(newArea); newArea->show(); newArea->showContextMenu(); } void ImagePreview::processNewArea() { if (m_areaStart == m_areaEnd) { // It was just a click, no area has been dragged return; } QRect newAreaPreview = QRect(m_areaStart, m_currentPos).normalized(); createNewArea(newAreaPreview, areaPreviewToActual(newAreaPreview)); } void ImagePreview::remapAreas() { - QList allAreas = this->findChildren(); - - if (allAreas.isEmpty()) { - return; - } - - foreach (ResizableFrame *area, allAreas) { + const auto allAreas = this->findChildren(); + for (ResizableFrame *area : allAreas) { area->setGeometry(areaActualToPreview(area->actualCoordinates())); } } QRect ImagePreview::rotateArea(QRect originalAreaGeometry, int angle) { // This is the current state of the image. We need the state before, so ... QSize unrotatedOriginalImageSize = getActualImageSize(); // ... un-rotate it unrotatedOriginalImageSize.transpose(); QRect rotatedAreaGeometry; rotatedAreaGeometry.setWidth(originalAreaGeometry.height()); rotatedAreaGeometry.setHeight(originalAreaGeometry.width()); if (angle == 90) { rotatedAreaGeometry.moveTo( unrotatedOriginalImageSize.height() - (originalAreaGeometry.height() + originalAreaGeometry.y()), originalAreaGeometry.x()); } else { rotatedAreaGeometry.moveTo( originalAreaGeometry.y(), unrotatedOriginalImageSize.width() - (originalAreaGeometry.width() + originalAreaGeometry.x())); } return rotatedAreaGeometry; } void ImagePreview::rotateAreas(int angle) { // Map all areas to their respective coordinates on the rotated actual image - QList allAreas = this->findChildren(); - foreach (ResizableFrame *area, allAreas) { + const auto allAreas = this->findChildren(); + for (ResizableFrame *area : allAreas) { area->setActualCoordinates(rotateArea(area->actualCoordinates(), angle)); } } void ImagePreview::resizeFinished() { qCDebug(AnnotationDialogLog) << "Reloading image after resize"; m_preloader.cancelPreload(); m_lastImage.reset(); reload(); } QRect ImagePreview::minMaxAreaPreview() const { return QRect(m_minX, m_minY, m_maxX, m_maxY); } void ImagePreview::createTaggedArea(QString category, QString tag, QRect geometry, bool showArea) { // Create a ResizableFrame (cleaned up in Dialog::tidyAreas()) ResizableFrame *newArea = new ResizableFrame(this); emit areaCreated(newArea); newArea->setGeometry(areaActualToPreview(geometry)); newArea->setActualCoordinates(geometry); newArea->setTagData(category, tag, AutomatedChange); newArea->setVisible(showArea); } void ImagePreview::setAreaCreationEnabled(bool state) { m_areaCreationEnabled = state; } // Currently only called when face detection/recognition is used void ImagePreview::fetchFullSizeImage() { if (m_fullSizeImage.isNull()) { m_fullSizeImage = QImage(m_info.fileName().absolute()); } if (m_angle != m_info.angle()) { QMatrix matrix; matrix.rotate(m_info.angle()); m_fullSizeImage = m_fullSizeImage.transformed(matrix); } } void ImagePreview::acceptProposedTag(QPair tagData, ResizableFrame *area) { // Be sure that we do have the category the proposed tag belongs to bool categoryFound = false; // Any warnings should only happen when the recognition database is e. g. copied from another // database location or has been changed outside of KPA. Anyways, this m_can_ happen, so we // have to handle it. QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); for (QList::ConstIterator categoryIt = categories.constBegin(); categoryIt != categories.constEnd(); ++categoryIt) { if ((*categoryIt)->name() == tagData.first) { if (!(*categoryIt)->positionable()) { KMessageBox::sorry(this, i18n("

Can't associate tag \"%2\"

" "

The category \"%1\" the tag \"%2\" belongs to is not positionable.

" "

If you want to use this tag, change this in the settings dialog. " "If this tag shouldn't be in the recognition database anymore, it can " "be deleted in the settings.

", tagData.first, tagData.second)); return; } categoryFound = true; break; } } if (!categoryFound) { KMessageBox::sorry(this, i18n("

Can't associate tag \"%2\"

" "

The category \"%1\" the tag \"%2\" belongs to does not exist.

" "

If you want to use this tag, add this category and mark it as positionable. " "If this tag shouldn't be in the recognition database anymore, it can " "be deleted in the settings dialog.

", tagData.first, tagData.second)); return; } // Tell all ListSelects that we accepted a proposed tag, so that the ListSelect // holding the respective category can ensure that the tag is checked emit proposedTagSelected(tagData.first, tagData.second); // Associate the area with the proposed tag area->setTagData(tagData.first, tagData.second); } bool ImagePreview::fuzzyAreaExists(QList &existingAreas, QRect area) { float maximumDeviation; for (int i = 0; i < existingAreas.size(); ++i) { // maximumDeviation is 15% of the mean value of the width and height of each area maximumDeviation = float(existingAreas.at(i).width() + existingAreas.at(i).height()) * 0.075; if ( distance(existingAreas.at(i).topLeft(), area.topLeft()) < maximumDeviation && distance(existingAreas.at(i).topRight(), area.topRight()) < maximumDeviation && distance(existingAreas.at(i).bottomLeft(), area.bottomLeft()) < maximumDeviation && distance(existingAreas.at(i).bottomRight(), area.bottomRight()) < maximumDeviation) { return true; } } return false; } float ImagePreview::distance(QPoint point1, QPoint point2) { QPoint difference = point1 - point2; return sqrt(pow(difference.x(), 2) + pow(difference.y(), 2)); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d97a7b1..d65b1938 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,556 +1,557 @@ cmake_minimum_required(VERSION 3.2.0) project(kphotoalbum LANGUAGES CXX VERSION 5.6.1) if(POLICY CMP0063) cmake_policy(SET CMP0063 NEW) endif() # provide drop-down menu for build-type in cmake-gui: set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS ";Debug;Release;RelWithDebInfo;MinSizeRel") list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) find_package(ECM REQUIRED NO_MODULE) list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ) include(KDEInstallDirs) include(KDECompilerSettings) include(KDECMakeSettings) include(FeatureSummary) # enable exceptions: kde_enable_exceptions() add_definitions( -DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII -DQT_NO_URL_CAST_FROM_STRING -DQT_NO_CAST_FROM_BYTEARRAY -DQT_DEPRECATED_WARNINGS -DQT_STRICT_ITERATORS -DQT_DISABLE_DEPRECATED_BEFORE=0x050900 + -DQT_DISABLE_Q_FOREACH ) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_REQUIRED TRUE) ########### dependencies ############### find_package(Qt5 5.9 REQUIRED COMPONENTS Sql Xml Widgets Network) find_package(Phonon4Qt5 REQUIRED) find_package(KF5 5.44 REQUIRED COMPONENTS Archive Completion Config CoreAddons DocTools I18n IconThemes JobWidgets KIO TextWidgets XmlGui WidgetsAddons) find_package(JPEG REQUIRED) if(JPEG_FOUND) include_directories(${JPEG_INCLUDE_DIR}) endif() ### 2018-12-30 jzarl # When Exiv2 0.26 can be deprecated, FindExiv2.cmake should be removed # and only find_package(exiv2) should be used find_package(exiv2 CONFIG QUIET) if(exiv2_FOUND) # search againg with REQUIRED, so that the feature summary correctly shows exiv as required dependency find_package(exiv2 CONFIG REQUIRED) set(EXIV2_LIBRARIES exiv2lib) else() find_package(Exiv2 REQUIRED) endif() find_package(KF5Kipi 5.1.0) set_package_properties(KF5Kipi PROPERTIES TYPE RECOMMENDED PURPOSE "Enables integration of KDE image plugin interface (shared functionality between KPhotoAlbum and other apps like gwenview or digikam)" ) set(HASKIPI ${KF5Kipi_FOUND}) find_package(KF5Purpose) set_package_properties(KF5Purpose PROPERTIES TYPE RECOMMENDED PURPOSE "Enables integration with KDE Purpose plugins, which provide image sharing and similar functionality." ) find_package(KF5KDcraw) set_package_properties(KF5KDcraw PROPERTIES TYPE OPTIONAL PURPOSE "Enables RAW image support" ) set(HAVE_KDCRAW ${KF5KDcraw_FOUND} ) find_package(Marble) set_package_properties(Marble PROPERTIES TYPE OPTIONAL PURPOSE "Enables support for geographic map location using embedded GPS information." ) set(HAVE_MARBLE ${Marble_FOUND}) if(Marble_FOUND) include(MarbleChecks) endif() add_custom_target( UpdateVersion ALL COMMAND ${CMAKE_COMMAND} -DBASE_DIR=${CMAKE_CURRENT_SOURCE_DIR} -DPROJECT_NAME=KPA -DPROJECT_VERSION="${PROJECT_VERSION}" -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/UpdateVersion.cmake" COMMENT "Updating version header." BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/version.h" ) # For config-kpa-*.h include_directories(${CMAKE_CURRENT_BINARY_DIR}) set(libdatebar_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/DateBar/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/DateBar/DateBarWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DateBar/ViewHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DateBar/MouseHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DateBar/MouseHandler.cpp ) set(libSettings_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Settings/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Settings/SettingsData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/SettingsDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/ViewerSizeConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/CategoryItem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/CategoryPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/TagGroupsPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/GeneralPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/FileVersionDetectionPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/ThumbnailsPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/ViewerPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/DatabaseBackendPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/UntaggedGroupBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/CategoriesGroupsWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/BirthdayPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/DateTableWidgetItem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/Logging.cpp ) set(libxmldb_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/Database.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/XMLCategoryCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/XMLCategory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/XMLImageDateCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/NumberedBackup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/FileReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/FileWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/ElementWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/XmlReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/CompressFileInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/Logging.cpp ) set(libThumbnailView_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/FilterWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailRequest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailToolTip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/GridResizeInteraction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/GridResizeSlider.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/SelectionInteraction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/MouseTrackingInteraction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/CellGeometry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailModel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailFacade.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/KeyboardEventHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailDND.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/Delegate.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/SelectionMaintainer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/VideoThumbnailCycler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/MouseInteraction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailFactory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/enums.cpp ) set(libPlugins_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/Logging.cpp ) if(KF5Kipi_FOUND) set(libPlugins_SRCS ${libPlugins_SRCS} ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/Interface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/ImageCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/ImageInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/CategoryImageCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/ImageCollectionSelector.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/PluginsPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/UploadWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/UploadImageCollection.cpp ) endif() if(KF5Purpose_FOUND) set(libPlugins_SRCS ${libPlugins_SRCS} ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/PurposeMenu.cpp ) endif() set(libViewer_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/ViewerWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/ImageDisplay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/ViewHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/SpeedDisplay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/InfoBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/CategoryImageConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/AbstractDisplay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/VideoDisplay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/TextDisplay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/InfoBoxResizer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/VisibleOptionsMenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/VideoShooter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/TaggedArea.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/Logging.cpp ) set(libCategoryListView_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/DragableTreeWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/CheckDropItem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/DragItemInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/Logging.cpp ) set(libHTMLGenerator_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/HTMLDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/Generator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/Setup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/ImageSizeCheckBox.h ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/ImageSizeCheckBox.cpp ) set(libUtilities_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/AlgorithmHelper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/ShowBusyCursor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/List.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/UniqFilenameMapper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/FileUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/BooleanGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/DeleteFiles.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/ToolTip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/JpeglibWithFix.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/StringSet.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/FastJpeg.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/DemoUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/DescriptionUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/FileNameUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/VideoUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/ImageUtil.cpp ) set(libMainWindow_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/DeleteDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/RunDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/FeatureDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/InvalidDateFinder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/AutoStackImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/TokenEditor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/WelcomeDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/Window.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/SplashScreen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/ExternalPopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/CategoryImagePopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/SearchBar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/ImageCounter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/DirtyIndicator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/StatisticsDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/BreadcrumbViewer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/StatusBar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/UpdateVideoThumbnail.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/DuplicateMerger/DuplicateMerger.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/DuplicateMerger/DuplicateMatch.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/DuplicateMerger/MergeToolTip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/CopyPopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/Options.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/Logging.cpp ) set(libImageManager_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ImageLoaderThread.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/AsyncLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ImageRequest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ImageClientInterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ImageDecoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/RawImageDecoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/RequestQueue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ThumbnailCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ImageEvent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ThumbnailBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/PreloadRequest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/CancelEvent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/VideoImageRescaleRequest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/VideoThumbnails.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/VideoLengthExtractor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ExtractOneVideoFrame.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/CacheFileInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/enums.cpp ) set(libDB_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/DB/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/Category.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/CategoryCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ExactCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageDate.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/MD5Map.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/MemberMap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageInfoList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageDB.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/FileInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/NegationCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/NewImageFinder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageScout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/NoTagCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/GroupCounter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/CategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageSearchInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/CategoryItem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ContainerCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ValueCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/OrCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/AndCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/FastDir.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/OptimizedFileList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/FileName.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/FileNameList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/CategoryPtr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ExifMode.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageDateCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageInfoPtr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/MD5.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/MediaCount.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/RawId.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/SimpleCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/UIDelegate.cpp ) set(libImportExport_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/Import.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/ImportMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/XMLHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/MiniViewer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/ImportHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/ImageRow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/ImportDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/ImportSettings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/KimFileReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/MD5CheckPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/Logging.cpp ) set(libAnnotationDialog_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/Dialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ListSelect.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ImagePreview.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ImagePreviewWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/DateEdit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/CompletableLineEdit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ListViewItemHider.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ShowSelectionOnlyManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ShortCutManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ResizableFrame.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/DescriptionEdit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/AreaTagSelectDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/enums.cpp ) set(libBrowser_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Browser/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Browser/BrowserWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/BrowserPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/OverviewPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/CategoryPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/ImageViewPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/TreeFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/Breadcrumb.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/BreadcrumbList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/AbstractCategoryModel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/FlatCategoryModel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/TreeCategoryModel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/CenteringIconView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/enums.cpp ) set(libExif_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Exif/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Exif/Database.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/InfoDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/SearchDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/SearchInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/TreeView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/Info.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/RangeWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/DatabaseElement.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/ReReadDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/Grid.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/SearchDialogSettings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/ExifPage.cpp ) set(libBackgroundTaskManager_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/JobInterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/JobManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/StatusIndicator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/JobViewer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/JobModel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/JobInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/CompletedJobInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/Priority.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/PriorityQueue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/Logging.cpp ) set(libBackgroundJobs_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundJobs/SearchForVideosWithoutLengthInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundJobs/ReadVideoLengthJob.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundJobs/SearchForVideosWithoutVideoThumbnailsJob.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundJobs/HandleVideoThumbnailRequestJob.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundJobs/ExtractOneThumbnailJob.cpp ) option(KPA_ENABLE_REMOTECONTROL "Build with support for companion Android/QML app." OFF) set(libRemoteControl_SRCS) if(KPA_ENABLE_REMOTECONTROL) # requires cmake 3.12 add_compile_definitions(KPA_ENABLE_REMOTECONTROL) set(libRemoteControl_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/RemoteCommand.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/RemoteConnection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/Server.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/RemoteInterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/SearchInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/RemoteImageRequest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/ImageNameStore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/ConnectionIndicator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/Logging.cpp ) endif() set(libMap_SRCS) if(Marble_FOUND) set(libMap_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Browser/GeoPositionPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/GeoCluster.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/MapView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/GeoCoordinates.cpp ) endif() add_subdirectory(images) add_subdirectory(icons) add_subdirectory(demo) add_subdirectory(themes) add_subdirectory(scripts) add_subdirectory(doc) ########### next target ############### set(kphotoalbum_SRCS main.cpp ${libdatebar_SRCS} ${libSettings_SRCS} ${libsurvey_SRCS} ${libxmldb_SRCS} ${libThumbnailView_SRCS} ${libPlugins_SRCS} ${libViewer_SRCS} ${libCategoryListView_SRCS} ${libHTMLGenerator_SRCS} ${libMainWindow_SRCS} ${libImageManager_SRCS} ${libDB_SRCS} ${libImportExport_SRCS} ${libAnnotationDialog_SRCS} ${libExif_SRCS} ${libBrowser_SRCS} ${libBackgroundTaskManager_SRCS} ${libBackgroundJobs_SRCS} ${libRemoteControl_SRCS} ${libMap_SRCS} ${libUtilities_SRCS} ) add_executable(kphotoalbum ${kphotoalbum_SRCS}) add_dependencies(kphotoalbum UpdateVersion) # External components target_link_libraries(kphotoalbum ${JPEG_LIBRARY} ${EXIV2_LIBRARIES} KF5::Archive KF5::Completion KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons KF5::I18n KF5::IconThemes KF5::JobWidgets KF5::KIOCore KF5::KIOWidgets KF5::TextWidgets KF5::XmlGui KF5::WidgetsAddons Phonon::phonon4qt5 Qt5::Network Qt5::Sql ) if(KF5Kipi_FOUND) target_link_libraries(kphotoalbum KF5::Kipi) endif() if(KF5Purpose_FOUND) target_link_libraries(kphotoalbum KF5::Purpose KF5::PurposeWidgets) endif() if(KF5KDcraw_FOUND) target_link_libraries(kphotoalbum KF5::KDcraw) endif() if(Marble_FOUND) target_link_libraries(kphotoalbum Marble) endif() install(TARGETS kphotoalbum ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) ########### install files ############### install(PROGRAMS org.kde.kphotoalbum.desktop org.kde.kphotoalbum-import.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES kphotoalbumrc DESTINATION ${KDE_INSTALL_CONFDIR}) install(FILES tips default-setup DESTINATION ${KDE_INSTALL_DATADIR}/kphotoalbum) install(FILES kphotoalbumui.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/kphotoalbum) install(FILES org.kde.kphotoalbum.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) configure_file(config-kpa-kdcraw.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kpa-kdcraw.h) configure_file(config-kpa-kipi.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kpa-kipi.h) configure_file(config-kpa-marble.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kpa-marble.h) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) # vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/TagGroupsPage.cpp b/Settings/TagGroupsPage.cpp index d6d4eb76..65bdcdde 100644 --- a/Settings/TagGroupsPage.cpp +++ b/Settings/TagGroupsPage.cpp @@ -1,796 +1,796 @@ /* Copyright (C) 2003-2020 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "TagGroupsPage.h" // Qt includes #include #include #include #include #include #include #include #include #include #include #include // KDE includes #include #include // Local includes #include "CategoriesGroupsWidget.h" #include "SettingsData.h" #include #include Settings::TagGroupsPage::TagGroupsPage(QWidget *parent) : QWidget(parent) { QGridLayout *layout = new QGridLayout(this); // The category and group tree layout->addWidget(new QLabel(i18nc("@label", "Categories and groups:")), 0, 0); m_categoryTreeWidget = new CategoriesGroupsWidget(this); m_categoryTreeWidget->header()->hide(); m_categoryTreeWidget->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_categoryTreeWidget, 1, 0); connect(m_categoryTreeWidget, &CategoriesGroupsWidget::customContextMenuRequested, this, &TagGroupsPage::showTreeContextMenu); connect(m_categoryTreeWidget, &CategoriesGroupsWidget::itemClicked, this, &TagGroupsPage::slotGroupSelected); // The member list m_selectGroupToAddTags = i18nc("@label/rich", "Select a group on the left side to add tags to it"); m_tagsInGroupLabel = new QLabel(m_selectGroupToAddTags); layout->addWidget(m_tagsInGroupLabel, 0, 1); m_membersListWidget = new QListWidget; m_membersListWidget->setEnabled(false); m_membersListWidget->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_membersListWidget, 1, 1); connect(m_membersListWidget, &QListWidget::itemChanged, this, &TagGroupsPage::checkItemSelection); connect(m_membersListWidget, &QListWidget::customContextMenuRequested, this, &TagGroupsPage::showMembersContextMenu); // The "pending rename actions" label m_pendingChangesLabel = new QLabel(i18nc("@label/rich", "There are pending changes on the categories page. " "Please save the changes before working on tag groups.")); m_pendingChangesLabel->hide(); layout->addWidget(m_pendingChangesLabel, 2, 0, 1, 2); QDialog *parentDialog = qobject_cast(parent); connect(parentDialog, &QDialog::rejected, this, &TagGroupsPage::discardChanges); // Context menu actions m_newGroupAction = new QAction(i18nc("@action:inmenu", "Add group ..."), this); connect(m_newGroupAction, &QAction::triggered, this, &TagGroupsPage::slotAddGroup); m_renameAction = new QAction(this); connect(m_renameAction, &QAction::triggered, this, &TagGroupsPage::slotRenameGroup); m_deleteAction = new QAction(this); connect(m_deleteAction, &QAction::triggered, this, &TagGroupsPage::slotDeleteGroup); m_deleteMemberAction = new QAction(this); connect(m_deleteMemberAction, &QAction::triggered, this, &TagGroupsPage::slotDeleteMember); m_renameMemberAction = new QAction(this); connect(m_renameMemberAction, &QAction::triggered, this, &TagGroupsPage::slotRenameMember); m_memberMap = DB::ImageDB::instance()->memberMap(); connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::itemRemoved, &m_memberMap, &DB::MemberMap::deleteItem); connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::itemRenamed, &m_memberMap, &DB::MemberMap::renameItem); connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::categoryRemoved, &m_memberMap, &DB::MemberMap::deleteCategory); m_dataChanged = false; } void Settings::TagGroupsPage::updateCategoryTree() { // Store all expanded items so that they can be expanded after reload QList> expandedItems = QList>(); for (QTreeWidgetItemIterator it { m_categoryTreeWidget }; *it; ++it) { if ((*it)->isExpanded()) { QString parentName; if ((*it)->parent() != nullptr) { parentName = (*it)->parent()->text(0); } expandedItems.append(QPair((*it)->text(0), parentName)); } } m_categoryTreeWidget->clear(); // Create a tree view of all groups and their sub-groups const QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); for (const DB::CategoryPtr &category : categories) { if (category->isSpecialCategory()) { continue; } // Add the real categories as top-level items QTreeWidgetItem *topLevelItem = new QTreeWidgetItem; topLevelItem->setText(0, category->name()); topLevelItem->setFlags(topLevelItem->flags() & Qt::ItemIsEnabled); QFont font = topLevelItem->font(0); font.setWeight(QFont::Bold); topLevelItem->setFont(0, font); m_categoryTreeWidget->addTopLevelItem(topLevelItem); // Build a map with all members for each group QMap membersForGroup; const QStringList allGroups = m_memberMap.groups(category->name()); - foreach (const QString &group, allGroups) { + for (const QString &group : allGroups) { // FIXME: Why does the member map return an empty category?! if (group.isEmpty()) { continue; } QStringList allMembers = m_memberMap.members(category->name(), group, true); membersForGroup[group] = allMembers; } // Add all groups (their sub-groups will be added recursively) addSubCategories(topLevelItem, membersForGroup, allGroups); } // Order the items alphabetically m_categoryTreeWidget->sortItems(0, Qt::AscendingOrder); // Re-expand all previously expanded items for (QTreeWidgetItemIterator it { m_categoryTreeWidget }; *it; ++it) { QString parentName; if ((*it)->parent() != nullptr) { parentName = (*it)->parent()->text(0); } if (expandedItems.contains(QPair((*it)->text(0), parentName))) { (*it)->setExpanded(true); } } } void Settings::TagGroupsPage::addSubCategories(QTreeWidgetItem *superCategory, const QMap &membersForGroup, const QStringList &allGroups) { // Process all group members for (auto memIt = membersForGroup.constBegin(); memIt != membersForGroup.constEnd(); ++memIt) { const QString &group = memIt.key(); bool isSubGroup = false; // Search for a membership in another group for the current group for (const QStringList &members : membersForGroup) { if (members.contains(group)) { isSubGroup = true; break; } } // Add the group if it's not member of another group if (!isSubGroup) { QTreeWidgetItem *groupItem = new QTreeWidgetItem; groupItem->setText(0, group); superCategory->addChild(groupItem); // Search the member list for other groups QMap subGroups; - foreach (const QString &groupName, allGroups) { + for (const QString &groupName : allGroups) { if (membersForGroup[group].contains(groupName)) { subGroups[groupName] = membersForGroup[groupName]; } } // If the list contains other groups, add them recursively if (subGroups.count() > 0) { addSubCategories(groupItem, subGroups, allGroups); } } } } QString Settings::TagGroupsPage::getCategory(QTreeWidgetItem *currentItem) { while (currentItem->parent() != nullptr) { currentItem = currentItem->parent(); } return currentItem->text(0); } void Settings::TagGroupsPage::showTreeContextMenu(QPoint point) { QTreeWidgetItem *currentItem = m_categoryTreeWidget->currentItem(); if (currentItem == nullptr) { return; } m_currentSubCategory = currentItem->text(0); if (currentItem->parent() == nullptr) { // It's a top-level, "real" category m_currentSuperCategory.clear(); } else { // It's a normal sub-category that belongs to another one m_currentSuperCategory = currentItem->parent()->text(0); } m_currentCategory = getCategory(currentItem); QMenu *menu = new QMenu; menu->addAction(m_newGroupAction); // "Real" top-level categories have to processed on the category page. if (!m_currentSuperCategory.isEmpty()) { menu->addSeparator(); m_renameAction->setText(i18nc("@action:inmenu", "Rename group \"%1\"", m_currentSubCategory)); menu->addAction(m_renameAction); m_deleteAction->setText(i18nc("@action:inmenu", "Delete group \"%1\"", m_currentSubCategory)); menu->addAction(m_deleteAction); } menu->exec(m_categoryTreeWidget->mapToGlobal(point)); delete menu; } void Settings::TagGroupsPage::categoryChanged(const QString &name) { if (name.isEmpty()) { return; } m_membersListWidget->blockSignals(true); m_membersListWidget->clear(); const QStringList list = getCategoryObject(name)->items() + m_memberMap.groups(name); QStringList alreadyAdded; for (const QString &member : list) { if (member.isEmpty()) { // This can happen if we add group that currently has no members. continue; } if (!alreadyAdded.contains(member)) { alreadyAdded << member; if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && !Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (name == Settings::SettingsData::instance()->untaggedCategory()) { if (member == Settings::SettingsData::instance()->untaggedTag()) { continue; } } } QListWidgetItem *newItem = new QListWidgetItem(member, m_membersListWidget); newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); newItem->setCheckState(Qt::Unchecked); } } m_currentGroup.clear(); m_membersListWidget->clearSelection(); m_membersListWidget->sortItems(); m_membersListWidget->setEnabled(false); m_membersListWidget->blockSignals(false); } void Settings::TagGroupsPage::slotGroupSelected(QTreeWidgetItem *item) { // When something else than a "real" category has been selected before, // we have to save it's members. if (!m_currentGroup.isEmpty()) { saveOldGroup(); } if (item->parent() == nullptr) { // A "real" category has been selected, not a group m_currentCategory.clear(); m_currentGroup.clear(); m_membersListWidget->setEnabled(false); categoryChanged(item->text(0)); m_tagsInGroupLabel->setText(m_selectGroupToAddTags); return; } // Let's see if the category changed QString itemCategory = getCategory(item); if (m_currentCategory != itemCategory) { m_currentCategory = itemCategory; categoryChanged(m_currentCategory); } m_currentGroup = item->text(0); selectMembers(m_currentGroup); m_tagsInGroupLabel->setText(i18nc("@label", "Tags in group \"%1\" of category \"%2\"", m_currentGroup, m_currentCategory)); } void Settings::TagGroupsPage::slotAddGroup() { bool ok; DB::CategoryPtr category = getCategoryObject(m_currentCategory); QStringList groups = m_memberMap.groups(m_currentCategory); QStringList tags; for (QString tag : category->items()) { if (!groups.contains(tag)) { tags << tag; } } //// reject existing group names: //KStringListValidator validator(groups); //QString newSubCategory = KInputDialog::getText(i18nc("@title:window","New Group"), // i18nc("@label:textbox","Group name:"), // QString() /*value*/, // &ok, // this /*parent*/, // &validator, // QString() /*mask*/, // QString() /*WhatsThis*/, // tags /*completion*/ // ); // FIXME: KF5-port: QInputDialog does not accept a validator, // and KInputDialog was removed in KF5. -> Reimplement input validation using other stuff QString newSubCategory = QInputDialog::getText(this, i18nc("@title:window", "New Group"), i18nc("@label:textbox", "Group name:"), QLineEdit::Normal, QString(), &ok); if (groups.contains(newSubCategory)) return; // only a workaround until GUI-support for validation is restored if (!ok) { return; } // Let's see if we already have this group if (groups.contains(newSubCategory)) { // (with the validator working correctly, we should not get to this point) KMessageBox::sorry(this, i18nc("@info", "

The group \"%1\" already exists.

", newSubCategory), i18nc("@title:window", "Cannot add group")); return; } // Add the group as a new tag to the respective category MainWindow::DirtyIndicator::suppressMarkDirty(true); category->addItem(newSubCategory); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Add] = newSubCategory; m_categoryChanges.append(categoryChange); // Add the group m_memberMap.addGroup(m_currentCategory, newSubCategory); // Display the new group categoryChanged(m_currentCategory); // Display the new item QTreeWidgetItem *parentItem = m_categoryTreeWidget->currentItem(); addNewSubItem(newSubCategory, parentItem); // Check if we also have to update some other group (in case this is not a top-level group) if (!m_currentSuperCategory.isEmpty()) { m_memberMap.addMemberToGroup(m_currentCategory, parentItem->text(0), newSubCategory); slotGroupSelected(parentItem); } m_dataChanged = true; } void Settings::TagGroupsPage::addNewSubItem(QString &name, QTreeWidgetItem *parentItem) { QTreeWidgetItem *newItem = new QTreeWidgetItem; newItem->setText(0, name); parentItem->addChild(newItem); if (!parentItem->isExpanded()) { parentItem->setExpanded(true); } } QTreeWidgetItem *Settings::TagGroupsPage::findCategoryItem(QString category) { QTreeWidgetItem *categoryItem = nullptr; for (int i = 0; i < m_categoryTreeWidget->topLevelItemCount(); ++i) { categoryItem = m_categoryTreeWidget->topLevelItem(i); if (categoryItem->text(0) == category) { break; } } return categoryItem; } void Settings::TagGroupsPage::checkItemSelection(QListWidgetItem *) { m_dataChanged = true; saveOldGroup(); updateCategoryTree(); } void Settings::TagGroupsPage::slotRenameGroup() { bool ok; DB::CategoryPtr category = getCategoryObject(m_currentCategory); QStringList groups = m_memberMap.groups(m_currentCategory); QStringList tags; for (QString tag : category->items()) { if (!groups.contains(tag)) { tags << tag; } } // FIXME: reject existing group names QString newSubCategoryName = QInputDialog::getText(this, i18nc("@title:window", "Rename Group"), i18nc("@label:textbox", "New group name:"), QLineEdit::Normal, m_currentSubCategory, &ok); if (!ok || m_currentSubCategory == newSubCategoryName) { return; } if (groups.contains(newSubCategoryName)) { // (with the validator working correctly, we should not get to this point) KMessageBox::sorry(this, xi18nc("@info", "Cannot rename group \"%1\" to \"%2\": " "\"%2\" already exists in category \"%3\"", m_currentSubCategory, newSubCategoryName, m_currentCategory), i18nc("@title:window", "Rename Group")); return; } QTreeWidgetItem *selectedGroup = m_categoryTreeWidget->currentItem(); saveOldGroup(); // Update the group m_memberMap.renameGroup(m_currentCategory, m_currentSubCategory, newSubCategoryName); // Update the tag in the respective category MainWindow::DirtyIndicator::suppressMarkDirty(true); category->renameItem(m_currentSubCategory, newSubCategoryName); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Rename] = m_currentSubCategory; categoryChange[CategoryEdit::NewName] = newSubCategoryName; m_categoryChanges.append(categoryChange); m_dataChanged = true; // Search for all possible sub-category items in this category that have to be renamed QTreeWidgetItem *categoryItem = findCategoryItem(m_currentCategory); for (int i = 0; i < categoryItem->childCount(); ++i) { renameAllSubCategories(categoryItem->child(i), m_currentSubCategory, newSubCategoryName); } // Update the displayed items categoryChanged(m_currentCategory); slotGroupSelected(selectedGroup); m_dataChanged = true; } void Settings::TagGroupsPage::renameAllSubCategories(QTreeWidgetItem *categoryItem, QString oldName, QString newName) { // Probably, it item itself has to be renamed if (categoryItem->text(0) == oldName) { categoryItem->setText(0, newName); } // Also check all sub-categories recursively for (int i = 0; i < categoryItem->childCount(); ++i) { renameAllSubCategories(categoryItem->child(i), oldName, newName); } } void Settings::TagGroupsPage::slotDeleteGroup() { QTreeWidgetItem *currentItem = m_categoryTreeWidget->currentItem(); QString message; QString title; if (currentItem->childCount() > 0) { message = xi18nc("@info", "Really delete group \"%1\"?" "Sub-categories of this group will be moved to the super category of \"%1\" (\"%2\"). " "All other memberships of the sub-categories will stay intact.", m_currentSubCategory, m_currentSuperCategory); } else { message = xi18nc("@info", "Really delete group \"%1\"?", m_currentSubCategory); } int res = KMessageBox::warningContinueCancel(this, message, i18nc("@title:window", "Delete Group"), KGuiItem(i18n("&Delete"), QString::fromUtf8("editdelete"))); if (res == KMessageBox::Cancel) { return; } // Delete the group m_memberMap.deleteGroup(m_currentCategory, m_currentSubCategory); // Delete the tag MainWindow::DirtyIndicator::suppressMarkDirty(true); getCategoryObject(m_currentCategory)->removeItem(m_currentSubCategory); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Remove] = m_currentSubCategory; m_categoryChanges.append(categoryChange); m_dataChanged = true; slotPageChange(); m_dataChanged = true; } void Settings::TagGroupsPage::saveOldGroup() { QStringList list; for (int i = 0; i < m_membersListWidget->count(); ++i) { QListWidgetItem *item = m_membersListWidget->item(i); if (item->checkState() == Qt::Checked) { list << item->text(); } } m_memberMap.setMembers(m_currentCategory, m_currentGroup, list); } void Settings::TagGroupsPage::selectMembers(const QString &group) { m_membersListWidget->blockSignals(true); m_membersListWidget->setEnabled(false); m_currentGroup = group; QStringList memberList = m_memberMap.members(m_currentCategory, group, false); for (int i = 0; i < m_membersListWidget->count(); ++i) { QListWidgetItem *item = m_membersListWidget->item(i); item->setCheckState(Qt::Unchecked); if (!m_memberMap.canAddMemberToGroup(m_currentCategory, group, item->text())) { item->setFlags(item->flags() & ~Qt::ItemIsSelectable & ~Qt::ItemIsEnabled); } else { item->setFlags(item->flags() | Qt::ItemIsSelectable | Qt::ItemIsEnabled); if (memberList.contains(item->text())) { item->setCheckState(Qt::Checked); } } } m_membersListWidget->setEnabled(true); m_membersListWidget->blockSignals(false); } void Settings::TagGroupsPage::slotPageChange() { m_tagsInGroupLabel->setText(m_selectGroupToAddTags); m_membersListWidget->setEnabled(false); m_membersListWidget->clear(); m_currentCategory.clear(); updateCategoryTree(); } void Settings::TagGroupsPage::saveSettings() { saveOldGroup(); slotPageChange(); DB::ImageDB::instance()->memberMap() = m_memberMap; m_categoryChanges.clear(); if (m_dataChanged) { m_dataChanged = false; MainWindow::DirtyIndicator::markDirty(); } m_categoryTreeWidget->setEnabled(true); m_membersListWidget->setEnabled(true); m_pendingChangesLabel->hide(); } void Settings::TagGroupsPage::discardChanges() { m_memberMap = DB::ImageDB::instance()->memberMap(); slotPageChange(); m_dataChanged = false; // Revert all changes to the "real" category objects MainWindow::DirtyIndicator::suppressMarkDirty(true); for (int i = m_categoryChanges.size() - 1; i >= 0; i--) { DB::CategoryPtr category = getCategoryObject(m_categoryChanges.at(i)[CategoryEdit::Category]); if (m_categoryChanges.at(i).contains(CategoryEdit::Add)) { // Remove added tags category->removeItem(m_categoryChanges.at(i)[CategoryEdit::Add]); } else if (m_categoryChanges.at(i).contains(CategoryEdit::Remove)) { // Add removed tags category->addItem(m_categoryChanges.at(i)[CategoryEdit::Add]); } else if (m_categoryChanges.at(i).contains(CategoryEdit::Rename)) { // Re-rename tags to their old name category->renameItem(m_categoryChanges.at(i)[CategoryEdit::NewName], m_categoryChanges.at(i)[Rename]); } } MainWindow::DirtyIndicator::suppressMarkDirty(false); m_categoryChanges.clear(); m_categoryTreeWidget->setEnabled(true); m_membersListWidget->setEnabled(true); m_pendingChangesLabel->hide(); } void Settings::TagGroupsPage::loadSettings() { categoryChanged(m_currentCategory); updateCategoryTree(); } void Settings::TagGroupsPage::categoryChangesPending() { m_categoryTreeWidget->setEnabled(false); m_membersListWidget->setEnabled(false); m_pendingChangesLabel->show(); } DB::MemberMap *Settings::TagGroupsPage::memberMap() { return &m_memberMap; } void Settings::TagGroupsPage::processDrop(QTreeWidgetItem *draggedItem, QTreeWidgetItem *targetItem) { if (targetItem->parent() != nullptr) { // Dropped on a group // Select the group m_categoryTreeWidget->setCurrentItem(targetItem); slotGroupSelected(targetItem); // Check the dragged group on the member side to make it a sub-group of the target group m_membersListWidget->findItems(draggedItem->text(0), Qt::MatchExactly)[0]->setCheckState(Qt::Checked); } else { // Dropped on a top-level category // Check if it's already a direct child of the category. // If so, we don't need to do anything. QTreeWidgetItem *parent = draggedItem->parent(); if (parent->parent() == nullptr) { return; } // Select the former super group m_categoryTreeWidget->setCurrentItem(parent); slotGroupSelected(parent); // Deselect the dragged group (this will bring it to the top level) m_membersListWidget->findItems(draggedItem->text(0), Qt::MatchExactly)[0]->setCheckState(Qt::Unchecked); } } void Settings::TagGroupsPage::showMembersContextMenu(QPoint point) { if (m_membersListWidget->currentItem() == nullptr) { return; } QMenu *menu = new QMenu; m_renameMemberAction->setText(i18nc("@action:inmenu", "Rename \"%1\"", m_membersListWidget->currentItem()->text())); menu->addAction(m_renameMemberAction); m_deleteMemberAction->setText(i18nc("@action:inmenu", "Delete \"%1\"", m_membersListWidget->currentItem()->text())); menu->addAction(m_deleteMemberAction); menu->exec(m_membersListWidget->mapToGlobal(point)); delete menu; } void Settings::TagGroupsPage::slotRenameMember() { bool ok; QString newTagName = QInputDialog::getText(this, i18nc("@title:window", "New Tag Name"), i18nc("@label:textbox", "Tag name:"), QLineEdit::Normal, m_membersListWidget->currentItem()->text(), &ok); if (!ok || newTagName == m_membersListWidget->currentItem()->text()) { return; } // Update the tag name in the database MainWindow::DirtyIndicator::suppressMarkDirty(true); getCategoryObject(m_currentCategory)->renameItem(m_membersListWidget->currentItem()->text(), newTagName); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Rename] = m_membersListWidget->currentItem()->text(); categoryChange[CategoryEdit::NewName] = newTagName; m_categoryChanges.append(categoryChange); // Update the displayed tag name m_membersListWidget->currentItem()->setText(newTagName); // Re-order the tags, as their alphabetial order may have changed m_membersListWidget->sortItems(); } void Settings::TagGroupsPage::slotDeleteMember() { QString memberToDelete = m_membersListWidget->currentItem()->text(); if (m_memberMap.groups(m_currentCategory).contains(memberToDelete)) { // The item to delete is a group // Find the tag in the tree view and select it ... QTreeWidgetItemIterator it(m_categoryTreeWidget); while (*it) { if ((*it)->text(0) == memberToDelete && getCategory((*it)) == m_currentCategory) { m_categoryTreeWidget->setCurrentItem((*it)); m_currentSubCategory = (*it)->text(0); m_currentSuperCategory = (*it)->parent()->text(0); break; } ++it; } // ... then delete it like it had been requested by the TreeWidget's context menu slotDeleteGroup(); } else { // The item to delete is a normal tag int res = KMessageBox::warningContinueCancel(this, xi18nc("@info", "Do you really want to delete \"%1\"?" "Deleting the item will remove any information " "about it from any image containing the item.", memberToDelete), i18nc("@title:window", "Really delete %1?", memberToDelete), KGuiItem(i18n("&Delete"), QString::fromUtf8("editdelete"))); if (res != KMessageBox::Continue) { return; } // Delete the tag as if it had been deleted from the annotation dialog. getCategoryObject(m_currentCategory)->removeItem(memberToDelete); slotPageChange(); } } DB::CategoryPtr Settings::TagGroupsPage::getCategoryObject(QString category) const { return DB::ImageDB::instance()->categoryCollection()->categoryForName(category); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewerWidget.cpp b/Viewer/ViewerWidget.cpp index 815f6d02..cce1b614 100644 --- a/Viewer/ViewerWidget.cpp +++ b/Viewer/ViewerWidget.cpp @@ -1,1452 +1,1453 @@ /* Copyright (C) 2003-2020 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ViewerWidget.h" #include "CategoryImageConfig.h" #include "ImageDisplay.h" #include "InfoBox.h" #include "SpeedDisplay.h" #include "TaggedArea.h" #include "TextDisplay.h" #include "VideoDisplay.h" #include "VideoShooter.h" #include "VisibleOptionsMenu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Viewer::ViewerWidget *Viewer::ViewerWidget::s_latest = nullptr; Viewer::ViewerWidget *Viewer::ViewerWidget::latest() { return s_latest; } // Notice the parent is zero to allow other windows to come on top of it. Viewer::ViewerWidget::ViewerWidget(UsageType type, QMap> *macroStore) : QStackedWidget(nullptr) , m_current(0) , m_popup(nullptr) , m_showingFullScreen(false) , m_forward(true) , m_isRunningSlideShow(false) , m_videoPlayerStoppedManually(false) , m_type(type) , m_currentCategory(DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name()) , m_inputMacros(macroStore) , m_myInputMacros(nullptr) { if (type == ViewerWindow) { setWindowFlags(Qt::Window); setAttribute(Qt::WA_DeleteOnClose); s_latest = this; } if (!m_inputMacros) { m_myInputMacros = m_inputMacros = new QMap>; } m_screenSaverCookie = -1; m_currentInputMode = InACategory; m_display = m_imageDisplay = new ImageDisplay(this); addWidget(m_imageDisplay); m_textDisplay = new TextDisplay(this); addWidget(m_textDisplay); createVideoViewer(); connect(m_imageDisplay, &ImageDisplay::possibleChange, this, &ViewerWidget::updateCategoryConfig); connect(m_imageDisplay, &ImageDisplay::imageReady, this, &ViewerWidget::updateInfoBox); connect(m_imageDisplay, &ImageDisplay::setCaptionInfo, this, &ViewerWidget::setCaptionWithDetail); connect(m_imageDisplay, &ImageDisplay::viewGeometryChanged, this, &ViewerWidget::remapAreas); // This must not be added to the layout, as it is standing on top of // the ImageDisplay m_infoBox = new InfoBox(this); m_infoBox->hide(); setupContextMenu(); m_slideShowTimer = new QTimer(this); m_slideShowTimer->setSingleShot(true); m_slideShowPause = Settings::SettingsData::instance()->slideShowInterval() * 1000; connect(m_slideShowTimer, &QTimer::timeout, this, &ViewerWidget::slotSlideShowNextFromTimer); m_speedDisplay = new SpeedDisplay(this); m_speedDisplay->hide(); setFocusPolicy(Qt::StrongFocus); QTimer::singleShot(2000, this, SLOT(test())); connect(DB::ImageDB::instance(), &DB::ImageDB::imagesDeleted, this, &ViewerWidget::slotRemoveDeletedImages); } void Viewer::ViewerWidget::setupContextMenu() { m_popup = new QMenu(this); m_actions = new KActionCollection(this); createSlideShowMenu(); createZoomMenu(); createRotateMenu(); createSkipMenu(); createShowContextMenu(); createInvokeExternalMenu(); createVideoMenu(); createCategoryImageMenu(); createFilterMenu(); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-edit-image-properties"), this, SLOT(editImage())); action->setText(i18nc("@action:inmenu", "Annotate...")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_1); m_popup->addAction(action); m_setStackHead = m_actions->addAction(QString::fromLatin1("viewer-set-stack-head"), this, SLOT(slotSetStackHead())); m_setStackHead->setText(i18nc("@action:inmenu", "Set as First Image in Stack")); m_actions->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4); m_popup->addAction(m_setStackHead); m_showExifViewer = m_actions->addAction(QString::fromLatin1("viewer-show-exif-viewer"), this, SLOT(showExifViewer())); m_showExifViewer->setText(i18nc("@action:inmenu", "Show Exif Viewer")); m_popup->addAction(m_showExifViewer); m_copyTo = m_actions->addAction(QString::fromLatin1("viewer-copy-to"), this, SLOT(copyTo())); m_copyTo->setText(i18nc("@action:inmenu", "Copy Image to...")); m_actions->setDefaultShortcut(m_copyTo, Qt::Key_F7); m_popup->addAction(m_copyTo); if (m_type == ViewerWindow) { action = m_actions->addAction(QString::fromLatin1("viewer-close"), this, SLOT(close())); action->setText(i18nc("@action:inmenu", "Close")); action->setShortcut(Qt::Key_Escape); m_actions->setShortcutsConfigurable(action, false); } m_popup->addAction(action); m_actions->readSettings(); const auto actions = m_actions->actions(); for (QAction *action : actions) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } } void Viewer::ViewerWidget::createShowContextMenu() { VisibleOptionsMenu *menu = new VisibleOptionsMenu(this, m_actions); connect(menu, &VisibleOptionsMenu::visibleOptionsChanged, this, &ViewerWidget::updateInfoBox); m_popup->addMenu(menu); } void Viewer::ViewerWidget::inhibitScreenSaver(bool inhibit) { QDBusMessage message; if (inhibit) { message = QDBusMessage::createMethodCall(QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("Inhibit")); message << QString(QString::fromLatin1("KPhotoAlbum")); message << QString(QString::fromLatin1("Giving a slideshow")); QDBusMessage reply = QDBusConnection::sessionBus().call(message); if (reply.type() == QDBusMessage::ReplyMessage) m_screenSaverCookie = reply.arguments().first().toInt(); } else { if (m_screenSaverCookie != -1) { message = QDBusMessage::createMethodCall(QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("UnInhibit")); message << (uint)m_screenSaverCookie; QDBusConnection::sessionBus().send(message); m_screenSaverCookie = -1; } } } void Viewer::ViewerWidget::createInvokeExternalMenu() { m_externalPopup = new MainWindow::ExternalPopup(m_popup); m_popup->addMenu(m_externalPopup); connect(m_externalPopup, &MainWindow::ExternalPopup::aboutToShow, this, &ViewerWidget::populateExternalPopup); } void Viewer::ViewerWidget::createRotateMenu() { m_rotateMenu = new QMenu(m_popup); m_rotateMenu->setTitle(i18nc("@title:inmenu", "Rotate")); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-rotate90"), this, SLOT(rotate90())); action->setText(i18nc("@action:inmenu", "Rotate clockwise")); action->setShortcut(Qt::Key_9); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-rotate180"), this, SLOT(rotate180())); action->setText(i18nc("@action:inmenu", "Flip Over")); action->setShortcut(Qt::Key_8); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-rotare270"), this, SLOT(rotate270())); // ^ this is a typo, isn't it?! action->setText(i18nc("@action:inmenu", "Rotate counterclockwise")); action->setShortcut(Qt::Key_7); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); m_popup->addMenu(m_rotateMenu); } void Viewer::ViewerWidget::createSkipMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@title:inmenu As in 'skip 2 images'", "Skip")); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-home"), this, SLOT(showFirst())); action->setText(i18nc("@action:inmenu Go to first image", "First")); action->setShortcut(Qt::Key_Home); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-end"), this, SLOT(showLast())); action->setText(i18nc("@action:inmenu Go to last image", "Last")); action->setShortcut(Qt::Key_End); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next"), this, SLOT(showNext())); action->setText(i18nc("@action:inmenu", "Show Next")); action->setShortcuts(QList() << Qt::Key_PageDown << Qt::Key_Space); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-10"), this, SLOT(showNext10())); action->setText(i18nc("@action:inmenu", "Skip 10 Forward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-100"), this, SLOT(showNext100())); action->setText(i18nc("@action:inmenu", "Skip 100 Forward")); m_actions->setDefaultShortcut(action, Qt::SHIFT + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-1000"), this, SLOT(showNext1000())); action->setText(i18nc("@action:inmenu", "Skip 1000 Forward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev"), this, SLOT(showPrev())); action->setText(i18nc("@action:inmenu", "Show Previous")); action->setShortcuts(QList() << Qt::Key_PageUp << Qt::Key_Backspace); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-10"), this, SLOT(showPrev10())); action->setText(i18nc("@action:inmenu", "Skip 10 Backward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-100"), this, SLOT(showPrev100())); action->setText(i18nc("@action:inmenu", "Skip 100 Backward")); m_actions->setDefaultShortcut(action, Qt::SHIFT + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-1000"), this, SLOT(showPrev1000())); action->setText(i18nc("@action:inmenu", "Skip 1000 Backward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-delete-current"), this, SLOT(deleteCurrent())); action->setText(i18nc("@action:inmenu", "Delete Image")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-remove-current"), this, SLOT(removeCurrent())); action->setText(i18nc("@action:inmenu", "Remove Image from Display List")); action->setShortcut(Qt::Key_Delete); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_popup->addMenu(popup); } void Viewer::ViewerWidget::createZoomMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@action:inmenu", "Zoom")); // PENDING(blackie) Only for image display? QAction *action = m_actions->addAction(QString::fromLatin1("viewer-zoom-in"), this, SLOT(zoomIn())); action->setText(i18nc("@action:inmenu", "Zoom In")); action->setShortcut(Qt::Key_Plus); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-out"), this, SLOT(zoomOut())); action->setText(i18nc("@action:inmenu", "Zoom Out")); action->setShortcut(Qt::Key_Minus); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-full"), this, SLOT(zoomFull())); action->setText(i18nc("@action:inmenu", "Full View")); action->setShortcut(Qt::Key_Period); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-pixel"), this, SLOT(zoomPixelForPixel())); action->setText(i18nc("@action:inmenu", "Pixel for Pixel View")); action->setShortcut(Qt::Key_Equal); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-toggle-fullscreen"), this, SLOT(toggleFullScreen())); action->setText(i18nc("@action:inmenu", "Toggle Full Screen")); action->setShortcuts(QList() << Qt::Key_F11 << Qt::Key_Return); popup->addAction(action); m_popup->addMenu(popup); } void Viewer::ViewerWidget::createSlideShowMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@title:inmenu", "Slideshow")); m_startStopSlideShow = m_actions->addAction(QString::fromLatin1("viewer-start-stop-slideshow"), this, SLOT(slotStartStopSlideShow())); m_startStopSlideShow->setText(i18nc("@action:inmenu", "Run Slideshow")); m_actions->setDefaultShortcut(m_startStopSlideShow, Qt::CTRL + Qt::Key_R); popup->addAction(m_startStopSlideShow); m_slideShowRunFaster = m_actions->addAction(QString::fromLatin1("viewer-run-faster"), this, SLOT(slotSlideShowFaster())); m_slideShowRunFaster->setText(i18nc("@action:inmenu", "Run Faster")); m_actions->setDefaultShortcut(m_slideShowRunFaster, Qt::CTRL + Qt::Key_Plus); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction(m_slideShowRunFaster); m_slideShowRunSlower = m_actions->addAction(QString::fromLatin1("viewer-run-slower"), this, SLOT(slotSlideShowSlower())); m_slideShowRunSlower->setText(i18nc("@action:inmenu", "Run Slower")); m_actions->setDefaultShortcut(m_slideShowRunSlower, Qt::CTRL + Qt::Key_Minus); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction(m_slideShowRunSlower); m_popup->addMenu(popup); } void Viewer::ViewerWidget::load(const DB::FileNameList &list, int index) { m_list = list; m_imageDisplay->setImageList(list); m_current = index; load(); bool on = (list.count() > 1); m_startStopSlideShow->setEnabled(on); m_slideShowRunFaster->setEnabled(on); m_slideShowRunSlower->setEnabled(on); } void Viewer::ViewerWidget::load() { const bool isReadable = QFileInfo(m_list[m_current].absolute()).isReadable(); const bool isVideo = isReadable && Utilities::isVideo(m_list[m_current]); if (isReadable) { if (isVideo) { m_display = m_videoDisplay; } else m_display = m_imageDisplay; } else { m_display = m_textDisplay; m_textDisplay->setText(i18n("File not available")); updateInfoBox(); } setCurrentWidget(m_display); m_infoBox->raise(); m_rotateMenu->setEnabled(!isVideo); m_categoryImagePopup->setEnabled(!isVideo); m_filterMenu->setEnabled(!isVideo); m_showExifViewer->setEnabled(!isVideo); if (m_exifViewer) m_exifViewer->setImage(m_list[m_current]); for (QAction *videoAction : qAsConst(m_videoActions)) { videoAction->setVisible(isVideo); } emit soughtTo(m_list[m_current]); bool ok = m_display->setImage(currentInfo(), m_forward); if (!ok) { close(); return; } setCaptionWithDetail(QString()); // PENDING(blackie) This needs to be improved, so that it shows the actions only if there are that many images to jump. for (QList::const_iterator it = m_forwardActions.constBegin(); it != m_forwardActions.constEnd(); ++it) (*it)->setEnabled(m_current + 1 < (int)m_list.count()); for (QList::const_iterator it = m_backwardActions.constBegin(); it != m_backwardActions.constEnd(); ++it) (*it)->setEnabled(m_current > 0); m_setStackHead->setEnabled(currentInfo()->isStacked()); if (isVideo) updateCategoryConfig(); if (m_isRunningSlideShow) m_slideShowTimer->start(m_slideShowPause); if (m_display == m_textDisplay) updateInfoBox(); // Add all tagged areas setTaggedAreasFromImage(); } void Viewer::ViewerWidget::setCaptionWithDetail(const QString &detail) { setWindowTitle(i18nc("@title:window %1 is the filename, %2 its detail info", "%1 %2", m_list[m_current].absolute(), detail)); } void Viewer::ViewerWidget::slotRemoveDeletedImages(const DB::FileNameList &imageList) { for (auto filename : imageList) { m_list.removeAll(filename); } } void Viewer::ViewerWidget::contextMenuEvent(QContextMenuEvent *e) { if (m_videoDisplay) { if (m_videoDisplay->isPaused()) m_playPause->setText(i18nc("@action:inmenu Start video playback", "Play")); else m_playPause->setText(i18nc("@action:inmenu Pause video playback", "Pause")); m_stop->setEnabled(m_videoDisplay->isPlaying()); } m_popup->exec(e->globalPos()); e->setAccepted(true); } void Viewer::ViewerWidget::showNextN(int n) { filterNone(); if (m_display == m_videoDisplay) { m_videoPlayerStoppedManually = true; m_videoDisplay->stop(); } if (m_current + n < (int)m_list.count()) { m_current += n; if (m_current >= (int)m_list.count()) m_current = (int)m_list.count() - 1; m_forward = true; load(); } } void Viewer::ViewerWidget::showNext() { showNextN(1); } void Viewer::ViewerWidget::removeCurrent() { removeOrDeleteCurrent(OnlyRemoveFromViewer); } void Viewer::ViewerWidget::deleteCurrent() { removeOrDeleteCurrent(RemoveImageFromDatabase); } void Viewer::ViewerWidget::removeOrDeleteCurrent(RemoveAction action) { const DB::FileName fileName = m_list[m_current]; if (action == RemoveImageFromDatabase) m_removed.append(fileName); m_list.removeAll(fileName); if (m_list.isEmpty()) close(); if (m_current == m_list.count()) showPrev(); else showNextN(0); } void Viewer::ViewerWidget::showNext10() { showNextN(10); } void Viewer::ViewerWidget::showNext100() { showNextN(100); } void Viewer::ViewerWidget::showNext1000() { showNextN(1000); } void Viewer::ViewerWidget::showPrevN(int n) { if (m_display == m_videoDisplay) m_videoDisplay->stop(); if (m_current > 0) { m_current -= n; if (m_current < 0) m_current = 0; m_forward = false; load(); } } void Viewer::ViewerWidget::showPrev() { showPrevN(1); } void Viewer::ViewerWidget::showPrev10() { showPrevN(10); } void Viewer::ViewerWidget::showPrev100() { showPrevN(100); } void Viewer::ViewerWidget::showPrev1000() { showPrevN(1000); } void Viewer::ViewerWidget::rotate90() { currentInfo()->rotate(90); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::rotate180() { currentInfo()->rotate(180); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::rotate270() { currentInfo()->rotate(270); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::showFirst() { showPrevN(m_list.count()); } void Viewer::ViewerWidget::showLast() { showNextN(m_list.count()); } void Viewer::ViewerWidget::closeEvent(QCloseEvent *event) { if (!m_removed.isEmpty()) { MainWindow::DeleteDialog dialog(this); dialog.exec(m_removed); } m_slideShowTimer->stop(); m_isRunningSlideShow = false; event->accept(); } DB::ImageInfoPtr Viewer::ViewerWidget::currentInfo() const { return m_list[m_current].info(); } void Viewer::ViewerWidget::infoBoxMove() { QPoint p = mapFromGlobal(QCursor::pos()); Settings::Position oldPos = Settings::SettingsData::instance()->infoBoxPosition(); Settings::Position pos = oldPos; int x = m_display->mapFromParent(p).x(); int y = m_display->mapFromParent(p).y(); int w = m_display->width(); int h = m_display->height(); if (x < w / 3) { if (y < h / 3) pos = Settings::TopLeft; else if (y > h * 2 / 3) pos = Settings::BottomLeft; else pos = Settings::Left; } else if (x > w * 2 / 3) { if (y < h / 3) pos = Settings::TopRight; else if (y > h * 2 / 3) pos = Settings::BottomRight; else pos = Settings::Right; } else { if (y < h / 3) pos = Settings::Top; else if (y > h * 2 / 3) pos = Settings::Bottom; } if (pos != oldPos) { Settings::SettingsData::instance()->setInfoBoxPosition(pos); updateInfoBox(); } } void Viewer::ViewerWidget::moveInfoBox() { m_infoBox->setSize(); Settings::Position pos = Settings::SettingsData::instance()->infoBoxPosition(); int lx = m_display->pos().x(); int ly = m_display->pos().y(); int lw = m_display->width(); int lh = m_display->height(); int bw = m_infoBox->width(); int bh = m_infoBox->height(); int bx, by; // x-coordinate if (pos == Settings::TopRight || pos == Settings::BottomRight || pos == Settings::Right) bx = lx + lw - 5 - bw; else if (pos == Settings::TopLeft || pos == Settings::BottomLeft || pos == Settings::Left) bx = lx + 5; else bx = lx + lw / 2 - bw / 2; // Y-coordinate if (pos == Settings::TopLeft || pos == Settings::TopRight || pos == Settings::Top) by = ly + 5; else if (pos == Settings::BottomLeft || pos == Settings::BottomRight || pos == Settings::Bottom) by = ly + lh - 5 - bh; else by = ly + lh / 2 - bh / 2; m_infoBox->move(bx, by); } void Viewer::ViewerWidget::resizeEvent(QResizeEvent *e) { moveInfoBox(); QWidget::resizeEvent(e); } void Viewer::ViewerWidget::updateInfoBox() { QString tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name(); if (currentInfo() || !m_currentInput.isEmpty() || (!m_currentCategory.isEmpty() && m_currentCategory != tokensCategory)) { QMap> map; QString text = Utilities::createInfoText(currentInfo(), &map); QString selecttext = QString::fromLatin1(""); if (m_currentCategory.isEmpty()) { selecttext = i18nc("Basically 'enter a category name'", "Setting Category: ") + m_currentInput; if (m_currentInputList.length() > 0) { selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}"); } } else if ((!m_currentInput.isEmpty() && m_currentCategory != tokensCategory)) { selecttext = i18nc("Basically 'enter a tag name'", "Assigning: ") + m_currentCategory + QString::fromLatin1("/") + m_currentInput; if (m_currentInputList.length() > 0) { selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}"); } } else if (!m_currentInput.isEmpty() && m_currentCategory == tokensCategory) { m_currentInput = QString::fromLatin1(""); } if (!selecttext.isEmpty()) text = selecttext + QString::fromLatin1("
") + text; if (Settings::SettingsData::instance()->showInfoBox() && !text.isNull() && (m_type != InlineViewer)) { m_infoBox->setInfo(text, map); m_infoBox->show(); } else m_infoBox->hide(); moveInfoBox(); } } Viewer::ViewerWidget::~ViewerWidget() { inhibitScreenSaver(false); if (s_latest == this) s_latest = nullptr; if (m_myInputMacros) delete m_myInputMacros; } void Viewer::ViewerWidget::toggleFullScreen() { setShowFullScreen(!m_showingFullScreen); } void Viewer::ViewerWidget::slotStartStopSlideShow() { bool wasRunningSlideShow = m_isRunningSlideShow; m_isRunningSlideShow = !m_isRunningSlideShow && m_list.count() != 1; if (wasRunningSlideShow) { m_startStopSlideShow->setText(i18nc("@action:inmenu", "Run Slideshow")); m_slideShowTimer->stop(); if (m_list.count() != 1) m_speedDisplay->end(); inhibitScreenSaver(false); } else { m_startStopSlideShow->setText(i18nc("@action:inmenu", "Stop Slideshow")); if (currentInfo()->mediaType() != DB::Video) m_slideShowTimer->start(m_slideShowPause); m_speedDisplay->start(); inhibitScreenSaver(true); } } void Viewer::ViewerWidget::slotSlideShowNextFromTimer() { // Load the next images. QTime timer; timer.start(); if (m_display == m_imageDisplay) slotSlideShowNext(); // ensure that there is a few milliseconds pause, so that an end slideshow keypress // can get through immediately, we don't want it to queue up behind a bunch of timer events, // which loaded a number of new images before the slideshow stops int ms = qMax(200, m_slideShowPause - timer.elapsed()); m_slideShowTimer->start(ms); } void Viewer::ViewerWidget::slotSlideShowNext() { m_forward = true; if (m_current + 1 < (int)m_list.count()) m_current++; else m_current = 0; load(); } void Viewer::ViewerWidget::slotSlideShowFaster() { changeSlideShowInterval(-500); } void Viewer::ViewerWidget::slotSlideShowSlower() { changeSlideShowInterval(+500); } void Viewer::ViewerWidget::changeSlideShowInterval(int delta) { if (m_list.count() == 1) return; m_slideShowPause += delta; m_slideShowPause = qMax(m_slideShowPause, 500); m_speedDisplay->display(m_slideShowPause); if (m_slideShowTimer->isActive()) m_slideShowTimer->start(m_slideShowPause); } void Viewer::ViewerWidget::editImage() { DB::ImageInfoList list; list.append(currentInfo()); MainWindow::Window::configureImages(list, true); } void Viewer::ViewerWidget::filterNone() { if (m_display == m_imageDisplay) { m_imageDisplay->filterNone(); m_filterMono->setChecked(false); m_filterBW->setChecked(false); m_filterContrastStretch->setChecked(false); m_filterHistogramEqualization->setChecked(false); } } void Viewer::ViewerWidget::filterSelected() { // The filters that drop bit depth below 32 should be the last ones // so that filters requiring more bit depth are processed first if (m_display == m_imageDisplay) { m_imageDisplay->filterNone(); if (m_filterBW->isChecked()) m_imageDisplay->filterBW(); if (m_filterContrastStretch->isChecked()) m_imageDisplay->filterContrastStretch(); if (m_filterHistogramEqualization->isChecked()) m_imageDisplay->filterHistogramEqualization(); if (m_filterMono->isChecked()) m_imageDisplay->filterMono(); } } void Viewer::ViewerWidget::filterBW() { if (m_display == m_imageDisplay) { if (m_filterBW->isChecked()) m_filterBW->setChecked(m_imageDisplay->filterBW()); else filterSelected(); } } void Viewer::ViewerWidget::filterContrastStretch() { if (m_display == m_imageDisplay) { if (m_filterContrastStretch->isChecked()) m_filterContrastStretch->setChecked(m_imageDisplay->filterContrastStretch()); else filterSelected(); } } void Viewer::ViewerWidget::filterHistogramEqualization() { if (m_display == m_imageDisplay) { if (m_filterHistogramEqualization->isChecked()) m_filterHistogramEqualization->setChecked(m_imageDisplay->filterHistogramEqualization()); else filterSelected(); } } void Viewer::ViewerWidget::filterMono() { if (m_display == m_imageDisplay) { if (m_filterMono->isChecked()) m_filterMono->setChecked(m_imageDisplay->filterMono()); else filterSelected(); } } void Viewer::ViewerWidget::slotSetStackHead() { MainWindow::Window::theMainWindow()->setStackHead(m_list[m_current]); } bool Viewer::ViewerWidget::showingFullScreen() const { return m_showingFullScreen; } void Viewer::ViewerWidget::setShowFullScreen(bool on) { if (on) { setWindowState(windowState() | Qt::WindowFullScreen); // set moveInfoBox(); } else { // We need to size the image when going out of full screen, in case we started directly in full screen // setWindowState(windowState() & ~Qt::WindowFullScreen); // reset resize(Settings::SettingsData::instance()->viewerSize()); } m_showingFullScreen = on; } void Viewer::ViewerWidget::updateCategoryConfig() { if (!CategoryImageConfig::instance()->isVisible()) return; CategoryImageConfig::instance()->setCurrentImage(m_imageDisplay->currentViewAsThumbnail(), currentInfo()); } void Viewer::ViewerWidget::populateExternalPopup() { m_externalPopup->populate(currentInfo(), m_list); } void Viewer::ViewerWidget::populateCategoryImagePopup() { m_categoryImagePopup->populate(m_imageDisplay->currentViewAsThumbnail(), m_list[m_current]); } void Viewer::ViewerWidget::show(bool slideShow) { QSize size; bool fullScreen; if (slideShow) { fullScreen = Settings::SettingsData::instance()->launchSlideShowFullScreen(); size = Settings::SettingsData::instance()->slideShowSize(); } else { fullScreen = Settings::SettingsData::instance()->launchViewerFullScreen(); size = Settings::SettingsData::instance()->viewerSize(); } if (fullScreen) setShowFullScreen(true); else resize(size); QWidget::show(); if (slideShow != m_isRunningSlideShow) { // The info dialog will show up at the wrong place if we call this function directly // don't ask me why - 4 Sep. 2004 15:13 -- Jesper K. Pedersen QTimer::singleShot(0, this, SLOT(slotStartStopSlideShow())); } } KActionCollection *Viewer::ViewerWidget::actions() { return m_actions; } int Viewer::ViewerWidget::find_tag_in_list(const QStringList &list, QString &namefound) { int found = 0; m_currentInputList = QString::fromLatin1(""); for (QStringList::ConstIterator listIter = list.constBegin(); listIter != list.constEnd(); ++listIter) { if (listIter->startsWith(m_currentInput, Qt::CaseInsensitive)) { found++; if (m_currentInputList.length() > 0) m_currentInputList = m_currentInputList + QString::fromLatin1(","); m_currentInputList = m_currentInputList + listIter->right(listIter->length() - m_currentInput.length()); if (found > 1 && m_currentInputList.length() > 20) { // already found more than we want to display // bail here for now // XXX: non-ideal? display more? certainly config 20 return found; } else { namefound = *listIter; } } } return found; } void Viewer::ViewerWidget::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Backspace) { // remove stuff from the current input string m_currentInput.remove(m_currentInput.length() - 1, 1); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); m_currentInputList = QString::fromLatin1(""); // } else if (event->modifier & (Qt::AltModifier | Qt::MetaModifier) && // event->key() == Qt::Key_Enter) { return; // we've handled it } else if (event->key() == Qt::Key_Comma) { // force set the "new" token if (!m_currentCategory.isEmpty()) { if (m_currentInput.left(1) == QString::fromLatin1("\"") || // allow a starting ' or " to signal a brand new category // this bypasses the auto-selection of matching characters m_currentInput.left(1) == QString::fromLatin1("\'")) { m_currentInput = m_currentInput.right(m_currentInput.length() - 1); } if (m_currentInput.isEmpty()) return; currentInfo()->addCategoryInfo(m_currentCategory, m_currentInput); DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_currentCategory); category->addItem(m_currentInput); } m_currentInput = QString::fromLatin1(""); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); return; // we've handled it } else if (event->modifiers() == 0 && event->key() >= Qt::Key_0 && event->key() <= Qt::Key_5) { bool ok; short rating = event->text().left(1).toShort(&ok, 10); if (ok) { currentInfo()->setRating(rating * 2); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); } } else if (event->modifiers() == 0 || event->modifiers() == Qt::ShiftModifier) { // search the category for matches QString namefound; QString incomingKey = event->text().left(1); // start searching for a new category name if (incomingKey == QString::fromLatin1("/")) { if (m_currentInput.isEmpty() && m_currentCategory.isEmpty()) { if (m_currentInputMode == InACategory) { m_currentInputMode = AlwaysStartWithCategory; } else { m_currentInputMode = InACategory; } } else { // reset the category to search through m_currentInput = QString::fromLatin1(""); m_currentCategory = QString::fromLatin1(""); } // use an assigned key or map to a given key for future reference } else if (m_currentInput.isEmpty() && // can map to function keys event->key() >= Qt::Key_F1 && event->key() <= Qt::Key_F35) { // we have a request to assign a macro key or use one Qt::Key key = (Qt::Key)event->key(); if (m_inputMacros->contains(key)) { // Use the requested toggle if (event->modifiers() == Qt::ShiftModifier) { if (currentInfo()->hasCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second)) { currentInfo()->removeCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second); } } else { currentInfo()->addCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second); } } else { (*m_inputMacros)[key] = qMakePair(m_lastCategory, m_lastFound); } updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); // handled it return; } else if (m_currentCategory.isEmpty()) { // still searching for a category to lock to m_currentInput += incomingKey; QStringList categorynames = DB::ImageDB::instance()->categoryCollection()->categoryTexts(); if (find_tag_in_list(categorynames, namefound) == 1) { // yay, we have exactly one! m_currentCategory = namefound; m_currentInput = QString::fromLatin1(""); m_currentInputList = QString::fromLatin1(""); } } else { m_currentInput += incomingKey; DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_currentCategory); QStringList items = category->items(); if (find_tag_in_list(items, namefound) == 1) { // yay, we have exactly one! if (currentInfo()->hasCategoryInfo(category->name(), namefound)) currentInfo()->removeCategoryInfo(category->name(), namefound); else currentInfo()->addCategoryInfo(category->name(), namefound); m_lastFound = namefound; m_lastCategory = m_currentCategory; m_currentInput = QString::fromLatin1(""); m_currentInputList = QString::fromLatin1(""); if (m_currentInputMode == AlwaysStartWithCategory) m_currentCategory = QString::fromLatin1(""); } } updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); } QWidget::keyPressEvent(event); return; } void Viewer::ViewerWidget::videoStopped() { if (!m_videoPlayerStoppedManually && m_isRunningSlideShow) slotSlideShowNext(); m_videoPlayerStoppedManually = false; } void Viewer::ViewerWidget::wheelEvent(QWheelEvent *event) { if (event->delta() < 0) { showNext(); } else { showPrev(); } } void Viewer::ViewerWidget::showExifViewer() { m_exifViewer = new Exif::InfoDialog(m_list[m_current], this); m_exifViewer->show(); } void Viewer::ViewerWidget::zoomIn() { m_display->zoomIn(); } void Viewer::ViewerWidget::zoomOut() { m_display->zoomOut(); } void Viewer::ViewerWidget::zoomFull() { m_display->zoomFull(); } void Viewer::ViewerWidget::zoomPixelForPixel() { m_display->zoomPixelForPixel(); } void Viewer::ViewerWidget::makeThumbnailImage() { VideoShooter::go(currentInfo(), this); } struct SeekInfo { SeekInfo(const QString &title, const char *name, int value, const QKeySequence &key) : title(title) , name(name) , value(value) , key(key) { } QString title; const char *name; int value; QKeySequence key; }; void Viewer::ViewerWidget::createVideoMenu() { QMenu *menu = new QMenu(m_popup); menu->setTitle(i18nc("@title:inmenu", "Seek")); m_videoActions.append(m_popup->addMenu(menu)); const QList list = { SeekInfo(i18nc("@action:inmenu", "10 minutes backward"), "seek-10-minute", -600000, QKeySequence(QString::fromLatin1("Ctrl+Left"))), SeekInfo(i18nc("@action:inmenu", "1 minute backward"), "seek-1-minute", -60000, QKeySequence(QString::fromLatin1("Shift+Left"))), SeekInfo(i18nc("@action:inmenu", "10 seconds backward"), "seek-10-second", -10000, QKeySequence(QString::fromLatin1("Left"))), SeekInfo(i18nc("@action:inmenu", "1 seconds backward"), "seek-1-second", -1000, QKeySequence(QString::fromLatin1("Up"))), SeekInfo(i18nc("@action:inmenu", "100 milliseconds backward"), "seek-100-millisecond", -100, QKeySequence(QString::fromLatin1("Shift+Up"))), SeekInfo(i18nc("@action:inmenu", "100 milliseconds forward"), "seek+100-millisecond", 100, QKeySequence(QString::fromLatin1("Shift+Down"))), SeekInfo(i18nc("@action:inmenu", "1 seconds forward"), "seek+1-second", 1000, QKeySequence(QString::fromLatin1("Down"))), SeekInfo(i18nc("@action:inmenu", "10 seconds forward"), "seek+10-second", 10000, QKeySequence(QString::fromLatin1("Right"))), SeekInfo(i18nc("@action:inmenu", "1 minute forward"), "seek+1-minute", 60000, QKeySequence(QString::fromLatin1("Shift+Right"))), SeekInfo(i18nc("@action:inmenu", "10 minutes forward"), "seek+10-minute", 600000, QKeySequence(QString::fromLatin1("Ctrl+Right"))) }; int count = 0; for (const SeekInfo &info : list) { if (count++ == 5) { QAction *sep = new QAction(menu); sep->setSeparator(true); menu->addAction(sep); } QAction *seek = m_actions->addAction(QString::fromLatin1(info.name), m_videoDisplay, SLOT(seek())); seek->setText(info.title); seek->setData(info.value); seek->setShortcut(info.key); m_actions->setShortcutsConfigurable(seek, false); menu->addAction(seek); } QAction *sep = new QAction(m_popup); sep->setSeparator(true); m_popup->addAction(sep); m_videoActions.append(sep); m_stop = m_actions->addAction(QString::fromLatin1("viewer-video-stop"), m_videoDisplay, SLOT(stop())); m_stop->setText(i18nc("@action:inmenu Stop video playback", "Stop")); m_popup->addAction(m_stop); m_videoActions.append(m_stop); m_playPause = m_actions->addAction(QString::fromLatin1("viewer-video-pause"), m_videoDisplay, SLOT(playPause())); // text set in contextMenuEvent() m_playPause->setShortcut(Qt::Key_P); m_actions->setShortcutsConfigurable(m_playPause, false); m_popup->addAction(m_playPause); m_videoActions.append(m_playPause); m_makeThumbnailImage = m_actions->addAction(QString::fromLatin1("make-thumbnail-image"), this, SLOT(makeThumbnailImage())); m_actions->setDefaultShortcut(m_makeThumbnailImage, Qt::ControlModifier + Qt::Key_S); m_makeThumbnailImage->setText(i18nc("@action:inmenu", "Use current frame in thumbnail view")); m_popup->addAction(m_makeThumbnailImage); m_videoActions.append(m_makeThumbnailImage); QAction *restart = m_actions->addAction(QString::fromLatin1("viewer-video-restart"), m_videoDisplay, SLOT(restart())); restart->setText(i18nc("@action:inmenu Restart video playback.", "Restart")); m_popup->addAction(restart); m_videoActions.append(restart); } void Viewer::ViewerWidget::createCategoryImageMenu() { m_categoryImagePopup = new MainWindow::CategoryImagePopup(m_popup); m_popup->addMenu(m_categoryImagePopup); connect(m_categoryImagePopup, &MainWindow::CategoryImagePopup::aboutToShow, this, &ViewerWidget::populateCategoryImagePopup); } void Viewer::ViewerWidget::createFilterMenu() { m_filterMenu = new QMenu(m_popup); m_filterMenu->setTitle(i18nc("@title:inmenu", "Filters")); m_filterNone = m_actions->addAction(QString::fromLatin1("filter-empty"), this, SLOT(filterNone())); m_filterNone->setText(i18nc("@action:inmenu", "Remove All Filters")); m_filterMenu->addAction(m_filterNone); m_filterBW = m_actions->addAction(QString::fromLatin1("filter-bw"), this, SLOT(filterBW())); m_filterBW->setText(i18nc("@action:inmenu", "Apply Grayscale Filter")); m_filterBW->setCheckable(true); m_filterMenu->addAction(m_filterBW); m_filterContrastStretch = m_actions->addAction(QString::fromLatin1("filter-cs"), this, SLOT(filterContrastStretch())); m_filterContrastStretch->setText(i18nc("@action:inmenu", "Apply Contrast Stretching Filter")); m_filterContrastStretch->setCheckable(true); m_filterMenu->addAction(m_filterContrastStretch); m_filterHistogramEqualization = m_actions->addAction(QString::fromLatin1("filter-he"), this, SLOT(filterHistogramEqualization())); m_filterHistogramEqualization->setText(i18nc("@action:inmenu", "Apply Histogram Equalization Filter")); m_filterHistogramEqualization->setCheckable(true); m_filterMenu->addAction(m_filterHistogramEqualization); m_filterMono = m_actions->addAction(QString::fromLatin1("filter-mono"), this, SLOT(filterMono())); m_filterMono->setText(i18nc("@action:inmenu", "Apply Monochrome Filter")); m_filterMono->setCheckable(true); m_filterMenu->addAction(m_filterMono); m_popup->addMenu(m_filterMenu); } void Viewer::ViewerWidget::test() { #ifdef TESTING QTimeLine *timeline = new QTimeLine; timeline->setStartFrame(_infoBox->y()); timeline->setEndFrame(height()); connect(timeline, &QTimeLine::frameChanged, this, &ViewerWidget::moveInfoBox); timeline->start(); #endif // TESTING } void Viewer::ViewerWidget::moveInfoBox(int y) { m_infoBox->move(m_infoBox->x(), y); } void Viewer::ViewerWidget::createVideoViewer() { m_videoDisplay = new VideoDisplay(this); addWidget(m_videoDisplay); connect(m_videoDisplay, &VideoDisplay::stopped, this, &ViewerWidget::videoStopped); } void Viewer::ViewerWidget::stopPlayback() { m_videoDisplay->stop(); } void Viewer::ViewerWidget::invalidateThumbnail() const { ImageManager::ThumbnailCache::instance()->removeThumbnail(m_list[m_current]); } void Viewer::ViewerWidget::setTaggedAreasFromImage() { // Clean all areas we probably already have - foreach (TaggedArea *area, findChildren()) { + const auto allAreas = findChildren(); + for (TaggedArea *area : allAreas) { area->deleteLater(); } DB::TaggedAreas taggedAreas = currentInfo()->taggedAreas(); addTaggedAreas(taggedAreas, AreaType::Standard); } void Viewer::ViewerWidget::addAdditionalTaggedAreas(DB::TaggedAreas taggedAreas) { addTaggedAreas(taggedAreas, AreaType::Highlighted); } void Viewer::ViewerWidget::addTaggedAreas(DB::TaggedAreas taggedAreas, AreaType type) { DB::TaggedAreasIterator areasInCategory(taggedAreas); QString category; QString tag; while (areasInCategory.hasNext()) { areasInCategory.next(); category = areasInCategory.key(); DB::PositionTagsIterator areaData(areasInCategory.value()); while (areaData.hasNext()) { areaData.next(); tag = areaData.key(); // Add a new frame for the area TaggedArea *newArea = new TaggedArea(this); newArea->setTagInfo(category, category, tag); newArea->setActualGeometry(areaData.value()); newArea->setHighlighted(type == AreaType::Highlighted); newArea->show(); connect(m_infoBox, &InfoBox::tagHovered, newArea, &TaggedArea::checkIsSelected); connect(m_infoBox, &InfoBox::noTagHovered, newArea, &TaggedArea::deselect); } } // Be sure to display the areas, as viewGeometryChanged is not always emitted on load QSize imageSize = currentInfo()->size(); QSize windowSize = this->size(); // On load, the image is never zoomed, so it's a bit easier ;-) double scaleWidth = double(imageSize.width()) / windowSize.width(); double scaleHeight = double(imageSize.height()) / windowSize.height(); int offsetTop = 0; int offsetLeft = 0; if (scaleWidth > scaleHeight) { offsetTop = (windowSize.height() - imageSize.height() / scaleWidth); } else { offsetLeft = (windowSize.width() - imageSize.width() / scaleHeight); } remapAreas( QSize(windowSize.width() - offsetLeft, windowSize.height() - offsetTop), QRect(QPoint(0, 0), QPoint(imageSize.width(), imageSize.height())), 1); } void Viewer::ViewerWidget::remapAreas(QSize viewSize, QRect zoomWindow, double sizeRatio) { QSize currentWindowSize = this->size(); int outerOffsetLeft = (currentWindowSize.width() - viewSize.width()) / 2; int outerOffsetTop = (currentWindowSize.height() - viewSize.height()) / 2; if (sizeRatio != 1) { zoomWindow = QRect( QPoint( double(zoomWindow.left()) * sizeRatio, double(zoomWindow.top()) * sizeRatio), QPoint( double(zoomWindow.left() + zoomWindow.width()) * sizeRatio, double(zoomWindow.top() + zoomWindow.height()) * sizeRatio)); } double scaleHeight = double(viewSize.height()) / zoomWindow.height(); double scaleWidth = double(viewSize.width()) / zoomWindow.width(); int innerOffsetLeft = -zoomWindow.left() * scaleWidth; int innerOffsetTop = -zoomWindow.top() * scaleHeight; const auto areas = findChildren(); for (TaggedArea *area : areas) { const QRect actualGeometry = area->actualGeometry(); QRect screenGeometry; screenGeometry.setWidth(actualGeometry.width() * scaleWidth); screenGeometry.setHeight(actualGeometry.height() * scaleHeight); screenGeometry.moveTo( actualGeometry.left() * scaleWidth + outerOffsetLeft + innerOffsetLeft, actualGeometry.top() * scaleHeight + outerOffsetTop + innerOffsetTop); area->setGeometry(screenGeometry); } } void Viewer::ViewerWidget::copyTo() { QUrl src = QUrl::fromLocalFile(m_list[m_current].absolute()); if (m_lastCopyToTarget.isNull()) { // get directory of src file m_lastCopyToTarget = QFileInfo(src.path()).path(); } QFileDialog dialog(this); dialog.setWindowTitle(i18nc("@title:window", "Copy Image to...")); // use directory of src as start-location: dialog.setDirectory(m_lastCopyToTarget); dialog.selectFile(src.fileName()); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setLabelText(QFileDialog::Accept, i18nc("@action:button", "Copy")); if (dialog.exec()) { QUrl dst = dialog.selectedUrls().first(); KIO::CopyJob *job = KIO::copy(src, dst); connect(job, &KIO::CopyJob::finished, job, &QObject::deleteLater); // get directory of dst file m_lastCopyToTarget = QFileInfo(dst.path()).path(); } } // vi:expandtab:tabstop=4 shiftwidth=4: