diff --git a/AnnotationDialog/Dialog.cpp b/AnnotationDialog/Dialog.cpp index 6a837606..74370cd1 100644 --- a/AnnotationDialog/Dialog.cpp +++ b/AnnotationDialog/Dialog.cpp @@ -1,1749 +1,1751 @@ /* 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 " + "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." + "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." )); + 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(i18n("KPhotoAlbum Annotations (%1/%2)", m_current + 1, m_origList.count())); + 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() ); 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( i18n("Annotations") ); + 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..." ) ); + ->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"), + 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 ); + 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"), + 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); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) // This conditional block is added to still be compatible with distributions shipping // older Qt versions. TODO: remove the check for Qt 5.6 as soon as it's reasonable // 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); #endif #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)") ); action->setShortcut(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") ); action->setShortcut( 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") ); action->setShortcut( Qt::CTRL+Qt::Key_S ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-next-image"), m_preview, SLOT(slotNext()) ); action->setText( i18n("Annotate Next") ); action->setShortcut( Qt::Key_PageDown ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-prev-image"), m_preview, SLOT(slotPrev()) ); action->setText( i18n("Annotate Previous") ); action->setShortcut( Qt::Key_PageUp ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-OK-dialog"), this, SLOT(doneTagging()) ); action->setText( i18n("OK dialog") ); action->setShortcut( Qt::CTRL+Qt::Key_Return ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-delete-image"), this, SLOT(slotDeleteImage()) ); action->setText( i18n("Delete") ); action->setShortcut( 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") ); action->setShortcut( 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") ); action->setShortcut( Qt::CTRL + Qt::Key_Space ); foreach (QAction* action, m_actions->actions()) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } } 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/BackgroundTaskManager/JobViewer.cpp b/BackgroundTaskManager/JobViewer.cpp index cf92b848..171dd460 100644 --- a/BackgroundTaskManager/JobViewer.cpp +++ b/BackgroundTaskManager/JobViewer.cpp @@ -1,80 +1,80 @@ /* Copyright 2012-2016 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) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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. If not, see . */ #include #include #include #include #include #include "JobViewer.h" #include "JobModel.h" #include "JobManager.h" BackgroundTaskManager::JobViewer::JobViewer(QWidget *parent) : QDialog(parent), m_model(nullptr) { - setWindowTitle(i18n("Background Job Viewer")); + setWindowTitle(i18nc("@title:window", "Background Job Viewer")); QVBoxLayout* mainLayout = new QVBoxLayout; setLayout(mainLayout); m_treeView = new QTreeView; mainLayout->addWidget(m_treeView); QDialogButtonBox* buttonBox = new QDialogButtonBox; m_pauseButton = buttonBox->addButton(i18n("Pause"), QDialogButtonBox::YesRole); buttonBox->addButton(QDialogButtonBox::Close); connect(m_pauseButton, SIGNAL(clicked()), this, SLOT(togglePause())); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::accept); mainLayout->addWidget(buttonBox); } void BackgroundTaskManager::JobViewer::setVisible(bool b) { if (b) { m_model = new JobModel(this); m_treeView->setModel(m_model); updatePauseButton(); } else { delete m_model; m_model = nullptr; } m_treeView->setColumnWidth(0, 50); m_treeView->setColumnWidth(1, 300); m_treeView->setColumnWidth(2, 300); m_treeView->setColumnWidth(3, 50); QDialog::setVisible(b); } void BackgroundTaskManager::JobViewer::togglePause() { JobManager::instance()->togglePaused(); updatePauseButton(); } void BackgroundTaskManager::JobViewer::updatePauseButton() { m_pauseButton->setText(JobManager::instance()->isPaused() ? i18n("Continue") : i18n("Pause")); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/OverviewPage.cpp b/Browser/OverviewPage.cpp index d130482f..b6e85ea3 100644 --- a/Browser/OverviewPage.cpp +++ b/Browser/OverviewPage.cpp @@ -1,352 +1,352 @@ /* Copyright (C) 2003-2016 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 "OverviewPage.h" #include "BrowserWidget.h" #include "CategoryPage.h" #include "enums.h" #include "ImageViewPage.h" #include #ifdef HAVE_KGEOMAP #include #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 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 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 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 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 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_count[index.row()] <= 1 ) 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() ); + 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 ); m_count[row] = items.count(); ++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/ChangeLog b/ChangeLog index 128030a6..b97b3ce4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,2636 +1,2636 @@ ====================== KPhotoalbum 5.3 released (20.01.2018) ====================== * Bugfix: Fix crash with prepared SQL queries on Qt 5.10. * Enhancement: Speed up startup by not counting images several times. * Enhancement: Improve robustness of video length detection and thumbnail extraction. * Change: Purged libkface from KPhotoAlbum. It's sadly not maintained anymore, so we are forced to finally remove the code. * Enhancement: Made the full screen image preview in the annotation dialog reachable by a button. * Change: Display absolute numbers instead of percentage when loading new images. * Bugfix (#385551): Remove dead/unreachable code. * Enhancement: Speed up video thumbnail creation. * New: Added the "kpa-merge" script (by Robert Krawitz), which can merge two (version 7) databases and also remove unused labels. * Enhancement: Speed up removal of images from database. * Enhancement: Add a "Maximum megapixels" option to the search dialog. * Enhancement: Don't re-compute an MD5 sum we already have when adding new images. * Bugfix: Overall KF5 port regression fixes (missing buttons etc.). * Bugfix: Sometimes, areas weren't updated correctly when annotating images and changing between them. ====================== KPhotoalbum 5.2 released (26.02.2017) ====================== * Enhancement: Add the "Geo Position" selection page to the browser (Thanks to Matthias Füssel for the original patch!) * Bugfix: Fixed disabled items in browser overview page. * Bugfix (#376635): Fix renaming and deletion of memberMap members. * Change: Maintained compatiblity with Qt < 5.6 (for now). * Bugfix: Fixed compilation with -fno-operator-names. * Bugfix: Added .kim import test case and fixed the import when using external images. * Enhancement: Improved the default layout for annotation dialog. * Bugfix (#374431): Prevent invalid characters in image description. * Bugfix: Fixed the path of kphotoalbumui.rc in kpa-backup.sh * Bugfix: Reviewed and fixed the install directories. * Bugfix: Fixed the date view on the birthdays page. * Bugfix: Fixed some issues in the thumbnail build progress counter. * Bugfix: Don't show empty descriptions in the InfoBox. * Bugfix: Fixed the search dialog. * Bugfix: Fixed the "remove tokens" dialog. ====================== KPhotoalbum 5.1 released (16.12.2016) ====================== * Enhancement (Bug# 370373): Made writing index.xml deterministic, so that diffing it produces minimal changes. * Bugfix: Fixed areas not drawn when a photo was viewed after a video in the Annotation Dialog. * Bugfix (#373263): KPA crashed when editing a birth date. Also restored the functionality if the whole birth date dialog, which was broken due to the KF5/Qt5 port. * Bugfix (#373128): Fixed the "Copy image(s) to..." and "Link image(s) to..." menu. * Bugfix: Prevent duplicate positionable tags. All except one are lost when saving. Apparently, nobody noticed this flaw since the introduction of positionable tags ;-) * Enhancement: Improved the workflow with tagged areas by adding a new dialog where a just-drawn area can be tagged at once, without having to move the mouse around and multiple clicks. * Change: Disable face recognition and detection by default. libkface is simply not good enough for general public consumption. The code can still be enabled via the ENABLE_PLAYGROUND cmake flag. * Bugfix: Prevent the image preview from flickering in the Annotation Dialog. * Bugfix: Fixed various issues with the Android Remote Control. ====================== KPhotoalbum 5.0.1 released (18.10.2016) ====================== * Version confusion: Well, it's _still the first_ beta release, and hopefully, we will make it this time. KDE's releaseme tool for KF5 does not support our automatic version generation anymore, so we had to adjust our sources. Sorry for the tagh spam ;-) ====================== KPhotoalbum 5.0-beta2 released (08.10.2016) ====================== * "beta2"?: Yes, beta2. We actually did a "beta1" release, but it contained some bugs/regressions that broke the whole thing, so we decided to not really relese it. But the tag is there ... ;-) * Overall changes (there were lots and lots of them): Ported KPA to Qt 5 and KF5, including updating all depreceated code. We also do not depend on the KDE4LibsSupport compatibility class. Cleaned up includes and stray references, dropped some legacy compatibility code. Updated the (many/most of the) icons to get a uniform "KF5 look". Revised commandline options. Use ffmpeg/ffprobe for video if available. Usability improvements for HTML export. We now ship AppData. Fixed some leaks detected by Valgrind and warnings by the clang static analyzer. ====================== KPhotoalbum 4.7.2 released (29.07.2016) ====================== * Bugfix: Fix building without KIPI. * Bugfix: Annotation dialog: Fix up/down key if there's a single match for an entered string. * Enhancement: Tag names are now matched word by word against all parts of the search string (using the Search dialog). * Bugfix: Prevent duplication of special categories when updating to dbv7. * Bugfix: Don't exclude "Z" when filling the "Tokens" category for the first time. * Bugfix: Overall fixes of assertions. * Change: Dropped compatibility with libkipi < 2.0. ====================== KPhotoalbum 4.7.1 released (22.02.2016) ====================== * Bugfix: Fix broken category settings dialog (categories can be added, renamed etc. again, was a regression of the category l10n removal). * Bugfix: Fix i18n problems with special categories. * Bugfix (#358971): Make import/export file filter translatable. * Bugfix: Fix overflow in viever cache size computation (Thanks to Robert Krawitz for pointing this out!). * Bugfix: Fix crash on i386 due to an uninitialized static variable (Thanks to Matthias Heukäufer for reporting the bug!). -* Enhancement: Various improvements on the EXIF DB. +* Enhancement: Various improvements on the Exif DB. ====================== KPhotoalbum 4.7 released (19.01.2016) ====================== * Bugfix: Various fixes of compilation failures on different distributions/gcc versions, as well as various fixes of crashes caused by regressions. * Enhancement/UI: The category settings dialog doesn't tell the user anymore that he must save the database now after renaming categories. Instead, the saving is done automatically now, and category name changes can only be done on a "clean" (saved) database, so that no unwanted changes will be auto-saved. * Enhancement: Better performance for single- and dual-core processors when creating thumbnails. * Enhancement: Various enhancements for tne RAW support. * Change: The "standard categories" are not translated anymore, due to a lot of problems with this approach. Each category's name is now stored as-is in the database. The "Tokens" category's name and icon can also be changed now. * Bugfix (#348096): Fix crash when accessing an image via KIPI plugins which has been deleted from the disc, but not yet from the collection. * Bugfix: Don't crash if a category is renamed or deleted which holds the currently selected "untagged images" tag. * Bugfix: If a category holding tag groups was renamed, the tag groups were lost. * Enhancement: Added a "mark as untagged" action, as proposed by the patch of Reimar Imhof. * Bugfix: Several fixes about the "untagged image" tag (counting, visibility etc.) * Enhancement: Added category drag and drop support for the browser's category page. * Enhancement: Added GPS coordinate search functionality for the search dialog as proposed by Reimar Imhof. * Bugfix: Fix crash: Decrease the rowCount of the items list when an item is skipped due to a filter input so that KPA won't crash when an empty row is clicked (and the row count actually matches the number of displayed items). * Enhancement: Add python script to show birthday info. Also Bugfixes. -* Enhancement: Enable exif search for lens data. Also various enhancements in the general handling - of EXIF lens data (esp. better lens info for generic canon lenses). +* Enhancement: Enable Exif search for lens data. Also various enhancements in the general handling + of Exif lens data (esp. better lens info for generic canon lenses). * Enhancement: Display the image's aspect ratio in the ctrl-i image overlay. * Bugfix: Ignore empty input while quick-tagging. * Enhancement: Add "link images" to context menu. * Bugfix: Fixed copy tags from last image (tags already associated with an area were added to the candidate list when they shouldn't be). ====================== KPhotoalbum 4.6.2 released ====================== * Bugfix: Fixed several compilation issues for various distributions and/or gcc versions. * Bugfix: Fix month plural when formatting age. * UI: Show the number of trained faces on the Face management settings page. * Enhancement: Add AfterShot2 to open-raw.pl. * Change: Do the dbv6 update without asking the user. But create a backup of all (probably) changed files before. Hopefully, this will be more (end-)user friendly and everybody gets a backup for free :-) * Bugfix: Hide the "untagged image" tag also when typing in parts of it's name. ====================== KPhotoalbum 4.6.1 released ====================== * UI: Moved the birth date dialog to the settings and added more functionality (typing the date directly, sorting by tag name or date). * Bugfix (#344462): Don't build thumbs when new image finder is cancelled. * Bugfix (#344419): When a new tag is added, select this very newly added tag, not the first tag that contains the new tag's name (which can as well be another one). * Improvement: better wording for DB upgrade messages. * Bugfix (#343822): Prevent KPA from crashing when a tag is dropped between two other tags and not onto one. * Bugfix (#342510): Fix crash with face recognition with some tag hierarchies. * Fix evaluation of GPS data for some cameras. ====================== KPhotoalbum 4.6 released ====================== * Enhancement: Allow incremental building of thumbnail cache. * UI: Add slider for resizing the thumbnail grid. * Enhancement: Show a message if the untagged tag feature is misconfigured. * Bugfix(#322514): Allow resizing of thumbnail grid without rebuilding the thumbnail cache. * Bugfix(#334393): Handle image path correctly when importing from a .kim file. * New Feature: Display images on a map (available in the annotation dialog and in the image viewer) * Change: Rename old category names in the database. "Persons" becomes "People", "Locations" becomes "Places". * Change: new database file version is 6. * Enhancement: better performance on NFS (#340127). * Bugfix(#340963): better handling of the '&' character in folder names. * Enhancement: Improved category and tag groups (subcategories) pages in the settings dialog. * New Feature: Face detection and recognition using libkface. * Bugfix(#338870): Display a dialog if index.xml has a syntax error. * New Feature: Specify the birth date for people (or any other category item for that matter) in Maintanence->Edit Birth Dates, and see their age when viewing images. * New Feature: Add a context menu option to copy images from the thumbnail view. ====================== KPhotoalbum 4.5 released ====================== * New Feature: Added an Android client, which can display images from a running KPhotoAlbum on the desktop * New Feature: Tags can now be associated with an area of the image. * Enhancement: Backspace now shows the previous image in the Viewer * Enhancement: Implement file copy dialog in the Viewer. -* Enhancement: Added option to ignore specific EXIF comments. - (Some digital cameras set a non-configurable EXIF comment in each image. +* Enhancement: Added option to ignore specific Exif comments. + (Some digital cameras set a non-configurable Exif comment in each image. With this option you can specify which comments should be ignored.) * Bugfix (#254641): The current image is now highlighted when selecting images using the keyboard. * Enhancement: On first start, the image folder defined in KDE settings is used. * Bugfix: find thumbnail even for short videos (Thanks to Christoph Moseler for identifying the problem, and sending an initial patch) * Enhancement: Implemented the Page-Up and Page-Down keypresses to select the previous and next image in the annotation dialog * Bugfix: Fix image import for kim files with external images. * Bugfix: Fix crash when reading .kim files. * Enhancement: Updated HTML darkJS theme * New Feature: Maintainance->sort all by date and time (With this command, you don't have to worry about stacks, locked images, or similar) * Bugfix: Prevent thumbnail progress bar from showing indefinitely when no MPlayer is installed. * Enhancement: Make thumbnail cache readable by all users. (This makes sharing a kphotoalbum database actually possible.) * Enhancement: Implement caching for thumbnail files. * Enhancement: Recalculate md5sum when image is changed by kipi-plugin. * Bugfix: Mark database dirty when images are deleted. * Enhancement: Run without video thumbnail/length support when mplayer is missing. * Bugfix: Don't try creating thumbnails for images not on disk. * Enhancement: Use cached QImage instead of invoking KIcon::pixmap on broken files * Enhancement: Add --info to script/kpa-backup.sh. ====================== KPhotoalbum 4.4 released ====================== Other changes done since 4.3 * Support for kipi 2.0.0 * Greatly improved database save performance 2012-10-29 Jesper K. Pedersen * NEW Feature: Maintenance -> Merge duplicates This will search for images with the same MD5 sum and show a dialog for selecting which of the duplicates to remove. Tag of the duplicates will be merged. If one of the duplicates are marked as fully tagged, the result will also be marked as fully tagged 2012-10-21 Jesper K. Pedersen * BUGFIX: Tooltips did not update when scrolling using the wheel * BUGFIX: Tooltip did not work on videos ====================== KPhotoalbum 4.3 released ====================== Other changes done since 4.2 * Minor bug fixes * Some speed optimization * Pattern based image search * Minor documentation update * KIPI support upgrade * New manual stacking option 2012-08-17 Jesper K. Pedersen * New Feature: press Ctrl-plus or Ctrl-minus in the thumbnail viewer to use the next video thumbnail (from the 10 extracted thumbnails) as the one displayed in the thumbnail viewer 2012-07-17 Jesper K. Pedersen * New Feature: Video thumbnails are now extracted using the new background thumbnail extractor rather than with MPlayerThumbs. 2012-06-10 Jesper K. Pedersen * New Feature: Introduced an led in the status bar, which blinks when background jobs are executed. Click on the led to get a detailed view of which jobs are executing. 2012-06-03 Jesper K. Pedersen * Internal: Removed the SQL backend. This includes removing the classes DB::Id and DB::IdList. This change was to simplify code, as the introduction of these two classes made a lot of code more complex. 2012-05-16 Jesper K. Pedersen * Internal: Introduced the class DB::FileName to represent a filename in the database. This is to solve the problem that files previously was represented by a QString, and it was impossible to know if the filename was relative to the image root or an absolute path. 2012-05-14 Jesper K. Pedersen * Add reasonable defaults for "Settings->File Searching and Versions->Copy File and Open with an External Application" 2012-05-12 Jesper K. Pedersen * BUGFIX: Exif info is now correctly extracted from .thm and .THM files again (has been broken since January 2006) 2012-05-09 Jesper K. Pedersen * New Feature: KPA now shows video lengths on top of video thumbnails. * New Feature: When you hover your mouse over a video thumbnail, 10 images from that video is displayed in place of the thumbnail. 2012-04-22 Jesper K. Pedersen * New feature: While watching videos you can now choose a given frame to be the image to show in the thumbnail view. ====================== KPhotoalbum 4.2 released ====================== Other changes done since 4.1.1 * Helper scripts for RAW workflow and backup * Documentation and tips * Fixes to prevent file corruption * Annotation dialog and search improvements * HTML export improvements * Option to skip symbolic links when detecting new files * Multi core CPU improvements * Background thumbnail building * Thumbnail pre-loading * Thumbnail tooltip fixes * Plenty of other bug and crash fixes 2011-05-30 Jesper K. Pedersen - * Improvement: If you start the exif viewer from the image viewer, + * Improvement: If you start the Exif viewer from the image viewer, then going to the next image in the image viewer, will update the - data shown in the exif viewer. + data shown in the Exif viewer. - If you want to show two exif viewers for two different files, then - simply start two exif viewer on an image, only the latest one + If you want to show two Exif viewers for two different files, then + simply start two Exif viewer on an image, only the latest one started will update when the image changes. 2011-05-15 Miika Turkia * Added video extensions webm, mts, ogg and ogv (fixes bug #269579) 2011-01-09 Miika Turkia * Fixed bug #262127, compile fails with exiv2-0.21 2010-12-22 Miika Turkia - * Fixed bugs #255286 and #237889, crashes on missing exif information + * Fixed bugs #255286 and #237889, crashes on missing Exif information 2010-10-22 Jesper K. Pedersen * Added "Export by symlink" to the export dialog. Thanks to Robert Krawitz for a patch. * Bugfix: If you right click on an image in the viewer and accidentally hover over Run Program (on the selection), it is extremely slow. Thanks to Robert Krawitz for a patch 2010-09-08 Miika Turkia * Patch to display filter button correctly, enable collapsing super categories, and copying with drag-drop categories (annotation dialog). Thanks to Reimar Imhof for the patch. 2010-09-07 Miika Turkia * bugfix: Autostack set to ignore images with no exact timestamp * Autostack affects selected images only (instead of whole database). Thanks to Andreas Neustifter for the patch. 2010-09-05 Miika Turkia * Tooltip is shown on thumbnail view only if mouse is idle for 200ms. 2010-08-30 Miika Turkia * Opening of annotation dialog: sort button selection fits real sorting. Sort menu items now have check boxes. Thanks to Reimar Imhof for the patch. * Fixes bug that prevented deselecting images in thumbnail view. Thanks to Reimar Imhof for the patch. 2010-08-28 Miika Turkia * With the new Thumbnail-View KPA crashes when creating a stack from 2 selected images (either via Ctrl-3 or the context menu). Thanks to Andreas Neustifter for reporting and patch. * Added option to sort category items aphabetically on annotation dialog (tree view alphabetical, flat alphabetical and most recently used are the currently supported options). Thanks to Reimar Imhof for the patch. 2010-08-19 Jesper K. Pedersen * ThumbnailView has been completely rewritten, so that it is much faster. * Thumbnail (re)building is now automatically build in the background when needed. 2010-07-07 Miika Turkia * Implemented automatic stacking of images. Images that are shot within specified time frame are stacked together (supports also stacking of matching MD5 sum as requested by someone on the IRC channel). 2010-07-03 Jesper K. Pedersen - * Continue reading info exif from files even if some fails. Thanks + * Continue reading info Exif from files even if some fails. Thanks for patch from Robert Krawitz. * Disable screensaver when running a slideshow. Thanks to Christian Schafmeister for a patch for this. * It is now possible to copy tags from one image in the browser (using Ctrl+c) and then paste that to several other images using Ctrl-v. Thanks to Andreas Neustifter for a patch with this feature. 2010-06-20 Miika Turkia * Fixed two bugs from HTMLExport theme darkJS that prevented w3c compliancy * Fixed two bugs from HTMLExport theme dark that prevented w3c compliancy * Fixed rating in viewer mode (numbers 1 to 5 give star rating) 2010-02-15 Miika Turkia * fixed bug #182046: Re-read metadata settings not saved. Thanks to Andreas Neustifter for patch. 2010-02-13 Miika Turkia * Removing blue link borders from images on HTMLExport theme dark. Thanks to Andreas Neustifter for patch. 2010-01-31 Miika Turkia * bugfix: HTML generator theme darkJS did not show fullsize images as dimensions were reported as -1. Now dimensions are retrieved from the first image if dealing with fullsize images. (Added also a version number to the theme.) 2010-01-30 Miika Turkia * Moved modified file detection configurations from General tab to it's own configuration tab called 'File Versions' (General tab was taller than my screen) * Error message changed to display correct menu text 2010-01-24 Miika Turkia * GUI: Added checkboxes for display filters to view which ones are selected. * Added check that color depth is enough for each filter. * bugfix: File name based autostacking of new images relied on relative paths and did not work if KPA was started from elsewhere 2010-01-23 Miika Turkia * Minor tweak on HTMLGenerator theme darkJS to highlight displayed thumbnail * New filters added for contrast streching and histogram equalization 2010-01-15 Jan Kundrát * Make copying a list of images with Ctrl+C work on all images, regardless if they are currently available on the disk or not. 2010-01-15 Hassan Ibraheem * Draw stack indicator correctly for images with different dimensions. 2010-01-06 Miika Turkia * Added possibility to use embedded thumbnails instead of decoding RAW files. User is able to specify what is the minimum size for the embedded thumbnail to be used. * Remove of some code that became dead with yesterdays bugfix 2010-01-05 Miika Turkia * fixed bug #220373: Viewer settings dialog did not load correct values and had wrong defaults 2010-01-04 Miika Turkia * ImageViewer and SlideShow added to HTMLGenerator's theme darkJS 2010-01-01 Miika Turkia * fixed bug #159718: Excluding subdirecories like .thumbs. Thanks to Michal Yogi Práznovský 2009-12-31 Miika Turkia * fixed bug #145735: Move deleted files to Trash. Thanks to Kirill Kalyuzhniy 2009-12-30 Miika Turkia * Tip added to inform users about rating with number keys 2009-12-30 Jesper K. Pedersen * fixed bug #220376: Shortcuts in the annotation dialog was broken in some translations * bugfix: KPA crashes when viewing images if viewer cache size is set to zero 2009-12-25 Jan Kundrát * Added a simple `script/focal-length-hist.py` for printing out a histogram of lens focal lengths used. It's a rather quick & dirty hack, but might be useful when deciding which focal lengths you use most before buying new lens. 2009-12-24 Jesper K. Pedersen * Bugfix: no more multiple lineedits with focus in the annotation dialog. * Now a viewer can be invoked from the annotation dialog by pressin Ctrl+Space * At end of viewing images, KPhotoalbum will now ask if you want to delete those images from disk that you deleted (by pressing del) during the viewing process. 2009-12-20 Miika Turkia * Added support for rating images using numbers from keyboard (0-5) * bugfix: KPA crashes when viewing images if viewer cache size is set to zero * On exporting HTMLGallery the escaping of quotes and carriage returns is required only for the JS array, on image page escaping the quotes actually messes up the HTML code. 2009-12-19 Miika Turkia * Added support for exporting picture date on HTMLGallery * bugfix: JavaScript based HTMLGallery theme shows information of wrong picture upon initial loading * Added support for HTMLGallery theme darkJS to navigate through images with arrow keys (left and right) 2009-11-25 Hassan Ibraheem - * fixed bug #211907: Fix EXIF support dialog at startup. Thanks to Olivier Berger + * fixed bug #211907: Fix Exif support dialog at startup. Thanks to Olivier Berger 2009-11-19 Hassan Ibraheem * fixed bug #147891: Crashing on wrong export directory in Plugin interface 2009-11-08 Hassan Ibraheem * fixed bug #211392: Incorrect rotation angle after annotating multiple images.Thanks to Andreas Neustifter. ====================== KPhotoalbum 4.1 released =================================== 2009-09-02 Miika Turkia * Redirecting with no index.html in URL added and cleaned up some comments in the theme darkJS 2009-08-28 Jan Kundrát * Fix segfaults when dragging images before the first one and after the last one 2009-08-15 Hassan Ibraheem * fixed bug #171520: preview images when annotating multiple images at once 2009-08-14 Jesper K. Pedersen * Improved zooming in rotated images (by Hassan Ibraheem) 2009-07-25 Jesper K. Pedersen * Show the file list directly in the Read Exif Info dialog. Previously it was a secondary dialog, with a do-not-show-again checkbox and a cancel button (none of those two made any sense there) 2009-07-20 Jesper K. Pedersen * Add a keybinding for delete in the viewer that will take the image of the display list. This is useful when comparing a number of images and want to take the worst one away every time to find the best one. Thanks to Wes Hardaker for a patch implementing this. 2009-07-19 Jesper K. Pedersen * Now it is possible to chose the thumbnail storage format from the settings dialog (ppm files which we used by default are pretty large, so on limited disk space you may wish to choose jpg instead to trade some speed for disk space. (thanks to Franck Sicard for a patch for this) * To move items around in the thumbnail viewer, you now first have to select them, then move them. This makes it so much easier to select items, as you do not have to find an empty space to start. * Once more updated the look of the icons in the thumbnail viewer. This time I copied the look and fell from Gwenview, which I hope everyone will like. * Refactored the code of the thumbnail viewer, so it now is much more clean and understandable. 2009-07-14 Jesper K. Pedersen * Thumbnail view icons are now square again, and background color is customizable. The default has changed slightly, so the background color is dark gray, and spacing are 4 pixels. This gives IMHO the best 3D effect. 2009-07-24 Jesper K. Pedersen * fixed bug #201116: kphotoalbum crashes when trying to remove image without deleting from disk 2009-07-12 Jesper K. Pedersen * Exif database insertion works again. Thanks to Matthias Fussel 2009-07-12 Jesper K. Pedersen * Fixed bug #199629 - new images don't trigger dirty state, thanks to mat.fuessel@gmx.net * Fixed bug #150971 - disable too low zoom levels, thanks to alexjironkin@gmail.com 2009-07-11 Jesper K. Pedersen * Now it is possible to add a category image from a context menu, which makes that process much faster (this fixes bug #195023). Thanks to Thomas Pircher for the work on that. 2009-07-10 Jesper K. Pedersen * Resizing the category overview page now is done without the actual thumbnails, that makes it much faster and much more smooth * Removed a lot of includes that was either no longer needed, or which at least could be replaced with a forward declaration. * Improved focus handling in the annotation dialog 2009-07-07 Hassan Ibraheem * Added support for searching by rating. 2009-07-05 Miika Turkia * Automatic redirection to smallest image sizes for theme dark JavaScript. Now the theme in question views smallest images on page and biggest images when the thumbnail or full sized image is clicked. 2009-07-04 Miika Turkia * bugfix: taking video files into account in HTML generation in JavaScript part (show thumbnail instead of video file). * Having full size image in the JS array instead of pagename for same sized imagepage. * Support for storing most of the settings (all relevant) of HTMLGallery. * Support for default theme (Default=true in kphotoalbum.theme file) * Added theme "Dark JavaScript" - if JavaScript is supported images are shown using it on main page - this is the default theme 2009-07-03 Hassan Ibraheem * Added support for rating multiple images at once in the annotation dialog. 2009-07-01 Miika Turkia * fixed bug #197107: Generated HTML pages have empty theme name and theme author. 2009-06-29 Jesper K. Pedersen * Made it much more explicit when done tagging images: - An item for showing images not yet tagged are available in the browser - A configuration option is available for specifying which category and tag should be used to specify untagged images. - This tag is set on images when loaded in - This tag is removed when selecting "done tagging" in the annotation dialog. It is also possible to select "continue later" in that dialog 2009-06-22 Jesper K. Pedersen * Speed up browsing of categories with many sub categories. This took browsing from the order of magnitude of half minutes to a second. * Added new theme for HTML pages which includes improved support using java script. (Thanks to Miika Turkia for a patch for this) * Added support for a copyright tag on the HTML generated pages (Thanks to Miika Turkia for a patch for this) 2009-06-21 Jesper K. Pedersen * When AnnotationDialog is called with multiple images, show selected only shows also partially selected items. Thanks to Hassan Ibraheem for patch. * fixed bug #171517: Repopulate categories lists whenever an image is loaded. Thanks to Hassan Ibraheem for patch. 2009-06-16 Jesper K. Pedersen * fixed issue #196095 - Add icon for hide non selected. Thanks to Hassan Ibraheem for patch. 2009-06-14 Jesper K. Pedersen * fixed bug #165742: ListSelect blanks out when typing an initial letter that does not match. Thanks to Hassan Ibraheem for the patch for this. * fixed bug #167045: add ENTER as search dialog tag auto completion key. Thanks to Hassan Ibraheem for the patch for this. * bug fix: The remove token dialog was broken second time it was brought up. * fixed bug #186708 - Tokens can not be access from the annotation dialog 2009-06-10 Jesper K. Pedersen * Somehow the XML database can get a file loaded twice. As a remedy until we find the real bug, we will ignore the new files when they are loaded from the database. Thanks to Franck Sicard for a patch for this 2009-06-08 Jesper K. Pedersen - * New images was not correctly inserted into the exif database + * New images was not correctly inserted into the Exif database (thanks to Bart Visscher for a patch that fixes this.) Please rerun "Maintainance->Recreate Exif Search Database" to ensure your database it working again. 2009-06-02 Jesper K. Pedersen * Reworked the browser completely, which has the following consequences: * Many of the changes are internal restructuring of the code to use Model/View * The GUI is now using Qt4 classes, which looks much more sexy * When limiting the content using the line edit, it will try and be clever about which branches are open * When limiting, all items of a sub category are shown if its parent matches. As an example of this, imagine a super category California, with sub categories Las Vegas and Los Angeless. If you type California in the search line edit, then both children will still be shown * The search bar will now keep focus all the time and send movement keys to the browser. This makes it easier to narrow in the browser using the searchbar. * The overview page is now shown in icon mode with icons centered 2009-05-31 Jesper K. Pedersen * Avoid crash in case "make install" was not executed. ====================== KPhotoalbum 4.0.1 released =================================== Other changes done since 4.0 * Fix compilation with new Marble and armel (Marble changed API again) * fixed bug #186807 - no cancel button in "create own database dialog" * fixed bug #192385 - it was not possible to hide items in the annotation between sessions. 2009-05-13 Jesper K. Pedersen * Changed default shortcut for copy from previous image in the annotation dialog to Alt+insert, as control+insert was eaten by the line edits, and the shortcut did thus not work when they had the keyboard focus. * BUGFIX: The HTML generator did not display the available themes correctly. Thanks to Miika Turkia for a patch for this. 2009-05-11 Jesper K. Pedersen * BUGFIX: Invoke external application for multiple files did not work. Thanks to Miika Turkia for a patch for this. ====================== KPhotoalbum 4.0 released =================================== Previous changes * Add a simple method for marking current image as a "head of stack", ie. the one to show * Fix abort() on shift-click to the middle of ThumbnailWidget right after program start * For some reason konsole doesn't accept drops and pastes of QUrls, so I encode as a plain string list in addition. * Invalide date finder dialog has now been improved slightly (the config dialog is hidden as soon as it is OK, so we can see theprogress dialog) * fixed bug #164199 - thumbnail label is chopped * fixed bug #167036 - kipi-plugins not installed; plugins menu still available but empty * Stopped leaking memory/threads when viewing video * fixed bug #164587 - KPA crashes when deleteing an image from the annotation dialog. 2009-03-08 Jesper K. Pedersen * Saving the toolbar now works again, including showing icons only. * Generate HTML is now working again. 2009-02-23 Jesper K. Pedersen * Removed the code for setting an image as wall paper (That has not yet been ported to KDE 4) 2009-02-21 Jesper K. Pedersen * In the video viewer, it is now possible to zoom the image. * seeking in the video widget is now working ====================== Snapshot 11/2-2009 =================================== 2008-09-19 Jesper K. Pedersen * Removed the survey. 2008-09-15 Henner Zeller * Implement thumbnail precaching of the next/previous page. 2008-09-07 Jan Kundrat * Image rating (KRatingWidget, that stars thingy) 2008-09-05 Jan Kundrat * Image stacking -- grouping somehow related images together 2008-09-01 Jan Kundrat * Remove flickering in the ThumbnailView 2008-08-30 Jan Kundrat * Fix recognition of video files whose name contains more than one dot 2008-07-22 Jesper K. Pedersen * new icons for the default categories. These icons will now by default be used in case the icon specified in the setup does not exists - this will help users migrating from KDE3 KPA to KDE4 version, where the default installed icons have changed. 2008-07-20 Jesper K. Pedersen * Keywords has now been replaced with Events in the default setup that people will get when they create a new database. 2008-07-17 Jesper K. Pedersen * (regression) The text editor in the annotation dialog now offers spell checking again - actually it is a completely new widget. 2008-07-16 Jesper K. Pedersen * (regression) the search bar is now correctly highlighted again. * New feature: The import dialog will now check if an image already exists in the DB with the same MD5 sum, and offer to merge the information about the images. 2008-07-14 Jan Kundrat * Improve the concept of "priorities" for image loading * Use all CPUs we have to decode images in parallel -- we're now SMP friendly * Preload image thumbnails from images in the thumbnail viewer even if they aren't visible yet 2008-07-13 Jan Kundrat * exiv2 upstream changed their API 2008-07-10 Jesper K. Pedersen * Added Maintenance->Statistics dialog, which shows a bit of statistics about your annotations. 2008-07-06 Jesper K. Pedersen * (regression) Images was not downloaded when importing from a remote URL. * When generating a .kim file together with HTML, the URL written into the .kim file did not include the destination directory. This had the result that images could not be found if the .kim file was copied locally. * If there are not categories in import file, it was not possible to complete the import dialog. * (regression) When loading images using the import dialog, it would ask if you trust date and time from Exif info. This is not needed, as the info from the import file will be used instead. * Don't show "Media Type" in the Import Dialog * (regression) The import dialog now correctly show new items with red, and select them in the combobox. 2008-06-23 Jan Kundrat * Use all CPU cores for parallelization of image loading 2008-06-22 Jesper K. Pedersen * Replace linear search in XMLImageDateCollection with range index. This means that the datebar will update way faster, making the whole application faster. Thanks to Henner Zeller. * (regression) Focus now works in the annotation dialog again. * Improved display of ranges in datebar of Henner Zeller 2008-06-21 Jesper K. Pedersen * Speed up populating the datebar with a factor of 4. Before it took 6 seconds to delete an image due to that, now it only take 1.5 second on my machine. This speed up should also be there for any operation that changes the amount of images shown in the datebar. 2008-06-20 Jesper K. Pedersen * A warning dialog came up complaining about images not being sorted in case one of the images did not have a date. * Sorting images did not work. 2008-06-19 Jesper K. Pedersen * (regression bug) Settings was not syncs correct (esp the info about where the database was located was not saves) * Don't offer to show videos on start up. Instead there now is an entry in the menu. * Exif/Database.cpp (offerInitialize): Don't offer to populate the exiv database anymore, this was really only needed when people upgraded to the last major version. 2008-06-18 Jesper K. Pedersen * Feature test dialog is no more offered during start up, but is instead part of the "welcome to KPA dialog". * Exif/SearchDialog.cpp (makeCamera): (regression bug) Exif info search -> Database did not scroll. Now it does. Also if no cameras are found, it will now tell you so instead of just showing an empty window. 2008-06-15 Jesper K. Pedersen * Deleting an image that was no longer on disk resulted in an error dialog telling that it could not be deleted. Now it will just act as if it did actually delete it, and take the image out of the database. * AnnotationDialog/ListSelect.cpp: The context menu for adding super categories from the image annotation dialog had the problem that the ampersand added by KDE would be in its name, so it would e.g. be named &Berlin 2008-05-17 Jan Kundrat * XMLDB/FIleReader.cpp: Don't crash on parsing XML files with empty supergroups * XMLDB/Database.cpp: Should ignore empty strings, too 2008-03-26 Jan Kundrat * Support localized month names in the KDateEdit properly 2008-03-15 Jan Kundrat - * Support different charsets when displaying EXIF/IPTC metadata + * Support different charsets when displaying Exif/IPTC metadata 2008-02-20 Jan Kundrat * Get list of supported Exif tags directly from the Exiv2 library. 2008-02-13 Jan Kundrat * Add Edit->Open List of Files menu that shows list of thumbnails that matches user-entered stuff. 2008-02-01 Jan Kundrat * Expand sensitive area of a thumbnail image to the whole cell (including category listing) 2007-11-25 Jan Kundrat * Use QWidget::showFullScreen() and showNormal() instead of hacky way of KWin::setState( winId(), NET::FullScreen ) (bug #151031) * Raise images/thumbnail cache limit to 4GB. Too bad KDE/Qt offers no portable way to get information about total available memory of a system. (Bug 151584) 2007-11-06 Jan Kundrat * Robert Krawitz: Fixed regression introduced in 720309 where Viewer adds requests with too high priority. Images to be preloaded were in fact scheduled for decoding before the first image. This should be converted to proper priority queue later... 2007-10-08 Jan Kundrat * Better error handling in KIM Import * Better interactivity in KIM Import 2007-10-03 Jan Kundrat * Give Viewer's requests for images higher priority than thumbnails have 2007-10-01 Jan Kundrat * Adjust the extension of images generated for HTML export. Based on a patch submitted by Miika Turkia . 2007-09-23 Jan Kundrat * Show image label in the infobox and thumbnail tooltip 2007-09-13 Jan Kundrat * Use list of RAW file extensions from libkdcraw instead of hardcoded values 2007-09-11 Tuomas Suutari * Fix behaviour of thumbnail selecting with keyboard. Fixes bug 149706. 2007-08-18 Jan Kundrat * Use libkdcraw intead of budled and rotten dcraw copy 2007-08-11 Jan Kundrat * Automatically hide mouse cursor and disable screensaver when in Viewer 2007-08-10 Jan Kundrat * Add Edit->Copy for copying a list of URLs to selected images to clipboard. Fixes bug 145628 for a second time :). 2007-08-09 Jan Kundrat * Rename File->Export to File->Export/Copy Images to reflect what it actually does and reduce confusion. Fixes bug 145628. 2007-08-08 Jan Kundrat * Applied Henner Zeller's patch for optimizing the thumbnail displaying queue complexity from O(n^2) to O(n*log n) 2007-07-23 Jan Kundrat * Applied Robert Krawitz' patch for using readdir_r() instead of QDir for performance reasons 2007-06-26 Jan Kundrat * Support PEF, a raw format from Pentax' DSLRs. 2007-06-16 Jan Kundrat * Display a warning instead of black screen when current image can't be shown in the Viewer * Stop video playback before seeking to another image * Changed MainWindow::Window::selectedOnDisk() to return list of images available in current view if selection is empty and reworked Viewer not to check for file availability before it's really needed 2007-06-11 Tuomas Suutari * Do not show -1 x -1 image sizes at all. And as suggested by Henner Zeller do not show mega pixel number if it is 0.0. 2007-06-10 Jan Kundrat * Use another dialog for deleting files. Patch submited by Christoph Moseler . * Allow re-using of tags from the previously tagged image in the annotation dialog (first part of bug #145158) 2007-05-23 Jan Kundrat * Make "show this image" fast again. 2007-05-19 Jan Kundrat * Recognize .flv and .erf files -- Fixes bugs 145366 and 141877. 2007-05-19 Jan Kundrat * Make "show this image" load all images in current view, thus making pressing Ctrl+A optional. Thanks to Shawn Willden for nice comments and Baptiste Mathus for review. Fixes bug 145309. 2007-05-10 Jesper K. Pedersen * Make it possible to configure the aspect ratio in the thumbnail viewer - thanks to Jan Kundrat 2007-05-09 Tuomas Suutari * Store filenames in Exif database using UTF-8 encoding. Thanks to Jan Kundrat for a patch. 2007-05-04 Tuomas Suutari * Make selection more visible in thumbnail view. * Apply Henner Zeller's patch to allow changing background color of the thumbnail view black. 2007-05-02 Tuomas Suutari * Apply Henner Zeller's patch to fix some ImageRequest memory leaks. 2007-04-29 Tuomas Suutari * Apply Henner Zeller's patch to show SizeAllCursor when pressing info box with mouse. 2007-04-23 Shawn Willden * Apply Henner Zeller's patch to disply correct size in image viewer's info box. 2007-04-14 Tuomas Suutari * Speed up shuffling of the images in random slideshow. 2007-04-12 Tuomas Suutari * Speed up member map loading by building member map closure on fly when adding members to group. (Reduces start-up time.) 2007-04-05 Tuomas Suutari * Deny cycles in member group configuration. 2007-04-04 Tuomas Suutari * Apply Michael J Gruber's patch to fix rotating raw files (or their thumbnails) twice. 2007-04-01 Tuomas Suutari * Selecting thumbnails with a mouse made more logical and fixed a bug that sometimes prevented a selected thumbnail to be added into internal selection list. 2007-03-31 Tuomas Suutari * Bugfix: In member group tab of the configuration dialog, when changing category from the dropdown list, items from a different category were sometimes added to members listbox. 2007-02-28 Tuomas Suutari * Bugfix: Selection was ignored in the first press of the Plugins menu item, if plugin loading was delayed. 2007-02-04 Jesper K. Pedersen * It is now possible to search for images with changed MD5 sum - very useful for searching for corrupted image files. 2007-01-13 Jesper K. Pedersen * Limit extra space in thumbnail viewer when showing categories (thx to Christoph Moseler). ====================== KPhotoAlbum 3.0 releases =================================== 2006-12-24 Jesper K. Pedersen * Do not show the current category when configuring sub categories. (Thanks to Baptiste Mathus for a patch) * Explicit set the font in the splash screen, so it always looks good. Thanks to Baptiste Mathus for finding this problem. * Fixed a compile error for certain compiler. Thanks to Christophe Choumert for a patch. ====================== Snapshot Monday 27/11-2006 =================================== 2006-11-27 Jesper K. Pedersen * Save configurations of special categories (should they be shown in the viewer etc) (Thanks to Christoph Moseler) * Bugfix: when going out of full screen mode, resize to the configured size. (Thanks to Henner Zeller) * Bugfix: better error handling for video loading (thx to Henner Zeller) 2006-11-26 Jesper K. Pedersen * Renamed Persons to People and Locations to Places. A huge thank you to Clytie Siddall clytie AT riverland DOT net DOT au for bringing this and numerous other language errors to my attention. * Instead of showing "No other Persons" show "None". Translated versions had problems as the "No other" part depended on the category. 2006-11-19 Jesper K. Pedersen * Bugfix: In the settings dialog you were asked if you really wanted to delete a category. Despite your answer it would be deleted nevertheless. (Thanks to Baptiste Mathus for finding this). * Do not show image size for videos (currently there are not way to get that information from the KDE video subsystem) 2006-11-18 Jesper K. Pedersen * Show mega pixel in info box, and zoom factor in viewer, thx to Henner Zeller * Fixed problem with windows layout not being read correctly if any category name contains non-latin1 character ====================== Snapshot Wednesday 15/11-2006 =================================== 2006-11-14 Jesper K. Pedersen * Bugfix: Recent added code to ensure that the splash screen would update timely broke the size of the main dialog. 2006-11-12 Jesper K. Pedersen * Directories only show the filename part now in the browser category directories. (Thanks to Christoph Moseler) * BUGFIX: Finally nailed down the bug where the annotation dialog did not show the first image preview. * Improved support for the flickr plug-in. Now tags are correctly uploaded. Thanks to Mark Jaroski for a patch. 2006-11-11 Jesper K. Pedersen * An item may be member of a number of categories. Mike may be a member of coworkers and friends. Selecting the item in one subcategory, should select him in all. * New option to show categories in the thumbnail view. Now it is also possible to set tokens directly in the thumbnail view. Thanks to (Thanks to Christoph Moseler) * Bugfix: Searching for description did not work (Thx to Christoph Moseler for finding the bug) * control + scroll now zoom on the datebar. (Thanks to Benjamin Bock for a patch) * Bugfix: Full screen viewer no longer worked very well after the change to loading only the size as needed. The reason for this was a race condition where the image was loaded before the window had sized it self. * Somehow running the viewer always forced it to show in a new window, that was definitely against the design. * Don't try and run a slide show if only one image is selected. Doing so will change zoom preference each time we try to go to the next image. 2006-11-08 Jesper K. Pedersen * Improved video backend detection. Thanks to Robert L Krawitz ====================== Snapshot Tuesday 7/11-2006 =================================== 2006-11-06 Jesper K. Pedersen * Reworked the pixel by pixel zoom, so it doesn't flicker the real sized image first.I beleive that pixel by pixel zoom now works in the reworked version. As an added bonus, I no longer loads the first image in full size, but only in viewer size. This has given a significant boost in starting up the viewer. If the user ever zooms, then the real sized image is loaded in the background and put in place when it is available - until then the user just sees the viewer sized version he was original looking at. 2006-11-05 Jesper K. Pedersen * Right clicking on an image should select it. * Stop slideshow when we hit a video, so we don't advance in the middle of the video. * Implemented Play/Pause/Stop/Restart for videos * Now Video playback also works with kaffein and kaboodle 2006-11-04 Jesper K. Pedersen * Enabling/Disabling the right items in the context menu in the viewer depending on image vs. video. * Zooming now works with video display. * Improved info when videos can't be loaded. 2006-10-29 Jesper K. Pederse * Show splash screen during database loading phase (thx to Christoph Moseler for finding this) * More video format tests. * The feature dialog now test if we can show video thumbnails. * The feature dialog now tests better to see if we can show videos. * New application icon * BUGFIX: Yet another attempt at fixing that darn inputting date bug. ====================== Snapshot Sunday 23/10-2006 =================================== 2006-10-22 Jesper K. Pedersen - * It is now possible to bring up the exif dialog from the viewer. + * It is now possible to bring up the Exif dialog from the viewer. * Bugfix: The numbered backup had by accident included the extension .zip for the index.xml file inside the zip file. (Thanks to Baptiste MATHUS for reporting). * Bugfix: Translated version of KPA would display image dates in English, but expect month input in the translated language. This has now been fixed so it expected input in English too. * Bugfix: The panes in the annotation dialog would not show up correctly in translated version of KPhotoAlbum. 2006-10-05 Jesper K. Pedersen * Bugfix: Database was marked as dirty when it started up. * Moved XML settings into Backend tab in the settings page. It really belongs here. * On public demand: made it possible to disable the splash screen. * New splash screen. 2006-09-25 Jesper K. Pederse - * Use alternate EXIF reading code in case EXIV2 is not installed. + * Use alternate Exif reading code in case EXIV2 is not installed. ====================== Snapshot Sunday 24/9-2006 =================================== 2006-09-24 Jesper K. Pedersen * Warn the user at start up if not all features are installed. * Fix up the sub category page in the annotation dialog. * Programmers point of view: The whole setup of marking the database dirty have been reworked. Now we just have a MainWindow::DirtyIndicator::markDirty(). This makes it easier to mark the DB dirty. Users point of view: Now drawing on images, and changing sub categories marks the DB dirty. 2006-09-23 Jesper K. Pedersen * Only write description element to the database if there is a description. * Do not allow the user to change sub categories for special categories (Just imagine he changed the directory structure shown using sub categories - he would get utterly confused from this, I'm sure) (Thanks to Christoph Moseler for pointing this out) * Do not allow the user to change the icon for special categories (Folder, Tokens, .. ). It doesn't add any value to them to be able to do so, and it just clutters up the dialog. (Thanks to Christoph Moseler for pointing this out). * Now that sub categories are shown with indentation, don't show a special icons for the super categories any more. 2006-09-17 Jesper K. Pedersen * BUGFIX: Fixed a crash happening when saving after a category had been renamed, and that category was included in the privacy settings. * BUGFIX: When renaming a category also rename the category thumbnails. (Thanks to Christoph Moseler for finding this) 2006-09-16 Jesper K. Pedersen * BUGFIX: The last snapshot had a bug where KPA would crash if you right clicked, in the annotation dialogs listbox, outside of any items. (Thanks to Christoph Moseler for finding this) * BUGFIX: Privacy info was not properly loaded. * BUGFIX: Changing the label for an image in the annotation dialog did not result in the label being updated in the thumbnail view. (Thanks to Christoph Moseler for finding this) * BUGFIX: Yet another fix to the selection code in the thumbnail viewer. What was broken in the latest snapshot was that you where not able to select a number of images, and drag them. 2006-09-09 Jesper K. Pedersen * BUGFIX: delete and rename of item in the annotation dialog didn't work if the item was not top level or it had sub items. * Added "remove item from parent category" to the list box in the annotation dialog, to allow the user to make a sub item top level. * Ensure every action's keybinding in the viewer is configurable (Thanks to Kimball Robinson for bringing it to my attention that they were not) * Disabled "Create Subcategory..." item when right mouse button was click outside any items (this coursed a crash). Thanks to Christoph Moseler for finding this. ====================== Snapshot Wednesday 6/9-2006 =================================== 2006-09-01 Jesper K. Pedersen * The checkboxes in the annotation dialog are now tristate, which means that there are no need for the "merge" and "remove" checkboxes. Also the "and" checkbox has been replaced with an "and" and a "or" radio button to make their meaning more understandable. * BUGFIX: Solved this crash: edit a category name in the settings dialog, press Apply and press OK. (thanks to Christoph Moseler for finding this. ====================== Snapshot Sunday 27/8-2006 =================================== 2006-08-27 Jesper K. Pedersen * Tokens got kind of lost when importing from a KPhotoAlbum 2.2 index file. * BUGFIX: Media Type Category was not filled correctly when finding new images. * It is now possible to set up sub categories in the annotation dialog, simply by dragging items onto other items. 2006-08-26 Jesper K. Pedersen * The listbox in the annotation dialog is now updated when an item is added as a sub item to another. * Improved set of file names recognized as videos. * BUGFIX: deselect all images when right clicking on an image that is currently not selected. 2006-08-25 Jesper K. Pedersen * It is now possible to use the search bar in the main window, when showing folders. * Searching in a list view in the browser is not anchored to the beginning of the word. * BUGFIX: It is now possible to have spaces in category names. * Fixed bug in last snapshot where the compressed index.xml didn't work. * Don't save Folders to the index.xml file again. I'm rather sure it is faster to calculate the info on start up rather than loading the extra XML. ====================== Snapshot Wednesday 23/8-2006 =================================== 2006-08-23 Jesper K. Pedersen * Now it is also possible to bind rotate left/right in the annotation dialog. 2006-08-22 Jesper K. Pedersen * Added a Media Type folder. 2006-08-21 Jesper K. Pedersen * Bugfix: Selection using the shift key and mouse button was broken if selected spanned several pages. * Bugfix: Pressing the mouse down on a selected image in the thumbnail viewer now deselect all other images if control or shift is not pressed. This was an inconsistent behavior compared to similar browsers. * Bugfix: holding control down in the browser when selecting an item (say a person), should jump directly to the images rather than going back to main browser window. This did not work. 2006-08-20 Jesper K. Pedersen * now it is possible to specify the icon size in used in the browser - esp usefull for showing larger images for persons in an icon view. * Added an info box which suggest people to see the introduction videos. * KPhotoAlbum is now capable of also managing videos. 2006-08-14 Jesper K. Pedersen * The list boxes in the AnnotationDialog now has an accelerator. 2006-08-13 Jesper K. Pedersen * Added function to thumbnail context bar to regenerate thumbnails, useful when the video thumbnail generator picks a random image for thumbnail. 2006-08-11 Jesper K. Pedersen * It is now possible to configure the key bindings in the annotation dialog. 2006-08-10 Jesper K. Pedersen * In the viewer it is now possible to use mouse wheel to scroll to the next/prev image. (Thanks to Christoph Moseler for a patch). 2006-07-23 Jesper K. Pedersen * Make it possible to set a category image for sub categories. * Show the category tree in the browser 2006-07-22 Jesper K. Pedersen * A new menu item now exist in the annotation dialogs context menu, namely "Add Sub Category" * In the annotation dialog, you may now switch between sorted category view and most recent view, by pressing and releasing the alt key. * In the annotation dialog, pressing and releasing the control will toggle showing items selected, only. 2006-07-14 Jesper K. Pedersen * In the annotation dialog, it is now possible to scroll the list of selected items, but pressing arrow up and down. * While typing in the annotation dialog, hide items that does not match the current typed text. =========================== KPhotoAlbum 2.2 released ============================= 2006-04-14 Jesper K. Pedersen * BUGFIX: Fixed some problems with member groups, when renaming categories. 2006-03-31 Jesper K. Pedersen * BUGFIX:Disallow pressing pgdown/pgup in the annotation dialog when searching. Thanks to Martin Hoeller for finding this crash. 2006-03-26 Jesper K. Pedersen * Bugfix: inaccurate times now survives a visit to the annotation dialog. * Bugfix: Member maps wasn't correctly renamed when renaming a category. * Save the layout of the annotation dialog in the file layout.xml next to the index.xml file. Early in the KPhotoAlbum 2.2 development, it was saved in the KDE Registry, but the code for doing this (which is out of my control) has a bug which makes it impossible to save non latin1 characters. Thanks to Martin Hoeller for finding the three above issues. 2006-03-19 Jesper K. Pedersen * VERY FINAL feature: Added a checkbox to the annotation dialog offering to remove annotations. Thanks for Robert L Krawitz for patch with this feature. * BUGFIX: Fixed crash when annotation dialog was up, and the datebar got a mouse move event (It had its mouse press blocked, so it got quite confused from seeing a mouse move without first a mouse press) Thanks to Martin Hoeller for finding this crash. * BUGFIX: The annotation window no longer crashes when reseting its layout. * Disable the delete button in the annotation dialog when annotating multiple images at the same time, or when using the dialog for searching. Thanks to Martin Hoeller for finding this crash. 2006-03-12 Jesper K. Pedersen * The datebar now has an explicit button for canceling selection. * Fixed bug where limited images on the date bar followed by a limiting images from the browser, and then unlimiting from the browser resulted in only items in the scope of the datebar selection being shown on the datebar. 2006-02-11 Jesper K. Pedersen * Drasticly improved performance of deleting images. Thanks for Robert L Krawitz for his analysis of the problem. * Don't show a warning about not being able to delete a file when it doesn't exists in the first place. Thanks to Robert L Krawitz. 2006-01-12 Jesper K. Pedersen * Its now possible to select whether the newest or the oldest thumbnail should be shown first. 2006-01-11 Jesper K. Pedersen * Made it possible to configure the thumbnail cache. 2006-01-02 Jesper K. Pedersen * Completely reimplemented the thumbnail view, to solve a huge amount of issues with QIconView. 2005-11-27 Jesper K. Pedersen - * Added an EXIF dialog (available from the browser), plus the - possibilities to see EXIF tags in the viewer. Set of EXIF tokens + * Added an Exif dialog (available from the browser), plus the + possibilities to see Exif tags in the viewer. Set of Exif tokens shown is configurable in the settings dialog. 2005-09-23 Jesper K. Pedersen * Searches using the description field are now case insensitive. * Stop selecting the first image in the thumbnail view when showing it up. * BUGFIX: Setting tokens should mark the database as dirty. * BUGFIX: Restart slideshow timer when user manually moves to a new image. 2005-08-29 Jesper K. Pedersen * Added a Exif dialog to the context menu in the browser. * KPhotoAlbum now stores its thumbnails in ~/.thumbnails complying with many other applications. 2005-08-28 Jesper K. Pedersen * Added an option to save the index.xml file in a compressed format, this speeds up loading the xml file by approximate a factor 2. 2005-08-26 Jesper K. Pedersen * Added command line option --export-in-2.1-format to allow KPhotoAlbum to save an index.xml that can be read by KPhotoAlbum 2.1. All configurations in the config dialog (like do auto save etc) will be lost though. * KPhotoAlbum does now offer to save numbered backups of the index.xml file: index.xml~0001~, index.xml~0002~ etc. In addition this file can be zipped to preserve disk space. 2005-08-19 Jesper K. Pedersen * recalculate checksum will now work only on the selected images if there is a selection. 2005-08-17 Jesper K. Pedersen * Pressing return in the search bar now selects the current item. 2005-08-03 Jesper K. Pedersen * Internal: Simplify ImageDate so it now is only a from and a to QDateTime - this makes it possible for backends to do better date searches (needed by the SQL backend) * Delay loading plugins - this will save us half a second at start up. 2005-07-23 Jesper K. Pedersen * Threw away the date folder, it is redundant now we have the datebar. * Internal: A lot of clean up in the code has happen over the last many month, and I've been working on making the backend plugable, and implement a SQL backend. I gave up with the SQL backend, as it was too hard. My code is still in there in case someone wants to take over that part. --------------------------- version 2.1 released -------------------------------------- 2005-04-07 Jesper K. Pedersen * BUGFIX: Using kapp->invokeHelp to show help rather than kapp->invokeBrowser, as the later doesn't work unless your default browser is konqueror. * BUGFIX: the category was sometimes shown twice in the status bar * BUGFIX: remove tokens didn't work for KDE 3.4 2005-04-03 Jesper K. Pedersen * BUGFIX: The pre-loading in the image viewer broke viewing images from the import wizard. * BUGFIX: The import dialog was broken with new versions of Qt. 2005-03-26 Jesper K. Pedersen * Added member group config to context menu of list select (which is the list boxes of the image property pages) * Added a jump-to-context button to the viewer, plus ensure keyboard focus would be better shown in the thumbnail view. * BUGFIX: When quiting KDE, KPhotoAlbum will asks if you want to save. This previously canceled logging out of KDE. 2005-03-20 Jesper K. Pedersen * Use KDE's date formater to given better result in the datebar. 2005-03-14 Jesper K. Pedersen * Added a survey, where I can get some feedback from the KPhotoAlbum users. 2005-02-20 Jesper K. Pedersen * New images should be inserted sorted. * When new images are found, the datebar are now updated. 2005-01-16 Jesper K. Pedersen * Added protection against odd sized images - like 400x1 (Thanks to Robert L Krawitz ) 2005-01-13 Jesper K. Pedersen * Fixed deadlock when generating thumbnails from the Maintenance menu (Thanks to Robert L Krawitz ) 2005-01-12 Jesper K. Pedersen * Made it possible to check if an image in the image loader was needed just prior to loading it - this speeds up page down in the thumbnail view * Made the settings dialog non modal, and added an apply button. * Finally the new thumbnail view seems to work. * Made it possible to configure if columns should be aligned in thumbnail view * Made it possible to configure space between columns. 2005-01-10 Jesper K. Pedersen * Display a grey thumbnail when the image for one reason or the other couldn't be loaded. Previously this resulted in the thumbnail containing garbage pixels. Thanks to Robert L Krawitz 2005-01-09 Jesper K. Pedersen - * Improves handling of EXIF data, in particular in the "Read EXIF + * Improves handling of Exif data, in particular in the "Read Exif Data" dialog. It does not overwrite the time, date, orientation, - or comment data unless the EXIF data is actually present. It also + or comment data unless the Exif data is actually present. It also adds two new options that do overwrite the time and date - information with the file modification date/time if the EXIF data + information with the file modification date/time if the Exif data isn't present. Thanks to Robert L Krawitz * BUGFIX: The date parser was not previous translated, while the date generator was, thus typing in date names did not work in the image config dialog, unless the translated names matched the untranslated ones. 2005-01-08 Jesper K. Pedersen * The date bar now support selecting a range of images. 2005-01-02 Jesper K. Pedersen * Reimplemented sort images as it could lead to image loss, plus was not guaranteed to preserve sort order for images with same key. * On Robert's request, removed the patch from 2004-12-08 to work around SUSE's max image size - due to Robert, it obviously didn't work. - * Read EXIF info out of .thm files (patch from Robert L Krawitz ) + * Read Exif info out of .thm files (patch from Robert L Krawitz ) 2004-12-30 Jesper K. Pedersen * Added the possibility to zoom out from current scope, thus seeing the context of the current image * Added Maintenance->Display Images with Incomplete Dates this will help update images with missing or invalid dates. * KPhotoAlbum no longer splits images into a bunch of folders, but does instead show them in one big collection, this makes the date bar more powerful, plus allows you to scroll backwards. * Added a date bar, that shows statistics about images, plus let you navigate to a given date. 2004-12-29 Jesper K. Pedersen * Added a AND checkbox to the search page, that allows to graphically choose among and/or searches. (Thanks to Jean-Michel FAYARD ) * HTML generation didn't work if the file names contained dot's like 2004-12-17-18.34.59.jpg * Added support for Canon CRW "digital negative" - thanks to Steffen Hansen . * Postpone checking if images is on disk till the information is needed, this speeds up start up with a couple of seconds. 2004-12-20 Jesper K. Pedersen * KPhotoAlbum will now escape all non-latin1 characters, so they still look good on web servers that seems to ignore the meta tag. (Like mine does ;-) 2004-12-08 Jesper K. Pedersen * Offer to make hard link from the export dialog (patch from Robert L Krawitz ) * SUSE introduced a maximum on QImages, a patch from Robert L Krawitz works around this, and ensures that KPhotoAlbum do not crash when images are larger than the maximum size. * The Date picker now remember dates from invocation to invocation (Thanks to Jean-Michel FAYARD for a patch for this) * Highlight date with images in the date picker. (Thanks to Jean-Michel FAYARD for a patch for this) 2004-12-04 Jesper K. Pedersen * Categories in Member Group config is now internationalized. * Categories in virtual album configuration (General tab in Settings) are now internationalized. 2004-12-02 Jesper K. Pedersen * In the viewer moved away every default key binding on lettered keys. The changes are: Quit: q -> escape Run Slideshow: s -> ctrl + r (now this is the same as from the thumbnail viewer) Show Infobox: i -> Ctrl + i Show drawings on images: d -> ctrl + d * In the viewer it is now possible to press a key to set a token, these tokens can the be used during browsing. This makes it easy to mark all images for say printing, by simply setting a token on each of them, and the later browsing for that token. 2004-11-10 Jesper K. Pedersen * Only items from the current image are shown in the image category editor. * Added an option to automatically load images when matches in the browser goes below what can be shown in a single view. 2004-11-09 Jesper K. Pedersen * Made it possible to disable searching for images on start up. * BUGFIX: Delayed the hide of the splash screen until the main window was shown. * Moved "show Tool Tips" to the Settings menu. * Added a search line. 2004-11-08 Jesper K. Pedersen * BUGFIX: Markup in the editor (like highlight for spelling errors) got saved and thus displayed with the image. * Renamed the menu bar descriptions for locking, so it hopefully is easier to understand. * update progress dialog for loading info from images after each image, rather than after every ten images. This matters if the images are say 80Mb large. --------------------------- version 2.0 released -------------------------------------- 2004-10-16 Jesper K. Pedersen * Items in the edit menu (among these "delete image") was not available if "display images not on disk" was activated from the browser. Thanks to Robert L Krawitz for finding the bug. 2004-09-26 Jesper K. Pedersen * Improved date editor: (1) sizing now works (2) there are no default buttons, so it is possible to press enter in the line edit to specify a date. (3) there now is a ">>" button to copy from date to to-date 2004-09-22 Jesper K. Pedersen * The editor for image comments now has an on the fly spell checker. * BUGFIX: Fixed tab order in the image config dialog. 2004-09-05 Jesper K. Pedersen * BUGFIX: remove splash screen before showing welcome dialog 2004-09-04 Jesper K. Pedersen * Added a way to browse directories within the database. Thanks to Jean-Michel FAYARD for patch. * BUGFIX: now that it is possible to set background color of thumbnail view, the foreground color must also be changed, to ensure maximum contrast (and that it is visible at all) * No key bindings should be made to letters in the main window, as this makes it impossible to press that letter in the browser to jump to the item in question. Thus I made these change to key bindings: I -> Ctrl+I (show selected images) Ctrl-I -> none (show selected images in new window) S -> ctrl+R (run slide show) Ctrl+S -> none (run randomized slideshow) (this conflicted with save which was and are still on Ctrl-s too) Remember you can make your own key bindings in KPhotoAlbum, so this are really just the defaults I change. 2004-08-23 Jesper K. Pedersen * Do not show items already matched when browsing. Thus do not show Jesper, if he is already matches in the current browsing scope. 2004-08-09 Jesper K. Pedersen * The viewer now preloads images, which makes it feel a LOT more responsive. 2004-08-08 Jesper K. Pedersen * Added zoom out to full view in viewer. 2004-08-07 Jesper K. Pedersen * Removed the preload hack for preview, which was broken anyway. Not needed now when image loading is so much faster. 2004-08-06 Jesper K. Pedersen * Size of main windows and configuration window are now stored in the index.xml file, and restored for future sessions 2004-07-24 Jesper K. Pedersen * Moved all plugins to one menu rather than scattered out over all the menus. The former seems like the best idea for easy finding a functionality, while the later seems to be best, to ensure that users know which functionality is KPhotoAlbum core features, and which are plugins, with plugins perhaps being less integrated than build-in features. * import/export is improved so during import of external images, KPhotoAlbum will first search for the images next to the .kim file, and then at the URL the images originally was uploaded to. 2004-07-22 Jesper K. Pedersen * Made it possible to hide the labels in the thumbnail view * Clean up the options dialog (made new group "thumbnail view") * Tried to improve the layout of the thumbnail view. This is the best I can do, unfortunately QIconView is utterly broken. 2004-07-18 Jesper K. Pedersen * BUGFIX: search dialog does now contain imported image attributes. * Fixed code for going into full screen (This forces the min version to be KDE 3.2, though) 2004-07-12 Jesper K. Pedersen * Automatic generated labels from file names now include all up till the last dot (Thanks to Jean-Michel FAYARD ) * Fixed utf-8 encoding description in HTML pages (Thanks to Jean-Michel FAYARD ) 2004-06-17 Jesper K. Pedersen * Put count of images in caption of image config window 2004-06-14 Jesper K. Pedersen * Added the filename to the caption of the viewer. * Thanks to Marco Caldarelli for a patch to - reread EXIF info, available from a dialog in the menu. + reread Exif info, available from a dialog in the menu. 2004-06-13 Jesper K. Pedersen * Thanks to Marcel Wiesweg for a patch for optimizing loading in the image property dialog 2004-06-08 Jesper K. Pedersen * Added a splash screen 2004-06-06 Jesper K. Pedersen * Bugfix: The time config was not set correctly for configuring multiple images, often resulting in many images getting the same time stamp. * Its now possible to configure which plugins should be loaded. 2004-06-03 Jesper K. Pedersen * demo directory are now copied rather than symlinked. With symlink, the installed demo files will also be modified when images are modified in the demo, which is not desired. * Added KIPI support. 2004-05-03 Jesper K. Pedersen * BUGFIX: When you press cancel during the export process you still get the info someone else could import your export. * BUGFIX: After an import the index.xml file was not marked modified * BUGFIX: export to HTML page and import resulted in rotated images getting even more rotated * BUGFIX: importing into a subdirectory did not always work. 2004-05-02 Jesper K. Pedersen * BUGFIX: KPhotoAlbum crashed in the following situation: - drag an image out from the thumbnail view - browse, so the thumbnail view shows some other images - drag another image out 2004-04-19 Jesper K. Pedersen * BUGFIX: The wrong image was invoked in the external viewer if the thumbnail view was scrolled. * BUGFIX: If description was not generated then other option groups was not printed in HTML exports. 2004-04-18 Jesper K. Pedersen * When opening the image config dialog, show a message box with a link to the help page. * Made it possible to re enable message boxes where the user previously checked do-not-show-again * Its now possible to find images where only a set of items are on (e.g. only me, and no one else) * BUGFIX: When generating HTML, only generate image on disk * Added a dialog with a description on how to enable webservers to server KPhotoAlbum files. 2004-04-16 Jesper K. Pedersen * BUGFIX: If the destination directory for HTML export exists, offer to remove it. This fixes the problem where it would instead just be put in a subdirectory. - * Loading descriptions from EXIF information can now be disabled + * Loading descriptions from Exif information can now be disabled * BUGFIX: when exporting the image loader also saved the large images (like 1024x768) to tumbnails dir. Please remove all thumbnails using the maintenance menu item. * BUGFIX: images inline in export files was broken if the extension was different from .jpg * Now its possible to ask for a .kim file on the HTML generated pages * BUGFIX: pressing cancel in export dialog still continue exporting. * BUGFIX: It was not possible to import twice in one session. * BUGFIX: images was rotated too must during export * now its possible to export images, and leave the .kim file next to the image set rather than putting them into the .kim file. 2004-04-15 Jesper K. Pedersen * Its now possible to delete images from within the image configuration dialog * Now you may click on a .kim file from say konqueror to start importing that file into your db. 2004-04-14 Jesper K. Pedersen * added Import/Export so its possible to share images with friends, without them having to write comments, and tell how is on the images. 2004-04-12 Jesper K. Pedersen * Its now possible to configure the toolbar * Added run slide show as a tool button * Now its possible to configure the shortcuts in the viewer. 2004-04-11 Jesper K. Pedersen * added run randomized slideshow * added run slide show to the thumbnail view, thus making it much easier to start a slide show if the images in view. * Made slide show timeout configurable * Made slide show size configurable, plus made it possible to specify full screen for configuration for configuration of normal view. 2004-03-27 Jesper K. Pedersen * rewrote the handling of dates in the image property dialog, so the GUI is much easier to work with - this must have been the oldest wish on my which list ;-) 2004-03-21 Jesper K. Pedersen * Added Images->sort to sort images in thumbnail view. Thanks to Thomas Schwarzgruber * Added Maintenance->remove all thumbnails and Maintenance->build thumbnails 2004-03-20 Jesper K. Pedersen * BUGFIX: now we are doing boundary check when panning, so you can't pan outside the image. * generating HTML and viewing images will now be the context if no images are selected. Context is the current search or current browse - e.g. all images from say Las Vegas. 2004-03-17 Jesper K. Pedersen * Added context menu for sorting options listbox (thanks to Reimar Imhof for a patch) 2004-03-10 Jesper K. Pedersen * BUGFIX: Fixed crash when moving images arround 2004-03-09 Jesper K. Pedersen * BUGFIX: If you have to rotate an image to view it, you get black stripes besides your image because of the resolution of the image (it's much higher than wide). So far so good. But if you zoom into the image via pressing '+' key, the black stripes don't change -> the shown width stays the same although the (virtually) height changed. Thanks to Thomas Schwarzgruber for a patch. 2004-03-07 Jesper K. Pedersen * BUGFIX: When renaming an item in the listselect, then category image is now also renamed. * BUGFIX: In the settings dialog, member group tab, ensure that the category list is updated when entering the page. * replaced "None" with "No Persons", "No Keywords" etc. * the count text in the browser is now "categories" rather than "images" when displaying categories like "persons", "locations" etc. * BUGFIX: Finally made panning work. Its now bound to the middle mouse button, which is more compatible to a wide range of application, rather than Ctrl+left button as was the case previously. 2004-03-02 Jesper K. Pedersen * BUGFIX: previous fix to member groups was broken. * use smoothScale rather than scale to get better looking thumbnails. 2004-02-15 Jesper K. Pedersen * BUGFIX: Pressing the rename button in member groups made KPhotoAlbum crash if no member groups are selected. --------------------------- version 1.1 released -------------------------------------- 2004-02-04 Jesper K. Pedersen * BUGFIX: description on index page was controlled by "description" checkbox rather than the description for each page 2004-02-01 Jesper K. Pedersen * BUGFIX: categories was not available for translations. * BUGFIX: the string **NONE** was not made available for translation everywhere, which resulted in the search dialog did not work for translated languages. 2004-01-31 Jesper K. Pedersen * BUGFIX: changed viewer from QDialog to QWidget as base class so the browser window could get on top of it. 2004-01-29 Jesper K. Pedersen * Made the image preview size configurable. * Ensure enable/disable state of menu bar is correct 2004-01-27 Jesper K. Pedersen * Made the KPhotoAlbum properties dialog look better. 2004-01-26 Jesper K. Pedersen * BUGFIX: Changed QActions to KActions in the viewer to avoid mem error when the viewer closes. * BUGFIX: Fixed bug introduced yesterday, where rotating multiple images resulting in them getting an odd angle. 2004-01-25 Jesper K. Pedersen * Split up the dialog for HTML generation, plus made it possible to specify which of the category (Persons/Location/...) plus description - to generate. * The image configuration dialog may now be closed by pressing Ctrl+return * Added a number of tooltips * Added a donate dialog, which makes it possible for people to donate money. * BUGFIX: when generating HTML, its no longer a problem if several images have the same base name, ie. if you may now generate holiday1/me.jpg andd holiday2/me.jpg at the same time. * BUGFIX: When the user presses the right mouse button on one of the list boxes containing options (Persons, Loactions, Events etc.) to show the context menu, then the state of the item was changed (from selected to non-selected or visa versa). * The total amount of images in the status bar is now updated when new images are found or existing images are deleted. * BUGFIX: don't load the search and multi-config images through the imageinfo clas as this will make KPhotoAlbum ask whether date should be trusted. - * Added a configuration option for whether EXIF rotation + * Added a configuration option for whether Exif rotation information should be used or not. 2004-01-23 Jesper K. Pedersen * BUGFIX: It was not possible to generate HTML more than once per session. - * Read date, orientation and description out of EXIF data. + * Read date, orientation and description out of Exif data. 2004-01-21 Jesper K. Pedersen * Thanks to Teemu Rytilahti , KPhotoAlbum will now offer themes when generating HTML. * Heavily optimized browsing functions. Now everything is hopefully lightning fast. 2004-01-20 Jesper K. Pedersen * The thumbnail overview and the image viewer now has a menu item for invoking an external program on the images. The list includes all programs claiming they can handle image/jpeg from KTrader. * Implemented Maintenance/Display Images not on Disk * Implemented Maintenance/Recalculate Checksum (Useful if images has been change for example using gimp) * Implemented Maintenance/Rescan for new Images * Only do auto save if there has been changes since the last auto save. 2004-01-19 Jesper K. Pedersen * MD5 sums are now calculated for each image, so that KPhotoAlbum can track movement of images in the directories 2004-01-18 Jesper K. Pedersen * Move cursor to the lower right corner when viewer goes into fullscreen mode * BUGFIX: Generate HTML with nothing selected now export all images from thumbnail view. * BUGFIX: Context menu for thumbnail view used to show up in the browser too. * BUGFIX: searches using the textbox didn't work. * BUGFIX: ensure that tip of the day has focus when it starts up. 2004-01-10 Jesper K. Pedersen * Improved HTML generation. 2004-01-04 Jesper K. Pedersen * The tool tip for the thumbnail view now contains an enlargement of the image. In addition the tool tip needs to be disabled, rather than it automatically hides. The tool tip window will temporarily hide when the mouse is outside the thumbnail view. The placement of the tool tip window is now improved, so it doesn't get partly of screen. 2004-01-03 Jesper K. Pedersen * Added an item to the context menu in the viewer for setting the current image as the background. (Thanks to Teemu Rytilahti ) for a patch with this feature. * New Feature: It is now possible to show the content views (those with overview of persons, locations, keywords etc) either with a list view or with an icon view. In addition it is also possible to show a different icon for each item (one icon for Joe, and another for Bill). 2003-12-30 Jesper K. Pedersen * BUGFIX: Member groups are not shown in the browser, in case the member group do not contain any items. 2003-12-26 Jesper K. Pedersen * Update thumnail view when an image has been rotated. * Added "View Selected" and "View Selected in new window" to the context menu for the thumbnail overview. * Enable/Disable state of "configure property items" are now correctly set. * On public demand, the file name is now shown in the status bar. 2003-12-13 Jesper K. Pedersen * BUGFIX: fixed crash where KPhotoAlbum crashes if you right click outside an item in one of the list boxes with properties (Persons, Locations, ...) 2003-12-05 Jesper K. Pedersen * BUGFIX: KPhotoAlbum used to crash on moving images around. 2003-12-01 Jesper K. Pedersen * New Feature: Its now possible to drag(copy) images from kphotoalbum to say the desktop. Unfortunately KPhotoAlbum crashes in KDE code if I drag an QImage out, so I can only drag the filename out, which means that rotated images are not rotated when dragged out. Still better than nothing ;-) * Go out of full screen if an item from the info box in the viewer is selected, and the viewer is on the same monitor as the browser. * Added Settings | Configure Shortcuts * Edit->Options was moved Settings->Configure Kphotoalbum. * New Feature: It is now possible to using fish:// or ftp:// when exporting to HTML * BUGFIX: Don't show HTML export dialog in case no item was selected. ---------------------------------- version 1.0 released -------------------- 2003-11-27 Jesper K. Pedersen * BUGFIX: Previously the image loader was asked to load all images on start up, making machines with low memory crash. This was clearly unintentional, and as such a bug. 2003-11-21 Jesper K. Pedersen * BUGFIX: indicator marks are now properly shown during selection of drawing on images. 2003-11-13 Jesper K. Pedersen * BUGFIX: File names did not contain an extension in the ThumbNail directory. This resulted in a name class if two files existed with same base name but different extension. * Completed the KPhotoAlbum handbook. * BUGFIX: KPhotoAlbum did always show one image less than the index promised. * IMPROVEMENT: Exchanged Ctrl+plus and Ctrl+minus in the viewer, so ctrl+plus now means run faster and ctrl+minus means run slower. This seems much more intuitive. 2003-10-21 Jesper K. Pedersen * BUGFIX: First search for an image, then set properties, and the app crashed. 2003-10-05 Jesper K. Pedersen * BUGFIX: KPhotoAlbum crashed when it found new images at start up. * Started writing a handbook for KPhotoAlbum. * BUGFIX: if the slideshow was running very fast, then keyevents got utterly much behind making it almost impossible to stop the slideshow. * Its now possible to configure image properties from within the viewer. 2003-10-04 Jesper K. Pedersen * Added new feature, which allows you to specify a default search option, that applies to all searches. That way you can exclude all private images, when looking at images with say, your parents. * BUGFIX: If a user added a new group, but never added any items to the group, then the group would not be available later for adding new items, but it was still available for selecting in configuration listbox and for searching. * BUGFIX: When a group was renamed or delete, the old name would still be available in the configuration listbox. * Added status bar icon for dirtyness. * BUGFIX: In the image config, selecting an item from the listbox did not mark the image dirty. * BUGFIX: Pressing Cancel in the find dialog, does no longer change the content of the browser window. diff --git a/DB/FileInfo.cpp b/DB/FileInfo.cpp index 9f34f578..115fa4fc 100644 --- a/DB/FileInfo.cpp +++ b/DB/FileInfo.cpp @@ -1,137 +1,137 @@ /* 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 "FileInfo.h" #include #include #include #include "Utilities/Util.h" #include "Exif/Info.h" using namespace DB; FileInfo FileInfo::read( const DB::FileName& fileName, DB::ExifMode mode ) { return FileInfo( fileName, mode ); } DB::FileInfo::FileInfo( const DB::FileName& fileName, DB::ExifMode mode ) : m_angle(0), m_fileName(fileName) { parseEXIV2( fileName ); if ( updateDataFromFileTimeStamp(fileName,mode)) m_date = QFileInfo( fileName.absolute() ).lastModified(); } Exiv2::ExifData& DB::FileInfo::getExifData() { return m_exifMap; } const DB::FileName& DB::FileInfo::getFileName() const { return m_fileName; } bool DB::FileInfo::updateDataFromFileTimeStamp(const DB::FileName& fileName, DB::ExifMode mode) { - // If the date is valid from EXIF reading, then we should not use the time stamp from the file. + // If the date is valid from Exif reading, then we should not use the time stamp from the file. if ( m_date.isValid() ) return false; // If we are not setting date, then we should of course not set the date if ( (mode & EXIFMODE_DATE) == 0 ) return false; // If we are we already have specifies that we want to sent the date (from the ReReadExif dialog), then we of course should. if ( (mode & EXIFMODE_USE_IMAGE_DATE_IF_INVALID_EXIF_DATE ) != 0) return true; // Always trust for videos (this is a way to say that we should not trust for scaned in images - which makes no sense for videos) if ( Utilities::isVideo(fileName) ) return true; // Finally use the info from the settings dialog return Settings::SettingsData::instance()->trustTimeStamps(); } void DB::FileInfo::parseEXIV2( const DB::FileName& fileName ) { m_exifMap = Exif::Info::instance()->metadata( fileName ).exif; // Date m_date = fetchEXIV2Date( m_exifMap, "Exif.Photo.DateTimeOriginal" ); if ( !m_date.isValid() ) { m_date = fetchEXIV2Date( m_exifMap, "Exif.Photo.DateTimeDigitized" ); if ( !m_date.isValid() ) m_date = fetchEXIV2Date( m_exifMap, "Exif.Image.DateTime" ); } // Angle if ( m_exifMap.findKey( Exiv2::ExifKey( "Exif.Image.Orientation" ) ) != m_exifMap.end() ) { const Exiv2::Exifdatum& datum = m_exifMap["Exif.Image.Orientation"]; int orientation = 0; if (datum.count() > 0) orientation = datum.toLong(); m_angle = orientationToAngle( orientation ); } // Description if( m_exifMap.findKey( Exiv2::ExifKey( "Exif.Image.ImageDescription" ) ) != m_exifMap.end() ) { const Exiv2::Exifdatum& datum = m_exifMap["Exif.Image.ImageDescription"]; m_description = QString::fromLocal8Bit( datum.toString().c_str() ).trimmed(); // some cameras seem to add control characters. Remove them: m_description.remove(QRegularExpression(QString::fromLatin1("\\p{Cc}"))); } } QDateTime FileInfo::fetchEXIV2Date( Exiv2::ExifData& map, const char* key ) { try { if ( map.findKey( Exiv2::ExifKey( key ) ) != map.end() ) { const Exiv2::Exifdatum& datum = map[key ]; return QDateTime::fromString( QString::fromLatin1(datum.toString().c_str()), Qt::ISODate ); } } catch (...) { } return QDateTime(); } int DB::FileInfo::orientationToAngle( int orientation ) { if ( orientation == 1 || orientation == 2 ) return 0; else if ( orientation == 3 || orientation == 4 ) return 180; else if ( orientation == 5 || orientation == 8 ) return 270; else if ( orientation == 6 || orientation == 7 ) return 90; return 0; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageInfo.cpp b/DB/ImageInfo.cpp index 7cc1e03a..6d489d7b 100644 --- a/DB/ImageInfo.cpp +++ b/DB/ImageInfo.cpp @@ -1,828 +1,828 @@ /* 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 "ImageInfo.h" #include "FileInfo.h" #include "Logging.h" #include #include #include #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_locked( false ), m_dirty( false ), m_delaySaving( 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_locked(false), m_delaySaving( true ) { QFileInfo fi( fileName.absolute() ); m_label = fi.completeBaseName(); m_angle = 0; setFileName(fileName); - // Read EXIF information + // Read Exif information if ( readExifInfo ) { ExifMode mode = EXIFMODE_INIT; if ( ! storeExifInfo) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName, mode); } m_dirty = false; m_delaySaving = false; } /** Change delaying of saving changes. * * Will save changes when set to false. * * Use this method to set multiple attributes with only one * database operation. * * Example: * \code * info.delaySavingChanges(true); * info.setLabel("Hello"); * info.setDescription("Hello world"); * info.delaySavingChanges(false); * \endcode * * \see saveChanges() */ void ImageInfo::delaySavingChanges(bool b) { m_delaySaving = b; if (!b) saveChanges(); } void ImageInfo::setLabel( const QString& desc ) { if (desc != m_label) m_dirty = true; m_label = desc; saveChangesIfNotDelayed(); } QString ImageInfo::label() const { return m_label; } void ImageInfo::setDescription( const QString& desc ) { if (desc != m_description) m_dirty = true; m_description = desc.trimmed(); saveChangesIfNotDelayed(); } 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; saveChangesIfNotDelayed(); } 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 { // OpenSuse leap 42.1 still ships with Qt 5.5 // TODO: remove this version check once we don't care about Qt 5.6 anymore... #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) return values.intersects( m_categoryInfomation[key] ); #else StringSet tmp = values; return ! tmp.intersect( m_categoryInfomation[key] ).isEmpty(); #endif } 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 ); saveChangesIfNotDelayed(); } } 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 ); } saveChangesIfNotDelayed(); } 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; } } } saveChangesIfNotDelayed(); } int ImageInfo::angle() const { return m_angle; } void ImageInfo::setAngle( int angle ) { if (angle != m_angle) m_dirty = true; m_angle = angle; saveChangesIfNotDelayed(); } 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; saveChangesIfNotDelayed(); } 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; saveChangesIfNotDelayed(); } 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; saveChangesIfNotDelayed(); } void ImageInfo::setVideoLength(int length) { if ( m_videoLength != length ) m_dirty = true; m_videoLength = length; saveChangesIfNotDelayed(); } int ImageInfo::videoLength() const { return m_videoLength; } void ImageInfo::setDate( const ImageDate& date ) { if (date != m_date) m_dirty = true; m_date = date; saveChangesIfNotDelayed(); } 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); saveChangesIfNotDelayed(); } 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; saveChangesIfNotDelayed(); } 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 ); bool oldDelaySaving = m_delaySaving; delaySavingChanges(true); // 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); } } delaySavingChanges(false); m_delaySaving = oldDelaySaving; // Database update if ( mode & EXIFMODE_DATABASE_UPDATE ) { Exif::Database::instance()->add( exifInfo ); #ifdef HAVE_KGEOMAP // 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; saveChangesIfNotDelayed(); } 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_delaySaving = true; 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; delaySavingChanges(false); if ( rating > 10 ) rating = 10; if ( rating < -1 ) rating = -1; m_rating = rating; m_stackId = stackId; m_stackOrder = stackOrder; m_videoLength= -1; } // TODO: we should get rid of this operator. It seems only be necessary // because of the 'delaySavings' field that gets a special value. // ImageInfo should just be a dumb data object holder and not incorporate // storing strategies. 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_dirty = other.m_dirty; m_rating = other.m_rating; m_stackId = other.m_stackId; m_stackOrder = other.m_stackOrder; m_videoLength = other.m_videoLength; delaySavingChanges(false); 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 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 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 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 ); } } saveChangesIfNotDelayed(); } 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); } } saveChangesIfNotDelayed(); } 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; } } saveChangesIfNotDelayed(); } 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 ); } saveChangesIfNotDelayed(); } void DB::ImageInfo::setPositionedTags(const QString& category, const QMap &positionedTags) { m_dirty = true; m_taggedAreas[category] = positionedTags; saveChangesIfNotDelayed(); } 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 KGeoMap::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() ); } KGeoMap::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/NewImageFinder.cpp b/DB/NewImageFinder.cpp index 155e024a..f872f722 100644 --- a/DB/NewImageFinder.cpp +++ b/DB/NewImageFinder.cpp @@ -1,740 +1,740 @@ /* 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 "NewImageFinder.h" #include "FastDir.h" #include "Logging.h" #include "ImageScout.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 using namespace DB; /***************************************************************** * * NOTES ON PERFORMANCE * ===== == =========== * * - Robert Krawitz 2018-05-24 * * * GENERAL NOTES ON STORAGE I/O * ------- ----- -- ------- --- * * The two main gates to loading new images are: * * 1) I/O (how fast can we read images off mass storage) * * Different I/O devices have different characteristics in terms of * througput, media latency, and protocol latency. * * - Throughput is the raw speed at which data can be transferred, * limited by the physical and/or electronic characteristics of * the medium and the interface. Short of reducing the amount of * data that's transferred, or clever games with using the most * efficient part of the medium (the outer tracks only for HDD's, * a practice referred to as "short stroking" because it reduces * the distance the head has to seek, at the cost of wasting a * lot of capacity), there's nothing that can be done about this. * * - Media latency is the latency component due to characteristics * of the underlying storage medium. For spinning disks, this is * a function of rotational latency and sek latency. In some * cases, particularly with hard disks, it is possible to reduce * media latency by arranging to access the data in a way that * reduces seeking. See DB/FastDir.cpp for an example of this. * * While media latency can sometimes be hidden by overlapping * I/O, generally not possible to avoid it. Sometimes trying too * hard can actually increase media latency if it results in I/O * operations competing against each other requiring additional * seeks. * * Overlapping I/O with computation is another matter; that can * easily yield benefit, especially if it eliminates rotational * latency. * * - Protocol latency. This refers to things like SATA overhead, * network overhead (for images stored on a network), and so * forth. This can encompass multiple things, and often they can * be pipelined by means of multiple queued I/O operations. For * example, multiple commands can be issued to modern interfaces * (SATA, NVMe) and many network interfaces without waiting for * earlier operations to return. * * If protocol latency is high compared with media latency, * having multiple requests outstanding simultaneously can * yield significant benefits. * * iostat is a valuable tool for investigating throughput and * looking for possible optimizations. The IO/sec and data * read/written per second when compared against known media * characteristics (disk and SSD throughput, network bandwidth) * provides valuable information about whether we're getting close * to full performance from the I/O, and user and system CPU time * give us additional clues about whether we're I/O-bound or * CPU-bound. * * Historically in the computer field, operations that require * relatively simple processing on large volumes of data are I/O * bound. But with very fast I/O devices such as NVMe SSDs, some * of which reach 3 GB/sec, that's not always the case. * * 2) Image (mostly JPEG) loading. * * This is a function of image characteristics and image processing * libraries. Sometimes it's possible to apply parameters to * the underlying image loader to speed it up. This shows up as user * CPU time. Usually the only way to improve this performance * characteristic is to use more or faster CPU cores (sometimes GPUs * can assist here) or use better image loading routines (better * libraries). * * * DESCRIPTION OF KPHOTOALBUM IMAGE LOAD PROCESS * ----------- -- ----------- ----- ---- ------- * * KPhotoAlbum, when it loads an image, performs three processing steps: * * 1) Compute the MD5 checksum * - * 2) Extract the EXIF metadata + * 2) Extract the Exif metadata * * 3) Generate a thumbnail * * Previous to this round of performance tuning, the first two steps * were performed in the first pass, and thumbnails were generated in * a separate pass. Assuming that the set of new images is large enough * that they cannot all fit in RAM buffers, this results in the I/O * being performed twice. The rewrite results in I/O being performed once. * * In addition, I have made many other changes: * * 1) Prior to the MD5 calculation step, a new thread, called a "scout * thread", reads the files into memory. While this memory is not * directly used in the later computations, it results in the images * being in RAM when they are later needed, making the I/O very fast * (copying data in memory rather than reading it from storage). * * This is a way to overlap I/O with computation. * * 2) The MD5 checksum uses its own I/O to read the data in in larger * chunks than the Qt MD5 routine does. The Qt routine reads it in * in 4KiB chunks; my experimentation has found that 256KiB chunks * are more efficient, even with a scout thread (it reduces the * number of system calls). * * 3) When searching for other images to stack with the image being * loaded, the new image loader no longer attempts to determine * whether other candidate filenames are present, nor does it * compute the MD5 checksum of any such files it does find. Rather, * it only checks for files that are already in KPhotoAlbum, either * previously or as a result of the current load. Merely checking * for the presence of another file is not cheap, and it's not * necessary; if an image will belong to a stack, we'll either know * it now or when other images that can be stacked are loaded. * - * 4) The EXIF metadata extraction is now done only once; previously + * 4) The Exif metadata extraction is now done only once; previously * it was performed several times at different stages of the loading * process. * * 5) The thumbnail index is now written out incrementally rather than * the entire index (which can be many megabytes in a large image * database) being rewritten frequently. The index is fully rewritten * prior to exit. * * * BASELINE PERFORMANCE * -------- ----------- * * These measurements were all taken on a Lenovo ThinkPad P70 with 32 * GB of dual-channel DDR4-2400 DRAM, a Xeon E3-1505M CPU (4 cores/8 * total hyperthreads, 2.8-3.7 GHz Skylake; usually runs around * 3.1-3.2 GHz in practice), a Seagate ST2000LM015-2E8174 2TB HDD, and * a Crucial MX300 1TB SATA SSD. Published numbers and measurements I * took otherwise indicate that the HDD can handle about 105-110 * MB/sec with a maximum of 180 IO/sec (in a favorable case). The SSD * is rated to handle 530 MB/sec read, 510 MB/sec write, 92K random * reads/sec, and 83K random writes/sec. * * The image set I used for all measurements, except as noted, * consists of 10839 total files of which about 85% are 20 MP JPEG and * the remainder (with a few exceptions are 20 MP RAW files from a * Canon EOS 7D mkII camera. The total dataset is about 92 GB in * size. * * I baselined both drives by reading the same dataset by means of * * % ls | xargs cat | dd bs=1048576 of=/dev/null * * The HDD required between 850 and 870 seconds (14'10" to 14'30") to * perform this operation, yielding about 105-108 MB/sec. The SSD * achieved about 271 MB/sec, which is well under its rated throughput * (hdparm -Tt yields 355 MB/sec, which is likewise nowhere close to * its rated throughput). hdparm -Tt on the HDD yields about 120 * MB/sec, but throughput to an HDD depends upon which part of the * disk is being read. The outer tracks have a greater angular * density to achieve the same linear density (in other words, the * circumference of an outer track is longer than that of an inner * track, and the data is stored at a constant linear density). So * hdparm isn't very useful on an HDD except as a best case. * * Note also that hdparm does a single stream read from the device. * It does not take advantage of the ability to queue multiple * requests. * * * ANALYSIS OF KPHOTOALBUM LOAD PERFORMANCE * -------- -- ----------- ---- ----------- * * I analyzed the following cases, with images stored both on the * HDD and the SSD: * * 1) Images loaded (All, JPEG only, RAW only) * * B) Thumbnail creation (Including, Excluding) * * C) Scout threads (0, 1, 2, 3) * * The JPG image set constitutes 9293 images totaling about 55 GB. The * JPEG files are mostly 20 MP high quality files, in the range of * 6-10 MB. * The RAW image set constitutes 1544 images totaling about 37 GB. The * RAW files are 20 MP files, in the range of 25 MB. * The ALL set consists of 10839 or 10840 images totaling about 92 GB * (the above set plus 2 .MOV files and in some cases one additional * JPEG file). * * Times are elapsed times; CPU consumption is approximate user+system * CPU consumption. Numbers in parentheses are with thumbnail * building disabled. Note that in the cases with no scout threads on * the SSD the times were reproducibly shorter with thumbnail building * enabled (reasons are not determined at this time). * * Cases building RAW thumbnails generally consumed somewhat more * system CPU (in the range of 10-15%) than JPEG-only cases. This may * be due to custom I/O routines used for generating thumbnails with * JPEG files; RAW files used the I/O provided by libkdcraw, which * uses smaller I/O operations. * * Estimating CPU time for mixed workloads proved very problematic, * as there were significant changes over time. * * Elapsed Time * ------- ---- * * SSD HDD * * JPG - 0 scouts 4:03 (3:59) * JPG - 1 scout 2:46 (2:44) * JPG - 2 scouts 2:20 (2:07) * JPG - 3 scouts 2:21 (1:58) * * ALL - 0 scouts 6:32 (7:03) 16:01 * ALL - 1 scout 4:33 (4:33) 15:01 * ALL - 2 scouts 3:37 (3:28) 16:59 * ALL - 3 scouts 3:36 (3:15) * * RAW - 0 scouts 2:18 (2:46) * RAW - 1 scout 1:46 (1:46) * RAW - 2 scouts 1:17 (1:17) * RAW - 3 scouts 1:13 (1:13) * * User+System CPU * ----------- --- * * SSD HDD * * JPG - 0 scouts 40% (12%) * JPG - 1 scout 70% (20%) * JPG - 2 scouts 85% (15%) * JPG - 3 scouts 85% (15%) * * RAW - 0 scouts 15% (10%) * RAW - 1 scout 18% (12%) * RAW - 2 scouts 25% (15%) * RAW - 3 scouts 25% (15%) * * I also used kcachegrind to measure CPU consumption on smaller * subsets of images (with and without thumbnail creation). In terms * of user CPU consumption, thumbnail creation constitutes the large * majority of CPU cycles for processing JPEG files, followed by MD5 - * computation, with EXIF parsing lagging far behind. For RAW files, + * computation, with Exif parsing lagging far behind. For RAW files, * MD5 computation consumes more cycles, likely in part due to the * larger size of RAW files but possibly also related to the smaller * filesize of embedded thumbnails (on the Canon 7D mkII, the embedded * thumbnail is full size but low quality). * * With thumbnail generation: * ---- --------- ----------- * * RAW JPEG * * Thumbnail generation 44% 82% * libjpeg processing 43% 82% * MD5 computation 51% 13% - * Read EXIF 1% 1.0% + * Read Exif 1% 1.0% * * Without thumbnail generation: * ------- --------- ----------- * * RAW JPEG * * MD5 computation 92% 80% - * Read EXIF 4% 10% + * Read Exif 4% 10% * * * CONCLUSIONS * ----------- * * For loading files from hard disk (likely the most common case), * there's no reason to consider any loading method other than using a * single scout thread and computing thumbnails concurrently. Even * with thumbnail computation, there is very little CPU utilization. * * Loading from SATA SSD benefits from two scout threads, and possibly * more. For minimal time to regain control, there is some benefit * seen from separating thumbnail generation from the rest of the * processing stages at the cost of more total elapsed time. This is * more evident with JPEG files than with RAW files in this test case. * RAW files typically have smaller thumbnail images which can be * extracted and processed more quickly than full-size JPEG files. On * a slower CPU, it may be desirable to return control to the user * even if the thumbnails are not built yet. * * Two other cases would be NVMe (or other very fast) SSDs and network * storage. Since we're seeing evidence of CPU saturation on SATA * SSDs, we would likely see this even more strongly with NVMe; with * large numbers of images it may be desirable to separate the * thumbnail building from the rest of the processing. It may also be * beneficial to use more scout threads. * * Network storage presents a different problem. It is likely to have * lower throughput -- and certainly much higher latency -- than even * HDD, unless the underlying storage medium is SSD and the data is * located on a very fast, low latency network. So there would be no * benefit to separating thumbnail processing. However, due to * protocol vs. media latency discussed above, it may well work to use * more scout threads. However, this may saturate the network and the * storage, to the detriment of other users, and there's probably no * general (or easily discoverable) optimum for this. * * It's my judgment that most images will be stored on HDDs for at * least the next few years, so tuning for that use case is probably * the best single choice to be made. * *****************************************************************/ namespace { // Number of scout threads for preloading images. More than one scout thread // yields about 10% less performance with higher IO/sec but lower I/O throughput, // most probably due to thrashing. constexpr int IMAGE_SCOUT_THREAD_COUNT = 1; } bool NewImageFinder::findImages() { // Load the information from the XML file. DB::FileNameSet loadedFiles; QElapsedTimer timer; timer.start(); // TODO: maybe the databas interface should allow to query if it // knows about an image ? Here we've to iterate through all of them and it // might be more efficient do do this in the database without fetching the // whole info. for ( const DB::FileName& fileName : DB::ImageDB::instance()->images()) { loadedFiles.insert(fileName); } m_pendingLoad.clear(); searchForNewFiles( loadedFiles, Settings::SettingsData::instance()->imageDirectory() ); int filesToLoad = m_pendingLoad.count(); loadExtraFiles(); qCDebug(TimingLog) << "Loaded " << filesToLoad << " images in " << timer.elapsed() / 1000.0 << " seconds"; // Man this is not super optimal, but will be changed onces the image finder moves to become a background task. if ( MainWindow::FeatureDialog::hasVideoThumbnailer() ) { BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::SearchForVideosWithoutVideoThumbnailsJob ); } // To avoid deciding if the new images are shown in a given thumbnail view or in a given search // we rather just go to home. return (!m_pendingLoad.isEmpty()); // returns if new images was found. } void NewImageFinder::searchForNewFiles( const DB::FileNameSet& loadedFiles, QString directory ) { qApp->processEvents( QEventLoop::AllEvents ); directory = Utilities::stripEndingForwardSlash(directory); const QString imageDir = Utilities::stripEndingForwardSlash(Settings::SettingsData::instance()->imageDirectory()); FastDir dir( directory ); const QStringList dirList = dir.entryList( ); ImageManager::RAWImageDecoder dec; QStringList excluded; excluded << Settings::SettingsData::instance()->excludeDirectories(); excluded = excluded.at(0).split(QString::fromLatin1(",")); bool skipSymlinks = Settings::SettingsData::instance()->skipSymlinks(); // Keep files within a directory more local by processing all files within the // directory, and then all subdirectories. QStringList subdirList; for( QStringList::const_iterator it = dirList.constBegin(); it != dirList.constEnd(); ++it ) { const DB::FileName file = DB::FileName::fromAbsolutePath(directory + QString::fromLatin1("/") + *it); if ( (*it) == QString::fromLatin1(".") || (*it) == QString::fromLatin1("..") || excluded.contains( (*it) ) || loadedFiles.contains( file ) || dec._skipThisFile(loadedFiles, file) || (*it) == QString::fromLatin1("CategoryImages") ) continue; QFileInfo fi( file.absolute() ); if ( !fi.isReadable() ) continue; if ( skipSymlinks && fi.isSymLink() ) continue; if ( fi.isFile() ) { if ( ! DB::ImageDB::instance()->isBlocking( file ) ) { if ( Utilities::canReadImage(file) ) m_pendingLoad.append( qMakePair( file, DB::Image ) ); else if ( Utilities::isVideo( file ) ) m_pendingLoad.append( qMakePair( file, DB::Video ) ); } } else if ( fi.isDir() ) { subdirList.append( file.absolute() ); } } for( QStringList::const_iterator it = subdirList.constBegin(); it != subdirList.constEnd(); ++it ) searchForNewFiles( loadedFiles, *it ); } void NewImageFinder::loadExtraFiles() { // FIXME: should be converted to a threadpool for SMP stuff and whatnot :] QProgressDialog dialog; QElapsedTimer timeSinceProgressUpdate; dialog.setLabelText( i18n("

Loading information from new files

" "

Depending on the number of images, this may take some time.
" "However, there is only a delay when new images are found.

") ); QProgressBar *progressBar = new QProgressBar; progressBar->setFormat( QLatin1String("%v/%m") ); dialog.setBar(progressBar); dialog.setMaximum( m_pendingLoad.count() ); dialog.setMinimumDuration( 1000 ); QAtomicInt loadedCount = 0; setupFileVersionDetection(); int count = 0; ImageScoutQueue asyncPreloadQueue; for( LoadList::Iterator it = m_pendingLoad.begin(); it != m_pendingLoad.end(); ++it ) { asyncPreloadQueue.enqueue((*it).first); } ImageScout scout(asyncPreloadQueue, loadedCount, IMAGE_SCOUT_THREAD_COUNT); scout.start(); Exif::Database::instance()->startInsertTransaction(); dialog.setValue( count ); // ensure to call setProgress(0) timeSinceProgressUpdate.start(); for( LoadList::Iterator it = m_pendingLoad.begin(); it != m_pendingLoad.end(); ++it, ++count ) { qApp->processEvents( QEventLoop::AllEvents ); if ( dialog.wasCanceled() ) { m_pendingLoad.clear(); Exif::Database::instance()->abortInsertTransaction(); return; } // (*it).first: DB::FileName // (*it).second: DB::MediaType loadExtraFile( (*it).first, (*it).second ); loadedCount++; // Atomic if ( timeSinceProgressUpdate.elapsed() >= 1000 ) { dialog.setValue( count ); timeSinceProgressUpdate.restart(); } } dialog.setValue( count ); // loadExtraFile() has already inserted all images into the // database, but without committing the changes DB::ImageDB::instance()->commitDelayedImages(); Exif::Database::instance()->commitInsertTransaction(); ImageManager::ThumbnailBuilder::instance()->save(); } void NewImageFinder::setupFileVersionDetection() { // should be cached because loading once per image is expensive m_modifiedFileCompString = Settings::SettingsData::instance()->modifiedFileComponent(); m_modifiedFileComponent = QRegExp(m_modifiedFileCompString); m_originalFileComponents << Settings::SettingsData::instance()->originalFileComponent(); m_originalFileComponents = m_originalFileComponents.at(0).split(QString::fromLatin1(";")); } void NewImageFinder::loadExtraFile( const DB::FileName& newFileName, DB::MediaType type ) { MD5 sum = Utilities::MD5Sum( newFileName ); if ( handleIfImageHasBeenMoved(newFileName, sum) ) return; // check to see if this is a new version of a previous image - // We'll get the EXIF data later, when we get the MD5 checksum. + // We'll get the Exif data later, when we get the MD5 checksum. ImageInfoPtr info = ImageInfoPtr(new ImageInfo( newFileName, type, false, false )); ImageInfoPtr originalInfo; DB::FileName originalFileName; if (Settings::SettingsData::instance()->detectModifiedFiles()) { // requires at least *something* in the modifiedFileComponent if (m_modifiedFileCompString.length() >= 0 && newFileName.relative().contains(m_modifiedFileComponent)) { for( QStringList::const_iterator it = m_originalFileComponents.constBegin(); it != m_originalFileComponents.constEnd(); ++it ) { QString tmp = newFileName.relative(); tmp.replace(m_modifiedFileComponent, (*it)); originalFileName = DB::FileName::fromRelativePath(tmp); MD5 originalSum; if (newFileName == originalFileName) originalSum = sum; else if (DB::ImageDB::instance()->md5Map()->containsFile( originalFileName ) ) originalSum = DB::ImageDB::instance()->md5Map()->lookupFile( originalFileName ); else // Do *not* attempt to compute the checksum here. It forces a filesystem // lookup on a file that may not exist and substantially degrades // performance by about 25% on an SSD and about 30% on a spinning disk. // If one of these other files exist, it will be found later in // the image search at which point we'll detect the modified file. continue; if ( DB::ImageDB::instance()->md5Map()->contains( originalSum ) ) { // we have a previous copy of this file; copy it's data // from the original. originalInfo = DB::ImageDB::instance()->info( originalFileName ); if ( !originalInfo ) { qCDebug(DBLog) << "Original info not found by name for " << originalFileName.absolute() << ", trying by MD5 sum."; originalFileName = DB::ImageDB::instance()->md5Map()->lookup( originalSum ); if (!originalFileName.isNull()) { qCDebug(DBLog) << "Substitute image " << originalFileName.absolute() << " found."; originalInfo = DB::ImageDB::instance()->info( originalFileName ); } if ( !originalInfo ) { qCWarning(DBLog,"How did that happen? We couldn't find info for the original image %s; can't copy the original data to %s", qPrintable(originalFileName.absolute()), qPrintable(newFileName.absolute())); continue; } } info->copyExtraData(*originalInfo); /* if requested to move, then delete old data from original */ if (Settings::SettingsData::instance()->moveOriginalContents() ) { originalInfo->removeExtraData(); } break; } } } } ImageInfoList newImages; newImages.append( info ); DB::ImageDB::instance()->addImages( newImages, false ); // also inserts image into exif db if present: info->setMD5Sum( sum ); DB::ImageDB::instance()->md5Map()->insert( sum, info->fileName()); if (originalInfo && Settings::SettingsData::instance()->autoStackNewFiles() ) { // stack the files together DB::FileName olderfile = originalFileName; DB::FileName newerfile = info->fileName(); DB::FileNameList tostack; // the newest file should go to the top of the stack tostack.append(newerfile); DB::FileNameList oldStack; if ( ( oldStack = DB::ImageDB::instance()->getStackFor( olderfile)).isEmpty() ) { tostack.append(olderfile); } else { for ( const DB::FileName& tmp : oldStack ) { tostack.append( tmp ); } } DB::ImageDB::instance()->stack(tostack); MainWindow::Window::theMainWindow()->setStackHead(newerfile); // ordering: XXX we ideally want to place the new image right // after the older one in the list. } markUnTagged(info); ImageManager::ThumbnailBuilder::instance()->buildOneThumbnail( info ); if ( info->isVideo() && MainWindow::FeatureDialog::hasVideoThumbnailer() ) { // needs to be done *after* insertion into database BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::ReadVideoLengthJob(info->fileName(), BackgroundTaskManager::BackgroundVideoPreviewRequest)); } } bool NewImageFinder::handleIfImageHasBeenMoved(const FileName &newFileName, const MD5& sum) { if ( DB::ImageDB::instance()->md5Map()->contains( sum ) ) { const DB::FileName matchedFileName = DB::ImageDB::instance()->md5Map()->lookup(sum); QFileInfo fi( matchedFileName.absolute() ); if ( !fi.exists() ) { // The file we had a collapse with didn't exists anymore so it is likely moved to this new name ImageInfoPtr info = DB::ImageDB::instance()->info( matchedFileName); if ( !info ) qCWarning(DBLog, "How did that happen? We couldn't find info for the images %s", qPrintable(matchedFileName.relative())); else { info->delaySavingChanges(true); fi = QFileInfo ( matchedFileName.relative() ); if ( info->label() == fi.completeBaseName() ) { fi = QFileInfo( newFileName.absolute() ); info->setLabel( fi.completeBaseName() ); } DB::ImageDB::instance()->renameImage( info, newFileName ); // We need to insert the new name into the MD5 map, // as it is a map, the value for the moved file will automatically be deleted. DB::ImageDB::instance()->md5Map()->insert( sum, info->fileName()); Exif::Database::instance()->remove( matchedFileName ); Exif::Database::instance()->add( newFileName); ImageManager::ThumbnailBuilder::instance()->buildOneThumbnail( info ); return true; } } } return false; // The image wasn't just moved } bool NewImageFinder::calculateMD5sums( const DB::FileNameList& list, DB::MD5Map* md5Map, bool* wasCanceled) { // FIXME: should be converted to a threadpool for SMP stuff and whatnot :] QProgressDialog dialog; dialog.setLabelText( i18np("

Calculating checksum for %1 file

","

Calculating checksums for %1 files

", list.size()) + i18n("

By storing a checksum for each image " "KPhotoAlbum is capable of finding images " "even when you have moved them on the disk.

")); dialog.setMaximum(list.size()); dialog.setMinimumDuration( 1000 ); int count = 0; DB::FileNameList cantRead; bool dirty = false; for (const FileName& fileName : list) { if ( count % 10 == 0 ) { dialog.setValue( count ); // ensure to call setProgress(0) qApp->processEvents( QEventLoop::AllEvents ); if ( dialog.wasCanceled() ) { if ( wasCanceled ) *wasCanceled = true; return dirty; } } MD5 md5 = Utilities::MD5Sum( fileName ); if (md5.isNull()) { cantRead << fileName; continue; } ImageInfoPtr info = ImageDB::instance()->info(fileName); if ( info->MD5Sum() != md5 ) { info->setMD5Sum( md5 ); dirty = true; ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } md5Map->insert( md5, fileName ); ++count; } if ( wasCanceled ) *wasCanceled = false; if ( !cantRead.empty() ) KMessageBox::informationList( nullptr, i18n("Following files could not be read:"), cantRead.toStringList(DB::RelativeToImageRoot) ); return dirty; } void DB::NewImageFinder::markUnTagged( ImageInfoPtr info ) { if ( Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() ) { info->addCategoryInfo( Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag() ); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/Database.cpp b/Exif/Database.cpp index 498a01f5..09424d67 100644 --- a/Exif/Database.cpp +++ b/Exif/Database.cpp @@ -1,684 +1,684 @@ /* 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 "Database.h" #include "Logging.h" #include "DB/ImageDB.h" #include "Exif/DatabaseElement.h" #include "MainWindow/Window.h" #include "Settings/SettingsData.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Exif; namespace { // schema version; bump it up whenever the database schema changes constexpr int DB_VERSION = 3; const Database::ElementList elements(int since=0) { static Database::ElementList elms; static int sinceDBVersion[DB_VERSION]{}; if ( elms.count() == 0 ) { elms.append( new RationalExifElement( "Exif.Photo.FocalLength" ) ); elms.append( new RationalExifElement( "Exif.Photo.ExposureTime" ) ); elms.append( new RationalExifElement( "Exif.Photo.ApertureValue" ) ); elms.append( new RationalExifElement( "Exif.Photo.FNumber" ) ); //elms.append( new RationalExifElement( "Exif.Photo.FlashEnergy" ) ); elms.append( new IntExifElement( "Exif.Photo.Flash" ) ); elms.append( new IntExifElement( "Exif.Photo.Contrast" ) ); elms.append( new IntExifElement( "Exif.Photo.Sharpness" ) ); elms.append( new IntExifElement( "Exif.Photo.Saturation" ) ); elms.append( new IntExifElement( "Exif.Image.Orientation" ) ); elms.append( new IntExifElement( "Exif.Photo.MeteringMode" ) ); elms.append( new IntExifElement( "Exif.Photo.ISOSpeedRatings" ) ); elms.append( new IntExifElement( "Exif.Photo.ExposureProgram" ) ); elms.append( new StringExifElement( "Exif.Image.Make" ) ); elms.append( new StringExifElement( "Exif.Image.Model" ) ); // gps info has been added in database schema version 2: sinceDBVersion[1] = elms.size(); elms.append( new IntExifElement( "Exif.GPSInfo.GPSVersionID" ) ); // actually a byte value elms.append( new RationalExifElement( "Exif.GPSInfo.GPSAltitude" ) ); elms.append( new IntExifElement( "Exif.GPSInfo.GPSAltitudeRef" ) ); // actually a byte value elms.append( new StringExifElement( "Exif.GPSInfo.GPSMeasureMode" ) ); elms.append( new RationalExifElement( "Exif.GPSInfo.GPSDOP" ) ); elms.append( new RationalExifElement( "Exif.GPSInfo.GPSImgDirection" ) ); elms.append( new RationalExifElement( "Exif.GPSInfo.GPSLatitude" ) ); elms.append( new StringExifElement( "Exif.GPSInfo.GPSLatitudeRef" ) ); elms.append( new RationalExifElement( "Exif.GPSInfo.GPSLongitude" ) ); elms.append( new StringExifElement( "Exif.GPSInfo.GPSLongitudeRef" ) ); elms.append( new RationalExifElement( "Exif.GPSInfo.GPSTimeStamp" ) ); // lens info has been added in database schema version 3: sinceDBVersion[2] = elms.size(); elms.append( new LensExifElement( ) ); } // query only for the newly added stuff: if (since > 0) return elms.mid(sinceDBVersion[since]); return elms; } } Exif::Database* Exif::Database::s_instance = nullptr; /** * @brief show and error message for the failed \p query and disable the Exif database. * The database is closed because at this point we can not trust the data inside. * @param query */ void Database::showErrorAndFail(QSqlQuery &query) const { const QString txt = i18n("

There was an error while accessing the Exif search database. " "The error is likely due to a broken database file.

" "

To fix this problem run Maintenance->Recreate Exif Search database.

" "
" "

For debugging: the command that was attempted to be executed was:
%1

" "

The error message obtained was:
%2

", query.lastQuery(), query.lastError().text() ); const QString technicalInfo = QString::fromUtf8("Error running query: %s\n Error was: %s") .arg(query.lastQuery(), query.lastError().text()); showErrorAndFail(txt, technicalInfo); } void Database::showErrorAndFail(const QString &errorMessage, const QString &technicalInfo) const { KMessageBox::information( MainWindow::Window::theMainWindow(), errorMessage, i18n("Error in Exif database"), QString::fromLatin1( "sql_error_in_exif_DB" ) ); qCWarning(ExifLog) << technicalInfo; // disable exif db for now: m_isFailed = true; } Exif::Database::Database() : m_isOpen(false) , m_isFailed(false) { m_db = QSqlDatabase::addDatabase( QString::fromLatin1( "QSQLITE" ), QString::fromLatin1( "exif" ) ); } void Exif::Database::openDatabase() { m_db.setDatabaseName( exifDBFile() ); m_isOpen = m_db.open(); if ( !m_isOpen ) { const QString txt = i18n("

There was an error while opening the Exif search database.

" "

To fix this problem run Maintenance->Recreate Exif Search database.

" "
" "

The error message obtained was:
%1

", m_db.lastError().text() ); const QString logMsg = QString::fromUtf8("Could not open Exif search database! " "Error was: %s").arg(m_db.lastError().text()); showErrorAndFail(txt, logMsg); return; } // If SQLite in Qt has Unicode feature, it will convert queries to // UTF-8 automatically. Otherwise we should do the conversion to // be able to store any Unicode character. m_doUTF8Conversion = !m_db.driver()->hasFeature(QSqlDriver::Unicode); } Exif::Database::~Database() { // We have to close the database before destroying the QSqlDatabase object, // otherwise Qt screams and kittens might die (see QSqlDatabase's // documentation) if ( m_db.isOpen() ) m_db.close(); } bool Exif::Database::isOpen() const { return m_isOpen && ! m_isFailed; } void Exif::Database::populateDatabase() { createMetadataTable(SchemaAndDataChanged); QStringList attributes; Q_FOREACH( DatabaseElement *element, elements() ) { attributes.append( element->createString() ); } QSqlQuery query( QString::fromLatin1( "create table if not exists exif (filename string PRIMARY KEY, %1 )") .arg( attributes.join( QString::fromLatin1(", ") ) ), m_db ); if ( !query.exec()) showErrorAndFail( query ); } void Exif::Database::updateDatabase() { if ( m_db.tables().isEmpty()) { const QString txt = i18n("

The Exif search database is corrupted and has no data.

" "

To fix this problem run Maintenance->Recreate Exif Search database.

" ); const QString logMsg = QString::fromUtf8("Database open but empty!"); showErrorAndFail(txt, logMsg); return; } const int version = DBFileVersion(); if (m_isFailed) return; if (version < DBVersion()) { // on the next update, we can just query the DB Version createMetadataTable(SchemaChanged); } // update schema if ( version < DBVersion() ) { QSqlQuery query( m_db ); for( const DatabaseElement *e : elements(version)) { query.prepare( QString::fromLatin1( "alter table exif add column %1") .arg( e->createString()) ); if ( !query.exec()) showErrorAndFail( query ); } } } void Exif::Database::createMetadataTable(DBSchemaChangeType change) { QSqlQuery query(m_db); query.prepare( QString::fromLatin1( "create table if not exists settings (keyword TEXT PRIMARY KEY, value TEXT) without rowid") ); if ( !query.exec()) { showErrorAndFail( query ); return; } query.prepare( QString::fromLatin1( "insert or replace into settings (keyword, value) values('DBVersion','%1')").arg( Database::DBVersion())); if ( !query.exec()) { showErrorAndFail( query ); return; } if (change == SchemaAndDataChanged) { query.prepare( QString::fromLatin1( "insert or replace into settings (keyword, value) values('GuaranteedDataVersion','%1')").arg( Database::DBVersion())); if ( !query.exec()) showErrorAndFail( query ); } } bool Exif::Database::add( const DB::FileName& fileName ) { if ( !isUsable() ) return false; try { Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data()); Q_ASSERT(image.get() != nullptr); image->readMetadata(); Exiv2::ExifData &exifData = image->exifData(); return insert( fileName, exifData ); } catch (...) { qCWarning(ExifLog, "Error while reading exif information from %s", qPrintable(fileName.absolute()) ); return false; } } bool Exif::Database::add( DB::FileInfo& fileInfo ) { if ( !isUsable() ) return false; return insert( fileInfo.getFileName(), fileInfo.getExifData() ); } bool Exif::Database::add( const DB::FileNameList& list ) { if ( !isUsable() ) return false; QList map; Q_FOREACH(const DB::FileName& fileName, list) { try { Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data()); Q_ASSERT(image.get() != nullptr); image->readMetadata(); map << DBExifInfo(fileName, image->exifData()); } catch (...) { qWarning("Error while reading exif information from %s", qPrintable(fileName.absolute()) ); } } insert(map); return true; } void Exif::Database::remove( const DB::FileName& fileName ) { if ( !isUsable() ) return; QSqlQuery query( m_db); query.prepare( QString::fromLatin1( "DELETE FROM exif WHERE fileName=?" )); query.bindValue( 0, fileName.absolute() ); if ( !query.exec() ) showErrorAndFail( query ); } void Exif::Database::remove( const DB::FileNameList& list ) { if ( !isUsable() ) return; m_db.transaction(); QSqlQuery query( m_db); query.prepare( QString::fromLatin1( "DELETE FROM exif WHERE fileName=?" )); Q_FOREACH(const DB::FileName& fileName, list) { query.bindValue( 0, fileName.absolute() ); if ( !query.exec() ) { m_db.rollback(); showErrorAndFail( query ); return; } } m_db.commit(); } QSqlQuery *Exif::Database::getInsertQuery() { if ( !isUsable() ) return nullptr; if ( m_insertTransaction ) return m_insertTransaction; if (m_queryString.isEmpty()) { QStringList formalList; Database::ElementList elms = elements(); for( const DatabaseElement *e : elms ) { formalList.append( e->queryString() ); } m_queryString = QString::fromLatin1( "INSERT OR REPLACE into exif values (?, %1) " ).arg( formalList.join( QString::fromLatin1( ", " ) ) ); } QSqlQuery *query = new QSqlQuery(m_db); if (query) query->prepare( m_queryString ); return query; } void Exif::Database::concludeInsertQuery( QSqlQuery *query ) { if ( m_insertTransaction ) return; m_db.commit(); delete query; } bool Exif::Database::startInsertTransaction() { Q_ASSERT(m_insertTransaction == nullptr); m_insertTransaction = getInsertQuery(); m_db.transaction(); return ( m_insertTransaction != nullptr ); } bool Exif::Database::commitInsertTransaction() { if (m_insertTransaction) { m_db.commit(); delete m_insertTransaction; m_insertTransaction = nullptr; } else qCWarning(ExifLog, "Trying to commit transaction, but no transaction is active!"); return true; } bool Exif::Database::abortInsertTransaction() { if (m_insertTransaction) { m_db.rollback(); delete m_insertTransaction; m_insertTransaction = nullptr; } else qCWarning(ExifLog, "Trying to abort transaction, but no transaction is active!"); return true; } bool Exif::Database::insert(const DB::FileName& filename, Exiv2::ExifData data ) { if ( !isUsable() ) return false; QSqlQuery *query = getInsertQuery(); query->bindValue( 0, filename.absolute() ); int i = 1; for( const DatabaseElement *e : elements() ) { query->bindValue( i++, e->valueFromExif(data)); } bool status = query->exec(); if ( !status ) showErrorAndFail( *query ); concludeInsertQuery( query ); return status; } bool Exif::Database::insert(QList map ) { if ( !isUsable() ) return false; QSqlQuery *query = getInsertQuery(); // not a const reference because DatabaseElement::valueFromExif uses operator[] on the exif datum Q_FOREACH ( DBExifInfo elt, map ) { query->bindValue( 0, elt.first.absolute() ); int i = 1; for( const DatabaseElement *e : elements() ) { query->bindValue( i++, e->valueFromExif(elt.second)); } if ( !query->exec() ) { showErrorAndFail( *query ); } } concludeInsertQuery( query ); return true; } Exif::Database* Exif::Database::instance() { if ( !s_instance ) { qCInfo(ExifLog) << "initializing Exif database..."; s_instance = new Exif::Database(); s_instance->init(); } return s_instance; } void Exif::Database::deleteInstance() { delete s_instance; s_instance = nullptr; } bool Exif::Database::isAvailable() { #ifdef QT_NO_SQL return false; #else return QSqlDatabase::isDriverAvailable( QString::fromLatin1( "QSQLITE" ) ); #endif } int Exif::Database::DBFileVersion() const { // previous to KPA 4.6, there was no metadata table: if ( !m_db.tables().contains( QString::fromLatin1("settings")) ) return 1; QSqlQuery query( QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'DBVersion'"), m_db ); if ( !query.exec() ) showErrorAndFail( query ); if (query.first()) { return query.value(0).toInt(); } return 0; } int Exif::Database::DBFileVersionGuaranteed() const { // previous to KPA 4.6, there was no metadata table: if ( !m_db.tables().contains( QString::fromLatin1("settings")) ) return 0; QSqlQuery query( QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'GuaranteedDataVersion'"), m_db ); if ( !query.exec() ) showErrorAndFail( query ); if (query.first()) { return query.value(0).toInt(); } return 0; } constexpr int Exif::Database::DBVersion() { return DB_VERSION; } bool Exif::Database::isUsable() const { return (isAvailable() && isOpen()); } QString Exif::Database::exifDBFile() { return ::Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("/exif-info.db"); } bool Exif::Database::readFields( const DB::FileName& fileName, ElementList &fields) const { if ( !isUsable() ) return false; bool foundIt = false; QStringList fieldList; for( const DatabaseElement *e : fields ) { fieldList.append( e->columnName() ); } QSqlQuery query( m_db ); // the query returns a single value, so we don't need the overhead for random access: query.setForwardOnly( true ); query.prepare( QString::fromLatin1( "select %1 from exif where filename=?") .arg( fieldList.join( QString::fromLatin1(", "))) ); query.bindValue( 0, fileName.absolute() ); if ( !query.exec() ) { showErrorAndFail( query ); } if ( query.next() ) { // file in exif db -> write back results int i=0; for( DatabaseElement *e : fields ) { e->setValue( query.value(i++) ); } foundIt = true; } return foundIt; } DB::FileNameSet Exif::Database::filesMatchingQuery( const QString& queryStr ) const { if ( !isUsable() ) return DB::FileNameSet(); DB::FileNameSet result; QSqlQuery query( queryStr, m_db ); if ( !query.exec() ) showErrorAndFail( query ); else { if ( m_doUTF8Conversion ) while ( query.next() ) result.insert( DB::FileName::fromAbsolutePath( QString::fromUtf8( query.value(0).toByteArray() ) ) ); else while ( query.next() ) result.insert( DB::FileName::fromAbsolutePath( query.value(0).toString() ) ); } return result; } QList< QPair > Exif::Database::cameras() const { QList< QPair > result; if ( !isUsable() ) return result; QSqlQuery query( QString::fromLatin1("SELECT DISTINCT Exif_Image_Make, Exif_Image_Model FROM exif"), m_db ); if ( !query.exec() ) { showErrorAndFail( query ); } else { while ( query.next() ) { QString make = query.value(0).toString(); QString model = query.value(1).toString(); if ( !make.isEmpty() && !model.isEmpty() ) result.append( qMakePair( make, model ) ); } } return result; } QList< QString > Exif::Database::lenses() const { QList< QString > result; if ( !isUsable() ) return result; QSqlQuery query( QString::fromLatin1("SELECT DISTINCT Exif_Photo_LensModel FROM exif"), m_db ); if ( !query.exec() ) { showErrorAndFail( query ); } else { while ( query.next() ) { QString lens = query.value(0).toString(); if ( !lens.isEmpty() ) result.append( lens ); } } return result; } void Exif::Database::init() { if ( !isAvailable() ) return; m_isFailed = false; m_insertTransaction = nullptr; bool dbExists = QFile::exists( exifDBFile() ); openDatabase(); if ( !isOpen() ) return; if ( !dbExists ) populateDatabase(); else updateDatabase(); } void Exif::Database::recreate() { // We create a backup of the current database in case // the user presse 'cancel' or there is any error. In that case // we want to go back to the original DB. const QString origBackup = exifDBFile() + QLatin1String(".bak"); m_db.close(); QDir().remove(origBackup); QDir().rename(exifDBFile(), origBackup); init(); const DB::FileNameList allImages = DB::ImageDB::instance()->images(); QProgressDialog dialog; dialog.setModal(true); - dialog.setLabelText(i18n("Rereading EXIF information from all images")); + dialog.setLabelText(i18n("Rereading Exif information from all images")); dialog.setMaximum(allImages.size()); // using a transaction here removes a *huge* overhead on the insert statements startInsertTransaction(); int i = 0; for (const DB::FileName& fileName : allImages) { const DB::ImageInfoPtr info = fileName.info(); dialog.setValue(i++); if (info->mediaType() == DB::Image) { add(fileName); } if ( i % 10 ) qApp->processEvents(); if (dialog.wasCanceled()) break; } // PENDING(blackie) We should count the amount of files that did not succeeded and warn the user. if (dialog.wasCanceled()) { abortInsertTransaction(); m_db.close(); QDir().remove(exifDBFile()); QDir().rename(origBackup, exifDBFile()); init(); } else { commitInsertTransaction(); QDir().remove(origBackup); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/Info.cpp b/Exif/Info.cpp index 78846d65..be4c88f3 100644 --- a/Exif/Info.cpp +++ b/Exif/Info.cpp @@ -1,244 +1,244 @@ /* 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 "Info.h" #include "Logging.h" #include "DB/ImageDB.h" #include "DB/ImageInfo.h" #include "Settings/SettingsData.h" #include "Utilities/StringSet.h" #include "Utilities/Util.h" #include #include #include #include using namespace Exif; Info* Info::s_instance = nullptr; QMap Info::info( const DB::FileName& fileName, StringSet wantedKeys, bool returnFullExifName, const QString& charset ) { QMap result; try { Metadata data = metadata( exifInfoFile(fileName) ); for (Exiv2::ExifData::const_iterator i = data.exif.begin(); i != data.exif.end(); ++i) { QString key = QString::fromLocal8Bit(i->key().c_str()); m_keys.insert( key ); if ( wantedKeys.contains( key ) ) { QString text = key; if ( !returnFullExifName ) text = key.split(QLatin1String(".")).last(); std::ostringstream stream; stream << *i; QString str( Utilities::cStringWithEncoding( stream.str().c_str(), charset ) ); result[ text ] += str; } } for (Exiv2::IptcData::const_iterator i = data.iptc.begin(); i != data.iptc.end(); ++i) { QString key = QString::fromLatin1(i->key().c_str()); m_keys.insert( key ); if ( wantedKeys.contains( key ) ) { QString text = key; if ( !returnFullExifName ) text = key.split( QString::fromLatin1(".") ).last(); std::ostringstream stream; stream << *i; QString str( Utilities::cStringWithEncoding( stream.str().c_str(), charset ) ); result[ text ] += str; } } } catch ( ... ) { } return result; } Info* Info::instance() { if ( !s_instance ) s_instance = new Info; return s_instance; } StringSet Info::availableKeys() { return m_keys; } QMap Info::infoForViewer( const DB::FileName& fileName, const QString& charset ) { return info( fileName, ::Settings::SettingsData::instance()->exifForViewer(), false, charset ); } QMap Info::infoForDialog( const DB::FileName& fileName, const QString& charset ) { return info( fileName, ::Settings::SettingsData::instance()->exifForDialog(), true, charset ); } StringSet Info::standardKeys() { static StringSet res; if ( !res.empty() ) return res; QList tags; std::ostringstream s; #if (EXIV2_TEST_VERSION(0,21,0)) const Exiv2::GroupInfo* gi = Exiv2::ExifTags::groupList(); while (gi->tagList_ != 0) { Exiv2::TagListFct tl = gi->tagList_; const Exiv2::TagInfo* ti = tl(); while (ti->tag_ != 0xFFFF) { tags << ti; ++ti; } ++gi; } for (QList::iterator it = tags.begin(); it != tags.end(); ++it) { while ( (*it)->tag_ != 0xffff ) { res.insert(QString::fromLatin1(Exiv2::ExifKey(**it).key().c_str())); ++(*it); } } #else tags << Exiv2::ExifTags::ifdTagList() << Exiv2::ExifTags::exifTagList() << Exiv2::ExifTags::iopTagList() << Exiv2::ExifTags::gpsTagList(); for (QList::iterator it = tags.begin(); it != tags.end(); ++it ) { while ( (*it)->tag_ != 0xffff ) { res.insert( QLatin1String(Exiv2::ExifKey( (*it)->tag_, Exiv2::ExifTags::ifdItem( (*it)->ifdId_ ) ).key().c_str() )); ++(*it); } } // Now the ugly part -- exiv2 doesn't have any way to get a list of // MakerNote tags in a reasonable form, so we have to parse it from strings for ( Exiv2::IfdId kind = Exiv2::canonIfdId; kind < Exiv2::lastIfdId; kind = static_cast( kind + 1 ) ) { #if EXIV2_TEST_VERSION(0,17,0) Exiv2::ExifTags::taglist( s, kind ); #else Exiv2::ExifTags::makerTaglist( s, kind ); #endif } #endif // IPTC tags use yet another format... Exiv2::IptcDataSets::dataSetList( s ); QStringList lines = QString( QLatin1String(s.str().c_str()) ).split( QChar::fromLatin1('\n') ); for ( QStringList::const_iterator it = lines.constBegin(); it != lines.constEnd(); ++it ) { if ( it->isEmpty() ) continue; QStringList fields = it->split( QChar::fromLatin1('\t') ); if ( fields.size() == 7 ) { QString id = fields[4]; if ( id.endsWith( QChar::fromLatin1(',') ) ) id.chop(1); res.insert( id ); } else { fields = it->split( QLatin1String(", ") ); if ( fields.size () >= 11 ) { res.insert( fields[8] ); } else { qCWarning(ExifLog) << "Unparsable output from exiv2 library: " << *it; continue; } } } return res; } Info::Info() { m_keys = standardKeys(); } void Exif::Info::writeInfoToFile( const DB::FileName& srcName, const QString& destName ) { // Load Exif from source image Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open( QFile::encodeName(srcName.absolute()).data() ); image->readMetadata(); Exiv2::ExifData data = image->exifData(); // Modify Exif information from database. DB::ImageInfoPtr info = DB::ImageDB::instance()->info( srcName ); data["Exif.Image.ImageDescription"] = info->description().toLocal8Bit().data(); image = Exiv2::ImageFactory::open( QFile::encodeName(destName).data() ); image->setExifData(data); image->writeMetadata(); } /** - * Some Canon cameras stores EXIF info in files ending in .thm, so we need to use those files for fetching EXIF info + * Some Canon cameras stores Exif info in files ending in .thm, so we need to use those files for fetching Exif info * if they exists. */ DB::FileName Exif::Info::exifInfoFile( const DB::FileName& fileName ) { QString dirName = QFileInfo( fileName.relative() ).path(); QString baseName = QFileInfo( fileName.relative() ).baseName(); DB::FileName name = DB::FileName::fromRelativePath(dirName + QString::fromLatin1("/") + baseName + QString::fromLatin1( ".thm" )); if ( name.exists() ) return name; name = DB::FileName::fromRelativePath(dirName + QString::fromLatin1("/") + baseName + QString::fromLatin1( ".THM" )); if ( name.exists() ) return name; return fileName; } Exif::Metadata Exif::Info::metadata( const DB::FileName& fileName ) { try { Exif::Metadata result; Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open( QFile::encodeName(fileName.absolute()).data() ); Q_ASSERT(image.get() != 0); image->readMetadata(); result.exif = image->exifData(); result.iptc = image->iptcData(); result.comment = image->comment(); return result; } catch ( ... ) { } return Exif::Metadata(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/InfoDialog.cpp b/Exif/InfoDialog.cpp index 5ac6c06f..0cd284e9 100644 --- a/Exif/InfoDialog.cpp +++ b/Exif/InfoDialog.cpp @@ -1,125 +1,125 @@ /* 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 #include #include #include #include #include #include #include #include "DB/ImageDB.h" #include "Exif/InfoDialog.h" #include "Exif/Info.h" #include "ImageManager/AsyncLoader.h" #include "ImageManager/ImageRequest.h" #include "Settings/SettingsData.h" #include "Grid.h" using Utilities::StringSet; Exif::InfoDialog::InfoDialog(const DB::FileName& fileName, QWidget* parent) : QDialog(parent) { - setWindowTitle( i18n("EXIF Information") ); + setWindowTitle( i18nc("@title:window", "Exif Information") ); setAttribute(Qt::WA_DeleteOnClose); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); buttonBox->button(QDialogButtonBox::Close)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QWidget* top = new QWidget(this); QVBoxLayout* vlay = new QVBoxLayout( top ); setLayout(vlay); vlay->addWidget(top); // -------------------------------------------------- File name and pixmap QHBoxLayout* hlay = new QHBoxLayout; vlay->addLayout(hlay); m_fileNameLabel = new QLabel( top ); QFont fnt = font(); fnt.setPointSize( (int) (fnt.pointSize() * 1.2) ); fnt.setWeight( QFont::Bold ); m_fileNameLabel->setFont( fnt ); m_fileNameLabel->setAlignment( Qt::AlignCenter ); hlay->addWidget( m_fileNameLabel, 1 ); m_pix = new QLabel( top ); hlay->addWidget( m_pix ); // -------------------------------------------------- Exif Grid m_grid = new Exif::Grid( top ); vlay->addWidget( m_grid ); // -------------------------------------------------- Current Search hlay = new QHBoxLayout; vlay->addLayout(hlay); - QLabel* searchLabel = new QLabel( i18n( "EXIF Label Search: "), top ); + QLabel* searchLabel = new QLabel( i18n( "Exif label search: "), top ); hlay->addWidget( searchLabel ); m_searchBox = new QLineEdit( top ); hlay->addWidget( m_searchBox ); hlay->addStretch( 1 ); QLabel* iptcLabel = new QLabel( i18n("IPTC character set:"), top ); m_iptcCharset = new QComboBox( top ); QStringList charsets; QList charsetsBA = QTextCodec::availableCodecs(); for (QList::const_iterator it = charsetsBA.constBegin(); it != charsetsBA.constEnd(); ++it ) charsets << QLatin1String(*it); m_iptcCharset->insertItems( 0, charsets ); m_iptcCharset->setCurrentIndex( qMax( 0, QTextCodec::availableCodecs().indexOf( Settings::SettingsData::instance()->iptcCharset().toLatin1() ) ) ); hlay->addWidget( iptcLabel ); hlay->addWidget( m_iptcCharset ); connect( m_searchBox, SIGNAL(textChanged(QString)), m_grid, SLOT(updateSearchString(QString)) ); connect( m_iptcCharset, SIGNAL(activated(QString)), m_grid, SLOT(setupUI(QString)) ); setImage(fileName); vlay->addWidget(buttonBox); } QSize Exif::InfoDialog::sizeHint() const { return QSize( 800, 400 ); } void Exif::InfoDialog::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& image) { if ( request->loadedOK() ) m_pix->setPixmap( QPixmap::fromImage(image) ); } void Exif::InfoDialog::setImage(const DB::FileName& fileName ) { m_fileNameLabel->setText( fileName.relative() ); m_grid->setFileName( fileName ); ImageManager::ImageRequest* request = new ImageManager::ImageRequest( fileName, QSize( 128, 128 ), fileName.info()->angle(), this ); request->setPriority( ImageManager::Viewer ); ImageManager::AsyncLoader::instance()->load( request ); } void Exif::InfoDialog::enterEvent(QEvent *) { m_grid->setFocus(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/ReReadDialog.cpp b/Exif/ReReadDialog.cpp index 532da2c8..dd5a7ed9 100644 --- a/Exif/ReReadDialog.cpp +++ b/Exif/ReReadDialog.cpp @@ -1,146 +1,146 @@ /* 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 "ReReadDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include Exif::ReReadDialog::ReReadDialog(QWidget* parent) : QDialog(parent) { - setWindowTitle( i18n("Read EXIF info from files") ); + setWindowTitle( i18nc("@title:window", "Read Exif Info from Files") ); QWidget* top = new QWidget; QVBoxLayout* lay1 = new QVBoxLayout( top ); setLayout(lay1); lay1->addWidget(top); - m_exifDB = new QCheckBox( i18n( "Update EXIF search database" ), top ); + m_exifDB = new QCheckBox( i18n( "Update Exif search database" ), top ); lay1->addWidget( m_exifDB ); if ( !Exif::Database::instance()->isUsable() ) { m_exifDB->hide(); } m_date = new QCheckBox( i18n( "Update image date" ), top ); lay1->addWidget( m_date ); - m_force_date = new QCheckBox( i18n( "Use modification date if EXIF not found" ), top ); + m_force_date = new QCheckBox( i18n( "Use modification date if Exif not found" ), top ); lay1->addWidget( m_force_date ); - m_orientation = new QCheckBox( i18n( "Update image orientation from EXIF information" ), top ); + m_orientation = new QCheckBox( i18n( "Update image orientation from Exif information" ), top ); lay1->addWidget( m_orientation ); - m_description = new QCheckBox( i18n( "Update image description from EXIF information" ), top ); + m_description = new QCheckBox( i18n( "Update image description from Exif information" ), top ); lay1->addWidget( m_description ); QGroupBox* box = new QGroupBox( i18n("Affected Files") ); lay1->addWidget( box ); QHBoxLayout* boxLayout = new QHBoxLayout( box ); m_fileList = new QListWidget; m_fileList->setSelectionMode( QAbstractItemView::NoSelection ); boxLayout->addWidget( m_fileList ); connect( m_date, SIGNAL(toggled(bool)), m_force_date, SLOT(setEnabled(bool)) ); connect( m_date, SIGNAL(toggled(bool)), this, SLOT(warnAboutDates(bool)) ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(buttonBox, &QDialogButtonBox::accepted, this, &ReReadDialog::readInfo); lay1->addWidget(buttonBox); } int Exif::ReReadDialog::exec( const DB::FileNameList& list ) { Settings::SettingsData *opt = Settings::SettingsData::instance(); m_exifDB->setChecked( opt->updateExifData() ); m_date->setChecked( opt->updateImageDate() ); m_force_date->setChecked( opt->useModDateIfNoExif() ); m_force_date->setEnabled( opt->updateImageDate() ); m_orientation->setChecked( opt->updateOrientation() ); m_description->setChecked( opt->updateDescription() ); m_list = list; m_fileList->clear(); m_fileList->addItems( list.toStringList(DB::RelativeToImageRoot) ); return QDialog::exec(); } void Exif::ReReadDialog::readInfo() { Settings::SettingsData *opt = Settings::SettingsData::instance(); opt->setUpdateExifData( m_exifDB->isChecked() ); opt->setUpdateImageDate( m_date->isChecked() ); opt->setUseModDateIfNoExif( m_force_date->isChecked() ); opt->setUpdateOrientation( m_orientation->isChecked() ); opt->setUpdateDescription( m_description->isChecked() ); KSharedConfig::openConfig()->sync(); DB::ExifMode mode = DB::EXIFMODE_FORCE; if ( m_exifDB->isChecked() ) mode |= DB::EXIFMODE_DATABASE_UPDATE; if ( m_date->isChecked() ) mode |= DB::EXIFMODE_DATE; if ( m_force_date->isChecked() ) mode |= DB::EXIFMODE_USE_IMAGE_DATE_IF_INVALID_EXIF_DATE; if ( m_orientation->isChecked() ) mode |= DB::EXIFMODE_ORIENTATION; if ( m_description->isChecked() ) mode |= DB::EXIFMODE_DESCRIPTION; accept(); DB::ImageDB::instance()->slotReread(m_list, mode); } void Exif::ReReadDialog::warnAboutDates( bool b ) { if ( !b ) return; - int ret = KMessageBox::warningContinueCancel( this, i18n("

Be aware that setting the data from EXIF may " + int ret = KMessageBox::warningContinueCancel( this, i18n("

Be aware that setting the data from Exif may " "overwrite data you have previously entered " "manually using the image configuration dialog.

" ), i18n( "Override image dates" ) ); if ( ret == KMessageBox::Cancel ) m_date->setChecked( false ); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/SearchDialog.cpp b/Exif/SearchDialog.cpp index 610d53e4..0bf2990a 100644 --- a/Exif/SearchDialog.cpp +++ b/Exif/SearchDialog.cpp @@ -1,432 +1,432 @@ /* 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 "SearchDialog.h" #include #include #include #include #include #include #include "Exif/Database.h" #include #include #include #include #include #include #include using namespace Exif; Exif::SearchDialog::SearchDialog( QWidget* parent ) : KPageDialog( parent ) { - setWindowTitle( i18n("EXIF Search") ); + setWindowTitle( i18nc("@title:window", "Exif Search") ); setFaceType( Tabbed ); QWidget* settings = new QWidget; KPageWidgetItem* page = new KPageWidgetItem( settings, i18n("Settings" ) ); addPage( page ); QVBoxLayout* vlay = new QVBoxLayout( settings ); // Iso, Exposure, Aperture, FNumber QHBoxLayout* hlay = new QHBoxLayout; vlay->addLayout( hlay ); QGridLayout* gridLayout = new QGridLayout; gridLayout->setSpacing( 6 ); hlay->addLayout( gridLayout ); hlay->addStretch( 1 ); makeISO( gridLayout ); makeExposureTime( gridLayout ); hlay->addSpacing(30); gridLayout = new QGridLayout; gridLayout->setSpacing( 6 ); hlay->addLayout( gridLayout ); hlay->addStretch( 1 ); m_apertureValue = makeApertureOrFNumber( i18n( "Aperture Value" ), QString::fromLatin1( "Exif_Photo_ApertureValue" ), gridLayout, 0 ); m_fNumber = makeApertureOrFNumber( i18n( "F Number" ), QString::fromLatin1( "Exif_Photo_FNumber" ), gridLayout, 1 ); hlay->addSpacing(30); // Focal length QHBoxLayout* focalLayout = new QHBoxLayout; focalLayout->setSpacing( 6 ); hlay->addLayout( focalLayout ); hlay->addStretch( 1 ); QLabel* label = new QLabel( i18n( "Focal Length" ) ); focalLayout->addWidget(label); m_fromFocalLength = new QSpinBox; focalLayout->addWidget(m_fromFocalLength); m_fromFocalLength->setRange( 0, 10000 ); m_fromFocalLength->setSingleStep( 10 ); label = new QLabel( i18nc("As in 'A range from x to y'","to")); focalLayout->addWidget(label); m_toFocalLength = new QSpinBox; focalLayout->addWidget(m_toFocalLength); m_toFocalLength->setRange( 0, 10000 ); m_toFocalLength->setSingleStep( 10 ); m_toFocalLength->setValue( 10000 ); QString suffix = i18nc( "This is millimeter for focal length, like 35mm", "mm" ); m_fromFocalLength->setSuffix( suffix ); m_toFocalLength->setSuffix( suffix ); connect( m_fromFocalLength, SIGNAL(valueChanged(int)), this, SLOT(fromFocalLengthChanged(int)) ); connect( m_toFocalLength, SIGNAL(valueChanged(int)), this, SLOT(toFocalLengthChanged(int)) ); // exposure program and Metring mode hlay = new QHBoxLayout; vlay->addLayout( hlay ); hlay->addWidget( makeExposureProgram( settings ) ); hlay->addWidget( makeMeteringMode( settings ) ); vlay->addStretch( 1 ); // ------------------------------------------------------------ Camera page = new KPageWidgetItem( makeCamera(), i18n("Camera") ); addPage( page ); // ------------------------------------------------------------ Lens page = new KPageWidgetItem( makeLens(), i18n("Lens") ); addPage( page ); // ------------------------------------------------------------ Misc QWidget* misc = new QWidget; addPage( new KPageWidgetItem( misc, i18n("Miscellaneous") ) ); vlay = new QVBoxLayout( misc ); vlay->addWidget( makeOrientation( misc ), 1 ); hlay = new QHBoxLayout; vlay->addLayout( hlay ); hlay->addWidget( makeContrast( misc ) ); hlay->addWidget( makeSharpness( misc ) ); hlay->addWidget( makeSaturation( misc ) ); vlay->addStretch( 1 ); } void Exif::SearchDialog::makeISO( QGridLayout* layout ) { Exif::RangeWidget::ValueList list; list << Exif::RangeWidget::Value( 100, QString::fromLatin1("100") ) << Exif::RangeWidget::Value( 200, QString::fromLatin1("200") ) << Exif::RangeWidget::Value( 400, QString::fromLatin1("400") ) << Exif::RangeWidget::Value( 800, QString::fromLatin1("800") ) << Exif::RangeWidget::Value( 1600, QString::fromLatin1("1600") ) << Exif::RangeWidget::Value( 3200, QString::fromLatin1("3200") ) << Exif::RangeWidget::Value( 6400, QString::fromLatin1("6400") ) << Exif::RangeWidget::Value( 12800, QString::fromLatin1("12800") ) << Exif::RangeWidget::Value( 25600, QString::fromLatin1("25600") ) << Exif::RangeWidget::Value( 51200, QString::fromLatin1("51200") ); m_iso = new RangeWidget( i18n("Iso setting" ), QString::fromLatin1( "Exif_Photo_ISOSpeedRatings" ), list, layout, 0 ); } void Exif::SearchDialog::makeExposureTime( QGridLayout* layout ) { QString secs = i18nc( "Example 1.6 secs (as in seconds)", "secs." ); Exif::RangeWidget::ValueList list; list << Exif::RangeWidget::Value( 1.0/4000, QString::fromLatin1( "1/4000" ) ) << Exif::RangeWidget::Value( 1.0/3200, QString::fromLatin1( "1/3200" ) ) << Exif::RangeWidget::Value( 1.0/2500, QString::fromLatin1( "1/2500" ) ) << Exif::RangeWidget::Value( 1.0/2000, QString::fromLatin1( "1/2000" ) ) << Exif::RangeWidget::Value( 1.0/1600, QString::fromLatin1( "1/1600" ) ) << Exif::RangeWidget::Value( 1.0/1250, QString::fromLatin1( "1/1250" ) ) << Exif::RangeWidget::Value( 1.0/1000, QString::fromLatin1( "1/1000" ) ) << Exif::RangeWidget::Value( 1.0/800, QString::fromLatin1( "1/800" ) ) << Exif::RangeWidget::Value( 1.0/640, QString::fromLatin1( "1/640" ) ) << Exif::RangeWidget::Value( 1.0/500, QString::fromLatin1( "1/500" ) ) << Exif::RangeWidget::Value( 1.0/400, QString::fromLatin1( "1/400" ) ) << Exif::RangeWidget::Value( 1.0/320, QString::fromLatin1( "1/320" ) ) << Exif::RangeWidget::Value( 1.0/250, QString::fromLatin1( "1/250" ) ) << Exif::RangeWidget::Value( 1.0/200, QString::fromLatin1( "1/200" ) ) << Exif::RangeWidget::Value( 1.0/160, QString::fromLatin1( "1/160" ) ) << Exif::RangeWidget::Value( 1.0/125, QString::fromLatin1( "1/125" ) ) << Exif::RangeWidget::Value( 1.0/100, QString::fromLatin1( "1/100" ) ) << Exif::RangeWidget::Value( 1.0/80, QString::fromLatin1( "1/80" ) ) << Exif::RangeWidget::Value( 1.0/60, QString::fromLatin1( "1/60" ) ) << Exif::RangeWidget::Value( 1.0/50, QString::fromLatin1( "1/50" ) ) << Exif::RangeWidget::Value( 1.0/40, QString::fromLatin1( "1/40" ) ) << Exif::RangeWidget::Value( 1.0/30, QString::fromLatin1( "1/30" ) ) << Exif::RangeWidget::Value( 1.0/25, QString::fromLatin1( "1/25" ) ) << Exif::RangeWidget::Value( 1.0/20, QString::fromLatin1( "1/20" ) ) << Exif::RangeWidget::Value( 1.0/15, QString::fromLatin1( "1/15" ) ) << Exif::RangeWidget::Value( 1.0/13, QString::fromLatin1( "1/13" ) ) << Exif::RangeWidget::Value( 1.0/10, QString::fromLatin1( "1/10" ) ) << Exif::RangeWidget::Value( 1.0/8, QString::fromLatin1( "1/8" ) ) << Exif::RangeWidget::Value( 1.0/6, QString::fromLatin1( "1/6" ) ) << Exif::RangeWidget::Value( 1.0/5, QString::fromLatin1( "1/5" ) ) << Exif::RangeWidget::Value( 1.0/4, QString::fromLatin1( "1/4" ) ) << Exif::RangeWidget::Value( 0.3, QString::fromLatin1( "0.3 %1" ).arg( secs ) ) << Exif::RangeWidget::Value( 0.4, QString::fromLatin1( "0.4 %1").arg(secs ) ) << Exif::RangeWidget::Value( 0.5, QString::fromLatin1( "0.5 %1").arg(secs ) ) << Exif::RangeWidget::Value( 0.6, QString::fromLatin1( "0.6 %1").arg(secs ) ) << Exif::RangeWidget::Value( 0.8, QString::fromLatin1( "0.8 %1").arg(secs ) ) << Exif::RangeWidget::Value( 1, i18n( "1 second" ) ) << Exif::RangeWidget::Value( 1.3, QString::fromLatin1( "1.3 %1").arg(secs ) ) << Exif::RangeWidget::Value( 1.6, QString::fromLatin1( "1.6 %1").arg(secs ) ) << Exif::RangeWidget::Value( 2, QString::fromLatin1( "2 %1").arg(secs ) ) << Exif::RangeWidget::Value( 2.5, QString::fromLatin1( "2.5 %1").arg(secs ) ) << Exif::RangeWidget::Value( 3.2, QString::fromLatin1( "3.2 %1").arg(secs ) ) << Exif::RangeWidget::Value( 4, QString::fromLatin1( "4 %1").arg(secs ) ) << Exif::RangeWidget::Value( 5, QString::fromLatin1( "5 %1").arg(secs ) ) << Exif::RangeWidget::Value( 6, QString::fromLatin1( "6 %1").arg(secs ) ) << Exif::RangeWidget::Value( 8, QString::fromLatin1( "8 %1").arg(secs ) ) << Exif::RangeWidget::Value( 10, QString::fromLatin1( "10 %1").arg(secs ) ) << Exif::RangeWidget::Value( 13, QString::fromLatin1( "13 %1").arg(secs ) ) << Exif::RangeWidget::Value( 15, QString::fromLatin1( "15 %1").arg(secs ) ) << Exif::RangeWidget::Value( 20, QString::fromLatin1( "20 %1").arg(secs ) ) << Exif::RangeWidget::Value( 25, QString::fromLatin1( "25 %1").arg(secs ) ) << Exif::RangeWidget::Value( 30, QString::fromLatin1( "30 %1").arg(secs ) ); m_exposureTime = new RangeWidget( i18n("Exposure time" ), QString::fromLatin1( "Exif_Photo_ExposureTime" ), list, layout, 1 ); } RangeWidget* Exif::SearchDialog::makeApertureOrFNumber( const QString& text, const QString& key, QGridLayout* layout, int row ) { Exif::RangeWidget::ValueList list; list << Exif::RangeWidget::Value( 1.4, QString::fromLatin1( "1.4" ) ) << Exif::RangeWidget::Value( 1.8, QString::fromLatin1( "1.8" ) ) << Exif::RangeWidget::Value( 2.0, QString::fromLatin1( "2.0" ) ) << Exif::RangeWidget::Value( 2.2, QString::fromLatin1( "2.2" ) ) << Exif::RangeWidget::Value( 2.5, QString::fromLatin1( "2.5" ) ) << Exif::RangeWidget::Value( 2.8, QString::fromLatin1( "2.8" ) ) << Exif::RangeWidget::Value( 3.2, QString::fromLatin1( "3.2" ) ) << Exif::RangeWidget::Value( 3.5, QString::fromLatin1( "3.5" ) ) << Exif::RangeWidget::Value( 4.0, QString::fromLatin1( "4.0" ) ) << Exif::RangeWidget::Value( 4.5, QString::fromLatin1( "4.5" ) ) << Exif::RangeWidget::Value( 5.0, QString::fromLatin1( "5.0" ) ) << Exif::RangeWidget::Value( 5.6, QString::fromLatin1( "5.6" ) ) << Exif::RangeWidget::Value( 6.3, QString::fromLatin1( "6.3" ) ) << Exif::RangeWidget::Value( 7.1, QString::fromLatin1( "7.1" ) ) << Exif::RangeWidget::Value( 8.0, QString::fromLatin1( "8.0" ) ) << Exif::RangeWidget::Value( 9.0, QString::fromLatin1( "9.0" ) ) << Exif::RangeWidget::Value( 10, QString::fromLatin1( "10" ) ) << Exif::RangeWidget::Value( 11, QString::fromLatin1( "11" ) ) << Exif::RangeWidget::Value( 13, QString::fromLatin1( "13" ) ) << Exif::RangeWidget::Value( 14, QString::fromLatin1( "14" ) ) << Exif::RangeWidget::Value( 16, QString::fromLatin1( "16" ) ) << Exif::RangeWidget::Value( 18, QString::fromLatin1( "18" ) ) << Exif::RangeWidget::Value( 20, QString::fromLatin1( "20" ) ) << Exif::RangeWidget::Value( 22, QString::fromLatin1( "22" ) ) << Exif::RangeWidget::Value( 25, QString::fromLatin1( "25" ) ) << Exif::RangeWidget::Value( 29, QString::fromLatin1( "29" ) ) << Exif::RangeWidget::Value( 32, QString::fromLatin1( "32" ) ) << Exif::RangeWidget::Value( 36, QString::fromLatin1( "36" ) ) << Exif::RangeWidget::Value( 40, QString::fromLatin1( "40" ) ) << Exif::RangeWidget::Value( 45, QString::fromLatin1( "45" ) ); return new RangeWidget( text, key, list, layout, row ); } #define addSetting(settings,text,num) \ { \ QCheckBox* cb = new QCheckBox( i18n( text ), box ); \ settings.append( Setting( cb, num ) ); \ layout->addWidget(cb); \ } QWidget* Exif::SearchDialog::makeExposureProgram( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Exposure Program" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_exposureProgram, "Not defined", 0 ); addSetting( m_exposureProgram, "Manual", 1 ); addSetting( m_exposureProgram, "Normal program", 2 ); addSetting( m_exposureProgram, "Aperture priority", 3 ); addSetting( m_exposureProgram, "Shutter priority", 4 ); addSetting( m_exposureProgram, "Creative program (biased toward depth of field)", 5 ); addSetting( m_exposureProgram, "Action program (biased toward fast shutter speed)", 6 ); addSetting( m_exposureProgram, "Portrait mode (for closeup photos with the background out of focus)", 7 ); addSetting( m_exposureProgram, "Landscape mode (for landscape photos with the background in focus)", 8 ); return box; } QWidget* Exif::SearchDialog::makeOrientation( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Orientation" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_orientation, "Not rotated", 0); addSetting( m_orientation, "Rotated counterclockwise", 6 ); addSetting( m_orientation, "Rotated clockwise", 8 ); addSetting( m_orientation, "Rotated 180 degrees", 3 ); return box; } QWidget* Exif::SearchDialog::makeMeteringMode( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Metering Mode" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_meteringMode, "Unknown", 0 ); addSetting( m_meteringMode, "Average", 1 ); addSetting( m_meteringMode, "CenterWeightedAverage", 2 ); addSetting( m_meteringMode, "Spot", 3 ); addSetting( m_meteringMode, "MultiSpot", 4 ); addSetting( m_meteringMode, "Pattern", 5 ); addSetting( m_meteringMode, "Partial", 6 ); addSetting( m_meteringMode, "Other", 255 ); return box; } QWidget* Exif::SearchDialog::makeContrast( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Contrast" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_contrast, "Normal", 0 ); addSetting( m_contrast, "Soft", 1 ); addSetting( m_contrast, "Hard", 2 ); return box; } QWidget* Exif::SearchDialog::makeSharpness( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Sharpness" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_sharpness, "Normal", 0 ); addSetting( m_sharpness, "Soft", 1 ); addSetting( m_sharpness, "Hard", 2 ); return box; } QWidget* Exif::SearchDialog::makeSaturation( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Saturation" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_saturation, "Normal", 0 ); addSetting( m_saturation, "Low", 1 ); addSetting( m_saturation, "High", 2 ); return box; } Exif::SearchInfo Exif::SearchDialog::info() { Exif::SearchInfo result; result.addSearchKey( QString::fromLatin1( "Exif_Photo_MeteringMode" ), m_meteringMode.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Photo_ExposureProgram" ), m_exposureProgram.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Image_Orientation" ), m_orientation.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Photo_MeteringMode" ), m_meteringMode.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Photo_Contrast" ), m_contrast.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Photo_Sharpness" ), m_sharpness.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Photo_Saturation" ), m_saturation.selected() ); result.addCamera( m_cameras.selected() ); result.addLens( m_lenses.selected() ); result.addRangeKey( m_iso->range() ); result.addRangeKey( m_exposureTime->range() ); result.addRangeKey( m_apertureValue->range() ); result.addRangeKey( m_fNumber->range() ); SearchInfo::Range focalRange( QString::fromLatin1( "Exif_Photo_FocalLength" ) ); focalRange.min = m_fromFocalLength->value(); focalRange.max = m_toFocalLength->value(); result.addRangeKey( focalRange ); return result; } QWidget* Exif::SearchDialog::makeCamera() { QScrollArea* view = new QScrollArea; view->setWidgetResizable(true); QWidget* w = new QWidget; view->setWidget( w ); QVBoxLayout* layout = new QVBoxLayout( w ); QList< QPair > cameras = Exif::Database::instance()->cameras(); std::sort(cameras.begin(),cameras.end()); for( QList< QPair >::ConstIterator cameraIt = cameras.constBegin(); cameraIt != cameras.constEnd(); ++cameraIt ) { QCheckBox* cb = new QCheckBox( QString::fromUtf8( "%1 - %2" ).arg( (*cameraIt).first.trimmed() ).arg( (*cameraIt).second.trimmed() ) ); layout->addWidget( cb ); m_cameras.append( Setting< QPair >( cb, *cameraIt ) ); } if ( cameras.isEmpty() ) { QLabel* label = new QLabel( i18n("No cameras found in the database") ); layout->addWidget( label ); } return view; } QWidget* Exif::SearchDialog::makeLens() { QScrollArea* view = new QScrollArea; view->setWidgetResizable(true); QWidget* w = new QWidget; view->setWidget( w ); QVBoxLayout* layout = new QVBoxLayout( w ); QList< QString > lenses = Exif::Database::instance()->lenses(); std::sort(lenses.begin(),lenses.end()); if ( lenses.isEmpty() ) { QLabel* label = new QLabel( i18n("No lenses found in the database") ); layout->addWidget( label ); } else { // add option "None" first lenses.prepend( i18nc("As in No persons, no locations etc.", "None") ); for( QList< QString >::ConstIterator lensIt = lenses.constBegin(); lensIt != lenses.constEnd(); ++lensIt ) { QCheckBox* cb = new QCheckBox( QString::fromUtf8( "%1" ).arg( (*lensIt).trimmed() ) ); layout->addWidget( cb ); m_lenses.append( Setting< QString >( cb, *lensIt ) ); } } if (Exif::Database::instance()->DBFileVersionGuaranteed() < 3) { QLabel* label = new QLabel( i18n("Not all images in the database have lens information. " - "Recreate the EXIF search database to ensure lens data for all images.") ); + "Recreate the Exif search database to ensure lens data for all images.") ); layout->addWidget(label); } return view; } void Exif::SearchDialog::fromFocalLengthChanged( int val ) { if ( m_toFocalLength->value() < val ) m_toFocalLength->setValue( val ); } void Exif::SearchDialog::toFocalLengthChanged( int val ) { if ( m_fromFocalLength->value() > val ) m_fromFocalLength->setValue( val ); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/HTMLGenerator/HTMLDialog.cpp b/HTMLGenerator/HTMLDialog.cpp index 2253228d..f4ee465c 100644 --- a/HTMLGenerator/HTMLDialog.cpp +++ b/HTMLGenerator/HTMLDialog.cpp @@ -1,651 +1,651 @@ /* 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 "HTMLDialog.h" #include "Logging.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 "Generator.h" #include "ImageSizeCheckBox.h" using namespace HTMLGenerator; HTMLDialog::HTMLDialog( QWidget* parent ) : KPageDialog(parent) , m_list() { - setWindowTitle( i18n("HTML Export") ); + setWindowTitle( i18nc("@title:window", "HTML Export") ); QWidget *mainWidget = new QWidget(this); this->layout()->addWidget(mainWidget); createContentPage(); createLayoutPage(); createDestinationPage(); // destUrl is only relevant for .kim file creation: connect(m_generateKimFile,&QCheckBox::toggled,m_destURL,&QLineEdit::setEnabled); // automatically fill in output directory: connect(m_title,&QLineEdit::editingFinished,this,&HTMLDialog::slotSuggestOutputDir); QDialogButtonBox *buttonBox = this->buttonBox(); connect(buttonBox, &QDialogButtonBox::accepted, this, &HTMLDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &HTMLDialog::reject); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); okButton->setEnabled( m_themes.size()>0 ); connect(okButton, &QPushButton::clicked, this, &HTMLDialog::slotOk); this->layout()->addWidget(buttonBox); } void HTMLDialog::createContentPage() { QWidget* contentPage = new QWidget; KPageWidgetItem* page = new KPageWidgetItem( contentPage, i18n("Content" ) ); page->setHeader( i18n("Content" ) ); page->setIcon( QIcon::fromTheme( QString::fromLatin1( "document-properties" ) ) ); addPage( page ); QVBoxLayout* lay1 = new QVBoxLayout( contentPage ); QGridLayout* lay2 = new QGridLayout; lay1->addLayout( lay2 ); QLabel* label = new QLabel( i18n("Page title:"), contentPage ); lay2->addWidget( label, 0, 0 ); m_title = new QLineEdit( contentPage ); label->setBuddy( m_title ); lay2->addWidget( m_title, 0, 1 ); // Copyright label = new QLabel( i18n("Copyright:"), contentPage ); label->setAlignment( Qt::AlignTop ); lay2->addWidget( label, 1, 0 ); m_copyright = new QLineEdit( contentPage ); m_copyright->setText( Settings::SettingsData::instance()->HTMLCopyright() ); label->setBuddy( m_copyright ); lay2->addWidget( m_copyright, 1, 1 ); // Description label = new QLabel( i18n("Description:"), contentPage ); label->setAlignment( Qt::AlignTop ); lay2->addWidget( label, 2, 0 ); m_description = new KTextEdit( contentPage ); label->setBuddy( m_description ); lay2->addWidget( m_description, 2, 1 ); m_generateKimFile = new QCheckBox( i18n("Create .kim export file"), contentPage ); m_generateKimFile->setChecked( Settings::SettingsData::instance()->HTMLKimFile() ); lay1->addWidget( m_generateKimFile ); m_inlineMovies = new QCheckBox( i18nc("Inline as a verb, i.e. 'please show movies right on the page, not as links'", "Inline Movies in pages" ), contentPage ); m_inlineMovies->setChecked( Settings::SettingsData::instance()->HTMLInlineMovies() ); lay1->addWidget( m_inlineMovies ); m_html5Video = new QCheckBox( i18nc("Tag as in HTML-tag, not as in image tag", "Use HTML5 video tag" ), contentPage ); m_html5Video->setChecked( Settings::SettingsData::instance()->HTML5Video() ); lay1->addWidget( m_html5Video ); QString avconv = QStandardPaths::findExecutable(QString::fromUtf8("avconv")); const QString ffmpeg2theora = QStandardPaths::findExecutable(QString::fromUtf8("ffmpeg2theora")); QStandardPaths::findExecutable(QString::fromUtf8("avconv")); if ( avconv.isNull() ) avconv = QStandardPaths::findExecutable(QString::fromUtf8("ffmpeg")); QString txt = i18n( "

This selection will generate video files suitable for displaying on web. " "avconv and ffmpeg2theora are required for video file generation.

" ); m_html5VideoGenerate = new QCheckBox( i18n( "Generate HTML5 video files (mp4 and ogg)" ), contentPage ); m_html5VideoGenerate->setChecked( Settings::SettingsData::instance()->HTML5VideoGenerate() ); lay1->addWidget( m_html5VideoGenerate ); m_html5VideoGenerate->setWhatsThis( txt ); if ( avconv.isNull() || ffmpeg2theora.isNull() ) m_html5VideoGenerate->setEnabled( false ); // What to include QGroupBox* whatToInclude = new QGroupBox( i18n( "What to Include" ), contentPage ); lay1->addWidget( whatToInclude ); QGridLayout* lay3 = new QGridLayout( whatToInclude ); QCheckBox* cb = new QCheckBox( i18n("Description"), whatToInclude ); m_whatToIncludeMap.insert( QString::fromLatin1("**DESCRIPTION**"), cb ); lay3->addWidget( cb, 0, 0 ); m_date = new QCheckBox( i18n("Date"), whatToInclude ); m_date->setChecked( Settings::SettingsData::instance()->HTMLDate() ); m_whatToIncludeMap.insert( QString::fromLatin1("**DATE**"), m_date ); lay3->addWidget( m_date, 0, 1 ); int row=1; int col=0; QString selectionsTmp = Settings::SettingsData::instance()->HTMLIncludeSelections(); QStringMatcher* pattern = new QStringMatcher(); pattern->setPattern(QString::fromLatin1("**DESCRIPTION**")); cb->setChecked( pattern->indexIn (selectionsTmp) >= 0 ? 1 : 0 ); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); for( QList::Iterator it = categories.begin(); it != categories.end(); ++it ) { if ( ! (*it)->isSpecialCategory() ) { QCheckBox* cb = new QCheckBox((*it)->name(), whatToInclude); lay3->addWidget( cb, row, col%2 ); m_whatToIncludeMap.insert( (*it)->name(), cb ); pattern->setPattern((*it)->name()); cb->setChecked( pattern->indexIn (selectionsTmp) >= 0 ? 1 : 0 ); if ( ++col % 2 == 0 ) ++row; } } } void HTMLDialog::createLayoutPage() { QWidget* layoutPage = new QWidget; KPageWidgetItem* page = new KPageWidgetItem( layoutPage, i18n("Layout" ) ); page->setHeader( i18n("Layout" ) ); page->setIcon( QIcon::fromTheme( QString::fromLatin1( "configure" )) ); addPage(page); QVBoxLayout* lay1 = new QVBoxLayout( layoutPage ); QGridLayout* lay2 = new QGridLayout; lay1->addLayout( lay2 ); // Thumbnail size QLabel* label = new QLabel( i18n("Thumbnail size:"), layoutPage ); lay2->addWidget( label, 0, 0 ); QHBoxLayout* lay3 = new QHBoxLayout; lay2->addLayout( lay3, 0, 1 ); m_thumbSize = new QSpinBox; m_thumbSize->setRange( 16, 256 ); m_thumbSize->setValue( Settings::SettingsData::instance()->HTMLThumbSize() ); lay3->addWidget( m_thumbSize ); lay3->addStretch(1); label->setBuddy( m_thumbSize ); // Number of columns label = new QLabel( i18n("Number of columns:"), layoutPage ); lay2->addWidget( label, 1, 0 ); QHBoxLayout* lay4 = new QHBoxLayout; lay2->addLayout( lay4, 1, 1 ); m_numOfCols = new QSpinBox; m_numOfCols->setRange( 1, 10 ); label->setBuddy( m_numOfCols); m_numOfCols->setValue( Settings::SettingsData::instance()->HTMLNumOfCols() ); lay4->addWidget( m_numOfCols ); lay4->addStretch( 1 ); // Theme box label = new QLabel( i18n("Theme:"), layoutPage ); lay2->addWidget( label, 2, 0 ); lay4 = new QHBoxLayout; lay2->addLayout( lay4, 2, 1 ); m_themeBox = new KComboBox( layoutPage ); label->setBuddy( m_themeBox ); lay4->addWidget( m_themeBox ); lay4->addStretch( 1 ); m_themeInfo = new QLabel( i18n("Theme Description"), layoutPage ); m_themeInfo->setWordWrap(true); lay2->addWidget( m_themeInfo, 3, 1 ); connect(m_themeBox, static_cast(&KComboBox::currentIndexChanged), this, &HTMLDialog::displayThemeDescription); populateThemesCombo(); // Image sizes QGroupBox* sizes = new QGroupBox( i18n("Image Sizes"), layoutPage ); lay1->addWidget( sizes ); QGridLayout* lay5 = new QGridLayout( sizes ); ImageSizeCheckBox* size320 = new ImageSizeCheckBox( 320, 200, sizes ); ImageSizeCheckBox* size640 = new ImageSizeCheckBox( 640, 480, sizes ); ImageSizeCheckBox* size800 = new ImageSizeCheckBox( 800, 600, sizes ); ImageSizeCheckBox* size1024 = new ImageSizeCheckBox( 1024, 768, sizes ); ImageSizeCheckBox* size1280 = new ImageSizeCheckBox( 1280, 1024, sizes ); ImageSizeCheckBox* size1600 = new ImageSizeCheckBox( 1600, 1200, sizes ); ImageSizeCheckBox* sizeOrig = new ImageSizeCheckBox( i18n("Full size"), sizes ); { int row = 0; int col = -1; lay5->addWidget( size320, row, ++col ); lay5->addWidget( size640, row, ++col ); lay5->addWidget( size800, row, ++col ); lay5->addWidget( size1024, row, ++col ); col =-1; lay5->addWidget( size1280, ++row, ++col ); lay5->addWidget( size1600, row, ++col ); lay5->addWidget( sizeOrig, row, ++col ); } QString tmp; if ((tmp = Settings::SettingsData::instance()->HTMLSizes()) != QString::fromLatin1("")) { QStringMatcher* pattern = new QStringMatcher(QString::fromLatin1("320")); size320->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); pattern->setPattern(QString::fromLatin1("640")); size640->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); pattern->setPattern(QString::fromLatin1("800")); size800->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0 ); pattern->setPattern(QString::fromLatin1("1024")); size1024->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); pattern->setPattern(QString::fromLatin1("1280")); size1280->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); pattern->setPattern(QString::fromLatin1("1600")); size1600->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); pattern->setPattern(QString::fromLatin1("-1")); sizeOrig->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); } else size800->setChecked( 1 ); m_sizeCheckBoxes << size800 << size1024 << size1280 << size640 << size1600 << size320 << sizeOrig; lay1->addStretch(1); QGridLayout* lay6 = new QGridLayout; lay1->addLayout( lay6 ); } void HTMLDialog::createDestinationPage() { QWidget* destinationPage = new QWidget; KPageWidgetItem* page = new KPageWidgetItem( destinationPage, i18n("Destination" ) ); page->setHeader( i18n("Destination" ) ); page->setIcon( QIcon::fromTheme( QString::fromLatin1( "drive-harddisk" ) ) ); addPage( page ); QVBoxLayout* lay1 = new QVBoxLayout( destinationPage ); QGridLayout* lay2 = new QGridLayout; lay1->addLayout( lay2 ); int row = -1; // Base Directory QLabel* label = new QLabel( i18n("Base directory:"), destinationPage ); lay2->addWidget( label, ++row, 0 ); QHBoxLayout* lay3 = new QHBoxLayout; lay2->addLayout( lay3, row, 1 ); m_baseDir = new QLineEdit( destinationPage ); lay3->addWidget( m_baseDir ); label->setBuddy( m_baseDir ); QPushButton* but = new QPushButton( QString::fromLatin1( ".." ), destinationPage ); lay3->addWidget( but ); but->setFixedWidth( 25 ); connect(but, &QPushButton::clicked, this, &HTMLDialog::selectDir); m_baseDir->setText( Settings::SettingsData::instance()->HTMLBaseDir() ); // Output Directory label = new QLabel( i18n("Gallery directory:"), destinationPage ); lay2->addWidget( label, ++row, 0 ); m_outputDir = new QLineEdit( destinationPage ); lay2->addWidget( m_outputDir, row, 1 ); label->setBuddy( m_outputDir ); // fully "Assembled" output Directory label = new QLabel( i18n("Output directory:"), destinationPage ); lay2->addWidget( label, ++row, 0 ); m_outputLabel = new QLabel( destinationPage ); lay2->addWidget( m_outputLabel, row, 1 ); label->setBuddy( m_outputLabel ); connect(m_baseDir, &QLineEdit::textChanged, this, &HTMLDialog::slotUpdateOutputLabel); connect(m_outputDir, &QLineEdit::textChanged, this, &HTMLDialog::slotUpdateOutputLabel); // initial text slotUpdateOutputLabel(); // Destination URL label = new QLabel( i18n("URL for final destination of .kim file:" ), destinationPage ); label->setToolTip( i18n( "

If you move the gallery to a remote location, set this to the destination URL.

" "

This only affects the generated .kim file.

" ) ); lay2->addWidget( label, ++row, 0 ); m_destURL = new QLineEdit( destinationPage ); m_destURL->setText( Settings::SettingsData::instance()->HTMLDestURL() ); lay2->addWidget( m_destURL, row, 1 ); label->setBuddy( m_destURL ); // Base URL label = new QLabel( i18n("Open gallery in browser:"), destinationPage ); lay2->addWidget( label, ++row, 0 ); m_openInBrowser = new QCheckBox(destinationPage); m_openInBrowser->setChecked(true); lay2->addWidget( m_openInBrowser, row, 1); label->setBuddy( m_openInBrowser ); lay1->addStretch( 1 ); } void HTMLDialog::slotOk() { if ( !checkVars() ) return; if( activeResolutions().count() < 1 ) { KMessageBox::sorry( nullptr, i18n( "You must select at least one resolution." ) ); return; } accept(); Settings::SettingsData::instance()->setHTMLBaseDir( m_baseDir->text() ); Settings::SettingsData::instance()->setHTMLDestURL( m_destURL->text() ); Settings::SettingsData::instance()->setHTMLCopyright( m_copyright->text() ); Settings::SettingsData::instance()->setHTMLDate( m_date->isChecked() ); Settings::SettingsData::instance()->setHTMLTheme( m_themeBox->currentIndex() ); Settings::SettingsData::instance()->setHTMLKimFile( m_generateKimFile->isChecked() ); Settings::SettingsData::instance()->setHTMLInlineMovies( m_inlineMovies->isChecked() ); Settings::SettingsData::instance()->setHTML5Video( m_html5Video->isChecked() ); Settings::SettingsData::instance()->setHTML5VideoGenerate( m_html5VideoGenerate->isChecked() ); Settings::SettingsData::instance()->setHTMLThumbSize( m_thumbSize->value() ); Settings::SettingsData::instance()->setHTMLNumOfCols( m_numOfCols->value() ); Settings::SettingsData::instance()->setHTMLSizes( activeSizes() ); Settings::SettingsData::instance()->setHTMLIncludeSelections( includeSelections() ); Generator generator( setup(), this ); generator.generate(); } void HTMLDialog::selectDir() { QString dir = QFileDialog::getExistingDirectory( this, i18n("Select base directory..."), m_baseDir->text() ); if ( !dir.isEmpty() ) m_baseDir->setText( dir ); } bool HTMLDialog::checkVars() { QString outputDir = m_baseDir->text() + QString::fromLatin1( "/" ) + m_outputDir->text(); // Ensure base dir is specified QString baseDir = m_baseDir->text(); if ( baseDir.isEmpty() ) { KMessageBox::error( this, i18n("

You did not specify a base directory. " "This is the topmost directory for your images. " "Under this directory you will find each generated collection " "in separate directories.

"), i18n("No Base Directory Specified") ); return false; } // ensure output directory is specified if ( m_outputDir->text().isEmpty() ) { KMessageBox::error( this, i18n("

You did not specify an output directory. " "This is a directory containing the actual images. " "The directory will be in the base directory specified above.

"), i18n("No Output Directory Specified") ); return false; } // ensure base dir exists QScopedPointer statJob( KIO::stat( QUrl::fromUserInput(baseDir), KIO::StatJob::DestinationSide, 1 /*only basic info*/)); KJobWidgets::setWindow( statJob.data(), MainWindow::Window::theMainWindow() ); if (!statJob->exec()) { KMessageBox::error( this, i18n("

Error while reading information about %1. " "This is most likely because the directory does not exist.

" "

The error message was: %2

", baseDir, statJob->errorString() ) ); return false; } KFileItem fileInfo( statJob->statResult(), QUrl::fromUserInput(baseDir) ); if ( !fileInfo.isDir() ) { KMessageBox::error( this, i18n("

%1 does not exist, is not a directory or " "cannot be written to.

", baseDir ) ); return false; } // test if destination directory exists. QScopedPointer existsJob( KIO::stat( QUrl::fromUserInput(outputDir), KIO::StatJob::DestinationSide, 0 /*only minimal info*/ )); KJobWidgets::setWindow( existsJob.data(), MainWindow::Window::theMainWindow() ); if ( existsJob->exec() ) { int answer = KMessageBox::warningYesNo( this, i18n("

Output directory %1 already exists. " "Usually, this means you should specify a new directory.

" "

Should %2 be deleted first?

", outputDir, outputDir ), i18n("Directory Exists"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QString::fromLatin1("html_export_delete_original_directory") ); if ( answer == KMessageBox::Yes ) { QScopedPointer delJob (KIO::del(QUrl::fromUserInput(outputDir))); KJobWidgets::setWindow( delJob.data(), MainWindow::Window::theMainWindow() ); delJob->exec(); } else return false; } return true; } QList HTMLDialog::activeResolutions() const { QList res; for( QList::ConstIterator sizeIt = m_sizeCheckBoxes.begin(); sizeIt != m_sizeCheckBoxes.end(); ++sizeIt ) { if ( (*sizeIt)->isChecked() ) res << *sizeIt; } return res; } QString HTMLDialog::activeSizes() const { QString res; for( QList::ConstIterator sizeIt = m_sizeCheckBoxes.begin(); sizeIt != m_sizeCheckBoxes.end(); ++sizeIt ) { if ( (*sizeIt)->isChecked() ) { if (res.length() > 0) res.append(QString::fromLatin1(",")); res.append(QString::number((*sizeIt)->width())); } } return res; } QString HTMLDialog::includeSelections() const { QString sel; Setup setupChoices = setup(); for( QMap::ConstIterator it = m_whatToIncludeMap.begin() ; it != m_whatToIncludeMap.end() ; ++it ) { QString name = it.key(); if ( setupChoices.includeCategory(name) ) { if (sel.length() > 0) sel.append(QString::fromLatin1(",")); sel.append(name); } } return sel; } void HTMLDialog::populateThemesCombo() { QStringList dirs = QStandardPaths::locateAll( QStandardPaths::DataLocation, QString::fromLocal8Bit("themes/"), QStandardPaths::LocateDirectory ); int i = 0; int theme = 0; int defaultthemes = 0; qCDebug(HTMLGeneratorLog) << "Theme directories:"<insertItem( i, i18n( "%1 (by %2)",themeName, themeAuthor ) ); // combined alternative m_themeBox->insertItem( i, i18n( "%1",themeName) ); m_themes.insert( i, themePath ); if (themeDefault == QString::fromLatin1("true")) { theme = i; defaultthemes++; } i++; } } if(m_themeBox->count() < 1) { KMessageBox::error( this, i18n("Could not find any themes - this is very likely an installation error" ) ); } if ( (Settings::SettingsData::instance()->HTMLTheme() >= 0) && (Settings::SettingsData::instance()->HTMLTheme() < m_themeBox->count()) ) m_themeBox->setCurrentIndex( Settings::SettingsData::instance()->HTMLTheme() ); else { m_themeBox->setCurrentIndex( theme ); if (defaultthemes > 1) KMessageBox::information( this, i18n("More than one theme is set as default, using theme %1", m_themeBox->currentText()) ); } } void HTMLDialog::displayThemeDescription(int themenr) { // SLOT: update m_themeInfo label whenever the m_theme QComboBox changes. QString outtxt = i18nc( "This is to show the author of the theme. E.g. copyright character (©) by itself will work fine on this context if no proper word is available in your language.", "by " ); outtxt.append( m_themeAuthors[themenr] ); outtxt.append( i18n( "\n " ) ); outtxt.append( m_themeDescriptions[themenr] ); m_themeInfo->setText( outtxt ); // Instead of two separate lists for authors and descriptions one could have a combined one by appending the text prior to storing within populateThemesCombo(), // however, storing author and descriptions separately might be cleaner. } void HTMLDialog::slotUpdateOutputLabel() { QString outputDir = QDir(m_baseDir->text()).filePath(m_outputDir->text()); // feedback on validity: if (outputDir == m_baseDir->text()) { m_outputLabel->setStyleSheet(QString::fromLatin1("QLabel { color : darkred; }")); outputDir.append(i18n("

Gallery directory cannot be empty.

")); } else if ( QDir(outputDir).exists()) { m_outputLabel->setStyleSheet(QString::fromLatin1("QLabel { color : darkorange; }")); outputDir.append(i18n("

The output directory already exists.

")); } else { m_outputLabel->setStyleSheet(QString::fromLatin1("QLabel { color : black; }")); } m_outputLabel->setText( outputDir ); } void HTMLDialog::slotSuggestOutputDir() { if (m_outputDir->text().isEmpty()) { // the title is often an adequate directory name: m_outputDir->setText( m_title->text() ); } } int HTMLDialog::exec(const DB::FileNameList& list) { if (list.empty()) { qCWarning(HTMLGeneratorLog) << "HTMLDialog called without images for export"; return false; } m_list = list; return QDialog::exec(); } Setup HTMLGenerator::HTMLDialog::setup() const { Setup setup; setup.setTitle( m_title->text() ); setup.setBaseDir( m_baseDir->text() ); if (m_openInBrowser->isEnabled()) { setup.setBaseURL( m_baseDir->text() ); } setup.setDestURL( m_destURL->text() ); setup.setOutputDir( m_outputDir->text() ); setup.setThumbSize( m_thumbSize->value() ); setup.setCopyright( m_copyright->text() ); setup.setDate( m_date->isChecked() ); setup.setDescription( m_description->toPlainText() ); setup.setNumOfCols( m_numOfCols->value() ); setup.setGenerateKimFile( m_generateKimFile->isChecked() ); setup.setThemePath( m_themes[m_themeBox->currentIndex()] ); for( QMap::ConstIterator includeIt = m_whatToIncludeMap.begin(); includeIt != m_whatToIncludeMap.end(); ++includeIt ) { setup.setIncludeCategory( includeIt.key(), includeIt.value()->isChecked() ); } setup.setImageList(m_list); setup.setResolutions( activeResolutions() ); setup.setInlineMovies( m_inlineMovies->isChecked() ); setup.setHtml5Video( m_html5Video->isChecked() ); setup.setHtml5VideoGenerate( m_html5VideoGenerate->isChecked() ); return setup; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/Export.cpp b/ImportExport/Export.cpp index 17c72f24..c56be693 100644 --- a/ImportExport/Export.cpp +++ b/ImportExport/Export.cpp @@ -1,409 +1,409 @@ /* 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 "Export.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "XMLHandler.h" using namespace ImportExport; void Export::imageExport(const DB::FileNameList& list) { ExportConfig config; if ( config.exec() == QDialog::Rejected ) return; int maxSize = -1; if ( config.mp_enforeMaxSize->isChecked() ) maxSize = config.mp_maxSize->value(); // Ask for zip file name QString zipFile = QFileDialog::getSaveFileName( nullptr, /* parent */ i18n("Save an export file"), /* caption */ QString(), /* directory */ i18n("KPhotoAlbum import files") + QString::fromLatin1( "(*.kim)" ) /*filter*/ ); if ( zipFile.isNull() ) return; bool ok; Export* exp = new Export( list, zipFile, config.mp_compress->isChecked(), maxSize, config.imageFileLocation(), QString::fromLatin1( "" ), config.mp_generateThumbnails->isChecked(), &ok); delete exp; // It will not return before done - we still need a class to connect slots etc. if ( ok ) showUsageDialog(); } // PENDING(blackie) add warning if images are to be copied into a non empty directory. ExportConfig::ExportConfig() { - setWindowTitle( i18n("Export Configuration / Copy Images") ); + setWindowTitle( i18nc("@title:window", "Export Configuration / Copy Images") ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::Help); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QWidget* top = new QWidget; mainLayout->addWidget(top); QVBoxLayout* lay1 = new QVBoxLayout( top ); // Include images QGroupBox* grp = new QGroupBox( i18n("How to Handle Images") ); lay1->addWidget( grp ); QVBoxLayout* boxLay = new QVBoxLayout( grp ); m_include = new QRadioButton( i18n("Include in .kim file"), grp ); m_manually = new QRadioButton( i18n("Do not copy files, only generate .kim file"), grp ); m_auto = new QRadioButton( i18n("Automatically copy next to .kim file"), grp ); m_link = new QRadioButton( i18n("Hard link next to .kim file"), grp ); m_symlink = new QRadioButton( i18n("Symbolic link next to .kim file"), grp ); m_auto->setChecked( true ); boxLay->addWidget( m_include ); boxLay->addWidget( m_manually ); boxLay->addWidget( m_auto ); boxLay->addWidget( m_link ); boxLay->addWidget( m_symlink ); // Compress mp_compress = new QCheckBox( i18n("Compress export file"), top ); lay1->addWidget( mp_compress ); // Generate thumbnails mp_generateThumbnails = new QCheckBox( i18n("Generate thumbnails"), top ); mp_generateThumbnails->setChecked( false ); lay1->addWidget( mp_generateThumbnails ); // Enforece max size QHBoxLayout* hlay = new QHBoxLayout; lay1->addLayout( hlay ); mp_enforeMaxSize = new QCheckBox( i18n( "Limit maximum image dimensions to: " ) ); hlay->addWidget( mp_enforeMaxSize ); mp_maxSize = new QSpinBox; mp_maxSize->setRange( 100,4000 ); hlay->addWidget( mp_maxSize ); mp_maxSize->setValue( 800 ); connect(mp_enforeMaxSize, &QCheckBox::toggled, mp_maxSize, &QSpinBox::setEnabled); mp_maxSize->setEnabled( false ); QString txt = i18n( "

If your images are stored in a non-compressed file format then you may check this; " "otherwise, this just wastes time during import and export operations.

" "

In other words, do not check this if your images are stored in jpg, png or gif; but do check this " "if your images are stored in tiff.

" ); mp_compress->setWhatsThis( txt ); txt = i18n( "

Generate thumbnail images

" ); mp_generateThumbnails->setWhatsThis( txt ); txt = i18n( "

With this option you may limit the maximum dimensions (width and height) of your images. " "Doing so will make the resulting export file smaller, but will of course also make the quality " "worse if someone wants to see the exported images with larger dimensions.

" ); mp_enforeMaxSize->setWhatsThis( txt ); mp_maxSize->setWhatsThis( txt ); txt = i18n("

When exporting images, bear in mind that there are two things the " "person importing these images again will need:
" "1) meta information (image content, date etc.)
" "2) the images themselves.

" "

The images themselves can either be placed next to the .kim file, " "or copied into the .kim file. Copying the images into the .kim file works well " "for a recipient who wants all, or most of those images, for example " "when emailing a whole group of images. However, when you place the " "images on the Web, a lot of people will see them but most likely only " "download a few of them. It works better in this kind of case, to " "separate the images and the .kim file, by place them next to each " "other, so the user can access the images s/he wants.

"); grp->setWhatsThis( txt ); m_include->setWhatsThis( txt ); m_manually->setWhatsThis( txt ); m_link->setWhatsThis( txt ); m_symlink->setWhatsThis( txt ); m_auto->setWhatsThis( txt ); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &ExportConfig::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &ExportConfig::reject); mainLayout->addWidget(buttonBox); QPushButton *helpButton = buttonBox->button(QDialogButtonBox::Help); connect(helpButton,&QPushButton::clicked, this,&ExportConfig::showHelp); } ImageFileLocation ExportConfig::imageFileLocation() const { if ( m_include->isChecked() ) return Inline; else if ( m_manually->isChecked() ) return ManualCopy; else if ( m_link->isChecked() ) return Link; else if ( m_symlink->isChecked() ) return Symlink; else return AutoCopy; } void ExportConfig::showHelp() { KHelpClient::invokeHelp( QString::fromLatin1( "chp-importExport" ) ); } Export::~Export() { delete m_eventLoop; } Export::Export( const DB::FileNameList& list, const QString& zipFile, bool compress, int maxSize, ImageFileLocation location, const QString& baseUrl, bool doGenerateThumbnails, bool *ok) : m_internalOk(true) , m_ok( ok ) , m_maxSize( maxSize ) , m_location( location ) , m_eventLoop( new QEventLoop ) { if (ok == nullptr) ok = &m_internalOk; *ok = true; m_destdir = QFileInfo( zipFile ).path(); m_zip = new KZip( zipFile ); m_zip->setCompression( compress ? KZip::DeflateCompression : KZip::NoCompression ); if ( ! m_zip->open( QIODevice::WriteOnly ) ) { KMessageBox::error( nullptr, i18n("Error creating zip file") ); *ok = false; return; } // Create progress dialog int total = 1; if (location != ManualCopy) total += list.size(); if (doGenerateThumbnails) total += list.size(); m_steps = 0; m_progressDialog = new QProgressDialog; m_progressDialog->setCancelButtonText(i18n("&Cancel")); m_progressDialog->setMaximum(total); m_progressDialog->setValue(0); m_progressDialog->show(); // Copy image files and generate thumbnails if ( location != ManualCopy ) { m_copyingFiles = true; copyImages( list ); } if ( *m_ok && doGenerateThumbnails ) { m_copyingFiles = false; generateThumbnails( list ); } if ( *m_ok ) { // Create the index.xml file m_progressDialog->setLabelText(i18n("Creating index file")); QByteArray indexml = XMLHandler().createIndexXML( list, baseUrl, m_location, &m_filenameMapper ); m_zip->writeFile( QString::fromLatin1( "index.xml" ), indexml.data() ); m_steps++; m_progressDialog->setValue( m_steps ); m_zip->close(); } } void Export::generateThumbnails(const DB::FileNameList& list) { m_progressDialog->setLabelText( i18n("Creating thumbnails") ); m_loopEntered = false; m_subdir = QString::fromLatin1( "Thumbnails/" ); m_filesRemaining = list.size(); // Used to break the event loop. for (const DB::FileName& fileName : list) { ImageManager::ImageRequest* request = new ImageManager::ImageRequest( fileName, QSize( 128, 128 ), fileName.info()->angle(), this ); request->setPriority( ImageManager::BatchTask ); ImageManager::AsyncLoader::instance()->load( request ); } if ( m_filesRemaining > 0 ) { m_loopEntered = true; m_eventLoop->exec(); } } void Export::copyImages(const DB::FileNameList& list) { Q_ASSERT( m_location != ManualCopy ); m_loopEntered = false; m_subdir = QString::fromLatin1( "Images/" ); m_progressDialog->setLabelText( i18n("Copying image files") ); m_filesRemaining = 0; for (const DB::FileName& fileName : list) { QString file = fileName.absolute(); QString zippedName = m_filenameMapper.uniqNameFor(fileName); if ( m_maxSize == -1 || Utilities::isVideo( fileName ) || Utilities::isRAW( fileName )) { if ( QFileInfo( file ).isSymLink() ) file = QFileInfo(file).readLink(); if ( m_location == Inline ) m_zip->addLocalFile( file, QString::fromLatin1( "Images/" ) + zippedName ); else if ( m_location == AutoCopy ) Utilities::copy( file, m_destdir + QString::fromLatin1( "/" ) + zippedName ); else if ( m_location == Link ) Utilities::makeHardLink( file, m_destdir + QString::fromLatin1( "/" ) + zippedName ); else if ( m_location == Symlink ) Utilities::makeSymbolicLink( file, m_destdir + QString::fromLatin1( "/" ) + zippedName ); m_steps++; m_progressDialog->setValue( m_steps ); } else { m_filesRemaining++; ImageManager::ImageRequest* request = new ImageManager::ImageRequest( DB::FileName::fromAbsolutePath(file), QSize( m_maxSize, m_maxSize ), 0, this ); request->setPriority( ImageManager::BatchTask ); ImageManager::AsyncLoader::instance()->load( request ); } // Test if the cancel button was pressed. qApp->processEvents( QEventLoop::AllEvents ); if ( m_progressDialog->wasCanceled() ) { *m_ok = false; return; } } if ( m_filesRemaining > 0 ) { m_loopEntered = true; m_eventLoop->exec(); } } void Export::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& image) { const DB::FileName fileName = request->databaseFileName(); if ( !request->loadedOK() ) return; const QString ext = (Utilities::isVideo( fileName ) || Utilities::isRAW( fileName )) ? QString::fromLatin1( "jpg" ) : QFileInfo( m_filenameMapper.uniqNameFor(fileName) ).completeSuffix(); // Add the file to the zip archive QString zipFileName = QString::fromLatin1( "%1/%2.%3" ).arg( Utilities::stripEndingForwardSlash(m_subdir)) .arg(QFileInfo( m_filenameMapper.uniqNameFor(fileName) ).baseName()).arg( ext ); QByteArray data; QBuffer buffer( &data ); buffer.open( QIODevice::WriteOnly ); image.save( &buffer, QFileInfo(zipFileName).suffix().toLower().toLatin1().constData() ); if ( m_location == Inline || !m_copyingFiles ) m_zip->writeFile( zipFileName, data.constData() ); else { QString file = m_destdir + QString::fromLatin1( "/" ) + m_filenameMapper.uniqNameFor(fileName); QFile out( file ); if ( !out.open( QIODevice::WriteOnly ) ) { KMessageBox::error( nullptr, i18n("Error writing file %1", file ) ); *m_ok = false; } out.write( data.constData(), data.size() ); out.close(); } qApp->processEvents( QEventLoop::AllEvents ); bool canceled = (!*m_ok || m_progressDialog->wasCanceled()); if ( canceled ) { *m_ok = false; m_eventLoop->exit(); ImageManager::AsyncLoader::instance()->stop( this ); return; } m_steps++; m_filesRemaining--; m_progressDialog->setValue( m_steps ); if ( m_filesRemaining == 0 && m_loopEntered ) m_eventLoop->exit(); } void Export::showUsageDialog() { QString txt = i18n( "

Other KPhotoAlbum users may now load the import file into their database, by choosing import in " "the file menu.

" "

If they find it on a web site, and the web server is correctly configured, all they need to do is simply " "to click it from within konqueror. To enable this, your web server needs to be configured for KPhotoAlbum. You do so by adding " "the following line to /etc/httpd/mime.types or similar:" "

application/vnd.kde.kphotoalbum-import kim
" "This will make your web server tell konqueror that it is a KPhotoAlbum file when clicking on the link, " "otherwise the web server will just tell konqueror that it is a plain text file.

" ); KMessageBox::information( nullptr, txt, i18n("How to Use the Export File"), QString::fromLatin1("export_how_to_use_the_export_file") ); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportHandler.cpp b/ImportExport/ImportHandler.cpp index dc90cc2f..575674dc 100644 --- a/ImportExport/ImportHandler.cpp +++ b/ImportExport/ImportHandler.cpp @@ -1,357 +1,357 @@ /* 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 "ImportHandler.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include "Utilities/Util.h" #include "KimFileReader.h" #include "ImportSettings.h" #include "MainWindow/Window.h" #include "DB/ImageDB.h" #include "Browser/BrowserWidget.h" #include "DB/MD5Map.h" #include "DB/Category.h" #include "DB/CategoryCollection.h" #include "Utilities/UniqFilenameMapper.h" #include "kio/job.h" #include using namespace ImportExport; ImportExport::ImportHandler::ImportHandler() : m_fileMapper(nullptr), m_finishedPressed(false), m_progress(0), m_reportUnreadableFiles( true ) , m_eventLoop( new QEventLoop ) { } ImportHandler::~ImportHandler() { delete m_fileMapper; delete m_eventLoop; } bool ImportExport::ImportHandler::exec( const ImportSettings& settings, KimFileReader* kimFileReader ) { m_settings = settings; m_kimFileReader = kimFileReader; m_finishedPressed = true; delete m_fileMapper; m_fileMapper = new Utilities::UniqFilenameMapper(m_settings.destination()); bool ok; // copy images if ( m_settings.externalSource() ) { copyFromExternal(); // If none of the images were to be copied, then we flushed the loop before we got started, in that case, don't start the loop. qCDebug(ImportExportLog) << "Copying" << m_pendingCopies.count() << "files from external source..."; if ( m_pendingCopies.count() > 0 ) ok = m_eventLoop->exec(); else ok = false; } else { ok = copyFilesFromZipFile(); if ( ok ) updateDB(); } if ( m_progress ) delete m_progress; return ok; } void ImportExport::ImportHandler::copyFromExternal() { m_pendingCopies = m_settings.selectedImages(); m_totalCopied = 0; m_progress = new QProgressDialog( MainWindow::Window::theMainWindow()); - m_progress->setWindowTitle(i18n("Copying Images") ); + m_progress->setWindowTitle(i18nc("@title:window", "Copying Images") ); m_progress->setMinimum( 0 ); m_progress->setMaximum( 2 * m_pendingCopies.count() ); m_progress->show(); connect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages); copyNextFromExternal(); } void ImportExport::ImportHandler::copyNextFromExternal() { DB::ImageInfoPtr info = m_pendingCopies[0]; if ( isImageAlreadyInDB( info ) ) { qCDebug(ImportExportLog) << info->fileName().relative() << "is already in database."; aCopyJobCompleted(0); return; } const DB::FileName fileName = info->fileName(); bool succeeded = false; QStringList tried; // First search for images next to the .kim file // Second search for images base on the image root as specified in the .kim file QList searchUrls { m_settings.kimFile().adjusted(QUrl::RemoveFilename) , m_settings.baseURL().adjusted(QUrl::RemoveFilename) }; Q_FOREACH(const QUrl& url, searchUrls) { QUrl src (url); src.setPath(src.path() + fileName.relative() ); std::unique_ptr statJob { KIO::stat(src, KIO::StatJob::SourceSide, 0 /* just query for existence */ ) }; KJobWidgets::setWindow(statJob.get(), MainWindow::Window::theMainWindow()); if ( statJob->exec() ) { QUrl dest = QUrl::fromLocalFile( m_fileMapper->uniqNameFor(fileName) ); m_job = KIO::file_copy( src, dest, -1, KIO::HideProgressInfo ); connect(m_job, &KIO::FileCopyJob::result, this, &ImportHandler::aCopyJobCompleted); succeeded = true; qCDebug(ImportExportLog) << "Copying" << src << "to" << dest; break; } else tried << src.toDisplayString(); } if (!succeeded) aCopyFailed( tried ); } bool ImportExport::ImportHandler::copyFilesFromZipFile() { DB::ImageInfoList images = m_settings.selectedImages(); m_totalCopied = 0; m_progress = new QProgressDialog( MainWindow::Window::theMainWindow()); - m_progress->setWindowTitle(i18n("Copying Images") ); + m_progress->setWindowTitle(i18nc("@title:window", "Copying Images") ); m_progress->setMinimum( 0 ); m_progress->setMaximum( 2 * m_pendingCopies.count() ); m_progress->show(); for( DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it ) { if ( !isImageAlreadyInDB( *it ) ) { const DB::FileName fileName = (*it)->fileName(); QByteArray data = m_kimFileReader->loadImage( fileName.relative() ); if ( data.isNull() ) return false; QString newName = m_fileMapper->uniqNameFor(fileName); QFile out( newName ); if ( !out.open( QIODevice::WriteOnly ) ) { KMessageBox::error( MainWindow::Window::theMainWindow(), i18n("Error when writing image %1", newName ) ); return false; } out.write( data.constData(), data.size() ); out.close(); } qApp->processEvents(); m_progress->setValue( ++m_totalCopied ); if ( m_progress->wasCanceled() ) { return false; } } return true; } void ImportExport::ImportHandler::updateDB() { disconnect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages); m_progress->setLabelText( i18n("Updating Database") ); int len = Settings::SettingsData::instance()->imageDirectory().length(); // image directory is always a prefix of destination if ( len == m_settings.destination().length() ) len = 0; else qCDebug(ImportExportLog) << "Re-rooting of ImageInfos from " << Settings::SettingsData::instance()->imageDirectory() << " to " << m_settings.destination(); // Run though all images DB::ImageInfoList images = m_settings.selectedImages(); for( DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it ) { DB::ImageInfoPtr info = *it; if ( len != 0) { // exchange prefix: QString name = m_settings.destination() + info->fileName().absolute().mid(len); qCDebug(ImportExportLog) << info->fileName().absolute() << " -> " << name; info->setFileName( DB::FileName::fromAbsolutePath(name) ); } if ( isImageAlreadyInDB( info ) ) { qCDebug(ImportExportLog) << "Updating ImageInfo for " << info->fileName().absolute(); updateInfo( matchingInfoFromDB( info ), info ); } else { qCDebug(ImportExportLog) << "Adding ImageInfo for " << info->fileName().absolute(); addNewRecord( info ); } m_progress->setValue( ++m_totalCopied ); if ( m_progress->wasCanceled() ) break; } Browser::BrowserWidget::instance()->home(); } void ImportExport::ImportHandler::stopCopyingImages() { m_job->kill(); } void ImportExport::ImportHandler::aCopyFailed( QStringList files ) { int result = m_reportUnreadableFiles ? KMessageBox::warningYesNoCancelList( m_progress, i18n("Cannot copy from any of the following locations:"), files, QString(), KStandardGuiItem::cont(), KGuiItem( i18n("Continue without Asking") )) : KMessageBox::Yes; switch (result) { case KMessageBox::Cancel: // This might be late -- if we managed to copy some files, we will // just throw away any changes to the DB, but some new image files // might be in the image directory... m_eventLoop->exit(false); m_pendingCopies.pop_front(); break; case KMessageBox::No: m_reportUnreadableFiles = false; // fall through default: aCopyJobCompleted( 0 ); } } void ImportExport::ImportHandler::aCopyJobCompleted( KJob* job ) { qCDebug(ImportExportLog) << "CopyJob" << job << "completed."; m_pendingCopies.pop_front(); if ( job && job->error() ) { job->uiDelegate()->showErrorMessage(); m_eventLoop->exit(false); } else if ( m_pendingCopies.count() == 0 ) { updateDB(); m_eventLoop->exit(true); } else if ( m_progress->wasCanceled() ) { m_eventLoop->exit(false); } else { m_progress->setValue( ++m_totalCopied ); copyNextFromExternal(); } } bool ImportExport::ImportHandler::isImageAlreadyInDB( const DB::ImageInfoPtr& info ) { return DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum()); } DB::ImageInfoPtr ImportExport::ImportHandler::matchingInfoFromDB( const DB::ImageInfoPtr& info ) { const DB::FileName name = DB::ImageDB::instance()->md5Map()->lookup(info->MD5Sum()); return DB::ImageDB::instance()->info(name); } /** * Merge the ImageInfo data from the kim file into the existing ImageInfo. */ void ImportExport::ImportHandler::updateInfo( DB::ImageInfoPtr dbInfo, DB::ImageInfoPtr newInfo ) { if ( dbInfo->label() != newInfo->label() && m_settings.importAction(QString::fromLatin1("*Label*")) == ImportSettings::Replace ) dbInfo->setLabel( newInfo->label() ); if ( dbInfo->description().simplified() != newInfo->description().simplified() ) { if ( m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Replace ) dbInfo->setDescription( newInfo->description() ); else if ( m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Merge ) dbInfo->setDescription( dbInfo->description() + QString::fromLatin1("

") + newInfo->description() ); } if (dbInfo->angle() != newInfo->angle() && m_settings.importAction(QString::fromLatin1("*Orientation*")) == ImportSettings::Replace ) dbInfo->setAngle( newInfo->angle() ); if (dbInfo->date() != newInfo->date() && m_settings.importAction(QString::fromLatin1("*Date*")) == ImportSettings::Replace ) dbInfo->setDate( newInfo->date() ); updateCategories( newInfo, dbInfo, false ); } void ImportExport::ImportHandler::addNewRecord( DB::ImageInfoPtr info ) { const DB::FileName importName = info->fileName(); DB::ImageInfoPtr updateInfo(new DB::ImageInfo(importName, info->mediaType(), false /*don't read exif */)); updateInfo->setLabel( info->label() ); updateInfo->setDescription( info->description() ); updateInfo->setDate( info->date() ); updateInfo->setAngle( info->angle() ); updateInfo->setMD5Sum( Utilities::MD5Sum( updateInfo->fileName() ) ); DB::ImageInfoList list; list.append(updateInfo); DB::ImageDB::instance()->addImages( list ); updateCategories( info, updateInfo, true ); } void ImportExport::ImportHandler::updateCategories( DB::ImageInfoPtr XMLInfo, DB::ImageInfoPtr DBInfo, bool forceReplace ) { // Run though the categories const QList matches = m_settings.categoryMatchSetting(); for ( const CategoryMatchSetting& match : matches ) { QString XMLCategoryName = match.XMLCategoryName(); QString DBCategoryName = match.DBCategoryName(); ImportSettings::ImportAction action = m_settings.importAction(DBCategoryName); const Utilities::StringSet items = XMLInfo->itemsOfCategory(XMLCategoryName); DB::CategoryPtr DBCategoryPtr = DB::ImageDB::instance()->categoryCollection()->categoryForName( DBCategoryName ); if ( !forceReplace && action == ImportSettings::Replace ) DBInfo->setCategoryInfo( DBCategoryName, Utilities::StringSet() ); if ( action == ImportSettings::Merge || action == ImportSettings::Replace || forceReplace ) { for ( const QString& item : items ) { if (match.XMLtoDB().contains( item ) ) { DBInfo->addCategoryInfo( DBCategoryName, match.XMLtoDB()[item] ); DBCategoryPtr->addItem( match.XMLtoDB()[item] ); } } } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/AutoStackImages.cpp b/MainWindow/AutoStackImages.cpp index b5692ccb..ae76a88e 100644 --- a/MainWindow/AutoStackImages.cpp +++ b/MainWindow/AutoStackImages.cpp @@ -1,326 +1,326 @@ /* Copyright (C) 2010-2018 Miika Turkia 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 "AutoStackImages.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MainWindow; AutoStackImages::AutoStackImages( QWidget* parent, const DB::FileNameList& list ) :QDialog( parent ), m_list( list ) { - setWindowTitle( i18n("Automatically Stack Images" ) ); + setWindowTitle( i18nc("@title:window", "Automatically Stack Images" ) ); QWidget* top = new QWidget; QVBoxLayout* lay1 = new QVBoxLayout( top ); setLayout(lay1); QWidget* containerMd5 = new QWidget( this ); lay1->addWidget( containerMd5 ); QHBoxLayout* hlayMd5 = new QHBoxLayout( containerMd5 ); m_matchingMD5 = new QCheckBox( i18n( "Stack images with identical MD5 sum") ); m_matchingMD5->setChecked( false ); hlayMd5->addWidget( m_matchingMD5 ); QWidget* containerFile = new QWidget( this ); lay1->addWidget( containerFile ); QHBoxLayout* hlayFile = new QHBoxLayout( containerFile ); m_matchingFile = new QCheckBox( i18n( "Stack images based on file version detection") ); m_matchingFile->setChecked( true ); hlayFile->addWidget( m_matchingFile ); m_origTop = new QCheckBox( i18n( "Original to top") ); m_origTop ->setChecked( false ); hlayFile->addWidget( m_origTop ); QWidget* containerContinuous = new QWidget( this ); lay1->addWidget( containerContinuous ); QHBoxLayout* hlayContinuous = new QHBoxLayout( containerContinuous ); //FIXME: This is hard to translate because of the split sentence. It is better //to use a single sentence here like "Stack images that are (were?) shot //within this time:" and use the spin method setSuffix() to set the "seconds". //Also: Would minutes not be a more sane time unit here? (schwarzer) m_continuousShooting = new QCheckBox( i18nc( "The whole sentence should read: *Stack images that are shot within x seconds of each other*. So images that are shot in one burst are automatically stacked together. (This sentence is before the x.)", "Stack images that are shot within" ) ); m_continuousShooting->setChecked( false ); hlayContinuous->addWidget( m_continuousShooting ); m_continuousThreshold = new QSpinBox; m_continuousThreshold->setRange( 1, 999 ); m_continuousThreshold->setSingleStep( 1 ); m_continuousThreshold->setValue( 2 ); hlayContinuous->addWidget( m_continuousThreshold ); QLabel* sec = new QLabel( i18nc( "The whole sentence should read: *Stack images that are shot within x seconds of each other*. (This being the text after x.)", "seconds" ), containerContinuous ); hlayContinuous->addWidget( sec ); QGroupBox* grpOptions = new QGroupBox( i18n("AutoStacking Options") ); QVBoxLayout* grpLayOptions = new QVBoxLayout( grpOptions ); lay1->addWidget( grpOptions ); m_autostackDefault = new QRadioButton( i18n( "Include matching image to appropriate stack (if one exists)") ); m_autostackDefault->setChecked( true ); grpLayOptions->addWidget( m_autostackDefault ); m_autostackUnstack = new QRadioButton( i18n( "Unstack images from their current stack and create new one for the matches") ); m_autostackUnstack->setChecked( false ); grpLayOptions->addWidget( m_autostackUnstack ); m_autostackSkip = new QRadioButton( i18n( "Skip images that are already in a stack") ); m_autostackSkip->setChecked( false ); grpLayOptions->addWidget( m_autostackSkip ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &AutoStackImages::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &AutoStackImages::reject); lay1->addWidget(buttonBox); } /* * This function searches for images with matching MD5 sums * Matches are automatically stacked */ void AutoStackImages::matchingMD5( DB::FileNameList& toBeShown ) { QMap< DB::MD5, DB::FileNameList > tostack; DB::FileNameList showIfStacked; // Stacking all images that have the same MD5 sum // First make a map of MD5 sums with corresponding images Q_FOREACH(const DB::FileName& fileName, m_list) { DB::MD5 sum = fileName.info()->MD5Sum(); if ( DB::ImageDB::instance()->md5Map()->contains( sum ) ) { if (tostack[sum].isEmpty()) tostack.insert(sum, DB::FileNameList() << fileName); else tostack[sum].append(fileName); } } // Then add images to stack (depending on configuration options) for( QMap::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it ) { if ( tostack[it.key()].count() > 1 ) { DB::FileNameList stack; for ( int i = 0; i < tostack[it.key()].count(); ++i ) { if ( !DB::ImageDB::instance()->getStackFor( tostack[it.key()][i]).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << tostack[it.key()][i]); else if ( m_autostackSkip->isChecked() ) continue; } showIfStacked.append( tostack[it.key()][i] ); stack.append( tostack[it.key()][i]); } if ( stack.size() > 1 ) { Q_FOREACH( const DB::FileName& a, showIfStacked ) { if ( !DB::ImageDB::instance()->getStackFor(a).isEmpty() ) Q_FOREACH( const DB::FileName& b, DB::ImageDB::instance()->getStackFor(a)) toBeShown.append( b ); else toBeShown.append(a); } DB::ImageDB::instance()->stack(stack); } showIfStacked.clear(); } } } /* * This function searches for images based on file version detection configuration. * Images that are detected to be versions of same file are stacked together. */ void AutoStackImages::matchingFile( DB::FileNameList& toBeShown ) { QMap< DB::MD5, DB::FileNameList > tostack; DB::FileNameList showIfStacked; QString modifiedFileCompString; QRegExp modifiedFileComponent; QStringList originalFileComponents; modifiedFileCompString = Settings::SettingsData::instance()->modifiedFileComponent(); modifiedFileComponent = QRegExp( modifiedFileCompString ); originalFileComponents << Settings::SettingsData::instance()->originalFileComponent(); originalFileComponents = originalFileComponents.at( 0 ).split( QString::fromLatin1(";") ); // Stacking all images based on file version detection // First round prepares the stacking Q_FOREACH( const DB::FileName& fileName, m_list ) { if ( modifiedFileCompString.length() >= 0 && fileName.relative().contains( modifiedFileComponent ) ) { for( QStringList::const_iterator it = originalFileComponents.constBegin(); it != originalFileComponents.constEnd(); ++it ) { QString tmp = fileName.relative(); tmp.replace( modifiedFileComponent, ( *it )); DB::FileName originalFileName = DB::FileName::fromRelativePath( tmp ); if ( originalFileName != fileName && m_list.contains( originalFileName ) ) { DB::MD5 sum = originalFileName.info()->MD5Sum(); if ( tostack[sum].isEmpty() ) { if ( m_origTop->isChecked() ) { tostack.insert( sum, DB::FileNameList() << originalFileName ); tostack[sum].append( fileName ); } else { tostack.insert( sum, DB::FileNameList() << fileName ); tostack[sum].append( originalFileName ); } } else tostack[sum].append(fileName); break; } } } } // Then add images to stack (depending on configuration options) for( QMap::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it ) { if ( tostack[it.key()].count() > 1 ) { DB::FileNameList stack; for ( int i = 0; i < tostack[it.key()].count(); ++i ) { if ( !DB::ImageDB::instance()->getStackFor( tostack[it.key()][i]).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << tostack[it.key()][i]); else if ( m_autostackSkip->isChecked() ) continue; } showIfStacked.append( tostack[it.key()][i] ); stack.append( tostack[it.key()][i]); } if ( stack.size() > 1 ) { Q_FOREACH( const DB::FileName& a, showIfStacked ) { if ( !DB::ImageDB::instance()->getStackFor(a).isEmpty() ) Q_FOREACH( const DB::FileName& b, DB::ImageDB::instance()->getStackFor(a)) toBeShown.append( b ); else toBeShown.append(a); } DB::ImageDB::instance()->stack(stack); } showIfStacked.clear(); } } } /* * This function searches for images that are shot within specified time frame */ void AutoStackImages::continuousShooting(DB::FileNameList &toBeShown ) { DB::ImageInfoPtr prev; Q_FOREACH(const DB::FileName& fileName, m_list) { DB::ImageInfoPtr info = fileName.info(); // Skipping images that do not have exact time stamp if ( info->date().start() != info->date().end() ) continue; if ( prev && ( prev->date().start().secsTo( info->date().start() ) < m_continuousThreshold->value() ) ) { DB::FileNameList stack; if ( !DB::ImageDB::instance()->getStackFor( prev->fileName() ).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << prev->fileName()); else if ( m_autostackSkip->isChecked() ) continue; } if ( !DB::ImageDB::instance()->getStackFor(fileName).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << fileName); else if ( m_autostackSkip->isChecked() ) continue; } stack.append(prev->fileName()); stack.append(info->fileName()); if ( !toBeShown.isEmpty() ) { if ( toBeShown.at( toBeShown.size() - 1 ).info()->fileName() != prev->fileName() ) toBeShown.append(prev->fileName()); } else { // if this is first insert, we have to include also the stacked images from previuous image if ( !DB::ImageDB::instance()->getStackFor( info->fileName() ).isEmpty() ) Q_FOREACH( const DB::FileName& a, DB::ImageDB::instance()->getStackFor( prev->fileName() ) ) toBeShown.append( a ); else toBeShown.append(prev->fileName()); } // Inserting stacked images from the current image if ( !DB::ImageDB::instance()->getStackFor( info->fileName() ).isEmpty() ) Q_FOREACH( const DB::FileName& a, DB::ImageDB::instance()->getStackFor(fileName)) toBeShown.append( a ); else toBeShown.append(info->fileName()); DB::ImageDB::instance()->stack(stack); } prev = info; } } void AutoStackImages::accept() { QDialog::accept(); Utilities::ShowBusyCursor dummy; DB::FileNameList toBeShown; if ( m_matchingMD5->isChecked() ) matchingMD5( toBeShown ); if ( m_matchingFile->isChecked() ) matchingFile( toBeShown ); if ( m_continuousShooting->isChecked() ) continuousShooting( toBeShown ); MainWindow::Window::theMainWindow()->showThumbNails(toBeShown); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/CopyPopup.cpp b/MainWindow/CopyPopup.cpp index 6f67c74b..6b0235d2 100644 --- a/MainWindow/CopyPopup.cpp +++ b/MainWindow/CopyPopup.cpp @@ -1,129 +1,129 @@ /* Copyright (C) 2014-2016 Tobias Leupold 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. */ // Qt includes #include #include // KDE includes #include #include // Local includes #include "CopyPopup.h" MainWindow::CopyPopup::CopyPopup(QWidget *parent, QUrl &selectedFile, QList &allSelectedFiles, QString &lastTarget, CopyType copyType) : QMenu(parent), m_selectedFile(selectedFile), m_allSelectedFiles(allSelectedFiles), m_lastTarget(lastTarget) { connect(this, &CopyPopup::triggered, this, &CopyPopup::slotCopy); QAction *action; bool haveSeveralFiles = m_allSelectedFiles.size() > 1; switch (copyType) { case Copy: setTitle(i18n("Copy image(s) to...")); action = addAction(i18n("Copy currently selected image to...")); action->setData(CopyAction::CopyCurrent); action = addAction(i18n("Copy all selected images to...")); action->setData(CopyAction::CopyAll); action->setEnabled(haveSeveralFiles); break; case Link: action = addAction(i18n("Link currently selected image to...")); setTitle(i18n("Link image(s) to...")); action->setData(CopyAction::LinkCurrent); action = addAction(i18n("Link all selected images to...")); action->setData(CopyAction::LinkAll); action->setEnabled(haveSeveralFiles); break; } } void MainWindow::CopyPopup::slotCopy(QAction *action) { if (m_lastTarget.isNull()) { m_lastTarget = m_allSelectedFiles.at(0).adjusted(QUrl::RemoveFilename).path(); } CopyAction copyAction = static_cast(action->data().toUInt()); QFileDialog dialog(this); if (copyAction == CopyCurrent || copyAction == LinkCurrent) { if (copyAction == CopyCurrent) { - dialog.setWindowTitle(i18nc("@title:window", "Copy image to...")); + dialog.setWindowTitle(i18nc("@title:window", "Copy Image to...")); dialog.setLabelText(QFileDialog::Accept, i18nc("@action:button", "Copy")); } else if (copyAction == LinkCurrent) { - dialog.setWindowTitle(i18nc("@title:window", "Link image to...")); + dialog.setWindowTitle(i18nc("@title:window", "Link Image to...")); dialog.setLabelText(QFileDialog::Accept, i18nc("@action:button", "Link")); } dialog.setDirectory(m_lastTarget); dialog.selectFile(m_selectedFile.fileName()); dialog.setAcceptMode(QFileDialog::AcceptSave); if (dialog.exec()) { QUrl target = dialog.selectedUrls().first(); m_lastTarget = target.adjusted(QUrl::RemoveFilename).path(); KIO::CopyJob *job; if (copyAction == CopyCurrent) { job = KIO::copy(m_selectedFile, target); } else if (copyAction == LinkCurrent) { job = KIO::link(m_selectedFile, target); } connect(job, &KIO::CopyJob::finished, job, &QObject::deleteLater); } } else if (copyAction == CopyAll || copyAction == LinkAll) { QString title; if (copyAction == CopyAll) { title = i18nc("@title:window", "Copy images to..."); } else if (copyAction == LinkAll) { title = i18nc("@title:window", "Link images to..."); } QString target = QFileDialog::getExistingDirectory(this, title, m_lastTarget, QFileDialog::ShowDirsOnly); if (! target.isEmpty()) { m_lastTarget = target; QUrl targetUrl = QUrl::fromLocalFile(target); KIO::CopyJob *job; if (copyAction == CopyAll) { job = KIO::copy(m_allSelectedFiles, targetUrl); } else if (copyAction == LinkAll) { job = KIO::link(m_allSelectedFiles, targetUrl); } connect(job, &KIO::CopyJob::finished, job, &QObject::deleteLater); } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DeleteDialog.cpp b/MainWindow/DeleteDialog.cpp index cd8b2cd1..91e3e3bb 100644 --- a/MainWindow/DeleteDialog.cpp +++ b/MainWindow/DeleteDialog.cpp @@ -1,109 +1,109 @@ /* 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 "DeleteDialog.h" #include #include #include #include #include #include #include #include "Utilities/DeleteFiles.h" using namespace MainWindow; DeleteDialog::DeleteDialog( QWidget* parent ) : QDialog(parent) , m_list() { - setWindowTitle( i18n("Removing items") ); + setWindowTitle( i18nc("@title:window", "Removing Items") ); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); QWidget* top = new QWidget; QVBoxLayout* lay1 = new QVBoxLayout( top ); mainLayout->addWidget(top); m_label = new QLabel; lay1->addWidget( m_label ); m_useTrash = new QRadioButton; lay1->addWidget( m_useTrash ); m_deleteFile = new QRadioButton; lay1->addWidget( m_deleteFile ); m_deleteFromDb = new QRadioButton; lay1->addWidget( m_deleteFromDb ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &DeleteDialog::deleteImages); connect(buttonBox, &QDialogButtonBox::rejected, this, &DeleteDialog::reject); mainLayout->addWidget(buttonBox); } int DeleteDialog::exec(const DB::FileNameList& list) { if (!list.size()) return 0; bool someFileExists = false; Q_FOREACH(const DB::FileName& file, list) { if ( file.exists() ) { someFileExists = true; break; } } const QString msg1 = i18np( "Removing 1 item", "Removing %1 items", list.size() ); const QString msg2 = i18np( "Selected item will be removed from the database.
What do you want to do with the file on disk?", "Selected %1 items will be removed from the database.
What do you want to do with the files on disk?", list.size() ); const QString txt = QString::fromLatin1( "

%1
%2

" ).arg(msg1).arg(msg2); m_useTrash->setText( i18np("Move file to Trash", "Move %1 files to Trash", list.size() ) ); m_deleteFile->setText( i18np( "Delete file from disk", "Delete %1 files from disk", list.size() ) ); m_deleteFromDb->setText( i18np( "Only remove the item from database", "Only remove %1 items from database", list.size() ) ); m_label->setText( txt ); m_list = list; // disable trash/delete options if files don't exist m_useTrash->setChecked( someFileExists ); m_useTrash->setEnabled( someFileExists ); m_deleteFile->setEnabled( someFileExists ); m_deleteFromDb->setChecked( !someFileExists ); return QDialog::exec(); } void DeleteDialog::deleteImages() { bool anyDeleted = Utilities::DeleteFiles::deleteFiles(m_list, m_deleteFile->isChecked() ? Utilities::DeleteFromDisk : m_useTrash->isChecked() ? Utilities::MoveToTrash : Utilities::BlockFromDatabase ); if ( anyDeleted ) accept(); else reject(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/FeatureDialog.cpp b/MainWindow/FeatureDialog.cpp index 9bccd3f1..41a0f51c 100644 --- a/MainWindow/FeatureDialog.cpp +++ b/MainWindow/FeatureDialog.cpp @@ -1,246 +1,246 @@ /* 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 #include #include "FeatureDialog.h" #include #include #include #include #include #include #include #include #include #include #include "Exif/Database.h" using namespace MainWindow; FeatureDialog::FeatureDialog( QWidget* parent ) :QDialog( parent ) { - setWindowTitle( i18n("KPhotoAlbum Feature Status") ); + 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.

"); + "

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("

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 or MPlayer to extract thumbnails from videos. These thumbnails are used to preview " "videos in the thumbnail viewer.

    " "

    In the past, MPlayer (in contrast to MPlayer2) often had problems extracting the length " "of videos and also often fails to extract the thumbnails used for cycling video thumbnails. " "For that reason, you should prefer ffmpeg or MPlayer2 over MPlayer, if possible.

    " ); text += i18n("

    Video metadata support

    " "

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

    " "

    Correct length information is also 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 return true; #else return false; #endif } QString FeatureDialog::mplayerBinary() { QString mplayer = QStandardPaths::findExecutable( QString::fromLatin1("mplayer2")); if ( mplayer.isNull() ) mplayer = QStandardPaths::findExecutable( QString::fromLatin1("mplayer")); return mplayer; } bool FeatureDialog::isMplayer2() { QProcess process; process.start( mplayerBinary(), QStringList() << QString::fromLatin1("--version")); process.waitForFinished(); const QString output = QString::fromLocal8Bit(process.readAllStandardOutput().data()); return output.contains(QString::fromLatin1("MPlayer2")); } 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() && mplayerBinary().isEmpty()); } bool FeatureDialog::hasVideoProber() { return ! ( ffprobeBinary().isEmpty() && mplayerBinary().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( "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() ); QString result = QString::fromLatin1("

    "); const QString red = QString::fromLatin1("%1"); const QString yellow = 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 ); } QString thumbnailSupport = hasVideoThumbnailer() ? ( !ffmpegBinary().isEmpty() || isMplayer2() ? yes : yellow.arg(i18n("Only with MPlayer1"))) : no ; result += formatString.arg(QString::fromLatin1("#videoPreview")).arg(i18n("Video thumbnail support")).arg(thumbnailSupport); QString videoinfoSupport = hasVideoProber() ? yes : no; result += formatString.arg(QString::fromLatin1("#videoInfo")).arg(i18n("Video metadata support")).arg(videoinfoSupport); 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/InvalidDateFinder.cpp b/MainWindow/InvalidDateFinder.cpp index 5925dfce..03a5ed55 100644 --- a/MainWindow/InvalidDateFinder.cpp +++ b/MainWindow/InvalidDateFinder.cpp @@ -1,149 +1,149 @@ /* 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 "InvalidDateFinder.h" #include #include #include #include "DB/ImageInfo.h" #include "DB/ImageDB.h" #include "DB/ImageDate.h" #include "DB/FileInfo.h" #include "MainWindow/Window.h" #include #include #include "Utilities/ShowBusyCursor.h" #include #include #include #include #include #include using namespace MainWindow; InvalidDateFinder::InvalidDateFinder( QWidget* parent ) :QDialog( parent ) { - setWindowTitle( i18n("Search for Images and Videos with Missing Dates" ) ); + setWindowTitle( i18nc("@title:window", "Search for Images and Videos with Missing Dates" ) ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QGroupBox* grp = new QGroupBox( i18n("Which Images and Videos to Display") ); QVBoxLayout* grpLay = new QVBoxLayout( grp ); mainLayout->addWidget( grp ); m_dateNotTime = new QRadioButton( i18n( "Search for images and videos with a valid date but an invalid time stamp") ); m_missingDate = new QRadioButton( i18n( "Search for images and videos missing date and time" ) ); m_partialDate = new QRadioButton( i18n( "Search for images and videos with only partial dates (like 1971 vs. 11/7-1971)") ); m_dateNotTime->setChecked( true ); grpLay->addWidget( m_dateNotTime ); grpLay->addWidget( m_missingDate ); grpLay->addWidget( m_partialDate ); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &InvalidDateFinder::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &InvalidDateFinder::reject); mainLayout->addWidget(buttonBox); } void InvalidDateFinder::accept() { QDialog::accept(); Utilities::ShowBusyCursor dummy; // create the info dialog QDialog* info = new QDialog; QVBoxLayout *mainLayout = new QVBoxLayout; info->setLayout(mainLayout); - info->setWindowTitle( i18n("Image Info" ) ); + info->setWindowTitle( i18nc("@title:window", "Image Info" ) ); KTextEdit* edit = new KTextEdit( info ); mainLayout->addWidget( edit ); edit->setText( i18n("

    Here you may see the date changes for the displayed items.

    ") ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); info->connect(buttonBox, &QDialogButtonBox::accepted, info, &QDialog::accept); info->connect(buttonBox, &QDialogButtonBox::rejected, info, &QDialog::reject); mainLayout->addWidget(buttonBox); // Now search for the images. const DB::FileNameList list = DB::ImageDB::instance()->images(); DB::FileNameList toBeShown; QProgressDialog dialog( nullptr); - dialog.setWindowTitle(i18n("Reading file properties")); + dialog.setWindowTitle(i18nc("@title:window", "Reading File Properties")); dialog.setMaximum(list.size()); dialog.setValue(0); int progress = 0; Q_FOREACH(const DB::FileName& fileName, list) { dialog.setValue( ++progress ); qApp->processEvents( QEventLoop::AllEvents ); if ( dialog.wasCanceled() ) break; if ( fileName.info()->isNull() ) continue; DB::ImageDate date = fileName.info()->date(); bool show = false; if ( m_dateNotTime->isChecked() ) { DB::FileInfo fi = DB::FileInfo::read( fileName, DB::EXIFMODE_DATE ); if ( fi.dateTime().date() == date.start().date() ) show = ( fi.dateTime().time() != date.start().time() ); if ( show ) { edit->append( QString::fromLatin1("%1:
    existing = %2
    new..... = %3" ) .arg(fileName.relative()) .arg(date.start().toString()) .arg(fi.dateTime().toString()) ); } } else if ( m_missingDate->isChecked() ) { show = !date.start().isValid(); } else if ( m_partialDate->isChecked() ) { show = ( date.start() != date.end() ); } if ( show ) toBeShown.append(fileName); } if ( m_dateNotTime->isChecked() ) { info->resize( 800, 600 ); edit->setReadOnly( true ); QFont f = edit->font(); f.setFamily( QString::fromLatin1( "fixed" ) ); edit->setFont( f ); info->show(); } else delete info; Window::theMainWindow()->showThumbNails( toBeShown ); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/TokenEditor.cpp b/MainWindow/TokenEditor.cpp index 2508a58c..0ceecd18 100644 --- a/MainWindow/TokenEditor.cpp +++ b/MainWindow/TokenEditor.cpp @@ -1,138 +1,138 @@ /* 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 "TokenEditor.h" #include #include #include #include #include #include #include #include #include #include "DB/ImageDB.h" #include "DB/CategoryCollection.h" #include "DB/Category.h" #include "DB/ImageSearchInfo.h" #include "Settings/SettingsData.h" using namespace MainWindow; TokenEditor::TokenEditor( QWidget* parent ) :QDialog( parent ) { - setWindowTitle( i18n( "Remove Tokens" ) ); + setWindowTitle( i18nc("@title:window", "Remove Tokens" ) ); QVBoxLayout *dialogLayout = new QVBoxLayout(this); QWidget* mainContents = new QWidget; QVBoxLayout* vlay = new QVBoxLayout( mainContents ); QLabel* label = new QLabel( i18n("Select tokens to remove from all images and videos:") ); vlay->addWidget( label ); QGridLayout* grid = new QGridLayout; vlay->addLayout( grid ); int index = 0; for ( int ch = 'A'; ch <= 'Z'; ch++, index++ ) { QChar token = QChar::fromLatin1( (char) ch ); QCheckBox* box = new QCheckBox( token ); grid->addWidget( box, index/5, index % 5 ); m_checkBoxes.append( box ); } QHBoxLayout* hlay = new QHBoxLayout; vlay->addLayout( hlay ); hlay->addStretch( 1 ); QPushButton* selectAll = new QPushButton( i18n("Select All") ); QPushButton* selectNone = new QPushButton( i18n("Select None") ); hlay->addWidget( selectAll ); hlay->addWidget( selectNone ); connect(selectAll, &QPushButton::clicked, this, &TokenEditor::selectAll); connect(selectNone, &QPushButton::clicked, this, &TokenEditor::selectNone); dialogLayout->addWidget(mainContents); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &TokenEditor::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &TokenEditor::reject); dialogLayout->addWidget(buttonBox); } void TokenEditor::show() { QStringList tokens = tokensInUse(); Q_FOREACH( QCheckBox *box, m_checkBoxes ) { box->setChecked( false ); QString txt = box->text().remove( QString::fromLatin1("&") ); box->setEnabled( tokens.contains( txt ) ); } QDialog::show(); } void TokenEditor::selectAll() { Q_FOREACH( QCheckBox *box, m_checkBoxes ) { box->setChecked( true ); } } void TokenEditor::selectNone() { Q_FOREACH( QCheckBox *box, m_checkBoxes ) { box->setChecked( false ); } } /** I would love to use Settings::optionValue, but that method does not forget about an item once it has seen it, which is really what it should do anyway, otherwise it would be way to expensive in use. */ QStringList TokenEditor::tokensInUse() { QStringList res; DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); QMap map = DB::ImageDB::instance()->classify( DB::ImageSearchInfo(), tokensCategory->name(), DB::anyMediaType ); for( QMap::Iterator it = map.begin(); it != map.end(); ++it ) { if ( it.value() > 0 ) res.append( it.key() ); } return res; } void TokenEditor::accept() { DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); Q_FOREACH( const QCheckBox *box, m_checkBoxes ) { if ( box->isChecked() && box->isEnabled() ) { QString txt = box->text().remove( QString::fromLatin1("&") ); tokensCategory->removeItem( txt ); } } QDialog::accept(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Window.cpp b/MainWindow/Window.cpp index 78c791da..9853b5fa 100644 --- a/MainWindow/Window.cpp +++ b/MainWindow/Window.cpp @@ -1,1987 +1,1987 @@ /* 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 #include "Window.h" #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 // for #if KIO_VERSION... #include #include #include #include #include #include #include #include #include #include #include #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 #ifdef HASKIPI # include #endif #include #include #include #include #include #include #include #include #include #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" 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 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."; createSarchBar(); 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); checkIfMplayerIsInstalled(); 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" ) ); loadPlugins(); 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."; + 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(); announceAndroidVersion(); } 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& current, 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 = Utilities::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() ); 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->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...") ); + 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->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 << QLatin1String("kphotoalbum") << 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 ); // 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 = QUrl::fromLocalFile(info->fileName().absolute()); QList allSelectedFiles; for (const QString &selectedFile : selected().toStringList(DB::AbsolutePath)) { allSelectedFiles << QUrl::fromLocalFile(selectedFile); } // "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 loadPlugins(); 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 ); } 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(); createSarchBar(); } 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_hasLoadedPlugins = true; return; // This is no good, but lets try and continue. } #ifdef HASKIPI connect(menu, &QMenu::aboutToShow, this, &Window::loadPlugins); m_hasLoadedPlugins = false; #else menu->setEnabled(false); m_hasLoadedPlugins = true; #endif } void MainWindow::Window::loadPlugins() { #ifdef HASKIPI Utilities::ShowBusyCursor dummy; if ( m_hasLoadedPlugins ) 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_hasLoadedPlugins = 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 = Utilities::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() { #if (KIO_VERSION >= ((5<<16)|(31<<8)|(0))) KRun::runUrl(QUrl(QString::fromLatin1("http://www.kphotoalbum.org/index.php?page=videos")) , QString::fromLatin1( "text/html" ) , this , KRun::RunFlags() ); #else // this signature is deprecated in newer kio versions // TODO: remove this when we don't support Ubuntu 16.04 LTS anymore KRun::runUrl(QUrl(QString::fromLatin1("http://www.kphotoalbum.org/index.php?page=videos")) , QString::fromLatin1( "text/html" ) , this ); #endif } 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 ); } 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::createSarchBar() { // 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); } 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::checkIfMplayerIsInstalled() { if (Options::the()->demoMode()) return; if ( !FeatureDialog::hasVideoThumbnailer() ) { KMessageBox::information( this, i18n("

    Unable to find ffmpeg or MPlayer on the system.

    " "

    Without either of these, KPhotoAlbum will not be able to display video thumbnails and video lengths. " "Please install the ffmpeg or MPlayer package

    "), i18n("Video thumbnails are not available"), QString::fromLatin1("mplayerNotInstalled")); } else { KMessageBox::enableMessage( QString::fromLatin1("mplayerNotInstalled") ); if ( FeatureDialog::ffmpegBinary().isEmpty() && !FeatureDialog::isMplayer2() ) { KMessageBox::information( this, i18n("

    You have MPlayer installed on your system, but it is unfortunately not version 2. " "MPlayer2 is on most systems a separate package, please install that if at all possible, " "as that version has much better support for extracting thumbnails from videos.

    "), i18n("MPlayer is too old"), QString::fromLatin1("mplayerVersionTooOld")); } else KMessageBox::enableMessage( QString::fromLatin1("mplayerVersionTooOld") ); } } bool MainWindow::Window::anyVideosSelected() const { Q_FOREACH(const DB::FileName& fileName, selected()) { if ( Utilities::isVideo(fileName)) return true; } return false; } void MainWindow::Window::announceAndroidVersion() { // Don't bother people with this information when they are starting KPA the first time if (DB::ImageDB::instance()->totalCount() < 100) return; const QString doNotShowKey = QString::fromLatin1( "announce_android_version_key" ); const QString txt = i18n("

    Did you know that there is an Android client for KPhotoAlbum?
    " "With the Android client you can view your images from your desktop.

    " "

    See youtube video or " "install from google play

    " ); KMessageBox::information( this, txt, QString(), doNotShowKey, KMessageBox::AllowLink ); } 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 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 // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/CategoryPage.cpp b/Settings/CategoryPage.cpp index 7fee9121..333dfa7d 100644 --- a/Settings/CategoryPage.cpp +++ b/Settings/CategoryPage.cpp @@ -1,563 +1,563 @@ /* Copyright (C) 2003-2014 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 "CategoryPage.h" // Qt includes #include #include #include #include #include #include #include #include #include #include // KDE includes #include #include #include // Local includes #include "DB/ImageDB.h" #include "DB/CategoryCollection.h" #include "DB/MemberMap.h" #include "MainWindow/Window.h" #include "MainWindow/DirtyIndicator.h" #include "UntaggedGroupBox.h" #include "SettingsDialog.h" #include "CategoryItem.h" Settings::CategoryPage::CategoryPage(QWidget* parent) : QWidget(parent) { QVBoxLayout* mainLayout = new QVBoxLayout(this); // The category settings QGroupBox* categoryGroupBox = new QGroupBox; mainLayout->addWidget(categoryGroupBox); categoryGroupBox->setTitle(i18n("Category Settings")); QHBoxLayout* categoryLayout = new QHBoxLayout(categoryGroupBox); // Category list QVBoxLayout* categorySideLayout = new QVBoxLayout; categoryLayout->addLayout(categorySideLayout); m_categoriesListWidget = new QListWidget; connect(m_categoriesListWidget, &QListWidget::itemClicked, this, &CategoryPage::editCategory); connect(m_categoriesListWidget, &QListWidget::itemSelectionChanged, this, &CategoryPage::editSelectedCategory); connect(m_categoriesListWidget, &QListWidget::itemChanged, this, &CategoryPage::categoryNameChanged); // This is needed to fix some odd behavior if the "New" button is double clicked connect(m_categoriesListWidget, &QListWidget::itemDoubleClicked, this, &CategoryPage::categoryDoubleClicked); connect(m_categoriesListWidget->itemDelegate(), SIGNAL(closeEditor(QWidget*,QAbstractItemDelegate::EndEditHint)), this, SLOT(listWidgetEditEnd(QWidget*,QAbstractItemDelegate::EndEditHint))); categorySideLayout->addWidget(m_categoriesListWidget); // New, Delete, and buttons QHBoxLayout* newDeleteRenameLayout = new QHBoxLayout; categorySideLayout->addLayout(newDeleteRenameLayout); m_newCategoryButton = new QPushButton(i18n("New")); connect(m_newCategoryButton, &QPushButton::clicked, this, &CategoryPage::newCategory); newDeleteRenameLayout->addWidget(m_newCategoryButton); m_delItem = new QPushButton(i18n("Delete")); connect(m_delItem, &QPushButton::clicked, this, &CategoryPage::deleteCurrentCategory); newDeleteRenameLayout->addWidget(m_delItem); m_renameItem = new QPushButton(i18n("Rename")); connect(m_renameItem, &QPushButton::clicked, this, &CategoryPage::renameCurrentCategory); newDeleteRenameLayout->addWidget(m_renameItem); // Category settings QVBoxLayout* rightSideLayout = new QVBoxLayout; categoryLayout->addLayout(rightSideLayout); // Header m_categoryLabel = new QLabel; m_categoryLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); rightSideLayout->addWidget(m_categoryLabel); // Pending rename label m_renameLabel = new QLabel; m_renameLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); rightSideLayout->addWidget(m_renameLabel); QDialog *parentDialog = qobject_cast(parent); connect( parentDialog, &QDialog::rejected, m_renameLabel, &QLabel::clear); // Some space looks better here :-) QLabel* spacer = new QLabel; rightSideLayout->addWidget(spacer); // Here we start with the actual settings QGridLayout* settingsLayout = new QGridLayout; rightSideLayout->addLayout(settingsLayout); int row = 0; // Positionable m_positionableLabel = new QLabel(i18n("Positionable tags:")); settingsLayout->addWidget(m_positionableLabel, row, 0); m_positionable = new QCheckBox(i18n("Tags in this category can be associated with areas within images")); settingsLayout->addWidget(m_positionable, row, 1); connect(m_positionable, &QCheckBox::clicked, this, &CategoryPage::positionableChanged); row++; // Icon m_iconLabel = new QLabel(i18n("Icon:")); settingsLayout->addWidget(m_iconLabel, row, 0); m_icon = new KIconButton; settingsLayout->addWidget(m_icon, row, 1); m_icon->setIconSize(32); m_icon->setIcon(QString::fromUtf8("personsIcon")); connect(m_icon, &KIconButton::iconChanged, this, &CategoryPage::iconChanged); row++; // Thumbnail size - m_thumbnailSizeInCategoryLabel = new QLabel(i18n("Thumbnail Size:")); + m_thumbnailSizeInCategoryLabel = new QLabel(i18n("Thumbnail size:")); settingsLayout->addWidget(m_thumbnailSizeInCategoryLabel, row, 0); m_thumbnailSizeInCategory = new QSpinBox; m_thumbnailSizeInCategory->setRange(32, 512); m_thumbnailSizeInCategory->setSingleStep(32); settingsLayout->addWidget(m_thumbnailSizeInCategory, row, 1); connect(m_thumbnailSizeInCategory, static_cast(&QSpinBox::valueChanged), this, &CategoryPage::thumbnailSizeChanged); row++; // Preferred View m_preferredViewLabel = new QLabel(i18n("Preferred view:")); settingsLayout->addWidget(m_preferredViewLabel, row, 0); m_preferredView = new QComboBox; settingsLayout->addWidget(m_preferredView, row, 1); m_preferredView->addItems(QStringList() << i18n("List View") << i18n("List View with Custom Thumbnails") << i18n("Icon View") << i18n("Icon View with Custom Thumbnails")); connect(m_preferredView, static_cast(&QComboBox::activated), this, &CategoryPage::preferredViewChanged); rightSideLayout->addStretch(); // Info about the database not being saved QHBoxLayout* dbNotSavedLayout = new QHBoxLayout; mainLayout->addLayout(dbNotSavedLayout); m_dbNotSavedLabel = new QLabel( i18n("" "The database has unsaved changes. As long as those are " "not saved,
    the names of categories can't be changed " "and new ones can't be added." "
    ")); m_dbNotSavedLabel->setWordWrap(true); dbNotSavedLayout->addWidget(m_dbNotSavedLabel); m_saveDbNowButton = new QPushButton(i18n("Save the DB now")); m_saveDbNowButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum); connect(m_saveDbNowButton, &QPushButton::clicked, this, &CategoryPage::saveDbNow); dbNotSavedLayout->addWidget(m_saveDbNowButton); resetInterface(); // Untagged images m_untaggedBox = new UntaggedGroupBox; mainLayout->addWidget(m_untaggedBox); m_currentCategory = 0; // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = false; m_categoryNamesChanged = false; } void Settings::CategoryPage::resetInterface() { enableDisable(false); m_categoriesListWidget->setItemSelected(m_categoriesListWidget->currentItem(), false); resetCategoryLabel(); m_renameLabel->hide(); } void Settings::CategoryPage::editSelectedCategory() { editCategory(m_categoriesListWidget->currentItem()); } void Settings::CategoryPage::editCategory(QListWidgetItem* i) { if (i == 0) { return; } m_categoryNameBeforeEdit = i->text(); Settings::CategoryItem* item = static_cast(i); m_currentCategory = item; m_categoryLabel->setText(i18n("Settings for category %1", item->originalName())); if (m_currentCategory->originalName() != m_categoryNameBeforeEdit) { m_renameLabel->setText(i18n("Pending change: rename to \"%1\"", m_categoryNameBeforeEdit)); m_renameLabel->show(); } else { m_renameLabel->clear(); m_renameLabel->hide(); } m_positionable->setChecked(item->positionable()); m_icon->setIcon(item->icon()); m_thumbnailSizeInCategory->setValue(item->thumbnailSize()); m_preferredView->setCurrentIndex(static_cast(item->viewType())); enableDisable(true); if (item->originalName() == DB::ImageDB::instance()->categoryCollection() ->categoryForSpecial(DB::Category::TokensCategory)->name()) { m_delItem->setEnabled(false); m_positionableLabel->setEnabled(false); m_positionable->setEnabled(false); m_thumbnailSizeInCategoryLabel->setEnabled(false); m_thumbnailSizeInCategory->setEnabled(false); m_preferredViewLabel->setEnabled(false); m_preferredView->setEnabled(false); } } void Settings::CategoryPage::categoryNameChanged(QListWidgetItem* item) { QString newCategoryName = item->text().simplified(); m_categoriesListWidget->blockSignals(true); item->setText(QString()); m_categoriesListWidget->blockSignals(false); // Now let's check if the new name is valid :-) // If it's empty, we're done here. The new name can't be empty. if (newCategoryName.isEmpty()) { resetCategory(item); return; } // We don't want to have special category names. // We do have to search both for the localized version and the C locale version, because a user // could start KPA e. g. with a German locale and create a "Folder" category (which would not // be caught by i18n("Folder")), and then start KPA with the C locale, which would produce a // doubled "Folder" category. if (newCategoryName == i18n("Folder") || newCategoryName == QString::fromUtf8("Folder") || newCategoryName == i18n("Media Type") || newCategoryName == QString::fromUtf8("Media Type")) { resetCategory(item); KMessageBox::sorry(this, i18n("

    Can't change the name of category \"%1\" to \"%2\":

    " "

    \"%2\" is a special category name which is reserved and can't " "be used for a normal category.

    ", m_currentCategory->text(), newCategoryName), i18n("Invalid category name")); return; } // Let's see if we already have a category with this name. if (m_categoriesListWidget->findItems(newCategoryName, Qt::MatchExactly).size() > 0) { resetCategory(item); KMessageBox::sorry(this, i18n("

    Can't change the name of category \"%1\" to \"%2\":

    " "

    A category with this name already exists.

    ", m_currentCategory->text(), newCategoryName), i18n("Invalid category name")); return; } // Let's see if we have any pending name changes that would cause collisions. for (int i = 0; i < m_categoriesListWidget->count(); i++) { Settings::CategoryItem* cat = static_cast(m_categoriesListWidget->item(i)); if (cat == m_currentCategory) { continue; } if (newCategoryName == cat->originalName()) { resetCategory(item); KMessageBox::sorry(this, i18n("

    Can't change the name of category \"%1\" to \"%2\":

    " "

    There's a pending rename action on the category \"%2\". " "Please save this change first.

    ", m_currentCategory->text(), newCategoryName), i18n("Unsaved pending renaming action")); return; } } m_categoriesListWidget->blockSignals(true); item->setText(newCategoryName); m_categoriesListWidget->blockSignals(false); emit categoryChangesPending(); m_untaggedBox->categoryRenamed(m_categoryNameBeforeEdit, newCategoryName); m_currentCategory->setLabel(newCategoryName); editCategory(m_currentCategory); m_categoryNamesChanged = true; } void Settings::CategoryPage::resetCategory(QListWidgetItem* item) { m_categoriesListWidget->blockSignals(true); item->setText(m_categoryNameBeforeEdit); m_categoriesListWidget->blockSignals(false); } void Settings::CategoryPage::positionableChanged(bool positionable) { if (! m_currentCategory) { return; } if (! positionable) { int answer = KMessageBox::questionYesNo(this, i18n("

    Do you really want to make \"%1\" " "non-positionable?

    " "

    All areas linked against this category " "will be deleted!

    ", m_currentCategory->text())); if (answer == KMessageBox::No) { m_positionable->setCheckState(Qt::Checked); return; } } m_currentCategory->setPositionable(positionable); } void Settings::CategoryPage::iconChanged(const QString& icon) { if(m_currentCategory) { m_currentCategory->setIcon(icon); } } void Settings::CategoryPage::thumbnailSizeChanged(int size) { if (m_currentCategory) { m_currentCategory->setThumbnailSize(size); } } void Settings::CategoryPage::preferredViewChanged(int i) { if (m_currentCategory) { m_currentCategory->setViewType(static_cast(i)); } } void Settings::CategoryPage::newCategory() { // This is needed to fix some odd behavior if the "New" button is double clicked if (m_editorOpen) { return; } else { m_editorOpen = true; } // Here starts the real function QString newCategory = i18n("New category"); QString checkedCategory = newCategory; int i = 1; while (m_categoriesListWidget->findItems(checkedCategory, Qt::MatchExactly).size() > 0) { i++; checkedCategory = QString::fromUtf8("%1 %2").arg(newCategory).arg(i); } m_categoriesListWidget->blockSignals(true); m_currentCategory = new Settings::CategoryItem(checkedCategory, QString(), DB::Category::TreeView, 64, m_categoriesListWidget); m_currentCategory->markAsNewCategory(); emit categoryChangesPending(); m_currentCategory->setLabel(checkedCategory); m_currentCategory->setSelected(true); m_categoriesListWidget->blockSignals(false); m_positionable->setChecked(false); m_icon->setIcon(QIcon()); m_thumbnailSizeInCategory->setValue(64); enableDisable(true); editCategory(m_currentCategory); m_categoriesListWidget->editItem(m_currentCategory); } void Settings::CategoryPage::deleteCurrentCategory() { int answer = KMessageBox::questionYesNo(this, i18n("

    Really delete category \"%1\"?

    ", m_currentCategory->text())); if (answer == KMessageBox::No) { return; } m_untaggedBox->categoryDeleted(m_currentCategory->text()); m_deletedCategories.append(m_currentCategory); m_categoriesListWidget->takeItem(m_categoriesListWidget->row(m_currentCategory)); m_currentCategory = 0; m_positionable->setChecked(false); m_icon->setIcon(QIcon()); m_thumbnailSizeInCategory->setValue(64); enableDisable(false); resetCategoryLabel(); editCategory(m_categoriesListWidget->currentItem()); emit categoryChangesPending(); } void Settings::CategoryPage::renameCurrentCategory() { // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = true; m_categoriesListWidget->editItem(m_currentCategory); } void Settings::CategoryPage::enableDisable(bool b) { m_delItem->setEnabled(b); m_positionableLabel->setEnabled(b); m_positionable->setEnabled(b); m_icon->setEnabled(b); m_iconLabel->setEnabled(b); m_thumbnailSizeInCategoryLabel->setEnabled(b); m_thumbnailSizeInCategory->setEnabled(b); m_preferredViewLabel->setEnabled(b); m_preferredView->setEnabled(b); m_categoriesListWidget->blockSignals(true); if (MainWindow::Window::theMainWindow()->dbIsDirty()) { m_dbNotSavedLabel->show(); m_saveDbNowButton->show(); m_renameItem->setEnabled(false); m_newCategoryButton->setEnabled(false); for (int i = 0; i < m_categoriesListWidget->count(); i++) { QListWidgetItem* currentItem = m_categoriesListWidget->item(i); currentItem->setFlags(currentItem->flags() & ~Qt::ItemIsEditable); } } else { m_dbNotSavedLabel->hide(); m_saveDbNowButton->hide(); m_renameItem->setEnabled(b); m_newCategoryButton->setEnabled(true); for (int i = 0; i < m_categoriesListWidget->count(); i++) { QListWidgetItem* currentItem = m_categoriesListWidget->item(i); currentItem->setFlags(currentItem->flags() | Qt::ItemIsEditable); } } m_categoriesListWidget->blockSignals(false); } void Settings::CategoryPage::saveSettings(Settings::SettingsData* opt, DB::MemberMap* memberMap) { // Delete items Q_FOREACH( CategoryItem *item, m_deletedCategories ) { item->removeFromDatabase(); } // Created or Modified items for (int i = 0; i < m_categoriesListWidget->count(); ++i) { CategoryItem* item = static_cast(m_categoriesListWidget->item(i)); item->submit(memberMap); } DB::ImageDB::instance()->memberMap() = *memberMap; m_untaggedBox->saveSettings(opt); if (m_categoryNamesChanged) { // Probably, one or more category names have been edited. Save the database so that // all thumbnails are referenced with the correct name. MainWindow::Window::theMainWindow()->slotSave(); m_categoryNamesChanged = false; } } void Settings::CategoryPage::loadSettings(Settings::SettingsData* opt) { m_categoriesListWidget->blockSignals(true); m_categoriesListWidget->clear(); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH( const DB::CategoryPtr category, categories ) { if (category->type() == DB::Category::PlainCategory || category->type() == DB::Category::TokensCategory) { Settings::CategoryItem *item = new CategoryItem(category->name(), category->iconName(), category->viewType(), category->thumbnailSize(), m_categoriesListWidget, category->positionable()); Q_UNUSED(item); } } m_categoriesListWidget->blockSignals(false); m_untaggedBox->loadSettings(opt); } void Settings::CategoryPage::categoryDoubleClicked(QListWidgetItem*) { // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = true; } void Settings::CategoryPage::listWidgetEditEnd(QWidget*, QAbstractItemDelegate::EndEditHint) { // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = false; } void Settings::CategoryPage::resetCategoryLabel() { - m_categoryLabel->setText(i18n("choose a category to edit it")); + m_categoryLabel->setText(i18n("Choose a category to edit it")); } void Settings::CategoryPage::saveDbNow() { MainWindow::Window::theMainWindow()->slotSave(); resetInterface(); enableDisable(false); } void Settings::CategoryPage::resetCategoryNamesChanged() { m_categoryNamesChanged = false; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/ExifPage.cpp b/Settings/ExifPage.cpp index 8d4b6da3..724b8f24 100644 --- a/Settings/ExifPage.cpp +++ b/Settings/ExifPage.cpp @@ -1,75 +1,75 @@ /* 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 "ExifPage.h" #include "SettingsData.h" #include #include #include #include #include #include #include "Exif/Info.h" #include "Exif/TreeView.h" Settings::ExifPage::ExifPage( QWidget* parent ) : QWidget( parent ) { QVBoxLayout* vlay = new QVBoxLayout( this ); QHBoxLayout* hlay1 = new QHBoxLayout(); QHBoxLayout* hlay2 = new QHBoxLayout(); vlay->addLayout( hlay1 ); vlay->addLayout( hlay2 ); - m_exifForViewer = new Exif::TreeView( i18n( "EXIF/IPTC info to show in the Viewer" ), this ); + m_exifForViewer = new Exif::TreeView( i18n( "Exif/IPTC info to show in the viewer" ), this ); hlay1->addWidget( m_exifForViewer ); - m_exifForDialog = new Exif::TreeView( i18n("EXIF/IPTC info to show in the EXIF dialog"), this ); + m_exifForDialog = new Exif::TreeView( i18n("Exif/IPTC info to show in the Exif dialog"), this ); hlay1->addWidget( m_exifForDialog ); QLabel* iptcCharsetLabel = new QLabel( i18n("Character set for image metadata:"), this ); m_iptcCharset = new KComboBox( this ); QStringList charsets; QList charsetsBA = QTextCodec::availableCodecs(); for (QList::const_iterator it = charsetsBA.constBegin(); it != charsetsBA.constEnd(); ++it ) charsets << QString::fromLatin1(*it); m_iptcCharset->insertItems( m_iptcCharset->count(), charsets ); hlay2->addStretch( 1 ); hlay2->addWidget( iptcCharsetLabel ); hlay2->addWidget( m_iptcCharset ); } void Settings::ExifPage::saveSettings( Settings::SettingsData* opt ) { opt->setExifForViewer( m_exifForViewer->selected() ) ; opt->setExifForDialog( m_exifForDialog->selected() ) ; opt->setIptcCharset( m_iptcCharset->currentText() ); } void Settings::ExifPage::loadSettings( Settings::SettingsData* opt ) { m_exifForViewer->reload(); m_exifForDialog->reload(); m_exifForViewer->setSelectedExif( Settings::SettingsData::instance()->exifForViewer() ); m_exifForDialog->setSelectedExif( Settings::SettingsData::instance()->exifForDialog() ); m_iptcCharset->setCurrentIndex( qMax( 0, QTextCodec::availableCodecs().indexOf( opt->iptcCharset().toLatin1() ) ) ); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/FileVersionDetectionPage.cpp b/Settings/FileVersionDetectionPage.cpp index 8a155350..5b983d46 100644 --- a/Settings/FileVersionDetectionPage.cpp +++ b/Settings/FileVersionDetectionPage.cpp @@ -1,224 +1,224 @@ /* 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 "FileVersionDetectionPage.h" #include "SettingsData.h" #include #include #include #include #include #include Settings::FileVersionDetectionPage::FileVersionDetectionPage( QWidget* parent ) : QWidget( parent ) { QVBoxLayout* topLayout = new QVBoxLayout( this ); QString txt; // General file searching { QGroupBox* generalBox = new QGroupBox( i18n("New File Searches"), this ); topLayout->addWidget( generalBox ); QVBoxLayout* layout = new QVBoxLayout(generalBox); // Search for images on startup m_searchForImagesOnStart = new QCheckBox( i18n("Search for new images and videos on startup"), generalBox ); layout->addWidget(m_searchForImagesOnStart); m_ignoreFileExtension = new QCheckBox( i18n("Ignore file extensions when searching for new images and videos"), generalBox); layout->addWidget(m_ignoreFileExtension); m_skipSymlinks = new QCheckBox( i18n("Skip symbolic links when searching for new images"), generalBox ); layout->addWidget(m_skipSymlinks); m_skipRawIfOtherMatches = new QCheckBox( i18n("Do not read RAW files if a matching JPEG/TIFF file exists"), generalBox ); layout->addWidget(m_skipRawIfOtherMatches); // Exclude directories from search QLabel* excludeDirectoriesLabel = new QLabel( i18n("Directories to exclude from new file search:" ), generalBox ); layout->addWidget(excludeDirectoriesLabel); m_excludeDirectories = new QLineEdit( generalBox ); layout->addWidget(m_excludeDirectories); excludeDirectoriesLabel->setBuddy( m_excludeDirectories ); txt = i18n( "

    KPhotoAlbum is capable of searching for new images and videos when started, this does, " "however, take some time, so instead you may wish to manually tell KPhotoAlbum to search for new images " "using Maintenance->Rescan for new images

    "); m_searchForImagesOnStart->setWhatsThis( txt ); txt = i18n( "

    KPhotoAlbum will normally search new images and videos by their file extension. " "If this option is set, all files neither in the database nor in the block list " "will be checked by their Mime type, regardless of their extension. This will take " "significantly longer than finding files by extension!

    "); m_ignoreFileExtension->setWhatsThis( txt ); txt = i18n( "

    KPhotoAlbum attempts to read all image files whether actual files or symbolic links. If you " "wish to ignore symbolic links, check this option. This is useful if for some reason you have e.g. " "both the original files and symbolic links to these files within your image directory.

    "); m_skipSymlinks->setWhatsThis( txt ); txt = i18n( "

    KPhotoAlbum is capable of reading certain kinds of RAW images. " "Some cameras store both a RAW image and a matching JPEG or TIFF image. " "This causes duplicate images to be stored in KPhotoAlbum, which may be undesirable. " "If this option is checked, KPhotoAlbum will not read RAW files for which matching image files also exist.

    "); m_skipRawIfOtherMatches->setWhatsThis( txt ); txt = i18n( "

    Directories defined here (separated by comma ,) are " "skipped when searching for new photos. Thumbnail directories of different " "tools should be configured here. E.g. xml,ThumbNails,.thumbs,.thumbnails.

    " ); excludeDirectoriesLabel->setWhatsThis( txt ); } // Original/Modified File Support { QGroupBox* modifiedBox = new QGroupBox( i18n("File Version Detection Settings"), this ); topLayout->addWidget( modifiedBox ); QVBoxLayout* layout = new QVBoxLayout(modifiedBox); m_detectModifiedFiles = new QCheckBox(i18n("Try to detect multiple versions of files"), modifiedBox); layout->addWidget(m_detectModifiedFiles); QLabel* modifiedFileComponentLabel = new QLabel( i18n("File versions search regexp:" ), modifiedBox ); layout->addWidget(modifiedFileComponentLabel); m_modifiedFileComponent = new QLineEdit(modifiedBox); layout->addWidget(m_modifiedFileComponent); QLabel* originalFileComponentLabel = new QLabel( i18n("Original file replacement text:" ), modifiedBox ); layout->addWidget(originalFileComponentLabel); m_originalFileComponent = new QLineEdit(modifiedBox); layout->addWidget(m_originalFileComponent); - m_moveOriginalContents = new QCheckBox(i18n("Move meta-data (i.e. delete tags from the original):"), modifiedBox); + m_moveOriginalContents = new QCheckBox(i18n("Move meta-data (i.e. delete tags from the original)"), modifiedBox); layout->addWidget(m_moveOriginalContents); m_autoStackNewFiles = new QCheckBox(i18n("Automatically stack new versions of images"), modifiedBox); layout->addWidget(m_autoStackNewFiles); txt = i18n( "

    When KPhotoAlbum searches for new files and finds a file that matches the " "modified file search regexp it is assumed that an original version of " "the image may exist. The regexp pattern will be replaced with the original " "file replacement text and if that file exists, all associated metadata (category " "information, ratings, etc) will be copied or moved from the original file to the new one.

    "); m_detectModifiedFiles->setWhatsThis( txt ); txt = i18n( "

    A perl regular expression that should match a modified file. " "

    • A dot matches a single character (\\. matches a dot)
    • " "
    • You can use the quantifiers *,+,?, or you can " " match multiple occurrences of an expression by using curly brackets (e.g. " "e{0,1} matches 0 or 1 occurrences of the character \"e\").
    • " "
    • You can group parts of the expression using parenthesis.
    • " "
    Example: -modified\\.(jpg|tiff)

    "); modifiedFileComponentLabel->setWhatsThis( txt ); m_modifiedFileComponent->setWhatsThis( txt ); txt = i18n( "

    A string that is used to replace the match from the File versions search regexp. " "This can be a semicolon (;) separated list. Each string is used to replace the match " "in the new file's name until an original file is found or we run out of options.

    "); originalFileComponentLabel->setWhatsThis( txt ); m_originalFileComponent->setWhatsThis( txt ); txt = i18n( "

    The tagging is moved from the original file to the new file. This way " "only the latest version of an image is tagged.

    " ); m_moveOriginalContents->setWhatsThis( txt ); txt = i18n( "

    If this option is set, new versions of an image are automatically stacked " "and placed to the top of the stack. This way the new image is shown when the " "stack is in collapsed state - the default state in KPhotoAlbum.

    " ); m_autoStackNewFiles->setWhatsThis( txt ); } // Copy File Support { QGroupBox* copyBox = new QGroupBox( i18nc("Configure the feature to make a copy of a file first and then open the copied file with an external application", "Copy File and Open with an External Application"), this ); topLayout->addWidget( copyBox ); QVBoxLayout* layout = new QVBoxLayout(copyBox); QLabel* copyFileComponentLabel = new QLabel( i18n("Copy file search regexp:" ), copyBox ); layout->addWidget(copyFileComponentLabel); m_copyFileComponent = new QLineEdit(copyBox); layout->addWidget(m_copyFileComponent); QLabel* copyFileReplacementComponentLabel = new QLabel( i18n("Copy file replacement text:" ), copyBox ); layout->addWidget(copyFileReplacementComponentLabel); m_copyFileReplacementComponent = new QLineEdit(copyBox); layout->addWidget(m_copyFileReplacementComponent); txt = i18n("

    KPhotoAlbum can make a copy of an image before opening it with an external application. This configuration defines how the new file is named.

    " "

    The regular expression defines the part of the original file name that is replaced with the replacement text. " "E.g. regexp \"\\.(jpg|png)\" and replacement text \"-mod.\\1\" would copy test.jpg to test-mod.jpg and open the new file in selected application.

    "); copyFileComponentLabel->setWhatsThis( txt ); m_copyFileComponent->setWhatsThis( txt ); copyFileReplacementComponentLabel->setWhatsThis( txt ); m_copyFileReplacementComponent->setWhatsThis( txt ); } } Settings::FileVersionDetectionPage::~FileVersionDetectionPage() { delete m_searchForImagesOnStart; delete m_ignoreFileExtension; delete m_skipSymlinks; delete m_skipRawIfOtherMatches; delete m_excludeDirectories; delete m_detectModifiedFiles; delete m_modifiedFileComponent; delete m_originalFileComponent; delete m_moveOriginalContents; delete m_autoStackNewFiles; delete m_copyFileComponent; delete m_copyFileReplacementComponent; } void Settings::FileVersionDetectionPage::loadSettings( Settings::SettingsData* opt ) { m_searchForImagesOnStart->setChecked( opt->searchForImagesOnStart() ); m_ignoreFileExtension->setChecked( opt->ignoreFileExtension() ); m_skipSymlinks->setChecked( opt->skipSymlinks() ); m_skipRawIfOtherMatches->setChecked( opt->skipRawIfOtherMatches() ); m_excludeDirectories->setText( opt->excludeDirectories() ); m_detectModifiedFiles->setChecked( opt->detectModifiedFiles() ); m_modifiedFileComponent->setText( opt->modifiedFileComponent() ); m_originalFileComponent->setText( opt->originalFileComponent() ); m_moveOriginalContents->setChecked( opt->moveOriginalContents() ); m_autoStackNewFiles->setChecked( opt->autoStackNewFiles() ); m_copyFileComponent->setText( opt->copyFileComponent() ); m_copyFileReplacementComponent->setText( opt->copyFileReplacementComponent() ); } void Settings::FileVersionDetectionPage::saveSettings( Settings::SettingsData* opt ) { opt->setSearchForImagesOnStart( m_searchForImagesOnStart->isChecked() ); opt->setIgnoreFileExtension( m_ignoreFileExtension->isChecked() ); opt->setSkipSymlinks( m_skipSymlinks->isChecked() ); opt->setSkipRawIfOtherMatches( m_skipRawIfOtherMatches->isChecked() ); opt->setExcludeDirectories( m_excludeDirectories->text() ); opt->setDetectModifiedFiles( m_detectModifiedFiles->isChecked() ); opt->setModifiedFileComponent( m_modifiedFileComponent->text() ); opt->setOriginalFileComponent( m_originalFileComponent->text() ); opt->setAutoStackNewFiles( m_autoStackNewFiles->isChecked() ); opt->setCopyFileComponent( m_copyFileComponent->text() ); opt->setCopyFileReplacementComponent( m_copyFileReplacementComponent->text() ); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/GeneralPage.cpp b/Settings/GeneralPage.cpp index d9009bd8..791a21c7 100644 --- a/Settings/GeneralPage.cpp +++ b/Settings/GeneralPage.cpp @@ -1,306 +1,306 @@ /* 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 "GeneralPage.h" #include #include #include #include #include #include #include #include #include #include "DB/CategoryCollection.h" #include "SettingsData.h" #include "MainWindow/Window.h" #include #include Settings::GeneralPage::GeneralPage( QWidget* parent ) : QWidget( parent ) { QVBoxLayout* lay1 = new QVBoxLayout( this ); QGroupBox* box = new QGroupBox( i18n( "Loading New Images" ), this ); lay1->addWidget( box ); QGridLayout* lay = new QGridLayout( box ); lay->setSpacing( 6 ); int row = 0; // Thrust time stamps QLabel* timeStampLabel = new QLabel( i18n("Trust image dates:"), box ); m_trustTimeStamps = new KComboBox( box ); m_trustTimeStamps->addItems( QStringList() << i18nc("As in 'always trust image dates'","Always") << i18nc("As in 'ask whether to trust image dates'","Ask") << i18nc("As in 'never trust image dates'","Never") ); timeStampLabel->setBuddy( m_trustTimeStamps ); lay->addWidget( timeStampLabel, row, 0 ); lay->addWidget( m_trustTimeStamps, row, 1, 1, 3 ); - // Do EXIF rotate + // Do Exif rotate row++; - m_useEXIFRotate = new QCheckBox( i18n( "Use EXIF orientation information" ), box ); + m_useEXIFRotate = new QCheckBox( i18n( "Use Exif orientation information" ), box ); lay->addWidget( m_useEXIFRotate, row, 0, 1, 4 ); - // Use EXIF description + // Use Exif description row++; - m_useEXIFComments = new QCheckBox( i18n( "Use EXIF description" ), box ); + m_useEXIFComments = new QCheckBox( i18n( "Use Exif description" ), box ); lay->addWidget( m_useEXIFComments, row, 0, 1, 4 ); connect(m_useEXIFComments, &QCheckBox::stateChanged, this, &GeneralPage::useEXIFCommentsChanged); m_stripEXIFComments = new QCheckBox(i18n("Strip out camera generated default descriptions"), box); connect(m_stripEXIFComments, &QCheckBox::stateChanged, this, &GeneralPage::stripEXIFCommentsChanged); lay->addWidget(m_stripEXIFComments, row, 1, 1, 4); row++; m_commentsToStrip = new QTextEdit(); m_commentsToStrip->setMaximumHeight(60); m_commentsToStrip->setEnabled(false); lay->addWidget(m_commentsToStrip, row, 1, 1, 4); // Use embedded thumbnail row++; m_useRawThumbnail = new QCheckBox( i18n("Use the embedded thumbnail in RAW file or halfsized RAW"), box ); lay->addWidget( m_useRawThumbnail, row, 0 ); row++; QLabel* label = new QLabel( i18n("Required size for the thumbnail:"), box ); m_useRawThumbnailWidth = new QSpinBox( box ); m_useRawThumbnailWidth->setRange( 100, 5000 ); m_useRawThumbnailWidth->setSingleStep( 64 ); lay->addWidget( label, row, 0 ); lay->addWidget( m_useRawThumbnailWidth, row, 1 ); label = new QLabel( QString::fromLatin1("x"), box ); m_useRawThumbnailHeight = new QSpinBox( box ); m_useRawThumbnailHeight->setRange( 100, 5000 ); m_useRawThumbnailHeight->setSingleStep( 64 ); lay->addWidget( label, row, 2 ); lay->addWidget( m_useRawThumbnailHeight, row, 3 ); box = new QGroupBox( i18n( "Histogram" ), this ); lay1->addWidget( box ); lay = new QGridLayout( box ); lay->setSpacing( 6 ); row = 0; m_showHistogram = new QCheckBox( i18n("Show histogram:"), box); lay->addWidget( m_showHistogram, row, 0 ); connect(m_showHistogram, &QCheckBox::stateChanged, this, &GeneralPage::showHistogramChanged); row++; label = new QLabel( i18n("Size of histogram columns in date bar:"), box ); m_barWidth = new QSpinBox; m_barWidth->setRange( 1, 100 ); m_barWidth->setSingleStep( 1 ); lay->addWidget( label, row, 0 ); lay->addWidget( m_barWidth, row, 1 ); label = new QLabel( QString::fromLatin1("x"), box ); m_barHeight = new QSpinBox; m_barHeight->setRange( 15, 100 ); lay->addWidget( label, row, 2 ); lay->addWidget( m_barHeight, row, 3 ); box = new QGroupBox( i18n( "Miscellaneous" ), this ); lay1->addWidget( box ); lay = new QGridLayout( box ); lay->setSpacing( 6 ); row = 0; // Show splash screen m_showSplashScreen = new QCheckBox( i18n("Show splash screen"), box ); lay->addWidget( m_showSplashScreen, row, 0 ); // Album Category row++; QLabel* albumCategoryLabel = new QLabel( i18n("Category for virtual albums:" ), box ); m_albumCategory = new QComboBox; lay->addWidget( albumCategoryLabel, row, 0 ); lay->addWidget( m_albumCategory, row, 1 ); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH( const DB::CategoryPtr category, categories ) { m_albumCategory->addItem( category->name() ); } m_listenForAndroidDevicesOnStartup = new QCheckBox(i18n("Listen for Android devices on startup")); lay->addWidget(m_listenForAndroidDevicesOnStartup); lay1->addStretch( 1 ); // Whats This QString txt; - txt = i18n( "

    KPhotoAlbum will try to read the image date from EXIF information in the image. " + txt = i18n( "

    KPhotoAlbum will try to read the image date from Exif information in the image. " "If that fails it will try to get the date from the file's time stamp.

    " "

    However, this information will be wrong if the image was scanned in (you want the date the image " "was taken, not the date of the scan).

    " "

    If you only scan images, in contrast to sometimes using " "a digital camera, you should reply no. If you never scan images, you should reply yes, " "otherwise reply ask. This will allow you to decide whether the images are from " "the scanner or the camera, from session to session.

    " ); timeStampLabel->setWhatsThis( txt ); m_trustTimeStamps->setWhatsThis( txt ); txt = i18n( "

    JPEG images may contain information about rotation. " "If you have a reason for not using this information to get a default rotation of " "your images, uncheck this check box.

    " "

    Note: Your digital camera may not write this information into the images at all.

    " ); m_useEXIFRotate->setWhatsThis( txt ); txt = i18n( "

    JPEG images may contain a description. " "Check this checkbox to specify if you want to use this as a " "default description for your images.

    " ); m_useEXIFComments->setWhatsThis( txt ); txt = i18n("

    KPhotoAlbum shares plugins with other imaging applications, some of which have the concept of albums. " "KPhotoAlbum does not have this concept; nevertheless, for certain plugins to function, KPhotoAlbum behaves " "to the plugin system as if it did.

    " "

    KPhotoAlbum does this by defining the current album to be the current view - that is, all the images the " "browser offers to display.

    " "

    In addition to the current album, KPhotoAlbum must also be able to give a list of all albums; " "the list of all albums is defined in the following way:" "

    • When KPhotoAlbum's browser displays the content of a category, say all People, then each item in this category " "will look like an album to the plugin.
    • " "
    • Otherwise, the category you specify using this option will be used; e.g. if you specify People " "with this option, then KPhotoAlbum will act as if you had just chosen to display people and then invoke " "the plugin which needs to know about all albums.

    " "

    Most users would probably want to specify Events here.

    "); albumCategoryLabel->setWhatsThis( txt ); m_albumCategory->setWhatsThis( txt ); txt = i18n( "Show the KPhotoAlbum splash screen on start up" ); m_showSplashScreen->setWhatsThis( txt ); txt = i18n("

    KPhotoAlbum is capable of showing your images on android devices. KPhotoAlbum will automatically pair with the app from " "android. This, however, requires that KPhotoAlbum on your desktop is listening for multicast messages. " "Checking this checkbox will make KPhotoAlbum do so automatically on start up. " "Alternatively, you can click the connection icon in the status bar to start listening."); m_listenForAndroidDevicesOnStartup->setWhatsThis(txt); txt = i18n("

    Some cameras automatically store generic comments in each image. " "These comments can be ignored automatically.

    " "

    Enter the comments that you want to ignore in the input field, one per line. " "Be sure to add the exact comment, including all whitespace.

    "); m_stripEXIFComments->setWhatsThis(txt); m_commentsToStrip->setWhatsThis(txt); } void Settings::GeneralPage::loadSettings( Settings::SettingsData* opt ) { m_trustTimeStamps->setCurrentIndex( opt->tTimeStamps() ); m_useEXIFRotate->setChecked( opt->useEXIFRotate() ); m_useEXIFComments->setChecked( opt->useEXIFComments() ); m_stripEXIFComments->setChecked( opt->stripEXIFComments() ); m_stripEXIFComments->setEnabled( opt->useEXIFComments() ); QStringList commentsToStrip = opt->EXIFCommentsToStrip(); QString commentsToStripStr; for (int i = 0; i < commentsToStrip.size(); ++i) { if (commentsToStripStr.size() > 0) { commentsToStripStr += QString::fromLatin1("\n"); } commentsToStripStr += commentsToStrip.at(i); } m_commentsToStrip->setPlainText(commentsToStripStr); m_commentsToStrip->setEnabled( opt->stripEXIFComments() ); m_useRawThumbnail->setChecked( opt->useRawThumbnail() ); setUseRawThumbnailSize(QSize(opt->useRawThumbnailSize().width(), opt->useRawThumbnailSize().height())); m_barWidth->setValue( opt->histogramSize().width() ); m_barHeight->setValue( opt->histogramSize().height() ); m_showHistogram->setChecked( opt->showHistogram() ); m_showSplashScreen->setChecked( opt->showSplashScreen() ); m_listenForAndroidDevicesOnStartup->setChecked(opt->listenForAndroidDevicesOnStartup()); DB::CategoryPtr cat = DB::ImageDB::instance()->categoryCollection()->categoryForName( opt->albumCategory() ); if ( !cat ) cat = DB::ImageDB::instance()->categoryCollection()->categories()[0]; m_albumCategory->setEditText(cat->name()); } void Settings::GeneralPage::saveSettings( Settings::SettingsData* opt ) { opt->setTTimeStamps( (TimeStampTrust) m_trustTimeStamps->currentIndex() ); opt->setUseEXIFRotate( m_useEXIFRotate->isChecked() ); opt->setUseEXIFComments( m_useEXIFComments->isChecked() ); opt->setStripEXIFComments(m_stripEXIFComments->isChecked()); QStringList commentsToStrip = m_commentsToStrip->toPlainText().split(QString::fromLatin1("\n")); // Put the processable list to opt opt->setEXIFCommentsToStrip(commentsToStrip); QString commentsToStripString; for ( QString comment : commentsToStrip ) { // separate comments with "-,-" and escape existing commas by doubling if ( !comment.isEmpty() ) commentsToStripString += comment.replace( QString::fromLatin1(","), QString::fromLatin1(",,") ) + QString::fromLatin1("-,-"); } // Put the storable list to opt opt->setCommentsToStrip(commentsToStripString); opt->setUseRawThumbnail( m_useRawThumbnail->isChecked() ); opt->setUseRawThumbnailSize(QSize(useRawThumbnailSize())); opt->setShowHistogram( m_showHistogram->isChecked() ); opt->setShowSplashScreen( m_showSplashScreen->isChecked() ); opt->setListenForAndroidDevicesOnStartup(m_listenForAndroidDevicesOnStartup->isChecked()); QString name = m_albumCategory->currentText(); if (name.isNull()) { name = DB::ImageDB::instance()->categoryCollection()->categoryNames()[0]; } opt->setAlbumCategory(name); opt->setHistogramSize(QSize(m_barWidth->value(), m_barHeight->value())); } void Settings::GeneralPage::setUseRawThumbnailSize( const QSize& size ) { m_useRawThumbnailWidth->setValue( size.width() ); m_useRawThumbnailHeight->setValue( size.height() ); } QSize Settings::GeneralPage::useRawThumbnailSize() { return QSize( m_useRawThumbnailWidth->value(), m_useRawThumbnailHeight->value() ); } void Settings::GeneralPage::showHistogramChanged( int state ) const { MainWindow::Window::theMainWindow()->setHistogramVisibilty( state == Qt::Checked ); } void Settings::GeneralPage::useEXIFCommentsChanged(int state) { m_stripEXIFComments->setEnabled(state); m_commentsToStrip->setEnabled(state && m_stripEXIFComments->isChecked() ); } void Settings::GeneralPage::stripEXIFCommentsChanged(int state) { m_commentsToStrip->setEnabled(state); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/SettingsData.cpp b/Settings/SettingsData.cpp index 840826f5..e9ba3347 100644 --- a/Settings/SettingsData.cpp +++ b/Settings/SettingsData.cpp @@ -1,549 +1,549 @@ /* 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 "SettingsData.h" #include #include #include #include #include #include #include #include #include #include "DB/CategoryCollection.h" #include "DB/ImageDB.h" #define STR(x) QString::fromLatin1(x) #define value( GROUP, OPTION, DEFAULT ) \ KSharedConfig::openConfig()->group( GROUP ).readEntry( OPTION, DEFAULT ) \ #define setValue( GROUP, OPTION, VALUE ) \ { \ KConfigGroup group = KSharedConfig::openConfig()->group( GROUP ); \ group.writeEntry( OPTION, VALUE ); \ group.sync(); \ } #define getValueFunc_( TYPE,FUNC, GROUP,OPTION,DEFAULT ) \ TYPE SettingsData::FUNC() const \ { return (TYPE) value( GROUP, OPTION, DEFAULT ); } #define setValueFunc_( FUNC,TYPE, GROUP,OPTION,VALUE ) \ void SettingsData::FUNC( const TYPE v ) \ { setValue( GROUP, OPTION, VALUE ); } #define getValueFunc( TYPE,FUNC, GROUP,DEFAULT ) getValueFunc_( TYPE,FUNC, #GROUP,#FUNC,DEFAULT ) #define setValueFunc( FUNC,TYPE, GROUP,OPTION ) setValueFunc_( FUNC,TYPE, #GROUP,#OPTION,v ) // TODO(mfwitten): document parameters. #define property_( GET_TYPE,GET_FUNC,GET_VALUE, SET_FUNC,SET_TYPE,SET_VALUE, GROUP,OPTION,GET_DEFAULT_1,GET_DEFAULT_2,GET_DEFAULT_2_TYPE ) \ GET_TYPE SettingsData::GET_FUNC() const \ { \ KConfigGroup g = KSharedConfig::openConfig()->group(GROUP); \ \ if ( !g.hasKey(OPTION) ) \ return GET_DEFAULT_1; \ \ GET_DEFAULT_2_TYPE v = g.readEntry( OPTION, (GET_DEFAULT_2_TYPE)GET_DEFAULT_2 ); \ return (GET_TYPE) GET_VALUE; \ } \ setValueFunc_( SET_FUNC,SET_TYPE, GROUP,OPTION,SET_VALUE ) #define property( GET_TYPE,GET_FUNC, SET_FUNC,SET_TYPE,SET_VALUE, GROUP,OPTION,GET_DEFAULT ) \ getValueFunc_( GET_TYPE,GET_FUNC, GROUP,OPTION,GET_DEFAULT) \ setValueFunc_( SET_FUNC,SET_TYPE, GROUP,OPTION,SET_VALUE ) #define property_copy( GET_FUNC,SET_FUNC, TYPE,GROUP,GET_DEFAULT ) \ property( TYPE,GET_FUNC, SET_FUNC,TYPE,v, #GROUP,#GET_FUNC,GET_DEFAULT ) #define property_ref_( GET_FUNC,SET_FUNC, TYPE,GROUP,GET_DEFAULT ) \ property( TYPE,GET_FUNC, SET_FUNC,TYPE&,v, GROUP,#GET_FUNC,GET_DEFAULT ) #define property_ref( GET_FUNC,SET_FUNC, TYPE,GROUP,GET_DEFAULT ) \ property( TYPE,GET_FUNC, SET_FUNC,TYPE&,v, #GROUP,#GET_FUNC,GET_DEFAULT ) #define property_enum( GET_FUNC,SET_FUNC, TYPE,GROUP,GET_DEFAULT ) \ property( TYPE,GET_FUNC, SET_FUNC,TYPE,(int)v, #GROUP,#GET_FUNC,(int)GET_DEFAULT ) #define property_sset( GET_FUNC,SET_FUNC, GROUP,GET_DEFAULT ) \ property_( StringSet,GET_FUNC,v.toSet(), SET_FUNC,StringSet&,v.toList(), #GROUP,#GET_FUNC,GET_DEFAULT,QStringList(),QStringList ) /** * smoothScale() is called from the image loading thread, therefore we need * to cache it this way, rather than going to KConfig. */ static bool _smoothScale = true; using namespace Settings; const WindowType Settings::MainWindow = "MainWindow"; const WindowType Settings::AnnotationDialog = "AnnotationDialog"; SettingsData* SettingsData::s_instance = nullptr; SettingsData* SettingsData::instance() { if ( ! s_instance ) qFatal("instance called before loading a setup!"); return s_instance; } bool SettingsData::ready() { return s_instance; } void SettingsData::setup( const QString& imageDirectory ) { if ( !s_instance ) s_instance = new SettingsData( imageDirectory ); } SettingsData::SettingsData( const QString& imageDirectory ) { m_hasAskedAboutTimeStamps = false; QString s = STR( "/" ); m_imageDirectory = imageDirectory.endsWith(s) ? imageDirectory : imageDirectory + s; _smoothScale = value( "Viewer", "smoothScale", true ); - // Split the list of EXIF comments that should be stripped automatically to a list + // Split the list of Exif comments that should be stripped automatically to a list QStringList commentsToStrip = value( "General", "commentsToStrip", QString::fromLatin1("") ).split(QString::fromLatin1("-,-"), QString::SkipEmptyParts ); for (QString &comment : commentsToStrip ) comment.replace( QString::fromLatin1(",,"), QString::fromLatin1(",") ); m_EXIFCommentsToStrip = commentsToStrip; } ///////////////// //// General //// ///////////////// property_copy( useEXIFRotate , setUseEXIFRotate , bool , General, true ) property_copy( useEXIFComments , setUseEXIFComments , bool , General, true ) property_copy( stripEXIFComments , setStripEXIFComments , bool , General, false ) property_copy( commentsToStrip , setCommentsToStrip , QString , General, QString::fromLatin1("") ) property_copy( searchForImagesOnStart, setSearchForImagesOnStart, bool , General, true ) property_copy( ignoreFileExtension , setIgnoreFileExtension , bool , General, false ) property_copy( skipSymlinks, setSkipSymlinks , bool , General, false ) property_copy( skipRawIfOtherMatches , setSkipRawIfOtherMatches , bool , General, false ) property_copy( useRawThumbnail , setUseRawThumbnail , bool , General, true ) property_copy( useRawThumbnailSize , setUseRawThumbnailSize , QSize , General, QSize(1024,768) ) property_copy( useCompressedIndexXML , setUseCompressedIndexXML , bool , General, true ) property_copy( compressBackup , setCompressBackup , bool , General, true ) property_copy( showSplashScreen , setShowSplashScreen , bool , General, true ) property_copy( showHistogram , setShowHistogram , bool , General, true ) property_copy( autoSave , setAutoSave , int , General, 5 ) property_copy( backupCount , setBackupCount , int , General, 5 ) property_enum( tTimeStamps , setTTimeStamps , TimeStampTrust, General, Always ) property_copy( excludeDirectories , setExcludeDirectories , QString , General, QString::fromLatin1("xml,ThumbNails,.thumbs") ) property_copy( recentAndroidAddress , setRecentAndroidAddress , QString , General, QString() ) property_copy( listenForAndroidDevicesOnStartup, setListenForAndroidDevicesOnStartup, bool, General, false) getValueFunc( QSize,histogramSize, General,QSize(15,30) ) getValueFunc( ViewSortType,viewSortType, General,(int)SortLastUse ) getValueFunc( AnnotationDialog::MatchType, matchType, General,(int)AnnotationDialog::MatchFromWordStart ) void SettingsData::setHistogramSize( const QSize& size ) { if ( size == histogramSize() ) return; setValue( "General", "histogramSize", size ); emit histogramSizeChanged( size ); } void SettingsData::setViewSortType( const ViewSortType tp ) { if ( tp == viewSortType() ) return; setValue( "General", "viewSortType", (int)tp ); emit viewSortTypeChanged( tp ); } void SettingsData::setMatchType( const AnnotationDialog::MatchType mt ) { if ( mt == matchType() ) return; setValue( "General", "matchType", (int)mt ); emit matchTypeChanged( mt ); } bool SettingsData::trustTimeStamps() { if ( tTimeStamps() == Always ) return true; else if ( tTimeStamps() == Never ) return false; else { if (!m_hasAskedAboutTimeStamps ) { QApplication::setOverrideCursor( Qt::ArrowCursor ); - QString txt = i18n("When reading time information of images, their EXIF info is used. " + QString txt = i18n("When reading time information of images, their Exif info is used. " "Exif info may, however, not be supported by your KPhotoAlbum installation, " "or no valid information may be in the file. " "As a backup, KPhotoAlbum may use the timestamp of the image - this may, " "however, not be valid in case the image is scanned in. " "So the question is, should KPhotoAlbum trust the time stamp on your images?" ); int answer = KMessageBox::questionYesNo( nullptr, txt, i18n("Trust Time Stamps?") ); QApplication::restoreOverrideCursor(); if ( answer == KMessageBox::Yes ) m_trustTimeStamps = true; else m_trustTimeStamps = false; m_hasAskedAboutTimeStamps = true; } return m_trustTimeStamps; } } //////////////////////////////// //// File Version Detection //// //////////////////////////////// property_copy( detectModifiedFiles , setDetectModifiedFiles , bool , FileVersionDetection, true ) property_copy( modifiedFileComponent , setModifiedFileComponent , QString , FileVersionDetection, "^(.*)-edited.([^.]+)$") property_copy( originalFileComponent , setOriginalFileComponent , QString , FileVersionDetection, "\1.\2" ) property_copy( moveOriginalContents , setMoveOriginalContents , bool , FileVersionDetection, false ) property_copy( autoStackNewFiles , setAutoStackNewFiles , bool , FileVersionDetection, true ) property_copy( copyFileComponent , setCopyFileComponent , QString , FileVersionDetection, "(.[^.]+)$" ) property_copy( copyFileReplacementComponent , setCopyFileReplacementComponent , QString , FileVersionDetection, "-edited\\1") //////////////////// //// Thumbnails //// //////////////////// property_copy( displayLabels , setDisplayLabels , bool , Thumbnails, true ) property_copy( displayCategories , setDisplayCategories , bool , Thumbnails, false ) property_copy( autoShowThumbnailView , setAutoShowThumbnailView , int , Thumbnails, 20 ) property_copy( showNewestThumbnailFirst, setShowNewestFirst , bool , Thumbnails, false ) property_copy( thumbnailDisplayGrid , setThumbnailDisplayGrid , bool , Thumbnails, false ) property_copy( previewSize , setPreviewSize , int , Thumbnails, 256 ) property_copy( thumbnailSpace , setThumbnailSpace , int , Thumbnails, 4 ) // not available via GUI, but should be consistent (and maybe confgurable for powerusers): property_copy( minimumThumbnailSize , setMinimumThumbnailSize , int , Thumbnails, 32 ) property_copy( maximumThumbnailSize , setMaximumThumbnailSize , int , Thumbnails, 4096 ) property_enum( thumbnailAspectRatio , setThumbnailAspectRatio , ThumbnailAspectRatio, Thumbnails, Aspect_3_2 ) property_ref( backgroundColor , setBackgroundColor , QString , Thumbnails, QColor(Qt::darkGray).name() ) property_copy( incrementalThumbnails , setIncrementalThumbnails , bool , Thumbnails, true ) // database specific so that changing it doesn't invalidate the thumbnail cache for other databases: getValueFunc_( int, thumbnailSize, groupForDatabase("Thumbnails"), "thumbSize", 256) void SettingsData::setThumbnailSize( int value ) { // enforce limits: value = qBound( minimumThumbnailSize(), value, maximumThumbnailSize()); if ( value != thumbnailSize() ) emit thumbnailSizeChanged(value); setValue( groupForDatabase("Thumbnails"), "thumbSize", value ); } int SettingsData::actualThumbnailSize() const \ { // this is database specific since it's a derived value of thumbnailSize int retval = value( groupForDatabase("Thumbnails"), "actualThumbSize", 0 ); // if no value has been set, use thumbnailSize if ( retval == 0 ) retval = thumbnailSize(); return retval; } void SettingsData::setActualThumbnailSize( int value ) { QPixmapCache::clear(); // enforce limits: value = qBound( minimumThumbnailSize(), value, thumbnailSize()); if ( value != actualThumbnailSize()) { setValue( groupForDatabase("Thumbnails"), "actualThumbSize", value ); emit actualThumbnailSizeChanged(value); } } //////////////// //// Viewer //// //////////////// property_ref ( viewerSize , setViewerSize , QSize , Viewer, QSize(1024,768) ) property_ref ( slideShowSize , setSlideShowSize , QSize , Viewer, QSize(1024,768) ) property_copy( launchViewerFullScreen , setLaunchViewerFullScreen , bool , Viewer, false ) property_copy( launchSlideShowFullScreen, setLaunchSlideShowFullScreen, bool , Viewer, true ) property_copy( showInfoBox , setShowInfoBox , bool , Viewer, true ) property_copy( showLabel , setShowLabel , bool , Viewer, true ) property_copy( showDescription , setShowDescription , bool , Viewer, true ) property_copy( showDate , setShowDate , bool , Viewer, true ) property_copy( showImageSize , setShowImageSize , bool , Viewer, true ) property_copy( showRating , setShowRating , bool , Viewer, true ) property_copy( showTime , setShowTime , bool , Viewer, true ) property_copy( showFilename , setShowFilename , bool , Viewer, false ) property_copy( showEXIF , setShowEXIF , bool , Viewer, true ) property_copy( slideShowInterval , setSlideShowInterval , int , Viewer, 5 ) property_copy( viewerCacheSize , setViewerCacheSize , int , Viewer, 195 ) property_copy( infoBoxWidth , setInfoBoxWidth , int , Viewer, 400 ) property_copy( infoBoxHeight , setInfoBoxHeight , int , Viewer, 300 ) property_enum( infoBoxPosition , setInfoBoxPosition , Position , Viewer, Bottom ) property_enum( viewerStandardSize , setViewerStandardSize , StandardViewSize, Viewer, FullSize ) bool SettingsData::smoothScale() const { return _smoothScale; } void SettingsData::setSmoothScale( bool b ) { _smoothScale = b; setValue( "Viewer", "smoothScale", b ); } //////////////////// //// Categories //// //////////////////// setValueFunc( setAlbumCategory,QString&, General,albumCategory ) QString SettingsData::albumCategory() const { QString category = value( "General", "albumCategory", STR("") ); if ( !DB::ImageDB::instance()->categoryCollection()->categoryNames().contains( category ) ) { category = DB::ImageDB::instance()->categoryCollection()->categoryNames()[0]; const_cast(this)->setAlbumCategory( category ); } return category; } property_ref( untaggedCategory, setUntaggedCategory, QString, General, i18n("Events")) property_ref( untaggedTag, setUntaggedTag, QString, General, i18n("untagged")) property_copy( untaggedImagesTagVisible, setUntaggedImagesTagVisible, bool, General, false) ////////////// //// Exif //// ////////////// property_sset( exifForViewer, setExifForViewer, Exif, StringSet() ) property_sset( exifForDialog, setExifForDialog, Exif, Exif::Info::instance()->standardKeys() ) property_ref ( iptcCharset , setIptcCharset , QString, Exif, QString() ) ///////////////////// //// Exif Import //// ///////////////////// property_copy( updateExifData , setUpdateExifData , bool , ExifImport, true ) property_copy( updateImageDate , setUpdateImageDate , bool , ExifImport, false ) property_copy( useModDateIfNoExif , setUseModDateIfNoExif , bool , ExifImport, true ) property_copy( updateOrientation , setUpdateOrientation , bool , ExifImport, false ) property_copy( updateDescription , setUpdateDescription , bool , ExifImport, false ) /////////////////////// //// Miscellaneous //// /////////////////////// property_copy( delayLoadingPlugins, setDelayLoadingPlugins, bool, Plug-ins, true ) property_ref_( HTMLBaseDir, setHTMLBaseDir, QString, groupForDatabase( "HTML Settings" ), QString::fromLocal8Bit(qgetenv( "HOME" )) + STR( "/public_html" ) ) property_ref_( HTMLBaseURL, setHTMLBaseURL, QString, groupForDatabase( "HTML Settings" ), STR( "file://" ) + HTMLBaseDir() ) property_ref_( HTMLDestURL, setHTMLDestURL, QString, groupForDatabase( "HTML Settings" ), STR( "file://" ) + HTMLBaseDir() ) property_ref_( HTMLCopyright, setHTMLCopyright, QString, groupForDatabase( "HTML Settings" ), STR( "" ) ) property_ref_( HTMLDate, setHTMLDate, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTMLTheme, setHTMLTheme, int, groupForDatabase( "HTML Settings" ), -1 ) property_ref_( HTMLKimFile, setHTMLKimFile, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTMLInlineMovies, setHTMLInlineMovies, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTML5Video, setHTML5Video, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTML5VideoGenerate, setHTML5VideoGenerate, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTMLThumbSize, setHTMLThumbSize, int, groupForDatabase( "HTML Settings" ), 128 ) property_ref_( HTMLNumOfCols, setHTMLNumOfCols, int, groupForDatabase( "HTML Settings" ), 5 ) property_ref_( HTMLSizes, setHTMLSizes, QString, groupForDatabase( "HTML Settings" ), STR("") ) property_ref_( HTMLIncludeSelections, setHTMLIncludeSelections, QString, groupForDatabase( "HTML Settings" ), STR("") ) property_ref_( password, setPassword, QString, groupForDatabase( "Privacy Settings" ), STR("") ) QDate SettingsData::fromDate() const { QString date = value( "Miscellaneous", "fromDate", STR("") ); return date.isEmpty() ? QDate( QDate::currentDate().year(), 1, 1 ) : QDate::fromString( date, Qt::ISODate ); } void SettingsData::setFromDate( const QDate& date) { if (date.isValid()) setValue( "Miscellaneous", "fromDate", date.toString( Qt::ISODate ) ); } QDate SettingsData::toDate() const { QString date = value( "Miscellaneous", "toDate", STR("") ); return date.isEmpty() ? QDate( QDate::currentDate().year()+1, 1, 1 ) : QDate::fromString( date, Qt::ISODate ); } void SettingsData::setToDate( const QDate& date) { if (date.isValid()) setValue( "Miscellaneous", "toDate", date.toString( Qt::ISODate ) ); } QString SettingsData::imageDirectory() const { return m_imageDirectory; } QString SettingsData::groupForDatabase( const char* setting ) const { return STR("%1 - %2").arg( STR(setting) ).arg( imageDirectory() ); } DB::ImageSearchInfo SettingsData::currentLock() const { return DB::ImageSearchInfo::loadLock(); } void SettingsData::setCurrentLock( const DB::ImageSearchInfo& info, bool exclude ) { info.saveLock(); setValue( groupForDatabase( "Privacy Settings" ), "exclude", exclude ); } bool SettingsData::lockExcludes() const { return value( groupForDatabase( "Privacy Settings" ), "exclude", false ); } getValueFunc_( bool,locked, groupForDatabase("Privacy Settings"),"locked",false ) void SettingsData::setLocked( bool lock, bool force ) { if ( lock == locked() && !force ) return; setValue( groupForDatabase( "Privacy Settings" ), "locked", lock ); emit locked( lock, lockExcludes() ); } void SettingsData::setWindowGeometry( WindowType win, const QRect& geometry ) { setValue( "Window Geometry", win, geometry ); } QRect SettingsData::windowGeometry( WindowType win ) const { return value( "Window Geometry", win, QRect(0,0,800,600) ); } bool Settings::SettingsData::hasUntaggedCategoryFeatureConfigured() const { return DB::ImageDB::instance()->categoryCollection()->categoryNames().contains( untaggedCategory() ) && DB::ImageDB::instance()->categoryCollection()->categoryForName( untaggedCategory())->items().contains( untaggedTag() ); } double Settings::SettingsData::getThumbnailAspectRatio() const { double ratio = 1.0; switch (Settings::SettingsData::instance()->thumbnailAspectRatio()) { case Settings::Aspect_16_9: ratio = 9.0 / 16; break; case Settings::Aspect_4_3: ratio = 3.0 / 4; break; case Settings::Aspect_3_2: ratio = 2.0 / 3; break; case Settings::Aspect_9_16: ratio = 16 / 9.0; break; case Settings::Aspect_3_4: ratio = 4 / 3.0; break; case Settings::Aspect_2_3: ratio = 3 / 2.0; break; case Settings::Aspect_1_1: ratio = 1.0; break; } return ratio; } QStringList Settings::SettingsData::EXIFCommentsToStrip() { return m_EXIFCommentsToStrip; } void Settings::SettingsData::setEXIFCommentsToStrip(QStringList EXIFCommentsToStrip) { m_EXIFCommentsToStrip = EXIFCommentsToStrip; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/SettingsDialog.cpp b/Settings/SettingsDialog.cpp index 6680fc31..2197ebdf 100644 --- a/Settings/SettingsDialog.cpp +++ b/Settings/SettingsDialog.cpp @@ -1,183 +1,183 @@ /* 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 "config-kpa-kipi.h" #include "SettingsDialog.h" #include #include #include #include #include #include "BirthdayPage.h" #include "CategoryPage.h" #include "DatabaseBackendPage.h" #include "ExifPage.h" #include "FileVersionDetectionPage.h" #include "GeneralPage.h" #include "PluginsPage.h" #include "TagGroupsPage.h" #include "ThumbnailsPage.h" #include "ViewerPage.h" #include struct Data { QString title; const char* icon; QWidget* widget; }; Settings::SettingsDialog::SettingsDialog( QWidget* parent) :KPageDialog( parent ) { m_generalPage = new Settings::GeneralPage(this); m_fileVersionDetectionPage = new Settings::FileVersionDetectionPage(this); m_thumbnailsPage = new Settings::ThumbnailsPage(this); m_categoryPage = new Settings::CategoryPage(this); m_tagGroupsPage = new Settings::TagGroupsPage(this); m_viewerPage = new Settings::ViewerPage(this); #ifdef HASKIPI m_pluginsPage = new Settings::PluginsPage(this); #endif m_exifPage = new Settings::ExifPage(this); m_birthdayPage = new Settings::BirthdayPage(this); m_databaseBackendPage = new Settings::DatabaseBackendPage(this); Data data[] = { { i18n("General"), "configure-shortcuts", m_generalPage }, { i18n("File Searching & Versions"), "system-search", m_fileVersionDetectionPage }, { i18n("Thumbnail View" ), "view-preview", m_thumbnailsPage }, { i18n("Categories"), "edit-group", m_categoryPage }, { i18n("Birthdays"), "view-calendar-birthday", m_birthdayPage }, { i18n("Tag Groups" ), "view-group", m_tagGroupsPage }, { i18n("Viewer" ), "document-preview", m_viewerPage }, #ifdef HASKIPI { i18n("Plugins" ), "plugins", m_pluginsPage }, #endif - { i18n("EXIF/IPTC Information" ), "document-properties", m_exifPage }, - { i18n("Database backend"), "document-save", m_databaseBackendPage }, + { i18n("Exif/IPTC Information" ), "document-properties", m_exifPage }, + { i18n("Database Backend"), "document-save", m_databaseBackendPage }, { QString(), "", 0 } }; int i = 0; while ( data[i].widget != 0 ) { KPageWidgetItem* page = new KPageWidgetItem( data[i].widget, data[i].title ); page->setHeader( data[i].title ); page->setIcon( QIcon::fromTheme( QString::fromLatin1( data[i].icon ) ) ); addPage( page ); ++i; } setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply); button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(this, &QDialog::accepted, this, &SettingsDialog::slotMyOK); connect(button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &SettingsDialog::slotMyOK); connect(this, &QDialog::rejected, m_birthdayPage, &Settings::BirthdayPage::discardChanges); - setWindowTitle( i18n( "Settings" ) ); + setWindowTitle( i18nc("@title:window", "Settings" ) ); connect(m_categoryPage, &Settings::CategoryPage::categoryChangesPending, m_tagGroupsPage, &Settings::TagGroupsPage::categoryChangesPending); connect(this, &SettingsDialog::currentPageChanged, m_tagGroupsPage, &Settings::TagGroupsPage::slotPageChange); connect(this, &SettingsDialog::currentPageChanged, m_birthdayPage, &Settings::BirthdayPage::pageChange); // slot is protected -> use old style connect: connect(this, SIGNAL(rejected()), m_categoryPage, SLOT(resetCategoryLabel())); } void Settings::SettingsDialog::show() { Settings::SettingsData* opt = Settings::SettingsData::instance(); m_generalPage->loadSettings( opt ); m_fileVersionDetectionPage->loadSettings( opt ); m_thumbnailsPage->loadSettings(opt); m_tagGroupsPage->loadSettings(); m_databaseBackendPage->loadSettings(opt); m_viewerPage->loadSettings(opt); #ifdef HASKIPI m_pluginsPage->loadSettings(opt); #endif m_categoryPage->loadSettings(opt); m_exifPage->loadSettings( opt ); m_categoryPage->enableDisable( false ); m_birthdayPage->reload(); m_categoryPage->resetCategoryNamesChanged(); QDialog::show(); } // QDialog has a slotOK which we do not want to override. void Settings::SettingsDialog::slotMyOK() { Utilities::ShowBusyCursor dummy; Settings::SettingsData* opt = Settings::SettingsData::instance(); m_categoryPage->resetInterface(); m_generalPage->saveSettings( opt ); m_fileVersionDetectionPage->saveSettings( opt ); m_thumbnailsPage->saveSettings(opt); m_birthdayPage->saveSettings(); m_tagGroupsPage->saveSettings(); m_categoryPage->saveSettings( opt, m_tagGroupsPage->memberMap() ); m_viewerPage->saveSettings( opt ); #ifdef HASKIPI m_pluginsPage->saveSettings( opt ); #endif m_exifPage->saveSettings(opt); m_databaseBackendPage->saveSettings(opt); emit changed(); KSharedConfig::openConfig()->sync(); } void Settings::SettingsDialog::showBackendPage() { setCurrentPage(m_backendPage); } void Settings::SettingsDialog::keyPressEvent(QKeyEvent*) { // This prevents the dialog to be closed if the ENTER key is pressed anywhere } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/ThumbnailsPage.cpp b/Settings/ThumbnailsPage.cpp index 77d2a704..2decba43 100644 --- a/Settings/ThumbnailsPage.cpp +++ b/Settings/ThumbnailsPage.cpp @@ -1,196 +1,196 @@ /* 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 "ThumbnailsPage.h" #include #include "SettingsData.h" #include #include #include #include #include #include Settings::ThumbnailsPage::ThumbnailsPage( QWidget* parent ) : QWidget( parent ) { QGridLayout* lay = new QGridLayout( this ); lay->setSpacing( 6 ); int row = 0; // Preview size QLabel* previewSizeLabel = new QLabel( i18n("Tooltip preview image size:" ) ); m_previewSize = new QSpinBox; m_previewSize->setRange( 0, 2000 ); m_previewSize->setSingleStep( 10 ); m_previewSize->setSpecialValueText( i18n("No Image Preview") ); lay->addWidget( previewSizeLabel, row, 0 ); lay->addWidget( m_previewSize, row, 1 ); // Thumbnail size ++row; QLabel* thumbnailSizeLabel = new QLabel( i18n("Thumbnail image size:" ) ); m_thumbnailSize = new QSpinBox; // range set from settings on load m_thumbnailSize->setSingleStep( 16 ); lay->addWidget( thumbnailSizeLabel, row, 0 ); lay->addWidget( m_thumbnailSize, row, 1 ); // incremental Thumbnail building ++row; m_incrementalThumbnails = new QCheckBox( i18n("Build thumbnails on demand" ) ); lay->addWidget( m_incrementalThumbnails, row, 0, 1, 2); // Thumbnail aspect ratio ++row; - QLabel* thumbnailAspectRatioLabel = new QLabel( i18n("Thumbnail table cells aspect ratio") ); + QLabel* thumbnailAspectRatioLabel = new QLabel( i18n("Thumbnail table cells aspect ratio:") ); m_thumbnailAspectRatio = new KComboBox( this ); m_thumbnailAspectRatio->addItems( QStringList() << i18n("1:1") << i18n("4:3") << i18n("3:2") << i18n("16:9") << i18n("3:4") << i18n("2:3") << i18n("9:16")); lay->addWidget( thumbnailAspectRatioLabel, row, 0 ); lay->addWidget( m_thumbnailAspectRatio, row, 1 ); // Space around cells ++row; - QLabel* thumbnailSpaceLabel = new QLabel( i18n("Space around cells") ); + QLabel* thumbnailSpaceLabel = new QLabel( i18n("Space around cells:") ); m_thumbnailSpace = new QSpinBox; m_thumbnailSpace->setRange( 0, 20 ); lay->addWidget( thumbnailSpaceLabel, row, 0 ); lay->addWidget( m_thumbnailSpace, row, 1 ); // Background color ++row; - QLabel* backgroundColorLabel = new QLabel( i18n("Background Color") ); + QLabel* backgroundColorLabel = new QLabel( i18n("Background color:") ); m_backgroundColor = new KColorButton; lay->addWidget( backgroundColorLabel, row, 0 ); lay->addWidget( m_backgroundColor, row, 1 ); // Display grid lines in the thumbnail view ++row; m_thumbnailDisplayGrid = new QCheckBox( i18n("Display grid around thumbnails" ) ); lay->addWidget( m_thumbnailDisplayGrid, row, 0, 1, 2 ); // Display Labels ++row; m_displayLabels = new QCheckBox( i18n("Display labels in thumbnail view" ) ); lay->addWidget( m_displayLabels, row, 0, 1, 2 ); // Display Categories ++row; m_displayCategories = new QCheckBox( i18n("Display categories in thumbnail view" ) ); lay->addWidget( m_displayCategories, row, 0, 1, 2 ); // Auto Show Thumbnail view ++row; QLabel* autoShowLabel = new QLabel( i18n("Threshold for automatic thumbnail view: "), this ); m_autoShowThumbnailView = new QSpinBox; m_autoShowThumbnailView->setRange( 0, 10000 ); m_autoShowThumbnailView->setSingleStep( 10 ); m_autoShowThumbnailView->setSpecialValueText( i18nc("Describing: 'ThumbnailView will not be automatically shown'","Disabled") ); lay->addWidget( autoShowLabel, row, 0 ); lay->addWidget( m_autoShowThumbnailView, row, 1 ); lay->setColumnStretch( 1, 1 ); lay->setRowStretch( ++row, 1 ); // Whats This QString txt; txt = i18n( "

    If you select Settings -> Show Tooltips in the thumbnail view, then you will see a small tool tip window " "displaying information about the thumbnails. This window includes a small preview image. " "This option configures the image size.

    " ); previewSizeLabel->setWhatsThis( txt ); m_previewSize->setWhatsThis( txt ); txt = i18n( "

    Thumbnail image size. Changing the thumbnail size here triggers a rebuild of the thumbnail database.

    " ); thumbnailSizeLabel->setWhatsThis( txt ); m_thumbnailSize->setWhatsThis( txt ); txt = i18n( "

    If this is set, thumbnails are built on demand. As you browse your image database, " "only those thumbnails that are needed are actually built. " "This means that when you change the thumbnail size, KPhotoAlbum will remain responsive " "even if you have lots of images.

    " "

    If this is not set, KPhotoAlbum will always build the thumbnails for all images as soon as possible. " "This means that when new images are found, KPhotoAlbum will immediately build thumbnails " "for them and you won't have a delay later while browsing.

    "); m_incrementalThumbnails->setWhatsThis( txt ); txt = i18n("

    Choose what aspect ratio the cells holding thumbnails should have.

    "); m_thumbnailAspectRatio->setWhatsThis( txt ); txt = i18n("

    How thick the cell padding should be.

    "); thumbnailSpaceLabel->setWhatsThis( txt ); txt = i18n("

    Background color to use in the thumbnail viewer

    "); backgroundColorLabel->setWhatsThis( txt ); m_backgroundColor->setWhatsThis( txt ); txt = i18n("

    If you want to see grid around your thumbnail images, " "select this option.

    "); m_thumbnailDisplayGrid->setWhatsThis( txt ); txt = i18n("

    Checking this option will show the base name for the file under " "thumbnails in the thumbnail view.

    "); m_displayLabels->setWhatsThis( txt ); txt = i18n("

    Checking this option will show the Categories for the file under " "thumbnails in the thumbnail view

    "); m_displayCategories->setWhatsThis( txt ); txt = i18n("

    When you are browsing, and the count gets below the value specified here, " "the thumbnails will be shown automatically. The alternative is to continue showing the " "browser until you press Show Images

    "); m_autoShowThumbnailView->setWhatsThis( txt ); autoShowLabel->setWhatsThis( txt ); } void Settings::ThumbnailsPage::loadSettings( Settings::SettingsData* opt ) { m_previewSize->setValue( opt->previewSize() ); m_thumbnailSize->setMinimum( opt->minimumThumbnailSize() ); m_thumbnailSize->setMaximum( opt->maximumThumbnailSize() ); m_thumbnailSize->setValue( opt->thumbnailSize() ); m_backgroundColor->setColor( QColor( opt->backgroundColor() ) ); m_thumbnailDisplayGrid->setChecked( opt->thumbnailDisplayGrid() ); m_thumbnailAspectRatio->setCurrentIndex( opt->thumbnailAspectRatio() ); m_thumbnailSpace->setValue( opt->thumbnailSpace() ); m_displayLabels->setChecked( opt->displayLabels() ); m_displayCategories->setChecked( opt->displayCategories() ); m_autoShowThumbnailView->setValue( opt->autoShowThumbnailView() ); m_incrementalThumbnails->setChecked( opt->incrementalThumbnails() ); } void Settings::ThumbnailsPage::saveSettings( Settings::SettingsData* opt ) { opt->setPreviewSize( m_previewSize->value() ); opt->setThumbnailSize( m_thumbnailSize->value() ); // ensure that the user actually sees the thumbnail size change: opt->setActualThumbnailSize( m_thumbnailSize->value() ); opt->setThumbnailAspectRatio( (ThumbnailAspectRatio) m_thumbnailAspectRatio->currentIndex() ); opt->setBackgroundColor( m_backgroundColor->color().name() ); opt->setThumbnailDisplayGrid( m_thumbnailDisplayGrid->isChecked() ); opt->setThumbnailSpace( m_thumbnailSpace->value() ); opt->setDisplayLabels( m_displayLabels->isChecked() ); opt->setDisplayCategories( m_displayCategories->isChecked() ); opt->setAutoShowThumbnailView( m_autoShowThumbnailView->value() ); opt->setIncrementalThumbnails( m_incrementalThumbnails->isChecked() ); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/ViewerPage.cpp b/Settings/ViewerPage.cpp index 5cbf11ff..de2ec8d4 100644 --- a/Settings/ViewerPage.cpp +++ b/Settings/ViewerPage.cpp @@ -1,113 +1,113 @@ /* 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 "ViewerPage.h" #include "SettingsData.h" #include #include #include #include #include #include "ViewerSizeConfig.h" #include Settings::ViewerPage::ViewerPage( QWidget* parent ) : QWidget( parent ) { QVBoxLayout* lay1 = new QVBoxLayout( this ); m_slideShowSetup = new ViewerSizeConfig( i18n( "Running Slide Show From Thumbnail View" ), this ); lay1->addWidget( m_slideShowSetup ); m_viewImageSetup = new ViewerSizeConfig( i18n( "Viewing Images and Videos From Thumbnail View" ), this ); lay1->addWidget( m_viewImageSetup ); QGridLayout* glay = new QGridLayout; lay1->addLayout( glay ); QLabel* label = new QLabel( i18n("Slideshow interval:" ), this ); glay->addWidget( label, 0, 0 ); m_slideShowInterval = new QSpinBox; m_slideShowInterval->setRange( 1, INT_MAX ); glay->addWidget( m_slideShowInterval, 0, 1 ); m_slideShowInterval->setSuffix( i18n( " sec" ) ); label->setBuddy( m_slideShowInterval ); label = new QLabel( i18n("Image cache:"), this ); glay->addWidget( label, 1, 0 ); m_cacheSize = new QSpinBox; m_cacheSize->setRange( 0, 16384 ); m_cacheSize->setSingleStep( 10 ); m_cacheSize->setSuffix( i18n(" Mbytes") ); glay->addWidget( m_cacheSize, 1, 1 ); label->setBuddy( m_cacheSize ); QString txt; QLabel* standardSizeLabel = new QLabel( i18n("Standard size in viewer:"), this ); m_viewerStandardSize = new KComboBox( this ); m_viewerStandardSize->addItems( QStringList() << i18n("Full Viewer Size") << i18n("Natural Image Size") << i18n("Natural Image Size If Possible") ); glay->addWidget( standardSizeLabel, 2, 0); glay->addWidget( m_viewerStandardSize, 2, 1 ); standardSizeLabel->setBuddy( m_viewerStandardSize ); txt = i18n("

    Set the standard size for images to be displayed in the viewer.

    " "

    Full Viewer Size indicates that the image will be stretched or shrunk to fill the viewer window.

    " "

    Natural Image Size indicates that the image will be displayed pixel for pixel.

    " "

    Natural Image Size If Possible indicates that the image will be displayed pixel for pixel if it would fit the window, " "otherwise it will be shrunk to fit the viewer.

    "); m_viewerStandardSize->setWhatsThis( txt); - QLabel* scalingLabel = new QLabel( i18n("Scaling Algorithm"), this ); + QLabel* scalingLabel = new QLabel( i18n("Scaling algorithm:"), this ); m_smoothScale = new KComboBox( this ); m_smoothScale->addItems( QStringList() << i18n("Fastest" ) << i18n("Best") ); scalingLabel->setBuddy( m_smoothScale ); glay->addWidget( scalingLabel, 3, 0 ); glay->addWidget( m_smoothScale, 3, 1 ); txt = i18n("

    When displaying images, KPhotoAlbum normally performs smooth scaling of the image. " "If this option is not set, KPhotoAlbum will use a faster but less smooth scaling method.

    "); scalingLabel->setWhatsThis( txt ); m_smoothScale->setWhatsThis( txt ); } void Settings::ViewerPage::loadSettings( Settings::SettingsData* opt ) { m_viewImageSetup->setLaunchFullScreen( opt->launchViewerFullScreen() ); m_viewImageSetup->setSize( opt->viewerSize() ); m_slideShowSetup->setLaunchFullScreen( opt->launchSlideShowFullScreen() ); m_slideShowSetup->setSize( opt->slideShowSize() ); m_slideShowInterval->setValue( opt->slideShowInterval() ); m_cacheSize->setValue( opt->viewerCacheSize() ); m_smoothScale->setCurrentIndex( opt->smoothScale() ); m_viewerStandardSize->setCurrentIndex( opt->viewerStandardSize() ); } void Settings::ViewerPage::saveSettings( Settings::SettingsData* opt ) { opt->setLaunchViewerFullScreen( m_viewImageSetup->launchFullScreen() ); opt->setViewerSize( m_viewImageSetup->size() ); opt->setSlideShowInterval( m_slideShowInterval->value() ); opt->setViewerCacheSize( m_cacheSize->value() ); opt->setSmoothScale( m_smoothScale->currentIndex() ); opt->setViewerStandardSize((StandardViewSize) m_viewerStandardSize->currentIndex()); opt->setSlideShowSize( m_slideShowSetup->size() ); opt->setLaunchSlideShowFullScreen( m_slideShowSetup->launchFullScreen() ); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewerWidget.cpp b/Viewer/ViewerWidget.cpp index a9474fb4..e70bd4d8 100644 --- a/Viewer/ViewerWidget.cpp +++ b/Viewer/ViewerWidget.cpp @@ -1,1531 +1,1531 @@ /* 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 "ViewerWidget.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 "CategoryImageConfig.h" #include "ImageDisplay.h" #include "InfoBox.h" #include "SpeedDisplay.h" #include "TaggedArea.h" #include "TextDisplay.h" #include "VideoDisplay.h" #include "VideoShooter.h" #include "VisibleOptionsMenu.h" Viewer::ViewerWidget* Viewer::ViewerWidget::s_latest = nullptr; Viewer::ViewerWidget* Viewer::ViewerWidget::latest() { return s_latest; } // Notice the parent is zero to allow other windows to come on top of it. Viewer::ViewerWidget::ViewerWidget( UsageType type, QMap > *macroStore ) :QStackedWidget( nullptr ) , m_current(0), m_popup(nullptr), m_showingFullScreen( false ), m_forward( true ) , m_isRunningSlideShow( false ), m_videoPlayerStoppedManually(false), m_type(type) , m_currentCategory(DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name()) , m_inputMacros(macroStore), m_myInputMacros(nullptr) { if ( type == ViewerWindow ) { setWindowFlags( Qt::Window ); setAttribute( Qt::WA_DeleteOnClose ); s_latest = this; } if (! m_inputMacros) { m_myInputMacros = m_inputMacros = new QMap >; } m_screenSaverCookie = -1; m_currentInputMode = InACategory; m_display = m_imageDisplay = new ImageDisplay( this ); addWidget( m_imageDisplay ); m_textDisplay = new TextDisplay( this ); addWidget( m_textDisplay ); createVideoViewer(); connect(m_imageDisplay, &ImageDisplay::possibleChange, this, &ViewerWidget::updateCategoryConfig); connect(m_imageDisplay, &ImageDisplay::imageReady, this, &ViewerWidget::updateInfoBox); connect(m_imageDisplay, &ImageDisplay::setCaptionInfo, this, &ViewerWidget::setCaptionWithDetail); connect(m_imageDisplay, &ImageDisplay::viewGeometryChanged, this, &ViewerWidget::remapAreas); // This must not be added to the layout, as it is standing on top of // the ImageDisplay m_infoBox = new InfoBox( this ); m_infoBox->hide(); setupContextMenu(); m_slideShowTimer = new QTimer( this ); m_slideShowTimer->setSingleShot( true ); m_slideShowPause = Settings::SettingsData::instance()->slideShowInterval() * 1000; connect(m_slideShowTimer, &QTimer::timeout, this, &ViewerWidget::slotSlideShowNextFromTimer); m_speedDisplay = new SpeedDisplay( this ); m_speedDisplay->hide(); setFocusPolicy( Qt::StrongFocus ); QTimer::singleShot( 2000, this, SLOT(test()) ); } void Viewer::ViewerWidget::setupContextMenu() { m_popup = new QMenu( this ); m_actions = new KActionCollection( this ); createSlideShowMenu(); createZoomMenu(); createRotateMenu(); createSkipMenu(); createShowContextMenu(); createWallPaperMenu(); createInvokeExternalMenu(); createVideoMenu(); createCategoryImageMenu(); createFilterMenu(); QAction * action = m_actions->addAction( QString::fromLatin1("viewer-edit-image-properties"), this, SLOT(editImage()) ); action->setText( i18nc("@action:inmenu","Annotate...") ); action->setShortcut( Qt::CTRL+Qt::Key_1 ); m_popup->addAction( action ); m_setStackHead = m_actions->addAction( QString::fromLatin1("viewer-set-stack-head"), this, SLOT(slotSetStackHead()) ); m_setStackHead->setText( i18nc("@action:inmenu","Set as First Image in Stack") ); m_setStackHead->setShortcut( Qt::CTRL+Qt::Key_4 ); m_popup->addAction( m_setStackHead ); m_showExifViewer = m_actions->addAction( QString::fromLatin1("viewer-show-exif-viewer"), this, SLOT(showExifViewer()) ); - m_showExifViewer->setText( i18nc("@action:inmenu","Show EXIF Viewer") ); + m_showExifViewer->setText( i18nc("@action:inmenu","Show Exif Viewer") ); m_popup->addAction( m_showExifViewer ); m_copyTo = m_actions->addAction( QString::fromLatin1("viewer-copy-to"), this, SLOT(copyTo()) ); - m_copyTo->setText( i18nc("@action:inmenu","Copy image to...") ); + m_copyTo->setText( i18nc("@action:inmenu","Copy Image to...") ); m_copyTo->setShortcut( Qt::Key_F7 ); m_popup->addAction( m_copyTo ); if ( m_type == ViewerWindow ) { action = m_actions->addAction( QString::fromLatin1("viewer-close"), this, SLOT(close()) ); action->setText( i18nc("@action:inmenu","Close") ); action->setShortcut( Qt::Key_Escape ); } m_popup->addAction( action ); m_actions->readSettings(); Q_FOREACH( QAction* action, m_actions->actions() ) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } } void Viewer::ViewerWidget::createShowContextMenu() { VisibleOptionsMenu* menu = new VisibleOptionsMenu( this, m_actions ); connect(menu, &VisibleOptionsMenu::visibleOptionsChanged, this, &ViewerWidget::updateInfoBox); m_popup->addMenu( menu ); } void Viewer::ViewerWidget::createWallPaperMenu() { // Setting wallpaper has still not yet been ported to KPA4 #ifndef DOES_STILL_NOT_WORK_IN_KPA4 m_wallpaperMenu = new QMenu( m_popup ); m_wallpaperMenu->setTitle( i18nc("@title:inmenu","Set as Wallpaper") ); QAction * action = m_actions->addAction( QString::fromLatin1("viewer-centered"), this, SLOT(slotSetWallpaperC()) ); action->setText( i18nc("@action:inmenu","Centered") ); m_wallpaperMenu->addAction(action); action = m_actions->addAction( QString::fromLatin1("viewer-tiled"), this, SLOT(slotSetWallpaperT()) ); action->setText( i18nc("@action:inmenu","Tiled") ); m_wallpaperMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-center-tiled"), this, SLOT(slotSetWallpaperCT()) ); action->setText( i18nc("@action:inmenu","Center Tiled") ); m_wallpaperMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-centered-maxspect"), this, SLOT(slotSetWallpaperCM()) ); action->setText( i18nc("@action:inmenu","Centered Maxpect") ); m_wallpaperMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-tiled-maxpect"), this, SLOT(slotSetWallpaperTM()) ); action->setText( i18nc("@action:inmenu","Tiled Maxpect") ); m_wallpaperMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-scaled"), this, SLOT(slotSetWallpaperS()) ); action->setText( i18nc("@action:inmenu","Scaled") ); m_wallpaperMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-centered-auto-fit"), this, SLOT(slotSetWallpaperCAF()) ); action->setText( i18nc("@action:inmenu","Centered Auto Fit") ); m_wallpaperMenu->addAction( action ); m_popup->addMenu( m_wallpaperMenu ); #endif // DOES_STILL_NOT_WORK_IN_KPA4 } void Viewer::ViewerWidget::inhibitScreenSaver( bool inhibit ) { QDBusMessage message; if (inhibit) { message = QDBusMessage::createMethodCall( QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("Inhibit") ); message << QString( QString::fromLatin1("KPhotoAlbum") ); message << QString( QString::fromLatin1("Giving a slideshow") ); QDBusMessage reply = QDBusConnection::sessionBus().call( message ); if ( reply.type() == QDBusMessage::ReplyMessage ) m_screenSaverCookie = reply.arguments().first().toInt(); } else { if ( m_screenSaverCookie != -1 ) { message = QDBusMessage::createMethodCall( QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("UnInhibit") ); message << (uint)m_screenSaverCookie; QDBusConnection::sessionBus().send( message ); m_screenSaverCookie = -1; } } } void Viewer::ViewerWidget::createInvokeExternalMenu() { m_externalPopup = new MainWindow::ExternalPopup( m_popup ); m_popup->addMenu( m_externalPopup ); connect(m_externalPopup, &MainWindow::ExternalPopup::aboutToShow, this, &ViewerWidget::populateExternalPopup); } void Viewer::ViewerWidget::createRotateMenu() { m_rotateMenu = new QMenu( m_popup ); m_rotateMenu->setTitle( i18nc("@title:inmenu","Rotate") ); QAction * action = m_actions->addAction( QString::fromLatin1("viewer-rotate90"), this, SLOT(rotate90()) ); action->setText( i18nc("@action:inmenu","Rotate clockwise") ); action->setShortcut( Qt::Key_9 ); m_rotateMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-rotate180"), this, SLOT(rotate180()) ); action->setText( i18nc("@action:inmenu","Flip Over") ); action->setShortcut( Qt::Key_8 ); m_rotateMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-rotare270"), this, SLOT(rotate270()) ); // ^ this is a typo, isn't it?! action->setText( i18nc("@action:inmenu","Rotate counterclockwise") ); action->setShortcut( Qt::Key_7 ); m_rotateMenu->addAction( action ); m_popup->addMenu( m_rotateMenu ); } void Viewer::ViewerWidget::createSkipMenu() { QMenu *popup = new QMenu( m_popup ); popup->setTitle( i18nc("@title:inmenu As in 'skip 2 images'","Skip") ); QAction * action = m_actions->addAction( QString::fromLatin1("viewer-home"), this, SLOT(showFirst()) ); action->setText( i18nc("@action:inmenu Go to first image","First") ); action->setShortcut( Qt::Key_Home ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-end"), this, SLOT(showLast()) ); action->setText( i18nc("@action:inmenu Go to last image","Last") ); action->setShortcut( Qt::Key_End ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next"), this, SLOT(showNext()) ); action->setText( i18nc("@action:inmenu","Show Next") ); action->setShortcuts(QList() << Qt::Key_PageDown << Qt::Key_Space); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next-10"), this, SLOT(showNext10()) ); action->setText( i18nc("@action:inmenu","Skip 10 Forward") ); action->setShortcut( Qt::CTRL+Qt::Key_PageDown ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next-100"), this, SLOT(showNext100()) ); action->setText( i18nc("@action:inmenu","Skip 100 Forward") ); action->setShortcut( Qt::SHIFT+Qt::Key_PageDown ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next-1000"), this, SLOT(showNext1000()) ); action->setText( i18nc("@action:inmenu","Skip 1000 Forward") ); action->setShortcut( Qt::CTRL+Qt::SHIFT+Qt::Key_PageDown ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev"), this, SLOT(showPrev()) ); action->setText( i18nc("@action:inmenu","Show Previous") ); action->setShortcuts(QList() << Qt::Key_PageUp << Qt::Key_Backspace); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev-10"), this, SLOT(showPrev10()) ); action->setText( i18nc("@action:inmenu","Skip 10 Backward") ); action->setShortcut( Qt::CTRL+Qt::Key_PageUp ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev-100"), this, SLOT(showPrev100()) ); action->setText( i18nc("@action:inmenu","Skip 100 Backward") ); action->setShortcut( Qt::SHIFT+Qt::Key_PageUp ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev-1000"), this, SLOT(showPrev1000()) ); action->setText( i18nc("@action:inmenu","Skip 1000 Backward") ); action->setShortcut( Qt::CTRL+Qt::SHIFT+Qt::Key_PageUp ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-delete-current"), this, SLOT(deleteCurrent()) ); action->setText( i18nc("@action:inmenu","Delete Image") ); action->setShortcut( Qt::CTRL + Qt::Key_Delete ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-remove-current"), this, SLOT(removeCurrent()) ); action->setText( i18nc("@action:inmenu","Remove Image from Display List") ); action->setShortcut( Qt::Key_Delete ); popup->addAction( action ); m_popup->addMenu( popup ); } void Viewer::ViewerWidget::createZoomMenu() { QMenu *popup = new QMenu( m_popup ); popup->setTitle( i18nc("@action:inmenu","Zoom") ); // PENDING(blackie) Only for image display? QAction * action = m_actions->addAction( QString::fromLatin1("viewer-zoom-in"), this, SLOT(zoomIn()) ); action->setText( i18nc("@action:inmenu","Zoom In") ); action->setShortcut( Qt::Key_Plus ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-zoom-out"), this, SLOT(zoomOut()) ); action->setText( i18nc("@action:inmenu","Zoom Out") ); action->setShortcut( Qt::Key_Minus ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-zoom-full"), this, SLOT(zoomFull()) ); action->setText( i18nc("@action:inmenu","Full View") ); action->setShortcut( Qt::Key_Period ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-zoom-pixel"), this, SLOT(zoomPixelForPixel()) ); action->setText( i18nc("@action:inmenu","Pixel for Pixel View") ); action->setShortcut( Qt::Key_Equal ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-toggle-fullscreen"), this, SLOT(toggleFullScreen()) ); action->setText( i18nc("@action:inmenu","Toggle Full Screen") ); action->setShortcuts(QList() << Qt::Key_F11 << Qt::Key_Return); popup->addAction( action ); m_popup->addMenu( popup ); } void Viewer::ViewerWidget::createSlideShowMenu() { QMenu *popup = new QMenu( m_popup ); popup->setTitle( i18nc("@title:inmenu","Slideshow") ); m_startStopSlideShow = m_actions->addAction( QString::fromLatin1("viewer-start-stop-slideshow"), this, SLOT(slotStartStopSlideShow()) ); m_startStopSlideShow->setText( i18nc("@action:inmenu","Run Slideshow") ); m_startStopSlideShow->setShortcut( Qt::CTRL+Qt::Key_R ); popup->addAction( m_startStopSlideShow ); m_slideShowRunFaster = m_actions->addAction( QString::fromLatin1("viewer-run-faster"), this, SLOT(slotSlideShowFaster()) ); m_slideShowRunFaster->setText( i18nc("@action:inmenu","Run Faster") ); m_slideShowRunFaster->setShortcut( Qt::CTRL + Qt::Key_Plus ); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction( m_slideShowRunFaster ); m_slideShowRunSlower = m_actions->addAction( QString::fromLatin1("viewer-run-slower"), this, SLOT(slotSlideShowSlower()) ); m_slideShowRunSlower->setText( i18nc("@action:inmenu","Run Slower") ); m_slideShowRunSlower->setShortcut( Qt::CTRL+Qt::Key_Minus ); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction( m_slideShowRunSlower ); m_popup->addMenu( popup ); } void Viewer::ViewerWidget::load( const DB::FileNameList& list, int index ) { m_list = list; m_imageDisplay->setImageList( list ); m_current = index; load(); bool on = ( list.count() > 1 ); m_startStopSlideShow->setEnabled(on); m_slideShowRunFaster->setEnabled(on); m_slideShowRunSlower->setEnabled(on); } void Viewer::ViewerWidget::load() { const bool isReadable = QFileInfo( currentInfo()->fileName().absolute() ).isReadable(); const bool isVideo = isReadable && Utilities::isVideo( currentInfo()->fileName() ); if ( isReadable ) { if ( isVideo ) { m_display = m_videoDisplay; } else m_display = m_imageDisplay; } else { m_display = m_textDisplay; m_textDisplay->setText( i18n("File not available") ); updateInfoBox(); } setCurrentWidget( m_display ); m_infoBox->raise(); m_rotateMenu->setEnabled( !isVideo ); m_wallpaperMenu->setEnabled( !isVideo ); m_categoryImagePopup->setEnabled( !isVideo ); m_filterMenu->setEnabled( !isVideo ); m_showExifViewer->setEnabled( !isVideo ); if ( m_exifViewer ) m_exifViewer->setImage( currentInfo()->fileName() ); Q_FOREACH( QAction* videoAction, m_videoActions ) { videoAction->setVisible( isVideo ); } emit soughtTo( m_list[ m_current ]); bool ok = m_display->setImage( currentInfo(), m_forward ); if ( !ok ) { close( false ); return; } setCaptionWithDetail( QString() ); // PENDING(blackie) This needs to be improved, so that it shows the actions only if there are that many images to jump. for( QList::const_iterator it = m_forwardActions.constBegin(); it != m_forwardActions.constEnd(); ++it ) (*it)->setEnabled( m_current +1 < (int) m_list.count() ); for( QList::const_iterator it = m_backwardActions.constBegin(); it != m_backwardActions.constEnd(); ++it ) (*it)->setEnabled( m_current > 0 ); m_setStackHead->setEnabled( currentInfo()->isStacked() ); if ( isVideo ) updateCategoryConfig(); if ( m_isRunningSlideShow ) m_slideShowTimer->start( m_slideShowPause ); if ( m_display == m_textDisplay ) updateInfoBox(); // Add all tagged areas addTaggedAreas(); } void Viewer::ViewerWidget::setCaptionWithDetail( const QString& detail ) { - setWindowTitle( QString::fromLatin1( "KPhotoAlbum - %1 %2" ) - .arg( currentInfo()->fileName().absolute() ) - .arg( detail ) ); + setWindowTitle( i18nc("@title:window %1 is the filename, %2 it's detail info", "%1 %2", + currentInfo()->fileName().absolute(), + detail ) ); } void Viewer::ViewerWidget::contextMenuEvent( QContextMenuEvent * e ) { if ( m_videoDisplay ) { if ( m_videoDisplay->isPaused() ) m_playPause->setText(i18nc("@action:inmenu Start video playback","Play")); else m_playPause->setText(i18nc("@action:inmenu Pause video playback","Pause")); m_stop->setEnabled( m_videoDisplay->isPlaying() ); } m_popup->exec( e->globalPos() ); e->setAccepted(true); } void Viewer::ViewerWidget::showNextN(int n) { filterNone(); if ( m_display == m_videoDisplay ) { m_videoPlayerStoppedManually = true; m_videoDisplay->stop(); } if ( m_current + n < (int) m_list.count() ) { m_current += n; if (m_current >= (int) m_list.count()) m_current = (int) m_list.count() - 1; m_forward = true; load(); } } void Viewer::ViewerWidget::showNext() { showNextN(1); } void Viewer::ViewerWidget::removeCurrent() { removeOrDeleteCurrent(OnlyRemoveFromViewer); } void Viewer::ViewerWidget::deleteCurrent() { removeOrDeleteCurrent( RemoveImageFromDatabase ); } void Viewer::ViewerWidget::removeOrDeleteCurrent( RemoveAction action ) { const DB::FileName fileName = m_list[m_current]; if ( action == RemoveImageFromDatabase ) m_removed.append(fileName); m_list.removeAll(fileName); if ( m_list.isEmpty() ) close(); if ( m_current == m_list.count() ) showPrev(); else showNextN(0); } void Viewer::ViewerWidget::showNext10() { showNextN(10); } void Viewer::ViewerWidget::showNext100() { showNextN(100); } void Viewer::ViewerWidget::showNext1000() { showNextN(1000); } void Viewer::ViewerWidget::showPrevN(int n) { if ( m_display == m_videoDisplay ) m_videoDisplay->stop(); if ( m_current > 0 ) { m_current -= n; if (m_current < 0) m_current = 0; m_forward = false; load(); } } void Viewer::ViewerWidget::showPrev() { showPrevN(1); } void Viewer::ViewerWidget::showPrev10() { showPrevN(10); } void Viewer::ViewerWidget::showPrev100() { showPrevN(100); } void Viewer::ViewerWidget::showPrev1000() { showPrevN(1000); } void Viewer::ViewerWidget::rotate90() { currentInfo()->rotate( 90 ); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[ m_current]); } void Viewer::ViewerWidget::rotate180() { currentInfo()->rotate( 180 ); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[ m_current]); } void Viewer::ViewerWidget::rotate270() { currentInfo()->rotate( 270 ); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[ m_current]); } void Viewer::ViewerWidget::showFirst() { showPrevN(m_list.count()); } void Viewer::ViewerWidget::showLast() { showNextN(m_list.count()); } void Viewer::ViewerWidget::slotSetWallpaperC() { setAsWallpaper(1); } void Viewer::ViewerWidget::slotSetWallpaperT() { setAsWallpaper(2); } void Viewer::ViewerWidget::slotSetWallpaperCT() { setAsWallpaper(3); } void Viewer::ViewerWidget::slotSetWallpaperCM() { setAsWallpaper(4); } void Viewer::ViewerWidget::slotSetWallpaperTM() { setAsWallpaper(5); } void Viewer::ViewerWidget::slotSetWallpaperS() { setAsWallpaper(6); } void Viewer::ViewerWidget::slotSetWallpaperCAF() { setAsWallpaper(7); } void Viewer::ViewerWidget::setAsWallpaper(int /*mode*/) { #ifdef DOES_STILL_NOT_WORK_IN_KPA4 if(mode>7 || mode<1) return; DCOPRef kdesktop("kdesktop","KBackgroundIface"); kdesktop.send("setWallpaper(QString,int)",currentInfo()->fileName(0),mode); #endif } bool Viewer::ViewerWidget::close( bool alsoDelete) { if ( !m_removed.isEmpty() ) { MainWindow::DeleteDialog dialog( this ); dialog.exec( m_removed ); } m_slideShowTimer->stop(); m_isRunningSlideShow = false; return QWidget::close(); if ( alsoDelete ) deleteLater(); } DB::ImageInfoPtr Viewer::ViewerWidget::currentInfo() const { return DB::ImageDB::instance()->info(m_list[ m_current]); // PENDING(blackie) can we postpone this lookup? } void Viewer::ViewerWidget::infoBoxMove() { QPoint p = mapFromGlobal( QCursor::pos() ); Settings::Position oldPos = Settings::SettingsData::instance()->infoBoxPosition(); Settings::Position pos = oldPos; int x = m_display->mapFromParent( p ).x(); int y = m_display->mapFromParent( p ).y(); int w = m_display->width(); int h = m_display->height(); if ( x < w/3 ) { if ( y < h/3 ) pos = Settings::TopLeft; else if ( y > h*2/3 ) pos = Settings::BottomLeft; else pos = Settings::Left; } else if ( x > w*2/3 ) { if ( y < h/3 ) pos = Settings::TopRight; else if ( y > h*2/3 ) pos = Settings::BottomRight; else pos = Settings::Right; } else { if ( y < h/3 ) pos = Settings::Top; else if ( y > h*2/3 ) pos = Settings::Bottom; } if ( pos != oldPos ) { Settings::SettingsData::instance()->setInfoBoxPosition( pos ); updateInfoBox(); } } void Viewer::ViewerWidget::moveInfoBox() { m_infoBox->setSize(); Settings::Position pos = Settings::SettingsData::instance()->infoBoxPosition(); int lx = m_display->pos().x(); int ly = m_display->pos().y(); int lw = m_display->width(); int lh = m_display->height(); int bw = m_infoBox->width(); int bh = m_infoBox->height(); int bx, by; // x-coordinate if ( pos == Settings::TopRight || pos == Settings::BottomRight || pos == Settings::Right ) bx = lx+lw-5-bw; else if ( pos == Settings::TopLeft || pos == Settings::BottomLeft || pos == Settings::Left ) bx = lx+5; else bx = lx+lw/2-bw/2; // Y-coordinate if ( pos == Settings::TopLeft || pos == Settings::TopRight || pos == Settings::Top ) by = ly+5; else if ( pos == Settings::BottomLeft || pos == Settings::BottomRight || pos == Settings::Bottom ) by = ly+lh-5-bh; else by = ly+lh/2-bh/2; m_infoBox->move(bx,by); } void Viewer::ViewerWidget::resizeEvent( QResizeEvent* e ) { moveInfoBox(); QWidget::resizeEvent( e ); } void Viewer::ViewerWidget::updateInfoBox() { QString tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name(); if ( currentInfo() || !m_currentInput.isEmpty() || (!m_currentCategory.isEmpty() && m_currentCategory != tokensCategory)) { QMap > map; QString text = Utilities::createInfoText( currentInfo(), &map ); QString selecttext = QString::fromLatin1(""); if (m_currentCategory.isEmpty()) { selecttext = i18nc("Basically 'enter a category name'","Setting Category: ") + m_currentInput; if (m_currentInputList.length() > 0) { selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}"); } } else if ( ( !m_currentInput.isEmpty() && m_currentCategory != tokensCategory)) { selecttext = i18nc("Basically 'enter a tag name'","Assigning: ") + m_currentCategory + QString::fromLatin1("/") + m_currentInput; if (m_currentInputList.length() > 0) { selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}"); } } else if ( !m_currentInput.isEmpty() && m_currentCategory == tokensCategory) { m_currentInput = QString::fromLatin1(""); } if (!selecttext.isEmpty()) text = selecttext + QString::fromLatin1("
    ") + text; if ( Settings::SettingsData::instance()->showInfoBox() && !text.isNull() && ( m_type != InlineViewer ) ) { m_infoBox->setInfo( text, map ); m_infoBox->show(); } else m_infoBox->hide(); moveInfoBox(); } } Viewer::ViewerWidget::~ViewerWidget() { inhibitScreenSaver(false); if ( s_latest == this ) s_latest = nullptr; if ( m_myInputMacros ) delete m_myInputMacros; } void Viewer::ViewerWidget::toggleFullScreen() { setShowFullScreen( !m_showingFullScreen ); } void Viewer::ViewerWidget::slotStartStopSlideShow() { bool wasRunningSlideShow = m_isRunningSlideShow; m_isRunningSlideShow = !m_isRunningSlideShow && m_list.count() != 1; if ( wasRunningSlideShow ) { m_startStopSlideShow->setText( i18nc("@action:inmenu","Run Slideshow") ); m_slideShowTimer->stop(); if ( m_list.count() != 1 ) m_speedDisplay->end(); inhibitScreenSaver(false); } else { m_startStopSlideShow->setText( i18nc("@action:inmenu","Stop Slideshow") ); if ( currentInfo()->mediaType() != DB::Video ) m_slideShowTimer->start( m_slideShowPause ); m_speedDisplay->start(); inhibitScreenSaver(true); } } void Viewer::ViewerWidget::slotSlideShowNextFromTimer() { // Load the next images. QTime timer; timer.start(); if ( m_display == m_imageDisplay ) slotSlideShowNext(); // ensure that there is a few milliseconds pause, so that an end slideshow keypress // can get through immediately, we don't want it to queue up behind a bunch of timer events, // which loaded a number of new images before the slideshow stops int ms = qMax( 200, m_slideShowPause - timer.elapsed() ); m_slideShowTimer->start( ms ); } void Viewer::ViewerWidget::slotSlideShowNext() { m_forward = true; if ( m_current +1 < (int) m_list.count() ) m_current++; else m_current = 0; load(); } void Viewer::ViewerWidget::slotSlideShowFaster() { changeSlideShowInterval(-500); } void Viewer::ViewerWidget::slotSlideShowSlower() { changeSlideShowInterval(+500); } void Viewer::ViewerWidget::changeSlideShowInterval( int delta ) { if ( m_list.count() == 1 ) return; m_slideShowPause += delta; m_slideShowPause = qMax( m_slideShowPause, 500 ); m_speedDisplay->display( m_slideShowPause ); if (m_slideShowTimer->isActive() ) m_slideShowTimer->start( m_slideShowPause ); } void Viewer::ViewerWidget::editImage() { DB::ImageInfoList list; list.append( currentInfo() ); MainWindow::Window::configureImages( list, true ); } void Viewer::ViewerWidget::filterNone() { if ( m_display == m_imageDisplay ) { m_imageDisplay->filterNone(); m_filterMono->setChecked( false ); m_filterBW->setChecked( false ); m_filterContrastStretch->setChecked( false ); m_filterHistogramEqualization->setChecked( false ); } } void Viewer::ViewerWidget::filterSelected() { // The filters that drop bit depth below 32 should be the last ones // so that filters requiring more bit depth are processed first if ( m_display == m_imageDisplay ) { m_imageDisplay->filterNone(); if (m_filterBW->isChecked()) m_imageDisplay->filterBW(); if (m_filterContrastStretch->isChecked()) m_imageDisplay->filterContrastStretch(); if (m_filterHistogramEqualization->isChecked()) m_imageDisplay->filterHistogramEqualization(); if (m_filterMono->isChecked()) m_imageDisplay->filterMono(); } } void Viewer::ViewerWidget::filterBW() { if ( m_display == m_imageDisplay ) { if ( m_filterBW->isChecked() ) m_filterBW->setChecked( m_imageDisplay->filterBW()); else filterSelected(); } } void Viewer::ViewerWidget::filterContrastStretch() { if ( m_display == m_imageDisplay ) { if (m_filterContrastStretch->isChecked()) m_filterContrastStretch->setChecked( m_imageDisplay->filterContrastStretch() ); else filterSelected(); } } void Viewer::ViewerWidget::filterHistogramEqualization() { if ( m_display == m_imageDisplay ) { if ( m_filterHistogramEqualization->isChecked() ) m_filterHistogramEqualization->setChecked( m_imageDisplay->filterHistogramEqualization() ); else filterSelected(); } } void Viewer::ViewerWidget::filterMono() { if ( m_display == m_imageDisplay ) { if ( m_filterMono->isChecked() ) m_filterMono->setChecked( m_imageDisplay->filterMono() ); else filterSelected(); } } void Viewer::ViewerWidget::slotSetStackHead() { MainWindow::Window::theMainWindow()->setStackHead(m_list[ m_current ]); } bool Viewer::ViewerWidget::showingFullScreen() const { return m_showingFullScreen; } void Viewer::ViewerWidget::setShowFullScreen( bool on ) { if ( on ) { setWindowState( windowState() | Qt::WindowFullScreen ); // set moveInfoBox(); } else { // We need to size the image when going out of full screen, in case we started directly in full screen // setWindowState( windowState() & ~Qt::WindowFullScreen ); // reset resize( Settings::SettingsData::instance()->viewerSize() ); } m_showingFullScreen = on; } void Viewer::ViewerWidget::updateCategoryConfig() { if ( !CategoryImageConfig::instance()->isVisible() ) return; CategoryImageConfig::instance()->setCurrentImage( m_imageDisplay->currentViewAsThumbnail(), currentInfo() ); } void Viewer::ViewerWidget::populateExternalPopup() { m_externalPopup->populate( currentInfo(), m_list ); } void Viewer::ViewerWidget::populateCategoryImagePopup() { m_categoryImagePopup->populate( m_imageDisplay->currentViewAsThumbnail(), m_list[m_current] ); } void Viewer::ViewerWidget::show( bool slideShow ) { QSize size; bool fullScreen; if ( slideShow ) { fullScreen = Settings::SettingsData::instance()->launchSlideShowFullScreen(); size = Settings::SettingsData::instance()->slideShowSize(); } else { fullScreen = Settings::SettingsData::instance()->launchViewerFullScreen(); size = Settings::SettingsData::instance()->viewerSize(); } if ( fullScreen ) setShowFullScreen( true ); else resize( size ); QWidget::show(); if ( slideShow != m_isRunningSlideShow) { // The info dialog will show up at the wrong place if we call this function directly // don't ask me why - 4 Sep. 2004 15:13 -- Jesper K. Pedersen QTimer::singleShot(0, this, SLOT(slotStartStopSlideShow()) ); } } KActionCollection* Viewer::ViewerWidget::actions() { return m_actions; } int Viewer::ViewerWidget::find_tag_in_list(const QStringList &list, QString &namefound) { int found = 0; m_currentInputList = QString::fromLatin1(""); for( QStringList::ConstIterator listIter = list.constBegin(); listIter != list.constEnd(); ++listIter ) { if (listIter->startsWith(m_currentInput, Qt::CaseInsensitive)) { found++; if (m_currentInputList.length() > 0) m_currentInputList = m_currentInputList + QString::fromLatin1(","); m_currentInputList =m_currentInputList + listIter->right(listIter->length() - m_currentInput.length()); if (found > 1 && m_currentInputList.length() > 20) { // already found more than we want to display // bail here for now // XXX: non-ideal? display more? certainly config 20 return found; } else { namefound = *listIter; } } } return found; } void Viewer::ViewerWidget::keyPressEvent( QKeyEvent* event ) { if (event->key() == Qt::Key_Backspace) { // remove stuff from the current input string m_currentInput.remove( m_currentInput.length()-1, 1 ); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); m_currentInputList = QString::fromLatin1(""); // } else if (event->modifier & (Qt::AltModifier | Qt::MetaModifier) && // event->key() == Qt::Key_Enter) { return; // we've handled it } else if (event->key() == Qt::Key_Comma) { // force set the "new" token if (!m_currentCategory.isEmpty()) { if (m_currentInput.left(1) == QString::fromLatin1("\"") || // allow a starting ' or " to signal a brand new category // this bypasses the auto-selection of matching characters m_currentInput.left(1) == QString::fromLatin1("\'")) { m_currentInput = m_currentInput.right(m_currentInput.length()-1); } if (m_currentInput.isEmpty()) return; currentInfo()->addCategoryInfo(m_currentCategory, m_currentInput); DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_currentCategory); category->addItem(m_currentInput); } m_currentInput = QString::fromLatin1(""); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); return; // we've handled it } else if ( event->modifiers() == 0 && event->key() >= Qt::Key_0 && event->key() <= Qt::Key_5 ) { bool ok; short rating = event->text().left(1).toShort(&ok, 10); if (ok) { currentInfo()->setRating(rating * 2); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); } } else if (event->modifiers() == 0 || event->modifiers() == Qt::ShiftModifier) { // search the category for matches QString namefound; QString incomingKey = event->text().left(1); // start searching for a new category name if (incomingKey == QString::fromLatin1("/")) { if (m_currentInput.isEmpty() && m_currentCategory.isEmpty()) { if (m_currentInputMode == InACategory) { m_currentInputMode = AlwaysStartWithCategory; } else { m_currentInputMode = InACategory; } } else { // reset the category to search through m_currentInput = QString::fromLatin1(""); m_currentCategory = QString::fromLatin1(""); } // use an assigned key or map to a given key for future reference } else if (m_currentInput.isEmpty() && // can map to function keys event->key() >= Qt::Key_F1 && event->key() <= Qt::Key_F35) { // we have a request to assign a macro key or use one Qt::Key key = (Qt::Key) event->key(); if (m_inputMacros->contains(key)) { // Use the requested toggle if ( event->modifiers() == Qt::ShiftModifier ) { if ( currentInfo()->hasCategoryInfo( (*m_inputMacros)[key].first, (*m_inputMacros)[key].second ) ) { currentInfo()->removeCategoryInfo( (*m_inputMacros)[key].first, (*m_inputMacros)[key].second ); } } else { currentInfo()->addCategoryInfo( (*m_inputMacros)[key].first, (*m_inputMacros)[key].second ); } } else { (*m_inputMacros)[key] = qMakePair(m_lastCategory, m_lastFound); } updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); // handled it return; } else if (m_currentCategory.isEmpty()) { // still searching for a category to lock to m_currentInput += incomingKey; QStringList categorynames = DB::ImageDB::instance()->categoryCollection()->categoryTexts(); if (find_tag_in_list(categorynames, namefound) == 1) { // yay, we have exactly one! m_currentCategory = namefound; m_currentInput = QString::fromLatin1(""); m_currentInputList = QString::fromLatin1(""); } } else { m_currentInput += incomingKey; DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection() ->categoryForName(m_currentCategory); QStringList items = category->items(); if (find_tag_in_list(items, namefound) == 1) { // yay, we have exactly one! if ( currentInfo()->hasCategoryInfo( category->name(), namefound ) ) currentInfo()->removeCategoryInfo( category->name(), namefound ); else currentInfo()->addCategoryInfo( category->name(), namefound ); m_lastFound = namefound; m_lastCategory = m_currentCategory; m_currentInput = QString::fromLatin1(""); m_currentInputList = QString::fromLatin1(""); if (m_currentInputMode == AlwaysStartWithCategory) m_currentCategory = QString::fromLatin1(""); } } updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); } QWidget::keyPressEvent( event ); return; } void Viewer::ViewerWidget::videoStopped() { if ( !m_videoPlayerStoppedManually && m_isRunningSlideShow ) slotSlideShowNext(); m_videoPlayerStoppedManually=false; } void Viewer::ViewerWidget::wheelEvent( QWheelEvent* event ) { if ( event->delta() < 0) { showNext(); } else { showPrev(); } } void Viewer::ViewerWidget::showExifViewer() { m_exifViewer = new Exif::InfoDialog( currentInfo()->fileName(), this ); m_exifViewer->show(); } void Viewer::ViewerWidget::zoomIn() { m_display->zoomIn(); } void Viewer::ViewerWidget::zoomOut() { m_display->zoomOut(); } void Viewer::ViewerWidget::zoomFull() { m_display->zoomFull(); } void Viewer::ViewerWidget::zoomPixelForPixel() { m_display->zoomPixelForPixel(); } void Viewer::ViewerWidget::makeThumbnailImage() { VideoShooter::go(currentInfo(), this); } struct SeekInfo { SeekInfo( const QString& title, const char* name, int value, const QKeySequence& key ) : title( title ), name(name), value(value), key(key) {} QString title; const char* name; int value; QKeySequence key; }; void Viewer::ViewerWidget::createVideoMenu() { QMenu* menu = new QMenu(m_popup); menu->setTitle(i18nc("@title:inmenu","Seek")); m_videoActions.append( m_popup->addMenu( menu ) ); QList list; list << SeekInfo( i18nc("@action:inmenu","10 minutes backward"), "seek-10-minute", -600000, QKeySequence(QString::fromLatin1("Ctrl+Left"))) << SeekInfo( i18nc("@action:inmenu","1 minute backward"), "seek-1-minute", -60000, QKeySequence(QString::fromLatin1( "Shift+Left"))) << SeekInfo( i18nc("@action:inmenu","10 seconds backward"), "seek-10-second", -10000, QKeySequence(QString::fromLatin1( "Left"))) << SeekInfo( i18nc("@action:inmenu","1 seconds backward"), "seek-1-second", -1000, QKeySequence(QString::fromLatin1( "Up"))) << SeekInfo( i18nc("@action:inmenu","100 milliseconds backward"), "seek-100-millisecond", -100, QKeySequence(QString::fromLatin1( "Shift+Up"))) << SeekInfo( i18nc("@action:inmenu","100 milliseconds forward"), "seek+100-millisecond", 100, QKeySequence(QString::fromLatin1( "Shift+Down"))) << SeekInfo( i18nc("@action:inmenu","1 seconds forward"), "seek+1-second", 1000, QKeySequence(QString::fromLatin1( "Down"))) << SeekInfo( i18nc("@action:inmenu","10 seconds forward"), "seek+10-second", 10000, QKeySequence(QString::fromLatin1( "Right"))) << SeekInfo( i18nc("@action:inmenu","1 minute forward"), "seek+1-minute", 60000, QKeySequence(QString::fromLatin1( "Shift+Right"))) << SeekInfo( i18nc("@action:inmenu","10 minutes forward"), "seek+10-minute", 600000, QKeySequence(QString::fromLatin1( "Ctrl+Right"))); int count=0; Q_FOREACH( const SeekInfo& info, list ) { if ( count++ == 5 ) { QAction* sep = new QAction( menu ); sep->setSeparator(true); menu->addAction(sep); } QAction * seek = m_actions->addAction( QString::fromLatin1(info.name), m_videoDisplay, SLOT(seek())); seek->setText(info.title); seek->setData(info.value); seek->setShortcut( info.key ); menu->addAction(seek); } QAction* sep = new QAction(m_popup); sep->setSeparator(true); m_popup->addAction( sep ); m_videoActions.append( sep ); m_stop = m_actions->addAction( QString::fromLatin1("viewer-video-stop"), m_videoDisplay, SLOT(stop()) ); m_stop->setText( i18nc("@action:inmenu Stop video playback","Stop") ); m_popup->addAction( m_stop ); m_videoActions.append(m_stop); m_playPause = m_actions->addAction( QString::fromLatin1("viewer-video-pause"), m_videoDisplay, SLOT(playPause()) ); // text set in contextMenuEvent() m_playPause->setShortcut( Qt::Key_P ); m_popup->addAction( m_playPause ); m_videoActions.append( m_playPause ); m_makeThumbnailImage = m_actions->addAction( QString::fromLatin1("make-thumbnail-image"), this, SLOT(makeThumbnailImage())); m_makeThumbnailImage->setShortcut(Qt::ControlModifier + Qt::Key_S); m_makeThumbnailImage->setText( i18nc("@action:inmenu","Use current frame in thumbnail view") ); m_popup->addAction(m_makeThumbnailImage); m_videoActions.append(m_makeThumbnailImage); QAction * restart = m_actions->addAction( QString::fromLatin1("viewer-video-restart"), m_videoDisplay, SLOT(restart()) ); restart->setText( i18nc("@action:inmenu Restart video playback.","Restart") ); m_popup->addAction( restart ); m_videoActions.append( restart ); } void Viewer::ViewerWidget::createCategoryImageMenu() { m_categoryImagePopup = new MainWindow::CategoryImagePopup( m_popup ); m_popup->addMenu( m_categoryImagePopup ); connect(m_categoryImagePopup, &MainWindow::CategoryImagePopup::aboutToShow, this, &ViewerWidget::populateCategoryImagePopup); } void Viewer::ViewerWidget::createFilterMenu() { m_filterMenu = new QMenu( m_popup ); m_filterMenu->setTitle( i18nc("@title:inmenu","Filters") ); m_filterNone = m_actions->addAction( QString::fromLatin1("filter-empty"), this, SLOT(filterNone()) ); m_filterNone->setText( i18nc("@action:inmenu","Remove All Filters") ); m_filterMenu->addAction( m_filterNone ); m_filterBW = m_actions->addAction( QString::fromLatin1("filter-bw"), this, SLOT(filterBW()) ); m_filterBW->setText( i18nc("@action:inmenu","Apply Grayscale Filter") ); m_filterBW->setCheckable( true ); m_filterMenu->addAction( m_filterBW ); m_filterContrastStretch = m_actions->addAction( QString::fromLatin1("filter-cs"), this, SLOT(filterContrastStretch()) ); m_filterContrastStretch->setText( i18nc("@action:inmenu","Apply Contrast Stretching Filter") ); m_filterContrastStretch->setCheckable( true ); m_filterMenu->addAction( m_filterContrastStretch ); m_filterHistogramEqualization = m_actions->addAction( QString::fromLatin1("filter-he"), this, SLOT(filterHistogramEqualization()) ); m_filterHistogramEqualization->setText( i18nc("@action:inmenu","Apply Histogram Equalization Filter") ); m_filterHistogramEqualization->setCheckable( true ); m_filterMenu->addAction( m_filterHistogramEqualization ); m_filterMono = m_actions->addAction( QString::fromLatin1("filter-mono"), this, SLOT(filterMono()) ); m_filterMono->setText( i18nc("@action:inmenu","Apply Monochrome Filter") ); m_filterMono->setCheckable( true ); m_filterMenu->addAction( m_filterMono ); m_popup->addMenu( m_filterMenu ); } void Viewer::ViewerWidget::test() { #ifdef TESTING QTimeLine* timeline = new QTimeLine; timeline->setStartFrame( _infoBox->y() ); timeline->setEndFrame( height() ); connect(timeline, &QTimeLine::frameChanged, this, &ViewerWidget::moveInfoBox); timeline->start(); #endif // TESTING } void Viewer::ViewerWidget::moveInfoBox( int y) { m_infoBox->move( m_infoBox->x(), y ); } void Viewer::ViewerWidget::createVideoViewer() { m_videoDisplay = new VideoDisplay( this ); addWidget( m_videoDisplay ); connect(m_videoDisplay, &VideoDisplay::stopped, this, &ViewerWidget::videoStopped); } void Viewer::ViewerWidget::stopPlayback() { m_videoDisplay->stop(); } void Viewer::ViewerWidget::invalidateThumbnail() const { ImageManager::ThumbnailCache::instance()->removeThumbnail( currentInfo()->fileName() ); } void Viewer::ViewerWidget::addTaggedAreas() { // Clean all areas we probably already have foreach (TaggedArea *area, findChildren()) { area->deleteLater(); } QMap> taggedAreas = currentInfo()->taggedAreas(); QMapIterator> areasInCategory(taggedAreas); QString category; QString tag; while (areasInCategory.hasNext()) { areasInCategory.next(); category = areasInCategory.key(); QMapIterator areaData(areasInCategory.value()); while (areaData.hasNext()) { areaData.next(); tag = areaData.key(); // Add a new frame for the area TaggedArea *newArea = new TaggedArea(this); newArea->setTagInfo(category, category, tag); newArea->setActualGeometry(areaData.value()); newArea->show(); connect(m_infoBox, &InfoBox::tagHovered, newArea, &TaggedArea::checkShowArea); connect(m_infoBox, &InfoBox::noTagHovered, newArea, &TaggedArea::resetViewStyle); } } // Be sure to display the areas, as viewGeometryChanged is not always emitted on load QSize imageSize = currentInfo()->size(); QSize windowSize = this->size(); // On load, the image is never zoomed, so it's a bit easier ;-) double scaleWidth = double(imageSize.width()) / windowSize.width(); double scaleHeight = double(imageSize.height()) / windowSize.height(); int offsetTop = 0; int offsetLeft = 0; if (scaleWidth > scaleHeight) { offsetTop = (windowSize.height() - imageSize.height() / scaleWidth); } else { offsetLeft = (windowSize.width() - imageSize.width() / scaleHeight); } remapAreas( QSize(windowSize.width() - offsetLeft, windowSize.height() - offsetTop), QRect(QPoint(0, 0), QPoint(imageSize.width(), imageSize.height())), 1 ); } void Viewer::ViewerWidget::remapAreas(QSize viewSize, QRect zoomWindow, double sizeRatio) { QSize currentWindowSize = this->size(); int outerOffsetLeft = (currentWindowSize.width() - viewSize.width()) / 2; int outerOffsetTop = (currentWindowSize.height() - viewSize.height()) / 2; if (sizeRatio != 1) { zoomWindow = QRect( QPoint( double(zoomWindow.left()) * sizeRatio, double(zoomWindow.top()) * sizeRatio ), QPoint( double(zoomWindow.left() + zoomWindow.width()) * sizeRatio, double(zoomWindow.top() + zoomWindow.height()) * sizeRatio ) ); } double scaleHeight = double(viewSize.height()) / zoomWindow.height(); double scaleWidth = double(viewSize.width()) / zoomWindow.width(); int innerOffsetLeft = -zoomWindow.left() * scaleWidth; int innerOffsetTop = -zoomWindow.top() * scaleHeight; Q_FOREACH(TaggedArea *area, findChildren()) { QRect actualGeometry = area->actualGeometry(); QRect screenGeometry; screenGeometry.setWidth(actualGeometry.width() * scaleWidth); screenGeometry.setHeight(actualGeometry.height() * scaleHeight); screenGeometry.moveTo( actualGeometry.left() * scaleWidth + outerOffsetLeft + innerOffsetLeft, actualGeometry.top() * scaleHeight + outerOffsetTop + innerOffsetTop ); area->setGeometry(screenGeometry); } } void Viewer::ViewerWidget::copyTo() { QUrl src = QUrl::fromLocalFile(currentInfo()->fileName().absolute()); if (m_lastCopyToTarget.isNull()) { // get directory of src file m_lastCopyToTarget = QFileInfo(src.path()).path(); } QFileDialog dialog( this ); - dialog.setWindowTitle( i18nc("@title:window", "Copy image to...") ); + dialog.setWindowTitle( i18nc("@title:window", "Copy Image to...") ); // use directory of src as start-location: dialog.setDirectory(m_lastCopyToTarget); dialog.selectFile(src.fileName()); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setLabelText(QFileDialog::Accept, i18nc("@action:button", "Copy")); if (dialog.exec()) { QUrl dst = dialog.selectedUrls().first(); KIO::CopyJob *job = KIO::copy(src, dst); connect(job, &KIO::CopyJob::finished, job, &QObject::deleteLater); // get directory of dst file m_lastCopyToTarget = QFileInfo(dst.path()).path(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/VisibleOptionsMenu.cpp b/Viewer/VisibleOptionsMenu.cpp index 81286d4c..e67a3f76 100644 --- a/Viewer/VisibleOptionsMenu.cpp +++ b/Viewer/VisibleOptionsMenu.cpp @@ -1,177 +1,177 @@ /* 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 "VisibleOptionsMenu.h" #include "Settings/SettingsData.h" #include #include #include #include #include #include "DB/Category.h" #include "DB/ImageDB.h" #include "DB/CategoryCollection.h" Viewer::VisibleOptionsMenu::VisibleOptionsMenu(QWidget* parent, KActionCollection* actions) : QMenu(i18n("Show..."), parent) { setTearOffEnabled(true); setTitle( i18n("Show") ); connect(this, &VisibleOptionsMenu::aboutToShow, this, &VisibleOptionsMenu::updateState); m_showInfoBox = actions->add( QString::fromLatin1("viewer-show-infobox") ); m_showInfoBox->setText( i18n("Show Info Box") ); m_showInfoBox->setShortcut( Qt::CTRL+Qt::Key_I ); m_showInfoBox->setChecked(Settings::SettingsData::instance()->showInfoBox()); connect(m_showInfoBox, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowInfoBox); addAction( m_showInfoBox ); m_showLabel = actions->add( QString::fromLatin1("viewer-show-label") ); m_showLabel->setText( i18n("Show Label") ); m_showLabel->setShortcut( 0 ); connect(m_showLabel, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowLabel); addAction( m_showLabel ); m_showDescription = actions->add( QString::fromLatin1("viewer-show-description") ); m_showDescription->setText( i18n("Show Description") ); m_showDescription->setShortcut( 0 ); connect(m_showDescription, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowDescription); addAction( m_showDescription ); m_showDate = actions->add(QString::fromLatin1("viewer-show-date") ); m_showDate->setText( i18n("Show Date") ); connect(m_showDate, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowDate); addAction( m_showDate ); m_showTime = actions->add(QString::fromLatin1("viewer-show-time") ); m_showTime->setText( i18n("Show Time") ); connect(m_showTime, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowTime); addAction( m_showTime ); m_showTime->setVisible( m_showDate->isChecked() ); m_showFileName = actions->add(QString::fromLatin1("viewer-show-filename") ); m_showFileName->setText( i18n("Show Filename") ); connect(m_showFileName, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowFilename); addAction( m_showFileName ); m_showExif = actions->add(QString::fromLatin1("viewer-show-exif") ); - m_showExif->setText( i18n("Show EXIF") ); + m_showExif->setText( i18n("Show Exif") ); connect(m_showExif, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowEXIF); addAction( m_showExif ); m_showImageSize = actions->add(QString::fromLatin1("viewer-show-imagesize") ); m_showImageSize->setText( i18n("Show Image Size") ); connect(m_showImageSize, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowImageSize); addAction( m_showImageSize ); m_showRating = actions->add(QString::fromLatin1("viewer-show-rating") ); m_showRating->setText( i18n("Show Rating") ); connect(m_showRating, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowRating); addAction( m_showRating ); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); for( QList::Iterator it = categories.begin(); it != categories.end(); ++it ) { KToggleAction* taction = actions->add( (*it)->name() ); m_actionList.append( taction ); taction->setText( (*it)->name() ); taction->setData( (*it)->name() ); addAction( taction ); connect(taction, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowCategory); } } void Viewer::VisibleOptionsMenu::toggleShowCategory( bool b ) { QAction* action = qobject_cast(sender() ); DB::ImageDB::instance()->categoryCollection()->categoryForName(action->data().value())->setDoShow( b ); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowLabel( bool b ) { Settings::SettingsData::instance()->setShowLabel( b ); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowDescription( bool b ) { Settings::SettingsData::instance()->setShowDescription( b ); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowDate( bool b ) { Settings::SettingsData::instance()->setShowDate( b ); m_showTime->setVisible( b ); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowFilename( bool b ) { Settings::SettingsData::instance()->setShowFilename( b ); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowTime( bool b ) { Settings::SettingsData::instance()->setShowTime( b ); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowEXIF( bool b ) { Settings::SettingsData::instance()->setShowEXIF( b ); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowImageSize( bool b ) { Settings::SettingsData::instance()->setShowImageSize( b ); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowRating( bool b ) { Settings::SettingsData::instance()->setShowRating( b ); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowInfoBox( bool b ) { Settings::SettingsData::instance()->setShowInfoBox( b ); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::updateState() { m_showInfoBox->setChecked( Settings::SettingsData::instance()->showInfoBox() ); m_showLabel->setChecked( Settings::SettingsData::instance()->showLabel() ); m_showDescription->setChecked( Settings::SettingsData::instance()->showDescription() ); m_showDate->setChecked( Settings::SettingsData::instance()->showDate() ); m_showTime->setChecked( Settings::SettingsData::instance()->showTime() ); m_showFileName->setChecked( Settings::SettingsData::instance()->showFilename() ); m_showExif->setChecked( Settings::SettingsData::instance()->showEXIF() ); m_showImageSize->setChecked( Settings::SettingsData::instance()->showImageSize() ); m_showRating->setChecked( Settings::SettingsData::instance()->showRating() ); Q_FOREACH( KToggleAction* action, m_actionList ) { action->setChecked( DB::ImageDB::instance()->categoryCollection()->categoryForName(action->data().value())->doShow() ); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/Database.cpp b/XMLDB/Database.cpp index ab9f14dd..c60c928e 100644 --- a/XMLDB/Database.cpp +++ b/XMLDB/Database.cpp @@ -1,746 +1,746 @@ /* 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 "Database.h" #include "Settings/SettingsData.h" #include #include #include "Utilities/Util.h" #include "DB/GroupCounter.h" #include "Browser/BrowserWidget.h" #include "DB/ImageInfo.h" #include "DB/ImageInfoPtr.h" #include "DB/CategoryCollection.h" #include "XMLCategory.h" #include #include #include "XMLImageDateCollection.h" #include "FileReader.h" #include "FileWriter.h" #include "Exif/Database.h" #include using Utilities::StringSet; bool XMLDB::Database::s_anyImageWithEmptySize = false; XMLDB::Database::Database( const QString& configFile ): m_fileName(configFile) { Utilities::checkForBackupFile( configFile ); FileReader reader( this ); reader.read( configFile ); m_nextStackId = reader.nextStackId(); connect( categoryCollection(), SIGNAL(itemRemoved(DB::Category*,QString)), this, SLOT(deleteItem(DB::Category*,QString)) ); connect( categoryCollection(), SIGNAL(itemRenamed(DB::Category*,QString,QString)), this, SLOT(renameItem(DB::Category*,QString,QString)) ); connect( categoryCollection(), SIGNAL(itemRemoved(DB::Category*,QString)), &m_members, SLOT(deleteItem(DB::Category*,QString)) ); connect( categoryCollection(), SIGNAL(itemRenamed(DB::Category*,QString,QString)), &m_members, SLOT(renameItem(DB::Category*,QString,QString)) ); connect( categoryCollection(), SIGNAL(categoryRemoved(QString)), &m_members, SLOT(deleteCategory(QString))); } uint XMLDB::Database::totalCount() const { return m_images.count(); } /** * I was considering merging the two calls to this method (one for images, one for video), but then I * realized that all the work is really done after the check for whether the given * imageInfo is of the right type, and as a match can't be both, this really * would buy me nothing. */ QMap XMLDB::Database::classify( const DB::ImageSearchInfo& info, const QString &category, DB::MediaType typemask ) { QMap map; DB::GroupCounter counter( category ); Utilities::StringSet alreadyMatched = info.findAlreadyMatched( category ); DB::ImageSearchInfo noMatchInfo = info; QString currentMatchTxt = noMatchInfo.categoryMatchText( category ); if ( currentMatchTxt.isEmpty() ) noMatchInfo.setCategoryMatchText( category, DB::ImageDB::NONE() ); else noMatchInfo.setCategoryMatchText( category, QString::fromLatin1( "%1 & %2" ).arg(currentMatchTxt).arg(DB::ImageDB::NONE()) ); // Iterate through the whole database of images. for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it ) { bool match = ( (*it)->mediaType() & typemask ) && !(*it)->isLocked() && info.match( *it ) && rangeInclude( *it ); if ( match ) { // If the given image is currently matched. // Now iterate through all the categories the current image // contains, and increase them in the map mapping from category // to count. StringSet items = (*it)->itemsOfCategory(category); counter.count( items ); for( StringSet::const_iterator it2 = items.begin(); it2 != items.end(); ++it2 ) { if ( !alreadyMatched.contains(*it2) ) // We do not want to match "Jesper & Jesper" map[*it2]++; } // Find those with no other matches if ( noMatchInfo.match( *it ) ) map[DB::ImageDB::NONE()]++; } } QMap groups = counter.result(); for( QMap::iterator it= groups.begin(); it != groups.end(); ++it ) { map[it.key()] = it.value(); } return map; } void XMLDB::Database::renameCategory( const QString& oldName, const QString newName ) { for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { (*it)->renameCategory( oldName, newName ); } } void XMLDB::Database::addToBlockList(const DB::FileNameList& list) { Q_FOREACH(const DB::FileName& fileName, list) { m_blockList.insert(fileName); } deleteList( list ); } void XMLDB::Database::deleteList(const DB::FileNameList& list) { Q_FOREACH(const DB::FileName& fileName, list) { DB::ImageInfoPtr inf = fileName.info(); StackMap::iterator found = m_stackMap.find(inf->stackId()); if ( inf->isStacked() && found != m_stackMap.end() ) { const DB::FileNameList origCache = found.value(); DB::FileNameList newCache; Q_FOREACH(const DB::FileName& cacheName, origCache) { if (fileName != cacheName) newCache.append(cacheName); } if (newCache.size() <= 1) { // we're destroying a stack Q_FOREACH(const DB::FileName& cacheName, newCache) { DB::ImageInfoPtr cacheInf = cacheName.info(); cacheInf->setStackId(0); cacheInf->setStackOrder(0); } m_stackMap.remove( inf->stackId() ); } else { m_stackMap.insert(inf->stackId(), newCache); } } m_imageCache.remove( inf->fileName().absolute() ); m_images.remove( inf ); } Exif::Database::instance()->remove( list ); emit totalChanged( m_images.count() ); emit imagesDeleted(list); emit dirty(); } void XMLDB::Database::renameItem( DB::Category* category, const QString& oldName, const QString& newName ) { for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { (*it)->renameItem( category->name(), oldName, newName ); } } void XMLDB::Database::deleteItem( DB::Category* category, const QString& value ) { for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { (*it)->removeCategoryInfo( category->name(), value ); } } void XMLDB::Database::lockDB( bool lock, bool exclude ) { DB::ImageSearchInfo info = Settings::SettingsData::instance()->currentLock(); for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { if ( lock ) { bool match = info.match( *it ); if ( !exclude ) match = !match; (*it)->setLocked( match ); } else (*it)->setLocked( false ); } } void XMLDB::Database::clearDelayedImages() { m_delayedCache.clear(); m_delayedUpdate.clear(); } void XMLDB::Database::forceUpdate( const DB::ImageInfoList& images ) { // FIXME: merge stack information DB::ImageInfoList newImages = images.sort(); if ( m_images.count() == 0 ) { // case 1: The existing imagelist is empty. Q_FOREACH( const DB::ImageInfoPtr& imageInfo, newImages ) m_imageCache.insert( imageInfo->fileName().absolute(), imageInfo ); m_images = newImages; } else if ( newImages.count() == 0 ) { // case 2: No images to merge in - that's easy ;-) return; } else if ( newImages.first()->date().start() > m_images.last()->date().start() ) { // case 2: The new list is later than the existsing Q_FOREACH( const DB::ImageInfoPtr& imageInfo, newImages ) m_imageCache.insert( imageInfo->fileName().absolute(), imageInfo ); m_images.appendList(newImages); } else if ( m_images.isSorted() ) { // case 3: The lists overlaps, and the existsing list is sorted Q_FOREACH( const DB::ImageInfoPtr& imageInfo, newImages ) m_imageCache.insert( imageInfo->fileName().absolute(), imageInfo ); m_images.mergeIn( newImages ); } else{ // case 4: The lists overlaps, and the existsing list is not sorted in the overlapping range. Q_FOREACH( const DB::ImageInfoPtr& imageInfo, newImages ) m_imageCache.insert( imageInfo->fileName().absolute(), imageInfo ); m_images.appendList( newImages ); } } void XMLDB::Database::addImages( const DB::ImageInfoList& images, bool doUpdate ) { Q_FOREACH( const DB::ImageInfoPtr& info, images ) { info->addCategoryInfo( i18n( "Media Type" ), info->mediaType() == DB::Image ? i18n( "Image" ) : i18n( "Video" ) ); m_delayedCache.insert( info->fileName().absolute(), info ); m_delayedUpdate << info; } if ( doUpdate ) { commitDelayedImages(); } } void XMLDB::Database::commitDelayedImages() { uint imagesAdded = m_delayedUpdate.count(); if ( imagesAdded > 0 ) { forceUpdate(m_delayedUpdate); m_delayedCache.clear(); m_delayedUpdate.clear(); - // It's the responsibility of the caller to add the EXIF information. + // It's the responsibility of the caller to add the Exif information. // It's more efficient from an I/O perspective to minimize the number - // of passes over the images, and with the ability to add the EXIF + // of passes over the images, and with the ability to add the Exif // data in a transaction, there's no longer any need to read it here. emit totalChanged( m_images.count() ); emit dirty(); } } void XMLDB::Database::renameImage( DB::ImageInfoPtr info, const DB::FileName& newName ) { info->delaySavingChanges(false); info->setFileName(newName); } DB::ImageInfoPtr XMLDB::Database::info( const DB::FileName& fileName ) const { if ( fileName.isNull() ) return DB::ImageInfoPtr(); const QString name = fileName.absolute(); if (m_imageCache.contains( name )) return m_imageCache[name]; if (m_delayedCache.contains( name )) return m_delayedCache[name]; Q_FOREACH( const DB::ImageInfoPtr& imageInfo, m_images ) m_imageCache.insert( imageInfo->fileName().absolute(), imageInfo ); if ( m_imageCache.contains( name ) ) { return m_imageCache[ name ]; } return DB::ImageInfoPtr(); } bool XMLDB::Database::rangeInclude( DB::ImageInfoPtr info ) const { if (m_selectionRange.start().isNull() ) return true; DB::ImageDate::MatchType tp = info->date().isIncludedIn( m_selectionRange ); if ( m_includeFuzzyCounts ) return ( tp == DB::ImageDate::ExactMatch || tp == DB::ImageDate::RangeMatch ); else return ( tp == DB::ImageDate::ExactMatch ); } DB::MemberMap& XMLDB::Database::memberMap() { return m_members; } void XMLDB::Database::save( const QString& fileName, bool isAutoSave ) { FileWriter saver( this ); saver.save( fileName, isAutoSave ); } DB::MD5Map* XMLDB::Database::md5Map() { return &m_md5map; } bool XMLDB::Database::isBlocking( const DB::FileName& fileName ) { return m_blockList.contains( fileName ); } DB::FileNameList XMLDB::Database::images() { return m_images.files(); } DB::FileNameList XMLDB::Database::search( const DB::ImageSearchInfo& info, bool requireOnDisk) const { return searchPrivate( info, requireOnDisk, true ); } DB::FileNameList XMLDB::Database::searchPrivate( const DB::ImageSearchInfo& info, bool requireOnDisk, bool onlyItemsMatchingRange) const { // When searching for images counts for the datebar, we want matches outside the range too. // When searching for images for the thumbnail view, we only want matches inside the range. DB::FileNameList result; for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it ) { bool match = !(*it)->isLocked() && info.match( *it ) && ( !onlyItemsMatchingRange || rangeInclude( *it )); match &= !requireOnDisk || DB::ImageInfo::imageOnDisk( (*it)->fileName() ); if (match) result.append((*it)->fileName()); } return result; } void XMLDB::Database::sortAndMergeBackIn(const DB::FileNameList& fileNameList) { DB::ImageInfoList infoList; Q_FOREACH( const DB::FileName &fileName, fileNameList ) infoList.append(fileName.info()); m_images.sortAndMergeBackIn(infoList); } DB::CategoryCollection* XMLDB::Database::categoryCollection() { return &m_categoryCollection; } QExplicitlySharedDataPointer XMLDB::Database::rangeCollection() { return QExplicitlySharedDataPointer( new XMLImageDateCollection( searchPrivate( Browser::BrowserWidget::instance()->currentContext(), false, false))); } void XMLDB::Database::reorder( const DB::FileName& item, const DB::FileNameList& selection, bool after) { Q_ASSERT(!item.isNull()); DB::ImageInfoList list = takeImagesFromSelection(selection); insertList(item, list, after ); } // Remove all the images from the database that match the given selection and // return that sublist. // This returns the selected and erased images in the order in which they appear // in the image list itself. DB::ImageInfoList XMLDB::Database::takeImagesFromSelection(const DB::FileNameList& selection) { DB::ImageInfoList result; if (selection.isEmpty()) return result; // iterate over all images (expensive!!) TODO: improve? for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); /**/ ) { const DB::FileName imagefile = (*it)->fileName(); DB::FileNameList::const_iterator si = selection.begin(); // for each image, iterate over selection, break on match for ( /**/; si != selection.end(); ++si ) { const DB::FileName file = *si; if ( imagefile == file ) { break; } } // if image is not in selection, simply advance to next, if not add to result and erase if (si == selection.end()) { ++it; } else { result << *it; m_imageCache.remove( (*it)->fileName().absolute() ); it = m_images.erase(it); } // if all images from selection are in result (size of lists is equal) break. if (result.size() == selection.size()) break; } return result; } void XMLDB::Database::insertList( const DB::FileName& fileName, const DB::ImageInfoList& list, bool after) { DB::ImageInfoListIterator imageIt = m_images.begin(); for( ; imageIt != m_images.end(); ++imageIt ) { if ( (*imageIt)->fileName() == fileName ) { break; } } // since insert() inserts before iterator increment when inserting AFTER image if ( after ) imageIt++; for( DB::ImageInfoListConstIterator it = list.begin(); it != list.end(); ++it ) { // the call to insert() destroys the given iterator so use the new one after the call imageIt = m_images.insert( imageIt, *it ); m_imageCache.insert( (*it)->fileName().absolute(), *it ); // increment always to retain order of selected images imageIt++; } emit dirty(); } bool XMLDB::Database::stack(const DB::FileNameList& items) { unsigned int changed = 0; QSet stacks; QList images; unsigned int stackOrder = 1; Q_FOREACH(const DB::FileName& fileName, items) { DB::ImageInfoPtr imgInfo = fileName.info(); Q_ASSERT( imgInfo ); if ( imgInfo->isStacked() ) { stacks << imgInfo->stackId(); stackOrder = qMax( stackOrder, imgInfo->stackOrder() + 1 ); } else { images << imgInfo; } } if ( stacks.size() > 1 ) return false; // images already in different stacks -> can't stack DB::StackID stackId = ( stacks.size() == 1 ) ? *(stacks.begin() ) : m_nextStackId++; Q_FOREACH( DB::ImageInfoPtr info, images ) { info->setStackOrder( stackOrder ); info->setStackId( stackId ); m_stackMap[stackId].append(info->fileName()); ++changed; ++stackOrder; } if ( changed ) emit dirty(); return changed; } void XMLDB::Database::unstack(const DB::FileNameList& items) { Q_FOREACH(const DB::FileName& fileName, items) { DB::FileNameList allInStack = getStackFor(fileName); if (allInStack.size() <= 2) { // we're destroying stack here Q_FOREACH(const DB::FileName& stackFileName, allInStack) { DB::ImageInfoPtr imgInfo = stackFileName.info(); Q_ASSERT( imgInfo ); if ( imgInfo->isStacked() ) { m_stackMap.remove( imgInfo->stackId() ); imgInfo->setStackId( 0 ); imgInfo->setStackOrder( 0 ); } } } else { DB::ImageInfoPtr imgInfo = fileName.info(); Q_ASSERT( imgInfo ); if ( imgInfo->isStacked() ) { m_stackMap[imgInfo->stackId()].removeAll(fileName); imgInfo->setStackId( 0 ); imgInfo->setStackOrder( 0 ); } } } if (!items.isEmpty()) emit dirty(); } DB::FileNameList XMLDB::Database::getStackFor(const DB::FileName& referenceImg) const { DB::ImageInfoPtr imageInfo = info( referenceImg ); if ( !imageInfo || ! imageInfo->isStacked() ) return DB::FileNameList(); StackMap::iterator found = m_stackMap.find(imageInfo->stackId()); if ( found != m_stackMap.end() ) return found.value(); // it wasn't in the cache -> rebuild it m_stackMap.clear(); for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it ) { if ( (*it)->isStacked() ) { DB::StackID stackid = (*it)->stackId(); m_stackMap[stackid].append((*it)->fileName()); } } found = m_stackMap.find(imageInfo->stackId()); if ( found != m_stackMap.end() ) return found.value(); else return DB::FileNameList(); } void XMLDB::Database::copyData(const DB::FileName &from, const DB::FileName &to) { (*info(to)).merge(*info(from)); } int XMLDB::Database::fileVersion() { // File format version, bump it up every time the format for the file changes. return 8; } // During profiling of loading, I found that a significant amount of time was spent in QDateTime::fromString. // Reviewing the code, I fount that it did a lot of extra checks we don't need (like checking if the string have // timezone information (which they won't in KPA), this function is a replacement that is faster than the original. QDateTime dateTimeFromString(const QString& str) { static QChar T = QChar::fromLatin1('T'); if ( str[10] == T) return QDateTime(QDate::fromString(str.left(10), Qt::ISODate),QTime::fromString(str.mid(11),Qt::ISODate)); else return QDateTime::fromString(str,Qt::ISODate); } DB::ImageInfoPtr XMLDB::Database::createImageInfo( const DB::FileName& fileName, ReaderPtr reader, Database* db, const QMap *newToOldCategory ) { static QString _label_ = QString::fromUtf8("label"); static QString _description_ = QString::fromUtf8("description"); static QString _startDate_ = QString::fromUtf8("startDate"); static QString _endDate_ = QString::fromUtf8("endDate"); static QString _yearFrom_ = QString::fromUtf8("yearFrom"); static QString _monthFrom_ = QString::fromUtf8("monthFrom"); static QString _dayFrom_ = QString::fromUtf8("dayFrom"); static QString _hourFrom_ = QString::fromUtf8("hourFrom"); static QString _minuteFrom_ = QString::fromUtf8("minuteFrom"); static QString _secondFrom_ = QString::fromUtf8("secondFrom"); static QString _yearTo_ = QString::fromUtf8("yearTo"); static QString _monthTo_ = QString::fromUtf8("monthTo"); static QString _dayTo_ = QString::fromUtf8("dayTo"); static QString _angle_ = QString::fromUtf8("angle"); static QString _md5sum_ = QString::fromUtf8("md5sum"); static QString _width_ = QString::fromUtf8("width"); static QString _height_ = QString::fromUtf8("height"); static QString _rating_ = QString::fromUtf8("rating"); static QString _stackId_ = QString::fromUtf8("stackId"); static QString _stackOrder_ = QString::fromUtf8("stackOrder"); static QString _gpsPrec_ = QString::fromUtf8("gpsPrec"); static QString _gpsLon_ = QString::fromUtf8("gpsLon"); static QString _gpsLat_ = QString::fromUtf8("gpsLat"); static QString _gpsAlt_ = QString::fromUtf8("gpsAlt"); static QString _videoLength_ = QString::fromUtf8("videoLength"); static QString _options_ = QString::fromUtf8("options"); static QString _0_ = QString::fromUtf8("0"); static QString _minus1_ = QString::fromUtf8("-1"); static QString _MediaType_ = i18n("Media Type"); static QString _Image_ = i18n("Image"); static QString _Video_ = i18n("Video"); QString label; if (reader->hasAttribute(_label_)) label = reader->attribute(_label_); else label = QFileInfo(fileName.relative()).completeBaseName(); QString description; if ( reader->hasAttribute(_description_) ) description = reader->attribute(_description_); DB::ImageDate date; if ( reader->hasAttribute(_startDate_) ) { QDateTime start; QString str = reader->attribute( _startDate_ ); if ( !str.isEmpty() ) start = dateTimeFromString( str ); str = reader->attribute( _endDate_ ); if ( !str.isEmpty() ) date = DB::ImageDate( start, dateTimeFromString(str) ); else date = DB::ImageDate( start ); } else { int yearFrom = 0, monthFrom = 0, dayFrom = 0, yearTo = 0, monthTo = 0, dayTo = 0, hourFrom = -1, minuteFrom = -1, secondFrom = -1; yearFrom = reader->attribute( _yearFrom_, _0_ ).toInt(); monthFrom = reader->attribute( _monthFrom_, _0_ ).toInt(); dayFrom = reader->attribute( _dayFrom_, _0_ ).toInt(); hourFrom = reader->attribute( _hourFrom_, _minus1_ ).toInt(); minuteFrom = reader->attribute( _minuteFrom_, _minus1_ ).toInt(); secondFrom = reader->attribute( _secondFrom_, _minus1_ ).toInt(); yearTo = reader->attribute( _yearTo_, _0_ ).toInt(); monthTo = reader->attribute( _monthTo_, _0_ ).toInt(); dayTo = reader->attribute( _dayTo_, _0_ ).toInt(); date = DB::ImageDate( yearFrom, monthFrom, dayFrom, yearTo, monthTo, dayTo, hourFrom, minuteFrom, secondFrom ); } int angle = reader->attribute( _angle_, _0_).toInt(); DB::MD5 md5sum(reader->attribute( _md5sum_ )); s_anyImageWithEmptySize |= !reader->hasAttribute(_width_); int w = reader->attribute( _width_ , _minus1_ ).toInt(); int h = reader->attribute( _height_ , _minus1_ ).toInt(); QSize size = QSize( w,h ); DB::MediaType mediaType = Utilities::isVideo(fileName) ? DB::Video : DB::Image; short rating = reader->attribute( _rating_, _minus1_ ).toShort(); DB::StackID stackId = reader->attribute( _stackId_, _0_ ).toULong(); unsigned int stackOrder = reader->attribute( _stackOrder_, _0_ ).toULong(); DB::ImageInfo* info = new DB::ImageInfo( fileName, label, description, date, angle, md5sum, size, mediaType, rating, stackId, stackOrder ); if ( reader->hasAttribute(_videoLength_)) info->setVideoLength(reader->attribute(_videoLength_).toInt()); DB::ImageInfoPtr result(info); possibleLoadCompressedCategories( reader, result, db, newToOldCategory ); while( reader->readNextStartOrStopElement(_options_).isStartToken) { readOptions( result, reader, newToOldCategory ); } info->addCategoryInfo( _MediaType_, info->mediaType() == DB::Image ? _Image_ : _Video_ ); return result; } void XMLDB::Database::readOptions( DB::ImageInfoPtr info, ReaderPtr reader, const QMap *newToOldCategory ) { static QString _name_ = QString::fromUtf8("name"); static QString _value_ = QString::fromUtf8("value"); static QString _option_ = QString::fromUtf8("option"); static QString _area_ = QString::fromUtf8("area"); while (reader->readNextStartOrStopElement(_option_).isStartToken) { QString name = FileReader::unescape( reader->attribute(_name_) ); // If the silent update to db version 6 has been done, use the updated category names. if (newToOldCategory) { name = newToOldCategory->key(name,name); } if ( !name.isNull() ) { // Read values while (reader->readNextStartOrStopElement(_value_).isStartToken) { QString value = reader->attribute(_value_); if (reader->hasAttribute(_area_)) { QStringList areaData = reader->attribute(_area_).split(QString::fromUtf8(" ")); int x = areaData[0].toInt(); int y = areaData[1].toInt(); int w = areaData[2].toInt(); int h = areaData[3].toInt(); QRect area = QRect(QPoint(x, y), QPoint(x + w - 1, y + h - 1)); if (! value.isNull()) { info->addCategoryInfo(name, value, area); } } else { if (! value.isNull()) { info->addCategoryInfo(name, value); } } reader->readEndElement(); } } } } void XMLDB::Database::possibleLoadCompressedCategories( ReaderPtr reader, DB::ImageInfoPtr info, Database* db, const QMap *newToOldCategory ) { if ( db == nullptr ) return; Q_FOREACH( const DB::CategoryPtr categoryPtr, db->m_categoryCollection.categories() ) { QString categoryName = categoryPtr->name(); QString oldCategoryName; if ( newToOldCategory ) { // translate to old categoryName, defaulting to the original name if not found: oldCategoryName = newToOldCategory->value( categoryName, categoryName ); } else { oldCategoryName = categoryName; } QString str = reader->attribute( FileWriter::escape( oldCategoryName ) ); if ( !str.isEmpty() ) { QStringList list = str.split(QString::fromLatin1( "," ), QString::SkipEmptyParts ); Q_FOREACH( const QString &tagString, list ) { int id = tagString.toInt(); QString name = static_cast(categoryPtr.data())->nameForId(id); info->addCategoryInfo( categoryName, name ); } } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileReader.cpp b/XMLDB/FileReader.cpp index d63dda2b..862f0a71 100644 --- a/XMLDB/FileReader.cpp +++ b/XMLDB/FileReader.cpp @@ -1,536 +1,536 @@ /* 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. */ // Local includes #include "CompressFileInfo.h" #include "Database.h" #include "FileReader.h" #include "Logging.h" #include "XMLCategory.h" #include #include #include #include // KDE includes #include #include // Qt includes #include #include #include #include #include #include void XMLDB::FileReader::read( const QString& configFile ) { static QString versionString = QString::fromUtf8("version"); static QString compressedString = QString::fromUtf8("compressed"); ReaderPtr reader = readConfigFile( configFile ); ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KPhotoAlbum")); if (!info.isStartToken) reader->complainStartElementExpected(QString::fromUtf8("KPhotoAlbum")); m_fileVersion = reader->attribute( versionString, QString::fromLatin1( "1" ) ).toInt(); if ( m_fileVersion > Database::fileVersion() ) { int ret = KMessageBox::warningContinueCancel( messageParent(), i18n("

    The database file (index.xml) is from a newer version of KPhotoAlbum!

    " "

    Chances are you will be able to read this file, but when writing it back, " "information saved in the newer version will be lost

    "), i18n("index.xml version mismatch"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString::fromLatin1( "checkDatabaseFileVersion" ) ); if (ret == KStandardGuiItem::Cancel) exit(-1); } setUseCompressedFileFormat( reader->attribute(compressedString).toInt() ); m_db->m_members.setLoading( true ); loadCategories( reader ); loadImages( reader ); loadBlockList( reader ); loadMemberGroups( reader ); //loadSettings(reader); m_db->m_members.setLoading( false ); checkIfImagesAreSorted(); checkIfAllImagesHaveSizeAttributes(); } void XMLDB::FileReader::createSpecialCategories() { // Setup the "Folder" category m_folderCategory = new XMLCategory(i18n("Folder"), QString::fromLatin1("folder"), DB::Category::TreeView, 32, false ); m_folderCategory->setType( DB::Category::FolderCategory ); // The folder category is not stored in the index.xml file, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if ( m_db->m_categoryCollection.categoryForName(m_folderCategory->name()) ) m_db->m_categoryCollection.removeCategory(m_folderCategory->name()); m_db->m_categoryCollection.addCategory( m_folderCategory ); dynamic_cast( m_folderCategory.data() )->setShouldSave( false ); // Setup the "Tokens" category DB::CategoryPtr tokenCat; if (m_fileVersion >= 7) { tokenCat = m_db->m_categoryCollection.categoryForSpecial( DB::Category::TokensCategory ); } else { // Before version 7, the "Tokens" category name wasn't stored to the settings. So ... // look for a literal "Tokens" category ... tokenCat = m_db->m_categoryCollection.categoryForName(QString::fromUtf8("Tokens")); if (!tokenCat) { // ... and a translated "Tokens" category if we don't have the literal one. tokenCat = m_db->m_categoryCollection.categoryForName(i18n("Tokens")); } if (tokenCat) { // in this case we need to give the tokens category its special meaning: m_db->m_categoryCollection.removeCategory(tokenCat->name()); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } } if (! tokenCat) { // Create a new "Tokens" category tokenCat = new XMLCategory(i18n("Tokens"), QString::fromUtf8("tag"), DB::Category::TreeView, 32, true); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } // KPhotoAlbum 2.2 did not write the tokens to the category section, // so unless we do this small trick they will not show up when importing. for (char ch = 'A'; ch <= 'Z'; ++ch) { tokenCat->addItem(QString::fromUtf8("%1").arg(QChar::fromLatin1(ch))); } // Setup the "Media Type" category DB::CategoryPtr mediaCat; mediaCat = new XMLCategory(i18n("Media Type"), QString::fromLatin1("view-categories"), DB::Category::TreeView, 32, false); mediaCat->addItem( i18n( "Image" ) ); mediaCat->addItem( i18n( "Video" ) ); mediaCat->setType( DB::Category::MediaTypeCategory ); dynamic_cast( mediaCat.data() )->setShouldSave( false ); // The media type is not stored in the media category, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if ( m_db->m_categoryCollection.categoryForName(mediaCat->name()) ) m_db->m_categoryCollection.removeCategory( mediaCat->name() ); m_db->m_categoryCollection.addCategory( mediaCat ); } void XMLDB::FileReader::loadCategories( ReaderPtr reader ) { static QString nameString = QString::fromUtf8("name"); static QString iconString = QString::fromUtf8("icon"); static QString viewTypeString = QString::fromUtf8("viewtype"); static QString showString = QString::fromUtf8("show"); static QString thumbnailSizeString = QString::fromUtf8("thumbnailsize"); static QString positionableString = QString::fromUtf8("positionable"); static QString metaString = QString::fromUtf8("meta"); static QString tokensString = QString::fromUtf8("tokens"); static QString valueString = QString::fromUtf8("value"); static QString idString = QString::fromUtf8("id"); static QString birthDateString = QString::fromUtf8("birthDate"); static QString categoriesString = QString::fromUtf8("Categories"); static QString categoryString = QString::fromUtf8("Category"); ElementInfo info = reader->readNextStartOrStopElement(categoriesString); if (!info.isStartToken) reader->complainStartElementExpected(categoriesString); while ( reader->readNextStartOrStopElement(categoryString).isStartToken) { const QString categoryName = unescape(reader->attribute(nameString)); if ( !categoryName.isNull() ) { // Read Category info QString icon = reader->attribute(iconString); DB::Category::ViewType type = (DB::Category::ViewType) reader->attribute( viewTypeString, QString::fromLatin1( "0" ) ).toInt(); int thumbnailSize = reader->attribute( thumbnailSizeString, QString::fromLatin1( "32" ) ).toInt(); bool show = (bool) reader->attribute( showString, QString::fromLatin1( "1" ) ).toInt(); bool positionable = (bool) reader->attribute( positionableString, QString::fromLatin1( "0" ) ).toInt(); bool tokensCat = reader->attribute(metaString) == tokensString; DB::CategoryPtr cat = m_db->m_categoryCollection.categoryForName( categoryName ); bool repairMode = false; if (cat) { int choice = KMessageBox::warningContinueCancel( messageParent(), i18n( "

    Line %1, column %2: duplicate category '%3'

    " "

    Choose continue to ignore the duplicate category and try an automatic repair, " "or choose cancel to quit.

    ", reader->lineNumber(), reader->columnNumber(), categoryName ), i18n("Error in database file")); if ( choice == KMessageBox::Continue ) repairMode = true; else exit(-1); } else { cat = new XMLCategory( categoryName, icon, type, thumbnailSize, show, positionable ); if (tokensCat) cat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory( cat ); } // Read values QStringList items; while( reader->readNextStartOrStopElement(valueString).isStartToken) { QString value = reader->attribute(valueString); if ( reader->hasAttribute(idString) ) { int id = reader->attribute(idString).toInt(); static_cast(cat.data())->setIdMapping( value, id ); } if (reader->hasAttribute(birthDateString)) cat->setBirthDate(value,QDate::fromString(reader->attribute(birthDateString), Qt::ISODate)); items.append( value ); reader->readEndElement(); } if ( repairMode ) { // merge with duplicate category qCInfo(XMLDBLog) << "Repairing category " << categoryName << ": merging items " << cat->items() << " with " << items; items.append(cat->items()); items.removeDuplicates(); } cat->setItems( items ); } } createSpecialCategories(); if (m_fileVersion < 7) { KMessageBox::information( messageParent(), i18nc("Leave \"Folder\" and \"Media Type\" untranslated below, those will show up with " "these exact names. Thanks :-)", "

    This version of KPhotoAlbum does not translate \"standard\" categories " "any more.

    " "

    This may mean that – if you use a locale other than English – some of your " "categories are now displayed in English.

    " "

    You can manually rename your categories any time and then save your database." "

    " "

    In some cases, you may get two additional empty categories, \"Folder\" and " "\"Media Type\". You can delete those.

    "), i18n("Changed standard category names") ); } } void XMLDB::FileReader::loadImages( ReaderPtr reader ) { static QString fileString = QString::fromUtf8("file"); static QString imagesString = QString::fromUtf8("images"); static QString imageString = QString::fromUtf8("image"); ElementInfo info = reader->readNextStartOrStopElement(imagesString); if (!info.isStartToken) reader->complainStartElementExpected(imagesString); while (reader->readNextStartOrStopElement(imageString).isStartToken) { const QString fileNameStr = reader->attribute(fileString); if ( fileNameStr.isNull() ) { qCWarning(XMLDBLog, "Element did not contain a file attribute" ); return; } const DB::FileName dbFileName = DB::FileName::fromRelativePath(fileNameStr); DB::ImageInfoPtr info = load( dbFileName, reader ); m_db->m_images.append(info); m_db->m_md5map.insert( info->MD5Sum(), dbFileName ); } } void XMLDB::FileReader::loadBlockList( ReaderPtr reader ) { static QString fileString = QString::fromUtf8("file"); static QString blockListString = QString::fromUtf8("blocklist"); static QString blockString = QString::fromUtf8("block"); ElementInfo info = reader->peekNext(); if ( info.isStartToken && info.tokenName == blockListString ) { reader->readNextStartOrStopElement(blockListString); while (reader->readNextStartOrStopElement(blockString).isStartToken) { QString fileName = reader->attribute(fileString); if ( !fileName.isEmpty() ) m_db->m_blockList.insert(DB::FileName::fromRelativePath(fileName)); reader->readEndElement(); } } } void XMLDB::FileReader::loadMemberGroups( ReaderPtr reader ) { static QString categoryString = QString::fromUtf8("category"); static QString groupNameString = QString::fromUtf8("group-name"); static QString memberString = QString::fromUtf8("member"); static QString membersString = QString::fromUtf8("members"); static QString memberGroupsString = QString::fromUtf8("member-groups"); ElementInfo info = reader->peekNext(); if ( info.isStartToken && info.tokenName == memberGroupsString) { reader->readNextStartOrStopElement(memberGroupsString); while(reader->readNextStartOrStopElement(memberString).isStartToken) { QString category = reader->attribute(categoryString); QString group = reader->attribute(groupNameString); if ( reader->hasAttribute(memberString) ) { QString member = reader->attribute(memberString); m_db->m_members.addMemberToGroup( category, group, member ); } else { QStringList members = reader->attribute(membersString).split( QString::fromLatin1( "," ), QString::SkipEmptyParts ); Q_FOREACH( const QString &memberItem, members ) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName( category ); if (!catPtr) { // category was not declared in "Categories" qCWarning(XMLDBLog) << "File corruption in index.xml. Inserting missing category: " << category; catPtr = new XMLCategory(category, QString::fromUtf8("dialog-warning"), DB::Category::TreeView, 32, false); m_db->m_categoryCollection.addCategory( catPtr ); } XMLCategory* cat = static_cast( catPtr.data() ); QString member = cat->nameForId( memberItem.toInt() ); if (member.isNull()) continue; m_db->m_members.addMemberToGroup( category, group, member ); } if(members.size() == 0) { // Groups are stored even if they are empty, so we also have to read them. // With no members, the above for loop will not be executed. m_db->m_members.addGroup(category, group); } } reader->readEndElement(); } } } /* void XMLDB::FileReader::loadSettings(ReaderPtr reader) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == settingsString) { reader->readNextStartOrStopElement(settingString); while(reader->readNextStartOrStopElement(settingString).isStartToken) { if (reader->hasAttribute(keyString) && reader->hasAttribute(valueString)) { m_db->m_settings.insert(unescape(reader->attribute(keyString)), unescape(reader->attribute(valueString))); } else { qWarning() << "File corruption in index.xml. Setting either lacking a key or a " << "value attribute. Ignoring this entry."; } reader->readEndElement(); } } } */ void XMLDB::FileReader::checkIfImagesAreSorted() { if ( !KMessageBox::shouldBeShownContinue( QString::fromLatin1( "checkWhetherImagesAreSorted" ) ) ) return; QDateTime last( QDate( 1900, 1, 1 ) ); bool wrongOrder = false; for( DB::ImageInfoListIterator it = m_db->m_images.begin(); !wrongOrder && it != m_db->m_images.end(); ++it ) { if ( last > (*it)->date().start() && (*it)->date().start().isValid() ) wrongOrder = true; last = (*it)->date().start(); } if ( wrongOrder ) { KMessageBox::information( messageParent(), i18n("

    Your images/videos are not sorted, which means that navigating using the date bar " "will only work suboptimally.

    " "

    In the Maintenance menu, you can find Display Images with Incomplete Dates " "which you can use to find the images that are missing date information.

    " "

    You can then select the images that you have reason to believe have a correct date " - "in either their EXIF data or on the file, and execute Maintenance->Read EXIF Info " + "in either their Exif data or on the file, and execute Maintenance->Read Exif Info " "to reread the information.

    " "

    Finally, once all images have their dates set, you can execute " "Maintenance->Sort All by Date & Time to sort them in the database.

    "), i18n("Images/Videos Are Not Sorted"), QString::fromLatin1( "checkWhetherImagesAreSorted" ) ); } } void XMLDB::FileReader::checkIfAllImagesHaveSizeAttributes() { QTime time; time.start(); if ( !KMessageBox::shouldBeShownContinue( QString::fromLatin1( "checkWhetherAllImagesIncludesSize" ) ) ) return; if ( m_db->s_anyImageWithEmptySize ) { KMessageBox::information( messageParent(), i18n("

    Not all the images in the database have information about image sizes; this is needed to " "get the best result in the thumbnail view. To fix this, simply go to the Maintenance menu, " "and first choose Remove All Thumbnails, and after that choose Build Thumbnails.

    " "

    Not doing so will result in extra space around images in the thumbnail view - that is all - so " "there is no urgency in doing it.

    "), i18n("Not All Images Have Size Information"), QString::fromLatin1( "checkWhetherAllImagesIncludesSize" ) ); } } DB::ImageInfoPtr XMLDB::FileReader::load( const DB::FileName& fileName, ReaderPtr reader ) { DB::ImageInfoPtr info = XMLDB::Database::createImageInfo(fileName, reader, m_db); m_nextStackId = qMax( m_nextStackId, info->stackId() + 1 ); info->createFolderCategoryItem( m_folderCategory, m_db->m_members ); return info; } XMLDB::ReaderPtr XMLDB::FileReader::readConfigFile( const QString& configFile ) { ReaderPtr reader = ReaderPtr(new XmlReader); QFile file( configFile ); if ( !file.exists() ) { // Load a default setup QFile file(Utilities::locateDataFile(QString::fromLatin1("default-setup"))); if ( !file.open( QIODevice::ReadOnly ) ) { KMessageBox::information( messageParent(), i18n( "

    KPhotoAlbum was unable to load a default setup, which indicates an installation error

    " "

    If you have installed KPhotoAlbum yourself, then you must remember to set the environment variable " "KDEDIRS, to point to the topmost installation directory.

    " "

    If you for example ran cmake with -DCMAKE_INSTALL_PREFIX=/usr/local/kde, then you must use the following " "environment variable setup (this example is for Bash and compatible shells):

    " "

    export KDEDIRS=/usr/local/kde

    " "

    In case you already have KDEDIRS set, simply append the string as if you where setting the PATH " "environment variable

    "), i18n("No default setup file found") ); } else { QTextStream stream( &file ); stream.setCodec( QTextCodec::codecForName("UTF-8") ); QString str = stream.readAll(); // Replace the default setup's category and tag names with localized ones str = str.replace(QString::fromUtf8("People"), i18n("People")); str = str.replace(QString::fromUtf8("Places"), i18n("Places")); str = str.replace(QString::fromUtf8("Events"), i18n("Events")); str = str.replace(QString::fromUtf8("untagged"), i18n("untagged")); str = str.replace( QRegExp( QString::fromLatin1("imageDirectory=\"[^\"]*\"")), QString::fromLatin1("") ); str = str.replace( QRegExp( QString::fromLatin1("htmlBaseDir=\"[^\"]*\"")), QString::fromLatin1("") ); str = str.replace( QRegExp( QString::fromLatin1("htmlBaseURL=\"[^\"]*\"")), QString::fromLatin1("") ); reader->addData(str); } } else { if ( !file.open( QIODevice::ReadOnly ) ) { KMessageBox::error( messageParent(), i18n("Unable to open '%1' for reading", configFile ), i18n("Error Running Demo") ); exit(-1); } reader->addData(file.readAll()); #if 0 QString errMsg; int errLine; int errCol; if ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol )) { file.close(); // If parsing index.xml fails let's see if we could use a backup instead Utilities::checkForBackupFile( configFile, i18n( "line %1 column %2 in file %3: %4", errLine , errCol , configFile , errMsg ) ); if ( !file.open( QIODevice::ReadOnly ) || ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol ) ) ) { KMessageBox::error( messageParent(), i18n( "Failed to recover the backup: %1", errMsg ) ); exit(-1); } } #endif } // Now read the content of the file. #if 0 QDomElement top = doc.documentElement(); if ( top.isNull() ) { KMessageBox::error( messageParent(), i18n("Error in file %1: No elements found", configFile ) ); exit(-1); } if ( top.tagName().toLower() != QString::fromLatin1( "kphotoalbum" ) && top.tagName().toLower() != QString::fromLatin1( "kimdaba" ) ) { // KimDaBa compatibility KMessageBox::error( messageParent(), i18n("Error in file %1: expected 'KPhotoAlbum' as top element but found '%2'", configFile , top.tagName() ) ); exit(-1); } #endif file.close(); return reader; } QString XMLDB::FileReader::unescape( const QString& str ) { static QHash cache; if ( cache.contains(str) ) return cache[str]; QString tmp( str ); // Matches encoded characters in attribute names QRegExp rx( QString::fromLatin1( "(_.)([0-9A-F]{2})" ) ); int pos = 0; // Unencoding special characters if compressed XML is selected if ( useCompressedFileFormat() ) { while ( ( pos = rx.indexIn( tmp, pos ) ) != -1 ) { QString before = rx.cap( 1 ) + rx.cap( 2 ); QString after = QString::fromLatin1( QByteArray::fromHex( rx.cap( 2 ).toLocal8Bit() ) ); tmp.replace( pos, before.length(), after ); pos += after.length(); } } else tmp.replace( QString::fromLatin1( "_" ), QString::fromLatin1( " " ) ); cache.insert(str,tmp); return tmp; } // TODO(hzeller): DEPENDENCY This pulls in the whole MainWindow dependency into the database backend. QWidget *XMLDB::FileReader::messageParent() { return MainWindow::Window::theMainWindow(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/main.cpp b/main.cpp index b5e92952..bc666428 100644 --- a/main.cpp +++ b/main.cpp @@ -1,134 +1,134 @@ /* Copyright (C) 2010-2018 The KPhotoAlbum development team 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "version.h" void migrateKDE4Config() { Kdelibs4ConfigMigrator migrator(QStringLiteral("kphotoalbum")); // the same name defined in the aboutData migrator.setConfigFiles(QStringList() << QStringLiteral("kphotoalbumrc")); migrator.setUiFiles(QStringList() << QStringLiteral("kphotoalbumui.rc")); if (migrator.migrate()) { KConfigGroup unnamedConfig = KSharedConfig::openConfig()->group(QString()); if ( unnamedConfig.hasKey( QString::fromLatin1("configfile") ) ) { // rename config file entry on update KConfigGroup generalConfig = KSharedConfig::openConfig()->group(QString::fromUtf8("General")); generalConfig.writeEntry( QString::fromLatin1("imageDBFile"), unnamedConfig.readEntry( QString::fromLatin1("configfile"))); unnamedConfig.deleteEntry( QString::fromLatin1("configfile") ); qWarning() << "Renamed config entry configfile to General.imageDBFile."; } } } int main( int argc, char** argv ) { KLocalizedString::setApplicationDomain("kphotoalbum"); QApplication app(argc, argv); migrateKDE4Config(); KAboutData aboutData( QStringLiteral("kphotoalbum"), //component name i18n("KPhotoAlbum"), // display name QStringLiteral(KPA_VERSION), i18n("KDE Photo Album"), // short description KAboutLicense::GPL, QString(), // copyright statement QString(), // other text QStringLiteral("http://www.kphotoalbum.org") // homepage ); aboutData.setOrganizationDomain("kde.org"); aboutData.addAuthor( i18n("Jesper K. Pedersen"), i18n("Development"), QStringLiteral("blackie@kde.org") ); aboutData.addAuthor( i18n("Hassan Ibraheem"),i18n("Development"), QStringLiteral("hasan.ibraheem@gmail.com")); aboutData.addAuthor( i18n("Miika Turkia"),i18n("Development"), QStringLiteral("miika.turkia@gmail.com")); aboutData.addAuthor( i18n("Tuomas Suutari"), i18n("SQL backend and numerous features"), QStringLiteral("thsuut@utu.fi") ); aboutData.addAuthor( i18n("Jan Kundrát"), i18n("Development"), QStringLiteral("jkt@gentoo.org")); aboutData.addAuthor( i18n("Henner Zeller"),i18n("Development"), QStringLiteral("h.zeller@acm.org")); aboutData.addAuthor( i18n("Andreas Neustifter"),i18n("Development"), QStringLiteral("andreas.neustifter@gmail.com")); aboutData.addAuthor( i18n("Johannes Zarl-Zierl"),i18n("Development"), QStringLiteral("johannes@zarl-zierl.at")); aboutData.addAuthor( i18n("Tobias Leupold"),i18n("Development"), QStringLiteral("tobias.leupold@gmx.de")); aboutData.addAuthor( i18n("Robert Krawitz"),i18n("Development"), QStringLiteral("rlk@alum.mit.edu")); aboutData.addCredit( i18n("Will Stephenson"), i18n("Developing an Icon for KPhotoAlbum"), QStringLiteral("will@stevello.free-online.co.uk") ); aboutData.addCredit( i18n("Teemu Rytilahti"), i18n("Sending patches implementing (.) the \"Set As Wallpaper\" menu in the viewer." "(.) Theme support for HTML generation"), QStringLiteral("teemu.rytilahti@kde-fi.org") ); aboutData.addCredit( i18n("Reimar Imhof"), i18n("Patch to sort items in option listboxes"), QStringLiteral("Reimar.Imhof@netCologne.de") ); - aboutData.addCredit( i18n("Thomas Schwarzgruber"), i18n("Patch to sort images in the thumbnail view, plus reading time info out of EXIF images for existing images"), QStringLiteral("possebaer@gmx.at") ); + aboutData.addCredit( i18n("Thomas Schwarzgruber"), i18n("Patch to sort images in the thumbnail view, plus reading time info out of Exif images for existing images"), QStringLiteral("possebaer@gmx.at") ); aboutData.addCredit( i18n("Marcel Wiesweg"), i18n("Patch which speed up loading of thumbnails plus preview in image property dialog."), QStringLiteral("marcel.wiesweg@gmx.de") ); - aboutData.addCredit( i18n("Marco Caldarelli"), i18n("Patch for making it possible to reread EXIF info using a nice dialog."), QStringLiteral("caldarel@yahoo.it") ); + aboutData.addCredit( i18n("Marco Caldarelli"), i18n("Patch for making it possible to reread Exif info using a nice dialog."), QStringLiteral("caldarel@yahoo.it") ); aboutData.addCredit( i18n("Jean-Michel FAYARD"), i18n("(.) Patch with directory info made available through the browser. (.) Patch for adding a check box for \"and/or\" searches in the search page."), QStringLiteral("jmfayard@gmail.com") ); aboutData.addCredit( i18n("Robert L Krawitz"), i18n("Numerous patches plus profiling KPhotoAlbum again and again."), QStringLiteral("rlk@alum.mit.edu") ); aboutData.addCredit( i18n("Christoph Moseler"), i18n("Numerous patches for lots of bugs plus patches for a few new features"), QStringLiteral("forums@moseler.net") ); aboutData.addCredit( i18n("Clytie Siddall"), i18n("Tremendous help with the English text in the application."), QStringLiteral("clytie@riverland.net.au") ); aboutData.addCredit( i18n("Wes Hardaker"),i18n("Some very useful features to improve workflow"), QStringLiteral("kpa@capturedonearth.com")); // initialize the commandline parser QCommandLineParser *parser = MainWindow::Options::the()->parser(); KAboutData::setApplicationData(aboutData); aboutData.setupCommandLine(parser); parser->process(app); aboutData.processCommandLine(parser); new MainWindow::SplashScreen(); MainWindow::Window *view = nullptr; try { view = new MainWindow::Window( 0 ); } catch (int retVal) { // MainWindow ctor throws if no config is loaded return retVal; } // qApp->setMainWidget( view ); view->setGeometry( Settings::SettingsData::instance()->windowGeometry( Settings::MainWindow ) ); (void) RemoteControl::RemoteInterface::instance(); int code = app.exec(); // I've heard multiple people complain about a crash in this line. // unfortunately valgrind doesn't tell me why that should be, and I haven't seen it myself. // Anyway, the line is really only needed when searching for memory leaks. // delete view; return code; } // vi:expandtab:tabstop=4 shiftwidth=4: