diff --git a/AnnotationDialog/Dialog.cpp b/AnnotationDialog/Dialog.cpp index bb88fc5c..26908c1d 100644 --- a/AnnotationDialog/Dialog.cpp +++ b/AnnotationDialog/Dialog.cpp @@ -1,1731 +1,1731 @@ /* Copyright (C) 2003-2019 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_KGEOMAP +#ifdef HAVE_MARBLE #include #include #include #include "Map/GeoCoordinates.h" #endif #include #include #include #include #include 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; // 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, SIGNAL(pageUpDownPressed(QKeyEvent *)), this, SLOT(descriptionPageUpDownPressed(QKeyEvent *))); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE // -------------------------------------------------- Map representation m_annotationMapContainer = new QWidget(this); QVBoxLayout *annotationMapContainerLayout = new QVBoxLayout(m_annotationMapContainer); m_annotationMap = new Map::MapView(this); annotationMapContainerLayout->addWidget(m_annotationMap); QHBoxLayout *mapLoadingProgressLayout = new QHBoxLayout(); annotationMapContainerLayout->addLayout(mapLoadingProgressLayout); m_mapLoadingProgress = new QProgressBar(this); mapLoadingProgressLayout->addWidget(m_mapLoadingProgress); m_mapLoadingProgress->hide(); m_cancelMapLoadingButton = new QPushButton(i18n("Cancel")); mapLoadingProgressLayout->addWidget(m_cancelMapLoadingButton); m_cancelMapLoadingButton->hide(); connect(m_cancelMapLoadingButton, SIGNAL(clicked()), this, SLOT(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, SIGNAL(visibilityChanged(bool)), this, SLOT(annotationMapVisibilityChanged(bool))); 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, SIGNAL(positionableTagSelected(QString, QString)), this, SLOT(positionableTagSelected(QString, QString))); connect(sel, SIGNAL(positionableTagDeselected(QString, QString)), this, SLOT(positionableTagDeselected(QString, QString))); connect(sel, SIGNAL(positionableTagRenamed(QString, QString, QString)), this, SLOT(positionableTagRenamed(QString, QString, QString))); 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, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(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, SIGNAL(clicked()), this, SLOT(slotRevert())); connect(m_okBut, SIGNAL(clicked()), this, SLOT(doneTagging())); connect(m_continueLaterBut, SIGNAL(clicked()), this, SLOT(continueLater())); connect(cancelBut, SIGNAL(clicked()), this, SLOT(reject())); connect(m_clearBut, SIGNAL(clicked()), this, SLOT(slotClear())); connect(optionsBut, SIGNAL(clicked()), this, SLOT(slotOptions())); connect(m_preview, SIGNAL(imageRotated(int)), this, SLOT(rotate(int))); connect(m_preview, SIGNAL(indexChanged(int)), this, SLOT(slotIndexChanged(int))); connect(m_preview, SIGNAL(imageDeleted(DB::ImageInfo)), this, SLOT(slotDeleteImage())); connect(m_preview, SIGNAL(copyPrevClicked()), this, SLOT(slotCopyPrevious())); connect(m_preview, SIGNAL(areaVisibilityChanged(bool)), this, SLOT(slotShowAreas(bool))); 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, SIGNAL(dateChanged(DB::ImageDate)), this, SLOT(slotStartDateChanged(DB::ImageDate))); 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, SIGNAL(stateChanged(int)), this, SLOT(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, SIGNAL(ratingChanged(uint)), this, SLOT(slotRatingChanged(uint))); 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(); connect(m_preview, &ImagePreviewWidget::togglePreview, this, &Dialog::togglePreview); return m_preview; } void AnnotationDialog::Dialog::slotRevert() { if (m_setup == InputSingleImageConfigMode) load(); } void AnnotationDialog::Dialog::slotIndexChanged(int index) { if (m_setup != InputSingleImageConfigMode) return; if (m_current >= 0) writeToInfo(); m_current = index; load(); } void AnnotationDialog::Dialog::doneTagging() { saveAndClose(); if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()) { for (DB::ImageInfoListIterator it = m_origList.begin(); it != m_origList.end(); ++it) { (*it)->removeCategoryInfo(Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag()); } } } /* * Copy tags (only tags/categories, not description/label/...) from previous image to the currently showed one */ void AnnotationDialog::Dialog::slotCopyPrevious() { if (m_setup != InputSingleImageConfigMode) return; if (m_current < 1) return; // FIXME: it would be better to compute the "previous image" in a better way, but let's stick with this for now... DB::ImageInfo &old_info = m_editList[m_current - 1]; m_positionableTagCandidates.clear(); m_lastSelectedPositionableTag.first = QString(); m_lastSelectedPositionableTag.second = QString(); Q_FOREACH (ListSelect *ls, m_optionList) { ls->setSelection(old_info.itemsOfCategory(ls->category())); // Also set all positionable tag candidates if (ls->positionable()) { QString category = ls->category(); QSet selectedTags = old_info.itemsOfCategory(category); QSet positionedTagSet = positionedTags(category); // Add the tag to the positionable candiate list, if no area is already associated with it Q_FOREACH (const auto &tag, selectedTags) { if (!positionedTagSet.contains(tag)) { addTagToCandidateList(category, tag); } } // Check all areas for a linked tag in this category that is probably not selected anymore for (ResizableFrame *area : areas()) { QPair tagData = area->tagData(); if (tagData.first == category) { if (!selectedTags.contains(tagData.second)) { // The linked tag is not selected anymore, so remove it area->removeTagData(); } } } } } } void AnnotationDialog::Dialog::load() { // Remove all areas tidyAreas(); // No areas have been changed m_areasChanged = false; // Empty the positionable tag candidate list and the last selected positionable tag m_positionableTagCandidates.clear(); m_lastSelectedPositionableTag = QPair(); DB::ImageInfo &info = m_editList[m_current]; m_startDate->setDate(info.date().start().date()); if (info.date().hasValidTime()) { m_time->show(); m_time->setTime(info.date().start().time()); m_isFuzzyDate->setChecked(false); } else { m_time->hide(); m_isFuzzyDate->setChecked(true); } if (info.date().start().date() == info.date().end().date()) m_endDate->setDate(QDate()); else m_endDate->setDate(info.date().end().date()); m_imageLabel->setText(info.label()); m_description->setPlainText(info.description()); if (m_setup == InputSingleImageConfigMode) m_rating->setRating(qMax(static_cast(0), info.rating())); m_ratingChanged = false; // A category areas have been linked against could have been deleted // or un-marked as positionable in the meantime, so ... QMap categoryIsPositionable; QList positionableCategories; Q_FOREACH (ListSelect *ls, m_optionList) { ls->setSelection(info.itemsOfCategory(ls->category())); ls->rePopulate(); // Get all selected positionable tags and add them to the candidate list if (ls->positionable()) { QSet selectedTags = ls->itemsOn(); Q_FOREACH (const QString &tagName, selectedTags) { addTagToCandidateList(ls->category(), tagName); } } // ... create a list of all categories and their positionability ... categoryIsPositionable[ls->category()] = ls->positionable(); if (ls->positionable()) { positionableCategories << ls->category(); } } // Create all tagged areas QMap> taggedAreas = info.taggedAreas(); QMapIterator> areasInCategory(taggedAreas); while (areasInCategory.hasNext()) { areasInCategory.next(); QString category = areasInCategory.key(); // ... and check if the respective category is actually there yet and still positionable // (operator[] will insert an empty item if the category has been deleted // and is thus missing in the QMap, but the respective key won't be true) if (categoryIsPositionable[category]) { QMapIterator areaData(areasInCategory.value()); while (areaData.hasNext()) { areaData.next(); QString tag = areaData.key(); // Be sure that the corresponding tag is still checked. The category could have // been un-marked as positionable in the meantime and the tag could have been // deselected, without triggering positionableTagDeselected and the area thus // still remaining. If the category is then re-marked as positionable, the area would // show up without the tag being selected. if (m_listSelectList[category]->tagIsChecked(tag)) { m_preview->preview()->createTaggedArea(category, tag, areaData.value(), m_preview->showAreas()); } } } } if (m_setup == InputSingleImageConfigMode) { setWindowTitle(i18nc("@title:window image %1 of %2 images", "Annotations (%1/%2)", m_current + 1, m_origList.count())); m_preview->canCreateAreas( m_setup == InputSingleImageConfigMode && !info.isVideo() && m_positionableCategories); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE updateMapForCurrentImage(); #endif } m_preview->updatePositionableCategories(positionableCategories); } void AnnotationDialog::Dialog::writeToInfo() { Q_FOREACH (ListSelect *ls, m_optionList) { ls->slotReturn(); } DB::ImageInfo &info = m_editList[m_current]; if (!info.size().isValid()) { // The actual image size has been fetched by ImagePreview, so we can add it to // the database silenty, so that it's saved if the database will be saved. info.setSize(m_preview->preview()->getActualImageSize()); } if (m_time->isHidden()) { if (m_endDate->date().isValid()) info.setDate(DB::ImageDate(QDateTime(m_startDate->date(), QTime(0, 0, 0)), QDateTime(m_endDate->date(), QTime(23, 59, 59)))); else info.setDate(DB::ImageDate(QDateTime(m_startDate->date(), QTime(0, 0, 0)), QDateTime(m_startDate->date(), QTime(23, 59, 59)))); } else info.setDate(DB::ImageDate(QDateTime(m_startDate->date(), m_time->time()))); // Generate a list of all tagged areas QMap> areas = taggedAreas(); info.setLabel(m_imageLabel->text()); info.setDescription(m_description->toPlainText()); Q_FOREACH (ListSelect *ls, m_optionList) { info.setCategoryInfo(ls->category(), ls->itemsOn()); if (ls->positionable()) { info.setPositionedTags(ls->category(), areas[ls->category()]); } } if (m_ratingChanged) { info.setRating(m_rating->rating()); m_ratingChanged = false; } } void AnnotationDialog::Dialog::ShowHideSearch(bool show) { m_megapixel->setVisible(show); m_megapixelLabel->setVisible(show); m_max_megapixel->setVisible(show); m_max_megapixelLabel->setVisible(show); m_searchRAW->setVisible(show); m_imageFilePatternLabel->setVisible(show); m_imageFilePattern->setVisible(show); m_isFuzzyDate->setChecked(show); m_isFuzzyDate->setVisible(!show); slotSetFuzzyDate(); m_ratingSearchMode->setVisible(show); m_ratingSearchLabel->setVisible(show); } QList AnnotationDialog::Dialog::areas() const { return m_preview->preview()->findChildren(); } QMap> AnnotationDialog::Dialog::taggedAreas() const { QMap> taggedAreas; foreach (ResizableFrame *area, areas()) { QPair tagData = area->tagData(); if (!tagData.first.isEmpty()) { taggedAreas[tagData.first][tagData.second] = area->actualCoordinates(); } } return taggedAreas; } int AnnotationDialog::Dialog::configure(DB::ImageInfoList list, bool oneAtATime) { ShowHideSearch(false); if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()) { DB::ImageDB::instance()->categoryCollection()->categoryForName(Settings::SettingsData::instance()->untaggedCategory())->addItem(Settings::SettingsData::instance()->untaggedTag()); } if (oneAtATime) { m_setup = InputSingleImageConfigMode; } else { m_setup = InputMultiImageConfigMode; // Hide the default positionable category selector m_preview->updatePositionableCategories(); } -#ifdef HAVE_KGEOMAP +#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; Q_FOREACH (ListSelect *ls, m_optionList) { setUpCategoryListBoxForMultiImageSelection(ls, list); } m_imageLabel->setText(QString()); m_imageFilePattern->setText(QString()); m_firstDescription = m_editList[0].description(); const bool allTextEqual = std::all_of(m_editList.begin(), m_editList.end(), [=](const DB::ImageInfo &item) -> bool { return item.description() == m_firstDescription; }); if (!allTextEqual) m_firstDescription = m_conflictText; m_description->setPlainText(m_firstDescription); } showHelpDialog(oneAtATime ? InputSingleImageConfigMode : InputMultiImageConfigMode); return exec(); } DB::ImageSearchInfo AnnotationDialog::Dialog::search(DB::ImageSearchInfo *search) { ShowHideSearch(true); -#ifdef HAVE_KGEOMAP +#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()); Q_FOREACH (const ListSelect *ls, m_optionList) { m_oldSearch.setCategoryMatchText(ls->category(), ls->text()); } //FIXME: for the user to search for 0-rated images, he must first change the rating to anything > 0 //then change back to 0 . if (m_ratingChanged) m_oldSearch.setRating(m_rating->rating()); m_ratingChanged = false; m_oldSearch.setSearchMode(m_ratingSearchMode->currentIndex()); m_oldSearch.setMegaPixel(m_megapixel->value()); m_oldSearch.setMaxMegaPixel(m_max_megapixel->value()); m_oldSearch.setSearchRAW(m_searchRAW->isChecked()); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE const Map::GeoCoordinates::Pair regionSelection = m_annotationMap->getRegionSelection(); m_oldSearch.setRegionSelection(regionSelection); #endif return m_oldSearch; } else return DB::ImageSearchInfo(); } void AnnotationDialog::Dialog::setup() { // Repopulate the listboxes in case data has changed // An group might for example have been renamed. Q_FOREACH (ListSelect *ls, m_optionList) { ls->populate(); } if (m_setup == SearchMode) { KGuiItem::assign(m_okBut, KGuiItem(i18nc("@action:button", "&Search"), QString::fromLatin1("find"))); m_continueLaterBut->hide(); m_revertBut->hide(); m_clearBut->show(); m_preview->setSearchMode(true); setWindowTitle(i18nc("@title:window title of the 'find images' window", "Search")); loadInfo(m_oldSearch); } else { m_okBut->setText(i18n("Done")); m_continueLaterBut->show(); m_revertBut->setEnabled(m_setup == InputSingleImageConfigMode); m_clearBut->hide(); m_revertBut->show(); m_preview->setSearchMode(false); m_preview->setToggleFullscreenPreviewEnabled(m_setup == InputSingleImageConfigMode); setWindowTitle(i18nc("@title:window", "Annotations")); } Q_FOREACH (ListSelect *ls, m_optionList) { ls->setMode(m_setup); } } void AnnotationDialog::Dialog::slotClear() { loadInfo(DB::ImageSearchInfo()); } void AnnotationDialog::Dialog::loadInfo(const DB::ImageSearchInfo &info) { m_startDate->setDate(info.date().start().date()); m_endDate->setDate(info.date().end().date()); Q_FOREACH (ListSelect *ls, m_optionList) { 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(); } bool AnnotationDialog::Dialog::hasChanges() { bool changed = false; if (m_setup == InputSingleImageConfigMode) { writeToInfo(); for (int i = 0; i < m_editList.count(); ++i) { changed |= (*(m_origList[i]) != m_editList[i]); } changed |= m_areasChanged; } else if (m_setup == InputMultiImageConfigMode) { changed |= (!m_startDate->date().isNull()); changed |= (!m_endDate->date().isNull()); Q_FOREACH (ListSelect *ls, m_optionList) { StringSet on, partialOn; std::tie(on, partialOn) = selectionForMultiSelect(ls, m_origList); changed |= (on != ls->itemsOn()); changed |= (partialOn != ls->itemsUnchanged()); } changed |= (!m_imageLabel->text().isEmpty()); changed |= (m_description->toPlainText() != m_firstDescription); changed |= m_ratingChanged; } return changed; } void AnnotationDialog::Dialog::rotate(int angle) { if (m_setup == InputMultiImageConfigMode) { // In doneTagging the preview will be queried for its angle. } else { DB::ImageInfo &info = m_editList[m_current]; info.rotate(angle, DB::RotateImageInfoOnly); emit imageRotated(info.fileName()); } } void AnnotationDialog::Dialog::slotSetFuzzyDate() { if (m_isFuzzyDate->isChecked()) { m_time->hide(); m_timeLabel->hide(); m_endDate->show(); m_endDateLabel->show(); } else { m_time->show(); m_timeLabel->show(); m_endDate->hide(); m_endDateLabel->hide(); } } void AnnotationDialog::Dialog::slotDeleteImage() { // CTRL+Del is a common key combination when editing text // TODO: The word right of cursor should be deleted as expected also in date and category fields if (m_setup == SearchMode) return; if (m_setup == InputMultiImageConfigMode) //TODO: probably delete here should mean remove from selection return; DB::ImageInfoPtr info = m_origList[m_current]; m_origList.remove(info); m_editList.removeAll(m_editList.at(m_current)); MainWindow::DirtyIndicator::markDirty(); if (m_origList.count() == 0) { doneTagging(); return; } if (m_current == (int)m_origList.count()) // we deleted the last image m_current--; load(); } void AnnotationDialog::Dialog::showHelpDialog(UsageMode type) { QString doNotShowKey; QString txt; if (type == SearchMode) { doNotShowKey = QString::fromLatin1("image_config_search_show_help"); txt = i18n("

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

" "

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

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

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

" "

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

"); } KMessageBox::information(this, txt, QString(), doNotShowKey, KMessageBox::AllowLink); } void AnnotationDialog::Dialog::resizeEvent(QResizeEvent *) { Settings::SettingsData::instance()->setWindowGeometry(Settings::AnnotationDialog, geometry()); } void AnnotationDialog::Dialog::moveEvent(QMoveEvent *) { Settings::SettingsData::instance()->setWindowGeometry(Settings::AnnotationDialog, geometry()); } void AnnotationDialog::Dialog::setupFocus() { QList list = findChildren(); QList orderedList; // Iterate through all widgets in our dialog. for (QObject *obj : list) { QWidget *current = static_cast(obj); if (!current->property("WantsFocus").isValid() || !current->isVisible()) continue; int cx = current->mapToGlobal(QPoint(0, 0)).x(); int cy = current->mapToGlobal(QPoint(0, 0)).y(); bool inserted = false; // Iterate through the ordered list of widgets, and insert the current one, so it is in the right position in the tab chain. for (QList::iterator orderedIt = orderedList.begin(); orderedIt != orderedList.end(); ++orderedIt) { const QWidget *w = *orderedIt; int wx = w->mapToGlobal(QPoint(0, 0)).x(); int wy = w->mapToGlobal(QPoint(0, 0)).y(); if (wy > cy || (wy == cy && wx >= cx)) { orderedList.insert(orderedIt, current); inserted = true; break; } } if (!inserted) orderedList.append(current); } // now setup tab order. QWidget *prev = nullptr; QWidget *first = nullptr; Q_FOREACH (QWidget *widget, orderedList) { if (prev) { setTabOrder(prev, widget); } else { first = widget; } prev = widget; } if (first) { setTabOrder(prev, first); } // Finally set focus on the first list select Q_FOREACH (QWidget *widget, orderedList) { if (widget->property("FocusCandidate").isValid() && widget->isVisible()) { widget->setFocus(); break; } } } void AnnotationDialog::Dialog::slotResetLayout() { m_dockWindow->restoreState(m_dockWindowCleanState); } void AnnotationDialog::Dialog::slotStartDateChanged(const DB::ImageDate &date) { if (date.start() == date.end()) m_endDate->setDate(QDate()); else m_endDate->setDate(date.end().date()); } void AnnotationDialog::Dialog::loadWindowLayout() { QString fileName = QString::fromLatin1("%1/layout.dat").arg(Settings::SettingsData::instance()->imageDirectory()); if (!QFileInfo(fileName).exists()) { // create default layout // label/date/rating in a visual block with description: m_dockWindow->splitDockWidget(m_generalDock, m_descriptionDock, Qt::Vertical); // more space for description: m_dockWindow->resizeDocks({ m_generalDock, m_descriptionDock }, { 60, 100 }, Qt::Vertical); // more space for preview: m_dockWindow->resizeDocks({ m_generalDock, m_descriptionDock, m_previewDock }, { 200, 200, 800 }, Qt::Horizontal); -#ifdef HAVE_KGEOMAP +#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() { m_actions = new KActionCollection(this); QAction *action = nullptr; action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-alphatree"), m_optionList.at(0), SLOT(slotSortAlphaTree())); action->setText(i18n("Sort Alphabetically (Tree)")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_F4); action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-alphaflat"), m_optionList.at(0), SLOT(slotSortAlphaFlat())); action->setText(i18n("Sort Alphabetically (Flat)")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-MRU"), m_optionList.at(0), SLOT(slotSortDate())); action->setText(i18n("Sort Most Recently Used")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-sort"), m_optionList.at(0), SLOT(toggleSortType())); action->setText(i18n("Toggle Sorting")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_T); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-showing-selected-only"), &ShowSelectionOnlyManager::instance(), SLOT(toggle())); action->setText(i18n("Toggle Showing Selected Items Only")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_S); action = m_actions->addAction(QString::fromLatin1("annotationdialog-next-image"), m_preview, SLOT(slotNext())); action->setText(i18n("Annotate Next")); m_actions->setDefaultShortcut(action, Qt::Key_PageDown); action = m_actions->addAction(QString::fromLatin1("annotationdialog-prev-image"), m_preview, SLOT(slotPrev())); action->setText(i18n("Annotate Previous")); m_actions->setDefaultShortcut(action, Qt::Key_PageUp); action = m_actions->addAction(QString::fromLatin1("annotationdialog-OK-dialog"), this, SLOT(doneTagging())); action->setText(i18n("OK dialog")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Return); action = m_actions->addAction(QString::fromLatin1("annotationdialog-delete-image"), this, SLOT(slotDeleteImage())); action->setText(i18n("Delete")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete); action = m_actions->addAction(QString::fromLatin1("annotationdialog-copy-previous"), this, SLOT(slotCopyPrevious())); action->setText(i18n("Copy tags from previous image")); m_actions->setDefaultShortcut(action, Qt::ALT + Qt::Key_Insert); action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-left"), m_preview, SLOT(rotateLeft())); action->setText(i18n("Rotate counterclockwise")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-right"), m_preview, SLOT(rotateRight())); action->setText(i18n("Rotate clockwise")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-viewer"), this, SLOT(togglePreview())); action->setText(i18n("Toggle fullscreen preview")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Space); foreach (QAction *action, m_actions->actions()) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } // the annotation dialog is created when it's first used; // therefore, its actions are registered well after the MainWindow sets up its actionCollection, // and it has to read the shortcuts here, after they are set up: m_actions->readSettings(); } KActionCollection *AnnotationDialog::Dialog::actions() { return m_actions; } void AnnotationDialog::Dialog::setUpCategoryListBoxForMultiImageSelection(ListSelect *listSel, const DB::ImageInfoList &images) { StringSet on, partialOn; std::tie(on, partialOn) = selectionForMultiSelect(listSel, images); listSel->setSelection(on, partialOn); } std::tuple AnnotationDialog::Dialog::selectionForMultiSelect(ListSelect *listSel, const DB::ImageInfoList &images) { const QString category = listSel->category(); const StringSet allItems = DB::ImageDB::instance()->categoryCollection()->categoryForName(category)->itemsInclCategories().toSet(); StringSet itemsNotSelectedOnAllImages; StringSet itemsOnSomeImages; for (DB::ImageInfoList::ConstIterator imageIt = images.begin(); imageIt != images.end(); ++imageIt) { const StringSet itemsOnThisImage = (*imageIt)->itemsOfCategory(category); itemsNotSelectedOnAllImages += (allItems - itemsOnThisImage); itemsOnSomeImages += itemsOnThisImage; } const StringSet itemsOnAllImages = allItems - itemsNotSelectedOnAllImages; const StringSet itemsPartiallyOn = itemsOnSomeImages - itemsOnAllImages; return std::make_tuple(itemsOnAllImages, itemsPartiallyOn); } 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 const 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) { Q_FOREACH (ListSelect *ls, m_optionList) { ls->slotReturn(); } 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())); Q_FOREACH (ListSelect *ls, m_optionList) { info->addCategoryInfo(ls->category(), ls->itemsOn()); info->removeCategoryInfo(ls->category(), ls->itemsOff()); } if (!m_imageLabel->text().isEmpty()) { info->setLabel(m_imageLabel->text()); } if (!m_description->toPlainText().isEmpty() && m_description->toPlainText().compare(m_conflictText)) { info->setDescription(m_description->toPlainText()); } if (m_ratingChanged) { info->setRating(m_rating->rating()); } } m_ratingChanged = false; } m_accept = QDialog::Accepted; if (anyChanges) { MainWindow::DirtyIndicator::markDirty(); } QDialog::accept(); } AnnotationDialog::Dialog::~Dialog() { qDeleteAll(m_optionList); m_optionList.clear(); } void AnnotationDialog::Dialog::togglePreview() { if (m_setup == InputSingleImageConfigMode) { if (m_stack->currentWidget() == m_fullScreenPreview) { m_stack->setCurrentWidget(m_dockWindow); m_fullScreenPreview->stopPlayback(); } else { DB::ImageInfo currentInfo = m_editList[m_current]; m_stack->setCurrentWidget(m_fullScreenPreview); m_fullScreenPreview->load(DB::FileNameList() << currentInfo.fileName()); // compute altered tags by removing existing tags from full set: const QMap> existingAreas = currentInfo.taggedAreas(); QMap> alteredAreas = taggedAreas(); for (auto catIt = existingAreas.constBegin(); catIt != existingAreas.constEnd(); ++catIt) { const QString &categoryName = catIt.key(); const QMap &tags = catIt.value(); for (auto tagIt = tags.cbegin(); tagIt != tags.constEnd(); ++tagIt) { const QString &tagName = tagIt.key(); const QRect &area = tagIt.value(); // remove unchanged areas if (area == alteredAreas[categoryName][tagName]) { alteredAreas[categoryName].remove(tagName); if (alteredAreas[categoryName].empty()) alteredAreas.remove(categoryName); } } } m_fullScreenPreview->addAdditionalTaggedAreas(alteredAreas); } } } void AnnotationDialog::Dialog::tidyAreas() { // Remove all areas marked on the preview image foreach (ResizableFrame *area, areas()) { area->markTidied(); area->deleteLater(); } } void AnnotationDialog::Dialog::slotNewArea(ResizableFrame *area) { area->setDialog(this); } void AnnotationDialog::Dialog::positionableTagSelected(QString category, QString tag) { // Be sure not to propose an already-associated tag QPair tagData = qMakePair(category, tag); foreach (ResizableFrame *area, areas()) { if (area->tagData() == tagData) { return; } } // Set the selected tag as the last selected positionable tag m_lastSelectedPositionableTag = tagData; // Add the tag to the positionable tag candidate list addTagToCandidateList(category, tag); } void AnnotationDialog::Dialog::positionableTagDeselected(QString category, QString tag) { // Remove the tag from the candidate list removeTagFromCandidateList(category, tag); // Search for areas linked against the tag on this image if (m_setup == InputSingleImageConfigMode) { QPair deselectedTag = QPair(category, tag); foreach (ResizableFrame *area, areas()) { if (area->tagData() == deselectedTag) { area->removeTagData(); m_areasChanged = true; // Only one area can be associated with the tag, so we can return here return; } } } // Removal of tagged areas in InputMultiImageConfigMode is done in DB::ImageInfo::removeCategoryInfo } void AnnotationDialog::Dialog::addTagToCandidateList(QString category, QString tag) { m_positionableTagCandidates << QPair(category, tag); } void AnnotationDialog::Dialog::removeTagFromCandidateList(QString category, QString tag) { // Is the deselected tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == tag) { m_lastSelectedPositionableTag = QPair(); } // Remove the tag from the candidate list m_positionableTagCandidates.removeAll(QPair(category, tag)); // When a positionable tag is entered via the AreaTagSelectDialog, it's added to this // list twice, so we use removeAll here to be sure to also wipe duplicate entries. } QPair AnnotationDialog::Dialog::lastSelectedPositionableTag() const { return m_lastSelectedPositionableTag; } QList> AnnotationDialog::Dialog::positionableTagCandidates() const { return m_positionableTagCandidates; } void AnnotationDialog::Dialog::slotShowAreas(bool showAreas) { foreach (ResizableFrame *area, areas()) { area->setVisible(showAreas); } } void AnnotationDialog::Dialog::positionableTagRenamed(QString category, QString oldTag, QString newTag) { // Is the renamed tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == oldTag) { m_lastSelectedPositionableTag.second = newTag; } // Check the candidate list for the tag QPair oldTagData = QPair(category, oldTag); if (m_positionableTagCandidates.contains(oldTagData)) { // The tag is in the list, so update it m_positionableTagCandidates.removeAt(m_positionableTagCandidates.indexOf(oldTagData)); m_positionableTagCandidates << QPair(category, newTag); } // Check if an area on the current image contains the changed or proposed tag foreach (ResizableFrame *area, areas()) { if (area->tagData() == oldTagData) { area->setTagData(category, newTag); } } } void AnnotationDialog::Dialog::descriptionPageUpDownPressed(QKeyEvent *event) { if (event->key() == Qt::Key_PageUp) { m_actions->action(QString::fromLatin1("annotationdialog-prev-image"))->trigger(); } else if (event->key() == Qt::Key_PageDown) { m_actions->action(QString::fromLatin1("annotationdialog-next-image"))->trigger(); } } void AnnotationDialog::Dialog::checkProposedTagData( QPair tagData, ResizableFrame *areaToExclude) const { foreach (ResizableFrame *area, areas()) { if (area != areaToExclude && area->proposedTagData() == tagData && area->tagData().first.isEmpty()) { area->removeProposedTagData(); } } } void AnnotationDialog::Dialog::areaChanged() { m_areasChanged = true; } /** * @brief positionableTagValid checks whether a given tag can still be associated to an area. * This checks for empty and duplicate tags. * @return */ bool AnnotationDialog::Dialog::positionableTagAvailable(const QString &category, const QString &tag) const { if (category.isEmpty() || tag.isEmpty()) return false; // does any area already have that tag? foreach (const ResizableFrame *area, areas()) { const auto tagData = area->tagData(); if (tagData.first == category && tagData.second == tag) return false; } return true; } /** * @brief Generates a set of positionable tags currently used on the image * @param category * @return */ QSet AnnotationDialog::Dialog::positionedTags(const QString &category) const { QSet tags; foreach (const ResizableFrame *area, areas()) { const auto tagData = area->tagData(); if (tagData.first == category) tags += tagData.second; } return tags; } AnnotationDialog::ListSelect *AnnotationDialog::Dialog::listSelectForCategory(const QString &category) { return m_listSelectList.value(category, nullptr); } -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE void AnnotationDialog::Dialog::updateMapForCurrentImage() { if (m_setup != InputSingleImageConfigMode) { return; } if ( m_editList[m_current].coordinates().hasCoordinates() ) { m_annotationMap->setCenter( m_editList[m_current] ); m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } } void AnnotationDialog::Dialog::annotationMapVisibilityChanged(bool visible) { // This populates the map if it's added when the dialog is already open if (visible) { // when the map dockwidget is already visible on show(), the call to // annotationMapVisibilityChanged is executed in the GUI thread. // This ensures that populateMap() doesn't block the GUI in this case: QTimer::singleShot(0, this, SLOT(populateMap())); } else { m_cancelMapLoading = true; } } void AnnotationDialog::Dialog::populateMap() { // populateMap is called every time the map widget gets visible if (m_mapIsPopulated) { return; } m_annotationMap->displayStatus(Map::MapView::MapStatus::Loading); m_cancelMapLoading = false; m_mapLoadingProgress->setMaximum( m_editList.count() ); m_mapLoadingProgress->show(); m_cancelMapLoadingButton->show(); int processedImages = 0; int imagesWithCoordinates = 0; foreach ( DB::ImageInfo info, m_editList ) { processedImages++; m_mapLoadingProgress->setValue(processedImages); // keep things responsive by processing events manually: QApplication::processEvents(); if ( info.coordinates().hasCoordinates() ) { m_annotationMap->addImage(info); imagesWithCoordinates++; } // m_cancelMapLoading is set to true by clicking the "Cancel" button if (m_cancelMapLoading) { m_annotationMap->clear(); break; } } // at this point either we canceled loading or the map is populated: m_mapIsPopulated = !m_cancelMapLoading; mapLoadingFinished(imagesWithCoordinates > 0, imagesWithCoordinates == processedImages); } void AnnotationDialog::Dialog::setCancelMapLoading() { m_cancelMapLoading = true; } void AnnotationDialog::Dialog::mapLoadingFinished(bool mapHasImages, bool allImagesHaveCoordinates) { m_mapLoadingProgress->hide(); m_cancelMapLoadingButton->hide(); if (m_setup == InputSingleImageConfigMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } else { if (m_setup == SearchMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SearchCoordinates); } else { if (mapHasImages) { if (!allImagesHaveCoordinates) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SomeImagesHaveNoCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::NoImagesHaveNoCoordinates); } } } if (m_setup != SearchMode) { m_annotationMap->zoomToMarkers(); updateMapForCurrentImage(); } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/Dialog.h b/AnnotationDialog/Dialog.h index 27a85f87..b3d6e324 100644 --- a/AnnotationDialog/Dialog.h +++ b/AnnotationDialog/Dialog.h @@ -1,246 +1,247 @@ /* Copyright (C) 2003-2019 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. */ #ifndef ANNOTATIONDIALOG_DIALOG_H #define ANNOTATIONDIALOG_DIALOG_H -#include "config-kpa-kgeomap.h" +#include "config-kpa-marble.h" +#include "enums.h" #include "ImagePreviewWidget.h" #include "ListSelect.h" -#include "enums.h" #include #include #include #include #include #include #include #include + class DockWidget; class KActionCollection; class KComboBox; class KLineEdit; class KRatingWidget; class KTextEdit; class QCloseEvent; class QDockWidget; class QMainWindow; class QMoveEvent; class QProgressBar; class QPushButton; class QResizeEvent; class QSplitter; class QStackedWidget; class QTimeEdit; namespace Viewer { class ViewerWidget; } namespace DB { class ImageInfo; } namespace Map { class MapView; } namespace AnnotationDialog { class ImagePreview; class DateEdit; class ShortCutManager; class ResizableFrame; class Dialog : public QDialog { Q_OBJECT public: explicit Dialog(QWidget *parent); ~Dialog() override; int configure(DB::ImageInfoList list, bool oneAtATime); DB::ImageSearchInfo search(DB::ImageSearchInfo *search = nullptr); KActionCollection *actions(); QPair lastSelectedPositionableTag() const; QList> positionableTagCandidates() const; void addTagToCandidateList(QString category, QString tag); void removeTagFromCandidateList(QString category, QString tag); void checkProposedTagData(QPair tagData, ResizableFrame *areaToExclude) const; void areaChanged(); bool positionableTagAvailable(const QString &category, const QString &tag) const; QSet positionedTags(const QString &category) const; /** * @return A list of all ResizableFrame objects on the current image */ QList areas() const; /** * @brief taggedAreas creates a map of all the currently tagged areas. * This is different from areas(), which also contains untagged areas. * This is different from \code m_editList[m_current].areas()\endcode, which * does not include newly added (or deleted) areas. * @return a map of currently tagged areas */ QMap> taggedAreas() const; ListSelect *listSelectForCategory(const QString &category); protected slots: void slotRevert(); void slotIndexChanged(int index); void doneTagging(); void continueLater(); void slotClear(); void slotOptions(); void slotSaveWindowSetup(); void slotDeleteOption(DB::Category *, const QString &); void slotRenameOption(DB::Category *, const QString &, const QString &); void reject() override; void rotate(int angle); void slotSetFuzzyDate(); void slotDeleteImage(); void slotResetLayout(); void slotStartDateChanged(const DB::ImageDate &); void slotCopyPrevious(); void slotShowAreas(bool showAreas); void slotRatingChanged(unsigned int); void togglePreview(); void descriptionPageUpDownPressed(QKeyEvent *event); void slotNewArea(ResizableFrame *area); void positionableTagSelected(QString category, QString tag); void positionableTagDeselected(QString category, QString tag); void positionableTagRenamed(QString category, QString oldTag, QString newTag); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE void setCancelMapLoading(); void annotationMapVisibilityChanged(bool visible); void populateMap(); #endif signals: void imageRotated(const DB::FileName &id); protected: QDockWidget *createDock(const QString &title, const QString &name, Qt::DockWidgetArea location, QWidget *widget); QWidget *createDateWidget(ShortCutManager &shortCutManager); QWidget *createPreviewWidget(); ListSelect *createListSel(const DB::CategoryPtr &category); void load(); void writeToInfo(); void setup(); void loadInfo(const DB::ImageSearchInfo &); int exec() override; void closeEvent(QCloseEvent *) override; void showTornOfWindows(); void hideTornOfWindows(); bool hasChanges(); void showHelpDialog(UsageMode); void resizeEvent(QResizeEvent *) override; void moveEvent(QMoveEvent *) override; void setupFocus(); void closeDialog(); void loadWindowLayout(); void setupActions(); void setUpCategoryListBoxForMultiImageSelection(ListSelect *, const DB::ImageInfoList &images); std::tuple selectionForMultiSelect(ListSelect *, const DB::ImageInfoList &images); void saveAndClose(); void ShowHideSearch(bool show); private: QStackedWidget *m_stack; Viewer::ViewerWidget *m_fullScreenPreview; DB::ImageInfoList m_origList; QList m_editList; int m_current; UsageMode m_setup; QList m_optionList; DB::ImageSearchInfo m_oldSearch; int m_accept; QList m_dockWidgets; // "special" named dockWidgets (used to set default layout): QDockWidget *m_generalDock; QDockWidget *m_previewDock; QDockWidget *m_descriptionDock; // Widgets QMainWindow *m_dockWindow; KLineEdit *m_imageLabel; DateEdit *m_startDate; DateEdit *m_endDate; QLabel *m_endDateLabel; QLabel *m_imageFilePatternLabel; KLineEdit *m_imageFilePattern; ImagePreviewWidget *m_preview; QPushButton *m_revertBut; QPushButton *m_clearBut; QPushButton *m_okBut; QPushButton *m_continueLaterBut; KTextEdit *m_description; QTimeEdit *m_time; QLabel *m_timeLabel; QCheckBox *m_isFuzzyDate; KRatingWidget *m_rating; KComboBox *m_ratingSearchMode; QLabel *m_ratingSearchLabel; bool m_ratingChanged; QSpinBox *m_megapixel; QLabel *m_megapixelLabel; QSpinBox *m_max_megapixel; QLabel *m_max_megapixelLabel; QCheckBox *m_searchRAW; QString m_conflictText; QString m_firstDescription; KActionCollection *m_actions; /** Clean state of the dock window. * * Used in slotResetLayout(). */ QByteArray m_dockWindowCleanState; void tidyAreas(); QPair m_lastSelectedPositionableTag; QList> m_positionableTagCandidates; QMap m_listSelectList; bool m_positionableCategories; bool m_areasChanged; -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE QDockWidget *m_mapDock; QWidget *m_annotationMapContainer; Map::MapView *m_annotationMap; void updateMapForCurrentImage(); QProgressBar *m_mapLoadingProgress; QPushButton *m_cancelMapLoadingButton; void mapLoadingFinished(bool mapHasImages, bool allImagesHaveCoordinates); bool m_cancelMapLoading; bool m_mapIsPopulated; #endif }; } #endif /* ANNOTATIONDIALOG_DIALOG_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/OverviewPage.cpp b/Browser/OverviewPage.cpp index 3026e7e4..fb902a7a 100644 --- a/Browser/OverviewPage.cpp +++ b/Browser/OverviewPage.cpp @@ -1,341 +1,341 @@ /* Copyright (C) 2003-2019 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 "OverviewPage.h" #include "BrowserWidget.h" #include "CategoryPage.h" #include "ImageViewPage.h" #include "enums.h" #include -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE #include "GeoPositionPage.h" #endif #include #include #include #include #include #include #include #include #include #include #include const int THUMBNAILSIZE = 70; AnnotationDialog::Dialog *Browser::OverviewPage::s_config = nullptr; Browser::OverviewPage::OverviewPage(const Breadcrumb &breadcrumb, const DB::ImageSearchInfo &info, BrowserWidget *browser) : BrowserPage(info, browser) , m_breadcrumb(breadcrumb) { //updateImageCount(); } int Browser::OverviewPage::rowCount(const QModelIndex &parent) const { if (parent != QModelIndex()) return 0; return categories().count() + -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE 1 + #endif 4; // Exiv search + Search info + Untagged Images + Show Image } QVariant Browser::OverviewPage::data(const QModelIndex &index, int role) const { if (role == ValueRole) return index.row(); const int row = index.row(); if (isCategoryIndex(row)) return categoryInfo(row, role); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE else if (isGeoPositionIndex(row)) return geoPositionInfo(role); #endif else if (isExivIndex(row)) return exivInfo(role); else if (isSearchIndex(row)) return searchInfo(role); else if (isUntaggedImagesIndex(row)) return untaggedImagesInfo(role); else if (isImageIndex(row)) return imageInfo(role); return QVariant(); } bool Browser::OverviewPage::isCategoryIndex(int row) const { return row < categories().count() && row >= 0; } bool Browser::OverviewPage::isGeoPositionIndex(int row) const { -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE return row == categories().count(); #else Q_UNUSED(row); return false; #endif } bool Browser::OverviewPage::isExivIndex(int row) const { int exivRow = categories().count(); -#ifdef HAVE_KGEOMAP - exivRow++; -#endif + #ifdef HAVE_MARBLE + exivRow++; + #endif return row == exivRow; } bool Browser::OverviewPage::isSearchIndex(int row) const { return rowCount() - 3 == row; } bool Browser::OverviewPage::isUntaggedImagesIndex(int row) const { return rowCount() - 2 == row; } bool Browser::OverviewPage::isImageIndex(int row) const { return rowCount() - 1 == row; } QList Browser::OverviewPage::categories() const { return DB::ImageDB::instance()->categoryCollection()->categories(); } QVariant Browser::OverviewPage::categoryInfo(int row, int role) const { if (role == Qt::DisplayRole) return categories()[row]->name(); else if (role == Qt::DecorationRole) return categories()[row]->icon(THUMBNAILSIZE); return QVariant(); } QVariant Browser::OverviewPage::geoPositionInfo(int role) const { if (role == Qt::DisplayRole) return i18n("Geo Position"); else if (role == Qt::DecorationRole) { return QIcon::fromTheme(QString::fromLatin1("globe")).pixmap(THUMBNAILSIZE); } return QVariant(); } QVariant Browser::OverviewPage::exivInfo(int role) const { if (role == Qt::DisplayRole) return i18n("Exif Info"); else if (role == Qt::DecorationRole) { return QIcon::fromTheme(QString::fromLatin1("document-properties")).pixmap(THUMBNAILSIZE); } return QVariant(); } QVariant Browser::OverviewPage::searchInfo(int role) const { if (role == Qt::DisplayRole) return i18nc("@action Search button in the browser view.", "Search"); else if (role == Qt::DecorationRole) return QIcon::fromTheme(QString::fromLatin1("system-search")).pixmap(THUMBNAILSIZE); return QVariant(); } QVariant Browser::OverviewPage::untaggedImagesInfo(int role) const { if (role == Qt::DisplayRole) return i18n("Untagged Images"); else if (role == Qt::DecorationRole) return QIcon::fromTheme(QString::fromUtf8("archive-insert")).pixmap(THUMBNAILSIZE); return QVariant(); } QVariant Browser::OverviewPage::imageInfo(int role) const { if (role == Qt::DisplayRole) return i18n("Show Thumbnails"); else if (role == Qt::DecorationRole) { QIcon icon = QIcon::fromTheme(QString::fromUtf8("view-preview")); QPixmap pixmap = icon.pixmap(THUMBNAILSIZE); // workaround for QListView in Qt 5.5: // On Qt5.5 if the last item in the list view has no DecorationRole, then // the whole list view "collapses" to the size of text-only items, // cutting off the existing thumbnails. // This can be triggered by an incomplete icon theme. if (pixmap.isNull()) { pixmap = QPixmap(THUMBNAILSIZE, THUMBNAILSIZE); pixmap.fill(Qt::transparent); } return pixmap; } return QVariant(); } Browser::BrowserPage *Browser::OverviewPage::activateChild(const QModelIndex &index) { const int row = index.row(); if (isCategoryIndex(row)) return new Browser::CategoryPage(categories()[row], BrowserPage::searchInfo(), browser()); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE else if (isGeoPositionIndex(row)) return new Browser::GeoPositionPage(BrowserPage::searchInfo(), browser()); #endif else if (isExivIndex(row)) return activateExivAction(); else if (isSearchIndex(row)) return activateSearchAction(); else if (isUntaggedImagesIndex(row)) { return activateUntaggedImagesAction(); } else if (isImageIndex(row)) return new ImageViewPage(BrowserPage::searchInfo(), browser()); return nullptr; } void Browser::OverviewPage::activate() { updateImageCount(); browser()->setModel(this); } Qt::ItemFlags Browser::OverviewPage::flags(const QModelIndex &index) const { if (isCategoryIndex(index.row()) && !m_rowHasSubcategories[index.row()]) return QAbstractListModel::flags(index) & ~Qt::ItemIsEnabled; else return QAbstractListModel::flags(index); } bool Browser::OverviewPage::isSearchable() const { return true; } Browser::BrowserPage *Browser::OverviewPage::activateExivAction() { QPointer dialog = new Exif::SearchDialog(browser()); { Utilities::ShowBusyCursor undoTheBusyWhileShowingTheDialog(Qt::ArrowCursor); if (dialog->exec() == QDialog::Rejected) { delete dialog; return nullptr; } // Dialog can be deleted by its parent in event loop while in exec() if (dialog.isNull()) return nullptr; } Exif::SearchInfo result = dialog->info(); DB::ImageSearchInfo info = BrowserPage::searchInfo(); info.addExifSearchInfo(dialog->info()); delete dialog; if (DB::ImageDB::instance()->count(info).total() == 0) { KMessageBox::information(browser(), i18n("Search did not match any images or videos."), i18n("Empty Search Result")); return nullptr; } return new OverviewPage(Breadcrumb(i18n("Exif Search")), info, browser()); } Browser::BrowserPage *Browser::OverviewPage::activateSearchAction() { if (!s_config) s_config = new AnnotationDialog::Dialog(browser()); Utilities::ShowBusyCursor undoTheBusyWhileShowingTheDialog(Qt::ArrowCursor); DB::ImageSearchInfo tmpInfo = BrowserPage::searchInfo(); DB::ImageSearchInfo info = s_config->search(&tmpInfo); // PENDING(blackie) why take the address? if (info.isNull()) return nullptr; if (DB::ImageDB::instance()->count(info).total() == 0) { KMessageBox::information(browser(), i18n("Search did not match any images or videos."), i18n("Empty Search Result")); return nullptr; } return new OverviewPage(Breadcrumb(i18nc("Breadcrumb denoting that we 'browsed' to a search result.", "search")), info, browser()); } Browser::Breadcrumb Browser::OverviewPage::breadcrumb() const { return m_breadcrumb; } bool Browser::OverviewPage::showDuringMovement() const { return true; } void Browser::OverviewPage::updateImageCount() { QElapsedTimer timer; timer.start(); int row = 0; for (const DB::CategoryPtr &category : categories()) { QMap items = DB::ImageDB::instance()->classify(BrowserPage::searchInfo(), category->name(), DB::anyMediaType, DB::ClassificationMode::PartialCount); m_rowHasSubcategories[row] = items.count() > 1; ++row; } qCDebug(TimingLog) << "Browser::Overview::updateImageCount(): " << timer.elapsed() << "ms."; } Browser::BrowserPage *Browser::OverviewPage::activateUntaggedImagesAction() { if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()) { DB::ImageSearchInfo info = BrowserPage::searchInfo(); info.addAnd(Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag()); return new ImageViewPage(info, browser()); } else { // Note: the same dialog text is used in MainWindow::Window::slotMarkUntagged(), // so if it is changed, be sure to also change it there! KMessageBox::information(browser(), i18n("

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

" "

Please follow these steps to do so:" "

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

"), i18n("Feature has not been configured")); return nullptr; } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d8519a8..5caf65fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,551 +1,551 @@ cmake_minimum_required(VERSION 3.2.0) project(kphotoalbum VERSION 5.5) 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 ) set(CMAKE_CXX_STANDARD 11) 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(KF5KGeoMap) -#set_package_properties(KF5KGeoMap -# PROPERTIES -# TYPE OPTIONAL -# PURPOSE "Enables support for geographic map location using embedded GPS information." -# ) -#set(HAVE_KGEOMAP ${KF5KGeoMap_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}) 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 ) 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 ) set(libMap_SRCS) -#if(KF5KGeoMap_FOUND) +if(Marble_FOUND) set(libMap_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Browser/GeoPositionPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/PositionBrowserWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/MapView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/GeoCoordinates.cpp ) -#endif() +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 doxygen headers so that they get visibiltiy in IDEs: ${CMAKE_CURRENT_SOURCE_DIR}/documentation/coding-standards.h ${CMAKE_CURRENT_SOURCE_DIR}/documentation/mainpage.h ${CMAKE_CURRENT_SOURCE_DIR}/documentation/phrase-book.h ${CMAKE_CURRENT_SOURCE_DIR}/documentation/videothumbnails.h ) 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(KF5KGeoMap_FOUND) -# target_link_libraries(kphotoalbum KF5::KGeoMap ) -#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-kgeomap.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kpa-kgeomap.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/DB/ImageInfo.cpp b/DB/ImageInfo.cpp index d737b4fa..a92ec960 100644 --- a/DB/ImageInfo.cpp +++ b/DB/ImageInfo.cpp @@ -1,799 +1,799 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageInfo.h" #include "CategoryCollection.h" #include "FileInfo.h" #include "ImageDB.h" #include "Logging.h" #include "MemberMap.h" #include #include #include #include #include #include #include #include using namespace DB; ImageInfo::ImageInfo() : m_null(true) , m_rating(-1) , m_stackId(0) , m_stackOrder(0) , m_videoLength(-1) , m_isMatched(false) , m_matchGeneration(-1) , m_locked(false) , m_dirty(false) { } ImageInfo::ImageInfo(const DB::FileName &fileName, MediaType type, bool readExifInfo, bool storeExifInfo) : m_imageOnDisk(YesOnDisk) , m_null(false) , m_size(-1, -1) , m_type(type) , m_rating(-1) , m_stackId(0) , m_stackOrder(0) , m_videoLength(-1) , m_isMatched(false) , m_matchGeneration(-1) , m_locked(false) { QFileInfo fi(fileName.absolute()); m_label = fi.completeBaseName(); m_angle = 0; setFileName(fileName); // Read Exif information if (readExifInfo) { ExifMode mode = EXIFMODE_INIT; if (!storeExifInfo) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName, mode); } m_dirty = false; } ImageInfo::ImageInfo(const ImageInfo &other) { *this = other; } void ImageInfo::setIsMatched(bool isMatched) { m_isMatched = isMatched; } bool ImageInfo::isMatched() const { return m_isMatched; } void ImageInfo::setMatchGeneration(int matchGeneration) { m_matchGeneration = matchGeneration; } int ImageInfo::matchGeneration() const { return m_matchGeneration; } void ImageInfo::setLabel(const QString &desc) { if (desc != m_label) m_dirty = true; m_label = desc; } QString ImageInfo::label() const { return m_label; } void ImageInfo::setDescription(const QString &desc) { if (desc != m_description) m_dirty = true; m_description = desc.trimmed(); } QString ImageInfo::description() const { return m_description; } void ImageInfo::setCategoryInfo(const QString &key, const StringSet &value) { // Don't check if really changed, because it's too slow. m_dirty = true; m_categoryInfomation[key] = value; } bool ImageInfo::hasCategoryInfo(const QString &key, const QString &value) const { return m_categoryInfomation[key].contains(value); } bool DB::ImageInfo::hasCategoryInfo(const QString &key, const StringSet &values) const { return values.intersects(m_categoryInfomation[key]); } StringSet ImageInfo::itemsOfCategory(const QString &key) const { return m_categoryInfomation[key]; } void ImageInfo::renameItem(const QString &category, const QString &oldValue, const QString &newValue) { if (m_taggedAreas.contains(category)) { if (m_taggedAreas[category].contains(oldValue)) { m_taggedAreas[category][newValue] = m_taggedAreas[category][oldValue]; m_taggedAreas[category].remove(oldValue); } } StringSet &set = m_categoryInfomation[category]; StringSet::iterator it = set.find(oldValue); if (it != set.end()) { m_dirty = true; set.erase(it); set.insert(newValue); } } DB::FileName ImageInfo::fileName() const { return m_fileName; } void ImageInfo::setFileName(const DB::FileName &fileName) { if (fileName != m_fileName) m_dirty = true; m_fileName = fileName; m_imageOnDisk = Unchecked; DB::CategoryPtr folderCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::FolderCategory); if (folderCategory) { DB::MemberMap &map = DB::ImageDB::instance()->memberMap(); createFolderCategoryItem(folderCategory, map); //ImageDB::instance()->setMemberMap( map ); } } void ImageInfo::rotate(int degrees, RotationMode mode) { // ensure positive degrees: degrees += 360; degrees = degrees % 360; if (degrees == 0) return; m_dirty = true; m_angle = (m_angle + degrees) % 360; if (degrees == 90 || degrees == 270) { m_size.transpose(); } // the AnnotationDialog manages this by itself and sets RotateImageInfoOnly: if (mode == RotateImageInfoAndAreas) { for (auto &areasOfCategory : m_taggedAreas) { for (auto &area : areasOfCategory) { QRect rotatedArea; // parameter order for QRect::setCoords: // setCoords( left, top, right, bottom ) // keep in mind that _size is already transposed switch (degrees) { case 90: rotatedArea.setCoords( m_size.width() - area.bottom(), area.left(), m_size.width() - area.top(), area.right()); break; case 180: rotatedArea.setCoords( m_size.width() - area.right(), m_size.height() - area.bottom(), m_size.width() - area.left(), m_size.height() - area.top()); break; case 270: rotatedArea.setCoords( area.top(), m_size.height() - area.right(), area.bottom(), m_size.height() - area.left()); break; default: // degrees==0; "odd" values won't happen. rotatedArea = area; break; } // update _taggedAreas[category][tag]: area = rotatedArea; } } } } int ImageInfo::angle() const { return m_angle; } void ImageInfo::setAngle(int angle) { if (angle != m_angle) m_dirty = true; m_angle = angle; } short ImageInfo::rating() const { return m_rating; } void ImageInfo::setRating(short rating) { Q_ASSERT((rating >= 0 && rating <= 10) || rating == -1); if (rating > 10) rating = 10; if (rating < -1) rating = -1; if (m_rating != rating) m_dirty = true; m_rating = rating; } DB::StackID ImageInfo::stackId() const { return m_stackId; } void ImageInfo::setStackId(const DB::StackID stackId) { if (stackId != m_stackId) m_dirty = true; m_stackId = stackId; } unsigned int ImageInfo::stackOrder() const { return m_stackOrder; } void ImageInfo::setStackOrder(const unsigned int stackOrder) { if (stackOrder != m_stackOrder) m_dirty = true; m_stackOrder = stackOrder; } void ImageInfo::setVideoLength(int length) { if (m_videoLength != length) m_dirty = true; m_videoLength = length; } int ImageInfo::videoLength() const { return m_videoLength; } void ImageInfo::setDate(const ImageDate &date) { if (date != m_date) m_dirty = true; m_date = date; } ImageDate &ImageInfo::date() { return m_date; } ImageDate ImageInfo::date() const { return m_date; } bool ImageInfo::operator!=(const ImageInfo &other) const { return !(*this == other); } bool ImageInfo::operator==(const ImageInfo &other) const { bool changed = (m_fileName != other.m_fileName || m_label != other.m_label || (!m_description.isEmpty() && !other.m_description.isEmpty() && m_description != other.m_description) || // one might be isNull. m_date != other.m_date || m_angle != other.m_angle || m_rating != other.m_rating || (m_stackId != other.m_stackId || !((m_stackId == 0) ? true : (m_stackOrder == other.m_stackOrder)))); if (!changed) { QStringList keys = DB::ImageDB::instance()->categoryCollection()->categoryNames(); for (QStringList::ConstIterator it = keys.constBegin(); it != keys.constEnd(); ++it) changed |= m_categoryInfomation[*it] != other.m_categoryInfomation[*it]; } return !changed; } void ImageInfo::renameCategory(const QString &oldName, const QString &newName) { m_dirty = true; m_categoryInfomation[newName] = m_categoryInfomation[oldName]; m_categoryInfomation.remove(oldName); m_taggedAreas[newName] = m_taggedAreas[oldName]; m_taggedAreas.remove(oldName); } void ImageInfo::setMD5Sum(const MD5 &sum, bool storeEXIF) { if (sum != m_md5sum) { // if we make a QObject derived class out of imageinfo, we might invalidate thumbnails from here // file changed -> reload/invalidate metadata: ExifMode mode = EXIFMODE_ORIENTATION | EXIFMODE_DATABASE_UPDATE; // fuzzy dates are usually set for a reason if (!m_date.isFuzzy()) mode |= EXIFMODE_DATE; // FIXME (ZaJ): the "right" thing to do would be to update the description // - if it is currently empty (done.) // - if it has been set from the exif info and not been changed (TODO) if (m_description.isEmpty()) mode |= EXIFMODE_DESCRIPTION; if (!storeEXIF) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName(), mode); // FIXME (ZaJ): it *should* make sense to set the ImageDB::md5Map() from here, but I want // to make sure I fully understand everything first... // this could also be done as signal md5Changed(old,new) // image size is invalidated by the thumbnail builder, if needed m_dirty = true; } m_md5sum = sum; } void ImageInfo::setLocked(bool locked) { m_locked = locked; } bool ImageInfo::isLocked() const { return m_locked; } void ImageInfo::readExif(const DB::FileName &fullPath, DB::ExifMode mode) { DB::FileInfo exifInfo = DB::FileInfo::read(fullPath, mode); // Date if (updateDateInformation(mode)) { const ImageDate newDate(exifInfo.dateTime()); setDate(newDate); } // Orientation if ((mode & EXIFMODE_ORIENTATION) && Settings::SettingsData::instance()->useEXIFRotate()) { setAngle(exifInfo.angle()); } // Description if ((mode & EXIFMODE_DESCRIPTION) && Settings::SettingsData::instance()->useEXIFComments()) { bool doSetDescription = true; QString desc = exifInfo.description(); if (Settings::SettingsData::instance()->stripEXIFComments()) { for (const auto &ignoredComment : Settings::SettingsData::instance()->EXIFCommentsToStrip()) { if (desc == ignoredComment) { doSetDescription = false; break; } } } if (doSetDescription) { setDescription(desc); } } // Database update if (mode & EXIFMODE_DATABASE_UPDATE) { Exif::Database::instance()->add(exifInfo); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE // GPS coords might have changed... m_coordsIsSet = false; #endif } } QStringList ImageInfo::availableCategories() const { return m_categoryInfomation.keys(); } QSize ImageInfo::size() const { return m_size; } void ImageInfo::setSize(const QSize &size) { if (size != m_size) m_dirty = true; m_size = size; } bool ImageInfo::imageOnDisk(const DB::FileName &fileName) { return fileName.exists(); } ImageInfo::ImageInfo(const DB::FileName &fileName, const QString &label, const QString &description, const ImageDate &date, int angle, const MD5 &md5sum, const QSize &size, MediaType type, short rating, unsigned int stackId, unsigned int stackOrder) { m_fileName = fileName; m_label = label; m_description = description; m_date = date; m_angle = angle; m_md5sum = md5sum; m_size = size; m_imageOnDisk = Unchecked; m_locked = false; m_null = false; m_type = type; m_dirty = true; if (rating > 10) rating = 10; if (rating < -1) rating = -1; m_rating = rating; m_stackId = stackId; m_stackOrder = stackOrder; m_videoLength = -1; } // Note: we need this operator because the base class QSharedData hides // its copy operator to make exclude the reference counting from being // copied. ImageInfo &ImageInfo::operator=(const ImageInfo &other) { m_fileName = other.m_fileName; m_label = other.m_label; m_description = other.m_description; m_date = other.m_date; m_categoryInfomation = other.m_categoryInfomation; m_taggedAreas = other.m_taggedAreas; m_angle = other.m_angle; m_imageOnDisk = other.m_imageOnDisk; m_md5sum = other.m_md5sum; m_null = other.m_null; m_size = other.m_size; m_type = other.m_type; m_rating = other.m_rating; m_stackId = other.m_stackId; m_stackOrder = other.m_stackOrder; m_videoLength = other.m_videoLength; m_isMatched = other.m_isMatched; m_matchGeneration = other.m_matchGeneration; #ifdef HAVE_KGEOMAP m_coordinates = other.m_coordinates; m_coordsIsSet = other.m_coordsIsSet; #endif m_locked = other.m_locked; m_dirty = other.m_dirty; return *this; } MediaType DB::ImageInfo::mediaType() const { return m_type; } bool ImageInfo::isVideo() const { return m_type == Video; } void DB::ImageInfo::createFolderCategoryItem(DB::CategoryPtr folderCategory, DB::MemberMap &memberMap) { QString folderName = Utilities::relativeFolderName(m_fileName.relative()); if (folderName.isEmpty()) return; if (!memberMap.contains(folderCategory->name(), folderName)) { QStringList directories = folderName.split(QString::fromLatin1("/")); QString curPath; for (QStringList::ConstIterator directoryIt = directories.constBegin(); directoryIt != directories.constEnd(); ++directoryIt) { if (curPath.isEmpty()) curPath = *directoryIt; else { QString oldPath = curPath; curPath = curPath + QString::fromLatin1("/") + *directoryIt; memberMap.addMemberToGroup(folderCategory->name(), oldPath, curPath); } } folderCategory->addItem(folderName); } m_categoryInfomation.insert(folderCategory->name(), StringSet() << folderName); } void DB::ImageInfo::copyExtraData(const DB::ImageInfo &from, bool copyAngle) { m_categoryInfomation = from.m_categoryInfomation; m_description = from.m_description; // Hmm... what should the date be? orig or modified? // _date = from._date; if (copyAngle) m_angle = from.m_angle; m_rating = from.m_rating; } void DB::ImageInfo::removeExtraData() { m_categoryInfomation.clear(); m_description.clear(); m_rating = -1; } void ImageInfo::merge(const ImageInfo &other) { // Merge date if (other.date() != m_date) { // a fuzzy date has been set by the user and therefore "wins" over an exact date. // two fuzzy dates can be merged // two exact dates should ideally be cross-checked with Exif information in the file. // Nevertheless, we merge them into a fuzzy date to avoid the complexity of checking the file. if (other.date().isFuzzy()) { if (m_date.isFuzzy()) m_date.extendTo(other.date()); else m_date = other.date(); } else if (!m_date.isFuzzy()) { m_date.extendTo(other.date()); } // else: keep m_date } // Merge description if (!other.description().isEmpty()) { if (m_description.isEmpty()) m_description = other.description(); else if (m_description != other.description()) m_description += QString::fromUtf8("\n-----------\n") + other.m_description; } // Clear untagged tag if only one of the images was untagged const QString untaggedCategory = Settings::SettingsData::instance()->untaggedCategory(); const QString untaggedTag = Settings::SettingsData::instance()->untaggedTag(); const bool isCompleted = !m_categoryInfomation[untaggedCategory].contains(untaggedTag) || !other.m_categoryInfomation[untaggedCategory].contains(untaggedTag); // Merge tags QSet keys = QSet::fromList(m_categoryInfomation.keys()); keys.unite(QSet::fromList(other.m_categoryInfomation.keys())); for (const QString &key : keys) { m_categoryInfomation[key].unite(other.m_categoryInfomation[key]); } // Clear untagged tag if only one of the images was untagged if (isCompleted) m_categoryInfomation[untaggedCategory].remove(untaggedTag); // merge stacks: if (isStacked() || other.isStacked()) { DB::FileNameList stackImages; if (!isStacked()) stackImages.append(fileName()); else stackImages.append(DB::ImageDB::instance()->getStackFor(fileName())); stackImages.append(DB::ImageDB::instance()->getStackFor(other.fileName())); DB::ImageDB::instance()->unstack(stackImages); if (!DB::ImageDB::instance()->stack(stackImages)) qCWarning(DBLog, "Could not merge stacks!"); } } void DB::ImageInfo::addCategoryInfo(const QString &category, const StringSet &values) { for (StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt) { if (!m_categoryInfomation[category].contains(*valueIt)) { m_dirty = true; m_categoryInfomation[category].insert(*valueIt); } } } void DB::ImageInfo::clearAllCategoryInfo() { m_categoryInfomation.clear(); m_taggedAreas.clear(); } void DB::ImageInfo::removeCategoryInfo(const QString &category, const StringSet &values) { for (StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt) { if (m_categoryInfomation[category].contains(*valueIt)) { m_dirty = true; m_categoryInfomation[category].remove(*valueIt); m_taggedAreas[category].remove(*valueIt); } } } void DB::ImageInfo::addCategoryInfo(const QString &category, const QString &value, const QRect &area) { if (!m_categoryInfomation[category].contains(value)) { m_dirty = true; m_categoryInfomation[category].insert(value); if (area.isValid()) { m_taggedAreas[category][value] = area; } } } void DB::ImageInfo::removeCategoryInfo(const QString &category, const QString &value) { if (m_categoryInfomation[category].contains(value)) { m_dirty = true; m_categoryInfomation[category].remove(value); m_taggedAreas[category].remove(value); } } void DB::ImageInfo::setPositionedTags(const QString &category, const QMap &positionedTags) { m_dirty = true; m_taggedAreas[category] = positionedTags; } bool DB::ImageInfo::updateDateInformation(int mode) const { if ((mode & EXIFMODE_DATE) == 0) return false; if ((mode & EXIFMODE_FORCE) != 0) return true; return true; } QMap> DB::ImageInfo::taggedAreas() const { return m_taggedAreas; } QRect DB::ImageInfo::areaForTag(QString category, QString tag) const { // QMap::value returns a default constructed value if the key is not found: return m_taggedAreas.value(category).value(tag); } -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE Map::GeoCoordinates DB::ImageInfo::coordinates() const { if (m_coordsIsSet) { return m_coordinates; } static const int EXIF_GPS_VERSIONID = 0; static const int EXIF_GPS_LATREF = 1; static const int EXIF_GPS_LAT = 2; static const int EXIF_GPS_LONREF = 3; static const int EXIF_GPS_LON = 4; static const int EXIF_GPS_ALTREF = 5; static const int EXIF_GPS_ALT = 6; static const QString S = QString::fromUtf8("S"); static const QString W = QString::fromUtf8("W"); static QList fields; if (fields.isEmpty()) { // the order here matters! we use the named int constants afterwards to refer to them: fields.append(new Exif::IntExifElement("Exif.GPSInfo.GPSVersionID")); // actually a byte value fields.append(new Exif::StringExifElement("Exif.GPSInfo.GPSLatitudeRef")); fields.append(new Exif::RationalExifElement("Exif.GPSInfo.GPSLatitude")); fields.append(new Exif::StringExifElement("Exif.GPSInfo.GPSLongitudeRef")); fields.append(new Exif::RationalExifElement("Exif.GPSInfo.GPSLongitude")); fields.append(new Exif::IntExifElement("Exif.GPSInfo.GPSAltitudeRef")); // actually a byte value fields.append(new Exif::RationalExifElement("Exif.GPSInfo.GPSAltitude")); } // read field values from database: bool foundIt = Exif::Database::instance()->readFields(m_fileName, fields); // if the Database query result doesn't contain exif GPS info (-> upgraded exifdb from DBVersion < 2), it is null // if the result is int 0, then there's no exif gps information in the image // otherwise we can proceed to parse the information if (foundIt && fields[EXIF_GPS_VERSIONID]->value().isNull()) { // update exif DB and repeat the search: Exif::Database::instance()->remove(fileName()); Exif::Database::instance()->add(fileName()); Exif::Database::instance()->readFields(m_fileName, fields); Q_ASSERT(!fields[EXIF_GPS_VERSIONID]->value().isNull()); } Map::GeoCoordinates coords; // gps info set? // don't use the versionid field here, because some cameras use 0 as its value if (foundIt && fields[EXIF_GPS_LAT]->value().toInt() != -1.0 && fields[EXIF_GPS_LON]->value().toInt() != -1.0) { // lat/lon/alt reference determines sign of float: double latr = (fields[EXIF_GPS_LATREF]->value().toString() == S) ? -1.0 : 1.0; double lat = fields[EXIF_GPS_LAT]->value().toFloat(); double lonr = (fields[EXIF_GPS_LONREF]->value().toString() == W) ? -1.0 : 1.0; double lon = fields[EXIF_GPS_LON]->value().toFloat(); double altr = (fields[EXIF_GPS_ALTREF]->value().toInt() == 1) ? -1.0 : 1.0; double alt = fields[EXIF_GPS_ALT]->value().toFloat(); if (lat != -1.0 && lon != -1.0) { coords.setLatLon(latr * lat, lonr * lon); if (alt != 0.0f) { coords.setAlt(altr * alt); } } } m_coordinates = coords; m_coordsIsSet = true; return m_coordinates; } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageInfo.h b/DB/ImageInfo.h index 3b69fb02..368caa95 100644 --- a/DB/ImageInfo.h +++ b/DB/ImageInfo.h @@ -1,247 +1,248 @@ /* Copyright (C) 2003-2019 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. */ #ifndef IMAGEINFO_H #define IMAGEINFO_H -#include "config-kpa-kgeomap.h" +#include "config-kpa-marble.h" #include "CategoryPtr.h" #include "ExifMode.h" #include "FileName.h" #include "ImageDate.h" +#include "Utilities/StringSet.h" #include "MD5.h" -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE #include #endif #include #include #include #include #include #include namespace Plugins { class ImageInfo; } namespace XMLDB { class Database; } namespace DB { enum PathType { RelativeToImageRoot, AbsolutePath }; enum RotationMode { RotateImageInfoAndAreas, RotateImageInfoOnly }; using Utilities::StringSet; class MemberMap; enum MediaType { Image = 0x01, Video = 0x02 }; const MediaType anyMediaType = MediaType(Image | Video); typedef unsigned int StackID; class ImageInfo : public QSharedData { public: ImageInfo(); explicit ImageInfo(const DB::FileName &fileName, MediaType type = Image, bool readExifInfo = true, bool storeExifInfo = true); ImageInfo(const DB::FileName &fileName, const QString &label, const QString &description, const ImageDate &date, int angle, const MD5 &md5sum, const QSize &size, MediaType type, short rating = -1, StackID stackId = 0, unsigned int stackOrder = 0); ImageInfo(const ImageInfo &other); FileName fileName() const; void setFileName(const DB::FileName &relativeFileName); void setLabel(const QString &); QString label() const; void setDescription(const QString &); QString description() const; void setDate(const ImageDate &); ImageDate date() const; ImageDate &date(); void readExif(const DB::FileName &fullPath, DB::ExifMode mode); void rotate(int degrees, RotationMode mode = RotateImageInfoAndAreas); int angle() const; void setAngle(int angle); short rating() const; void setRating(short rating); bool isStacked() const { return m_stackId != 0; } StackID stackId() const; unsigned int stackOrder() const; void setStackOrder(const unsigned int stackOrder); void setVideoLength(int seconds); int videoLength() const; void setCategoryInfo(const QString &key, const StringSet &value); void addCategoryInfo(const QString &category, const StringSet &values); /** * Enable a tag within a category for this image. * Optionally, the tag's position can be given (for positionable categories). * @param category the category name * @param value the tag name * @param area the image region that the tag applies to. */ void addCategoryInfo(const QString &category, const QString &value, const QRect &area = QRect()); void clearAllCategoryInfo(); void removeCategoryInfo(const QString &category, const StringSet &values); void removeCategoryInfo(const QString &category, const QString &value); /** * Set the tagged areas for the image. * It is assumed that the positioned tags have already been set to the ImageInfo * using one of the functions setCategoryInfo or addCategoryInfo. * * @param category the category name. * @param positionedTags a mapping of tag names to image areas. */ void setPositionedTags(const QString &category, const QMap &positionedTags); bool hasCategoryInfo(const QString &key, const QString &value) const; bool hasCategoryInfo(const QString &key, const StringSet &values) const; QStringList availableCategories() const; StringSet itemsOfCategory(const QString &category) const; void renameItem(const QString &key, const QString &oldValue, const QString &newValue); void renameCategory(const QString &oldName, const QString &newName); bool operator!=(const ImageInfo &other) const; bool operator==(const ImageInfo &other) const; ImageInfo &operator=(const ImageInfo &other); static bool imageOnDisk(const DB::FileName &fileName); const MD5 &MD5Sum() const { return m_md5sum; } void setMD5Sum(const MD5 &sum, bool storeEXIF = true); void setLocked(bool); bool isLocked() const; bool isNull() const { return m_null; } QSize size() const; void setSize(const QSize &size); MediaType mediaType() const; void setMediaType(MediaType type) { if (type != m_type) m_dirty = true; m_type = type; } bool isVideo() const; void createFolderCategoryItem(DB::CategoryPtr, DB::MemberMap &memberMap); void copyExtraData(const ImageInfo &from, bool copyAngle = true); void removeExtraData(); /** * Merge another ImageInfo into this one. * The other ImageInfo is not altered in any way or removed. */ void merge(const ImageInfo &other); QMap> taggedAreas() const; /** * Return the area associated with a tag. * @param category the category name * @param tag the tag name * @return the associated area, or QRect() if no association exists. */ QRect areaForTag(QString category, QString tag) const; void setIsMatched(bool isMatched); bool isMatched() const; void setMatchGeneration(int matchGeneration); int matchGeneration() const; -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE Map::GeoCoordinates coordinates() const; #endif protected: void setIsNull(bool b) { m_null = b; } bool isDirty() const { return m_dirty; } void setIsDirty(bool b) { m_dirty = b; } bool updateDateInformation(int mode) const; void setStackId(const StackID stackId); friend class XMLDB::Database; private: DB::FileName m_fileName; QString m_label; QString m_description; ImageDate m_date; QMap m_categoryInfomation; QMap> m_taggedAreas; int m_angle; enum OnDisk { YesOnDisk, NoNotOnDisk, Unchecked }; mutable OnDisk m_imageOnDisk; MD5 m_md5sum; bool m_null; QSize m_size; MediaType m_type; short m_rating; StackID m_stackId; unsigned int m_stackOrder; int m_videoLength; bool m_isMatched; int m_matchGeneration; -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE mutable Map::GeoCoordinates m_coordinates; mutable bool m_coordsIsSet = false; #endif // Cache information bool m_locked; // Will be set to true after every change bool m_dirty; }; } #endif /* IMAGEINFO_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageSearchInfo.cpp b/DB/ImageSearchInfo.cpp index 247e35db..bf4fac5d 100644 --- a/DB/ImageSearchInfo.cpp +++ b/DB/ImageSearchInfo.cpp @@ -1,664 +1,665 @@ /* Copyright (C) 2003-2019 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 "ImageSearchInfo.h" #include "AndCategoryMatcher.h" #include "CategoryMatcher.h" #include "ContainerCategoryMatcher.h" #include "ExactCategoryMatcher.h" #include "ImageDB.h" #include "Logging.h" #include "NegationCategoryMatcher.h" #include "NoTagCategoryMatcher.h" #include "OrCategoryMatcher.h" #include "ValueCategoryMatcher.h" #include #include #include #include #include #include #include using namespace DB; static QAtomicInt s_matchGeneration; static int nextGeneration() { return s_matchGeneration++; } ImageSearchInfo::ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description) : m_date(date) , m_label(label) , m_description(description) , m_rating(-1) , m_megapixel(0) , m_max_megapixel(0) , m_ratingSearchMode(0) , m_searchRAW(false) , m_isNull(false) , m_isCacheable(true) , m_compiled(false) , m_matchGeneration(nextGeneration()) { } ImageSearchInfo::ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description, const QString &fnPattern) : m_date(date) , m_label(label) , m_description(description) , m_fnPattern(fnPattern) , m_rating(-1) , m_megapixel(0) , m_max_megapixel(0) , m_ratingSearchMode(0) , m_searchRAW(false) , m_isNull(false) , m_isCacheable(true) , m_compiled(false) , m_matchGeneration(nextGeneration()) { } QString ImageSearchInfo::label() const { return m_label; } QRegExp ImageSearchInfo::fnPattern() const { return m_fnPattern; } QString ImageSearchInfo::description() const { return m_description; } void ImageSearchInfo::checkIfNull() { if (m_compiled || isNull()) return; if (m_date.isNull() && m_label.isEmpty() && m_description.isEmpty() && m_rating == -1 && m_megapixel == 0 && m_exifSearchInfo.isNull() && m_categoryMatchText.isEmpty() #ifdef HAVE_KGEOMAP && !m_regionSelection.first.hasCoordinates() && !m_regionSelection.second.hasCoordinates() #endif ) { m_isNull = true; } } ImageSearchInfo::ImageSearchInfo() : m_rating(-1) , m_megapixel(0) , m_max_megapixel(0) , m_ratingSearchMode(0) , m_searchRAW(false) , m_isNull(true) , m_isCacheable(true) , m_compiled(false) , m_matchGeneration(nextGeneration()) { } bool ImageSearchInfo::isNull() const { return m_isNull; } bool ImageSearchInfo::isCacheable() const { return m_isCacheable; } void ImageSearchInfo::setCacheable(bool cacheable) { m_isCacheable = cacheable; } bool ImageSearchInfo::match(ImageInfoPtr info) const { if (m_isNull) return true; if (m_isCacheable && info->matchGeneration() == m_matchGeneration) return info->isMatched(); bool ok = doMatch(info); if (m_isCacheable) { info->setMatchGeneration(m_matchGeneration); info->setIsMatched(ok); } return ok; } bool ImageSearchInfo::doMatch(ImageInfoPtr info) const { if (!m_compiled) compile(); // -------------------------------------------------- Rating //ok = ok && (_rating == -1 ) || ( _rating == info->rating() ); if (m_rating != -1) { switch (m_ratingSearchMode) { case 1: // Image rating at least selected if (m_rating > info->rating()) return false; break; case 2: // Image rating less than selected if (m_rating < info->rating()) return false; break; case 3: // Image rating not equal if (m_rating == info->rating()) return false; break; default: if (m_rating != info->rating()) return false; break; } } // -------------------------------------------------- Resolution if (m_megapixel && (m_megapixel * 1000000 > info->size().width() * info->size().height())) return false; if (m_max_megapixel && m_max_megapixel < m_megapixel && (m_max_megapixel * 1000000 < info->size().width() * info->size().height())) return false; // -------------------------------------------------- Date QDateTime actualStart = info->date().start(); QDateTime actualEnd = info->date().end(); if (m_date.start().isValid()) { if (actualEnd < m_date.start() || (m_date.end().isValid() && actualStart > m_date.end())) return false; } else if (m_date.end().isValid() && actualStart > m_date.end()) { return false; } // -------------------------------------------------- Label if (m_label.isEmpty() && info->label().indexOf(m_label) == -1) return false; // -------------------------------------------------- RAW if (m_searchRAW && !ImageManager::RAWImageDecoder::isRAW(info->fileName())) return false; -#ifdef HAVE_KGEOMAP + +#ifdef HAVE_MARBLE // Search for GPS Position if (m_usingRegionSelection) { if (!info->coordinates().hasCoordinates()) return false; float infoLat = info->coordinates().lat(); if (m_regionSelectionMinLat > infoLat || m_regionSelectionMaxLat < infoLat) return false; float infoLon = info->coordinates().lon(); if (m_regionSelectionMinLon > infoLon || m_regionSelectionMaxLon < infoLon) return false; } #endif // -------------------------------------------------- File name pattern if (!m_fnPattern.isEmpty() && m_fnPattern.indexIn(info->fileName().relative()) == -1) return false; // -------------------------------------------------- Options // alreadyMatched map is used to make it possible to search for // Jesper & None QMap alreadyMatched; for (CategoryMatcher *optionMatcher : m_categoryMatchers) { if (!optionMatcher->eval(info, alreadyMatched)) return false; } // -------------------------------------------------- Text if (!m_description.isEmpty()) { const QString &txt(info->description()); QStringList list = m_description.split(QChar::fromLatin1(' '), QString::SkipEmptyParts); Q_FOREACH (const QString &word, list) { if (txt.indexOf(word, 0, Qt::CaseInsensitive) == -1) return false; } } // -------------------------------------------------- EXIF if (!m_exifSearchInfo.matches(info->fileName())) return false; return true; } QString ImageSearchInfo::categoryMatchText(const QString &name) const { return m_categoryMatchText[name]; } void ImageSearchInfo::setCategoryMatchText(const QString &name, const QString &value) { if (value.isEmpty()) { m_categoryMatchText.remove(name); } else { m_categoryMatchText[name] = value; } m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::addAnd(const QString &category, const QString &value) { // Escape literal "&"s in value by doubling it QString escapedValue = value; escapedValue.replace(QString::fromUtf8("&"), QString::fromUtf8("&&")); QString val = categoryMatchText(category); if (!val.isEmpty()) val += QString::fromLatin1(" & ") + escapedValue; else val = escapedValue; setCategoryMatchText(category, val); m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setRating(short rating) { m_rating = rating; m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setMegaPixel(short megapixel) { m_megapixel = megapixel; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setMaxMegaPixel(short max_megapixel) { m_max_megapixel = max_megapixel; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setSearchMode(int index) { m_ratingSearchMode = index; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setSearchRAW(bool searchRAW) { m_searchRAW = searchRAW; m_matchGeneration = nextGeneration(); } QString ImageSearchInfo::toString() const { QString res; bool first = true; for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { if (!it.value().isEmpty()) { if (first) first = false; else res += QString::fromLatin1(" / "); QString txt = it.value(); if (txt == ImageDB::NONE()) txt = i18nc("As in No persons, no locations etc. I do realize that translators may have problem with this, " "but I need some how to indicate the category, and users may create their own categories, so this is " "the best I can do - Jesper.", "No %1", it.key()); if (txt.contains(QString::fromLatin1("|"))) txt.replace(QString::fromLatin1("&"), QString::fromLatin1(" %1 ").arg(i18n("and"))); else txt.replace(QString::fromLatin1("&"), QString::fromLatin1(" / ")); txt.replace(QString::fromLatin1("|"), QString::fromLatin1(" %1 ").arg(i18n("or"))); txt.replace(QString::fromLatin1("!"), QString::fromLatin1(" %1 ").arg(i18n("not"))); txt.replace(ImageDB::NONE(), i18nc("As in no other persons, or no other locations. " "I do realize that translators may have problem with this, " "but I need some how to indicate the category, and users may create their own categories, so this is " "the best I can do - Jesper.", "No other %1", it.key())); res += txt.simplified(); } } return res; } void ImageSearchInfo::debug() { for (QMap::Iterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { qCDebug(DBCategoryMatcherLog) << it.key() << ", " << it.value(); } } // PENDING(blackie) move this into the Options class instead of having it here. void ImageSearchInfo::saveLock() const { KConfigGroup config = KSharedConfig::openConfig()->group(Settings::SettingsData::instance()->groupForDatabase("Privacy Settings")); config.writeEntry(QString::fromLatin1("label"), m_label); config.writeEntry(QString::fromLatin1("description"), m_description); config.writeEntry(QString::fromLatin1("categories"), m_categoryMatchText.keys()); for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { config.writeEntry(it.key(), it.value()); } config.sync(); } ImageSearchInfo ImageSearchInfo::loadLock() { KConfigGroup config = KSharedConfig::openConfig()->group(Settings::SettingsData::instance()->groupForDatabase("Privacy Settings")); ImageSearchInfo info; info.m_label = config.readEntry("label"); info.m_description = config.readEntry("description"); QStringList categories = config.readEntry(QString::fromLatin1("categories"), QStringList()); for (QStringList::ConstIterator it = categories.constBegin(); it != categories.constEnd(); ++it) { info.setCategoryMatchText(*it, config.readEntry(*it, QString())); } return info; } ImageSearchInfo::ImageSearchInfo(const ImageSearchInfo &other) { m_date = other.m_date; m_categoryMatchText = other.m_categoryMatchText; m_label = other.m_label; m_description = other.m_description; m_fnPattern = other.m_fnPattern; m_isNull = other.m_isNull; m_compiled = false; m_rating = other.m_rating; m_ratingSearchMode = other.m_ratingSearchMode; m_megapixel = other.m_megapixel; m_max_megapixel = other.m_max_megapixel; m_searchRAW = other.m_searchRAW; m_exifSearchInfo = other.m_exifSearchInfo; m_matchGeneration = other.m_matchGeneration; m_isCacheable = other.m_isCacheable; -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE m_regionSelection = other.m_regionSelection; #endif } void ImageSearchInfo::compile() const { m_exifSearchInfo.search(); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE // Prepare Search for GPS Position m_usingRegionSelection = m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates(); if (m_usingRegionSelection) { using std::max; using std::min; m_regionSelectionMinLat = min(m_regionSelection.first.lat(), m_regionSelection.second.lat()); m_regionSelectionMaxLat = max(m_regionSelection.first.lat(), m_regionSelection.second.lat()); m_regionSelectionMinLon = min(m_regionSelection.first.lon(), m_regionSelection.second.lon()); m_regionSelectionMaxLon = max(m_regionSelection.first.lon(), m_regionSelection.second.lon()); } #endif deleteMatchers(); for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { QString category = it.key(); QString matchText = it.value(); QStringList orParts = matchText.split(QString::fromLatin1("|"), QString::SkipEmptyParts); DB::ContainerCategoryMatcher *orMatcher = new DB::OrCategoryMatcher; Q_FOREACH (QString orPart, orParts) { // Split by " & ", not only by "&", so that the doubled "&"s won't be used as a split point QStringList andParts = orPart.split(QString::fromLatin1(" & "), QString::SkipEmptyParts); DB::ContainerCategoryMatcher *andMatcher; bool exactMatch = false; bool negate = false; andMatcher = new DB::AndCategoryMatcher; Q_FOREACH (QString str, andParts) { static QRegExp regexp(QString::fromLatin1("^\\s*!\\s*(.*)$")); if (regexp.exactMatch(str)) { // str is preceded with NOT negate = true; str = regexp.cap(1); } str = str.trimmed(); CategoryMatcher *valueMatcher; if (str == ImageDB::NONE()) { // mark AND-group as containing a "No other" condition exactMatch = true; continue; } else { valueMatcher = new DB::ValueCategoryMatcher(category, str); if (negate) valueMatcher = new DB::NegationCategoryMatcher(valueMatcher); } andMatcher->addElement(valueMatcher); } if (exactMatch) { DB::CategoryMatcher *exactMatcher = nullptr; // if andMatcher has exactMatch set, but no CategoryMatchers, then // matching "category / None" is what we want: if (andMatcher->mp_elements.count() == 0) { exactMatcher = new DB::NoTagCategoryMatcher(category); } else { ExactCategoryMatcher *noOtherMatcher = new ExactCategoryMatcher(category); if (andMatcher->mp_elements.count() == 1) noOtherMatcher->setMatcher(andMatcher->mp_elements[0]); else noOtherMatcher->setMatcher(andMatcher); exactMatcher = noOtherMatcher; } if (negate) exactMatcher = new DB::NegationCategoryMatcher(exactMatcher); orMatcher->addElement(exactMatcher); } else if (andMatcher->mp_elements.count() == 1) orMatcher->addElement(andMatcher->mp_elements[0]); else if (andMatcher->mp_elements.count() > 1) orMatcher->addElement(andMatcher); } CategoryMatcher *matcher = nullptr; if (orMatcher->mp_elements.count() == 1) matcher = orMatcher->mp_elements[0]; else if (orMatcher->mp_elements.count() > 1) matcher = orMatcher; if (matcher) { m_categoryMatchers.append(matcher); if (DBCategoryMatcherLog().isDebugEnabled()) { qCDebug(DBCategoryMatcherLog) << "Matching text '" << matchText << "' in category " << category << ":"; matcher->debug(0); qCDebug(DBCategoryMatcherLog) << "."; } } } m_compiled = true; } ImageSearchInfo::~ImageSearchInfo() { deleteMatchers(); } void ImageSearchInfo::debugMatcher() const { if (!m_compiled) compile(); qCDebug(DBCategoryMatcherLog, "And:"); for (CategoryMatcher *optionMatcher : m_categoryMatchers) { optionMatcher->debug(1); } } QList> ImageSearchInfo::query() const { if (!m_compiled) compile(); // Combine _optionMachers to one list of lists in Disjunctive // Normal Form and return it. QList::Iterator it = m_categoryMatchers.begin(); QList> result; if (it == m_categoryMatchers.end()) return result; result = convertMatcher(*it); ++it; for (; it != m_categoryMatchers.end(); ++it) { QList> current = convertMatcher(*it); QList> oldResult = result; result.clear(); for (QList resultIt : oldResult) { for (QList currentIt : current) { QList tmp; tmp += resultIt; tmp += currentIt; result.append(tmp); } } } return result; } Utilities::StringSet ImageSearchInfo::findAlreadyMatched(const QString &group) const { Utilities::StringSet result; QString str = categoryMatchText(group); if (str.contains(QString::fromLatin1("|"))) { return result; } QStringList list = str.split(QString::fromLatin1("&"), QString::SkipEmptyParts); Q_FOREACH (QString part, list) { QString nm = part.trimmed(); if (!nm.contains(QString::fromLatin1("!"))) result.insert(nm); } return result; } void ImageSearchInfo::deleteMatchers() const { qDeleteAll(m_categoryMatchers); m_categoryMatchers.clear(); } QList ImageSearchInfo::extractAndMatcher(CategoryMatcher *matcher) const { QList result; AndCategoryMatcher *andMatcher; SimpleCategoryMatcher *simpleMatcher; if ((andMatcher = dynamic_cast(matcher))) { for (CategoryMatcher *child : andMatcher->mp_elements) { SimpleCategoryMatcher *simpleMatcher = dynamic_cast(child); Q_ASSERT(simpleMatcher); result.append(simpleMatcher); } } else if ((simpleMatcher = dynamic_cast(matcher))) result.append(simpleMatcher); else Q_ASSERT(false); return result; } /** Convert matcher to Disjunctive Normal Form. * * @return OR-list of AND-lists. (e.g. OR(AND(a,b),AND(c,d))) */ QList> ImageSearchInfo::convertMatcher(CategoryMatcher *item) const { QList> result; OrCategoryMatcher *orMacther; if ((orMacther = dynamic_cast(item))) { for (CategoryMatcher *child : orMacther->mp_elements) { result.append(extractAndMatcher(child)); } } else result.append(extractAndMatcher(item)); return result; } short ImageSearchInfo::rating() const { return m_rating; } ImageDate ImageSearchInfo::date() const { return m_date; } void ImageSearchInfo::addExifSearchInfo(const Exif::SearchInfo info) { m_exifSearchInfo = info; m_isNull = false; } void DB::ImageSearchInfo::renameCategory(const QString &oldName, const QString &newName) { m_categoryMatchText[newName] = m_categoryMatchText[oldName]; m_categoryMatchText.remove(oldName); m_compiled = false; } -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE Map::GeoCoordinates::Pair ImageSearchInfo::regionSelection() const { return m_regionSelection; } void ImageSearchInfo::setRegionSelection(const Map::GeoCoordinates::Pair& actRegionSelection) { m_regionSelection = actRegionSelection; m_compiled = false; if (m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates()) { m_isNull = false; } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageSearchInfo.h b/DB/ImageSearchInfo.h index 31a2db0f..537a8d76 100644 --- a/DB/ImageSearchInfo.h +++ b/DB/ImageSearchInfo.h @@ -1,146 +1,146 @@ /* Copyright (C) 2003-2019 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. */ #ifndef IMAGESEARCHINFO_H #define IMAGESEARCHINFO_H -#include +#include #include "ImageDate.h" #include "ImageInfoPtr.h" #include -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE #include #endif #include #include #include namespace DB { class SimpleCategoryMatcher; class ImageInfo; class CategoryMatcher; class ImageSearchInfo { public: ImageSearchInfo(); ~ImageSearchInfo(); ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description); ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description, const QString &fnPattern); ImageSearchInfo(const ImageSearchInfo &other); ImageDate date() const; QString categoryMatchText(const QString &name) const; void setCategoryMatchText(const QString &name, const QString &value); void renameCategory(const QString &oldName, const QString &newName); QString label() const; QRegExp fnPattern() const; QString description() const; /** * @brief checkIfNull evaluates whether the filter is indeed empty and * sets isNull() to \c true if that is the case. * You only need to call this if you re-use an existing ImageSearchInfo * and set/reset search parameters. * @see ThumbnailView::toggleRatingFilter */ void checkIfNull(); bool isNull() const; bool match(ImageInfoPtr) const; QList> query() const; void addAnd(const QString &category, const QString &value); short rating() const; void setRating(short rating); QString toString() const; void setMegaPixel(short megapixel); void setMaxMegaPixel(short maxmegapixel); void setSearchRAW(bool m_searchRAW); void setSearchMode(int index); void saveLock() const; static ImageSearchInfo loadLock(); void debug(); void debugMatcher() const; Utilities::StringSet findAlreadyMatched(const QString &group) const; void addExifSearchInfo(const Exif::SearchInfo info); // By default, an ImageSearchInfo is cacheable, but only one search // is cached per image. For a search that's only going to be // performed once, don't try to cache the result. void setCacheable(bool cacheable); bool isCacheable() const; -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE Map::GeoCoordinates::Pair regionSelection() const; void setRegionSelection(const Map::GeoCoordinates::Pair &actRegionSelection); #endif protected: void compile() const; void deleteMatchers() const; QList extractAndMatcher(CategoryMatcher *andMatcher) const; QList> convertMatcher(CategoryMatcher *) const; private: ImageDate m_date; QMap m_categoryMatchText; QString m_label; QString m_description; QRegExp m_fnPattern; short m_rating; short m_megapixel; short m_max_megapixel; int m_ratingSearchMode; bool m_searchRAW; bool m_isNull; bool m_isCacheable; mutable bool m_compiled; mutable QList m_categoryMatchers; Exif::SearchInfo m_exifSearchInfo; int m_matchGeneration; bool doMatch(ImageInfoPtr) const; -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE Map::GeoCoordinates::Pair m_regionSelection; mutable bool m_usingRegionSelection = false; mutable float m_regionSelectionMinLat; mutable float m_regionSelectionMaxLat; mutable float m_regionSelectionMinLon; mutable float m_regionSelectionMaxLon; #endif // When adding new instance variable, please notice that this class as an explicit written copy constructor. }; } #endif /* IMAGESEARCHINFO_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/FeatureDialog.cpp b/MainWindow/FeatureDialog.cpp index db01b3b1..ba4c9ed1 100644 --- a/MainWindow/FeatureDialog.cpp +++ b/MainWindow/FeatureDialog.cpp @@ -1,220 +1,220 @@ /* Copyright (C) 2003-2019 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 +#include #include "FeatureDialog.h" #include #include #include #include #include #include #include #include #include #include -#include -#include #include using namespace MainWindow; FeatureDialog::FeatureDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Feature Status")); QTextBrowser *browser = new QTextBrowser(this); QString text = i18n("

Overview

" "

Below you may see the list of compile- and runtime features KPhotoAlbum has, and their status:

" "%1", featureString()); text += i18n("

What can I do if I miss a feature?

" "

If you compiled KPhotoAlbum yourself, then please review the sections below to learn what to install " "to get the feature in question. If on the other hand you installed KPhotoAlbum from a binary package, please tell " "whoever made the package about this defect, eventually including the information from the section below.

" "

In case you are missing a feature and you did not compile KPhotoAlbum yourself, please do consider doing so. " "It really is not that hard. If you need help compiling KPhotoAlbum, feel free to ask on the " "KPhotoAlbum mailing list

" "

The steps to compile KPhotoAlbum can be seen on " "the KPhotoAlbum home page. If you have never compiled a KDE application, then please ensure that " "you have the developer packages installed, in most distributions they go under names like kdelibs-devel

"); text += i18n("

Plug-ins support

" "

KPhotoAlbum has a plug-in system with lots of extensions. You may among other things find plug-ins for:" "

    " "
  • Writing images to cds or dvd's
  • " "
  • Adjusting timestamps on your images
  • " "
  • Making a calendar featuring your images
  • " "
  • Uploading your images to flickr
  • " "
  • Upload your images to facebook
  • " "

" "

The plug-in library is called KIPI, and may be downloaded from the " "KDE Userbase Wiki

"); text += i18n("

SQLite database support

" "

KPhotoAlbum allows you to search using a certain number of Exif tags. For this KPhotoAlbum " "needs an SQLite database. " "In addition the Qt package for SQLite (e.g. qt-sql-sqlite) must be installed.

"); - text += i18n( "

Map view for geotagged images

" - "

If KPhotoAlbum has been built with support for libkgeomap, " - "KPhotoAlbum can show images with GPS information on a map." - "

" ); + text += i18n("

Map view for geotagged images

" + "

If KPhotoAlbum has been built with support for Marble, " + "KPhotoAlbum can show images with GPS information on a map." + "

"); text += i18n("

Video support

" "

KPhotoAlbum relies on Qt's Phonon architecture for displaying videos; this in turn relies on GStreamer. " "If this feature is not enabled for you, have a look at the " "KPhotoAlbum wiki article on video support.

"); QStringList mimeTypes = supportedVideoMimeTypes(); mimeTypes.sort(); if (mimeTypes.isEmpty()) text += i18n("

No video mime types found, which indicates that either Qt was compiled without phonon support, or there were missing codecs

"); else text += i18n("

Phonon is capable of playing movies of these mime types:

  • %1

", mimeTypes.join(QString::fromLatin1("
  • "))); text += i18n("

    Video thumbnail support

    " "

    KPhotoAlbum can use ffmpeg to extract thumbnails from videos. These thumbnails are used to preview " "videos in the thumbnail viewer.

    "); text += i18n("

    Video metadata support

    " "

    KPhotoAlbum can use ffprobe to extract length information from videos." "

    " "

    Correct length information is necessary for correct rendering of video thumbnails.

    "); browser->setText(text); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(browser); this->setLayout(layout); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); layout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); } QSize FeatureDialog::sizeHint() const { return QSize(800, 600); } bool MainWindow::FeatureDialog::hasKIPISupport() { #ifdef HASKIPI return true; #else return false; #endif } bool MainWindow::FeatureDialog::hasEXIV2DBSupport() { return Exif::Database::isAvailable(); } bool MainWindow::FeatureDialog::hasGeoMapSupport() { -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE return true; #else return false; #endif } QString FeatureDialog::ffmpegBinary() { QString ffmpeg = QStandardPaths::findExecutable(QString::fromLatin1("ffmpeg")); return ffmpeg; } QString FeatureDialog::ffprobeBinary() { QString ffprobe = QStandardPaths::findExecutable(QString::fromLatin1("ffprobe")); return ffprobe; } bool FeatureDialog::hasVideoThumbnailer() { return !ffmpegBinary().isEmpty(); } bool FeatureDialog::hasVideoProber() { return !ffprobeBinary().isEmpty(); } bool MainWindow::FeatureDialog::hasAllFeaturesAvailable() { // Only answer those that are compile time tests, otherwise we will pay a penalty each time we start up. return hasKIPISupport() && hasEXIV2DBSupport() && hasGeoMapSupport() && hasVideoThumbnailer() && hasVideoProber(); } struct Data { Data() {} Data(const QString &title, const QString tag, bool featureFound) : title(title) , tag(tag) , featureFound(featureFound) { } QString title; QString tag; bool featureFound; }; QString MainWindow::FeatureDialog::featureString() { QList features; features << Data(i18n("Plug-ins available"), QString::fromLatin1("#kipi"), hasKIPISupport()); features << Data(i18n("SQLite database support (used for Exif searches)"), QString::fromLatin1("#database"), hasEXIV2DBSupport()); features << Data(i18n("Map view for geotagged images."), QString::fromLatin1("#geomap"), hasGeoMapSupport()); features << Data(i18n("Video support"), QString::fromLatin1("#video"), !supportedVideoMimeTypes().isEmpty()); features << Data(i18n("Video thumbnail support"), QString::fromLatin1("#videoPreview"), hasVideoThumbnailer()); features << Data(i18n("Video metadata support"), QString::fromLatin1("#videoInfo"), hasVideoProber()); QString result = QString::fromLatin1("

    "); const QString red = QString::fromLatin1("%1"); const QString yes = i18nc("Feature available", "Yes"); const QString no = red.arg(i18nc("Feature not available", "No")); const QString formatString = QString::fromLatin1(""); for (QList::ConstIterator featureIt = features.constBegin(); featureIt != features.constEnd(); ++featureIt) { result += formatString .arg((*featureIt).tag) .arg((*featureIt).title) .arg((*featureIt).featureFound ? yes : no); } result += QString::fromLatin1("
    %2%3

    "); return result; } QStringList MainWindow::FeatureDialog::supportedVideoMimeTypes() { return Phonon::BackendCapabilities::availableMimeTypes(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Window.cpp b/MainWindow/Window.cpp index 1b2f0201..6c1062fa 100644 --- a/MainWindow/Window.cpp +++ b/MainWindow/Window.cpp @@ -1,1997 +1,1997 @@ /* Copyright (C) 2003-2019 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 "Window.h" #include #include #ifdef HAVE_STDLIB_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for #if KIO_VERSION... #include #ifdef HASKIPI #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HASKIPI #include #endif #ifdef KF5Purpose_FOUND #include #endif #include "AutoStackImages.h" #include "BreadcrumbViewer.h" #include "CopyPopup.h" #include "DeleteDialog.h" #include "DirtyIndicator.h" #include "DuplicateMerger/DuplicateMerger.h" #include "ExternalPopup.h" #include "FeatureDialog.h" #include "ImageCounter.h" #include "InvalidDateFinder.h" #include "Logging.h" #include "Options.h" #include "SearchBar.h" #include "SplashScreen.h" #include "StatisticsDialog.h" #include "StatusBar.h" #include "TokenEditor.h" #include "UpdateVideoThumbnail.h" #include "WelcomeDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace DB; MainWindow::Window *MainWindow::Window::s_instance = nullptr; MainWindow::Window::Window(QWidget *parent) : KXmlGuiWindow(parent) , m_annotationDialog(nullptr) , m_deleteDialog(nullptr) , m_htmlDialog(nullptr) , m_tokenEditor(nullptr) { -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE m_positionBrowser = 0; #endif qCDebug(MainWindowLog) << "Using icon theme: " << QIcon::themeName(); qCDebug(MainWindowLog) << "Icon search paths: " << QIcon::themeSearchPaths(); QElapsedTimer timer; timer.start(); SplashScreen::instance()->message(i18n("Loading Database")); s_instance = this; bool gotConfigFile = load(); if (!gotConfigFile) throw 0; qCInfo(TimingLog) << "MainWindow: Loading Database: " << timer.restart() << "ms."; SplashScreen::instance()->message(i18n("Loading Main Window")); QWidget *top = new QWidget(this); QVBoxLayout *lay = new QVBoxLayout(top); lay->setSpacing(2); lay->setContentsMargins(2, 2, 2, 2); setCentralWidget(top); m_stack = new QStackedWidget(top); lay->addWidget(m_stack, 1); m_dateBar = new DateBar::DateBarWidget(top); lay->addWidget(m_dateBar); m_dateBarLine = new QFrame(top); m_dateBarLine->setFrameStyle(QFrame::HLine | QFrame::Plain); m_dateBarLine->setLineWidth(0); m_dateBarLine->setMidLineWidth(0); QPalette pal = m_dateBarLine->palette(); pal.setColor(QPalette::WindowText, QColor("#c4c1bd")); m_dateBarLine->setPalette(pal); lay->addWidget(m_dateBarLine); setHistogramVisibilty(Settings::SettingsData::instance()->showHistogram()); m_browser = new Browser::BrowserWidget(m_stack); m_thumbnailView = new ThumbnailView::ThumbnailFacade(); m_stack->addWidget(m_browser); m_stack->addWidget(m_thumbnailView->gui()); m_stack->setCurrentWidget(m_browser); m_settingsDialog = nullptr; qCInfo(TimingLog) << "MainWindow: Loading MainWindow: " << timer.restart() << "ms."; setupMenuBar(); qCInfo(TimingLog) << "MainWindow: setupMenuBar: " << timer.restart() << "ms."; createSearchBar(); qCInfo(TimingLog) << "MainWindow: createSearchBar: " << timer.restart() << "ms."; setupStatusBar(); qCInfo(TimingLog) << "MainWindow: setupStatusBar: " << timer.restart() << "ms."; // Misc m_autoSaveTimer = new QTimer(this); connect(m_autoSaveTimer, &QTimer::timeout, this, &Window::slotAutoSave); startAutoSaveTimer(); connect(m_browser, &Browser::BrowserWidget::showingOverview, this, &Window::showBrowser); connect(m_browser, SIGNAL(pathChanged(Browser::BreadcrumbList)), m_statusBar->mp_pathIndicator, SLOT(setBreadcrumbs(Browser::BreadcrumbList))); connect(m_statusBar->mp_pathIndicator, SIGNAL(widenToBreadcrumb(Browser::Breadcrumb)), m_browser, SLOT(widenToBreadcrumb(Browser::Breadcrumb))); connect(m_browser, SIGNAL(pathChanged(Browser::BreadcrumbList)), this, SLOT(updateDateBar(Browser::BreadcrumbList))); connect(m_dateBar, &DateBar::DateBarWidget::dateSelected, m_thumbnailView, &ThumbnailView::ThumbnailFacade::gotoDate); connect(m_dateBar, &DateBar::DateBarWidget::toolTipInfo, this, &Window::showDateBarTip); connect(Settings::SettingsData::instance(), SIGNAL(histogramSizeChanged(QSize)), m_dateBar, SLOT(setHistogramBarSize(QSize))); connect(Settings::SettingsData::instance(), SIGNAL(actualThumbnailSizeChanged(int)), this, SLOT(slotThumbnailSizeChanged())); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeChange, this, &Window::setDateRange); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeCleared, this, &Window::clearDateRange); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::currentDateChanged, m_dateBar, &DateBar::DateBarWidget::setDate); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::showImage, this, &Window::showImage); connect(m_thumbnailView, SIGNAL(showSelection()), this, SLOT(slotView())); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::fileIdUnderCursorChanged, this, &Window::slotSetFileName); connect(DB::ImageDB::instance(), SIGNAL(totalChanged(uint)), this, SLOT(updateDateBar())); connect(DB::ImageDB::instance()->categoryCollection(), SIGNAL(categoryCollectionChanged()), this, SLOT(slotOptionGroupChanged())); connect(m_browser, SIGNAL(imageCount(uint)), m_statusBar->mp_partial, SLOT(showBrowserMatches(uint))); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, this, &Window::updateContextMenuFromSelectionSize); checkIfVideoThumbnailerIsInstalled(); executeStartupActions(); qCInfo(TimingLog) << "MainWindow: executeStartupActions " << timer.restart() << "ms."; QTimer::singleShot(0, this, SLOT(delayedInit())); updateContextMenuFromSelectionSize(0); // Automatically save toolbar settings setAutoSaveSettings(); qCInfo(TimingLog) << "MainWindow: misc setup time: " << timer.restart() << "ms."; } MainWindow::Window::~Window() { DB::ImageDB::deleteInstance(); ImageManager::ThumbnailCache::deleteInstance(); Exif::Database::deleteInstance(); } void MainWindow::Window::delayedInit() { QElapsedTimer timer; timer.start(); SplashScreen *splash = SplashScreen::instance(); setupPluginMenu(); qCInfo(TimingLog) << "MainWindow: setupPluginMenu: " << timer.restart() << "ms."; if (Settings::SettingsData::instance()->searchForImagesOnStart() || Options::the()->searchForImagesOnStart()) { splash->message(i18n("Searching for New Files")); qApp->processEvents(); DB::ImageDB::instance()->slotRescan(); qCInfo(TimingLog) << "MainWindow: Search for New Files: " << timer.restart() << "ms."; } if (!Settings::SettingsData::instance()->delayLoadingPlugins()) { splash->message(i18n("Loading Plug-ins")); loadKipiPlugins(); qCInfo(TimingLog) << "MainWindow: Loading Plug-ins: " << timer.restart() << "ms."; } splash->done(); show(); updateDateBar(); qCInfo(TimingLog) << "MainWindow: MainWindow.show():" << timer.restart() << "ms."; QUrl importUrl = Options::the()->importFile(); if (importUrl.isValid()) { // I need to do this in delayed init to get the import window on top of the normal window ImportExport::Import::imageImport(importUrl); qCInfo(TimingLog) << "MainWindow: imageImport:" << timer.restart() << "ms."; } else { // I need to postpone this otherwise the tip dialog will not get focus on start up KTipDialog::showTip(this); } Exif::Database::instance(); // Load the database qCInfo(TimingLog) << "MainWindow: Loading Exif DB:" << timer.restart() << "ms."; if (!Options::the()->listen().isNull()) RemoteControl::RemoteInterface::instance().listen(Options::the()->listen()); else if (Settings::SettingsData::instance()->listenForAndroidDevicesOnStartup()) RemoteControl::RemoteInterface::instance().listen(); } bool MainWindow::Window::slotExit() { if (Options::the()->demoMode()) { QString txt = i18n("

    Delete Your Temporary Demo Database

    " "

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

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

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

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

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

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

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

    " "

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

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

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

    " "

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

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

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

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

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

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

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

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

    " "

    Please follow these steps to do so:" "

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

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

    Unable to find ffmpeg on the system.

    " "

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

    "), i18n("Video thumbnails are not available"), QString::fromLatin1("VideoThumbnailerNotInstalled")); } } bool MainWindow::Window::anyVideosSelected() const { Q_FOREACH (const DB::FileName &fileName, selected()) { if (Utilities::isVideo(fileName)) return true; } return false; } void MainWindow::Window::setHistogramVisibilty(bool visible) const { if (visible) { m_dateBar->show(); m_dateBarLine->show(); } else { m_dateBar->hide(); m_dateBarLine->hide(); } } void MainWindow::Window::slotImageRotated(const DB::FileName &fileName) { // An image has been rotated by the annotation dialog or the viewer. // We have to reload the respective thumbnail to get it in the right angle ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } bool MainWindow::Window::dbIsDirty() const { return m_statusBar->mp_dirtyIndicator->isSaveDirty(); } -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE void MainWindow::Window::showPositionBrowser() { Browser::PositionBrowserWidget *positionBrowser = positionBrowserWidget(); m_stack->setCurrentWidget(positionBrowser); updateStates(false); } Browser::PositionBrowserWidget *MainWindow::Window::positionBrowserWidget() { if (m_positionBrowser == 0) { m_positionBrowser = createPositionBrowser(); } return m_positionBrowser; } Browser::PositionBrowserWidget *MainWindow::Window::createPositionBrowser() { Browser::PositionBrowserWidget *widget = new Browser::PositionBrowserWidget(m_stack); m_stack->addWidget(widget); return widget; } #endif UserFeedback MainWindow::Window::askWarningContinueCancel(const QString &msg, const QString &title, const QString &dialogId) { auto answer = KMessageBox::warningContinueCancel(this, msg, title, KStandardGuiItem::cont(), KStandardGuiItem::cancel(), dialogId); return (answer == KMessageBox::Continue) ? UserFeedback::Confirm : UserFeedback::Deny; } UserFeedback MainWindow::Window::askQuestionYesNo(const QString &msg, const QString &title, const QString &dialogId) { auto answer = KMessageBox::questionYesNo(this, msg, title, KStandardGuiItem::yes(), KStandardGuiItem::no(), dialogId); return (answer == KMessageBox::Yes) ? UserFeedback::Confirm : UserFeedback::Deny; } void MainWindow::Window::showInformation(const QString &msg, const QString &title, const QString &dialogId) { KMessageBox::information(this, msg, title, dialogId); } void MainWindow::Window::showSorry(const QString &msg, const QString &title, const QString &) { KMessageBox::sorry(this, msg, title); } void MainWindow::Window::showError(const QString &msg, const QString &title, const QString &) { KMessageBox::error(this, msg, title); } bool MainWindow::Window::isDialogDisabled(const QString &dialogId) { // Note(jzarl): there are different methods for different kinds of dialogs. // However, all these methods share exactly the same code in KMessageBox. // If that ever changes, we can still update our implementation - until then I won't just copy a stupid API... return !KMessageBox::shouldBeShownContinue(dialogId); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Window.h b/MainWindow/Window.h index d628ad4b..331ab82c 100644 --- a/MainWindow/Window.h +++ b/MainWindow/Window.h @@ -1,308 +1,309 @@ /* Copyright (C) 2003-2019 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. */ #ifndef MAINWINDOW_WINDOW_H #define MAINWINDOW_WINDOW_H +#include +#ifdef HAVE_MARBLE +#include +#endif #include #include #include #include #include #include #include #include #include #include -#ifdef HAVE_KGEOMAP -#include -#endif class QAction; class QCloseEvent; class QContextMenuEvent; class QFrame; class QLabel; class QMoveEvent; class QResizeEvent; class QStackedWidget; class QTimer; class KActionMenu; class KTipDialog; class KToggleAction; #ifdef HASKIPI namespace KIPI { class PluginLoader; } #endif namespace AnnotationDialog { class Dialog; } namespace Browser { class BrowserWidget; class BreadcrumbList; } namespace DateBar { class DateBarWidget; } namespace DB { class ImageInfoList; } namespace HTMLGenerator { class HTMLDialog; } namespace Plugins { class Interface; } namespace Settings { class SettingsDialog; } namespace ThumbnailView { class ThumbnailFacade; } class BreadcrumbViewer; namespace MainWindow { class DeleteDialog; class StatusBar; class TokenEditor; class Window : public KXmlGuiWindow, public DB::UIDelegate { Q_OBJECT public: explicit Window(QWidget *parent); ~Window() override; static void configureImages(const DB::ImageInfoList &list, bool oneAtATime); static Window *theMainWindow(); DB::FileNameList selected(ThumbnailView::SelectionMode mode = ThumbnailView::ExpandCollapsedStacks) const; DB::ImageSearchInfo currentContext(); QString currentBrowseCategory() const; void setStackHead(const DB::FileName &image); void setHistogramVisibilty(bool visible) const; bool dbIsDirty() const; -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE void showPositionBrowser(); Browser::PositionBrowserWidget *positionBrowserWidget(); #endif // implement UI delegate interface // Note(jzarl): we just could create a UIDelegate class that takes a QWidget, // implementing the same messageParent approach that we took before. // For now, I don't see anything wrong with directly implementing the interface instead. // I may change my mind later and I'm ready to convinced of the errors of my way, though... DB::UserFeedback askWarningContinueCancel(const QString &msg, const QString &title, const QString &dialogId) override; DB::UserFeedback askQuestionYesNo(const QString &msg, const QString &title, const QString &dialogId) override; void showInformation(const QString &msg, const QString &title, const QString &dialogId) override; void showSorry(const QString &msg, const QString &title, const QString &) override; void showError(const QString &msg, const QString &title, const QString &) override; bool isDialogDisabled(const QString &dialogId) override; public slots: void showThumbNails(const DB::FileNameList &items); void loadKipiPlugins(); void reloadThumbnails(ThumbnailView::SelectionUpdateMethod method = ThumbnailView::MaintainSelection); void runDemo(); void slotImageRotated(const DB::FileName &fileName); void slotSave(); protected slots: void showThumbNails(); bool slotExit(); void slotOptions(); void slotConfigureAllImages(); void slotConfigureImagesOneAtATime(); void slotCreateImageStack(); void slotUnStackImages(); void slotSetStackHead(); void slotCopySelectedURLs(); void slotPasteInformation(); void slotDeleteSelected(); void slotReReadExifInfo(); void slotAutoStackImages(); void slotSearch(); void slotView(bool reuse = true, bool slideShow = false, bool random = false); void slotViewNewWindow(); void slotSortByDateAndTime(); void slotSortAllByDateAndTime(); void slotLimitToSelected(); void slotExportToHTML(); void slotAutoSave(); void showBrowser(); void slotOptionGroupChanged(); void showTipOfDay(); void lockToDefaultScope(); void setDefaultScopePositive(); void setDefaultScopeNegative(); void unlockFromDefaultScope(); void changePassword(); void slotConfigureKeyBindings(); void slotSetFileName(const DB::FileName &); void updateContextMenuFromSelectionSize(int selectionSize); void slotUpdateViewMenu(DB::Category::ViewType); void slotShowNotOnDisk(); void slotBuildThumbnails(); void slotBuildThumbnailsIfWanted(); void slotRunSlideShow(); void slotRunRandomizedSlideShow(); void slotConfigureToolbars(); void slotNewToolbarConfig(); void slotImport(); void slotExport(); void delayedInit(); void slotReenableMessages(); void slotImagesChanged(const QList &); void slotSelectionChanged(int count); void plug(); void slotRemoveTokens(); void slotShowListOfFiles(); void updateDateBar(const Browser::BreadcrumbList &); void updateDateBar(); void slotShowImagesWithInvalidDate(); void slotShowImagesWithChangedMD5Sum(); void showDateBarTip(const QString &); void slotJumpToContext(); void setDateRange(const DB::ImageDate &); void clearDateRange(); void startAutoSaveTimer(); void slotRecalcCheckSums(); void slotShowExifInfo(); void showFeatures(); void showImage(const DB::FileName &fileName); void slotOrderIncr(); void slotOrderDecr(); void slotRotateSelectedLeft(); void slotRotateSelectedRight(); void rotateSelected(int angle); void showVideos(); void slotStatistics(); void slotRecreateExifDB(); void useNextVideoThumbnail(); void usePreviousVideoThumbnail(); void mergeDuplicates(); void slotThumbnailSizeChanged(); void slotMarkUntagged(); protected: void configureImages(bool oneAtATime); QString welcome(); void closeEvent(QCloseEvent *e) override; void resizeEvent(QResizeEvent *) override; void moveEvent(QMoveEvent *) override; void setupMenuBar(); void createAnnotationDialog(); bool load(); void contextMenuEvent(QContextMenuEvent *e) override; void setLocked(bool b, bool force, bool recount = true); void configImages(const DB::ImageInfoList &list, bool oneAtATime); void updateStates(bool thumbNailView); DB::FileNameList selectedOnDisk(); void setupPluginMenu(); void launchViewer(const DB::FileNameList &mediaList, bool reuse, bool slideShow, bool random); void setupStatusBar(); void setPluginMenuState(const char *name, const QList &actions); void createSearchBar(); void executeStartupActions(); void checkIfVideoThumbnailerIsInstalled(); bool anyVideosSelected() const; -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE Browser::PositionBrowserWidget *createPositionBrowser(); #endif private: static Window *s_instance; ThumbnailView::ThumbnailFacade *m_thumbnailView; Settings::SettingsDialog *m_settingsDialog; QPointer m_annotationDialog; QStackedWidget *m_stack; QTimer *m_autoSaveTimer; Browser::BrowserWidget *m_browser; DeleteDialog *m_deleteDialog; QAction *m_lock; QAction *m_unlock; QAction *m_setDefaultPos; QAction *m_setDefaultNeg; QAction *m_jumpToContext; HTMLGenerator::HTMLDialog *m_htmlDialog; QAction *m_configOneAtATime; QAction *m_configAllSimultaniously; QAction *m_createImageStack; QAction *m_unStackImages; QAction *m_setStackHead; QAction *m_view; QAction *m_rotLeft; QAction *m_rotRight; QAction *m_sortByDateAndTime; QAction *m_sortAllByDateAndTime; QAction *m_AutoStackImages; QAction *m_viewInNewWindow; KActionMenu *m_viewMenu; KToggleAction *m_smallListView; KToggleAction *m_largeListView; KToggleAction *m_largeIconView; QAction *m_generateHtml; QAction *m_copy; QAction *m_paste; QAction *m_deleteSelected; QAction *m_limitToMarked; QAction *m_selectAll; QAction *m_clearSelection; QAction *m_runSlideShow; QAction *m_runRandomSlideShow; Plugins::Interface *m_pluginInterface; QAction *m_showExifDialog; #ifdef HASKIPI KIPI::PluginLoader *m_pluginLoader; #endif QAction *m_recreateThumbnails; QAction *m_useNextVideoThumbnail; QAction *m_usePreviousVideoThumbnail; QAction *m_markUntagged; TokenEditor *m_tokenEditor; DateBar::DateBarWidget *m_dateBar; QFrame *m_dateBarLine; bool m_hasLoadedKipiPlugins; QMap> m_viewerInputMacros; MainWindow::StatusBar *m_statusBar; QString m_lastTarget; -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE Browser::PositionBrowserWidget *m_positionBrowser; #endif }; } #endif /* MAINWINDOW_WINDOW_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/ImageInfo.cpp b/Plugins/ImageInfo.cpp index 3abb1b28..93913b4c 100644 --- a/Plugins/ImageInfo.cpp +++ b/Plugins/ImageInfo.cpp @@ -1,369 +1,369 @@ /* Copyright (C) 2003-2018 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 "ImageInfo.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #define KEXIV_ORIENTATION_UNSPECIFIED 0 #define KEXIV_ORIENTATION_NORMAL 1 #define KEXIV_ORIENTATION_HFLIP 2 #define KEXIV_ORIENTATION_ROT_180 3 #define KEXIV_ORIENTATION_VFLIP 4 #define KEXIV_ORIENTATION_ROT_90_HFLIP 5 #define KEXIV_ORIENTATION_ROT_90 6 #define KEXIV_ORIENTATION_ROT_90_VFLIP 7 #define KEXIV_ORIENTATION_ROT_270 8 /** * Convert a rotation in degrees to a KExiv2::ImageOrientation value. */ static int deg2KexivOrientation(int deg) { deg = (deg + 360) % 360; ; switch (deg) { case 0: return KEXIV_ORIENTATION_NORMAL; case 90: return KEXIV_ORIENTATION_ROT_90; case 180: return KEXIV_ORIENTATION_ROT_180; case 270: return KEXIV_ORIENTATION_ROT_270; default: qCWarning(PluginsLog) << "Rotation of " << deg << "degrees can't be mapped to KExiv2::ImageOrientation value."; return KEXIV_ORIENTATION_UNSPECIFIED; } } /** * Convert a KExiv2::ImageOrientation value into a degrees angle. */ static int kexivOrientation2deg(int orient) { switch (orient) { case KEXIV_ORIENTATION_NORMAL: return 0; case KEXIV_ORIENTATION_ROT_90: return 90; case KEXIV_ORIENTATION_ROT_180: return 280; case KEXIV_ORIENTATION_ROT_270: return 270; default: qCWarning(PluginsLog) << "KExiv2::ImageOrientation value " << orient << " not a pure rotation. Discarding orientation info."; return 0; } } Plugins::ImageInfo::ImageInfo(KIPI::Interface *interface, const QUrl &url) : KIPI::ImageInfoShared(interface, url) { m_info = DB::ImageDB::instance()->info(DB::FileName::fromAbsolutePath(_url.path())); } QMap Plugins::ImageInfo::attributes() { if (m_info == nullptr) { // This can happen if we're trying to access an image that // has been deleted on-disc, but not yet the database return QMap(); } Q_ASSERT(m_info); QMap res; res.insert(QString::fromLatin1("name"), QFileInfo(m_info->fileName().absolute()).baseName()); res.insert(QString::fromLatin1("comment"), m_info->description()); res.insert(QLatin1String("date"), m_info->date().start()); res.insert(QLatin1String("dateto"), m_info->date().end()); res.insert(QLatin1String("isexactdate"), m_info->date().start() == m_info->date().end()); res.insert(QString::fromLatin1("orientation"), deg2KexivOrientation(m_info->angle())); res.insert(QString::fromLatin1("angle"), deg2KexivOrientation(m_info->angle())); // for compatibility with older versions. Now called orientation. res.insert(QString::fromLatin1("title"), m_info->label()); res.insert(QString::fromLatin1("rating"), m_info->rating()); // not supported: //res.insert(QString::fromLatin1("colorlabel"), xxx ); //res.insert(QString::fromLatin1("picklabel"), xxx ); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE Map::GeoCoordinates position = m_info->coordinates(); if (position.hasCoordinates()) { res.insert(QString::fromLatin1("longitude"), QVariant(position.lon())); res.insert(QString::fromLatin1("latitude"), QVariant(position.lat())); if (position.hasAltitude()) res.insert(QString::fromLatin1("altitude"), QVariant(position.alt())); } #endif // Flickr plug-in expects the item tags, so we better give them. QString text; QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); QStringList tags; QStringList tagspath; const QLatin1String sep("/"); Q_FOREACH (const DB::CategoryPtr category, categories) { QString categoryName = category->name(); if (category->isSpecialCategory()) continue; // I don't know why any categories except the above should be excluded //if ( category->doShow() ) { Utilities::StringSet items = m_info->itemsOfCategory(categoryName); Q_FOREACH (const QString &tag, items) { tags.append(tag); // digikam compatible tag path: // note: this produces a semi-flattened hierarchy. // instead of "Places/France/Paris" this will yield "Places/Paris" tagspath.append(categoryName + sep + tag); } //} } res.insert(QString::fromLatin1("tagspath"), tagspath); res.insert(QString::fromLatin1("keywords"), tags); res.insert(QString::fromLatin1("tags"), tags); // for compatibility with older versions. Now called keywords. // TODO: implement this: //res.insert(QString::fromLatin1( "filesize" ), xxx ); // not supported: //res.insert(QString::fromLatin1( "creators" ), xxx ); //res.insert(QString::fromLatin1( "credit" ), xxx ); //res.insert(QString::fromLatin1( "rights" ), xxx ); //res.insert(QString::fromLatin1( "source" ), xxx ); return res; } void Plugins::ImageInfo::clearAttributes() { if (m_info) { // official behaviour is to delete all officially supported attributes: QStringList attr; attr.append(QString::fromLatin1("comment")); attr.append(QString::fromLatin1("date")); attr.append(QString::fromLatin1("title")); attr.append(QString::fromLatin1("orientation")); attr.append(QString::fromLatin1("tagspath")); attr.append(QString::fromLatin1("rating")); attr.append(QString::fromLatin1("colorlabel")); attr.append(QString::fromLatin1("picklabel")); attr.append(QString::fromLatin1("gpslocation")); attr.append(QString::fromLatin1("copyrights")); delAttributes(attr); } } void Plugins::ImageInfo::addAttributes(const QMap &amap) { if (m_info && !amap.empty()) { QMap map = amap; if (map.contains(QLatin1String("name"))) { // plugin renamed the item // TODO: implement this qCWarning(PluginsLog, "File renaming by kipi-plugin not supported."); //map.remove(QLatin1String("name")); } if (map.contains(QLatin1String("comment"))) { // is it save to do that? digikam seems to allow multiple comments on a single image // if a plugin assumes that it is adding a comment, not setting it, things might go badly... m_info->setDescription(map[QLatin1String("comment")].toString()); map.remove(QLatin1String("comment")); } // note: this probably won't work as expected because according to the spec, // "isexactdate" is supposed to be readonly and therefore never set here: if (map.contains(QLatin1String("isexactdate")) && map.contains(QLatin1String("date"))) { m_info->setDate(DB::ImageDate(map[QLatin1String("date")].toDateTime())); map.remove(QLatin1String("date")); } else if (map.contains(QLatin1String("date")) && map.contains(QLatin1String("dateto"))) { m_info->setDate(DB::ImageDate(map[QLatin1String("date")].toDateTime(), map[QLatin1String("dateto")].toDateTime())); map.remove(QLatin1String("date")); map.remove(QLatin1String("dateto")); } else if (map.contains(QLatin1String("date"))) { m_info->setDate(DB::ImageDate(map[QLatin1String("date")].toDateTime())); map.remove(QLatin1String("date")); } if (map.contains(QLatin1String("angle"))) { qCWarning(PluginsLog, "Kipi-plugin uses deprecated attribute \"angle\"."); m_info->setAngle(kexivOrientation2deg(map[QLatin1String("angle")].toInt())); map.remove(QLatin1String("angle")); } if (map.contains(QLatin1String("orientation"))) { m_info->setAngle(kexivOrientation2deg(map[QLatin1String("orientation")].toInt())); map.remove(QLatin1String("orientation")); } if (map.contains(QLatin1String("title"))) { m_info->setLabel(map[QLatin1String("title")].toString()); map.remove(QLatin1String("title")); } if (map.contains(QLatin1String("rating"))) { m_info->setRating(map[QLatin1String("rating")].toInt()); map.remove(QLatin1String("rating")); } if (map.contains(QLatin1String("tagspath"))) { const QStringList tagspaths = map[QLatin1String("tagspath")].toStringList(); const DB::CategoryCollection *categories = DB::ImageDB::instance()->categoryCollection(); DB::MemberMap &memberMap = DB::ImageDB::instance()->memberMap(); Q_FOREACH (const QString &path, tagspaths) { qCDebug(PluginsLog) << "Adding tags: " << path; QStringList tagpath = path.split(QLatin1String("/"), QString::SkipEmptyParts); // Note: maybe tagspaths with only one component or with unknown first component // should be added to the "keywords"/"Events" category? if (tagpath.size() < 2) { qCWarning(PluginsLog) << "Ignoring incompatible tag: " << path; continue; } // first component is the category, const QString categoryName = tagpath.takeFirst(); DB::CategoryPtr cat = categories->categoryForName(categoryName); if (cat) { QString previousTag; // last component is the tag: // others define hierarchy: Q_FOREACH (const QString ¤tTag, tagpath) { if (!cat->items().contains(currentTag)) { qCDebug(PluginsLog) << "Adding tag " << currentTag << " to category " << categoryName; // before we can use a tag, we have to add it cat->addItem(currentTag); } if (!previousTag.isNull()) { if (!memberMap.isGroup(categoryName, previousTag)) { // create a group for the parent tag, so we can add a sub-category memberMap.addGroup(categoryName, previousTag); } if (memberMap.canAddMemberToGroup(categoryName, previousTag, currentTag)) { // make currentTag a member of the previousTag group memberMap.addMemberToGroup(categoryName, previousTag, currentTag); } else { qCWarning(PluginsLog) << "Cannot make " << currentTag << " a subcategory of " << categoryName << "/" << previousTag << "!"; } } previousTag = currentTag; } qCDebug(PluginsLog) << "Adding tag " << previousTag << " in category " << categoryName << " to image " << m_info->label(); // previousTag must be a valid category (see addItem() above...) m_info->addCategoryInfo(categoryName, previousTag); } else { qCWarning(PluginsLog) << "Unknown category: " << categoryName; } } map.remove(QLatin1String("tagspath")); } // remove read-only keywords: map.remove(QLatin1String("filesize")); map.remove(QLatin1String("isexactdate")); map.remove(QLatin1String("keywords")); map.remove(QLatin1String("tags")); map.remove(QLatin1String("altitude")); map.remove(QLatin1String("longitude")); map.remove(QLatin1String("latitude")); // colorlabel // picklabel // creators // credit // rights // source MainWindow::DirtyIndicator::markDirty(); if (!map.isEmpty()) { qCWarning(PluginsLog) << "The following attributes are not (yet) supported by the KPhotoAlbum KIPI interface:" << map; } } } void Plugins::ImageInfo::delAttributes(const QStringList &attrs) { if (m_info && !attrs.empty()) { QStringList delAttrs = attrs; if (delAttrs.contains(QLatin1String("comment"))) { m_info->setDescription(QString()); delAttrs.removeAll(QLatin1String("comment")); } // not supported: date if (delAttrs.contains(QLatin1String("orientation")) || delAttrs.contains(QLatin1String("angle"))) { m_info->setAngle(0); delAttrs.removeAll(QLatin1String("orientation")); delAttrs.removeAll(QLatin1String("angle")); } if (delAttrs.contains(QLatin1String("rating"))) { m_info->setRating(-1); delAttrs.removeAll(QLatin1String("rating")); } if (delAttrs.contains(QLatin1String("title"))) { m_info->setLabel(QString()); delAttrs.removeAll(QLatin1String("title")); } // TODO: // (colorlabel) // (picklabel) // copyrights // not supported: gpslocation if (delAttrs.contains(QLatin1String("tags")) || delAttrs.contains(QLatin1String("tagspath"))) { m_info->clearAllCategoryInfo(); delAttrs.removeAll(QLatin1String("tags")); delAttrs.removeAll(QLatin1String("tagspath")); } MainWindow::DirtyIndicator::markDirty(); if (!delAttrs.isEmpty()) { qCWarning(PluginsLog) << "The following attributes are not (yet) supported by the KPhotoAlbum KIPI interface:" << delAttrs; } } } void Plugins::ImageInfo::cloneData(ImageInfoShared *const other) { ImageInfoShared::cloneData(other); if (m_info) { Plugins::ImageInfo *inf = static_cast(other); m_info->setDate(inf->m_info->date()); MainWindow::DirtyIndicator::markDirty(); } } bool Plugins::ImageInfo::isPositionAttribute(const QString &key) { return (key == QString::fromLatin1("longitude") || key == QString::fromLatin1("latitude") || key == QString::fromLatin1("altitude") || key == QString::fromLatin1("positionPrecision")); } bool Plugins::ImageInfo::isCategoryAttribute(const QString &key) { return (key != QString::fromLatin1("tags") && !isPositionAttribute(key)); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/InfoBox.cpp b/Viewer/InfoBox.cpp index 19e98825..b4d428a1 100644 --- a/Viewer/InfoBox.cpp +++ b/Viewer/InfoBox.cpp @@ -1,348 +1,348 @@ /* Copyright (C) 2003-2018 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 "InfoBox.h" // Qt includes #include #include #include #include #include #include // KDE includes #include #include #include // Local includes #include #include #include #include -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE #include #endif #include "VisibleOptionsMenu.h" using namespace Settings; Viewer::InfoBox::InfoBox(Viewer::ViewerWidget *viewer) : QTextBrowser(viewer) , m_viewer(viewer) , m_hoveringOverLink(false) , m_infoBoxResizer(this) , m_menu(nullptr) -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE , m_map(nullptr) #endif { setFrameStyle(Box | Plain); setLineWidth(1); setMidLineWidth(0); setAutoFillBackground(false); QPalette p = palette(); p.setColor(QPalette::Base, QColor(0, 0, 0, 170)); // r, g, b, A p.setColor(QPalette::Text, Qt::white); p.setColor(QPalette::Link, QColor(Qt::blue).light()); setPalette(p); m_jumpToContext = new QToolButton(this); m_jumpToContext->setIcon(QIcon::fromTheme(QString::fromUtf8("kphotoalbum"))); m_jumpToContext->setFixedSize(16, 16); connect(m_jumpToContext, &QToolButton::clicked, this, &InfoBox::jumpToContext); connect(this, SIGNAL(highlighted(QString)), SLOT(linkHovered(QString))); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE m_showOnMap = new QToolButton(this); m_showOnMap->setIcon(QIcon::fromTheme(QString::fromUtf8("atmosphere"))); m_showOnMap->setFixedSize(16, 16); m_showOnMap->setCursor(Qt::ArrowCursor); m_showOnMap->setToolTip(i18n("Show the geographic position of this image on a map")); connect(m_showOnMap, &QToolButton::clicked, this, &InfoBox::launchMapView); m_showOnMap->hide(); connect(m_viewer, &ViewerWidget::soughtTo, this, &InfoBox::updateMapForCurrentImage); #endif KRatingWidget *rating = new KRatingWidget(nullptr); // Unfortunately, the KRatingWidget now thinks that it has some absurdly big // dimensions. This call will persuade it to stay reasonably small. rating->adjustSize(); for (int i = 0; i <= 10; ++i) { rating->setRating(i); // QWidget::grab() does not create an alpha channel // Therefore, we need to create a mask using heuristics (yes, this is slow, but we only do it once) QPixmap pixmap = rating->grab(); pixmap.setMask(pixmap.createHeuristicMask()); m_ratingPixmap.append(pixmap); } delete rating; } QVariant Viewer::InfoBox::loadResource(int type, const QUrl &name) { if (name.scheme() == QString::fromUtf8("kratingwidget")) { int rating = name.port(); Q_ASSERT(0 <= rating && rating <= 10); return m_ratingPixmap[rating]; } return QTextBrowser::loadResource(type, name); } void Viewer::InfoBox::setSource(const QUrl &source) { int index = source.path().toInt(); QPair p = m_linkMap[index]; QString category = p.first; QString value = p.second; Browser::BrowserWidget::instance()->load(category, value); showBrowser(); } void Viewer::InfoBox::setInfo(const QString &text, const QMap> &linkMap) { m_linkMap = linkMap; setText(text); hackLinkColorForQt44(); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE if (m_viewer->currentInfo()->coordinates().hasCoordinates()) { m_showOnMap->show(); } else { m_showOnMap->hide(); } #endif setSize(); } void Viewer::InfoBox::setSize() { const int maxWidth = Settings::SettingsData::instance()->infoBoxWidth(); const int maxHeight = Settings::SettingsData::instance()->infoBoxHeight(); document()->setPageSize(QSize(maxWidth, maxHeight)); bool showVerticalBar = document()->size().height() > maxHeight; setVerticalScrollBarPolicy(showVerticalBar ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); const int realWidth = static_cast(document()->idealWidth()) + (showVerticalBar ? verticalScrollBar()->width() + frameWidth() : 0) + m_jumpToContext->width() + 10; resize(realWidth, qMin((int)document()->size().height(), maxHeight)); } void Viewer::InfoBox::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { possiblyStartResize(event->pos()); } QTextBrowser::mousePressEvent(event); } void Viewer::InfoBox::mouseReleaseEvent(QMouseEvent *event) { if (m_infoBoxResizer.isActive()) { Settings::SettingsData::instance()->setInfoBoxWidth(width()); Settings::SettingsData::instance()->setInfoBoxHeight(height()); } m_infoBoxResizer.deactivate(); QTextBrowser::mouseReleaseEvent(event); } void Viewer::InfoBox::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { if (m_infoBoxResizer.isActive()) { m_infoBoxResizer.setPos(event->pos()); } else { m_viewer->infoBoxMove(); } // Do not tell QTextBrowser about the mouse movement, as this will just start a selection. } else { updateCursor(event->pos()); QTextBrowser::mouseMoveEvent(event); } } void Viewer::InfoBox::linkHovered(const QString &linkName) { if (linkName.isEmpty()) { emit noTagHovered(); } else { emit tagHovered(m_linkMap[linkName.toInt()]); } m_hoveringOverLink = !linkName.isNull(); } void Viewer::InfoBox::jumpToContext() { Browser::BrowserWidget::instance()->addImageView(m_viewer->currentInfo()->fileName()); showBrowser(); } void Viewer::InfoBox::showBrowser() { QDesktopWidget *desktop = qApp->desktop(); if (desktop->screenNumber(Browser::BrowserWidget::instance()) == desktop->screenNumber(m_viewer)) { if (m_viewer->showingFullScreen()) { m_viewer->setShowFullScreen(false); } MainWindow::Window::theMainWindow()->raise(); } } /** * Update the cursor based on the cursors position in the info box */ void Viewer::InfoBox::updateCursor(const QPoint &pos) { const int border = 25; bool left = (pos.x() < border); bool right = pos.x() > width() - border; bool top = pos.y() < border; bool bottom = pos.y() > height() - border; Settings::Position windowPos = Settings::SettingsData::instance()->infoBoxPosition(); Qt::CursorShape shape = Qt::SizeAllCursor; if (m_hoveringOverLink) { shape = Qt::PointingHandCursor; } else if (atBlackoutPos(left, right, top, bottom, windowPos)) { shape = Qt::SizeAllCursor; } else if ((left && top) || (right && bottom)) { shape = Qt::SizeFDiagCursor; } else if ((left && bottom) || (right && top)) { shape = Qt::SizeBDiagCursor; } else if (top || bottom) { shape = Qt::SizeVerCursor; } else if (left || right) { shape = Qt::SizeHorCursor; } setCursor(shape); viewport()->setCursor(shape); } /** * Return true if we are at an edge of the image info box that is towards the edge of the viewer. * We can forexample not make the box taller at the bottom if the box is sitting at the bottom of the viewer. */ bool Viewer::InfoBox::atBlackoutPos(bool left, bool right, bool top, bool bottom, Settings::Position pos) const { return (left && (pos == Left || pos == TopLeft || pos == BottomLeft)) || (right && (pos == Right || pos == TopRight || pos == BottomRight)) || (top && (pos == Top || pos == TopLeft || pos == TopRight)) || (bottom && (pos == Bottom || pos == BottomLeft || pos == BottomRight)); } void Viewer::InfoBox::possiblyStartResize(const QPoint &pos) { const int border = 25; bool left = (pos.x() < border); bool right = pos.x() > width() - border; bool top = pos.y() < border; bool bottom = pos.y() > height() - border; if (left || right || top || bottom) { m_infoBoxResizer.setup(left, right, top, bottom); } } void Viewer::InfoBox::resizeEvent(QResizeEvent *) { QPoint pos = viewport()->rect().adjusted(0, 2, -m_jumpToContext->width() - 2, 0).topRight(); m_jumpToContext->move(pos); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE pos.setY(pos.y() + 20); m_showOnMap->move(pos); #endif } void Viewer::InfoBox::hackLinkColorForQt44() { QTextCursor cursor(document()); Q_FOREVER { QTextCharFormat f = cursor.charFormat(); if (f.isAnchor()) { f.setForeground(QColor(Qt::blue).light()); QTextCursor c2 = cursor; c2.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); c2.setCharFormat(f); } if (cursor.atEnd()) { break; } cursor.movePosition(QTextCursor::NextCharacter); } } void Viewer::InfoBox::contextMenuEvent(QContextMenuEvent *event) { if (!m_menu) { m_menu = new VisibleOptionsMenu(m_viewer, new KActionCollection((QObject *)nullptr)); connect(m_menu, &VisibleOptionsMenu::visibleOptionsChanged, m_viewer, &ViewerWidget::updateInfoBox); } m_menu->exec(event->globalPos()); } -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE void Viewer::InfoBox::launchMapView() { if (!m_map) { m_map = new Map::MapView(m_viewer, Map::MapView::MapViewWindow); } m_map->addImage(m_viewer->currentInfo()); m_map->setShowThumbnails(false); m_map->zoomToMarkers(); m_map->show(); m_map->raise(); } void Viewer::InfoBox::updateMapForCurrentImage(DB::FileName) { if (!m_map) { return; } if (m_viewer->currentInfo()->coordinates().hasCoordinates()) { m_map->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); m_map->clear(); m_map->addImage(m_viewer->currentInfo()); m_map->setCenter(m_viewer->currentInfo()); } else { m_map->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/InfoBox.h b/Viewer/InfoBox.h index b2f7be99..23609eed 100644 --- a/Viewer/InfoBox.h +++ b/Viewer/InfoBox.h @@ -1,107 +1,107 @@ -/* Copyright (C) 2003-2014 Jesper K. Pedersen +/* Copyright (C) 2003-2018 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. */ #ifndef INFOBOX_H #define INFOBOX_H -#include "config-kpa-kgeomap.h" +#include "config-kpa-marble.h" // Qt includes #include #include // KDE includes #include // Local includes #include "InfoBoxResizer.h" #include "ViewerWidget.h" #include // Qt classes class QMenu; class QToolButton; namespace Map { // Local classes class MapView; } namespace Viewer { // Local classes class VisibleOptionsMenu; class InfoBox : public QTextBrowser { Q_OBJECT public: explicit InfoBox(ViewerWidget *parent); void setSource(const QUrl &source) override; void setInfo(const QString &text, const QMap> &linkMap); void setSize(); protected: QVariant loadResource(int type, const QUrl &name) override; void mouseMoveEvent(QMouseEvent *) override; void mousePressEvent(QMouseEvent *) override; void mouseReleaseEvent(QMouseEvent *) override; void resizeEvent(QResizeEvent *) override; void contextMenuEvent(QContextMenuEvent *event) override; void updateCursor(const QPoint &pos); bool atBlackoutPos(bool left, bool right, bool top, bool bottom, Settings::Position windowPos) const; void showBrowser(); void possiblyStartResize(const QPoint &pos); void hackLinkColorForQt44(); protected slots: void jumpToContext(); void linkHovered(const QString &linkName); -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE void launchMapView(); void updateMapForCurrentImage(DB::FileName); #endif signals: void tagHovered(QPair tagData); void noTagHovered(); private: // Variables QMap> m_linkMap; ViewerWidget *m_viewer; QToolButton *m_jumpToContext; bool m_hoveringOverLink; InfoBoxResizer m_infoBoxResizer; VisibleOptionsMenu *m_menu; QList m_ratingPixmap; -#ifdef HAVE_KGEOMAP +#ifdef HAVE_MARBLE QToolButton *m_showOnMap; QPointer m_map; #endif }; } #endif // INFOBOX_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/config-kpa-kgeomap.h.cmake b/config-kpa-kgeomap.h.cmake deleted file mode 100644 index faf35ade..00000000 --- a/config-kpa-kgeomap.h.cmake +++ /dev/null @@ -1,2 +0,0 @@ -/* Define to 1 if KGeoMap should be compiled in */ -#cmakedefine HAVE_KGEOMAP 1 diff --git a/config-kpa-marble.h.cmake b/config-kpa-marble.h.cmake new file mode 100644 index 00000000..d528bcc1 --- /dev/null +++ b/config-kpa-marble.h.cmake @@ -0,0 +1,2 @@ +/* Define to 1 if Marble should be compiled in */ +#cmakedefine HAVE_MARBLE 1