diff --git a/AnnotationDialog/Dialog.cpp b/AnnotationDialog/Dialog.cpp index e98e989d..da9bf121 100644 --- a/AnnotationDialog/Dialog.cpp +++ b/AnnotationDialog/Dialog.cpp @@ -1,1752 +1,1752 @@ /* 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 "Dialog.h" #include "DescriptionEdit.h" #include "enums.h" #include "ImagePreviewWidget.h" #include "DateEdit.h" #include "ListSelect.h" #include "Logging.h" #include "ResizableFrame.h" #include "ShortCutManager.h" #include "ShowSelectionOnlyManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_KGEOMAP #include #include #include #endif #include #include #include #include #include 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 // -------------------------------------------------- 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 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> taggedAreas; QPair tagData; foreach (ResizableFrame *area, areas()) { tagData = area->tagData(); if ( !tagData.first.isEmpty() ) { taggedAreas[tagData.first][tagData.second] = area->actualCoordinates(); } } 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(), taggedAreas[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(); } int AnnotationDialog::Dialog::configure( DB::ImageInfoList list, bool oneAtATime ) { ShowHideSearch(false); if ( Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() ) { DB::ImageDB::instance()->categoryCollection()->categoryForName( Settings::SettingsData::instance()->untaggedCategory() ) ->addItem(Settings::SettingsData::instance()->untaggedTag() ); } if (oneAtATime) { m_setup = InputSingleImageConfigMode; } else { m_setup = InputMultiImageConfigMode; // Hide the default positionable category selector m_preview->updatePositionableCategories(); } #ifdef HAVE_KGEOMAP m_mapIsPopulated = false; m_annotationMap->clear(); #endif m_origList = list; m_editList.clear(); for( DB::ImageInfoListConstIterator it = list.constBegin(); it != list.constEnd(); ++it ) { m_editList.append( *(*it) ); } setup(); if ( oneAtATime ) { m_current = 0; m_preview->configure( &m_editList, true ); load(); } else { m_preview->configure( &m_editList, false ); m_preview->canCreateAreas( false ); m_startDate->setDate( QDate() ); m_endDate->setDate( QDate() ); m_time->hide(); m_rating->setRating( 0 ); m_ratingChanged = false; m_areasChanged = false; Q_FOREACH( ListSelect *ls, m_optionList ) { setUpCategoryListBoxForMultiImageSelection( ls, list ); } m_imageLabel->setText(QString()); m_imageFilePattern->setText(QString()); m_firstDescription = m_editList[0].description(); const bool allTextEqual = std::all_of(m_editList.begin(), m_editList.end(), [=] (const DB::ImageInfo& item) -> bool { return item.description() == m_firstDescription; }); if ( !allTextEqual ) m_firstDescription = m_conflictText; m_description->setPlainText( m_firstDescription ); } showHelpDialog( oneAtATime ? InputSingleImageConfigMode : InputMultiImageConfigMode ); return exec(); } DB::ImageSearchInfo AnnotationDialog::Dialog::search( DB::ImageSearchInfo* search ) { ShowHideSearch(true); #ifdef HAVE_KGEOMAP m_mapIsPopulated = false; m_annotationMap->clear(); #endif m_setup = SearchMode; if ( search ) m_oldSearch = *search; setup(); m_preview->setImage(Utilities::locateDataFile(QString::fromLatin1("pics/search.jpg"))); m_ratingChanged = false ; showHelpDialog( SearchMode ); int ok = exec(); if ( ok == QDialog::Accepted ) { - const QDateTime start = m_startDate->date().isNull() ? QDateTime() : QDateTime(m_startDate->date()); - const QDateTime end = m_endDate->date().isNull() ? QDateTime() : QDateTime( m_endDate->date() ); + 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 const KGeoMap::GeoCoordinates::Pair regionSelection = m_annotationMap->getRegionSelection(); m_oldSearch.setRegionSelection(regionSelection); #endif return m_oldSearch; } else return DB::ImageSearchInfo(); } void AnnotationDialog::Dialog::setup() { // Repopulate the listboxes in case data has changed // An group might for example have been renamed. Q_FOREACH( ListSelect *ls, m_optionList ) { 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 // 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; info->delaySavingChanges(true); 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() ); } info->delaySavingChanges(false); } 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 { m_stack->setCurrentWidget( m_fullScreenPreview ); m_fullScreenPreview->load( DB::FileNameList() << m_editList[ m_current].fileName() ); } } } void AnnotationDialog::Dialog::tidyAreas() { // Remove all areas marked on the preview image foreach (ResizableFrame *area, areas()) { area->markTidied(); area->deleteLater(); } } void AnnotationDialog::Dialog::slotNewArea(ResizableFrame *area) { area->setDialog(this); } void AnnotationDialog::Dialog::positionableTagSelected(QString category, QString tag) { // Be sure not to propose an already-associated tag QPair tagData = qMakePair(category, tag); foreach (ResizableFrame *area, areas()) { if (area->tagData() == tagData) { return; } } // Set the selected tag as the last selected positionable tag m_lastSelectedPositionableTag = tagData; // Add the tag to the positionable tag candidate list addTagToCandidateList(category, tag); } void AnnotationDialog::Dialog::positionableTagDeselected(QString category, QString tag) { // Remove the tag from the candidate list removeTagFromCandidateList(category, tag); // Search for areas linked against the tag on this image if (m_setup == InputSingleImageConfigMode) { QPair deselectedTag = QPair(category, tag); foreach (ResizableFrame *area, areas()) { if (area->tagData() == deselectedTag) { area->removeTagData(); m_areasChanged = true; // Only one area can be associated with the tag, so we can return here return; } } } // Removal of tagged areas in InputMultiImageConfigMode is done in DB::ImageInfo::removeCategoryInfo } void AnnotationDialog::Dialog::addTagToCandidateList(QString category, QString tag) { m_positionableTagCandidates << QPair(category, tag); } void AnnotationDialog::Dialog::removeTagFromCandidateList(QString category, QString tag) { // Is the deselected tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == tag) { m_lastSelectedPositionableTag = QPair(); } // Remove the tag from the candidate list m_positionableTagCandidates.removeAll(QPair(category, tag)); // When a positionable tag is entered via the AreaTagSelectDialog, it's added to this // list twice, so we use removeAll here to be sure to also wipe duplicate entries. } QPair AnnotationDialog::Dialog::lastSelectedPositionableTag() const { return m_lastSelectedPositionableTag; } QList> AnnotationDialog::Dialog::positionableTagCandidates() const { return m_positionableTagCandidates; } void AnnotationDialog::Dialog::slotShowAreas(bool showAreas) { foreach (ResizableFrame *area, areas()) { area->setVisible(showAreas); } } void AnnotationDialog::Dialog::positionableTagRenamed(QString category, QString oldTag, QString newTag) { // Is the renamed tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == oldTag) { m_lastSelectedPositionableTag.second = newTag; } // Check the candidate list for the tag QPair oldTagData = QPair(category, oldTag); if (m_positionableTagCandidates.contains(oldTagData)) { // The tag is in the list, so update it m_positionableTagCandidates.removeAt(m_positionableTagCandidates.indexOf(oldTagData)); m_positionableTagCandidates << QPair(category, newTag); } // Check if an area on the current image contains the changed or proposed tag foreach (ResizableFrame *area, areas()) { if (area->tagData() == oldTagData) { area->setTagData(category, newTag); } } } void AnnotationDialog::Dialog::descriptionPageUpDownPressed(QKeyEvent *event) { if (event->key() == Qt::Key_PageUp) { m_actions->action(QString::fromLatin1("annotationdialog-prev-image"))->trigger(); } else if (event->key() == Qt::Key_PageDown) { m_actions->action(QString::fromLatin1("annotationdialog-next-image"))->trigger(); } } void AnnotationDialog::Dialog::checkProposedTagData( QPair tagData, ResizableFrame *areaToExclude) const { foreach (ResizableFrame *area, areas()) { if (area != areaToExclude && area->proposedTagData() == tagData && area->tagData().first.isEmpty()) { area->removeProposedTagData(); } } } void AnnotationDialog::Dialog::areaChanged() { m_areasChanged = true; } /** * @brief positionableTagValid checks whether a given tag can still be associated to an area. * This checks for empty and duplicate tags. * @return */ bool AnnotationDialog::Dialog::positionableTagAvailable(const QString &category, const QString &tag) const { if (category.isEmpty() || tag.isEmpty()) return false; // does any area already have that tag? foreach (const ResizableFrame *area, areas()) { const auto tagData = area->tagData(); if (tagData.first == category && tagData.second == tag) return false; } return true; } /** * @brief Generates a set of positionable tags currently used on the image * @param category * @return */ QSet AnnotationDialog::Dialog::positionedTags(const QString &category) const { QSet tags; foreach (const ResizableFrame *area, areas()) { const auto tagData = area->tagData(); if (tagData.first == category) tags += tagData.second; } return tags; } AnnotationDialog::ListSelect *AnnotationDialog::Dialog::listSelectForCategory(const QString &category) { return m_listSelectList.value(category,nullptr); } #ifdef HAVE_KGEOMAP void AnnotationDialog::Dialog::updateMapForCurrentImage() { if (m_setup != InputSingleImageConfigMode) { return; } if (m_editList[m_current].coordinates().hasCoordinates()) { m_annotationMap->setCenter(m_editList[m_current]); m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } } void AnnotationDialog::Dialog::annotationMapVisibilityChanged(bool visible) { // This populates the map if it's added when the dialog is already open if ( visible ) { // when the map dockwidget is already visible on show(), the call to // annotationMapVisibilityChanged is executed in the GUI thread. // This ensures that populateMap() doesn't block the GUI in this case: QTimer::singleShot(0, this, SLOT(populateMap())); } else { m_cancelMapLoading = true; } } void AnnotationDialog::Dialog::populateMap() { // populateMap is called every time the map widget gets visible if (m_mapIsPopulated) { return; } m_annotationMap->displayStatus(Map::MapView::MapStatus::Loading); m_cancelMapLoading = false; m_mapLoadingProgress->setMaximum(m_editList.count()); m_mapLoadingProgress->show(); m_cancelMapLoadingButton->show(); int processedImages = 0; int imagesWithCoordinates = 0; foreach (DB::ImageInfo info, m_editList) { processedImages++; m_mapLoadingProgress->setValue(processedImages); // keep things responsive by processing events manually: QApplication::processEvents(); if (info.coordinates().hasCoordinates()) { m_annotationMap->addImage(info); imagesWithCoordinates++; } // m_cancelMapLoading is set to true by clicking the "Cancel" button if (m_cancelMapLoading) { m_annotationMap->clear(); break; } } // at this point either we canceled loading or the map is populated: m_mapIsPopulated = ! m_cancelMapLoading; mapLoadingFinished(imagesWithCoordinates > 0, imagesWithCoordinates == processedImages); } void AnnotationDialog::Dialog::setCancelMapLoading() { m_cancelMapLoading = true; } void AnnotationDialog::Dialog::mapLoadingFinished(bool mapHasImages, bool allImagesHaveCoordinates) { m_mapLoadingProgress->hide(); m_cancelMapLoadingButton->hide(); if (m_setup == InputSingleImageConfigMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } else { if (m_setup == SearchMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SearchCoordinates); } else { if (mapHasImages) { if (! allImagesHaveCoordinates) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SomeImagesHaveNoCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::NoImagesHaveNoCoordinates); } } } if (m_setup != SearchMode) { m_annotationMap->zoomToMarkers(); updateMapForCurrentImage(); } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageDate.cpp b/DB/ImageDate.cpp index 24ceebc9..9ef3d33c 100644 --- a/DB/ImageDate.cpp +++ b/DB/ImageDate.cpp @@ -1,406 +1,418 @@ /* Copyright (C) 2003-2010 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 "ImageDate.h" #include #include #include using namespace DB; ImageDate::ImageDate( const QDate& date ) { m_start = QDateTime( date, QTime(0,0,0) ); m_end = QDateTime( date, QTime(0,0,0) ); } ImageDate::ImageDate( const QDateTime& date) { m_start = date; m_end = date; } bool ImageDate::operator<=( const ImageDate& other ) const { // This operator is used by QMap when checking for equal elements, thus we need the second part too. return m_start < other.m_start || (m_start == other.m_start && m_end <= other.m_end); } ImageDate::ImageDate() { } bool ImageDate::isNull() const { return m_start.isNull(); } bool ImageDate::isFuzzy() const { return m_start != m_end; } static bool isFirstSecOfMonth( const QDateTime& date ) { return date.date().day() == 1 && date.time().hour() == 0 && date.time().minute() == 0; } static bool isLastSecOfMonth( QDateTime date ) { return isFirstSecOfMonth( date.addSecs(1) ); } static bool isFirstSecOfDay( const QDateTime& time ) { return time.time().hour() == 0 && time.time().minute() == 0 && time.time().second() == 0; } static bool isLastSecOfDay( const QDateTime& time ) { return time.time().hour() == 23 && time.time().minute() == 59 && time.time().second() == 59; } QString ImageDate::toString( bool withTime ) const { if ( m_start.isNull() ) return QString(); if ( m_start == m_end ) { if ( withTime && !isFirstSecOfDay(m_start)) return m_start.toString( QString::fromLatin1( "d. MMM yyyy hh:mm:ss" ) ); else return m_start.toString( QString::fromLatin1( "d. MMM yyyy" ) ); } // start is different from end. if ( isFirstSecOfMonth(m_start) && isLastSecOfMonth(m_end) ) { if ( m_start.date().month() == 1 && m_end.date().month() == 12 ) { if ( m_start.date().year() == m_end.date().year() ) { // 2005 return QString::number( m_start.date().year() ); } else { // 2005-2006 return QString::fromLatin1("%1 - %2").arg( m_start.date().year() ).arg( m_end.date().year() ); } } else { // a whole month, but not a whole year. if ( m_start.date().year() == m_end.date().year() &&m_start.date().month() == m_end.date().month() ) { // jan 2005 return QString::fromLatin1( "%1 %2" ) .arg( QLocale().standaloneMonthName(m_start.date().month(), QLocale::ShortFormat) ).arg(m_start.date().year() ); } else { // jan 2005 - feb 2006 return QString::fromLatin1( "%1 %2 - %3 %4" ) .arg( QLocale().standaloneMonthName(m_start.date().month(), QLocale::ShortFormat) ).arg(m_start.date().year() ) .arg( QLocale().standaloneMonthName(m_end.date().month(), QLocale::ShortFormat) ).arg( m_end.date().year() ); } } } if ( !withTime || (isFirstSecOfDay( m_start ) && isLastSecOfDay( m_end )) ) { if (m_start.date() == m_end.date() ) { // A whole day return m_start.toString( QString::fromLatin1( "d. MMM yyyy" ) ); } else { // A day range return QString::fromLatin1("%1 - %2" ) .arg(m_start.toString( QString::fromLatin1( "d. MMM yyyy" ) ) ) .arg(m_end.toString( QString::fromLatin1( "d. MMM yyyy" ) ) ); } } // Range smaller than one day. if ( withTime && ( !isFirstSecOfDay( m_start ) || !isLastSecOfDay( m_end ) )) return QString::fromLatin1("%1 - %2" ) .arg(m_start.toString( QString::fromLatin1( "d. MMM yyyy hh:mm" ) ) ) .arg(m_end.toString( QString::fromLatin1( "d. MMM yyyy hh:mm" ) ) ); else return QString::fromLatin1("%1 - %2" ) .arg(m_start.toString( QString::fromLatin1( "d. MMM yyyy" ) ) ) .arg(m_end.toString( QString::fromLatin1( "d. MMM yyyy" ) ) ); } bool ImageDate::operator==( const ImageDate& other ) const { return m_start == other.m_start && m_end == other.m_end; } bool ImageDate::operator!=( const ImageDate& other ) const { return !(*this == other ); } QString ImageDate::formatRegexp() { static QString str; if ( str.isEmpty() ) { str = QString::fromLatin1( "^((\\d\\d?)([-. /]+|$))?((" ); QStringList months = monthNames(); for( QStringList::ConstIterator monthIt = months.constBegin(); monthIt != months.constEnd(); ++monthIt ) str += QString::fromLatin1("%1|").arg( *monthIt ); str += QString::fromLatin1("\\d?\\d)([-. /]+|$))?(\\d\\d(\\d\\d)?)?$" ); } return str; } QDateTime ImageDate::start() const { return m_start; } QDateTime ImageDate::end() const { return m_end; } bool ImageDate::operator<( const ImageDate& other ) const { return start() < other.start() || ( start() == other.start() && end() < other.end() ); } ImageDate::ImageDate( const QDateTime& start, const QDateTime& end ) { - if (start <= end) + if (!start.isValid() || !end.isValid() || start <= end) { m_start = start; m_end = end; } else { m_start = end; m_end = start; } } +ImageDate::ImageDate( const QDate& start, const QDate& end ) +{ + if (!start.isValid() || !end.isValid() || start <= end) + { + m_start = QDateTime( start, QTime( 0,0,0 ) ); + m_end = QDateTime( end, QTime( 23, 59, 59 ) ); + } else { + m_start = QDateTime( end, QTime( 0,0,0 ) ); + m_end = QDateTime (start, QTime( 23, 59, 59 ) ); + } +} + static QDate addMonth( int year, int month ) { if ( month == 12 ) { year++; month = 1; } else month++; return QDate( year, month, 1 ); } ImageDate::ImageDate( int yearFrom, int monthFrom, int dayFrom, int yearTo, int monthTo, int dayTo, int hourFrom, int minuteFrom, int secondFrom ) { if ( yearFrom <= 0 ) { m_start = QDateTime(); m_end = QDateTime(); return; } if ( monthFrom <= 0 ) { m_start = QDateTime( QDate( yearFrom, 1, 1 ) ); m_end = QDateTime( QDate( yearFrom+1, 1, 1 ) ).addSecs(-1); } else if ( dayFrom <= 0 ) { m_start = QDateTime( QDate( yearFrom, monthFrom, 1 ) ); m_end = QDateTime( addMonth( yearFrom, monthFrom ) ).addSecs(-1); } else if ( hourFrom < 0 ) { m_start = QDateTime( QDate( yearFrom, monthFrom, dayFrom ) ); m_end = QDateTime( QDate( yearFrom, monthFrom, dayFrom ).addDays(1) ).addSecs(-1); } else if ( minuteFrom < 0 ) { m_start = QDateTime( QDate( yearFrom, monthFrom, dayFrom ), QTime( hourFrom, 0, 0 ) ); m_end = QDateTime( QDate( yearFrom, monthFrom, dayFrom ), QTime( hourFrom, 23, 59 ) ); } else if ( secondFrom < 0 ) { m_start = QDateTime( QDate( yearFrom, monthFrom, dayFrom ), QTime( hourFrom, minuteFrom, 0 ) ); m_end = QDateTime( QDate( yearFrom, monthFrom, dayFrom ), QTime( hourFrom, minuteFrom, 59 ) ); } else { m_start = QDateTime( QDate( yearFrom, monthFrom, dayFrom ), QTime( hourFrom, minuteFrom, secondFrom ) ); m_end = m_start; } if ( yearTo > 0 ) { m_end = QDateTime( QDate( yearTo +1 , 1, 1 ) ).addSecs( -1 ); if ( monthTo > 0 ) { m_end = QDateTime( addMonth( yearTo, monthTo ) ).addSecs( -1 ); if ( dayTo > 0 ) { if ( dayFrom == dayTo && monthFrom == monthTo && yearFrom == yearTo ) m_end = m_start; else m_end = QDateTime( QDate( yearTo, monthTo, dayTo ).addDays(1) ).addSecs(-1); } } // It should not be possible here for m_end < m_start. Q_ASSERT( m_start <= m_end ); } } QDate ImageDate::parseDate( const QString& date, bool startDate ) { int year = 0; int month = 0; int day = 0; QRegExp regexp( formatRegexp(), Qt::CaseInsensitive ); if ( regexp.exactMatch( date ) ) { QString dayStr = regexp.cap(2); QString monthStr = regexp.cap(5).toLower(); QString yearStr= regexp.cap(7); if ( dayStr.length() != 0 ) day = dayStr.toInt(); if ( yearStr.length() != 0 ) { year = yearStr.toInt(); if ( year < 50 ) year += 2000; if ( year < 100 ) year += 1900; } if ( monthStr.length() != 0 ) { int index = monthNames().indexOf( monthStr ); if ( index != -1 ) month = (index%12)+1; else month = monthStr.toInt(); } if ( year == 0 ) year = QDate::currentDate().year(); if ( month == 0 ) { if ( startDate ) { month = 1; day = 1; } else { month = 12; day = 31; } } else if ( day == 0 ) { if ( startDate ) day = 1; else day = QDate( year, month, 1 ).daysInMonth(); } return QDate( year, month, day ); } else return QDate(); } bool ImageDate::hasValidTime() const { return m_start == m_end; } ImageDate::ImageDate( const QDate& start, QDate end, const QTime& time ) { - if ( end.isNull() ) + if ( !end.isValid() ) end = start; if ( start == end && time.isValid() ) { m_start = QDateTime( start, time ); m_end = m_start; } else { if (start > end) { m_end = QDateTime( start, QTime( 0,0,0 ) ); m_start = QDateTime( end, QTime( 23, 59, 59 ) ); } else { m_start = QDateTime( start, QTime( 0,0,0 ) ); m_end = QDateTime( end, QTime( 23, 59, 59 ) ); } } } ImageDate::MatchType ImageDate::isIncludedIn( const ImageDate& searchRange ) const { if ( searchRange.start() <= start() && searchRange.end() >= end() ) return ExactMatch; if ( searchRange.start() <= end() && searchRange.end() >= start() ) { return RangeMatch; } return DontMatch; } bool ImageDate::includes( const QDateTime& date ) const { return ImageDate( date ).isIncludedIn( *this ) == ExactMatch; } void ImageDate::extendTo(const ImageDate &other) { if (other.isNull()) return; if (isNull()) { m_start = other.m_start; m_end = other.m_end; } else { if (other.m_start < m_start) m_start = other.m_start; if (other.m_end > m_end) m_end = other.m_end; } } QStringList DB::ImageDate::monthNames() { static QStringList res; if ( res.isEmpty() ) { for ( int i = 1; i <= 12; ++i ) { res << QLocale().standaloneMonthName(i, QLocale::ShortFormat); } for ( int i = 1; i <= 12; ++i ) { res << QLocale().standaloneMonthName(i, QLocale::LongFormat); } res << i18nc("Abbreviated month name","jan") << i18nc("Abbreviated month name","feb") << i18nc("Abbreviated month name","mar") << i18nc("Abbreviated month name","apr") << i18nc("Abbreviated month name","may") << i18nc("Abbreviated month name","jun") << i18nc("Abbreviated month name","jul") << i18nc("Abbreviated month name","aug") << i18nc("Abbreviated month name","sep") << i18nc("Abbreviated month name","oct") << i18nc("Abbreviated month name","nov") << i18nc("Abbreviated month name","dec"); res << QString::fromLatin1("jan") << QString::fromLatin1("feb") << QString::fromLatin1("mar") << QString::fromLatin1("apr") << QString::fromLatin1("may") << QString::fromLatin1("jun") << QString::fromLatin1("jul") << QString::fromLatin1("aug") << QString::fromLatin1("sep") << QString::fromLatin1("oct") << QString::fromLatin1("nov") << QString::fromLatin1("dec"); for ( int i = 1; i <= 12; ++i ) { res << QLocale().monthName(i, QLocale::ShortFormat); } for ( int i = 1; i <= 12; ++i ) { res << QLocale().monthName(i, QLocale::LongFormat); } for ( QStringList::iterator it = res.begin(); it != res.end(); ++it ) *it = it->toLower(); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageDate.h b/DB/ImageDate.h index 545f168c..b4c2ac26 100644 --- a/DB/ImageDate.h +++ b/DB/ImageDate.h @@ -1,71 +1,72 @@ /* Copyright (C) 2003-2010 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 IMAGEDATE_H #define IMAGEDATE_H #include #include #include namespace DB { class ImageDate { public: ImageDate(); + ImageDate( const QDate& start, const QDate& end ); ImageDate( const QDateTime& start, const QDateTime& end ); ImageDate( int yearFrom, int monthFrom, int dayFrom, int yearTo, int monthTo, int dayTo, int hourFrom, int minuteFrom, int secondFrom ); explicit ImageDate( const QDate& ); explicit ImageDate( const QDateTime& ); ImageDate( const QDate& start, QDate end, const QTime& time ); QDateTime start() const; QDateTime end() const; static QDate parseDate( const QString& date, bool startDate ); bool operator<( const ImageDate& other ) const; bool operator<=( const ImageDate& other ) const; bool operator==( const ImageDate& other ) const; bool operator!=( const ImageDate& other ) const; bool isValid() const { return !isNull(); } bool isNull() const; bool isFuzzy() const; QString toString( bool withTime = true ) const; bool hasValidTime() const; enum MatchType { DontMatch, ExactMatch, RangeMatch }; MatchType isIncludedIn( const ImageDate& searchRange ) const; bool includes( const QDateTime& date ) const; void extendTo( const ImageDate& other); protected: static QStringList monthNames(); static QString formatRegexp(); private: QDateTime m_start, m_end; }; } #endif /* IMAGEDATE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageSearchInfo.cpp b/DB/ImageSearchInfo.cpp index 27c9f2f4..8c87037a 100644 --- a/DB/ImageSearchInfo.cpp +++ b/DB/ImageSearchInfo.cpp @@ -1,634 +1,634 @@ /* Copyright (C) 2003-2015 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 "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; } 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().isNull() ) { + if ( m_date.start().isValid() ) { if ( actualEnd < m_date.start() || - ( !m_date.end().isNull() && actualStart > m_date.end() ) ) + ( m_date.end().isValid() && actualStart > m_date.end() ) ) return false; - } else if ( !m_date.end().isNull() && actualStart > m_date.end() ) { + } else if ( m_date.end().isValid() && actualStart > m_date.end() ) { return false; } // -------------------------------------------------- Label if ( m_label.isEmpty() && info->label().indexOf(m_label) == -1 ) return false; // -------------------------------------------------- RAW if ( m_searchRAW && !ImageManager::RAWImageDecoder::isRAW( info->fileName()) ) return false; #ifdef HAVE_KGEOMAP // Search for GPS Position if (m_usingRegionSelection) { if ( !info->coordinates().hasCoordinates() ) return false; float infoLat = info->coordinates().lat(); if ( m_regionSelectionMinLat > infoLat || m_regionSelectionMaxLat < infoLat ) return false; float infoLon = info->coordinates().lon(); if ( m_regionSelectionMinLon > infoLon || m_regionSelectionMaxLon < infoLon ) return false; } #endif // -------------------------------------------------- File name pattern if ( !m_fnPattern.isEmpty() && m_fnPattern.indexIn( info->fileName().relative() ) == -1 ) return false; // -------------------------------------------------- Options // alreadyMatched map is used to make it possible to search for // Jesper & None QMap alreadyMatched; for (CategoryMatcher* optionMatcher : m_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 ) { 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 m_regionSelection = other.m_regionSelection; #endif } void ImageSearchInfo::compile() const { m_exifSearchInfo.search(); #ifdef HAVE_KGEOMAP // Prepare Search for GPS Position m_usingRegionSelection = m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates(); if (m_usingRegionSelection) { using std::min; using std::max; 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; } 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 KGeoMap::GeoCoordinates::Pair ImageSearchInfo::regionSelection() const { return m_regionSelection; } void ImageSearchInfo::setRegionSelection(const KGeoMap::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: