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

Could not save the window layout.

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

Could not save the window layout.

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

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

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

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

" "

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

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

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

" "

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

" ); } KMessageBox::information( this, txt, QString(), doNotShowKey, KMessageBox::AllowLink ); } void AnnotationDialog::Dialog::resizeEvent( QResizeEvent* ) { Settings::SettingsData::instance()->setWindowGeometry( Settings::AnnotationDialog, geometry() ); } void AnnotationDialog::Dialog::moveEvent( QMoveEvent * ) { Settings::SettingsData::instance()->setWindowGeometry( Settings::AnnotationDialog, geometry() ); } void AnnotationDialog::Dialog::setupFocus() { QList list = findChildren(); QList orderedList; // Iterate through all widgets in our dialog. for ( QObject* obj : list ) { QWidget* current = static_cast( obj ); if ( !current->property("WantsFocus").isValid() || !current->isVisible() ) continue; int cx = current->mapToGlobal( QPoint(0,0) ).x(); int cy = current->mapToGlobal( QPoint(0,0) ).y(); bool inserted = false; // Iterate through the ordered list of widgets, and insert the current one, so it is in the right position in the tab chain. for( QList::iterator orderedIt = orderedList.begin(); orderedIt != orderedList.end(); ++orderedIt ) { const QWidget* w = *orderedIt; int wx = w->mapToGlobal( QPoint(0,0) ).x(); int wy = w->mapToGlobal( QPoint(0,0) ).y(); if ( wy > cy || ( wy == cy && wx >= cx ) ) { orderedList.insert( orderedIt, current ); inserted = true; break; } } if (!inserted) orderedList.append( current ); } // now setup tab order. QWidget* prev = nullptr; QWidget* first = nullptr; Q_FOREACH( QWidget *widget, orderedList ) { if ( prev ) { setTabOrder( prev, widget ); } else { first = widget; } prev = widget; } if ( first ) { setTabOrder( prev, first ); } // Finally set focus on the first list select Q_FOREACH( QWidget *widget, orderedList ) { if ( widget->property("FocusCandidate").isValid() && widget->isVisible() ) { widget->setFocus(); break; } } } void AnnotationDialog::Dialog::slotResetLayout() { m_dockWindow->restoreState(m_dockWindowCleanState); } void AnnotationDialog::Dialog::slotStartDateChanged( const DB::ImageDate& date ) { if ( date.start() == date.end() ) m_endDate->setDate( QDate() ); else m_endDate->setDate( date.end().date() ); } void AnnotationDialog::Dialog::loadWindowLayout() { QString fileName = QString::fromLatin1( "%1/layout.dat" ).arg( Settings::SettingsData::instance()->imageDirectory() ); if ( !QFileInfo(fileName).exists() ) { // create default layout // label/date/rating in a visual block with description: m_dockWindow->splitDockWidget(m_generalDock, m_descriptionDock, Qt::Vertical); -#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); + m_actions->setDefaultShortcut(action, Qt::CTRL+Qt::Key_F4); action = m_actions->addAction( QString::fromLatin1("annotationdialog-sort-alphaflat"), m_optionList.at(0), SLOT(slotSortAlphaFlat()) ); action->setText( i18n("Sort Alphabetically (Flat)") ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-sort-MRU"), m_optionList.at(0), SLOT(slotSortDate()) ); action->setText( i18n("Sort Most Recently Used") ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-toggle-sort"), m_optionList.at(0), SLOT(toggleSortType()) ); action->setText( i18n("Toggle Sorting") ); - action->setShortcut( Qt::CTRL+Qt::Key_T ); + m_actions->setDefaultShortcut(action, Qt::CTRL+Qt::Key_T ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-toggle-showing-selected-only"), &ShowSelectionOnlyManager::instance(), SLOT(toggle()) ); action->setText( i18n("Toggle Showing Selected Items Only") ); - action->setShortcut( Qt::CTRL+Qt::Key_S ); + m_actions->setDefaultShortcut(action, Qt::CTRL+Qt::Key_S ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-next-image"), m_preview, SLOT(slotNext()) ); action->setText( i18n("Annotate Next") ); - action->setShortcut( Qt::Key_PageDown ); + m_actions->setDefaultShortcut(action, Qt::Key_PageDown ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-prev-image"), m_preview, SLOT(slotPrev()) ); action->setText( i18n("Annotate Previous") ); - action->setShortcut( Qt::Key_PageUp ); + m_actions->setDefaultShortcut(action, 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 ); + m_actions->setDefaultShortcut(action, Qt::CTRL+Qt::Key_Return ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-delete-image"), this, SLOT(slotDeleteImage()) ); action->setText( i18n("Delete") ); - action->setShortcut( Qt::CTRL+Qt::Key_Delete ); + m_actions->setDefaultShortcut(action, Qt::CTRL+Qt::Key_Delete ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-copy-previous"), this, SLOT(slotCopyPrevious()) ); action->setText( i18n("Copy tags from previous image") ); - action->setShortcut( Qt::ALT+Qt::Key_Insert ); + m_actions->setDefaultShortcut(action, Qt::ALT+Qt::Key_Insert ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-rotate-left"), m_preview, SLOT(rotateLeft()) ); action->setText( i18n("Rotate counterclockwise") ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-rotate-right"), m_preview, SLOT(rotateRight()) ); action->setText( i18n("Rotate clockwise") ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-toggle-viewer"), this, SLOT(togglePreview()) ); action->setText( i18n("Toggle fullscreen preview") ); - action->setShortcut( Qt::CTRL + Qt::Key_Space ); + m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Space ); foreach (QAction* action, m_actions->actions()) { - action->setShortcutContext(Qt::WindowShortcut); - addAction(action); - } + action->setShortcutContext(Qt::WindowShortcut); + addAction(action); + } + + // the annotation dialog is created when it's first used; + // therefore, its actions are registered well after the MainWindow sets up its actionCollection, + // and it has to read the shortcuts here, after they are set up: + m_actions->readSettings(); } KActionCollection* AnnotationDialog::Dialog::actions() { return m_actions; } void AnnotationDialog::Dialog::setUpCategoryListBoxForMultiImageSelection( ListSelect* listSel, const DB::ImageInfoList& images ) { StringSet on, partialOn; std::tie(on,partialOn) = selectionForMultiSelect( listSel, images ); listSel->setSelection( on, partialOn ); } std::tuple AnnotationDialog::Dialog::selectionForMultiSelect( ListSelect* listSel, const DB::ImageInfoList& images ) { const QString category = listSel->category(); const StringSet allItems = DB::ImageDB::instance()->categoryCollection()->categoryForName( category )->itemsInclCategories().toSet(); StringSet itemsNotSelectedOnAllImages; StringSet itemsOnSomeImages; for ( DB::ImageInfoList::ConstIterator imageIt = images.begin(); imageIt != images.end(); ++ imageIt ) { const StringSet itemsOnThisImage = (*imageIt)->itemsOfCategory( category ); itemsNotSelectedOnAllImages += ( allItems - itemsOnThisImage ); itemsOnSomeImages += itemsOnThisImage; } const StringSet itemsOnAllImages = allItems - itemsNotSelectedOnAllImages; const StringSet itemsPartiallyOn = itemsOnSomeImages - itemsOnAllImages; return std::make_tuple( itemsOnAllImages, itemsPartiallyOn ); } void AnnotationDialog::Dialog::slotRatingChanged( unsigned int ) { m_ratingChanged = true; } void AnnotationDialog::Dialog::continueLater() { saveAndClose(); } void AnnotationDialog::Dialog::saveAndClose() { tidyAreas(); m_fullScreenPreview->stopPlayback(); if (m_origList.isEmpty()) { // all images are deleted. QDialog::accept(); return; } // I need to check for the changes first, as the case for m_setup == InputSingleImageConfigMode, saves to the m_origList, // and we can thus not check for changes anymore const bool anyChanges = hasChanges(); if ( m_setup == InputSingleImageConfigMode ) { writeToInfo(); for ( int i = 0; i < m_editList.count(); ++i ) { *(m_origList[i]) = m_editList[i]; } } else if ( m_setup == InputMultiImageConfigMode ) { Q_FOREACH( ListSelect *ls, m_optionList ) { ls->slotReturn(); } for( DB::ImageInfoListConstIterator it = m_origList.constBegin(); it != m_origList.constEnd(); ++it ) { DB::ImageInfoPtr info = *it; info->delaySavingChanges(true); if ( !m_startDate->date().isNull() ) info->setDate( DB::ImageDate( m_startDate->date(), m_endDate->date(), m_time->time() ) ); Q_FOREACH( ListSelect *ls, m_optionList ) { info->addCategoryInfo( ls->category(), ls->itemsOn() ); info->removeCategoryInfo( ls->category(), ls->itemsOff() ); } if ( !m_imageLabel->text().isEmpty() ) { info->setLabel( m_imageLabel->text() ); } if ( !m_description->toPlainText().isEmpty() && m_description->toPlainText().compare( m_conflictText ) ) { info->setDescription( m_description->toPlainText() ); } if( m_ratingChanged) { info->setRating( m_rating->rating() ); } info->delaySavingChanges(false); } m_ratingChanged = false; } m_accept = QDialog::Accepted; if (anyChanges) { MainWindow::DirtyIndicator::markDirty(); } QDialog::accept(); } AnnotationDialog::Dialog::~Dialog() { qDeleteAll( m_optionList ); m_optionList.clear(); } void AnnotationDialog::Dialog::togglePreview() { if (m_setup == InputSingleImageConfigMode) { if ( m_stack->currentWidget() == m_fullScreenPreview ) { m_stack->setCurrentWidget( m_dockWindow ); m_fullScreenPreview->stopPlayback(); } else { m_stack->setCurrentWidget( m_fullScreenPreview ); m_fullScreenPreview->load( DB::FileNameList() << m_editList[ m_current].fileName() ); } } } void AnnotationDialog::Dialog::tidyAreas() { // Remove all areas marked on the preview image foreach (ResizableFrame *area, areas()) { area->markTidied(); area->deleteLater(); } } void AnnotationDialog::Dialog::slotNewArea(ResizableFrame *area) { area->setDialog(this); } void AnnotationDialog::Dialog::positionableTagSelected(QString category, QString tag) { // Be sure not to propose an already-associated tag QPair tagData = qMakePair(category, tag); foreach (ResizableFrame *area, areas()) { if (area->tagData() == tagData) { return; } } // Set the selected tag as the last selected positionable tag m_lastSelectedPositionableTag = tagData; // Add the tag to the positionable tag candidate list addTagToCandidateList(category, tag); } void AnnotationDialog::Dialog::positionableTagDeselected(QString category, QString tag) { // Remove the tag from the candidate list removeTagFromCandidateList(category, tag); // Search for areas linked against the tag on this image if (m_setup == InputSingleImageConfigMode) { QPair deselectedTag = QPair(category, tag); foreach (ResizableFrame *area, areas()) { if (area->tagData() == deselectedTag) { area->removeTagData(); m_areasChanged = true; // Only one area can be associated with the tag, so we can return here return; } } } // Removal of tagged areas in InputMultiImageConfigMode is done in DB::ImageInfo::removeCategoryInfo } void AnnotationDialog::Dialog::addTagToCandidateList(QString category, QString tag) { m_positionableTagCandidates << QPair(category, tag); } void AnnotationDialog::Dialog::removeTagFromCandidateList(QString category, QString tag) { // Is the deselected tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == tag) { m_lastSelectedPositionableTag = QPair(); } // Remove the tag from the candidate list m_positionableTagCandidates.removeAll(QPair(category, tag)); // When a positionable tag is entered via the AreaTagSelectDialog, it's added to this // list twice, so we use removeAll here to be sure to also wipe duplicate entries. } QPair AnnotationDialog::Dialog::lastSelectedPositionableTag() const { return m_lastSelectedPositionableTag; } QList> AnnotationDialog::Dialog::positionableTagCandidates() const { return m_positionableTagCandidates; } void AnnotationDialog::Dialog::slotShowAreas(bool showAreas) { foreach (ResizableFrame *area, areas()) { area->setVisible(showAreas); } } void AnnotationDialog::Dialog::positionableTagRenamed(QString category, QString oldTag, QString newTag) { // Is the renamed tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == oldTag) { m_lastSelectedPositionableTag.second = newTag; } // Check the candidate list for the tag QPair oldTagData = QPair(category, oldTag); if (m_positionableTagCandidates.contains(oldTagData)) { // The tag is in the list, so update it m_positionableTagCandidates.removeAt(m_positionableTagCandidates.indexOf(oldTagData)); m_positionableTagCandidates << QPair(category, newTag); } // Check if an area on the current image contains the changed or proposed tag foreach (ResizableFrame *area, areas()) { if (area->tagData() == oldTagData) { area->setTagData(category, newTag); } } } void AnnotationDialog::Dialog::descriptionPageUpDownPressed(QKeyEvent *event) { if (event->key() == Qt::Key_PageUp) { m_actions->action(QString::fromLatin1("annotationdialog-prev-image"))->trigger(); } else if (event->key() == Qt::Key_PageDown) { m_actions->action(QString::fromLatin1("annotationdialog-next-image"))->trigger(); } } void AnnotationDialog::Dialog::checkProposedTagData( QPair tagData, ResizableFrame *areaToExclude) const { foreach (ResizableFrame *area, areas()) { if (area != areaToExclude && area->proposedTagData() == tagData && area->tagData().first.isEmpty()) { area->removeProposedTagData(); } } } void AnnotationDialog::Dialog::areaChanged() { m_areasChanged = true; } /** * @brief positionableTagValid checks whether a given tag can still be associated to an area. * This checks for empty and duplicate tags. * @return */ bool AnnotationDialog::Dialog::positionableTagAvailable(const QString &category, const QString &tag) const { if (category.isEmpty() || tag.isEmpty()) return false; // does any area already have that tag? foreach (const ResizableFrame *area, areas()) { const auto tagData = area->tagData(); if (tagData.first == category && tagData.second == tag) return false; } return true; } /** * @brief Generates a set of positionable tags currently used on the image * @param category * @return */ QSet AnnotationDialog::Dialog::positionedTags(const QString &category) const { QSet tags; foreach (const ResizableFrame *area, areas()) { const auto tagData = area->tagData(); if (tagData.first == category) tags += tagData.second; } return tags; } AnnotationDialog::ListSelect *AnnotationDialog::Dialog::listSelectForCategory(const QString &category) { return m_listSelectList.value(category,nullptr); } #ifdef HAVE_KGEOMAP void AnnotationDialog::Dialog::updateMapForCurrentImage() { if (m_setup != InputSingleImageConfigMode) { return; } if (m_editList[m_current].coordinates().hasCoordinates()) { m_annotationMap->setCenter(m_editList[m_current]); m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } } void AnnotationDialog::Dialog::annotationMapVisibilityChanged(bool visible) { // This populates the map if it's added when the dialog is already open if ( visible ) { // when the map dockwidget is already visible on show(), the call to // annotationMapVisibilityChanged is executed in the GUI thread. // This ensures that populateMap() doesn't block the GUI in this case: QTimer::singleShot(0, this, SLOT(populateMap())); } else { m_cancelMapLoading = true; } } void AnnotationDialog::Dialog::populateMap() { // populateMap is called every time the map widget gets visible if (m_mapIsPopulated) { return; } m_annotationMap->displayStatus(Map::MapView::MapStatus::Loading); m_cancelMapLoading = false; m_mapLoadingProgress->setMaximum(m_editList.count()); m_mapLoadingProgress->show(); m_cancelMapLoadingButton->show(); int processedImages = 0; int imagesWithCoordinates = 0; foreach (DB::ImageInfo info, m_editList) { processedImages++; m_mapLoadingProgress->setValue(processedImages); // keep things responsive by processing events manually: QApplication::processEvents(); if (info.coordinates().hasCoordinates()) { m_annotationMap->addImage(info); imagesWithCoordinates++; } // m_cancelMapLoading is set to true by clicking the "Cancel" button if (m_cancelMapLoading) { m_annotationMap->clear(); break; } } // at this point either we canceled loading or the map is populated: m_mapIsPopulated = ! m_cancelMapLoading; mapLoadingFinished(imagesWithCoordinates > 0, imagesWithCoordinates == processedImages); } void AnnotationDialog::Dialog::setCancelMapLoading() { m_cancelMapLoading = true; } void AnnotationDialog::Dialog::mapLoadingFinished(bool mapHasImages, bool allImagesHaveCoordinates) { m_mapLoadingProgress->hide(); m_cancelMapLoadingButton->hide(); if (m_setup == InputSingleImageConfigMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } else { if (m_setup == SearchMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SearchCoordinates); } else { if (mapHasImages) { if (! allImagesHaveCoordinates) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SomeImagesHaveNoCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::NoImagesHaveNoCoordinates); } } } if (m_setup != SearchMode) { m_annotationMap->zoomToMarkers(); updateMapForCurrentImage(); } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/CMakeLists.txt b/CMakeLists.txt index 87ad2b5b..3012f7a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,521 +1,521 @@ cmake_minimum_required(VERSION 3.2.0) project(kphotoalbum VERSION 5.4) if(POLICY CMP0063) cmake_policy(SET CMP0063 NEW) endif() # provide drop-down menu for build-type in cmake-gui: set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS ";Debug;Release;RelWithDebInfo;MinSizeRel") list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) find_package(ECM REQUIRED NO_MODULE) list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ) include(KDEInstallDirs) include(KDECompilerSettings) include(KDECMakeSettings) include(FeatureSummary) # enable exceptions: kde_enable_exceptions() add_definitions( -DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII -DQT_NO_URL_CAST_FROM_STRING -DQT_NO_CAST_FROM_BYTEARRAY -DQT_DEPRECATED_WARNINGS ) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_REQUIRED TRUE) ########### dependencies ############### -find_package(Qt5 REQUIRED COMPONENTS Sql Xml Widgets Network) +find_package(Qt5 5.9 REQUIRED COMPONENTS Sql Xml Widgets Network) find_package(Phonon4Qt5 REQUIRED) -find_package(KF5 REQUIRED COMPONENTS Archive Completion Config CoreAddons DocTools I18n IconThemes JobWidgets KIO TextWidgets XmlGui WidgetsAddons) +find_package(KF5 5.44 REQUIRED COMPONENTS Archive Completion Config CoreAddons DocTools I18n IconThemes JobWidgets KIO TextWidgets XmlGui WidgetsAddons) find_package(JPEG REQUIRED) if(JPEG_FOUND) include_directories(${JPEG_INCLUDE_DIR}) endif() find_package(Exiv2 REQUIRED) find_package(KF5Kipi 5.1.0) set_package_properties(KF5Kipi PROPERTIES TYPE RECOMMENDED PURPOSE "Enables integration of KDE image plugin interface (shared functionality between KPhotoAlbum and other apps like gwenview or digikam)" ) set(HASKIPI ${KF5Kipi_FOUND}) find_package(KF5KDcraw) set_package_properties(KF5KDcraw PROPERTIES TYPE OPTIONAL PURPOSE "Enables RAW image support" ) set(HAVE_KDCRAW ${KF5KDcraw_FOUND} ) find_package(KF5KGeoMap) set_package_properties(KF5KGeoMap PROPERTIES TYPE OPTIONAL PURPOSE "Enables support for geographic map location using embedded GPS information." ) set(HAVE_KGEOMAP ${KF5KGeoMap_FOUND}) add_custom_target( UpdateVersion ALL COMMAND ${CMAKE_COMMAND} -DBASE_DIR=${CMAKE_CURRENT_SOURCE_DIR} -DPROJECT_NAME=KPA -DPROJECT_VERSION="${PROJECT_VERSION}" -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/UpdateVersion.cmake" COMMENT "Updating version header." BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/version.h" ) # For config-kpa-*.h include_directories(${CMAKE_CURRENT_BINARY_DIR}) set(libdatebar_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/DateBar/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/DateBar/DateBarWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DateBar/ViewHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DateBar/MouseHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DateBar/MouseHandler.cpp ) set(libSettings_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Settings/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Settings/SettingsData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/SettingsDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/ViewerSizeConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/CategoryItem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/CategoryPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/TagGroupsPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/GeneralPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/FileVersionDetectionPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/ThumbnailsPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/ViewerPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/DatabaseBackendPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/UntaggedGroupBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/CategoriesGroupsWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/BirthdayPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/DateTableWidgetItem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/Logging.cpp ) set(libxmldb_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/Database.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/XMLCategoryCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/XMLCategory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/XMLImageDateCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/NumberedBackup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/FileReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/FileWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/ElementWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/XmlReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/CompressFileInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/XMLDB/Logging.cpp ) set(libThumbnailView_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailRequest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailToolTip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/GridResizeInteraction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/GridResizeSlider.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/SelectionInteraction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/MouseTrackingInteraction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/CellGeometry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailModel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailFacade.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/KeyboardEventHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailDND.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/Delegate.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/SelectionMaintainer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/VideoThumbnailCycler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/MouseInteraction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/ThumbnailFactory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailView/enums.cpp ) set(libPlugins_SRCS) if(KF5Kipi_FOUND) set(libPlugins_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/Interface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/ImageCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/ImageInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/CategoryImageCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/ImageCollectionSelector.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/PluginsPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/UploadWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/UploadImageCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Plugins/Logging.cpp ) endif() set(libViewer_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/ViewerWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/ImageDisplay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/ViewHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/SpeedDisplay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/InfoBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/CategoryImageConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/AbstractDisplay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/VideoDisplay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/TextDisplay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/InfoBoxResizer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/VisibleOptionsMenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/VideoShooter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/TaggedArea.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Viewer/Logging.cpp ) set(libCategoryListView_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/DragableTreeWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/CheckDropItem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/DragItemInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/Logging.cpp ) set(libHTMLGenerator_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/HTMLDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/Generator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/Setup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/ImageSizeCheckBox.h ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HTMLGenerator/ImageSizeCheckBox.cpp ) set(libUtilities_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/AlgorithmHelper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/ShowBusyCursor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/List.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/UniqFilenameMapper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/Util.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/BooleanGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/DeleteFiles.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/ToolTip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/JpeglibWithFix.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/QStr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/StringSet.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/FastJpeg.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/DemoUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Utilities/DescriptionUtil.cpp ) set(libMainWindow_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/DeleteDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/RunDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/FeatureDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/InvalidDateFinder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/AutoStackImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/TokenEditor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/WelcomeDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/Window.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/SplashScreen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/ExternalPopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/CategoryImagePopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/SearchBar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/ImageCounter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/DirtyIndicator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/StatisticsDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/BreadcrumbViewer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/StatusBar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/UpdateVideoThumbnail.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/DuplicateMerger/DuplicateMerger.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/DuplicateMerger/DuplicateMatch.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/DuplicateMerger/MergeToolTip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/CopyPopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/Options.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow/Logging.cpp ) set(libImageManager_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ImageLoaderThread.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/AsyncLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ImageRequest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ImageClientInterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ImageDecoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/RawImageDecoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/RequestQueue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ThumbnailCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ImageEvent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ThumbnailBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/PreloadRequest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/CancelEvent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/VideoImageRescaleRequest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/VideoThumbnails.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/VideoLengthExtractor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/ExtractOneVideoFrame.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/CacheFileInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImageManager/enums.cpp ) set(libDB_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/DB/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/Category.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/CategoryCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ExactCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageDate.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/MD5Map.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/MemberMap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageInfoList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageDB.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/FileInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/NegationCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/NewImageFinder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageScout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/NoTagCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/GroupCounter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/CategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageSearchInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/CategoryItem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ContainerCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ValueCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/OrCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/AndCategoryMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/FastDir.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/OptimizedFileList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/FileName.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/FileNameList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/CategoryPtr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ExifMode.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageDateCollection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/ImageInfoPtr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/MD5.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/MediaCount.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/RawId.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DB/SimpleCategoryMatcher.cpp ) set(libImportExport_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/Import.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/ImportMatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/XMLHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/MiniViewer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/ImportHandler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/ImageRow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/ImportDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/ImportSettings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/KimFileReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/MD5CheckPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ImportExport/Logging.cpp ) set(libAnnotationDialog_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/Dialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ListSelect.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ImagePreview.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ImagePreviewWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/DateEdit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/CompletableLineEdit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ListViewItemHider.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ShowSelectionOnlyManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ShortCutManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/ResizableFrame.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/DescriptionEdit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/AreaTagSelectDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AnnotationDialog/enums.cpp ) set(libBrowser_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Browser/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Browser/BrowserWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/BrowserPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/OverviewPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/CategoryPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/ImageViewPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/TreeFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/Breadcrumb.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/BreadcrumbList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/AbstractCategoryModel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/FlatCategoryModel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/TreeCategoryModel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/CenteringIconView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/enums.cpp ) set(libExif_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Exif/documentation.h ${CMAKE_CURRENT_SOURCE_DIR}/Exif/Database.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/InfoDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/SearchDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/SearchInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/TreeView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/Info.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/RangeWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/DatabaseElement.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/ReReadDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/Grid.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/Logging.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Exif/SearchDialogSettings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Settings/ExifPage.cpp ) set(libBackgroundTaskManager_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/JobInterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/JobManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/StatusIndicator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/JobViewer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/JobModel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/JobInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/CompletedJobInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/Priority.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/PriorityQueue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundTaskManager/Logging.cpp ) set(libBackgroundJobs_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundJobs/SearchForVideosWithoutLengthInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundJobs/ReadVideoLengthJob.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundJobs/SearchForVideosWithoutVideoThumbnailsJob.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundJobs/HandleVideoThumbnailRequestJob.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BackgroundJobs/ExtractOneThumbnailJob.cpp ) set(libRemoteControl_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/RemoteCommand.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/RemoteConnection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/Server.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/RemoteInterface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/SearchInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/RemoteImageRequest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/ImageNameStore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/ConnectionIndicator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RemoteControl/Logging.cpp ) set(libMap_SRCS) if(KF5KGeoMap_FOUND) set(libMap_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Browser/GeoPositionPage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Browser/PositionBrowserWidget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/MapView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/MapMarkerModelHelper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/SearchMarkerTiler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Map/Logging.cpp ) endif() add_subdirectory(images) add_subdirectory(icons) add_subdirectory(demo) add_subdirectory(themes) add_subdirectory(scripts) add_subdirectory(doc) ########### next target ############### set(kphotoalbum_SRCS main.cpp ${libdatebar_SRCS} ${libSettings_SRCS} ${libsurvey_SRCS} ${libxmldb_SRCS} ${libThumbnailView_SRCS} ${libPlugins_SRCS} ${libViewer_SRCS} ${libCategoryListView_SRCS} ${libHTMLGenerator_SRCS} ${libMainWindow_SRCS} ${libImageManager_SRCS} ${libDB_SRCS} ${libImportExport_SRCS} ${libAnnotationDialog_SRCS} ${libExif_SRCS} ${libBrowser_SRCS} ${libBackgroundTaskManager_SRCS} ${libBackgroundJobs_SRCS} ${libRemoteControl_SRCS} ${libMap_SRCS} ${libUtilities_SRCS} # add doxygen headers so that they get visibiltiy in IDEs: ${CMAKE_CURRENT_SOURCE_DIR}/documentation/coding-standards.h ${CMAKE_CURRENT_SOURCE_DIR}/documentation/mainpage.h ${CMAKE_CURRENT_SOURCE_DIR}/documentation/phrase-book.h ${CMAKE_CURRENT_SOURCE_DIR}/documentation/videothumbnails.h ) add_executable(kphotoalbum ${kphotoalbum_SRCS}) add_dependencies(kphotoalbum UpdateVersion) # External components target_link_libraries(kphotoalbum ${JPEG_LIBRARY} ${EXIV2_LIBRARIES} KF5::Archive KF5::Completion KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons KF5::I18n KF5::IconThemes KF5::JobWidgets KF5::KIOCore KF5::KIOWidgets KF5::TextWidgets KF5::XmlGui KF5::WidgetsAddons Phonon::phonon4qt5 Qt5::Network Qt5::Sql ) if(KF5Kipi_FOUND) target_link_libraries(kphotoalbum KF5::Kipi) endif() if(KF5KDcraw_FOUND) target_link_libraries(kphotoalbum KF5::KDcraw) endif() if(KF5KGeoMap_FOUND) target_link_libraries(kphotoalbum KF5::KGeoMap ) endif() install(TARGETS kphotoalbum ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) ########### install files ############### install(PROGRAMS org.kde.kphotoalbum.desktop org.kde.kphotoalbum-import.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES kphotoalbumrc DESTINATION ${KDE_INSTALL_CONFDIR}) install(FILES tips default-setup DESTINATION ${KDE_INSTALL_DATADIR}/kphotoalbum) install(FILES kphotoalbumui.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/kphotoalbum) install(FILES org.kde.kphotoalbum.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) configure_file(config-kpa-kdcraw.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kpa-kdcraw.h) configure_file(config-kpa-kipi.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kpa-kipi.h) configure_file(config-kpa-kgeomap.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kpa-kgeomap.h) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) # vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ChangeLog b/ChangeLog index 66780c1e..b26afbe2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,2698 +1,2711 @@ +* Bugfix: Fix loading of user shortcuts for the annotation dialog. + +* Enhancement: Add "clear selection" menu item. + +* Change: More detailed debug output. + +* Deprecation: Removed compatibility with Qt versions < 5.9. + +* Enhancement: Smaller perfornance improvements during startup. + +* Bugfix: Fix zooming for raw images. + Thanks to Angel Lopez for the bug report and help in finding the root cause! + ====================== KPhotoalbum 5.4 released (13.10.2018) ====================== * Change: Database file now uses version 8 format. * Update/Bugfix: Various l10n fixes concerning capitalization and contexts. * Update: Review default values for Settings, pre-define contemporary ones: - Ignore common auto-generated Exif comments by default. - The database file is now optimized for speed by default (old default value: optimized for human readability) - If less than 20 images are matched, the thumbnail view is shown (old default value: 0) - The thumbnail grid is now optimized for a 3:2 aspect ratio (old default value: 4:3) - The size of thumbnails is now 256x256 pixels (old default value: 150) - When starting a slideshow, it is now shown in fullscreen by default (old default value: 800x600px) - Detect edited images by default (old default value: feature was not configured) This means that images that have been edited using the "create a copy and open" feature now are recognized by default. The tags of the original image are copied and both images are automatically stacked. * Bugfix: Use the correct initial value when renaming tag groups using the settings page. * Enhancement: Improve startup 10-15% by eliding unnecessary category creation. * Bugfixes: Don't continue accessing the Exif db if unusable, reset failed state when recreating db. * Bugfix: Fix display of "<=" and ">=" rating options in search dialog. * Bugfix: Fix overflow in parsing GPS information from Exif, leading to false coordinates. * Enhancement: Add FreeBSD as supported platform * Bugfix: Fixed build with Qt 5.11, replace deprecated API usage. * Enhancement: Do not write unnecessary data to index.xml by using defaults. * Enhancements: Improved thumbnail generation: - Improve thumbnail generation speed and efficiency. - Overlap thumbnail create with image load. - Generate new thumbnails for moved images detected during load. * Bugfix: More reliable auto-stacking of images. * Enhancements: Improved speed of loading/removing new images: - Insert Exif data for each loaded image as it's loaded, but within a transaction. - Elide unnecessary MD5 computations. - Reduce I/O wait time of image loader by introducing a scout thread. - Improve I/O efficiency for MD5 calculation. - Fetch Exif data only once during load. - Improve efficiency of loading previews from RAW images. - Ignore more sidecar files to improve performance (.xmp, .pp3, and .pto). * Enhancement/Bugfix: Scale the date bar appropriately upon exposure, add one minute scaler. * Update: Make Android app compilable with C++14 and later. * Rework: General code cleanup, coding standard updates etc. all over the place. ====================== 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 deprecated 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. ====================== 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: 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. 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, then going to the next image in the image viewer, will update the 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 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 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 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 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 (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 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. * 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. ====================== 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 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 Data" dialog. It does not overwrite the time, date, orientation, 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 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 ) 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. 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 * 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 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. 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/FileNameList.h b/DB/FileNameList.h index 520b8593..8b6634dc 100644 --- a/DB/FileNameList.h +++ b/DB/FileNameList.h @@ -1,49 +1,49 @@ /* Copyright 2012 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 . */ #ifndef FILENAMELIST_H #define FILENAMELIST_H #include #include #include "FileName.h" #include "ImageInfo.h" namespace DB { class FileNameList : public QList { public: FileNameList() {} - explicit FileNameList( const QList& ); + FileNameList( const QList& ); /** * @brief Create a FileNameList from a list of absolute filenames. * @param files */ explicit FileNameList(const QStringList &files); QStringList toStringList(DB::PathType) const; FileNameList& operator<<(const DB::FileName& ); FileNameList reversed() const; }; } #endif // FILENAMELIST_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageDate.cpp b/DB/ImageDate.cpp index d4ae4317..3554916f 100644 --- a/DB/ImageDate.cpp +++ b/DB/ImageDate.cpp @@ -1,374 +1,374 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageDate.h" #include #include #include using namespace DB; ImageDate::ImageDate( const QDate& date ) { m_start = QDateTime( date, QTime(0,0,0) ); m_end = QDateTime( date, QTime(0,0,0) ); } ImageDate::ImageDate( const QDateTime& date) { m_start = date; m_end = date; } bool ImageDate::operator<=( const ImageDate& other ) const { // This operator is used by QMap when checking for equal elements, thus we need the second part too. return m_start < other.m_start || (m_start == other.m_start && m_end <= other.m_end); } ImageDate::ImageDate() { } bool ImageDate::isNull() const { return m_start.isNull(); } bool ImageDate::isFuzzy() const { return m_start != m_end; } static bool isFirstSecOfMonth( const QDateTime& date ) { return date.date().day() == 1 && date.time().hour() == 0 && date.time().minute() == 0; } static bool isLastSecOfMonth( QDateTime date ) { return isFirstSecOfMonth( date.addSecs(1) ); } static bool isFirstSecOfDay( const QDateTime& time ) { return time.time().hour() == 0 && time.time().minute() == 0 && time.time().second() == 0; } static bool isLastSecOfDay( const QDateTime& time ) { return time.time().hour() == 23 && time.time().minute() == 59 && time.time().second() == 59; } QString ImageDate::toString( bool withTime ) const { if ( m_start.isNull() ) return QString(); if ( m_start == m_end ) { if ( withTime && !isFirstSecOfDay(m_start)) return m_start.toString( QString::fromLatin1( "d. MMM yyyy hh:mm:ss" ) ); else return m_start.toString( QString::fromLatin1( "d. MMM yyyy" ) ); } // start is different from end. if ( isFirstSecOfMonth(m_start) && isLastSecOfMonth(m_end) ) { if ( m_start.date().month() == 1 && m_end.date().month() == 12 ) { if ( m_start.date().year() == m_end.date().year() ) { // 2005 return QString::number( m_start.date().year() ); } else { // 2005-2006 return QString::fromLatin1("%1 - %2").arg( m_start.date().year() ).arg( m_end.date().year() ); } } else { // a whole month, but not a whole year. if ( m_start.date().year() == m_end.date().year() &&m_start.date().month() == m_end.date().month() ) { // jan 2005 return QString::fromLatin1( "%1 %2" ) .arg( QLocale().standaloneMonthName(m_start.date().month(), QLocale::ShortFormat) ).arg(m_start.date().year() ); } else { // jan 2005 - feb 2006 return QString::fromLatin1( "%1 %2 - %3 %4" ) .arg( QLocale().standaloneMonthName(m_start.date().month(), QLocale::ShortFormat) ).arg(m_start.date().year() ) .arg( QLocale().standaloneMonthName(m_end.date().month(), QLocale::ShortFormat) ).arg( m_end.date().year() ); } } } - if ( isFirstSecOfDay( m_start ) && isLastSecOfDay( m_end ) ) { + if ( !withTime || (isFirstSecOfDay( m_start ) && isLastSecOfDay( m_end )) ) { if (m_start.date() == m_end.date() ) { // A whole day return m_start.toString( QString::fromLatin1( "d. MMM yyyy" ) ); } else { // A day range return QString::fromLatin1("%1 - %2" ) .arg(m_start.toString( QString::fromLatin1( "d. MMM yyyy" ) ) ) .arg(m_end.toString( QString::fromLatin1( "d. MMM yyyy" ) ) ); } } // Range smaller than one day. if ( withTime && ( !isFirstSecOfDay( m_start ) || !isLastSecOfDay( m_end ) )) return QString::fromLatin1("%1 - %2" ) .arg(m_start.toString( QString::fromLatin1( "d. MMM yyyy hh:mm" ) ) ) .arg(m_end.toString( QString::fromLatin1( "d. MMM yyyy hh:mm" ) ) ); else return QString::fromLatin1("%1 - %2" ) .arg(m_start.toString( QString::fromLatin1( "d. MMM yyyy" ) ) ) .arg(m_end.toString( QString::fromLatin1( "d. MMM yyyy" ) ) ); } bool ImageDate::operator==( const ImageDate& other ) const { return m_start == other.m_start && m_end == other.m_end; } bool ImageDate::operator!=( const ImageDate& other ) const { return !(*this == other ); } QString ImageDate::formatRegexp() { static QString str; if ( str.isEmpty() ) { str = QString::fromLatin1( "^((\\d\\d?)([-. /]+|$))?((" ); QStringList months = monthNames(); for( QStringList::ConstIterator monthIt = months.constBegin(); monthIt != months.constEnd(); ++monthIt ) str += QString::fromLatin1("%1|").arg( *monthIt ); str += QString::fromLatin1("\\d?\\d)([-. /]+|$))?(\\d\\d(\\d\\d)?)?$" ); } return str; } QDateTime ImageDate::start() const { return m_start; } QDateTime ImageDate::end() const { return m_end; } bool ImageDate::operator<( const ImageDate& other ) const { return start() < other.start() || ( start() == other.start() && end() < other.end() ); } ImageDate::ImageDate( const QDateTime& start, const QDateTime& end ) { m_start = start; m_end = end; } static QDate addMonth( int year, int month ) { if ( month == 12 ) { year++; month = 1; } else month++; return QDate( year, month, 1 ); } ImageDate::ImageDate( int yearFrom, int monthFrom, int dayFrom, int yearTo, int monthTo, int dayTo, int hourFrom, int minuteFrom, int secondFrom ) { if ( yearFrom <= 0 ) { m_start = QDateTime(); m_end = QDateTime(); return; } if ( monthFrom <= 0 ) { m_start = QDateTime( QDate( yearFrom, 1, 1 ) ); m_end = QDateTime( QDate( yearFrom+1, 1, 1 ) ).addSecs(-1); } else if ( dayFrom <= 0 ) { m_start = QDateTime( QDate( yearFrom, monthFrom, 1 ) ); m_end = QDateTime( addMonth( yearFrom, monthFrom ) ).addSecs(-1); } else if ( hourFrom < 0 ) { m_start = QDateTime( QDate( yearFrom, monthFrom, dayFrom ) ); m_end = QDateTime( QDate( yearFrom, monthFrom, dayFrom ).addDays(1) ).addSecs(-1); } else if ( minuteFrom < 0 ) { m_start = QDateTime( QDate( yearFrom, monthFrom, dayFrom ), QTime( hourFrom, 0, 0 ) ); m_end = QDateTime( QDate( yearFrom, monthFrom, dayFrom ), QTime( hourFrom, 23, 59 ) ); } else if ( secondFrom < 0 ) { m_start = QDateTime( QDate( yearFrom, monthFrom, dayFrom ), QTime( hourFrom, minuteFrom, 0 ) ); m_end = QDateTime( QDate( yearFrom, monthFrom, dayFrom ), QTime( hourFrom, minuteFrom, 59 ) ); } else { m_start = QDateTime( QDate( yearFrom, monthFrom, dayFrom ), QTime( hourFrom, minuteFrom, secondFrom ) ); m_end = m_start; } if ( yearTo > 0 ) { m_end = QDateTime( QDate( yearTo +1 , 1, 1 ) ).addSecs( -1 ); if ( monthTo > 0 ) { m_end = QDateTime( addMonth( yearTo, monthTo ) ).addSecs( -1 ); if ( dayTo > 0 ) { if ( dayFrom == dayTo && monthFrom == monthTo && yearFrom == yearTo ) m_end = m_start; else m_end = QDateTime( QDate( yearTo, monthTo, dayTo ).addDays(1) ).addSecs(-1); } } } } QDate ImageDate::parseDate( const QString& date, bool startDate ) { int year = 0; int month = 0; int day = 0; QRegExp regexp( formatRegexp(), Qt::CaseInsensitive ); if ( regexp.exactMatch( date ) ) { QString dayStr = regexp.cap(2); QString monthStr = regexp.cap(5).toLower(); QString yearStr= regexp.cap(7); if ( dayStr.length() != 0 ) day = dayStr.toInt(); if ( yearStr.length() != 0 ) { year = yearStr.toInt(); if ( year < 50 ) year += 2000; if ( year < 100 ) year += 1900; } if ( monthStr.length() != 0 ) { int index = monthNames().indexOf( monthStr ); if ( index != -1 ) month = (index%12)+1; else month = monthStr.toInt(); } if ( year == 0 ) year = QDate::currentDate().year(); if ( month == 0 ) { if ( startDate ) { month = 1; day = 1; } else { month = 12; day = 31; } } else if ( day == 0 ) { if ( startDate ) day = 1; else day = QDate( year, month, 1 ).daysInMonth(); } return QDate( year, month, day ); } else return QDate(); } bool ImageDate::hasValidTime() const { return m_start == m_end; } ImageDate::ImageDate( const QDate& start, QDate end, const QTime& time ) { if ( end.isNull() ) end = start; if ( start == end && time.isValid() ) { m_start = QDateTime( start, time ); m_end = m_start; } else { m_start = QDateTime( start, QTime( 0,0,0 ) ); m_end = QDateTime( end, QTime( 23, 59, 59 ) ); } } ImageDate::MatchType ImageDate::isIncludedIn( const ImageDate& searchRange ) const { if ( searchRange.start() <= start() && searchRange.end() >= end() ) return ExactMatch; if ( searchRange.start() <= end() && searchRange.end() >= start() ) { return RangeMatch; } return DontMatch; } bool ImageDate::includes( const QDateTime& date ) const { return ImageDate( date ).isIncludedIn( *this ) == ExactMatch; } QStringList DB::ImageDate::monthNames() { static QStringList res; if ( res.isEmpty() ) { for ( int i = 1; i <= 12; ++i ) { res << QLocale().standaloneMonthName(i, QLocale::ShortFormat); } for ( int i = 1; i <= 12; ++i ) { res << QLocale().standaloneMonthName(i, QLocale::LongFormat); } res << i18nc("Abbreviated month name","jan") << i18nc("Abbreviated month name","feb") << i18nc("Abbreviated month name","mar") << i18nc("Abbreviated month name","apr") << i18nc("Abbreviated month name","may") << i18nc("Abbreviated month name","jun") << i18nc("Abbreviated month name","jul") << i18nc("Abbreviated month name","aug") << i18nc("Abbreviated month name","sep") << i18nc("Abbreviated month name","oct") << i18nc("Abbreviated month name","nov") << i18nc("Abbreviated month name","dec"); res << QString::fromLatin1("jan") << QString::fromLatin1("feb") << QString::fromLatin1("mar") << QString::fromLatin1("apr") << QString::fromLatin1("may") << QString::fromLatin1("jun") << QString::fromLatin1("jul") << QString::fromLatin1("aug") << QString::fromLatin1("sep") << QString::fromLatin1("oct") << QString::fromLatin1("nov") << QString::fromLatin1("dec"); for ( int i = 1; i <= 12; ++i ) { res << QLocale().monthName(i, QLocale::ShortFormat); } for ( int i = 1; i <= 12; ++i ) { res << QLocale().monthName(i, QLocale::LongFormat); } for ( QStringList::iterator it = res.begin(); it != res.end(); ++it ) *it = it->toLower(); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageInfo.cpp b/DB/ImageInfo.cpp index 7e1670a3..d2910742 100644 --- a/DB/ImageInfo.cpp +++ b/DB/ImageInfo.cpp @@ -1,850 +1,843 @@ /* 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_isMatched( false ), m_matchGeneration( -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_isMatched( false ), m_matchGeneration( -1 ) , m_locked(false), m_delaySaving( true ) { QFileInfo fi( fileName.absolute() ); m_label = fi.completeBaseName(); m_angle = 0; setFileName(fileName); // Read Exif information if ( readExifInfo ) { ExifMode mode = EXIFMODE_INIT; if ( ! storeExifInfo) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName, mode); } m_dirty = false; m_delaySaving = false; } void ImageInfo::setIsMatched(bool isMatched) { m_isMatched = isMatched; } bool ImageInfo::isMatched() const { return m_isMatched; } void ImageInfo::setMatchGeneration(int matchGeneration) { m_matchGeneration = matchGeneration; } int ImageInfo::matchGeneration() const { return m_matchGeneration; } /** 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/MD5.h b/DB/MD5.h index b3684866..ec860ae5 100644 --- a/DB/MD5.h +++ b/DB/MD5.h @@ -1,64 +1,71 @@ /* Copyright (C) 2018 Johannes Zarl-Zierl Copyright (C) 2007-2010 Tuomas Suutari 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 St, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef DB_MD5_H #define DB_MD5_H #include #include namespace DB { class FileName; class MD5 { public: MD5(); explicit MD5(const QString& md5str); bool isNull() const; MD5& operator=(const QString& md5str); /** Get hex string representation of this. * If this->isNull(), returns null string. */ QString toHexString() const; bool operator==(const MD5 &other) const; bool operator!=(const MD5& other) const; bool operator<(const MD5& other) const; + inline uint hash() const { return (uint) m_v0 ^ m_v1; } + private: bool m_isNull; qulonglong m_v0; qulonglong m_v1; }; +inline uint qHash(const MD5 &key) +{ + return key.hash(); +} + DB::MD5 MD5Sum( const DB::FileName& fileName ); } #endif /* DB_MD5_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/MD5Map.cpp b/DB/MD5Map.cpp index 27bd9902..a055c15c 100644 --- a/DB/MD5Map.cpp +++ b/DB/MD5Map.cpp @@ -1,66 +1,66 @@ /* 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 "MD5Map.h" using namespace DB; void MD5Map::insert( const MD5& md5sum, const DB::FileName& fileName ) { m_map.insert( md5sum, fileName ); m_i_map.insert( fileName, md5sum ); } DB::FileName MD5Map::lookup( const MD5& md5sum ) const { return m_map[md5sum]; } MD5 MD5Map::lookupFile( const DB::FileName& fileName ) const { return m_i_map[fileName]; } bool MD5Map::contains( const MD5& md5sum ) const { return m_map.contains( md5sum ); } bool MD5Map::containsFile( const DB::FileName& fileName ) const { return m_i_map.contains( fileName ); } void MD5Map::clear() { m_map.clear(); m_i_map.clear(); } DB::FileNameSet DB::MD5Map::diff( const MD5Map& other ) const { DB::FileNameSet res; - for( QMap::ConstIterator it = m_map.begin(); it != m_map.end(); ++it ) { + for( MD5FileMap::ConstIterator it = m_map.begin(); it != m_map.end(); ++it ) { if ( other.lookup( it.key() ) != it.value() ) res.insert( it.value() ); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/MD5Map.h b/DB/MD5Map.h index 28a3d078..fdb8d344 100644 --- a/DB/MD5Map.h +++ b/DB/MD5Map.h @@ -1,54 +1,57 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MD5MAP_H #define MD5MAP_H #include -#include +#include #include "MD5.h" #include namespace DB { + typedef QHash MD5FileMap; + typedef QHash FileMD5Map; + /** This class may be overridden by a which wants to store md5 information directly in a database, rather than in a map in memory. **/ class MD5Map { public: virtual ~MD5Map() {} virtual void insert( const MD5& md5sum, const DB::FileName& fileName ); virtual DB::FileName lookup( const MD5& md5sum ) const; virtual MD5 lookupFile( const DB::FileName& fileName ) const; virtual bool contains( const MD5& md5sum ) const; virtual bool containsFile( const DB::FileName& fileName ) const; virtual void clear(); virtual DB::FileNameSet diff( const MD5Map& other ) const; private: - QMap m_map; - QMap m_i_map; + MD5FileMap m_map; + FileMD5Map m_i_map; }; } #endif /* MD5MAP_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/NewImageFinder.h b/DB/NewImageFinder.h index 539d7b64..98505fc0 100644 --- a/DB/NewImageFinder.h +++ b/DB/NewImageFinder.h @@ -1,56 +1,55 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef NEWIMAGEFINDER_H #define NEWIMAGEFINDER_H #include "ImageInfo.h" #include "ImageInfoPtr.h" namespace DB { class MD5Map; -class IdList; class FileNameList; class NewImageFinder { public: bool findImages(); bool calculateMD5sums(const DB::FileNameList& list, DB::MD5Map* map, bool* wasCanceled=nullptr); protected: void searchForNewFiles( const DB::FileNameSet& loadedFiles, QString directory ); void setupFileVersionDetection(); void loadExtraFiles(); void loadExtraFile( const DB::FileName& name, DB::MediaType type ); void markUnTagged( ImageInfoPtr info ); bool handleIfImageHasBeenMoved( const DB::FileName& newFileName, const MD5& sum ); private: typedef QList< QPair< DB::FileName, DB::MediaType > > LoadList; LoadList m_pendingLoad; QString m_modifiedFileCompString; QRegExp m_modifiedFileComponent; QStringList m_originalFileComponents; }; } #endif /* NEWIMAGEFINDER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/XMLHandler.h b/ImportExport/XMLHandler.h index 83ba97fc..9f758253 100644 --- a/ImportExport/XMLHandler.h +++ b/ImportExport/XMLHandler.h @@ -1,53 +1,52 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef XMLHANDLER_H #define XMLHANDLER_H #include #include #include #include #include #include "Export.h" // ImageFileLocation namespace Utilities { class UniqFilenameMapper; } -namespace DB { class IdList; } namespace ImportExport { class XMLHandler { public: QByteArray createIndexXML( const DB::FileNameList& images, const QString& baseUrl, ImageFileLocation location, Utilities::UniqFilenameMapper* nameMap); protected: QDomElement save( QDomDocument doc, const DB::ImageInfoPtr& info ); void writeCategories( QDomDocument doc, QDomElement elm, const DB::ImageInfoPtr& info ); }; } #endif /* XMLHANDLER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Options.cpp b/MainWindow/Options.cpp index 08252d28..55df95da 100644 --- a/MainWindow/Options.cpp +++ b/MainWindow/Options.cpp @@ -1,157 +1,147 @@ /* Copyright (C) 2016 Johannes Zarl-Zierl 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 "Options.h" #include "Logging.h" #include #include #include MainWindow::Options* MainWindow::Options::s_instance = nullptr; namespace MainWindow { class Options::OptionsPrivate { public: QCommandLineParser parser; // legacy option: "-c " QCommandLineOption configFile { QLatin1String("c"), i18n("Use instead of the default. Deprecated - use '--db ' instead."), i18n("databaseFile") }; QCommandLineOption dbFile { QLatin1String("db"), i18n("Use instead of the default."), i18n("databaseFile") }; QCommandLineOption demoOption {QLatin1String("demo"), i18n( "Starts KPhotoAlbum with a prebuilt set of demo images." )}; QCommandLineOption importFile { QLatin1String("import"), i18n( "Import file." ), i18n("file.kim") }; // QCommandLineParser doesn't support optional values. // therefore, we need two separate options: QCommandLineOption listen { QLatin1String("listen"), i18n("Listen for network connections.") }; QCommandLineOption listenAddress { QLatin1String("listen-address"), i18n("Listen for network connections on address ."), i18n("interface_address") }; QCommandLineOption searchOnStartup {QLatin1String("search"), i18n( "Search for new images on startup." )}; }; } MainWindow::Options *MainWindow::Options::the() { if (!s_instance) s_instance = new Options(); return s_instance; } QCommandLineParser *MainWindow::Options::parser() const { return &(d->parser); } QUrl MainWindow::Options::dbFile() const { QUrl db; if (d->parser.isSet( d->dbFile)) { db = QUrl::fromLocalFile(d->parser.value( d->dbFile)); } else if (d->parser.isSet( d->configFile)) { // support for legacy option db = QUrl::fromLocalFile( d->parser.value( d->configFile )); } return db; } bool MainWindow::Options::demoMode() const { return d->parser.isSet( d->demoOption ); } QUrl MainWindow::Options::importFile() const { if (d->parser.isSet( d->importFile)) return QUrl::fromLocalFile( d->parser.value( d->importFile )); return QUrl(); } QHostAddress MainWindow::Options::listen() const { QHostAddress address; QString value = d->parser.value( d->listenAddress); if ( d->parser.isSet(d->listen) || !value.isEmpty()) { if (value.isEmpty()) address = QHostAddress::Any; else address = QHostAddress(value); } -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) if (address.isMulticast() || address == QHostAddress::Broadcast) -#else - // OpenSuse leap 42.1 still ships with Qt 5.5 - // TODO: remove this once we don't care about Qt 5.5 anymore... - if (address == QHostAddress::Broadcast) -#endif { qCWarning(MainWindowLog) << "Won't bind to address"<parser.isSet( d->searchOnStartup ); } MainWindow::Options::Options() : d(new OptionsPrivate) { d->parser.addVersionOption(); d->parser.addHelpOption(); - // OpenSuse leap 42.1 still ships with Qt 5.5 - // TODO: remove this version check once we don't care about Qt 5.5-5.7 anymore... -#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) d->configFile.setFlags(QCommandLineOption::HiddenFromHelp); -#endif d->parser.addOptions( QList() << d->configFile << d->dbFile << d->demoOption << d->importFile << d->listen << d->listenAddress << d->searchOnStartup ); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Window.cpp b/MainWindow/Window.cpp index a3cce5e7..4fd8672c 100644 --- a/MainWindow/Window.cpp +++ b/MainWindow/Window.cpp @@ -1,1988 +1,1992 @@ /* 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 #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(); + createSearchBar(); qCInfo(TimingLog) << "MainWindow: createSearchBar: " << timer.restart() << "ms."; setupStatusBar(); qCInfo(TimingLog) << "MainWindow: setupStatusBar: " << timer.restart() << "ms."; // Misc m_autoSaveTimer = new QTimer( this ); connect(m_autoSaveTimer, &QTimer::timeout, this, &Window::slotAutoSave); startAutoSaveTimer(); connect(m_browser, &Browser::BrowserWidget::showingOverview, this, &Window::showBrowser); connect( m_browser, SIGNAL(pathChanged(Browser::BreadcrumbList)), m_statusBar->mp_pathIndicator, SLOT(setBreadcrumbs(Browser::BreadcrumbList)) ); connect( m_statusBar->mp_pathIndicator, SIGNAL(widenToBreadcrumb(Browser::Breadcrumb)), m_browser, SLOT(widenToBreadcrumb(Browser::Breadcrumb)) ); connect( m_browser, SIGNAL(pathChanged(Browser::BreadcrumbList)), this, SLOT(updateDateBar(Browser::BreadcrumbList)) ); connect(m_dateBar, &DateBar::DateBarWidget::dateSelected, m_thumbnailView, &ThumbnailView::ThumbnailFacade::gotoDate); connect(m_dateBar, &DateBar::DateBarWidget::toolTipInfo, this, &Window::showDateBarTip); connect( Settings::SettingsData::instance(), SIGNAL(histogramSizeChanged(QSize)), m_dateBar, SLOT(setHistogramBarSize(QSize)) ); connect( Settings::SettingsData::instance(), SIGNAL(actualThumbnailSizeChanged(int)), this, SLOT(slotThumbnailSizeChanged()) ); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeChange, this, &Window::setDateRange); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeCleared, this, &Window::clearDateRange); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::currentDateChanged, m_dateBar, &DateBar::DateBarWidget::setDate); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::showImage, this, &Window::showImage); connect( m_thumbnailView, SIGNAL(showSelection()), this, SLOT(slotView()) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::fileIdUnderCursorChanged, this, &Window::slotSetFileName); connect( DB::ImageDB::instance(), SIGNAL(totalChanged(uint)), this, SLOT(updateDateBar()) ); connect( DB::ImageDB::instance()->categoryCollection(), SIGNAL(categoryCollectionChanged()), this, SLOT(slotOptionGroupChanged()) ); connect( m_browser, SIGNAL(imageCount(uint)), m_statusBar->mp_partial, SLOT(showBrowserMatches(uint)) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, this, &Window::updateContextMenuFromSelectionSize); 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."; 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 = MD5Sum( fileName ); ImageInfoPtr originalInfo; if ( DB::ImageDB::instance()->md5Map()->contains( originalSum ) ) { originalInfo = DB::ImageDB::instance()->info( fileName ); } else { originalInfo = fileName.info(); } // fail silent if there is no info for the file. if (!originalInfo) return; Q_FOREACH(const DB::FileName& newFile, selected()) { newFile.info()->copyExtraData(*originalInfo, false); } DirtyIndicator::markDirty(); } void MainWindow::Window::slotReReadExifInfo() { DB::FileNameList files = selectedOnDisk(); static Exif::ReReadDialog* dialog = nullptr; if ( ! dialog ) dialog = new Exif::ReReadDialog( this ); if ( dialog->exec( files ) == QDialog::Accepted ) DirtyIndicator::markDirty(); } void MainWindow::Window::slotAutoStackImages() { const DB::FileNameList list = selected(); if (list.isEmpty()) { KMessageBox::sorry( this, i18n("No item is selected."), i18n("No Selection") ); return; } QPointer stacker = new AutoStackImages( this, list ); if ( stacker->exec() == QDialog::Accepted ) showThumbNails(); delete stacker; } /** * In thumbnail mode, return a list of files that are selected. * Otherwise, return all images in the current scope/context. */ DB::FileNameList MainWindow::Window::selected( ThumbnailView::SelectionMode mode) const { if ( m_thumbnailView->gui() == m_stack->currentWidget() ) return m_thumbnailView->selection(mode); else // return all images in the current scope (parameter false: include images not on disk) return DB::ImageDB::instance()->currentScope(false); } void MainWindow::Window::slotViewNewWindow() { slotView( false, false ); } /* * Returns a list of files that are both selected and on disk. If there are no * selected files, returns all files form current context that are on disk. * Note: On some setups (NFS), this can be a very time-consuming method! * */ DB::FileNameList MainWindow::Window::selectedOnDisk() { const DB::FileNameList list = selected(ThumbnailView::NoExpandCollapsedStacks); DB::FileNameList listOnDisk; Q_FOREACH(const DB::FileName& fileName, list) { if (DB::ImageInfo::imageOnDisk(fileName)) listOnDisk.append(fileName); } return listOnDisk; } void MainWindow::Window::slotView( bool reuse, bool slideShow, bool random ) { launchViewer(selected(ThumbnailView::NoExpandCollapsedStacks), reuse, slideShow, random ); } void MainWindow::Window::launchViewer(const DB::FileNameList& inputMediaList, bool reuse, bool slideShow, bool random) { DB::FileNameList mediaList = inputMediaList; int seek = -1; if (mediaList.isEmpty()) { mediaList = m_thumbnailView->imageList( ThumbnailView::ViewOrder ); } else if (mediaList.size() == 1) { // we fake it so it appears the user has selected all images // and magically scrolls to the originally selected one const DB::FileName first = mediaList.at(0); mediaList = m_thumbnailView->imageList( ThumbnailView::ViewOrder ); seek = mediaList.indexOf(first); } if (mediaList.isEmpty()) mediaList = DB::ImageDB::instance()->currentScope( false ); if (mediaList.isEmpty()) { KMessageBox::sorry( this, i18n("There are no images to be shown.") ); return; } if (random) { mediaList = DB::FileNameList(Utilities::shuffleList(mediaList)); } Viewer::ViewerWidget* viewer; if ( reuse && Viewer::ViewerWidget::latest() ) { viewer = Viewer::ViewerWidget::latest(); viewer->raise(); viewer->activateWindow(); } else viewer = new Viewer::ViewerWidget(Viewer::ViewerWidget::ViewerWindow, &m_viewerInputMacros); connect(viewer, &Viewer::ViewerWidget::soughtTo, m_thumbnailView, &ThumbnailView::ThumbnailFacade::changeSingleSelection); connect(viewer, &Viewer::ViewerWidget::imageRotated, this, &Window::slotImageRotated); viewer->show( slideShow ); viewer->load( mediaList, seek < 0 ? 0 : seek ); viewer->raise(); } void MainWindow::Window::slotSortByDateAndTime() { DB::ImageDB::instance()->sortAndMergeBackIn(selected()); showThumbNails( DB::ImageDB::instance()->search( Browser::BrowserWidget::instance()->currentContext())); DirtyIndicator::markDirty(); } void MainWindow::Window::slotSortAllByDateAndTime() { DB::ImageDB::instance()->sortAndMergeBackIn(DB::ImageDB::instance()->images()); if ( m_thumbnailView->gui() == m_stack->currentWidget() ) showThumbNails( DB::ImageDB::instance()->search( Browser::BrowserWidget::instance()->currentContext())); DirtyIndicator::markDirty(); } QString MainWindow::Window::welcome() { QString configFileName; QPointer dialog = new WelcomeDialog( this ); // exit if the user dismissed the welcome dialog if (!dialog->exec()) { qApp->quit(); } configFileName = dialog->configFileName(); delete dialog; return configFileName; } void MainWindow::Window::closeEvent( QCloseEvent* e ) { bool quit = true; quit = slotExit(); // If I made it here, then the user canceled if ( !quit ) e->ignore(); else e->setAccepted(true); } void MainWindow::Window::slotLimitToSelected() { Utilities::ShowBusyCursor dummy; showThumbNails( selected() ); } void MainWindow::Window::setupMenuBar() { // File menu KStandardAction::save( this, SLOT(slotSave()), actionCollection() ); KStandardAction::quit( this, SLOT(slotExit()), actionCollection() ); m_generateHtml = actionCollection()->addAction( QString::fromLatin1("exportHTML") ); m_generateHtml->setText( i18n("Generate HTML...") ); connect(m_generateHtml, &QAction::triggered, this, &Window::slotExportToHTML); QAction* a = actionCollection()->addAction( QString::fromLatin1("import"), this, SLOT(slotImport()) ); a->setText( i18n( "Import...") ); a = actionCollection()->addAction( QString::fromLatin1("export"), this, SLOT(slotExport()) ); a->setText( i18n( "Export/Copy Images...") ); // Go menu a = KStandardAction::back( m_browser, SLOT(back()), actionCollection() ); connect(m_browser, &Browser::BrowserWidget::canGoBack, a, &QAction::setEnabled); a->setEnabled( false ); a = KStandardAction::forward( m_browser, SLOT(forward()), actionCollection() ); connect(m_browser, &Browser::BrowserWidget::canGoForward, a, &QAction::setEnabled); a->setEnabled( false ); a = KStandardAction::home( m_browser, SLOT(home()), actionCollection() ); actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_Home); connect(a, &QAction::triggered, m_dateBar, &DateBar::DateBarWidget::clearSelection); KStandardAction::redisplay( m_browser, SLOT(go()), actionCollection() ); // The Edit menu m_copy = KStandardAction::copy( this, SLOT(slotCopySelectedURLs()), actionCollection() ); m_paste = KStandardAction::paste( this, SLOT(slotPasteInformation()), actionCollection() ); m_paste->setEnabled(false); m_selectAll = KStandardAction::selectAll( m_thumbnailView, SLOT(selectAll()), actionCollection() ); + m_clearSelection = KStandardAction::deselect( m_thumbnailView, SLOT(clearSelection()), actionCollection() ); + m_clearSelection->setEnabled(false); KStandardAction::find( this, SLOT(slotSearch()), actionCollection() ); m_deleteSelected = actionCollection()->addAction(QString::fromLatin1("deleteSelected")); m_deleteSelected->setText( i18nc("Delete selected images", "Delete Selected" ) ); m_deleteSelected->setIcon( QIcon::fromTheme( QString::fromLatin1("edit-delete") ) ); actionCollection()->setDefaultShortcut(m_deleteSelected, Qt::Key_Delete); connect(m_deleteSelected, &QAction::triggered, this, &Window::slotDeleteSelected); a = actionCollection()->addAction(QString::fromLatin1("removeTokens"), this, SLOT(slotRemoveTokens())); a->setText( i18n("Remove Tokens...") ); a = actionCollection()->addAction(QString::fromLatin1("showListOfFiles"), this, SLOT(slotShowListOfFiles())); a->setText( i18n("Open List of Files...")) ; m_configOneAtATime = actionCollection()->addAction( QString::fromLatin1("oneProp"), this, SLOT(slotConfigureImagesOneAtATime()) ); m_configOneAtATime->setText( i18n( "Annotate Individual Items" ) ); actionCollection()->setDefaultShortcut(m_configOneAtATime, Qt::CTRL + Qt::Key_1); m_configAllSimultaniously = actionCollection()->addAction( QString::fromLatin1("allProp"), this, SLOT(slotConfigureAllImages()) ); m_configAllSimultaniously->setText( i18n( "Annotate Multiple Items at a Time" ) ); actionCollection()->setDefaultShortcut(m_configAllSimultaniously, Qt::CTRL + Qt::Key_2); m_createImageStack = actionCollection()->addAction( QString::fromLatin1("createImageStack"), this, SLOT(slotCreateImageStack()) ); m_createImageStack->setText( i18n("Merge Images into a Stack") ); actionCollection()->setDefaultShortcut(m_createImageStack, Qt::CTRL + Qt::Key_3); m_unStackImages = actionCollection()->addAction( QString::fromLatin1("unStackImages"), this, SLOT(slotUnStackImages()) ); m_unStackImages->setText( i18n("Remove Images from Stack") ); m_setStackHead = actionCollection()->addAction( QString::fromLatin1("setStackHead"), this, SLOT(slotSetStackHead()) ); m_setStackHead->setText( i18n("Set as First Image in Stack") ); actionCollection()->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4); m_rotLeft = actionCollection()->addAction( QString::fromLatin1("rotateLeft"), this, SLOT(slotRotateSelectedLeft()) ); m_rotLeft->setText( i18n( "Rotate counterclockwise" ) ); actionCollection()->setDefaultShortcut(m_rotLeft, Qt::Key_7); m_rotRight = actionCollection()->addAction( QString::fromLatin1("rotateRight"), this, SLOT(slotRotateSelectedRight()) ); m_rotRight->setText( i18n( "Rotate clockwise" ) ); actionCollection()->setDefaultShortcut(m_rotRight, Qt::Key_9); // The Images menu m_view = actionCollection()->addAction( QString::fromLatin1("viewImages"), this, SLOT(slotView()) ); m_view->setText( i18n("View") ); actionCollection()->setDefaultShortcut(m_view, Qt::CTRL + Qt::Key_I); m_viewInNewWindow = actionCollection()->addAction( QString::fromLatin1("viewImagesNewWindow"), this, SLOT(slotViewNewWindow()) ); m_viewInNewWindow->setText( i18n("View (In New Window)") ); m_runSlideShow = actionCollection()->addAction( QString::fromLatin1("runSlideShow"), this, SLOT(slotRunSlideShow()) ); m_runSlideShow->setText( i18n("Run Slide Show") ); m_runSlideShow->setIcon( QIcon::fromTheme( QString::fromLatin1("view-presentation") ) ); actionCollection()->setDefaultShortcut(m_runSlideShow, Qt::CTRL + Qt::Key_R); m_runRandomSlideShow = actionCollection()->addAction( QString::fromLatin1("runRandomizedSlideShow"), this, SLOT(slotRunRandomizedSlideShow()) ); m_runRandomSlideShow->setText( i18n( "Run Randomized Slide Show" ) ); a = actionCollection()->addAction( QString::fromLatin1("collapseAllStacks"), m_thumbnailView, SLOT(collapseAllStacks()) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::collapseAllStacksEnabled, a, &QAction::setEnabled); a->setEnabled(false); a->setText( i18n("Collapse all stacks" )); a = actionCollection()->addAction( QString::fromLatin1("expandAllStacks"), m_thumbnailView, SLOT(expandAllStacks()) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::expandAllStacksEnabled, a, &QAction::setEnabled); a->setEnabled(false); a->setText( i18n("Expand all stacks" )); QActionGroup* grp = new QActionGroup( this ); a = actionCollection()->add( QString::fromLatin1("orderIncr"), this, SLOT(slotOrderIncr()) ); a->setText( i18n("Show &Oldest First") ) ; a->setActionGroup(grp); a->setChecked( !Settings::SettingsData::instance()->showNewestThumbnailFirst() ); a = actionCollection()->add( QString::fromLatin1("orderDecr"), this, SLOT(slotOrderDecr()) ); a->setText( i18n("Show &Newest First") ); a->setActionGroup(grp); a->setChecked( Settings::SettingsData::instance()->showNewestThumbnailFirst() ); m_sortByDateAndTime = actionCollection()->addAction( QString::fromLatin1("sortImages"), this, SLOT(slotSortByDateAndTime()) ); m_sortByDateAndTime->setText( i18n("Sort Selected by Date && Time") ); m_limitToMarked = actionCollection()->addAction( QString::fromLatin1("limitToMarked"), this, SLOT(slotLimitToSelected()) ); m_limitToMarked->setText( i18n("Limit View to Selection") ); m_jumpToContext = actionCollection()->addAction( QString::fromLatin1("jumpToContext"), this, SLOT(slotJumpToContext()) ); m_jumpToContext->setText( i18n("Jump to Context") ); actionCollection()->setDefaultShortcut(m_jumpToContext, Qt::CTRL + Qt::Key_J); m_jumpToContext->setIcon( QIcon::fromTheme( QString::fromLatin1( "kphotoalbum" ) ) ); // icon suggestion: go-jump (don't know the exact meaning though, so I didn't replace it right away m_lock = actionCollection()->addAction( QString::fromLatin1("lockToDefaultScope"), this, SLOT(lockToDefaultScope()) ); m_lock->setText( i18n("Lock Images") ); m_unlock = actionCollection()->addAction( QString::fromLatin1("unlockFromDefaultScope"), this, SLOT(unlockFromDefaultScope()) ); m_unlock->setText( i18n("Unlock") ); a = actionCollection()->addAction( QString::fromLatin1("changeScopePasswd"), this, SLOT(changePassword()) ); a->setText( i18n("Change Password...") ); actionCollection()->setDefaultShortcut(a, 0); m_setDefaultPos = actionCollection()->addAction( QString::fromLatin1("setDefaultScopePositive"), this, SLOT(setDefaultScopePositive()) ); m_setDefaultPos->setText( i18n("Lock Away All Other Items") ); m_setDefaultNeg = actionCollection()->addAction( QString::fromLatin1("setDefaultScopeNegative"), this, SLOT(setDefaultScopeNegative()) ); m_setDefaultNeg->setText( i18n("Lock Away Current Set of Items") ); // Maintenance a = actionCollection()->addAction( QString::fromLatin1("findUnavailableImages"), this, SLOT(slotShowNotOnDisk()) ); a->setText( i18n("Display Images and Videos Not on Disk") ); a = actionCollection()->addAction( QString::fromLatin1("findImagesWithInvalidDate"), this, SLOT(slotShowImagesWithInvalidDate()) ); a->setText( i18n("Display Images and Videos with Incomplete Dates...") ); #ifdef DOES_STILL_NOT_WORK_IN_KPA4 a = actionCollection()->addAction( QString::fromLatin1("findImagesWithChangedMD5Sum"), this, SLOT(slotShowImagesWithChangedMD5Sum()) ); a->setText( i18n("Display Images and Videos with Changed MD5 Sum") ); #endif //DOES_STILL_NOT_WORK_IN_KPA4 a = actionCollection()->addAction( QLatin1String("mergeDuplicates"), this, SLOT(mergeDuplicates())); a->setText(i18n("Merge duplicates")); a = actionCollection()->addAction( QString::fromLatin1("rebuildMD5s"), this, SLOT(slotRecalcCheckSums()) ); a->setText( i18n("Recalculate Checksum") ); a = actionCollection()->addAction( QString::fromLatin1("rescan"), DB::ImageDB::instance(), SLOT(slotRescan()) ); + a->setIcon(QIcon::fromTheme( QString::fromLatin1( "document-import" ) )); a->setText( i18n("Rescan for Images and Videos") ); QAction* recreateExif = actionCollection()->addAction( QString::fromLatin1( "recreateExifDB" ), this, SLOT(slotRecreateExifDB()) ); recreateExif->setText( i18n("Recreate Exif Search Database") ); QAction* rereadExif = actionCollection()->addAction( QString::fromLatin1("reReadExifInfo"), this, SLOT(slotReReadExifInfo()) ); rereadExif->setText( i18n("Read Exif Info from Files...") ); m_sortAllByDateAndTime = actionCollection()->addAction( QString::fromLatin1("sortAllImages"), this, SLOT(slotSortAllByDateAndTime()) ); m_sortAllByDateAndTime->setText( i18n("Sort All by Date && Time") ); m_sortAllByDateAndTime->setEnabled(true); m_AutoStackImages = actionCollection()->addAction( QString::fromLatin1( "autoStack" ), this, SLOT (slotAutoStackImages()) ); m_AutoStackImages->setText( i18n("Automatically Stack Selected Images...") ); a = actionCollection()->addAction( QString::fromLatin1("buildThumbs"), this, SLOT(slotBuildThumbnails()) ); a->setText( i18n("Build Thumbnails") ); a->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 ); + m_clearSelection->setEnabled(selectionSize > 0); } void MainWindow::Window::rotateSelected( int angle ) { const DB::FileNameList list = selected(); if (list.isEmpty()) { KMessageBox::sorry( this, i18n("No item is selected."), i18n("No Selection") ); } else { Q_FOREACH(const DB::FileName& fileName, list) { fileName.info()->rotate(angle); ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } m_statusBar->mp_dirtyIndicator->markDirty(); } } void MainWindow::Window::slotRotateSelectedLeft() { rotateSelected( -90 ); reloadThumbnails(); } void MainWindow::Window::slotRotateSelectedRight() { rotateSelected( 90 ); reloadThumbnails(); } void MainWindow::Window::reloadThumbnails( ThumbnailView::SelectionUpdateMethod method ) { m_thumbnailView->reload( method ); updateContextMenuFromSelectionSize( m_thumbnailView->selection().size() ); } void MainWindow::Window::slotUpdateViewMenu( DB::Category::ViewType type ) { if ( type == DB::Category::TreeView ) m_smallListView->setChecked( true ); else if ( type == DB::Category::ThumbedTreeView ) m_largeListView->setChecked( true ); else if ( type == DB::Category::ThumbedIconView ) m_largeIconView->setChecked( true ); } void MainWindow::Window::slotShowNotOnDisk() { DB::FileNameList notOnDisk; Q_FOREACH(const DB::FileName& fileName, DB::ImageDB::instance()->images()) { if ( !fileName.exists() ) notOnDisk.append(fileName); } showThumbNails(notOnDisk); } void MainWindow::Window::slotShowImagesWithChangedMD5Sum() { #ifdef DOES_STILL_NOT_WORK_IN_KPA4 Utilities::ShowBusyCursor dummy; StringSet changed = DB::ImageDB::instance()->imagesWithMD5Changed(); showThumbNails( changed.toList() ); #else // DOES_STILL_NOT_WORK_IN_KPA4 qFatal("Code commented out in MainWindow::Window::slotShowImagesWithChangedMD5Sum"); #endif // DOES_STILL_NOT_WORK_IN_KPA4 } void MainWindow::Window::updateStates( bool thumbNailView ) { m_selectAll->setEnabled( thumbNailView ); m_deleteSelected->setEnabled( thumbNailView ); m_limitToMarked->setEnabled( thumbNailView ); m_jumpToContext->setEnabled( thumbNailView ); } void MainWindow::Window::slotRunSlideShow() { slotView( true, true ); } void MainWindow::Window::slotRunRandomizedSlideShow() { slotView( true, true, true ); } MainWindow::Window* MainWindow::Window::theMainWindow() { Q_ASSERT( s_instance ); return s_instance; } void MainWindow::Window::slotConfigureToolbars() { QPointer dlg = new KEditToolBar(guiFactory()); connect(dlg, SIGNAL(newToolbarConfig()), SLOT(slotNewToolbarConfig())); dlg->exec(); delete dlg; } void MainWindow::Window::slotNewToolbarConfig() { createGUI(); - createSarchBar(); + createSearchBar(); } void MainWindow::Window::slotImport() { ImportExport::Import::imageImport(); } void MainWindow::Window::slotExport() { ImportExport::Export::imageExport(selectedOnDisk()); } void MainWindow::Window::slotReenableMessages() { int ret = KMessageBox::questionYesNo( this, i18n("

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

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

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

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

" ), i18n("Internal Error") ); m_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 = 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() +void MainWindow::Window::createSearchBar() { // Set up the search tool bar SearchBar* bar = new SearchBar( this ); bar->setLineEditEnabled(false); bar->setObjectName(QString::fromUtf8("searchBar")); connect(bar, &SearchBar::textChanged, m_browser, &Browser::BrowserWidget::slotLimitToMatch); connect(bar, &SearchBar::returnPressed, m_browser, &Browser::BrowserWidget::slotInvokeSeleted); connect(bar, &SearchBar::keyPressed, m_browser, &Browser::BrowserWidget::scrollKeyPressed); connect(m_browser, &Browser::BrowserWidget::viewChanged, bar, &SearchBar::reset); connect(m_browser, &Browser::BrowserWidget::isSearchable, bar, &SearchBar::setLineEditEnabled); } 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/MainWindow/Window.h b/MainWindow/Window.h index 6d461bda..2a254fbc 100644 --- a/MainWindow/Window.h +++ b/MainWindow/Window.h @@ -1,269 +1,270 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MAINWINDOW_WINDOW_H #define MAINWINDOW_WINDOW_H #include #include #include #include #include #include #include #include #include #ifdef HAVE_KGEOMAP #include #endif class QAction; class QCloseEvent; class QContextMenuEvent; class QFrame; class QLabel; class QMoveEvent; class QResizeEvent; class QStackedWidget; class QTimer; class KActionMenu; class KTipDialog; class KToggleAction; #ifdef HASKIPI namespace KIPI { class PluginLoader; } #endif namespace AnnotationDialog { class Dialog; } namespace Browser{ class BrowserWidget; class BreadcrumbList; } namespace DateBar { class DateBarWidget; } namespace DB { class ImageInfoList; } namespace HTMLGenerator { class HTMLDialog; } namespace Plugins { class Interface; } namespace Settings { class SettingsDialog; } namespace ThumbnailView { class ThumbnailFacade; } class BreadcrumbViewer; namespace MainWindow { class DeleteDialog; class StatusBar; class TokenEditor; class Window :public KXmlGuiWindow { Q_OBJECT public: explicit Window( QWidget* parent ); ~Window(); static void configureImages( const DB::ImageInfoList& list, bool oneAtATime ); static Window* theMainWindow(); DB::FileNameList selected( ThumbnailView::SelectionMode mode = ThumbnailView::ExpandCollapsedStacks ) const; DB::ImageSearchInfo currentContext(); QString currentBrowseCategory() const; void setStackHead( const DB::FileName& image ); void setHistogramVisibilty( bool visible ) const; bool dbIsDirty() const; #ifdef HAVE_KGEOMAP void showPositionBrowser(); Browser::PositionBrowserWidget* positionBrowserWidget(); #endif public slots: void showThumbNails(const DB::FileNameList& items); void loadPlugins(); void reloadThumbnails( ThumbnailView::SelectionUpdateMethod method = ThumbnailView::MaintainSelection ); void runDemo(); void slotImageRotated(const DB::FileName& fileName); void slotSave(); protected slots: void showThumbNails(); bool slotExit(); void slotOptions(); void slotConfigureAllImages(); void slotConfigureImagesOneAtATime(); void slotCreateImageStack(); void slotUnStackImages(); void slotSetStackHead(); void slotCopySelectedURLs(); void slotPasteInformation(); void slotDeleteSelected(); void slotReReadExifInfo(); void slotAutoStackImages(); void slotSearch(); void slotView( bool reuse = true, bool slideShow = false, bool random = false ); void slotViewNewWindow(); void slotSortByDateAndTime(); void slotSortAllByDateAndTime(); void slotLimitToSelected(); void slotExportToHTML(); void slotAutoSave(); void showBrowser(); void slotOptionGroupChanged(); void showTipOfDay(); void lockToDefaultScope(); void setDefaultScopePositive(); void setDefaultScopeNegative(); void unlockFromDefaultScope(); void changePassword(); void slotConfigureKeyBindings(); void slotSetFileName( const DB::FileName& ); void updateContextMenuFromSelectionSize(int selectionSize); void slotUpdateViewMenu( DB::Category::ViewType ); void slotShowNotOnDisk(); void slotBuildThumbnails(); void slotBuildThumbnailsIfWanted(); void slotRunSlideShow(); void slotRunRandomizedSlideShow(); void slotConfigureToolbars(); void slotNewToolbarConfig(); void slotImport(); void slotExport(); void delayedInit(); void slotReenableMessages(); void slotImagesChanged( const QList& ); void slotSelectionChanged(int count); void plug(); void slotRemoveTokens(); void slotShowListOfFiles(); void updateDateBar( const Browser::BreadcrumbList& ); void updateDateBar(); void slotShowImagesWithInvalidDate(); void slotShowImagesWithChangedMD5Sum(); void showDateBarTip( const QString& ); void slotJumpToContext(); void setDateRange( const DB::ImageDate& ); void clearDateRange(); void startAutoSaveTimer(); void slotRecalcCheckSums(); void slotShowExifInfo(); void showFeatures(); void showImage( const DB::FileName& fileName ); void slotOrderIncr(); void slotOrderDecr(); void slotRotateSelectedLeft(); void slotRotateSelectedRight(); void rotateSelected( int angle ); void showVideos(); void slotStatistics(); void slotRecreateExifDB(); void useNextVideoThumbnail(); void usePreviousVideoThumbnail(); void mergeDuplicates(); void slotThumbnailSizeChanged(); void slotMarkUntagged(); protected: void configureImages( bool oneAtATime ); QString welcome(); virtual void closeEvent( QCloseEvent* e ); virtual void resizeEvent( QResizeEvent* ); virtual void moveEvent ( QMoveEvent * ); void setupMenuBar(); void createAnnotationDialog(); bool load(); virtual void contextMenuEvent( QContextMenuEvent* e ); void setLocked( bool b, bool force, bool recount=true ); void configImages( const DB::ImageInfoList& list, bool oneAtATime ); void updateStates( bool thumbNailView ); DB::FileNameList selectedOnDisk(); void setupPluginMenu(); void launchViewer(const DB::FileNameList& mediaList, bool reuse, bool slideShow, bool random); void setupStatusBar(); void setPluginMenuState( const char* name, const QList& actions ); - void createSarchBar(); + void createSearchBar(); void executeStartupActions(); void checkIfMplayerIsInstalled(); bool anyVideosSelected() const; void announceAndroidVersion(); #ifdef HAVE_KGEOMAP Browser::PositionBrowserWidget* createPositionBrowser(); #endif private: static Window* s_instance; ThumbnailView::ThumbnailFacade* m_thumbnailView; Settings::SettingsDialog* m_settingsDialog; QPointer m_annotationDialog; QStackedWidget* m_stack; QTimer* m_autoSaveTimer; Browser::BrowserWidget* m_browser; DeleteDialog* m_deleteDialog; QAction* m_lock; QAction* m_unlock; QAction* m_setDefaultPos; QAction* m_setDefaultNeg; QAction* m_jumpToContext; HTMLGenerator::HTMLDialog* m_htmlDialog; QAction* m_configOneAtATime; QAction* m_configAllSimultaniously; QAction* m_createImageStack; QAction* m_unStackImages; QAction* m_setStackHead; QAction* m_view; QAction* m_rotLeft; QAction* m_rotRight; QAction* m_sortByDateAndTime; QAction* m_sortAllByDateAndTime; QAction* m_AutoStackImages; QAction* m_viewInNewWindow; KActionMenu* m_viewMenu; KToggleAction* m_smallListView; KToggleAction* m_largeListView; KToggleAction* m_largeIconView; QAction* m_generateHtml; QAction* m_copy; QAction* m_paste; QAction* m_deleteSelected; QAction* m_limitToMarked; QAction* m_selectAll; + QAction* m_clearSelection; QAction* m_runSlideShow; QAction* m_runRandomSlideShow; Plugins::Interface* m_pluginInterface; QAction* m_showExifDialog; #ifdef HASKIPI KIPI::PluginLoader* m_pluginLoader; #endif QAction* m_recreateThumbnails; QAction* m_useNextVideoThumbnail; QAction* m_usePreviousVideoThumbnail; QAction* m_markUntagged; TokenEditor* m_tokenEditor; DateBar::DateBarWidget* m_dateBar; QFrame* m_dateBarLine; bool m_hasLoadedPlugins; QMap > m_viewerInputMacros; MainWindow::StatusBar* m_statusBar; QString m_lastTarget; #ifdef HAVE_KGEOMAP Browser::PositionBrowserWidget* m_positionBrowser; #endif }; } #endif /* MAINWINDOW_WINDOW_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailFacade.cpp b/ThumbnailView/ThumbnailFacade.cpp index 47a73d75..3381d0ed 100644 --- a/ThumbnailView/ThumbnailFacade.cpp +++ b/ThumbnailView/ThumbnailFacade.cpp @@ -1,175 +1,180 @@ /* 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 "ThumbnailFacade.h" #include "ImageManager/ThumbnailCache.h" #include #include "Settings/SettingsData.h" #include "ThumbnailToolTip.h" #include "ThumbnailModel.h" #include "CellGeometry.h" #include "ThumbnailWidget.h" #include "GridResizeSlider.h" ThumbnailView::ThumbnailFacade* ThumbnailView::ThumbnailFacade::s_instance = nullptr; ThumbnailView::ThumbnailFacade::ThumbnailFacade() :m_cellGeometry( nullptr ), m_model( nullptr ),m_widget( nullptr ), m_toolTip( nullptr ) { // To avoid one of the components references one of the other before it has been initialized, we first construct them all with null. m_cellGeometry = new CellGeometry(this); m_model = new ThumbnailModel(this); m_widget = new ThumbnailWidget(this); m_toolTip = new ThumbnailToolTip( m_widget ); connect(m_widget, &ThumbnailWidget::showImage, this, &ThumbnailFacade::showImage); connect(m_widget, &ThumbnailWidget::showSelection, this, &ThumbnailFacade::showSelection); connect(m_widget, &ThumbnailWidget::fileIdUnderCursorChanged, this, &ThumbnailFacade::fileIdUnderCursorChanged); connect(m_widget, &ThumbnailWidget::currentDateChanged, this, &ThumbnailFacade::currentDateChanged); connect(m_widget, &ThumbnailWidget::selectionCountChanged, this, &ThumbnailFacade::selectionChanged); connect(m_model, &ThumbnailModel::collapseAllStacksEnabled, this, &ThumbnailFacade::collapseAllStacksEnabled); connect(m_model, &ThumbnailModel::expandAllStacksEnabled, this, &ThumbnailFacade::expandAllStacksEnabled); s_instance = this; } QWidget* ThumbnailView::ThumbnailFacade::gui() { return m_widget; } void ThumbnailView::ThumbnailFacade::gotoDate( const DB::ImageDate& date, bool b) { m_widget->gotoDate( date, b ); } void ThumbnailView::ThumbnailFacade::setCurrentItem( const DB::FileName& fileName ) { widget()->setCurrentItem(fileName); } void ThumbnailView::ThumbnailFacade::reload( SelectionUpdateMethod method ) { m_widget->reload( method ); } DB::FileNameList ThumbnailView::ThumbnailFacade::selection( ThumbnailView::SelectionMode mode ) const { return m_widget->selection(mode); } DB::FileNameList ThumbnailView::ThumbnailFacade::imageList(Order order) const { return m_model->imageList(order); } DB::FileName ThumbnailView::ThumbnailFacade::mediaIdUnderCursor() const { return m_widget->mediaIdUnderCursor(); } DB::FileName ThumbnailView::ThumbnailFacade::currentItem() const { return m_model->imageAt(m_widget->currentIndex().row()); } void ThumbnailView::ThumbnailFacade::setImageList(const DB::FileNameList& list) { m_model->setImageList(list); } void ThumbnailView::ThumbnailFacade::setSortDirection( SortDirection direction ) { m_model->setSortDirection( direction ); } QSlider* ThumbnailView::ThumbnailFacade::createResizeSlider() { return new GridResizeSlider( this ); } void ThumbnailView::ThumbnailFacade::selectAll() { m_widget->selectAll(); } +void ThumbnailView::ThumbnailFacade::clearSelection() +{ + m_widget->clearSelection(); +} + void ThumbnailView::ThumbnailFacade::showToolTipsOnImages( bool on ) { m_toolTip->setActive( on ); } void ThumbnailView::ThumbnailFacade::toggleStackExpansion(const DB::FileName& fileName) { m_model->toggleStackExpansion(fileName); } void ThumbnailView::ThumbnailFacade::collapseAllStacks() { m_model->collapseAllStacks(); } void ThumbnailView::ThumbnailFacade::expandAllStacks() { m_model->expandAllStacks(); } void ThumbnailView::ThumbnailFacade::updateDisplayModel() { m_model->updateDisplayModel(); } void ThumbnailView::ThumbnailFacade::changeSingleSelection(const DB::FileName& fileName) { m_widget->changeSingleSelection(fileName); } ThumbnailView::ThumbnailModel* ThumbnailView::ThumbnailFacade::model() { Q_ASSERT( m_model ); return m_model; } ThumbnailView::CellGeometry* ThumbnailView::ThumbnailFacade::cellGeometry() { Q_ASSERT( m_cellGeometry ); return m_cellGeometry; } ThumbnailView::ThumbnailWidget* ThumbnailView::ThumbnailFacade::widget() { Q_ASSERT( m_widget ); return m_widget; } ThumbnailView::ThumbnailFacade* ThumbnailView::ThumbnailFacade::instance() { Q_ASSERT( s_instance ); return s_instance; } void ThumbnailView::ThumbnailFacade::slotRecreateThumbnail() { Q_FOREACH( const DB::FileName& fileName, widget()->selection( NoExpandCollapsedStacks )) { ImageManager::ThumbnailCache::instance()->removeThumbnail( fileName ); BackgroundJobs::HandleVideoThumbnailRequestJob::removeFullScaleFrame(fileName); m_model->updateCell(fileName); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailFacade.h b/ThumbnailView/ThumbnailFacade.h index 1e58e785..4d0f9a48 100644 --- a/ThumbnailView/ThumbnailFacade.h +++ b/ThumbnailView/ThumbnailFacade.h @@ -1,91 +1,92 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef THUMBNAILFACADE_H #define THUMBNAILFACADE_H #include "ThumbnailFactory.h" #include "ThumbnailWidget.h" #include class QSlider; namespace ThumbnailView { class ThumbnailModel; class CellGeometry; class ThumbnailPainter; class ThumbnailToolTip; class ThumbnailFacade :public QObject, public ThumbnailFactory { Q_OBJECT public: static ThumbnailFacade* instance(); ThumbnailFacade(); QWidget* gui(); void setCurrentItem( const DB::FileName& fileName ); void reload( SelectionUpdateMethod method ); DB::FileNameList selection( ThumbnailView::SelectionMode mode = ExpandCollapsedStacks ) const; DB::FileNameList imageList(Order) const; DB::FileName mediaIdUnderCursor() const; DB::FileName currentItem() const; void setImageList(const DB::FileNameList& list); void setSortDirection( SortDirection ); /** * @brief createResizeSlider returns a QSlider that can be used to resize the thumbnail grid. * @return a (horizontal) QSlider */ QSlider* createResizeSlider(); public slots: void gotoDate( const DB::ImageDate& date, bool includeRanges ); void selectAll(); + void clearSelection(); void showToolTipsOnImages( bool b ); void toggleStackExpansion(const DB::FileName& id); void collapseAllStacks(); void expandAllStacks(); void updateDisplayModel(); void changeSingleSelection(const DB::FileName& fileName); void slotRecreateThumbnail(); signals: void showImage( const DB::FileName& id ); void showSelection(); void fileIdUnderCursorChanged( const DB::FileName& id ); void currentDateChanged( const QDateTime& ); void selectionChanged(int numberOfItemsSelected ); void collapseAllStacksEnabled(bool enabled); void expandAllStacksEnabled(bool enabled); private: ThumbnailModel* model() override; CellGeometry* cellGeometry() override; ThumbnailWidget* widget() override; private: static ThumbnailFacade* s_instance; CellGeometry* m_cellGeometry; ThumbnailModel* m_model; ThumbnailWidget* m_widget; ThumbnailPainter* m_painter; ThumbnailToolTip* m_toolTip; }; } #endif /* THUMBNAILFACADE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ImageDisplay.cpp b/Viewer/ImageDisplay.cpp index 681c5a94..469b92f4 100644 --- a/Viewer/ImageDisplay.cpp +++ b/Viewer/ImageDisplay.cpp @@ -1,738 +1,764 @@ /* 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 "ImageDisplay.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include "DB/ImageDB.h" #include "ImageManager/AsyncLoader.h" #include "Settings/SettingsData.h" #include "Viewer/ViewHandler.h" #include /** Area displaying the actual image in the viewer. The purpose of this class is to display the actual image in the viewer. This involves controlling zooming and drawing on the images. This class is quite complicated as it had to both be fast and memory efficient. The following are dead end tried: 1) Initially QPainter::setWindow was used for zooming the images, but this had the effect that if you zoom to 100x100 from a 2300x1700 image on a 800x600 display, then Qt would internally create a pixmap with the size (2300/100)*800, (1700/100)*600, which takes up 1.4Gb of memory! 2) I tried doing all scaling and cropping using QPixmap's as that would allow me to keep all transformations on the X Server site (making resizing fast - or I beleived so). Unfortunately it showed up that this was much slower than doing it using QImage, and the result was thus that the looking at a series of images was slow. The process is as follows: - The image loaded from disk is rotated and stored in _loadedImage. Initially this image is as large as the view, until the user starts zooming, at which time the image is reloaded to the size as it is on disk. - Then _loadedImage is cropped and scaled to _croppedAndScaledImg. This image is the size of the display. Resizing the window thus needs to redo step. - Finally in paintEvent _croppedAndScaledImg is drawn to the screen. - The aboce might very likely be simplified. Back in the old days it needed to be that + The above might very likely be simplified. Back in the old days it needed to be that complex to allow drawing on images. To propagate the cache, we need to know which direction the images are viewed in, which is the job of the instance variable _forward. */ Viewer::ImageDisplay::ImageDisplay( QWidget* parent) :AbstractDisplay( parent ), m_reloadImageInProgress( false ), m_forward(true), m_curIndex(0),m_busy( false ), m_cursorHiding(true) { m_viewHandler = new ViewHandler( this ); setMouseTracking( true ); m_cursorTimer = new QTimer( this ); m_cursorTimer->setSingleShot(true); connect(m_cursorTimer, &QTimer::timeout, this, &ImageDisplay::hideCursor); showCursor(); } /** * If mouse cursor hiding is enabled, hide the cursor right now */ void Viewer::ImageDisplay::hideCursor() { if (m_cursorHiding) setCursor( Qt::BlankCursor ); } /** * If mouse cursor hiding is enabled, show normal cursor and start a timer that will hide it later */ void Viewer::ImageDisplay::showCursor() { if (m_cursorHiding) { unsetCursor(); m_cursorTimer->start( 1500 ); } } /** * Prevent hideCursor() and showCursor() from altering cursor state */ void Viewer::ImageDisplay::disableCursorHiding() { m_cursorHiding = false; } /** * Enable automatic mouse cursor hiding */ void Viewer::ImageDisplay::enableCursorHiding() { m_cursorHiding = true; } void Viewer::ImageDisplay::mousePressEvent( QMouseEvent* event ) { // disable cursor hiding till button release disableCursorHiding(); QMouseEvent e( event->type(), mapPos( event->pos() ), event->button(), event->buttons(), event->modifiers() ); double ratio = sizeRatio( QSize(m_zEnd.x()-m_zStart.x(), m_zEnd.y()-m_zStart.y()), size() ); bool block = m_viewHandler->mousePressEvent( &e, event->pos(), ratio ); if ( !block ) QWidget::mousePressEvent( event ); update(); } void Viewer::ImageDisplay::mouseMoveEvent( QMouseEvent* event ) { // just reset the timer showCursor(); QMouseEvent e( event->type(), mapPos( event->pos() ), event->button(), event->buttons(), event->modifiers() ); double ratio = sizeRatio( QSize(m_zEnd.x()-m_zStart.x(), m_zEnd.y()-m_zStart.y()), size() ); bool block = m_viewHandler->mouseMoveEvent( &e, event->pos(), ratio ); if ( !block ) QWidget::mouseMoveEvent( event ); update(); } void Viewer::ImageDisplay::mouseReleaseEvent( QMouseEvent* event ) { // enable cursor hiding and reset timer enableCursorHiding(); showCursor(); m_cache.remove( m_curIndex ); QMouseEvent e( event->type(), mapPos( event->pos() ), event->button(), event->buttons(), event->modifiers() ); double ratio = sizeRatio( QSize(m_zEnd.x()-m_zStart.x(), m_zEnd.y()-m_zStart.y()), size() ); bool block = m_viewHandler->mouseReleaseEvent( &e, event->pos(), ratio ); if ( !block ) { QWidget::mouseReleaseEvent( event ); } emit possibleChange(); update(); } bool Viewer::ImageDisplay::setImage( DB::ImageInfoPtr info, bool forward ) { + qCDebug(ViewerLog) << "setImage(" << info->fileName().relative() << "," << forward <<")"; m_info = info; m_loadedImage = QImage(); // Find the index of the current image m_curIndex = 0; Q_FOREACH( const DB::FileName &filename, m_imageList) { if ( filename == info->fileName() ) break; ++m_curIndex; } if ( m_cache.contains(m_curIndex) && m_cache[m_curIndex].angle == info->angle()) { const ViewPreloadInfo& found = m_cache[m_curIndex]; m_loadedImage = found.img; updateZoomPoints( Settings::SettingsData::instance()->viewerStandardSize(), found.img.size() ); cropAndScale(); info->setSize( found.size ); emit imageReady(); } else { requestImage( info, true ); busy(); } m_forward = forward; updatePreload(); return true; } void Viewer::ImageDisplay::resizeEvent( QResizeEvent* event ) { ImageManager::AsyncLoader::instance()->stop( this, ImageManager::StopOnlyNonPriorityLoads ); m_cache.clear(); if ( m_info ) { cropAndScale(); if ( event->size().width() > 1.5*this->m_loadedImage.size().width() || event->size().height() > 1.5*this->m_loadedImage.size().height() ) - potentialyLoadFullSize(); // Only do if we scale much bigger. + potentiallyLoadFullSize(); // Only do if we scale much bigger. } updatePreload(); } void Viewer::ImageDisplay::paintEvent( QPaintEvent* ) { int x = ( width() - m_croppedAndScaledImg.width() ) / 2; int y = ( height() - m_croppedAndScaledImg.height() ) / 2; QPainter painter( this ); painter.fillRect( 0,0, width(), height(), Qt::black ); painter.drawImage( x,y, m_croppedAndScaledImg ); } QPoint Viewer::ImageDisplay::offset( int logicalWidth, int logicalHeight, int physicalWidth, int physicalHeight, double* ratio ) { double rat = sizeRatio( QSize( logicalWidth, logicalHeight ), QSize( physicalWidth, physicalHeight ) ); int ox = (int) (physicalWidth - logicalWidth*rat)/2; int oy = (int) (physicalHeight - logicalHeight*rat)/2; if ( ratio ) *ratio = rat; return QPoint(ox,oy); } void Viewer::ImageDisplay::zoom( QPoint p1, QPoint p2 ) { + qCDebug(ViewerLog, "zoom(%d,%d, %d,%d)",p1.x(),p1.y(),p2.x(),p2.y()); m_cache.remove( m_curIndex ); normalize( p1, p2 ); double ratio; QPoint off = offset( (p2-p1).x(), (p2-p1).y(), width(), height(), &ratio ); off = off / ratio; p1.setX( p1.x() - off.x() ); p1.setY( p1.y() - off.y() ); p2.setX( p2.x()+off.x() ); p2.setY( p2.y()+off.y() ); m_zStart = p1; m_zEnd = p2; - potentialyLoadFullSize(); + potentiallyLoadFullSize(); cropAndScale(); } QPoint Viewer::ImageDisplay::mapPos( QPoint p ) { QPoint off = offset( qAbs( m_zEnd.x()-m_zStart.x() ), qAbs( m_zEnd.y()-m_zStart.y() ), width(), height(), 0 ); p -= off; int x = (int) (m_zStart.x() + (m_zEnd.x()-m_zStart.x())*((double)p.x()/ (width()-2*off.x()))); int y = (int) (m_zStart.y() + (m_zEnd.y()-m_zStart.y())*((double)p.y()/ (height()-2*off.y()))); return QPoint( x, y ); } void Viewer::ImageDisplay::xformPainter( QPainter* p ) { QPoint off = offset( qAbs( m_zEnd.x()-m_zStart.x() ), qAbs( m_zEnd.y()-m_zStart.y() ), width(), height(), 0 ); double s = (width()-2*off.x())/qAbs( (double)m_zEnd.x()-m_zStart.x()); p->scale( s, s ); p->translate( -m_zStart.x(), -m_zStart.y() ); } void Viewer::ImageDisplay::zoomIn() { + qCDebug(ViewerLog, "zoomIn()"); QPoint size = (m_zEnd-m_zStart); QPoint p1 = m_zStart + size*(0.2/2); QPoint p2 = m_zEnd - size*(0.2/2); zoom(p1, p2); } void Viewer::ImageDisplay::zoomOut() { + qCDebug(ViewerLog, "zoomOut()"); QPoint size = (m_zEnd-m_zStart); //Bug 150971, Qt tries to render bigger and bigger images (10000x10000), hence running out of memory. if ( ( size.x() * size.y() > 25*1024*1024 ) ) return; QPoint p1 = m_zStart - size*(0.25/2); QPoint p2 = m_zEnd + size*(0.25/2); zoom(p1,p2); } void Viewer::ImageDisplay::zoomFull() { + qCDebug(ViewerLog, "zoomFull()"); m_zStart = QPoint(0,0); m_zEnd = QPoint( m_loadedImage.width(), m_loadedImage.height() ); zoom( QPoint(0,0), QPoint( m_loadedImage.width(), m_loadedImage.height() ) ); } void Viewer::ImageDisplay::normalize( QPoint& p1, QPoint& p2 ) { int minx = qMin( p1.x(), p2.x() ); int miny = qMin( p1.y(), p2.y() ); int maxx = qMax( p1.x(), p2.x() ); int maxy = qMax( p1.y(), p2.y() ); p1 = QPoint( minx, miny ); p2 = QPoint( maxx, maxy ); } void Viewer::ImageDisplay::pan( const QPoint& point ) { m_zStart += point; m_zEnd += point; cropAndScale(); } void Viewer::ImageDisplay::cropAndScale() { if ( m_loadedImage.isNull() ) { return; } if ( m_zStart != QPoint(0,0) || m_zEnd != QPoint( m_loadedImage.width(), m_loadedImage.height() ) ) { + qCDebug(ViewerLog) << "cropAndScale(): using cropped image" << m_zStart << "-" << m_zEnd; m_croppedAndScaledImg = m_loadedImage.copy( m_zStart.x(), m_zStart.y(), m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y() ); } else + { + qCDebug(ViewerLog) << "cropAndScale(): using full image."; m_croppedAndScaledImg = m_loadedImage; + } updateZoomCaption(); if ( !m_croppedAndScaledImg.isNull() ) // I don't know how this can happen, but it seems not to be dangerous. + { + qCDebug(ViewerLog) << "cropAndScale(): scaling image to" << width() <<"x"<< height(); m_croppedAndScaledImg = m_croppedAndScaledImg.scaled( width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + } else { + qCDebug(ViewerLog) << "cropAndScale(): image is null."; + } update(); emit viewGeometryChanged(m_croppedAndScaledImg.size(), QRect(m_zStart, m_zEnd), sizeRatio(m_loadedImage.size(), m_info->size())); } void Viewer::ImageDisplay::filterNone() { cropAndScale(); update(); } bool Viewer::ImageDisplay::filterMono() { m_croppedAndScaledImg = m_croppedAndScaledImg.convertToFormat(m_croppedAndScaledImg.Format_Mono); update(); return true; } // I can't believe there isn't a standard conversion for this??? -- WH bool Viewer::ImageDisplay::filterBW() { if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error( this, i18n("Insufficient color depth for this filter")); return false; } for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int gray = qGray(pixel); int alpha = qAlpha(pixel); m_croppedAndScaledImg.setPixel(x, y, qRgba(gray, gray, gray, alpha)); } } update(); return true; } bool Viewer::ImageDisplay::filterContrastStretch() { int redMin, redMax, greenMin, greenMax, blueMin, blueMax; redMin = greenMin = blueMin = 255; redMax = greenMax = blueMax = 0; if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error( this, i18n("Insufficient color depth for this filter")); return false; } // Look for minimum and maximum intensities within each color channel for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int red = qRed(pixel); int green = qGreen(pixel); int blue = qBlue(pixel); redMin = redMin < red ? redMin : red; redMax = redMax > red ? redMax : red; greenMin = greenMin < green ? greenMin : green; greenMax = greenMax > green ? greenMax : green; blueMin = blueMin < blue ? blueMin : blue; blueMax = blueMax > blue ? blueMax : blue; } } // Calculate factor for stretching each color intensity throughout the // whole range float redFactor, greenFactor, blueFactor; redFactor = ((float)(255) / (float) (redMax - redMin)); greenFactor = ((float)(255) / (float) (greenMax - greenMin)); blueFactor = ((float)(255) / (float) (blueMax - blueMin)); // Perform the contrast stretching for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int red = qRed(pixel); int green = qGreen(pixel); int blue = qBlue(pixel); int alpha = qAlpha(pixel); red = (red - redMin) * redFactor; red = red < 255 ? red : 255; red = red > 0 ? red : 0; green = (green - greenMin) * greenFactor; green = green < 255 ? green : 255; green = green > 0 ? green : 0; blue = (blue - blueMin) * blueFactor; blue = blue < 255 ? blue : 255; blue = blue > 0 ? blue : 0; m_croppedAndScaledImg.setPixel(x, y, qRgba(red, green, blue, alpha)); } } update(); return true; } bool Viewer::ImageDisplay::filterHistogramEqualization() { int width, height; float R_histogram[256]; float G_histogram[256]; float B_histogram[256]; float d; if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error( this, i18n("Insufficient color depth for this filter")); return false; } memset(R_histogram, 0, sizeof(R_histogram)); memset(G_histogram, 0, sizeof(G_histogram)); memset(B_histogram, 0, sizeof(B_histogram)); width = m_croppedAndScaledImg.width(); height = m_croppedAndScaledImg.height(); d = 1.0 / width / height; // Populate histogram for each color channel for (int y = 0; y < height; ++y) { for (int x = 1; x < width; ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); R_histogram[qRed(pixel)] += d; G_histogram[qGreen(pixel)] += d; B_histogram[qBlue(pixel)] += d; } } // Transfer histogram table to cumulative distribution table float R_sum = 0.0; float G_sum = 0.0; float B_sum = 0.0; for (int i = 0; i < 256; ++i) { R_sum += R_histogram[i]; G_sum += G_histogram[i]; B_sum += B_histogram[i]; R_histogram[i] = R_sum * 255 + 0.5; G_histogram[i] = G_sum * 255 + 0.5; B_histogram[i] = B_sum * 255 + 0.5; } // Equalize the image for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); m_croppedAndScaledImg.setPixel( x, y, qRgba(R_histogram[qRed(pixel)], G_histogram[qGreen(pixel)], B_histogram[qBlue(pixel)], qAlpha(pixel)) ); } } update(); return true; } void Viewer::ImageDisplay::updateZoomCaption() { const QSize imgSize = m_loadedImage.size(); // similar to sizeRatio(), but we take the _highest_ factor. double ratio = ((double)imgSize.width())/(m_zEnd.x()-m_zStart.x()); if ( ratio * (m_zEnd.y()-m_zStart.y()) < imgSize.height() ) { ratio = ((double)imgSize.height())/(m_zEnd.y()-m_zStart.y()); } emit setCaptionInfo((ratio > 1.05) ? ki18n("[ zoom x%1 ]").subs(ratio, 0, 'f', 1).toString() : QString()); } QImage Viewer::ImageDisplay::currentViewAsThumbnail() const { if ( m_croppedAndScaledImg.isNull() ) return QImage(); else return m_croppedAndScaledImg.scaled( 512, 512, Qt::KeepAspectRatio, Qt::SmoothTransformation ); } bool Viewer::ImageDisplay::isImageZoomed( const Settings::StandardViewSize type, const QSize& imgSize ) { if (type == Settings::FullSize) return true; if ( type == Settings::NaturalSizeIfFits ) return !(imgSize.width() < width() && imgSize.height() < height() ); return false; } void Viewer::ImageDisplay::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& image) { const DB::FileName fileName = request->databaseFileName(); const QSize imgSize = request->size(); const QSize fullSize = request->fullSize(); const int angle = request->angle(); const bool loadedOK = request->loadedOK(); if ( loadedOK && fileName == m_info->fileName() ) { if ( fullSize.isValid() && !m_info->size().isValid() ) m_info->setSize( fullSize ); if ( !m_reloadImageInProgress ) updateZoomPoints( Settings::SettingsData::instance()->viewerStandardSize(), image.size() ); else { // See documentation for zoomPixelForPixel for details. - // We just loaded a likel much larger image, so the zoom points - // need to be scaled. Notice _loadedImage is the size of the + // We just loaded a likely much larger image, so the zoom points + // need to be scaled. Notice m_loadedImage is the size of the // old image. - double ratio = sizeRatio( m_loadedImage.size(), m_info->size() ); + // when using raw images, the decoded image may be a preview + // and have a size different from m_info->size(). Therefore, use fullSize here: + double ratio = sizeRatio( m_loadedImage.size(), fullSize ); + qCDebug(ViewerLog) << "Old size:" << m_loadedImage.size() << "; new size:" << m_info->size(); + qCDebug(ViewerLog) << "Req size:" << imgSize << "fullsize:" << fullSize; + qCDebug(ViewerLog) << "pixmapLoaded(): Zoom region was" << m_zStart <<"-"<viewerCacheSize() * 1024LL * 1024LL ) / (width()*height()*4)); bool cacheFull = (m_cache.count() > cacheSize); int incr = ( m_forward ? 1 : -1 ); int nextOnesInCache = 0; // Iterate from the current image in the direction of the viewing for ( int i = m_curIndex+incr; cacheSize ; i += incr ) { if ( m_forward ? ( i >= (int) m_imageList.count() ) : (i < 0) ) break; DB::ImageInfoPtr info = DB::ImageDB::instance()->info(m_imageList[i]); if ( !info ) { qCWarning(ViewerLog, "Info was null for index %d!", i); return; } if ( m_cache.contains(i) ) { nextOnesInCache++; if ( nextOnesInCache >= ceil(cacheSize/2.0) && cacheFull ) { // Ok enough images in cache return; } } else { requestImage( info ); if ( cacheFull ) { // The cache was full, we need to delete an item from the cache. // First try to find an item from the direction we came from for ( int j = ( m_forward ? 0 : m_imageList.count() -1 ); j != m_curIndex; j += ( m_forward ? 1 : -1 ) ) { if ( m_cache.contains(j) ) { m_cache.remove(j); return; } } // OK We found no item in the direction we came from (think of home/end keys) for ( int j = ( m_forward ? m_imageList.count() -1 : 0 ); j != m_curIndex; j += ( m_forward ? -1 : 1 ) ) { if ( m_cache.contains(j) ) { m_cache.remove(j); return; } } Q_ASSERT( false ); // We should never get here. } return; } } } int Viewer::ImageDisplay::indexOf( const DB::FileName& fileName ) { int i = 0; Q_FOREACH( const DB::FileName &name, m_imageList ) { if ( name == fileName ) break; ++i; } return i; } void Viewer::ImageDisplay::busy() { if ( !m_busy ) qApp->setOverrideCursor( Qt::WaitCursor ); m_busy = true; } void Viewer::ImageDisplay::unbusy() { if ( m_busy ) qApp->restoreOverrideCursor(); m_busy = false; } void Viewer::ImageDisplay::zoomPixelForPixel() { + qCDebug(ViewerLog, "zoomPixelForPixel()"); // This is rather tricky. // We want to zoom to a pixel level for the real image, which we might // or might not have loaded yet. // // First we ask for zoom points as they would look like had we had the // real image loaded now. (We need to ask for them, for the real image, // otherwise we would just zoom to the pixel level of the view size // image) updateZoomPoints( Settings::NaturalSize, m_info->size() ); // The points now, however might not match the current visible image - // as this image might be be only view size large. We therefore need // to scale the coordinates. double ratio = sizeRatio( m_loadedImage.size(), m_info->size() ); + qCDebug(ViewerLog) << "zoomPixelForPixel(): Zoom region was" << m_zStart <<"-"<size() != m_loadedImage.size() ) { + qCDebug(ViewerLog) << "Loading full size image for " << m_info->fileName().relative(); ImageManager::ImageRequest* request = new ImageManager::ImageRequest( m_info->fileName(), QSize(-1,-1), m_info->angle(), this ); request->setPriority( ImageManager::Viewer ); ImageManager::AsyncLoader::instance()->load( request ); busy(); m_reloadImageInProgress = true; } } /** * return the ratio of the two sizes. That is newSize/baseSize. */ double Viewer::ImageDisplay::sizeRatio( const QSize& baseSize, const QSize& newSize ) const { double res = ((double)newSize.width())/baseSize.width(); if ( res * baseSize.height() > newSize.height() ) { res = ((double)newSize.height())/baseSize.height(); } return res; } void Viewer::ImageDisplay::requestImage( const DB::ImageInfoPtr& info, bool priority ) { Settings::StandardViewSize viewSize = Settings::SettingsData::instance()->viewerStandardSize(); QSize s = size(); if ( viewSize == Settings::NaturalSize ) s = QSize(-1,-1); ImageManager::ImageRequest* request = new ImageManager::ImageRequest( info->fileName(), s, info->angle(), this ); request->setUpScale( viewSize == Settings::FullSize ); request->setPriority( priority ? ImageManager::Viewer : ImageManager::ViewerPreload ); ImageManager::AsyncLoader::instance()->load( request ); } void Viewer::ImageDisplay::hideEvent(QHideEvent *) { m_viewHandler->hideEvent(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ImageDisplay.h b/Viewer/ImageDisplay.h index dbd2c666..bb9a4f79 100644 --- a/Viewer/ImageDisplay.h +++ b/Viewer/ImageDisplay.h @@ -1,146 +1,145 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef IMAGEDISPLAY_H #define IMAGEDISPLAY_H #include #include #include #include #include "ImageManager/ImageClientInterface.h" #include #include "DB/ImageInfoPtr.h" #include "AbstractDisplay.h" #include "Settings/SettingsData.h" #include class QTimer; namespace DB { class ImageInfo; } namespace Viewer { class ViewHandler; class ViewerWidget; struct ViewPreloadInfo { ViewPreloadInfo() {} ViewPreloadInfo( const QImage& img, const QSize& size, int angle ) : img(img), size(size), angle(angle) {} QImage img; QSize size; int angle; }; class ImageDisplay :public Viewer::AbstractDisplay, public ImageManager::ImageClientInterface { Q_OBJECT public: explicit ImageDisplay( QWidget* parent ); bool setImage( DB::ImageInfoPtr info, bool forward ) override; QImage currentViewAsThumbnail() const; void pixmapLoaded(ImageManager::ImageRequest* request, const QImage& image) override; void setImageList( const DB::FileNameList& list ); void filterNone(); void filterSelected(); bool filterMono(); bool filterBW(); bool filterContrastStretch(); bool filterHistogramEqualization(); public slots: void zoomIn() override; void zoomOut() override; void zoomFull() override; void zoomPixelForPixel() override; protected slots: void hideCursor(); void showCursor(); void disableCursorHiding(); void enableCursorHiding(); signals: void possibleChange(); void imageReady(); void setCaptionInfo(const QString& info); void viewGeometryChanged(QSize viewSize, QRect zoomWindow, double sizeRatio); protected: virtual void mousePressEvent( QMouseEvent* event ) override; virtual void mouseMoveEvent( QMouseEvent* event ) override; virtual void mouseReleaseEvent( QMouseEvent* event ) override; virtual void resizeEvent( QResizeEvent* event ) override; virtual void paintEvent( QPaintEvent* event ) override; void hideEvent(QHideEvent* ) override; QPoint mapPos( QPoint ); QPoint offset( int logicalWidth, int logicalHeight, int physicalWidth, int physicalHeight, double* ratio ); void xformPainter( QPainter* ); void cropAndScale(); void updatePreload(); int indexOf( const DB::FileName& fileName ); void requestImage( const DB::ImageInfoPtr& info, bool priority = false ); /** display zoom factor in title of display window */ void updateZoomCaption(); friend class ViewHandler; void zoom( QPoint p1, QPoint p2 ); void normalize( QPoint& p1, QPoint& p2 ); void pan( const QPoint& ); - void retryZoom(); void busy(); void unbusy(); bool isImageZoomed( const Settings::StandardViewSize type, const QSize& imgSize ); void updateZoomPoints( const Settings::StandardViewSize type, const QSize& imgSize ); - void potentialyLoadFullSize(); + void potentiallyLoadFullSize(); double sizeRatio( const QSize& baseSize, const QSize& newSize ) const; private: QImage m_loadedImage; QImage m_croppedAndScaledImg; ViewHandler* m_viewHandler; // zoom points in the coordinate system of the image. QPoint m_zStart; QPoint m_zEnd; QMap m_cache; DB::FileNameList m_imageList; QMap m_loadMap; bool m_reloadImageInProgress; int m_forward; int m_curIndex; bool m_busy; ViewerWidget* m_viewer; QTimer* m_cursorTimer; bool m_cursorHiding; }; } #endif /* IMAGEDISPLAY_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/SpeedDisplay.cpp b/Viewer/SpeedDisplay.cpp index 5853ae48..64754c5c 100644 --- a/Viewer/SpeedDisplay.cpp +++ b/Viewer/SpeedDisplay.cpp @@ -1,91 +1,95 @@ /* 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 "SpeedDisplay.h" #include #include #include #include #include Viewer::SpeedDisplay::SpeedDisplay( QWidget* parent ) :QLabel( parent ) { m_timeLine = new QTimeLine( 1000, this); connect( m_timeLine, SIGNAL(frameChanged(int)), this, SLOT(setAlphaChannel(int)) ); m_timeLine->setFrameRange( 0, 170 ); m_timeLine->setDirection( QTimeLine::Backward ); m_timer = new QTimer( this ); m_timer->setSingleShot(true); connect(m_timer, &QTimer::timeout, m_timeLine, &QTimeLine::start); setAutoFillBackground(true); } void Viewer::SpeedDisplay::display( int i ) { + // FIXME(jzarl): if the user sets a different shortcut, this is inaccurate + // -> dynamically update this text setText( i18nc("OSD for slideshow, num of seconds per image","

%1 s

", i/1000.0 ) ); go(); } void Viewer::SpeedDisplay::start( ) { + // FIXME(jzarl): if the user sets a different shortcut, this is inaccurate + // -> dynamically update this text setText( i18nc("OSD for slideshow","

Starting Slideshow
Ctrl++ makes the slideshow faster
Ctrl + - makes the slideshow slower

")); go(); } void Viewer::SpeedDisplay::go() { resize( sizeHint() ); QWidget* p = static_cast( parent() ); move( ( p->width() - width() )/2, ( p->height() - height() )/2 ); setAlphaChannel( 170, 255 ); m_timer->start( 1000 ); m_timeLine->stop(); show(); raise(); } void Viewer::SpeedDisplay::end() { setText( i18nc("OSD for slideshow","

Ending Slideshow

") ); go(); } void Viewer::SpeedDisplay::setAlphaChannel(int background, int label) { QPalette p = palette(); p.setColor(QPalette::Background, QColor(0,0,0,background)); // r,g,b,A p.setColor(QPalette::WindowText, QColor(255,255,255,label) ); setPalette(p); } void Viewer::SpeedDisplay::setAlphaChannel(int alpha) { setAlphaChannel(alpha,alpha); if ( alpha == 0 ) hide(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewerWidget.cpp b/Viewer/ViewerWidget.cpp index 5d7027fa..aef28b7a 100644 --- a/Viewer/ViewerWidget.cpp +++ b/Viewer/ViewerWidget.cpp @@ -1,1532 +1,1460 @@ /* 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 #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_actions->setDefaultShortcut(action, Qt::CTRL+Qt::Key_1 ); m_popup->addAction( action ); m_setStackHead = m_actions->addAction( QString::fromLatin1("viewer-set-stack-head"), this, SLOT(slotSetStackHead()) ); m_setStackHead->setText( i18nc("@action:inmenu","Set as First Image in Stack") ); - m_setStackHead->setShortcut( Qt::CTRL+Qt::Key_4 ); + m_actions->setDefaultShortcut(m_setStackHead, Qt::CTRL+Qt::Key_4 ); m_popup->addAction( m_setStackHead ); m_showExifViewer = m_actions->addAction( QString::fromLatin1("viewer-show-exif-viewer"), this, SLOT(showExifViewer()) ); m_showExifViewer->setText( i18nc("@action:inmenu","Show Exif Viewer") ); m_popup->addAction( m_showExifViewer ); m_copyTo = m_actions->addAction( QString::fromLatin1("viewer-copy-to"), this, SLOT(copyTo()) ); m_copyTo->setText( i18nc("@action:inmenu","Copy Image to...") ); - m_copyTo->setShortcut( Qt::Key_F7 ); + m_actions->setDefaultShortcut(m_copyTo, Qt::Key_F7 ); m_popup->addAction( m_copyTo ); if ( m_type == ViewerWindow ) { action = m_actions->addAction( QString::fromLatin1("viewer-close"), this, SLOT(close()) ); action->setText( i18nc("@action:inmenu","Close") ); action->setShortcut( Qt::Key_Escape ); + m_actions->setShortcutsConfigurable(action, false); } m_popup->addAction( action ); m_actions->readSettings(); 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_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-rotate180"), this, SLOT(rotate180()) ); action->setText( i18nc("@action:inmenu","Flip Over") ); action->setShortcut( Qt::Key_8 ); + m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-rotare270"), this, SLOT(rotate270()) ); // ^ this is a typo, isn't it?! action->setText( i18nc("@action:inmenu","Rotate counterclockwise") ); action->setShortcut( Qt::Key_7 ); + m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction( action ); m_popup->addMenu( m_rotateMenu ); } void Viewer::ViewerWidget::createSkipMenu() { QMenu *popup = new QMenu( m_popup ); popup->setTitle( i18nc("@title:inmenu As in 'skip 2 images'","Skip") ); QAction * action = m_actions->addAction( QString::fromLatin1("viewer-home"), this, SLOT(showFirst()) ); action->setText( i18nc("@action:inmenu Go to first image","First") ); action->setShortcut( Qt::Key_Home ); + m_actions->setShortcutsConfigurable(action, false); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-end"), this, SLOT(showLast()) ); action->setText( i18nc("@action:inmenu Go to last image","Last") ); action->setShortcut( Qt::Key_End ); + m_actions->setShortcutsConfigurable(action, false); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next"), this, SLOT(showNext()) ); action->setText( i18nc("@action:inmenu","Show Next") ); action->setShortcuts(QList() << Qt::Key_PageDown << Qt::Key_Space); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next-10"), this, SLOT(showNext10()) ); action->setText( i18nc("@action:inmenu","Skip 10 Forward") ); - action->setShortcut( Qt::CTRL+Qt::Key_PageDown ); + m_actions->setDefaultShortcut(action, Qt::CTRL+Qt::Key_PageDown ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next-100"), this, SLOT(showNext100()) ); action->setText( i18nc("@action:inmenu","Skip 100 Forward") ); - action->setShortcut( Qt::SHIFT+Qt::Key_PageDown ); + m_actions->setDefaultShortcut(action, Qt::SHIFT+Qt::Key_PageDown ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next-1000"), this, SLOT(showNext1000()) ); action->setText( i18nc("@action:inmenu","Skip 1000 Forward") ); - action->setShortcut( Qt::CTRL+Qt::SHIFT+Qt::Key_PageDown ); + m_actions->setDefaultShortcut(action, Qt::CTRL+Qt::SHIFT+Qt::Key_PageDown ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev"), this, SLOT(showPrev()) ); action->setText( i18nc("@action:inmenu","Show Previous") ); action->setShortcuts(QList() << Qt::Key_PageUp << Qt::Key_Backspace); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev-10"), this, SLOT(showPrev10()) ); action->setText( i18nc("@action:inmenu","Skip 10 Backward") ); - action->setShortcut( Qt::CTRL+Qt::Key_PageUp ); + m_actions->setDefaultShortcut(action, Qt::CTRL+Qt::Key_PageUp ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev-100"), this, SLOT(showPrev100()) ); action->setText( i18nc("@action:inmenu","Skip 100 Backward") ); - action->setShortcut( Qt::SHIFT+Qt::Key_PageUp ); + m_actions->setDefaultShortcut(action, Qt::SHIFT+Qt::Key_PageUp ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev-1000"), this, SLOT(showPrev1000()) ); action->setText( i18nc("@action:inmenu","Skip 1000 Backward") ); - action->setShortcut( Qt::CTRL+Qt::SHIFT+Qt::Key_PageUp ); + m_actions->setDefaultShortcut(action, Qt::CTRL+Qt::SHIFT+Qt::Key_PageUp ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-delete-current"), this, SLOT(deleteCurrent()) ); action->setText( i18nc("@action:inmenu","Delete Image") ); - action->setShortcut( Qt::CTRL + Qt::Key_Delete ); + m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-remove-current"), this, SLOT(removeCurrent()) ); action->setText( i18nc("@action:inmenu","Remove Image from Display List") ); action->setShortcut( Qt::Key_Delete ); + m_actions->setShortcutsConfigurable(action, false); popup->addAction( action ); m_popup->addMenu( popup ); } void Viewer::ViewerWidget::createZoomMenu() { QMenu *popup = new QMenu( m_popup ); popup->setTitle( i18nc("@action:inmenu","Zoom") ); // PENDING(blackie) Only for image display? QAction * action = m_actions->addAction( QString::fromLatin1("viewer-zoom-in"), this, SLOT(zoomIn()) ); action->setText( i18nc("@action:inmenu","Zoom In") ); action->setShortcut( Qt::Key_Plus ); + m_actions->setShortcutsConfigurable(action, false); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-zoom-out"), this, SLOT(zoomOut()) ); action->setText( i18nc("@action:inmenu","Zoom Out") ); action->setShortcut( Qt::Key_Minus ); + m_actions->setShortcutsConfigurable(action, false); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-zoom-full"), this, SLOT(zoomFull()) ); action->setText( i18nc("@action:inmenu","Full View") ); action->setShortcut( Qt::Key_Period ); + m_actions->setShortcutsConfigurable(action, false); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-zoom-pixel"), this, SLOT(zoomPixelForPixel()) ); action->setText( i18nc("@action:inmenu","Pixel for Pixel View") ); action->setShortcut( Qt::Key_Equal ); + m_actions->setShortcutsConfigurable(action, false); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-toggle-fullscreen"), this, SLOT(toggleFullScreen()) ); action->setText( i18nc("@action:inmenu","Toggle Full Screen") ); action->setShortcuts(QList() << Qt::Key_F11 << Qt::Key_Return); popup->addAction( action ); m_popup->addMenu( popup ); } void Viewer::ViewerWidget::createSlideShowMenu() { QMenu *popup = new QMenu( m_popup ); popup->setTitle( i18nc("@title:inmenu","Slideshow") ); m_startStopSlideShow = m_actions->addAction( QString::fromLatin1("viewer-start-stop-slideshow"), this, SLOT(slotStartStopSlideShow()) ); m_startStopSlideShow->setText( i18nc("@action:inmenu","Run Slideshow") ); - m_startStopSlideShow->setShortcut( Qt::CTRL+Qt::Key_R ); + m_actions->setDefaultShortcut(m_startStopSlideShow, Qt::CTRL+Qt::Key_R ); popup->addAction( m_startStopSlideShow ); m_slideShowRunFaster = m_actions->addAction( QString::fromLatin1("viewer-run-faster"), this, SLOT(slotSlideShowFaster()) ); m_slideShowRunFaster->setText( i18nc("@action:inmenu","Run Faster") ); - m_slideShowRunFaster->setShortcut( Qt::CTRL + Qt::Key_Plus ); // if you change this, please update the info in Viewer::SpeedDisplay + m_actions->setDefaultShortcut(m_slideShowRunFaster, Qt::CTRL + Qt::Key_Plus ); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction( m_slideShowRunFaster ); m_slideShowRunSlower = m_actions->addAction( QString::fromLatin1("viewer-run-slower"), this, SLOT(slotSlideShowSlower()) ); m_slideShowRunSlower->setText( i18nc("@action:inmenu","Run Slower") ); - m_slideShowRunSlower->setShortcut( Qt::CTRL+Qt::Key_Minus ); // if you change this, please update the info in Viewer::SpeedDisplay + m_actions->setDefaultShortcut(m_slideShowRunSlower, Qt::CTRL+Qt::Key_Minus ); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction( m_slideShowRunSlower ); m_popup->addMenu( popup ); } void Viewer::ViewerWidget::load( const DB::FileNameList& list, int index ) { m_list = list; m_imageDisplay->setImageList( list ); m_current = index; load(); bool on = ( list.count() > 1 ); m_startStopSlideShow->setEnabled(on); m_slideShowRunFaster->setEnabled(on); m_slideShowRunSlower->setEnabled(on); } void Viewer::ViewerWidget::load() { const bool isReadable = QFileInfo( 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( i18nc("@title:window %1 is the filename, %2 its 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 ); + m_actions->setShortcutsConfigurable(seek, false); menu->addAction(seek); } QAction* sep = new QAction(m_popup); sep->setSeparator(true); m_popup->addAction( sep ); m_videoActions.append( sep ); m_stop = m_actions->addAction( QString::fromLatin1("viewer-video-stop"), m_videoDisplay, SLOT(stop()) ); m_stop->setText( i18nc("@action:inmenu Stop video playback","Stop") ); m_popup->addAction( m_stop ); m_videoActions.append(m_stop); m_playPause = m_actions->addAction( QString::fromLatin1("viewer-video-pause"), m_videoDisplay, SLOT(playPause()) ); // text set in contextMenuEvent() m_playPause->setShortcut( Qt::Key_P ); + m_actions->setShortcutsConfigurable(m_playPause, false); m_popup->addAction( m_playPause ); m_videoActions.append( m_playPause ); m_makeThumbnailImage = m_actions->addAction( QString::fromLatin1("make-thumbnail-image"), this, SLOT(makeThumbnailImage())); - m_makeThumbnailImage->setShortcut(Qt::ControlModifier + Qt::Key_S); + m_actions->setDefaultShortcut(m_makeThumbnailImage,Qt::ControlModifier + Qt::Key_S); m_makeThumbnailImage->setText( i18nc("@action:inmenu","Use current frame in thumbnail view") ); m_popup->addAction(m_makeThumbnailImage); m_videoActions.append(m_makeThumbnailImage); QAction * restart = m_actions->addAction( QString::fromLatin1("viewer-video-restart"), m_videoDisplay, SLOT(restart()) ); restart->setText( i18nc("@action:inmenu Restart video playback.","Restart") ); m_popup->addAction( restart ); m_videoActions.append( restart ); } void Viewer::ViewerWidget::createCategoryImageMenu() { m_categoryImagePopup = new MainWindow::CategoryImagePopup( m_popup ); m_popup->addMenu( m_categoryImagePopup ); connect(m_categoryImagePopup, &MainWindow::CategoryImagePopup::aboutToShow, this, &ViewerWidget::populateCategoryImagePopup); } void Viewer::ViewerWidget::createFilterMenu() { m_filterMenu = new QMenu( m_popup ); m_filterMenu->setTitle( i18nc("@title:inmenu","Filters") ); m_filterNone = m_actions->addAction( QString::fromLatin1("filter-empty"), this, SLOT(filterNone()) ); m_filterNone->setText( i18nc("@action:inmenu","Remove All Filters") ); m_filterMenu->addAction( m_filterNone ); m_filterBW = m_actions->addAction( QString::fromLatin1("filter-bw"), this, SLOT(filterBW()) ); m_filterBW->setText( i18nc("@action:inmenu","Apply Grayscale Filter") ); m_filterBW->setCheckable( true ); m_filterMenu->addAction( m_filterBW ); m_filterContrastStretch = m_actions->addAction( QString::fromLatin1("filter-cs"), this, SLOT(filterContrastStretch()) ); m_filterContrastStretch->setText( i18nc("@action:inmenu","Apply Contrast Stretching Filter") ); m_filterContrastStretch->setCheckable( true ); m_filterMenu->addAction( m_filterContrastStretch ); m_filterHistogramEqualization = m_actions->addAction( QString::fromLatin1("filter-he"), this, SLOT(filterHistogramEqualization()) ); m_filterHistogramEqualization->setText( i18nc("@action:inmenu","Apply Histogram Equalization Filter") ); m_filterHistogramEqualization->setCheckable( true ); m_filterMenu->addAction( m_filterHistogramEqualization ); m_filterMono = m_actions->addAction( QString::fromLatin1("filter-mono"), this, SLOT(filterMono()) ); m_filterMono->setText( i18nc("@action:inmenu","Apply Monochrome Filter") ); m_filterMono->setCheckable( true ); m_filterMenu->addAction( m_filterMono ); m_popup->addMenu( m_filterMenu ); } void Viewer::ViewerWidget::test() { #ifdef TESTING QTimeLine* timeline = new QTimeLine; timeline->setStartFrame( _infoBox->y() ); timeline->setEndFrame( height() ); connect(timeline, &QTimeLine::frameChanged, this, &ViewerWidget::moveInfoBox); timeline->start(); #endif // TESTING } void Viewer::ViewerWidget::moveInfoBox( int y) { m_infoBox->move( m_infoBox->x(), y ); } void Viewer::ViewerWidget::createVideoViewer() { m_videoDisplay = new VideoDisplay( this ); addWidget( m_videoDisplay ); connect(m_videoDisplay, &VideoDisplay::stopped, this, &ViewerWidget::videoStopped); } void Viewer::ViewerWidget::stopPlayback() { m_videoDisplay->stop(); } void Viewer::ViewerWidget::invalidateThumbnail() const { ImageManager::ThumbnailCache::instance()->removeThumbnail( 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...") ); // 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/ViewerWidget.h b/Viewer/ViewerWidget.h index db747437..d6573067 100644 --- a/Viewer/ViewerWidget.h +++ b/Viewer/ViewerWidget.h @@ -1,254 +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. */ #ifndef VIEWER_H #define VIEWER_H #include #include #include #include #include #include #include class KActionCollection; class QAction; class QContextMenuEvent; class QKeyEvent; class QMenu; class QResizeEvent; class QStackedWidget; class QWheelEvent; namespace DB { class ImageInfo; class Id; } namespace MainWindow { class ExternalPopup; class CategoryImagePopup; } namespace Exif { class InfoDialog; } namespace Viewer { class AbstractDisplay; class ImageDisplay; class InfoBox; class SpeedDisplay; class TextDisplay; class VideoDisplay; class VideoShooter; class ViewerWidget :public QStackedWidget { Q_OBJECT public: enum UsageType { InlineViewer, ViewerWindow }; ViewerWidget( UsageType type = ViewerWindow, QMap > *macroStore = nullptr); ~ViewerWidget(); static ViewerWidget* latest(); void load( const DB::FileNameList& list, int index = 0 ); void infoBoxMove(); bool showingFullScreen() const; void setShowFullScreen( bool on ); void show( bool slideShow ); KActionCollection* actions(); public slots: bool close(bool alsoDelete = false ); void updateInfoBox(); void test(); void moveInfoBox( int ); void stopPlayback(); void remapAreas(QSize viewSize, QRect zoomWindow, double sizeRatio); void copyTo(); signals: void soughtTo( const DB::FileName& id ); void imageRotated(const DB::FileName& id); protected: void contextMenuEvent ( QContextMenuEvent * e ) override; void resizeEvent( QResizeEvent* ) override; void keyPressEvent( QKeyEvent* ) override; void wheelEvent( QWheelEvent* event ) override; void moveInfoBox(); - void setAsWallpaper(int mode); void load(); void setupContextMenu(); void createShowContextMenu(); - void createWallPaperMenu(); void createInvokeExternalMenu(); void createRotateMenu(); void createSkipMenu(); void createZoomMenu(); void createSlideShowMenu(); void createVideoMenu(); void createCategoryImageMenu(); void createFilterMenu(); void changeSlideShowInterval( int delta ); void createVideoViewer(); void inhibitScreenSaver( bool inhibit ); DB::ImageInfoPtr currentInfo() const; friend class InfoBox; private: void showNextN(int); void showPrevN(int); int find_tag_in_list(const QStringList &list, QString &namefound); void invalidateThumbnail() const; enum RemoveAction { RemoveImageFromDatabase, OnlyRemoveFromViewer }; void removeOrDeleteCurrent( RemoveAction ); protected slots: void showNext(); void showNext10(); void showNext100(); void showNext1000(); void showPrev(); void showPrev10(); void showPrev100(); void showPrev1000(); void showFirst(); void showLast(); void deleteCurrent(); void removeCurrent(); void rotate90(); void rotate180(); void rotate270(); void toggleFullScreen(); void slotStartStopSlideShow(); void slotSlideShowNext(); void slotSlideShowNextFromTimer(); void slotSlideShowFaster(); void slotSlideShowSlower(); void editImage(); void filterNone(); void filterSelected(); void filterBW(); void filterContrastStretch(); void filterHistogramEqualization(); void filterMono(); void slotSetStackHead(); void updateCategoryConfig(); - void slotSetWallpaperC(); - void slotSetWallpaperT(); - void slotSetWallpaperCT(); - void slotSetWallpaperCM(); - void slotSetWallpaperTM(); - void slotSetWallpaperS(); - void slotSetWallpaperCAF(); void populateExternalPopup(); void populateCategoryImagePopup(); void videoStopped(); void showExifViewer(); void zoomIn(); void zoomOut(); void zoomFull(); void zoomPixelForPixel(); void makeThumbnailImage(); /** Set the current window title (filename) and add the given detail */ void setCaptionWithDetail( const QString& detail ); private: static ViewerWidget* s_latest; friend class VideoShooter; QList m_forwardActions; QList m_backwardActions; QAction * m_startStopSlideShow; QAction * m_slideShowRunFaster; QAction * m_slideShowRunSlower; QAction * m_setStackHead; QAction * m_filterNone; QAction * m_filterSelected; QAction * m_filterBW; QAction * m_filterContrastStretch; QAction * m_filterHistogramEqualization; QAction * m_filterMono; AbstractDisplay* m_display; ImageDisplay* m_imageDisplay; VideoDisplay* m_videoDisplay; TextDisplay* m_textDisplay; int m_screenSaverCookie; DB::FileNameList m_list; DB::FileNameList m_removed; int m_current; QRect m_textRect; QMenu* m_popup; QMenu* m_rotateMenu; - QMenu* m_wallpaperMenu; QMenu* m_filterMenu; MainWindow::ExternalPopup* m_externalPopup; MainWindow::CategoryImagePopup* m_categoryImagePopup; int m_width; int m_height; QPixmap m_pixmap; QAction * m_delete; QAction * m_showExifViewer; QPointer m_exifViewer; QAction * m_copyTo; InfoBox* m_infoBox; QImage m_currentImage; bool m_showingFullScreen; int m_slideShowPause; SpeedDisplay* m_speedDisplay; KActionCollection* m_actions; bool m_forward; QTimer* m_slideShowTimer; bool m_isRunningSlideShow; QList m_videoActions; QAction * m_stop; QAction * m_playPause; QAction * m_makeThumbnailImage; bool m_videoPlayerStoppedManually; UsageType m_type; enum InputMode { InACategory, AlwaysStartWithCategory }; InputMode m_currentInputMode; QString m_currentInput; QString m_currentCategory; QString m_currentInputList; QString m_lastFound; QString m_lastCategory; QMap >* m_inputMacros; QMap >* m_myInputMacros; void addTaggedAreas(); QString m_lastCopyToTarget; }; } #endif /* VIEWER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/VisibleOptionsMenu.cpp b/Viewer/VisibleOptionsMenu.cpp index e67a3f76..47bd0491 100644 --- a/Viewer/VisibleOptionsMenu.cpp +++ b/Viewer/VisibleOptionsMenu.cpp @@ -1,177 +1,175 @@ /* 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 ); + actions->setDefaultShortcut(m_showInfoBox, 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") ); 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/FileReader.cpp b/XMLDB/FileReader.cpp index 862f0a71..5df8f378 100644 --- a/XMLDB/FileReader.cpp +++ b/XMLDB/FileReader.cpp @@ -1,536 +1,560 @@ /* 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 ); + if ( m_db->md5Map()->containsFile(dbFileName)) + { + if (m_db->md5Map()->contains(info->MD5Sum())) + { + qCWarning(XMLDBLog) << "Merging duplicate entry for file" << dbFileName.relative(); + DB::ImageInfoPtr existingInfo = m_db->info(dbFileName); + existingInfo->merge(*info); + } else { + qCCritical(XMLDBLog).nospace() << "Conflicting information for file " << dbFileName.relative() + << ": duplicate entry with different MD5 sum! Bailing out..."; + KMessageBox::error( messageParent(), + i18n( "

Line %1, column %2: duplicate entry for file '%3' with different MD5 sum.

" + "

Manual repair required!

", + reader->lineNumber(), + reader->columnNumber(), + dbFileName.relative() + ), + i18n("Error in database file")); + exit(-1); + } + } + else + { + 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 " "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/XMLDB/FileWriter.cpp b/XMLDB/FileWriter.cpp index bcd63e62..b4cdc724 100644 --- a/XMLDB/FileWriter.cpp +++ b/XMLDB/FileWriter.cpp @@ -1,527 +1,500 @@ /* 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 "FileWriter.h" #include "CompressFileInfo.h" #include "Database.h" #include "ElementWriter.h" #include "Logging.h" #include "NumberedBackup.h" #include "XMLCategory.h" #include #include #include #include #include #include #include #include #include -// I've added this to provide anyone interested -// with a quick and easy means to benchmark performance differences -// between old and new save behaviour. -// Note: in Qt4, saving files was deterministic "by accident"; with Qt5, we have to work for it -// (mostly because QSet is randomized now) -// FIXME(ZaJ): this should be removed "soon" (at latest by KPA 5.2.0) -#define DETERMINISTIC_DBSAVE - -// print saving time: -//#define BENCHMARK_FILEWRITER - // // // // +++++++++++++++++++++++++++++++ REMEMBER ++++++++++++++++++++++++++++++++ // // // // // Update XMLDB::Database::fileVersion every time you update the file format! // // // // // // // // // (sorry for the noise, but it is really important :-) using Utilities::StringSet; void XMLDB::FileWriter::save( const QString& fileName, bool isAutoSave ) { setUseCompressedFileFormat( Settings::SettingsData::instance()->useCompressedIndexXML() ); if ( !isAutoSave ) NumberedBackup().makeNumberedBackup(); // prepare XML document for saving: m_db->m_categoryCollection.initIdMap(); QFile out(fileName + QString::fromLatin1(".tmp")); if ( !out.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry( messageParent(), i18n("

Could not save the image database to XML.

" "File %1 could not be opened because of the following error: %2" , out.fileName(), out.errorString() ) ); return; } QTime t; if (TimingLog().isDebugEnabled()) t.start(); QXmlStreamWriter writer(&out); writer.setAutoFormatting(true); writer.writeStartDocument(); { ElementWriter dummy(writer, QString::fromLatin1("KPhotoAlbum")); writer.writeAttribute( QString::fromLatin1( "version" ), QString::number(Database::fileVersion())); writer.writeAttribute( QString::fromLatin1( "compressed" ), QString::number(useCompressedFileFormat())); saveCategories( writer ); saveImages( writer ); saveBlockList( writer ); saveMemberGroups( writer ); //saveSettings(writer); } writer.writeEndDocument(); qCDebug(TimingLog) << "XMLDB::FileWriter::save(): Saving took" << t.elapsed() <<"ms"; // State: index.xml has previous DB version, index.xml.tmp has the current version. // original file can be safely deleted if ( ( ! QFile::remove( fileName ) ) && QFile::exists( fileName ) ) { KMessageBox::sorry( messageParent(), i18n("

Failed to remove old version of image database.

" "

Please try again or replace the file %1 with file %2 manually!

", fileName, out.fileName() ) ); return; } // State: index.xml doesn't exist, index.xml.tmp has the current version. if ( ! out.rename( fileName ) ) { KMessageBox::sorry( messageParent(), i18n("

Failed to move temporary XML file to permanent location.

" "

Please try again or rename file %1 to %2 manually!

", out.fileName(), fileName ) ); // State: index.xml.tmp has the current version. return; } // State: index.xml has the current version. } void XMLDB::FileWriter::saveCategories( QXmlStreamWriter& writer ) { QStringList categories = DB::ImageDB::instance()->categoryCollection()->categoryNames(); ElementWriter dummy(writer, QString::fromLatin1("Categories") ); DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial( DB::Category::TokensCategory ); for (QString name : categories) { DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(name); if (! shouldSaveCategory(name)) { continue; } ElementWriter dummy(writer, QString::fromUtf8("Category")); writer.writeAttribute(QString::fromUtf8("name"), name); writer.writeAttribute(QString::fromUtf8("icon"), category->iconName()); writer.writeAttribute(QString::fromUtf8("show"), QString::number(category->doShow())); writer.writeAttribute(QString::fromUtf8("viewtype"), QString::number(category->viewType())); writer.writeAttribute(QString::fromUtf8("thumbnailsize"), QString::number(category->thumbnailSize())); writer.writeAttribute(QString::fromUtf8("positionable"), QString::number(category->positionable())); if (category == tokensCategory) { writer.writeAttribute(QString::fromUtf8("meta"),QString::fromUtf8("tokens")); } // FIXME (l3u): // Correct me if I'm wrong, but we don't need this, as the tags used as groups are // added to the respective category anyway when they're created, so there's no need to // re-add them here. Apart from this, adding an empty group (one without members) does // add an empty tag ("") doing so. /* QStringList list = Utilities::mergeListsUniqly(category->items(), m_db->_members.groups(name)); */ Q_FOREACH(const QString &tagName, category->items()) { ElementWriter dummy( writer, QString::fromLatin1("value") ); writer.writeAttribute( QString::fromLatin1("value"), tagName ); writer.writeAttribute( QString::fromLatin1( "id" ), QString::number(static_cast( category.data() )->idForName( tagName ) )); QDate birthDate = category->birthDate(tagName); if (!birthDate.isNull()) writer.writeAttribute( QString::fromUtf8("birthDate"), birthDate.toString(Qt::ISODate) ); } } } void XMLDB::FileWriter::saveImages( QXmlStreamWriter& writer ) { DB::ImageInfoList list = m_db->m_images; // Copy files from clipboard to end of overview, so we don't loose them Q_FOREACH(const DB::ImageInfoPtr &infoPtr, m_db->m_clipboard) { list.append(infoPtr); } { ElementWriter dummy(writer, QString::fromLatin1( "images" ) ); Q_FOREACH(const DB::ImageInfoPtr &infoPtr, list) { save( writer, infoPtr ); } } } void XMLDB::FileWriter::saveBlockList( QXmlStreamWriter& writer ) { ElementWriter dummy( writer, QString::fromLatin1( "blocklist" ) ); -#ifdef DETERMINISTIC_DBSAVE QList blockList = m_db->m_blockList.toList(); // sort blocklist to get diffable files std::sort(blockList.begin(), blockList.end()); -#else - QSet blockList = m_db->m_blockList; -#endif Q_FOREACH(const DB::FileName &block, blockList) { ElementWriter dummy( writer, QString::fromLatin1( "block" ) ); writer.writeAttribute( QString::fromLatin1( "file" ), block.relative() ); } } void XMLDB::FileWriter::saveMemberGroups( QXmlStreamWriter& writer ) { if ( m_db->m_members.isEmpty() ) return; ElementWriter dummy( writer, QString::fromLatin1( "member-groups" ) ); for( QMap< QString,QMap >::ConstIterator memberMapIt= m_db->m_members.memberMap().constBegin(); memberMapIt != m_db->m_members.memberMap().constEnd(); ++memberMapIt ) { const QString categoryName = memberMapIt.key(); // FIXME (l3u): This can happen when an empty sub-category (group) is present. // Would be fine to fix the reason why this happens in the first place. if (categoryName.isEmpty()) { continue; } if ( !shouldSaveCategory( categoryName ) ) continue; QMap groupMap = memberMapIt.value(); for( QMap::ConstIterator groupMapIt= groupMap.constBegin(); groupMapIt != groupMap.constEnd(); ++groupMapIt ) { // FIXME (l3u): This can happen when an empty sub-category (group) is present. // Would be fine to fix the reason why this happens in the first place. if (groupMapIt.key().isEmpty()) { continue; } if ( useCompressedFileFormat() ) { StringSet members = groupMapIt.value(); ElementWriter dummy( writer, QString::fromLatin1( "member" ) ); writer.writeAttribute( QString::fromLatin1( "category" ), categoryName ); writer.writeAttribute( QString::fromLatin1( "group-name" ), groupMapIt.key() ); QStringList idList; Q_FOREACH(const QString& member, members) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName( categoryName ); XMLCategory* category = static_cast( catPtr.data() ); if (category->idForName(member)==0) qCWarning(XMLDBLog) << "Member" << member << "in group" << categoryName << "->" << groupMapIt.key() << "has no id!"; idList.append( QString::number( category->idForName( member ) ) ); } -#ifdef DETERMINISTIC_DBSAVE std::sort(idList.begin(), idList.end()); -#endif writer.writeAttribute( QString::fromLatin1( "members" ), idList.join( QString::fromLatin1( "," ) ) ); } else { -#ifdef DETERMINISTIC_DBSAVE QStringList members = groupMapIt.value().toList(); std::sort(members.begin(), members.end()); -#else - QSet members = groupMapIt.value(); -#endif Q_FOREACH(const QString& member, members) { ElementWriter dummy( writer, QString::fromLatin1( "member" ) ); writer.writeAttribute( QString::fromLatin1( "category" ), memberMapIt.key() ); writer.writeAttribute( QString::fromLatin1( "group-name" ), groupMapIt.key() ); writer.writeAttribute( QString::fromLatin1( "member" ), member ); } // Add an entry even if the group is empty // (this is not necessary for the compressed format) if (members.size() == 0) { ElementWriter dummy(writer, QString::fromLatin1("member")); writer.writeAttribute(QString::fromLatin1("category"), memberMapIt.key()); writer.writeAttribute(QString::fromLatin1("group-name"), groupMapIt.key()); } } } } } /* Perhaps, we may need this later ;-) void XMLDB::FileWriter::saveSettings(QXmlStreamWriter& writer) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementWriter dummy(writer, settingsString); QMap settings; // For testing settings.insert(QString::fromUtf8("tokensCategory"), QString::fromUtf8("Tokens")); settings.insert(QString::fromUtf8("untaggedCategory"), QString::fromUtf8("Events")); settings.insert(QString::fromUtf8("untaggedTag"), QString::fromUtf8("untagged")); QMapIterator settingsIterator(settings); while (settingsIterator.hasNext()) { ElementWriter dummy(writer, settingString); settingsIterator.next(); writer.writeAttribute(keyString, escape(settingsIterator.key())); writer.writeAttribute(valueString, escape(settingsIterator.value())); } } */ void XMLDB::FileWriter::save( QXmlStreamWriter& writer, const DB::ImageInfoPtr& info ) { ElementWriter dummy( writer, QString::fromLatin1("image") ); writer.writeAttribute( QString::fromLatin1("file"), info->fileName().relative() ); if ( info->label() != QFileInfo(info->fileName().relative()).completeBaseName() ) writer.writeAttribute( QString::fromLatin1("label"), info->label() ); if ( !info->description().isEmpty() ) writer.writeAttribute( QString::fromLatin1("description"), info->description() ); DB::ImageDate date = info->date(); QDateTime start = date.start(); QDateTime end = date.end(); writer.writeAttribute( QString::fromLatin1( "startDate" ), start.toString(Qt::ISODate) ); if ( start != end ) writer.writeAttribute( QString::fromLatin1( "endDate" ), end.toString(Qt::ISODate) ); if ( info->angle() != 0 ) writer.writeAttribute( QString::fromLatin1("angle"), QString::number(info->angle())); writer.writeAttribute( QString::fromLatin1( "md5sum" ), info->MD5Sum().toHexString() ); writer.writeAttribute( QString::fromLatin1( "width" ), QString::number(info->size().width())); writer.writeAttribute( QString::fromLatin1( "height" ), QString::number(info->size().height())); if ( info->rating() != -1 ) { writer.writeAttribute( QString::fromLatin1("rating"), QString::number(info->rating())); } if ( info->stackId() ) { writer.writeAttribute( QString::fromLatin1("stackId"), QString::number(info->stackId())); writer.writeAttribute( QString::fromLatin1("stackOrder"), QString::number(info->stackOrder())); } if ( info->isVideo() ) writer.writeAttribute( QLatin1String("videoLength"), QString::number(info->videoLength())); if ( useCompressedFileFormat() ) writeCategoriesCompressed( writer, info ); else writeCategories( writer, info ); } QString XMLDB::FileWriter::areaToString(QRect area) const { QStringList areaString; areaString.append( QString::number(area.x()) ); areaString.append( QString::number(area.y()) ); areaString.append( QString::number(area.width()) ); areaString.append( QString::number(area.height()) ); return areaString.join( QString::fromLatin1(" ") ); } void XMLDB::FileWriter::writeCategories( QXmlStreamWriter& writer, const DB::ImageInfoPtr& info ) { ElementWriter topElm(writer, QString::fromLatin1("options"), false ); QStringList grps = info->availableCategories(); Q_FOREACH(const QString &name, grps) { if ( !shouldSaveCategory( name ) ) continue; ElementWriter categoryElm(writer, QString::fromLatin1("option"), false ); -#ifdef DETERMINISTIC_DBSAVE QStringList items = info->itemsOfCategory(name).toList(); std::sort(items.begin(), items.end()); -#else - QSet items = info->itemsOfCategory(name); -#endif if ( !items.isEmpty() ) { topElm.writeStartElement(); categoryElm.writeStartElement(); writer.writeAttribute( QString::fromLatin1("name"), name ); } Q_FOREACH(const QString& itemValue, items) { ElementWriter dummy( writer, QString::fromLatin1("value") ); writer.writeAttribute( QString::fromLatin1("value"), itemValue ); QRect area = info->areaForTag(name, itemValue); if ( ! area.isNull() ) { writer.writeAttribute(QString::fromLatin1("area"), areaToString(area)); } } } } void XMLDB::FileWriter::writeCategoriesCompressed( QXmlStreamWriter& writer, const DB::ImageInfoPtr& info ) { QMap>> positionedTags; QList categoryList = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH(const DB::CategoryPtr &category, categoryList) { QString categoryName = category->name(); if ( !shouldSaveCategory( categoryName ) ) continue; StringSet items = info->itemsOfCategory(categoryName); if ( !items.empty() ) { QStringList idList; Q_FOREACH(const QString &itemValue, items) { QRect area = info->areaForTag(categoryName, itemValue); if ( area.isValid() ) { // Positioned tags can't be stored in the "fast" format // so we have to handle them separately positionedTags[categoryName] << QPair(itemValue, area); } else { int id = static_cast(category.data())->idForName(itemValue); idList.append( QString::number( id ) ); } } // Possibly all ids of a category have area information, so only // write the category attribute if there are actually ids to write if ( !idList.isEmpty() ) { -#ifdef DETERMINISTIC_DBSAVE std::sort(idList.begin(), idList.end()); -#endif writer.writeAttribute( escape( categoryName ), idList.join( QString::fromLatin1( "," ) ) ); } } } // Add a "readable" sub-element for the positioned tags // FIXME: can this be merged with the code in writeCategories()? if ( ! positionedTags.isEmpty() ) { ElementWriter topElm( writer, QString::fromLatin1("options"), false ); topElm.writeStartElement(); QMapIterator>> categoryWithAreas(positionedTags); while (categoryWithAreas.hasNext()) { categoryWithAreas.next(); ElementWriter categoryElm( writer, QString::fromLatin1("option"), false ); categoryElm.writeStartElement(); writer.writeAttribute( QString::fromLatin1("name"), categoryWithAreas.key() ); QList> areas = categoryWithAreas.value(); std::sort(areas.begin(),areas.end(), [](QPair a, QPair b) { return a.first < b.first; } ); Q_FOREACH( const auto &positionedTag, areas) { ElementWriter dummy( writer, QString::fromLatin1("value") ); writer.writeAttribute( QString::fromLatin1("value"), positionedTag.first ); writer.writeAttribute( QString::fromLatin1("area"), areaToString(positionedTag.second) ); } } } } bool XMLDB::FileWriter::shouldSaveCategory( const QString& categoryName ) const { // Profiling indicated that this function was a hotspot, so this cache improved saving speed with 25% static QHash cache; if ( cache.contains(categoryName)) return cache[categoryName]; // A few bugs has shown up, where an invalid category name has crashed KPA. It therefore checks for such invalid names here. if ( !m_db->m_categoryCollection.categoryForName( categoryName ) ) { qCWarning(XMLDBLog,"Invalid category name: %s", qPrintable(categoryName)); cache.insert(categoryName,false); return false; } const bool shouldSave = dynamic_cast( m_db->m_categoryCollection.categoryForName( categoryName ).data() )->shouldSave(); cache.insert(categoryName,shouldSave); return shouldSave; } /** * Escape problematic characters in a string that forms an XML attribute name. * N.B.: Attribute values do not need to be escaped! */ QString XMLDB::FileWriter::escape( const QString& str ) { static QHash cache; if ( cache.contains(str) ) return cache[str]; QString tmp( str ); // Regex to match characters that are not allowed to start XML attribute names const QRegExp rx( QString::fromLatin1( "([^a-zA-Z0-9:_])" ) ); int pos = 0; // Encoding special characters if compressed XML is selected if ( useCompressedFileFormat() ) { while ( ( pos = rx.indexIn( tmp, pos ) ) != -1 ) { QString before = rx.cap( 1 ); QString after; after.sprintf( "_.%0X", rx.cap( 1 ).data()->toLatin1()); 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::FileWriter::messageParent() { return MainWindow::Window::theMainWindow(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/kphotoalbumui.rc b/kphotoalbumui.rc index 2dcc5ded..b33d66da 100644 --- a/kphotoalbumui.rc +++ b/kphotoalbumui.rc @@ -1,120 +1,121 @@ - + + &View Privacy &Maintenance Plugins Export Import Images Batch Plugins Tools Main Toolbar diff --git a/org.kde.kphotoalbum.appdata.xml b/org.kde.kphotoalbum.appdata.xml index 05f9d8af..1ecbc92a 100644 --- a/org.kde.kphotoalbum.appdata.xml +++ b/org.kde.kphotoalbum.appdata.xml @@ -1,94 +1,95 @@ org.kde.kphotoalbum CC0-1.0 GPL-2.0 KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum KPhotoAlbum Kfotoalbum KPhotoAlbum KPhotoAlbum xxKPhotoAlbumxx KPhotoAlbum KDE image management software Programari per a gestionar les imatges al KDE Programari per a gestionar les imatges al KDE KDE-Bildverwaltungssoftware KDE image management software Software de gestión de imágenes para KDE Logiciel de gestion d'images KDE Software de xestión de imaxes de KDE Perangkat lunak pengelolaan image KDE Software KDE per la gestione delle immagini Software voor beheer van afbeeldingen Oprogramowanie do zarządzania obrazami w KDE Aplicação de gestão de imagens do KDE Software de gerenciamento de imagens do KDE KDE softvér na manipuláciu s obrázkami KDE bildhanteringsprogramvara KDE resim yönetim yazılımı Програма для керування зображеннями у KDE xxKDE image management softwarexx

KPhotoAlbum is an application for tagging and managing a photo collection and making it searchable.

El KPhotoAlbum és una aplicació per etiquetar i gestionar una col·lecció de fotografies i fer-les fàcils de trobar.

El KPhotoAlbum és una aplicació per etiquetar i gestionar una col·lecció de fotografies i fer-les fàcils de trobar.

KPhotoalbum ist eine Anwendung zum verschlagworten, verwalten und durchsuchen Ihrer Fotosammlung.

KPhotoAlbum is an application for tagging and managing a photo collection and making it searchable.

KPhotoAlbum es una aplicación para etiquetar y gestionar una colección de fotografías que permite realizar búsquedas.

KPhotoAlbum est une application permettant de gérer une collection de photos, en y définissant des balises et en y effectuant des recherches.

KPhotoAlbum é un aplicativo para etiquetar e xestionar unha colección de fotos e permitir buscar nela.

KPhotoAlbum adalah sebuah aplikasi untuk penandaan dan pengelolaan sebuah koleksi foto dan membuatnya dapat dicari.

KPhotoAlbum è un'applicazione per etichettare e gestire una raccolta di immagini in modo da poter eseguire ricerche al suo interno.

KPhotoAlbum is een toepassing voor aanbrengen van tags en beheer van een fotoverzameling en deze doorzoekbaar maken.

KPhotoAlbum jest programem do znaczenia i zarządzania zbiorami zdjęć.

O KPhotoAlbum é uma aplicação para marcar e gerir uma colecção de fotografias, tornando-a fácil de pesquisar.

KPhotoAlbum é um aplicativo para etiquetar e gerenciar uma coleção de fotos e torná-la pesquisável.

KPhotoAlbum je aplikácia na označovanie a správu kolekcie fotografií a jej prehľadávanie.

Kfotoalbum är ett program för att etikettera och hantera en fotosamling och göra den sökbar.

KPhotoAlbum resim koleksiyonunu etiketlemek ve yöneten ve onu aranabilir yapan için bir uygulamadır.

KPhotoAlbum — програма для створення міток у збірці фотографій та керування збіркою із можливостями пошуку зображень.

xxKPhotoAlbum is an application for tagging and managing a photo collection and making it searchable.xx

https://www.kphotoalbum.org/kphotoalbum_big.jpg The main screen in KPhotoAlbum La pantalla principal del KPhotoAlbum La pantalla principal del KPhotoAlbum Hlavní obrazovka pro KPhotoAlbum The main screen in KPhotoAlbum La ventana principal de KPhotoAlbum L'écran principal de KPhotoAlbum + A pantalla principal de KPhotoAlbum La schermata principale in KPhotoAlbum Het hoofdscherm in KPhotoAlbum O ecrã principal no KPhotoAlbum A tela principal do KPhotoAlbum Huvudskärmen i Kfotoalbum Знімок головного вікна KPhotoAlbum xxThe main screen in KPhotoAlbumxx http://kphotoalbum.org https://bugs.kde.org/enter_bug.cgi?format=guided&product=kphotoalbum kphotoalbum@mail.kdab.com KDE kphotoalbum
diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt index 98d1ec7f..d5ded439 100644 --- a/scripts/CMakeLists.txt +++ b/scripts/CMakeLists.txt @@ -1,9 +1,5 @@ -option( KPA_INSTALL_DEVSCRIPTS "Install shell scripts useful to developers." OFF) -if( KPA_INSTALL_DEVSCRIPTS ) - set ( DEVSCRIPTS kpa-mktestdb.sh ) -endif() install( PROGRAMS org.kde.kphotoalbum.open-raw.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) -install( PROGRAMS open-raw.pl kpa-backup.sh ${DEVSCRIPTS} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin ) +install( PROGRAMS open-raw.pl kpa-backup.sh DESTINATION ${CMAKE_INSTALL_PREFIX}/bin ) # vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/scripts/kpa-mktestdb.sh b/scripts/kpa-mktestdb.sh deleted file mode 100755 index 3f9fba01..00000000 --- a/scripts/kpa-mktestdb.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/bin/bash -### -PROGNAME=kpa-mktestdb -VERSION="0.1" -COPYRIGHT="Copyright 2014 Johannes Zarl " -LICENSE="LGPL-3" -### -LOCALPREFIX=`kde4-config --localprefix` -# default locations: -KPARC=$LOCALPREFIX/share/config/kphotoalbumrc -### -TEMPLATE= -DESTINATION= -NO_ACT= -VERBOSE= - -get_config_value() -{ - if [ -r "$KPARC" ] - then - sed -n 's/#.*// ; s/'$1'=\(.*\)/\1/p' "$KPARC" - else - echo "$KPARC does not exist!" >&2 - return 1 - fi -} - -print_version() -{ - echo "$PROGNAME $VERSION" - echo "$COPYRIGHT" - echo "This work is licensed under: $LICENSE" -} - -print_help() { - echo "Usage: $0 [-n] DESTINATION" >&2 - echo "" >&2 - echo "Create a KPhotoAlbum test database directory from a template." >&2 - echo "index.xml, thumbnails, and exif databases are copied," >&2 - echo "everything else is symlinked." >&2 - echo "" >&2 - echo "Options:" >&2 - echo "-n|--no-act Only print what would be done, don't change files." >&2 - echo "-v|--verbose Print commands as they are executed." >&2 - echo "--version Print version information" >&2 - echo "--template Specify an alternative template directory." >&2 - echo " (default: $TEMPLATE)" >&2 - echo "" >&2 -} - -INDEXFILE=`get_config_value configfile` -if [ -f "$INDEXFILE" ] -then - TEMPLATE=`dirname "$INDEXFILE"` -fi - -### Parse commandline: -TEMP=`getopt -o hnv --long help,no-act,template:,version,verbose \ - -n "$PROGNAME" -- "$@"` - -if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi - -# Note the quotes around `$TEMP': they are essential! -eval set -- "$TEMP" - -while true ; do - case "$1" in - -h|--help) print_help ; exit ;; - --version) print_version ; exit ;; - -v|--verbose) VERBOSE=1 ; shift ;; - -n|--no-act) NO_ACT=1 ; shift ;; - -t|--template) TEMPLATE="$2" ; shift 2 ;; - --) shift ; break ;; - *) echo "Internal error!" ; exit 1 ;; - esac -done - -if [ "$#" -eq 0 ] -then - echo "Missing destination directory!" >&2 - exit 1 -fi - -if [ "$#" -gt 1 ] -then - echo "Unknown extra parameters: $@" >&2 - exit 1 -fi - -DESTINATION="$1" - -if [ -e "$DESTINATION" ] -then - echo "Destination directory exists. Bailing out..." >&2 - exit 2 -fi - -if [ ! -d "$TEMPLATE" ] -then - echo "Template does not exist. Bailing out..." >&2 - exit 2 -fi - -if [ -n "$NO_ACT" ] -then - act() { - echo "NOT executing: $*" - } -else - act() { - if [ -n "$VERBOSE" ] - then - echo "Executing: $*" - fi - "$@" - } -fi - -###### Do the work: - -REQUIRED="index.xml" -OPTIONAL="exif-info.db recognition.db .thumbnails .videoThumbnails" - -# check requirements: -if [ ! -d "$TEMPLATE" ] -then - echo "Template directory does not exist!" >&2 - exit 3 -fi -for f in $REQUIRED -do - f="$TEMPLATE/$f" - if [ ! -e "$f" ] - then - echo "Missing file: $f" >&2 - exit 3 - fi -done - -if ! act mkdir "$DESTINATION" -then - echo "Could not create destination directory!" - exit 3 -fi - -# copy files: -files=`ls -A1 "$TEMPLATE"` -for f in $files -do - if echo "$REQUIRED $OPTIONAL" |grep -q "\<$f\>" - then - act cp "$TEMPLATE/$f" "$DESTINATION" - else - act ln -s "$TEMPLATE/$f" "$DESTINATION" - fi -done