diff --git a/AndroidRemoteControl/RemoteInterface.cpp b/AndroidRemoteControl/RemoteInterface.cpp index 8a9952a5..997d1fcb 100644 --- a/AndroidRemoteControl/RemoteInterface.cpp +++ b/AndroidRemoteControl/RemoteInterface.cpp @@ -1,292 +1,292 @@ /* Copyright (C) 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 "RemoteInterface.h" #include "Action.h" #include "Client.h" #include "ImageDetails.h" #include "ImageStore.h" #include "RemoteCommand.h" #include "ScreenInfo.h" #include "Settings.h" #include #include #include #include #include #include #include #include #include using namespace RemoteControl; RemoteInterface::RemoteInterface() : m_categories(new CategoryModel(this)), m_categoryItems(new ThumbnailModel(this)), m_thumbnailModel(new ThumbnailModel(this)), m_discoveryModel(new DiscoveryModel(this)) { m_connection = new Client; connect(m_connection, SIGNAL(gotCommand(RemoteCommand)), this, SLOT(handleCommand(RemoteCommand))); connect(m_connection, &Client::gotConnected,this, &RemoteInterface::connectionChanged); connect(m_connection, &Client::gotConnected, this, &RemoteInterface::requestInitialData); connect(m_connection, &Client::disconnected, this, &RemoteInterface::gotDisconnected); connect(m_connection, &Client::disconnected, this, &RemoteInterface::connectionChanged); qRegisterMetaType("RemoteControl::CategoryModel*"); qRegisterMetaType("ThumbnailModel*"); qRegisterMetaType("DiscoveryModel*"); QTimer::singleShot(1000, this, SLOT(pushAwayFromStartupState())); } void RemoteInterface::setCurrentPage(Page page) { if (m_currentPage != page) { m_currentPage = page; emit currentPageChanged(); } } void RemoteInterface::setListCategoryValues(const QStringList& values) { if (m_listCategoryValues != values) { m_listCategoryValues = values; emit listCategoryValuesChanged(); } } void RemoteInterface::requestHomePageImages() { m_connection->sendCommand(StaticImageRequest(ScreenInfo::instance().overviewIconSize())); } void RemoteInterface::gotDisconnected() { setCurrentPage(Page::UnconnectedPage); } void RemoteInterface::setHomePageImages(const StaticImageResult& command) { m_homeImage = command.homeIcon; emit homeImageChanged(); m_kphotoalbumImage = command.kphotoalbumIcon; emit kphotoalbumImageChange(); m_discoveryImage = command.discoverIcon; emit discoveryImageChanged(); } RemoteInterface& RemoteInterface::instance() { static RemoteInterface interface; return interface; } bool RemoteInterface::isConnected() const { return m_connection->isConnected(); } void RemoteInterface::sendCommand(const RemoteCommand& command) { m_connection->sendCommand(command); } QString RemoteInterface::currentCategory() const { return m_search.currentCategory(); } QImage RemoteInterface::discoveryImage() const { return m_discoveryImage; } void RemoteInterface::setActiveThumbnailModel(RemoteInterface::ModelType type) { ThumbnailModel* newModel = (type == ModelType::Thumbnail ? m_thumbnailModel : m_discoveryModel); if (newModel != m_activeThumbnailModel) { m_activeThumbnailModel = newModel; activeThumbnailModelChanged(); } m_activeThumbnailModel->setImages({}); } void RemoteInterface::goHome() { requestInitialData(); } void RemoteInterface::goBack() { if(m_history.canGoBack()) m_history.goBackward(); else qApp->quit(); } void RemoteInterface::goForward() { if (m_history.canGoForward()) m_history.goForward(); } void RemoteInterface::selectCategory(const QString& category, int type) { m_search.addCategory(category); m_history.push(std::unique_ptr(new ShowCategoryValueAction(m_search, static_cast(type)))); } void RemoteInterface::selectCategoryValue(const QString& value) { m_search.addValue(value); m_history.push(std::unique_ptr(new ShowThumbnailsAction(m_search))); } void RemoteInterface::showThumbnails() { m_history.push(std::unique_ptr(new ShowThumbnailsAction(m_search))); } void RemoteInterface::showImage(int imageId) { m_history.push(std::unique_ptr(new ShowImagesAction(imageId, m_search))); } void RemoteInterface::requestDetails(int imageId) { m_connection->sendCommand(ImageDetailsRequest(imageId)); } void RemoteInterface::activateSearch(const QString& search) { QStringList list = search.split(";;;"); QString category = list[0]; QString item = list[1]; SearchInfo result; result.addCategory(category); result.addValue(item); m_history.push(std::unique_ptr(new ShowThumbnailsAction(result))); } void RemoteInterface::doDiscovery() { m_history.push(std::unique_ptr(new DiscoverAction(m_search, m_discoveryModel))); } void RemoteInterface::showOverviewPage() { m_history.push(std::unique_ptr(new ShowOverviewAction(m_search))); } void RemoteInterface::setToken(int imageId, const QString &token) { sendCommand(ToggleTokenRequest(imageId, token, ToggleTokenRequest::On)); } void RemoteInterface::removeToken(int imageId, const QString &token) { sendCommand(ToggleTokenRequest(imageId, token, ToggleTokenRequest::Off)); } void RemoteInterface::rerequestOverviewPageData() { requestHomePageImages(); m_history.rerunTopItem(); } void RemoteInterface::pushAwayFromStartupState() { // Avoid that the "not connected page" show for a few milliseconds while the connection is being set up. if (!isConnected() && m_currentPage == Types::Startup) setCurrentPage(Types::UnconnectedPage); } void RemoteInterface::setCurrentView(int imageId) { emit jumpToImage(m_activeThumbnailModel->indexOf(imageId)); } QString RemoteInterface::networkAddress() const { QStringList result; for (const QHostAddress& address : QNetworkInterface::allAddresses()) { if (address.isLoopback() || address.toIPv4Address() == 0) continue; result.append(address.toString()); } return result.join(QStringLiteral(", ")); } QStringList RemoteInterface::tokens() const { - // FIXME: in KPA the tokens category is now retreived using categoryForSpecial + // FIXME: in KPA the tokens category is now retrieved using categoryForSpecial return ImageDetails::instance().itemsOfCategory(QStringLiteral("Tokens")); } void RemoteInterface::requestInitialData() { requestHomePageImages(); m_history.push(std::unique_ptr(new ShowOverviewAction({}))); } void RemoteInterface::handleCommand(const RemoteCommand& command) { if (command.commandType() == CommandType::ThumbnailResult) updateImage(static_cast(command)); else if (command.commandType() == CommandType::CategoryListResult) updateCategoryList(static_cast(command)); else if (command.commandType() == CommandType::SearchResult) gotSearchResult(static_cast(command)); else if (command.commandType() == CommandType::TimeCommand) ; // Used for debugging, it will print time stamp when decoded else if (command.commandType() == CommandType::ImageDetailsResult) { ImageDetails::instance().setData(static_cast(command)); emit tokensChanged(); } else if (command.commandType() == CommandType::CategoryItemsResult) setListCategoryValues(static_cast(command).items); else if (command.commandType() == CommandType::StaticImageResult) setHomePageImages(static_cast(command)); else qFatal("Unhandled command"); } void RemoteInterface::updateImage(const ThumbnailResult& command) { ImageStore::instance().updateImage(command.imageId, command.image, command.label, command.type); } void RemoteInterface::updateCategoryList(const CategoryListResult& command) { ScreenInfo::instance().setCategoryCount(command.categories.count()); m_categories->setCategories(command.categories); } void RemoteInterface::gotSearchResult(const SearchResult& result) { if (result.type == SearchType::Images) { m_activeThumbnailModel->setImages(result.result); } else if (result.type == SearchType::CategoryItems) { m_categoryItems->setImages(result.result); } } diff --git a/AnnotationDialog/Dialog.cpp b/AnnotationDialog/Dialog.cpp index e6210447..6a837606 100644 --- a/AnnotationDialog/Dialog.cpp +++ b/AnnotationDialog/Dialog.cpp @@ -1,1749 +1,1749 @@ /* 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_ratingSearchMode->addItems( QStringList() << i18n("==") << i18n(">=") << i18n("<=") << i18n("!=") ); m_ratingSearchLabel->setBuddy( m_ratingSearchMode ); lay9->addWidget( m_ratingSearchMode ); // File name search pattern QHBoxLayout* lay10 = new QHBoxLayout; lay2->addLayout( lay10 ); m_imageFilePatternLabel = new QLabel( i18n("File Name Pattern: " ) ); lay10->addWidget( m_imageFilePatternLabel ); m_imageFilePattern = new KLineEdit; m_imageFilePattern->setObjectName( i18n("File Name Pattern") ); lay10->addWidget( m_imageFilePattern ); shortCutManager.addLabel( m_imageFilePatternLabel ); m_imageFilePatternLabel->setBuddy( m_imageFilePattern ); m_searchRAW = new QCheckBox( i18n("Search only for RAW files") ); lay2->addWidget( m_searchRAW ); lay9->addStretch( 1 ); lay2->addStretch(1); return top; } QWidget* AnnotationDialog::Dialog::createPreviewWidget() { m_preview = new ImagePreviewWidget(); connect(m_preview, &ImagePreviewWidget::togglePreview, this, &Dialog::togglePreview); return m_preview; } void AnnotationDialog::Dialog::slotRevert() { if ( m_setup == InputSingleImageConfigMode ) load(); } void AnnotationDialog::Dialog::slotIndexChanged( int index ) { if ( m_setup != InputSingleImageConfigMode ) return; if(m_current >= 0 ) writeToInfo(); m_current = index; load(); } void AnnotationDialog::Dialog::doneTagging() { saveAndClose(); if ( Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() ) { for( DB::ImageInfoListIterator it = m_origList.begin(); it != m_origList.end(); ++it ) { (*it)->removeCategoryInfo( Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag() ); } } } /* * Copy tags (only tags/categories, not description/label/...) from previous image to the currently showed one */ void AnnotationDialog::Dialog::slotCopyPrevious() { if ( m_setup != InputSingleImageConfigMode ) return; if ( m_current < 1 ) return; // FIXME: it would be better to compute the "previous image" in a better way, but let's stick with this for now... DB::ImageInfo& old_info = m_editList[ m_current - 1 ]; m_positionableTagCandidates.clear(); m_lastSelectedPositionableTag.first = QString(); m_lastSelectedPositionableTag.second = QString(); Q_FOREACH( ListSelect *ls, m_optionList ) { ls->setSelection( old_info.itemsOfCategory( ls->category() ) ); // Also set all positionable tag candidates if ( ls->positionable() ) { QString category = ls->category(); QSet selectedTags = old_info.itemsOfCategory( category ); QSet positionedTagSet = positionedTags( category ); // Add the tag to the positionable candiate list, if no area is already associated with it Q_FOREACH(const auto &tag, selectedTags) { if (!positionedTagSet.contains(tag)) { addTagToCandidateList(category, tag); } } // Check all areas for a linked tag in this category that is probably not selected anymore for(ResizableFrame *area : areas()) { QPair tagData = area->tagData(); if (tagData.first == category) { if (! selectedTags.contains(tagData.second)) { // The linked tag is not selected anymore, so remove it area->removeTagData(); } } } } } } void AnnotationDialog::Dialog::load() { // Remove all areas tidyAreas(); // No areas have been changed m_areasChanged = false; // Empty the positionable tag candidate list and the last selected positionable tag m_positionableTagCandidates.clear(); m_lastSelectedPositionableTag = QPair(); DB::ImageInfo& info = m_editList[ m_current ]; m_startDate->setDate( info.date().start().date() ); if( info.date().hasValidTime() ) { m_time->show(); m_time->setTime( info.date().start().time()); m_isFuzzyDate->setChecked(false); } else { m_time->hide(); m_isFuzzyDate->setChecked(true); } if ( info.date().start().date() == info.date().end().date() ) m_endDate->setDate( QDate() ); else m_endDate->setDate( info.date().end().date() ); m_imageLabel->setText( info.label() ); m_description->setPlainText( info.description() ); if ( m_setup == InputSingleImageConfigMode ) m_rating->setRating( qMax( static_cast(0), info.rating() ) ); m_ratingChanged = false; // A category areas have been linked against could have been deleted // or un-marked as positionable in the meantime, so ... QMap categoryIsPositionable; QList positionableCategories; Q_FOREACH( ListSelect *ls, m_optionList ) { ls->setSelection( info.itemsOfCategory( ls->category() ) ); ls->rePopulate(); // Get all selected positionable tags and add them to the candidate list if (ls->positionable()) { QSet selectedTags = ls->itemsOn(); Q_FOREACH( const QString &tagName, selectedTags ) { addTagToCandidateList( ls->category(), tagName ); } } // ... create a list of all categories and their positionability ... categoryIsPositionable[ls->category()] = ls->positionable(); if (ls->positionable()) { positionableCategories << ls->category(); } } // Create all tagged areas QMap> taggedAreas = info.taggedAreas(); QMapIterator> areasInCategory(taggedAreas); while (areasInCategory.hasNext()) { areasInCategory.next(); QString category = areasInCategory.key(); // ... and check if the respective category is actually there yet and still positionable // (operator[] will insert an empty item if the category has been deleted // and is thus missing in the QMap, but the respective key won't be true) if (categoryIsPositionable[category]) { QMapIterator areaData(areasInCategory.value()); while (areaData.hasNext()) { areaData.next(); QString tag = areaData.key(); // Be sure that the corresponding tag is still checked. The category could have // been un-marked as positionable in the meantime and the tag could have been // deselected, without triggering positionableTagDeselected and the area thus // still remaining. If the category is then re-marked as positionable, the area would // show up without the tag being selected. if(m_listSelectList[category]->tagIsChecked(tag)) { m_preview->preview()->createTaggedArea(category, tag, areaData.value(), m_preview->showAreas()); } } } } if (m_setup == InputSingleImageConfigMode) { setWindowTitle(i18n("KPhotoAlbum Annotations (%1/%2)", m_current + 1, m_origList.count())); m_preview->canCreateAreas( m_setup == InputSingleImageConfigMode && ! info.isVideo() && m_positionableCategories ); #ifdef HAVE_KGEOMAP updateMapForCurrentImage(); #endif } m_preview->updatePositionableCategories(positionableCategories); } void AnnotationDialog::Dialog::writeToInfo() { Q_FOREACH( ListSelect *ls, m_optionList ) { ls->slotReturn(); } DB::ImageInfo& info = m_editList[ m_current ]; if (! info.size().isValid()) { // The actual image size has been fetched by ImagePreview, so we can add it to // the database silenty, so that it's saved if the database will be saved. info.setSize(m_preview->preview()->getActualImageSize()); } if ( m_time->isHidden() ) { if ( m_endDate->date().isValid() ) info.setDate( DB::ImageDate( QDateTime( m_startDate->date(), QTime(0,0,0) ), QDateTime( m_endDate->date(), QTime( 23,59,59) ) ) ); else info.setDate( DB::ImageDate( QDateTime( m_startDate->date(), QTime(0,0,0) ), QDateTime( m_startDate->date(), QTime( 23,59,59) ) ) ); } else info.setDate( DB::ImageDate( QDateTime( m_startDate->date(), m_time->time() ) ) ); // Generate a list of all tagged areas QMap> taggedAreas; QPair tagData; foreach (ResizableFrame *area, areas()) { tagData = area->tagData(); if ( !tagData.first.isEmpty() ) { taggedAreas[tagData.first][tagData.second] = area->actualCoordinates(); } } info.setLabel( m_imageLabel->text() ); info.setDescription( m_description->toPlainText() ); Q_FOREACH( ListSelect *ls, m_optionList ) { info.setCategoryInfo( ls->category(), ls->itemsOn() ); if (ls->positionable()) { info.setPositionedTags(ls->category(), taggedAreas[ls->category()]); } } if ( m_ratingChanged ) { info.setRating( m_rating->rating() ); m_ratingChanged = false; } } void AnnotationDialog::Dialog::ShowHideSearch( bool show ) { m_megapixel->setVisible( show ); m_megapixelLabel->setVisible( show ); m_max_megapixel->setVisible( show ); m_max_megapixelLabel->setVisible( show ); m_searchRAW->setVisible( show ); m_imageFilePatternLabel->setVisible( show ); m_imageFilePattern->setVisible( show ); m_isFuzzyDate->setChecked( show ); m_isFuzzyDate->setVisible( !show ); slotSetFuzzyDate(); m_ratingSearchMode->setVisible( show ); m_ratingSearchLabel->setVisible( show ); } QList AnnotationDialog::Dialog::areas() const { return m_preview->preview()->findChildren(); } int AnnotationDialog::Dialog::configure( DB::ImageInfoList list, bool oneAtATime ) { ShowHideSearch(false); if ( Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() ) { DB::ImageDB::instance()->categoryCollection()->categoryForName( Settings::SettingsData::instance()->untaggedCategory() ) ->addItem(Settings::SettingsData::instance()->untaggedTag() ); } if (oneAtATime) { m_setup = InputSingleImageConfigMode; } else { m_setup = InputMultiImageConfigMode; // Hide the default positionable category selector m_preview->updatePositionableCategories(); } #ifdef HAVE_KGEOMAP m_mapIsPopulated = false; m_annotationMap->clear(); #endif m_origList = list; m_editList.clear(); for( DB::ImageInfoListConstIterator it = list.constBegin(); it != list.constEnd(); ++it ) { m_editList.append( *(*it) ); } setup(); if ( oneAtATime ) { m_current = 0; m_preview->configure( &m_editList, true ); load(); } else { m_preview->configure( &m_editList, false ); m_preview->canCreateAreas( false ); m_startDate->setDate( QDate() ); m_endDate->setDate( QDate() ); m_time->hide(); m_rating->setRating( 0 ); m_ratingChanged = false; m_areasChanged = false; Q_FOREACH( ListSelect *ls, m_optionList ) { setUpCategoryListBoxForMultiImageSelection( ls, list ); } m_imageLabel->setText(QString()); m_imageFilePattern->setText(QString()); m_firstDescription = m_editList[0].description(); const bool allTextEqual = std::all_of(m_editList.begin(), m_editList.end(), [=] (const DB::ImageInfo& item) -> bool { return item.description() == m_firstDescription; }); if ( !allTextEqual ) m_firstDescription = m_conflictText; m_description->setPlainText( m_firstDescription ); } showHelpDialog( oneAtATime ? InputSingleImageConfigMode : InputMultiImageConfigMode ); return exec(); } DB::ImageSearchInfo AnnotationDialog::Dialog::search( DB::ImageSearchInfo* search ) { ShowHideSearch(true); #ifdef HAVE_KGEOMAP m_mapIsPopulated = false; m_annotationMap->clear(); #endif m_setup = SearchMode; if ( search ) m_oldSearch = *search; setup(); m_preview->setImage(Utilities::locateDataFile(QString::fromLatin1("pics/search.jpg"))); m_ratingChanged = false ; showHelpDialog( SearchMode ); int ok = exec(); if ( ok == QDialog::Accepted ) { const QDateTime start = m_startDate->date().isNull() ? QDateTime() : QDateTime(m_startDate->date()); const QDateTime end = m_endDate->date().isNull() ? QDateTime() : QDateTime( m_endDate->date() ); m_oldSearch = DB::ImageSearchInfo( DB::ImageDate( start, end ), m_imageLabel->text(), m_description->toPlainText(), m_imageFilePattern->text()); Q_FOREACH( const ListSelect *ls, m_optionList ) { m_oldSearch.setCategoryMatchText( ls->category(), ls->text() ); } //FIXME: for the user to search for 0-rated images, he must first change the rating to anything > 0 //then change back to 0 . if( m_ratingChanged) m_oldSearch.setRating( m_rating->rating() ); m_ratingChanged = false; m_oldSearch.setSearchMode( m_ratingSearchMode->currentIndex() ); m_oldSearch.setMegaPixel( m_megapixel->value() ); m_oldSearch.setMaxMegaPixel( m_max_megapixel->value() ); m_oldSearch.setSearchRAW( m_searchRAW->isChecked() ); #ifdef HAVE_KGEOMAP const KGeoMap::GeoCoordinates::Pair regionSelection = m_annotationMap->getRegionSelection(); m_oldSearch.setRegionSelection(regionSelection); #endif return m_oldSearch; } else return DB::ImageSearchInfo(); } void AnnotationDialog::Dialog::setup() { // Repopulate the listboxes in case data has changed // An group might for example have been renamed. Q_FOREACH( ListSelect *ls, m_optionList ) { ls->populate(); } if ( m_setup == SearchMode ) { KGuiItem::assign(m_okBut, KGuiItem(i18nc("@action:button","&Search"), QString::fromLatin1("find")) ); m_continueLaterBut->hide(); m_revertBut->hide(); m_clearBut->show(); m_preview->setSearchMode(true); setWindowTitle( i18nc("@title:window title of the 'find images' window","Search") ); loadInfo( m_oldSearch ); } else { m_okBut->setText( i18n("Done") ); m_continueLaterBut->show(); m_revertBut->setEnabled( m_setup == InputSingleImageConfigMode ); m_clearBut->hide(); m_revertBut->show(); m_preview->setSearchMode(false); m_preview->setToggleFullscreenPreviewEnabled(m_setup == InputSingleImageConfigMode); setWindowTitle( i18n("Annotations") ); } 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); action = m_actions->addAction( QString::fromLatin1("annotationdialog-sort-alphaflat"), m_optionList.at(0), SLOT(slotSortAlphaFlat()) ); action->setText( i18n("Sort Alphabetically (Flat)") ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-sort-MRU"), m_optionList.at(0), SLOT(slotSortDate()) ); action->setText( i18n("Sort Most Recently Used") ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-toggle-sort"), m_optionList.at(0), SLOT(toggleSortType()) ); action->setText( i18n("Toggle Sorting") ); action->setShortcut( Qt::CTRL+Qt::Key_T ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-toggle-showing-selected-only"), &ShowSelectionOnlyManager::instance(), SLOT(toggle()) ); action->setText( i18n("Toggle Showing Selected Items Only") ); action->setShortcut( Qt::CTRL+Qt::Key_S ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-next-image"), m_preview, SLOT(slotNext()) ); action->setText( i18n("Annotate Next") ); action->setShortcut( Qt::Key_PageDown ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-prev-image"), m_preview, SLOT(slotPrev()) ); action->setText( i18n("Annotate Previous") ); action->setShortcut( Qt::Key_PageUp ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-OK-dialog"), this, SLOT(doneTagging()) ); action->setText( i18n("OK dialog") ); action->setShortcut( Qt::CTRL+Qt::Key_Return ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-delete-image"), this, SLOT(slotDeleteImage()) ); action->setText( i18n("Delete") ); action->setShortcut( Qt::CTRL+Qt::Key_Delete ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-copy-previous"), this, SLOT(slotCopyPrevious()) ); action->setText( i18n("Copy tags from previous image") ); action->setShortcut( Qt::ALT+Qt::Key_Insert ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-rotate-left"), m_preview, SLOT(rotateLeft()) ); action->setText( i18n("Rotate counterclockwise") ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-rotate-right"), m_preview, SLOT(rotateRight()) ); action->setText( i18n("Rotate clockwise") ); action = m_actions->addAction( QString::fromLatin1("annotationdialog-toggle-viewer"), this, SLOT(togglePreview()) ); action->setText( i18n("Toggle fullscreen preview") ); action->setShortcut( Qt::CTRL + Qt::Key_Space ); foreach (QAction* action, m_actions->actions()) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } } KActionCollection* AnnotationDialog::Dialog::actions() { return m_actions; } void AnnotationDialog::Dialog::setUpCategoryListBoxForMultiImageSelection( ListSelect* listSel, const DB::ImageInfoList& images ) { StringSet on, partialOn; std::tie(on,partialOn) = selectionForMultiSelect( listSel, images ); listSel->setSelection( on, partialOn ); } std::tuple AnnotationDialog::Dialog::selectionForMultiSelect( ListSelect* listSel, const DB::ImageInfoList& images ) { const QString category = listSel->category(); const StringSet allItems = DB::ImageDB::instance()->categoryCollection()->categoryForName( category )->itemsInclCategories().toSet(); StringSet itemsNotSelectedOnAllImages; StringSet itemsOnSomeImages; for ( DB::ImageInfoList::ConstIterator imageIt = images.begin(); imageIt != images.end(); ++ imageIt ) { const StringSet itemsOnThisImage = (*imageIt)->itemsOfCategory( category ); itemsNotSelectedOnAllImages += ( allItems - itemsOnThisImage ); itemsOnSomeImages += itemsOnThisImage; } const StringSet itemsOnAllImages = allItems - itemsNotSelectedOnAllImages; const StringSet itemsPartiallyOn = itemsOnSomeImages - itemsOnAllImages; return std::make_tuple( itemsOnAllImages, itemsPartiallyOn ); } void AnnotationDialog::Dialog::slotRatingChanged( unsigned int ) { m_ratingChanged = true; } void AnnotationDialog::Dialog::continueLater() { saveAndClose(); } void AnnotationDialog::Dialog::saveAndClose() { tidyAreas(); m_fullScreenPreview->stopPlayback(); if (m_origList.isEmpty()) { // all images are deleted. QDialog::accept(); return; } // I need to check for the changes first, as the case for m_setup == InputSingleImageConfigMode, saves to the m_origList, // and we can thus not check for changes anymore const bool anyChanges = hasChanges(); if ( m_setup == InputSingleImageConfigMode ) { writeToInfo(); for ( int i = 0; i < m_editList.count(); ++i ) { *(m_origList[i]) = m_editList[i]; } } else if ( m_setup == InputMultiImageConfigMode ) { Q_FOREACH( ListSelect *ls, m_optionList ) { ls->slotReturn(); } for( DB::ImageInfoListConstIterator it = m_origList.constBegin(); it != m_origList.constEnd(); ++it ) { DB::ImageInfoPtr info = *it; info->delaySavingChanges(true); if ( !m_startDate->date().isNull() ) info->setDate( DB::ImageDate( m_startDate->date(), m_endDate->date(), m_time->time() ) ); Q_FOREACH( ListSelect *ls, m_optionList ) { info->addCategoryInfo( ls->category(), ls->itemsOn() ); info->removeCategoryInfo( ls->category(), ls->itemsOff() ); } if ( !m_imageLabel->text().isEmpty() ) { info->setLabel( m_imageLabel->text() ); } if ( !m_description->toPlainText().isEmpty() && m_description->toPlainText().compare( m_conflictText ) ) { info->setDescription( m_description->toPlainText() ); } if( m_ratingChanged) { info->setRating( m_rating->rating() ); } info->delaySavingChanges(false); } m_ratingChanged = false; } m_accept = QDialog::Accepted; if (anyChanges) { MainWindow::DirtyIndicator::markDirty(); } QDialog::accept(); } AnnotationDialog::Dialog::~Dialog() { qDeleteAll( m_optionList ); m_optionList.clear(); } void AnnotationDialog::Dialog::togglePreview() { if (m_setup == InputSingleImageConfigMode) { if ( m_stack->currentWidget() == m_fullScreenPreview ) { m_stack->setCurrentWidget( m_dockWindow ); m_fullScreenPreview->stopPlayback(); } else { m_stack->setCurrentWidget( m_fullScreenPreview ); m_fullScreenPreview->load( DB::FileNameList() << m_editList[ m_current].fileName() ); } } } void AnnotationDialog::Dialog::tidyAreas() { // Remove all areas marked on the preview image foreach (ResizableFrame *area, areas()) { area->markTidied(); area->deleteLater(); } } void AnnotationDialog::Dialog::slotNewArea(ResizableFrame *area) { area->setDialog(this); } void AnnotationDialog::Dialog::positionableTagSelected(QString category, QString tag) { // Be sure not to propose an already-associated tag QPair tagData = qMakePair(category, tag); foreach (ResizableFrame *area, areas()) { if (area->tagData() == tagData) { return; } } // Set the selected tag as the last selected positionable tag m_lastSelectedPositionableTag = tagData; // Add the tag to the positionable tag candidate list addTagToCandidateList(category, tag); } void AnnotationDialog::Dialog::positionableTagDeselected(QString category, QString tag) { // Remove the tag from the candidate list removeTagFromCandidateList(category, tag); // Search for areas linked against the tag on this image if (m_setup == InputSingleImageConfigMode) { QPair deselectedTag = QPair(category, tag); foreach (ResizableFrame *area, areas()) { if (area->tagData() == deselectedTag) { area->removeTagData(); m_areasChanged = true; // Only one area can be associated with the tag, so we can return here return; } } } // Removal of tagged areas in InputMultiImageConfigMode is done in DB::ImageInfo::removeCategoryInfo } void AnnotationDialog::Dialog::addTagToCandidateList(QString category, QString tag) { m_positionableTagCandidates << QPair(category, tag); } void AnnotationDialog::Dialog::removeTagFromCandidateList(QString category, QString tag) { // Is the deselected tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == tag) { m_lastSelectedPositionableTag = QPair(); } // Remove the tag from the candidate list m_positionableTagCandidates.removeAll(QPair(category, tag)); // When a positionable tag is entered via the AreaTagSelectDialog, it's added to this // list twice, so we use removeAll here to be sure to also wipe duplicate entries. } QPair AnnotationDialog::Dialog::lastSelectedPositionableTag() const { return m_lastSelectedPositionableTag; } QList> AnnotationDialog::Dialog::positionableTagCandidates() const { return m_positionableTagCandidates; } void AnnotationDialog::Dialog::slotShowAreas(bool showAreas) { foreach (ResizableFrame *area, areas()) { area->setVisible(showAreas); } } void AnnotationDialog::Dialog::positionableTagRenamed(QString category, QString oldTag, QString newTag) { // Is the renamed tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == oldTag) { m_lastSelectedPositionableTag.second = newTag; } // Check the candidate list for the tag QPair oldTagData = QPair(category, oldTag); if (m_positionableTagCandidates.contains(oldTagData)) { // The tag is in the list, so update it m_positionableTagCandidates.removeAt(m_positionableTagCandidates.indexOf(oldTagData)); m_positionableTagCandidates << QPair(category, newTag); } // Check if an area on the current image contains the changed or proposed tag foreach (ResizableFrame *area, areas()) { if (area->tagData() == oldTagData) { area->setTagData(category, newTag); } } } void AnnotationDialog::Dialog::descriptionPageUpDownPressed(QKeyEvent *event) { if (event->key() == Qt::Key_PageUp) { m_actions->action(QString::fromLatin1("annotationdialog-prev-image"))->trigger(); } else if (event->key() == Qt::Key_PageDown) { m_actions->action(QString::fromLatin1("annotationdialog-next-image"))->trigger(); } } void AnnotationDialog::Dialog::checkProposedTagData( QPair tagData, ResizableFrame *areaToExclude) const { foreach (ResizableFrame *area, areas()) { if (area != areaToExclude && area->proposedTagData() == tagData && area->tagData().first.isEmpty()) { area->removeProposedTagData(); } } } void AnnotationDialog::Dialog::areaChanged() { m_areasChanged = true; } /** * @brief positionableTagValid checks whether a given tag can still be associated to an area. * This checks for empty and duplicate tags. * @return */ bool AnnotationDialog::Dialog::positionableTagAvailable(const QString &category, const QString &tag) const { if (category.isEmpty() || tag.isEmpty()) return false; // does any area already have that tag? foreach (const ResizableFrame *area, areas()) { const auto tagData = area->tagData(); if (tagData.first == category && tagData.second == tag) return false; } return true; } /** * @brief Generates a set of positionable tags currently used on the image * @param category * @return */ QSet AnnotationDialog::Dialog::positionedTags(const QString &category) const { QSet tags; foreach (const ResizableFrame *area, areas()) { const auto tagData = area->tagData(); if (tagData.first == category) tags += tagData.second; } return tags; } AnnotationDialog::ListSelect *AnnotationDialog::Dialog::listSelectForCategory(const QString &category) { return m_listSelectList.value(category,nullptr); } #ifdef HAVE_KGEOMAP void AnnotationDialog::Dialog::updateMapForCurrentImage() { if (m_setup != InputSingleImageConfigMode) { return; } if (m_editList[m_current].coordinates().hasCoordinates()) { m_annotationMap->setCenter(m_editList[m_current]); m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } } void AnnotationDialog::Dialog::annotationMapVisibilityChanged(bool visible) { // This populates the map if it's added when the dialog is already open if ( visible ) { // when the map dockwidget is already visible on show(), the call to // annotationMapVisibilityChanged is executed in the GUI thread. // This ensures that populateMap() doesn't block the GUI in this case: QTimer::singleShot(0, this, SLOT(populateMap())); } else { m_cancelMapLoading = true; } } void AnnotationDialog::Dialog::populateMap() { // populateMap is called every time the map widget gets visible if (m_mapIsPopulated) { return; } m_annotationMap->displayStatus(Map::MapView::MapStatus::Loading); m_cancelMapLoading = false; m_mapLoadingProgress->setMaximum(m_editList.count()); m_mapLoadingProgress->show(); m_cancelMapLoadingButton->show(); int processedImages = 0; int imagesWithCoordinates = 0; foreach (DB::ImageInfo info, m_editList) { processedImages++; m_mapLoadingProgress->setValue(processedImages); // keep things responsive by processing events manually: QApplication::processEvents(); if (info.coordinates().hasCoordinates()) { m_annotationMap->addImage(info); imagesWithCoordinates++; } // m_cancelMapLoading is set to true by clicking the "Cancel" button if (m_cancelMapLoading) { m_annotationMap->clear(); break; } } // at this point either we canceled loading or the map is populated: m_mapIsPopulated = ! m_cancelMapLoading; mapLoadingFinished(imagesWithCoordinates > 0, imagesWithCoordinates == processedImages); } void AnnotationDialog::Dialog::setCancelMapLoading() { m_cancelMapLoading = true; } void AnnotationDialog::Dialog::mapLoadingFinished(bool mapHasImages, bool allImagesHaveCoordinates) { m_mapLoadingProgress->hide(); m_cancelMapLoadingButton->hide(); if (m_setup == InputSingleImageConfigMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } else { if (m_setup == SearchMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SearchCoordinates); } else { if (mapHasImages) { if (! allImagesHaveCoordinates) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SomeImagesHaveNoCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::NoImagesHaveNoCoordinates); } } } if (m_setup != SearchMode) { m_annotationMap->zoomToMarkers(); updateMapForCurrentImage(); } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FastDir.cpp b/DB/FastDir.cpp index e36ac8bf..6ed7ad72 100644 --- a/DB/FastDir.cpp +++ b/DB/FastDir.cpp @@ -1,187 +1,192 @@ /* Copyright (C) 2010-2018 Jesper Pedersen and Robert Krawitz 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 "FastDir.h" #include "Logging.h" #include #include #include extern "C" { #include #include #include /* * Ideally the order of entries returned by readdir() should be close * to optimal; intuitively, it should reflect the order in which inodes * are returned from getdents() or equivalent, which should be the order * in which they're stored on the disk. Experimentally, that isn't always * true. One test, involving 10839 files totaling 90 GB resulted in * readdir() returning files in random order, where "find" returned them * sorted (and the same version of find does *not* in general return files * in alphabetical order). * * By repeated measurement, loading the files in the order returned by * readdir took about 16:30, where loading them in alphanumeric sorted order * took about 15:00. Running a similar test outside of kpa (using the order * returned by readdir() vs. sorted to cat the files through dd and measuring * the time) yielded if anything an even greater discrepancy (17:35 vs. 14:10). * * This issue is filesystem dependent, but is known to affect the extN * filesystems commonly used on Linux that use a hashed tree structure to * store directories. See e. g. * http://home.ifi.uio.no/paalh/publications/files/ipccc09.pdf and its * accompanying presentation * http://www.linux-kongress.org/2009/slides/linux_disk_io_performance_havard_espeland.pdf * * We could do even better by sorting by block position, but that would * greatly increase complexity. */ - -// Note: on FreeBSD, __linux__ is also defined -#ifdef __BSD__ -# include -# include -#else #ifdef __linux__ -# include -# include -#endif // __linux__ +# include +# include +# define HAVE_STATFS +# define STATFS_FSTYPE_EXT2 EXT2_SUPER_MAGIC // Includes EXT3_SUPER_MAGIC, EXT4_SUPER_MAGIC +#else +#ifdef __FreeBSD__ +# include +# include +# include +# define HAVE_STATFS +# define STATFS_FSTYPE_EXT2 FS_EXT2FS #endif +// other platforms fall back to known-safe (but slower) implementation +#endif // __linux__ } typedef QMap InodeMap; typedef QSet StringSet; DB::FastDir::FastDir(const QString &path) : m_path(path) { InodeMap tmpAnswer; DIR *dir; dirent *file; QByteArray bPath(QFile::encodeName(path)); dir = opendir( bPath.constData() ); if ( !dir ) return; const bool doSortByInode = sortByInode(bPath); const bool doSortByName = sortByName(bPath); #if defined(QT_THREAD_SUPPORT) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(Q_OS_CYGWIN) // ZaJ (2016-03-23): while porting to Qt5/KF5, this code-path is disabled on my system // I don't want to touch this right now since I can't verify correctness in any way. // rlk 2018-05-20: readdir_r is deprecated as of glibc 2.24; see // http://man7.org/linux/man-pages/man3/readdir_r.3.html. // There are problems with MAXNAMLEN/NAME_MAX and friends, that // can differ from filesystem to filesystem. It's also expected // that POSIX will (if it hasn't already) deprecate readdir_r // and require readdir to be thread safe. union dirent_buf { struct KDE_struct_dirent mt_file; char b[sizeof(struct dirent) + MAXNAMLEN + 1]; } *u = new union dirent_buf; while ( readdir_r(dir, &(u->mt_file), &file ) == 0 && file ) #else // FIXME: use 64bit versions of readdir and dirent? while ( (file = readdir(dir)) ) #endif // QT_THREAD_SUPPORT && _POSIX_THREAD_SAFE_FUNCTIONS { if ( doSortByInode ) tmpAnswer.insert(file->d_ino, QFile::decodeName(file->d_name)); else m_sortedList.append(QFile::decodeName(file->d_name)); } #if defined(QT_THREAD_SUPPORT) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(Q_OS_CYGWIN) delete u; #endif (void) closedir(dir); if ( doSortByInode ) { for ( InodeMap::iterator it = tmpAnswer.begin(); it != tmpAnswer.end(); ++it ) { m_sortedList << it.value(); } } else if ( doSortByName ) { m_sortedList.sort(); } } // No currently known filesystems where sort by name is optimal constexpr bool DB::sortByName(const QByteArray &) { return false; } bool DB::sortByInode(const QByteArray &path) { -#ifdef __linux__ +#ifdef HAVE_STATFS struct statfs buf; if ( statfs( path.constData(), &buf ) == -1 ) return -1; // Add other filesystems as appropriate switch ( buf.f_type ) { - case EXT2_SUPER_MAGIC: // Includes EXT3_SUPER_MAGIC, EXT4_SUPER_MAGIC + case STATFS_FSTYPE_EXT2: return true; default: return false; } -#else // __linux__ +#else // HAVE_STATFS + Q_UNUSED(path); return false; -#endif // __linux__ +#endif // HAVE_STATFS } const QStringList DB::FastDir::entryList() const { return m_sortedList; } QStringList DB::FastDir::sortFileList(const StringSet &files) const { QStringList answer; StringSet tmp(files); for ( const QString &fileName : m_sortedList ) { if ( tmp.contains( fileName ) ) { answer << fileName; tmp.remove( fileName ); } else if ( tmp.contains( m_path + fileName ) ) { answer << m_path + fileName; tmp.remove( m_path + fileName ); } } if ( tmp.count() > 0 ) { qCDebug(FastDirLog) << "Files left over after sorting on " << m_path; for ( const QString &fileName : tmp ) { qCDebug(FastDirLog) << fileName; answer << fileName; } } return answer; } QStringList DB::FastDir::sortFileList(const QStringList &files) const { StringSet tmp; for ( const QString &fileName : files ) { tmp << fileName; } return sortFileList(tmp); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileNameList.cpp b/DB/FileNameList.cpp index 6071f90a..902ff2cd 100644 --- a/DB/FileNameList.cpp +++ b/DB/FileNameList.cpp @@ -1,54 +1,60 @@ /* 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 . */ #include "FileNameList.h" DB::FileNameList::FileNameList(const QList& other ) { QList::operator=(other); } +DB::FileNameList::FileNameList(const QStringList &files) +{ + for (const QString& file: files) + append(DB::FileName::fromAbsolutePath(file)); +} + QStringList DB::FileNameList::toStringList(DB::PathType type) const { QStringList res; for (const DB::FileName& fileName : *this) { if ( type == DB::RelativeToImageRoot ) res.append( fileName.relative() ); else res.append( fileName.absolute()); } return res; } DB::FileNameList &DB::FileNameList::operator <<(const DB::FileName & fileName) { QList::operator<<(fileName); return *this; } DB::FileNameList DB::FileNameList::reversed() const { FileNameList res; for (const FileName& fileName : *this) { res.prepend(fileName); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileNameList.h b/DB/FileNameList.h index 310690b2..520b8593 100644 --- a/DB/FileNameList.h +++ b/DB/FileNameList.h @@ -1,44 +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& ); + /** + * @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/ImageScout.cpp b/DB/ImageScout.cpp index 072b19ba..34313ec8 100644 --- a/DB/ImageScout.cpp +++ b/DB/ImageScout.cpp @@ -1,275 +1,271 @@ /* Copyright (C) 2018 Robert Krawitz 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 "ImageScout.h" #include "Logging.h" #include #include #include #include #include extern "C" { #include #include } using namespace DB; namespace { constexpr int DEFAULT_SCOUT_BUFFER_SIZE = 1048576; // *sizeof(int) bytes // We might want this to be bytes rather than images. constexpr int DEFAULT_MAX_SEEKAHEAD_IMAGES = 10; constexpr int SEEKAHEAD_WAIT_MS = 10; // 10 milliseconds, and retry constexpr int TERMINATION_WAIT_MS = 10; // 10 milliseconds, and retry } // 1048576 with a single scout thread empirically yields best performance // on a Seagate 2TB 2.5" disk, sustaining throughput in the range of // 95-100 MB/sec with 100-110 IO/sec on large files. This is close to what // would be expected. A SATA SSD (Crucial MX300) is much less sensitive to // I/O size and scout thread, achieving about 340 MB/sec with high CPU // utilization. class DB::ImageScoutThread :public QThread { friend class DB::ImageScout; public: ImageScoutThread( ImageScoutQueue &, QMutex *, QAtomicInt &count, - QAtomicInt &preloadCount, QAtomicInt &skippedCount, - int index ); + QAtomicInt &preloadCount, QAtomicInt &skippedCount ); protected: virtual void run(); void setBufSize(int); int getBufSize(); void setMaxSeekAhead(int); int getMaxSeekAhead(); void setReadLimit(int); int getReadLimit(); private: void doRun(char *); ImageScoutQueue& m_queue; QMutex *m_mutex; QAtomicInt& m_loadedCount; QAtomicInt& m_preloadedCount; QAtomicInt& m_skippedCount; int m_scoutBufSize; int m_maxSeekAhead; int m_readLimit; - int m_index; bool m_isStarted; }; ImageScoutThread::ImageScoutThread( ImageScoutQueue &queue, QMutex *mutex, QAtomicInt &count, QAtomicInt &preloadedCount, - QAtomicInt &skippedCount, int index ) + QAtomicInt &skippedCount ) : m_queue(queue), m_mutex(mutex), m_loadedCount(count), m_preloadedCount(preloadedCount), m_skippedCount(skippedCount), m_scoutBufSize(DEFAULT_SCOUT_BUFFER_SIZE), m_maxSeekAhead(DEFAULT_MAX_SEEKAHEAD_IMAGES), m_readLimit(-1), - m_index(index), m_isStarted(false) { } void ImageScoutThread::doRun(char *tmpBuf) { while ( !isInterruptionRequested() ) { QMutexLocker locker(m_mutex); if ( m_queue.isEmpty() ) { return; } DB::FileName fileName = m_queue.dequeue(); locker.unlock(); // If we're behind the reader, move along m_preloadedCount++; if ( m_loadedCount.load() >= m_preloadedCount.load() ) { m_skippedCount++; continue; } else { // Don't get too far ahead of the loader, or we just waste memory // TODO: wait on something rather than polling while (m_preloadedCount.load() >= m_loadedCount.load() + m_maxSeekAhead && ! isInterruptionRequested()) { QThread::msleep(SEEKAHEAD_WAIT_MS); } // qCDebug(DBImageScoutLog) << ">>>>>Scout: preload" << m_preloadedCount.load() << "load" << m_loadedCount.load() << fileName.relative(); } int inputFD = open( QFile::encodeName( fileName.absolute()).constData(), O_RDONLY ); int bytesRead = 0; if ( inputFD >= 0 ) { while ( read( inputFD, tmpBuf, m_scoutBufSize ) && ( m_readLimit < 0 || ( (bytesRead += m_scoutBufSize) < m_readLimit ) ) && ! isInterruptionRequested() ) { } (void) close( inputFD ); } } } void ImageScoutThread::setBufSize(int bufSize) { if ( ! m_isStarted ) m_scoutBufSize = bufSize; } int ImageScoutThread::getBufSize() { return m_scoutBufSize; } void ImageScoutThread::setMaxSeekAhead(int maxSeekAhead) { if ( ! m_isStarted ) m_maxSeekAhead = maxSeekAhead; } int ImageScoutThread::getMaxSeekAhead() { return m_maxSeekAhead; } void ImageScoutThread::setReadLimit(int readLimit) { if ( ! m_isStarted ) m_readLimit = readLimit; } int ImageScoutThread::getReadLimit() { return m_readLimit; } void ImageScoutThread::run() { m_isStarted = true; char *tmpBuf = new char[m_scoutBufSize]; doRun( tmpBuf ); delete[] tmpBuf; } ImageScout::ImageScout(ImageScoutQueue &images, QAtomicInt &count, int threads) : m_preloadedCount(0), m_skippedCount(0), m_isStarted(false), m_scoutBufSize(DEFAULT_SCOUT_BUFFER_SIZE), m_maxSeekAhead(DEFAULT_MAX_SEEKAHEAD_IMAGES), m_readLimit(-1) { if (threads > 0) { for (int i = 0; i < threads; i++) { ImageScoutThread *t = new ImageScoutThread( images, threads > 1 ? &m_mutex : nullptr, count, m_preloadedCount, - m_skippedCount, - i); + m_skippedCount ); m_scoutList.append( t ); } } } ImageScout::~ImageScout() { if ( m_scoutList.count() > 0 ) { for ( QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it ) { if (m_isStarted) { if ( ! (*it)->isFinished() ) { (*it)->requestInterruption(); while ( ! (*it)->isFinished() ) QThread::msleep(TERMINATION_WAIT_MS); } } delete (*it); } } qCDebug(DBImageScoutLog) << "Total files:" << m_preloadedCount << "skipped" << m_skippedCount; } void ImageScout::start() { // Yes, there's a race condition here between isStartd and setting // the buf size or seek ahead...but this isn't a hot code path! if ( ! m_isStarted && m_scoutList.count() > 0 ) { m_isStarted = true; for ( QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it ) { (*it)->start(); } } } void ImageScout::setBufSize(int bufSize) { if ( ! m_isStarted && bufSize > 0 ) { m_scoutBufSize = bufSize; for ( QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it ) { (*it)->setBufSize( m_scoutBufSize ); } } } int ImageScout::getBufSize() { return m_scoutBufSize; } void ImageScout::setMaxSeekAhead(int maxSeekAhead) { if ( ! m_isStarted && maxSeekAhead > 0 ) { m_maxSeekAhead = maxSeekAhead; for ( QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it ) { (*it)->setMaxSeekAhead( m_maxSeekAhead ); } } } int ImageScout::getMaxSeekAhead() { return m_maxSeekAhead; } void ImageScout::setReadLimit(int readLimit) { if ( ! m_isStarted && readLimit > 0 ) { m_readLimit = readLimit; for ( QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it ) { (*it)->setReadLimit( m_readLimit ); } } } int ImageScout::getReadLimit() { return m_readLimit; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/OptimizedFileList.cpp b/DB/OptimizedFileList.cpp index c3fce97d..207677fd 100644 --- a/DB/OptimizedFileList.cpp +++ b/DB/OptimizedFileList.cpp @@ -1,158 +1,126 @@ /* Copyright (C) 2018 Robert Krawitz 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 "FastDir.h" +#include "Logging.h" #include "OptimizedFileList.h" extern "C" { #include +#include +#include #include - -// Note: on FreeBSD, __linux__ is also defined -#ifdef __BSD__ -# include -# include -#else -#ifdef __linux__ -# include -#endif // __linux__ -#endif } -#include #include +#include +#include DB::OptimizedFileList::OptimizedFileList(const QStringList &files) : m_fileList(files), m_haveOptimizedFiles(false) { optimizeFiles(); } DB::OptimizedFileList::OptimizedFileList(const DB::FileNameList &files) : m_fileList(files.toStringList(DB::AbsolutePath)), m_haveOptimizedFiles(false) { optimizeFiles(); } QString DB::OptimizedFileList::getDirName(const QString& path) { static const QString pathSep(QString::fromLatin1("/")); int lastChar = path.lastIndexOf(pathSep); if (lastChar <= 0) return QString::fromLatin1("./"); else return path.left(lastChar + 1); } void DB::OptimizedFileList::optimizeFiles() const { if ( m_haveOptimizedFiles ) return; DirMap dirMap; QStringList dirList; // Map files to directories - for ( const QString fileName : m_fileList ) { + for ( const QString &fileName : m_fileList ) { QString dir = getDirName(fileName); if (! dirMap.contains( dir ) ) { StringSet newDir; dirMap.insert(dir, newDir); dirList << dir; } dirMap[ dir ] << fileName; } - struct statfs statbuf; - for ( QString dirName : dirList ) { + struct stat statbuf; + for ( const QString &dirName : dirList ) { const StringSet &files(dirMap[dirName]); FastDir dir(dirName); QStringList sortedList = dir.sortFileList(files); QString fsName( QString::fromLatin1( "NULLFS" ) ); - if ( statfs( QByteArray(QFile::encodeName(dirName)).constData(), &statbuf ) == 0 ) { + if ( stat( QByteArray(QFile::encodeName(dirName)).constData(), &statbuf ) == 0 ) { QCryptographicHash md5calculator(QCryptographicHash::Md5); - QByteArray md5Buffer((const char *) &(statbuf.f_fsid), sizeof(statbuf.f_fsid)); + QByteArray md5Buffer((const char *) &(statbuf.st_dev), sizeof(statbuf.st_dev)); md5calculator.addData(md5Buffer); fsName = QString::fromLatin1(md5calculator.result().toHex()); } if ( ! m_fsMap.contains( fsName ) ) { QStringList newList; m_fsMap.insert(fsName, newList); } m_fsMap[ fsName ] += sortedList; } FSMap tmpFsMap(m_fsMap); while ( tmpFsMap.size() > 1 ) { QStringList filesystemsToRemove; for (FSMap::iterator it = tmpFsMap.begin(); it != tmpFsMap.end(); ++it) { if ( it.value().length() > 0 ) { m_optimizedList.append(it.value().takeFirst()); } else { filesystemsToRemove << it.key(); } } - for (QString fs : filesystemsToRemove) { + for (const QString &fs : filesystemsToRemove) { tmpFsMap.remove( fs ); } } if ( tmpFsMap.size() > 0 ) { QStringList &remainder(tmpFsMap.last()); m_optimizedList += remainder; } + // for (QStringList::iterator it = m_optimizedList.begin(); it != m_optimizedList.end(); ++it) { + // qDebug() << *it; + // } m_haveOptimizedFiles = true; } -const QStringList DB::OptimizedFileList::optimizedFiles() const +QStringList DB::OptimizedFileList::optimizedFiles() const { return m_optimizedList; } -DB::FileNameList DB::OptimizedFileList::dbListFromStrings(const QStringList &files) -{ - DB::FileNameList answer; - for (QString fileName : files) - answer << DB::FileName::fromAbsolutePath(fileName); - return answer; -} - -const DB::FileNameList DB::OptimizedFileList::optimizedDbFiles() const -{ - return dbListFromStrings(m_optimizedList); -} - -const QStringList DB::OptimizedFileList::getFilesystemIDs() const +DB::FileNameList DB::OptimizedFileList::optimizedDbFiles() const { - return m_fsMap.keys(); -} - -const QStringList DB::OptimizedFileList::getFilesByFilesystem(const QString &id) const -{ - if ( m_fsMap.contains( id ) ) - return m_fsMap[id]; - else - return QStringList(); -} - -const DB::FileNameList DB::OptimizedFileList::getDbFilesByFilesystem(const QString &id) const -{ - if ( m_fsMap.contains( id ) ) - return dbListFromStrings(m_fsMap[id]); - else - return DB::FileNameList(); + return FileNameList(m_optimizedList); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/OptimizedFileList.h b/DB/OptimizedFileList.h index eca45db8..cc149510 100644 --- a/DB/OptimizedFileList.h +++ b/DB/OptimizedFileList.h @@ -1,65 +1,63 @@ /* Copyright (C) 2018 Robert Krawitz 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 OPTIMIZEDFILELIST_H #define OPTIMIZEDFILELIST_H #include #include #include #include namespace DB { typedef QSet StringSet; typedef QMap DirMap; // Key is MD5 hash of the (opaque) contents of f_fsid typedef QMap FSMap; /** * Provide a list of files optimized by filesystem. * File names are interleaved across all filesystems * with files belonging to them. + * + * In other words, you can put in a list of files, and get + * back a list that is optimized for read performance. */ class OptimizedFileList { public: explicit OptimizedFileList(const DB::FileNameList &files); explicit OptimizedFileList(const QStringList &files); - const QStringList optimizedFiles() const; - const DB::FileNameList optimizedDbFiles() const; - /* Opaque filesystem IDs */ - const QStringList getFilesystemIDs() const; - const QStringList getFilesByFilesystem(const QString &id) const; - const DB::FileNameList getDbFilesByFilesystem(const QString &id) const; + QStringList optimizedFiles() const; + DB::FileNameList optimizedDbFiles() const; private: OptimizedFileList(); void optimizeFiles() const; const QStringList m_fileList; mutable QStringList m_optimizedList; mutable bool m_haveOptimizedFiles; mutable FSMap m_fsMap; - static DB::FileNameList dbListFromStrings(const QStringList &files); static QString getDirName(const QString &); }; } #endif /* OPTIMIZEDFILELIST_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/Database.cpp b/Exif/Database.cpp index fd2a3a13..498a01f5 100644 --- a/Exif/Database.cpp +++ b/Exif/Database.cpp @@ -1,637 +1,684 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Database.h" #include "Logging.h" #include "DB/ImageDB.h" #include "Exif/DatabaseElement.h" #include "MainWindow/Window.h" #include "Settings/SettingsData.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Exif; namespace { // schema version; bump it up whenever the database schema changes constexpr int DB_VERSION = 3; const Database::ElementList elements(int since=0) { static Database::ElementList elms; static int sinceDBVersion[DB_VERSION]{}; if ( elms.count() == 0 ) { elms.append( new RationalExifElement( "Exif.Photo.FocalLength" ) ); elms.append( new RationalExifElement( "Exif.Photo.ExposureTime" ) ); elms.append( new RationalExifElement( "Exif.Photo.ApertureValue" ) ); elms.append( new RationalExifElement( "Exif.Photo.FNumber" ) ); //elms.append( new RationalExifElement( "Exif.Photo.FlashEnergy" ) ); elms.append( new IntExifElement( "Exif.Photo.Flash" ) ); elms.append( new IntExifElement( "Exif.Photo.Contrast" ) ); elms.append( new IntExifElement( "Exif.Photo.Sharpness" ) ); elms.append( new IntExifElement( "Exif.Photo.Saturation" ) ); elms.append( new IntExifElement( "Exif.Image.Orientation" ) ); elms.append( new IntExifElement( "Exif.Photo.MeteringMode" ) ); elms.append( new IntExifElement( "Exif.Photo.ISOSpeedRatings" ) ); elms.append( new IntExifElement( "Exif.Photo.ExposureProgram" ) ); elms.append( new StringExifElement( "Exif.Image.Make" ) ); elms.append( new StringExifElement( "Exif.Image.Model" ) ); // gps info has been added in database schema version 2: sinceDBVersion[1] = elms.size(); elms.append( new IntExifElement( "Exif.GPSInfo.GPSVersionID" ) ); // actually a byte value elms.append( new RationalExifElement( "Exif.GPSInfo.GPSAltitude" ) ); elms.append( new IntExifElement( "Exif.GPSInfo.GPSAltitudeRef" ) ); // actually a byte value elms.append( new StringExifElement( "Exif.GPSInfo.GPSMeasureMode" ) ); elms.append( new RationalExifElement( "Exif.GPSInfo.GPSDOP" ) ); elms.append( new RationalExifElement( "Exif.GPSInfo.GPSImgDirection" ) ); elms.append( new RationalExifElement( "Exif.GPSInfo.GPSLatitude" ) ); elms.append( new StringExifElement( "Exif.GPSInfo.GPSLatitudeRef" ) ); elms.append( new RationalExifElement( "Exif.GPSInfo.GPSLongitude" ) ); elms.append( new StringExifElement( "Exif.GPSInfo.GPSLongitudeRef" ) ); elms.append( new RationalExifElement( "Exif.GPSInfo.GPSTimeStamp" ) ); // lens info has been added in database schema version 3: sinceDBVersion[2] = elms.size(); elms.append( new LensExifElement( ) ); } // query only for the newly added stuff: if (since > 0) return elms.mid(sinceDBVersion[since]); return elms; } } Exif::Database* Exif::Database::s_instance = nullptr; -static void showError( QSqlQuery& query ) +/** + * @brief show and error message for the failed \p query and disable the Exif database. + * The database is closed because at this point we can not trust the data inside. + * @param query + */ +void Database::showErrorAndFail(QSqlQuery &query) const { const QString txt = - i18n("

There was an error while executing the SQL backend command. " + i18n("

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

" "

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

" "
" "

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

" "

The error message obtained was:
%2

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

There was an error while opening the Exif search database.

" + "

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

" + "
" + "

The error message obtained was:
%1

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

The Exif search database is corrupted and has no data.

" + "

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

" + ); + const QString logMsg = QString::fromUtf8("Database open but empty!"); + showErrorAndFail(txt, logMsg); + return; + } + + const int version = DBFileVersion(); + if (m_isFailed) + return; if (version < DBVersion()) { // on the next update, we can just query the DB Version createMetadataTable(SchemaChanged); } // update schema if ( version < DBVersion() ) { QSqlQuery query( m_db ); for( const DatabaseElement *e : elements(version)) { query.prepare( QString::fromLatin1( "alter table exif add column %1") .arg( e->createString()) ); if ( !query.exec()) - showError( query ); + showErrorAndFail( query ); } } } void Exif::Database::createMetadataTable(DBSchemaChangeType change) { QSqlQuery query(m_db); query.prepare( QString::fromLatin1( "create table if not exists settings (keyword TEXT PRIMARY KEY, value TEXT) without rowid") ); if ( !query.exec()) - showError( query ); + { + showErrorAndFail( query ); + return; + } query.prepare( QString::fromLatin1( "insert or replace into settings (keyword, value) values('DBVersion','%1')").arg( Database::DBVersion())); if ( !query.exec()) - showError( query ); + { + showErrorAndFail( query ); + return; + } if (change == SchemaAndDataChanged) { query.prepare( QString::fromLatin1( "insert or replace into settings (keyword, value) values('GuaranteedDataVersion','%1')").arg( Database::DBVersion())); if ( !query.exec()) - showError( query ); + showErrorAndFail( query ); } } bool Exif::Database::add( const DB::FileName& fileName ) { if ( !isUsable() ) return false; try { Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data()); Q_ASSERT(image.get() != nullptr); image->readMetadata(); Exiv2::ExifData &exifData = image->exifData(); return insert( fileName, exifData ); } catch (...) { qCWarning(ExifLog, "Error while reading exif information from %s", qPrintable(fileName.absolute()) ); return false; } } bool Exif::Database::add( DB::FileInfo& fileInfo ) { if ( !isUsable() ) return false; return insert( fileInfo.getFileName(), fileInfo.getExifData() ); } bool Exif::Database::add( const DB::FileNameList& list ) { if ( !isUsable() ) return false; QList map; Q_FOREACH(const DB::FileName& fileName, list) { try { Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data()); Q_ASSERT(image.get() != nullptr); image->readMetadata(); map << DBExifInfo(fileName, image->exifData()); } catch (...) { qWarning("Error while reading exif information from %s", qPrintable(fileName.absolute()) ); } } insert(map); return true; } void Exif::Database::remove( const DB::FileName& fileName ) { if ( !isUsable() ) return; QSqlQuery query( m_db); query.prepare( QString::fromLatin1( "DELETE FROM exif WHERE fileName=?" )); query.bindValue( 0, fileName.absolute() ); if ( !query.exec() ) - showError( query ); + showErrorAndFail( query ); } void Exif::Database::remove( const DB::FileNameList& list ) { if ( !isUsable() ) return; m_db.transaction(); QSqlQuery query( m_db); query.prepare( QString::fromLatin1( "DELETE FROM exif WHERE fileName=?" )); Q_FOREACH(const DB::FileName& fileName, list) { query.bindValue( 0, fileName.absolute() ); if ( !query.exec() ) { m_db.rollback(); - showError( query ); + showErrorAndFail( query ); return; } } m_db.commit(); } QSqlQuery *Exif::Database::getInsertQuery() { if ( !isUsable() ) - return NULL; + return nullptr; if ( m_insertTransaction ) return m_insertTransaction; if (m_queryString.isEmpty()) { QStringList formalList; Database::ElementList elms = elements(); for( const DatabaseElement *e : elms ) { formalList.append( e->queryString() ); } m_queryString = QString::fromLatin1( "INSERT OR REPLACE into exif values (?, %1) " ).arg( formalList.join( QString::fromLatin1( ", " ) ) ); } QSqlQuery *query = new QSqlQuery(m_db); if (query) query->prepare( m_queryString ); return query; } void Exif::Database::concludeInsertQuery( QSqlQuery *query ) { if ( m_insertTransaction ) return; m_db.commit(); delete query; } bool Exif::Database::startInsertTransaction() { Q_ASSERT(m_insertTransaction == nullptr); m_insertTransaction = getInsertQuery(); m_db.transaction(); return ( m_insertTransaction != nullptr ); } bool Exif::Database::commitInsertTransaction() { if (m_insertTransaction) { m_db.commit(); delete m_insertTransaction; - m_insertTransaction = NULL; + m_insertTransaction = nullptr; } else qCWarning(ExifLog, "Trying to commit transaction, but no transaction is active!"); return true; } bool Exif::Database::abortInsertTransaction() { if (m_insertTransaction) { m_db.rollback(); delete m_insertTransaction; - m_insertTransaction = NULL; + m_insertTransaction = nullptr; } else qCWarning(ExifLog, "Trying to abort transaction, but no transaction is active!"); return true; } bool Exif::Database::insert(const DB::FileName& filename, Exiv2::ExifData data ) { if ( !isUsable() ) return false; QSqlQuery *query = getInsertQuery(); query->bindValue( 0, filename.absolute() ); int i = 1; for( const DatabaseElement *e : elements() ) { query->bindValue( i++, e->valueFromExif(data)); } bool status = query->exec(); if ( !status ) - showError( *query ); + showErrorAndFail( *query ); concludeInsertQuery( query ); return status; } bool Exif::Database::insert(QList map ) { if ( !isUsable() ) return false; QSqlQuery *query = getInsertQuery(); // not a const reference because DatabaseElement::valueFromExif uses operator[] on the exif datum Q_FOREACH ( DBExifInfo elt, map ) { query->bindValue( 0, elt.first.absolute() ); int i = 1; for( const DatabaseElement *e : elements() ) { query->bindValue( i++, e->valueFromExif(elt.second)); } if ( !query->exec() ) { - showError( *query ); + showErrorAndFail( *query ); } } concludeInsertQuery( query ); return true; } Exif::Database* Exif::Database::instance() { if ( !s_instance ) { + qCInfo(ExifLog) << "initializing Exif database..."; s_instance = new Exif::Database(); s_instance->init(); } return s_instance; } void Exif::Database::deleteInstance() { delete s_instance; s_instance = nullptr; } bool Exif::Database::isAvailable() { #ifdef QT_NO_SQL return false; #else return QSqlDatabase::isDriverAvailable( QString::fromLatin1( "QSQLITE" ) ); #endif } int Exif::Database::DBFileVersion() const { // previous to KPA 4.6, there was no metadata table: if ( !m_db.tables().contains( QString::fromLatin1("settings")) ) return 1; QSqlQuery query( QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'DBVersion'"), m_db ); if ( !query.exec() ) - showError( query ); + showErrorAndFail( query ); if (query.first()) { return query.value(0).toInt(); } return 0; } int Exif::Database::DBFileVersionGuaranteed() const { // previous to KPA 4.6, there was no metadata table: if ( !m_db.tables().contains( QString::fromLatin1("settings")) ) return 0; QSqlQuery query( QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'GuaranteedDataVersion'"), m_db ); if ( !query.exec() ) - showError( query ); + showErrorAndFail( query ); if (query.first()) { return query.value(0).toInt(); } return 0; } constexpr int Exif::Database::DBVersion() { return DB_VERSION; } bool Exif::Database::isUsable() const { return (isAvailable() && isOpen()); } QString Exif::Database::exifDBFile() { return ::Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("/exif-info.db"); } bool Exif::Database::readFields( const DB::FileName& fileName, ElementList &fields) const { if ( !isUsable() ) return false; bool foundIt = false; QStringList fieldList; for( const DatabaseElement *e : fields ) { fieldList.append( e->columnName() ); } QSqlQuery query( m_db ); // the query returns a single value, so we don't need the overhead for random access: query.setForwardOnly( true ); query.prepare( QString::fromLatin1( "select %1 from exif where filename=?") .arg( fieldList.join( QString::fromLatin1(", "))) ); query.bindValue( 0, fileName.absolute() ); if ( !query.exec() ) { - showError( query ); + showErrorAndFail( query ); } if ( query.next() ) { // file in exif db -> write back results int i=0; for( DatabaseElement *e : fields ) { e->setValue( query.value(i++) ); } foundIt = true; } return foundIt; } DB::FileNameSet Exif::Database::filesMatchingQuery( const QString& queryStr ) const { if ( !isUsable() ) return DB::FileNameSet(); DB::FileNameSet result; QSqlQuery query( queryStr, m_db ); if ( !query.exec() ) - showError( query ); + showErrorAndFail( query ); else { if ( m_doUTF8Conversion ) while ( query.next() ) result.insert( DB::FileName::fromAbsolutePath( QString::fromUtf8( query.value(0).toByteArray() ) ) ); else while ( query.next() ) result.insert( DB::FileName::fromAbsolutePath( query.value(0).toString() ) ); } return result; } QList< QPair > Exif::Database::cameras() const { QList< QPair > result; if ( !isUsable() ) return result; QSqlQuery query( QString::fromLatin1("SELECT DISTINCT Exif_Image_Make, Exif_Image_Model FROM exif"), m_db ); if ( !query.exec() ) { - showError( query ); + showErrorAndFail( query ); } else { while ( query.next() ) { QString make = query.value(0).toString(); QString model = query.value(1).toString(); if ( !make.isEmpty() && !model.isEmpty() ) result.append( qMakePair( make, model ) ); } } return result; } QList< QString > Exif::Database::lenses() const { QList< QString > result; if ( !isUsable() ) return result; QSqlQuery query( QString::fromLatin1("SELECT DISTINCT Exif_Photo_LensModel FROM exif"), m_db ); if ( !query.exec() ) { - showError( query ); + showErrorAndFail( query ); } else { while ( query.next() ) { QString lens = query.value(0).toString(); if ( !lens.isEmpty() ) result.append( lens ); } } return result; } void Exif::Database::init() { if ( !isAvailable() ) return; - m_insertTransaction = NULL; + m_isFailed = false; + m_insertTransaction = nullptr; bool dbExists = QFile::exists( exifDBFile() ); openDatabase(); if ( !isOpen() ) return; if ( !dbExists ) populateDatabase(); else updateDatabase(); } void Exif::Database::recreate() { // We create a backup of the current database in case // the user presse 'cancel' or there is any error. In that case // we want to go back to the original DB. const QString origBackup = exifDBFile() + QLatin1String(".bak"); m_db.close(); QDir().remove(origBackup); QDir().rename(exifDBFile(), origBackup); init(); const DB::FileNameList allImages = DB::ImageDB::instance()->images(); QProgressDialog dialog; dialog.setModal(true); dialog.setLabelText(i18n("Rereading EXIF information from all images")); dialog.setMaximum(allImages.size()); // using a transaction here removes a *huge* overhead on the insert statements - m_db.transaction(); + startInsertTransaction(); int i = 0; for (const DB::FileName& fileName : allImages) { const DB::ImageInfoPtr info = fileName.info(); dialog.setValue(i++); if (info->mediaType() == DB::Image) { add(fileName); } if ( i % 10 ) qApp->processEvents(); if (dialog.wasCanceled()) break; } // PENDING(blackie) We should count the amount of files that did not succeeded and warn the user. if (dialog.wasCanceled()) { - m_db.rollback(); + abortInsertTransaction(); m_db.close(); QDir().remove(exifDBFile()); QDir().rename(origBackup, exifDBFile()); init(); } else { - m_db.commit(); + commitInsertTransaction(); QDir().remove(origBackup); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/Database.h b/Exif/Database.h index 865c224a..e1f71f91 100644 --- a/Exif/Database.h +++ b/Exif/Database.h @@ -1,138 +1,141 @@ /* 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 EXIFDATABASE_H #define EXIFDATABASE_H #include #include #include #include #include #include namespace Exiv2 { class ExifData; } typedef QPair Rational; typedef QList RationalList; typedef QPair DBExifInfo; namespace Exif { class DatabaseElement; // ============================================================================ // IMPORTANT IMPORTANT IMPORTANT IMPORTANT IMPORTANT IMPORTANT IMPORTANT // ============================================================================ // // It is the resposibility of the methods in here to bail out in case database // support is not available ( !isAvailable() ). This is to simplify client code. class Database { public: typedef QList ElementList; typedef QPair Camera; typedef QList CameraList; typedef QString Lens; typedef QList LensList; static Database* instance(); static void deleteInstance(); static bool isAvailable(); /** * @brief DBVersion is the exif search database schema version currently supported by KPhotoAlbum. * @return the Exif Database version */ static constexpr int DBVersion(); bool isOpen() const; bool isUsable() const; /** * @brief DBFileVersion is the database schema version used in the exif-info.db file. * @return the database schema version used by the database file, or 0 on error. */ int DBFileVersion() const; /** * @brief DBFileVersionGuaranteed reflects DBVersion of the last time the exif db has been built. * It is just like the DBFileVersion() but concerning the data. * The schema version is automatically updated to a newer schema, but normally the * data in the exif database is not. * In this situation, only newly added pictures are populated with the new fields, whereas * existing pictures have empty values. * However, once the user rebuilds the exif database, we can guarantee all entries in the * database to conform to the new schema, and DBFileVersionGuaranteed() will be updated to the new value. * @return 0 <= DBFileVersionGuaranteed() <= DBFileVersion() */ int DBFileVersionGuaranteed() const; /** * @brief add a file and its exif data to the database. * If the file already exists in the database, the new data replaces the existing data. * @param fileInfo the file * @return */ bool add( DB::FileInfo& fileInfo ); bool add( const DB::FileName& fileName ); bool add( const DB::FileNameList& list ); void remove( const DB::FileName& fileName ); void remove( const DB::FileNameList& list ); /** * @brief readFields searches the exif database for a given file and fills the element list with values. * If the query fails or has no result, the ElementList is not changed. * @param fileName * @param fields a list of the DatabaseElements that you want to read. * @return true, if the fileName is found in the database, false otherwise. */ bool readFields( const DB::FileName& fileName, ElementList &fields) const; DB::FileNameSet filesMatchingQuery( const QString& query ) const; CameraList cameras() const; LensList lenses() const; void recreate(); bool startInsertTransaction(); bool commitInsertTransaction(); bool abortInsertTransaction(); protected: enum DBSchemaChangeType { SchemaChanged, SchemaAndDataChanged }; static QString exifDBFile(); void openDatabase(); void populateDatabase(); void updateDatabase(); void createMetadataTable(DBSchemaChangeType change); static QString connectionName(); bool insert( const DB::FileName& filename, Exiv2::ExifData ); bool insert( QList ); private: + void showErrorAndFail( QSqlQuery &query ) const; + void showErrorAndFail(const QString &errorMessage , const QString &technicalInfo) const; bool m_isOpen; bool m_doUTF8Conversion; + mutable bool m_isFailed; Database(); ~Database(); void init(); QSqlQuery *getInsertQuery(); void concludeInsertQuery(QSqlQuery *); static Database* s_instance; QString m_queryString; QSqlDatabase m_db; QSqlQuery *m_insertTransaction; }; } #endif /* EXIFDATABASE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/DatabaseElement.cpp b/Exif/DatabaseElement.cpp index b0d1a7ec..b1c10077 100644 --- a/Exif/DatabaseElement.cpp +++ b/Exif/DatabaseElement.cpp @@ -1,252 +1,250 @@ /* 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 "DatabaseElement.h" #include "Logging.h" #include #include static QString replaceDotWithUnderscore( const char* cstr ) { QString str( QString::fromLatin1( cstr ) ); return str.replace( QString::fromLatin1( "." ), QString::fromLatin1( "_" ) ); } Exif::DatabaseElement::DatabaseElement() : m_value() { } QVariant Exif::DatabaseElement::value() const { return m_value; } void Exif::DatabaseElement::setValue( QVariant val ) { m_value = val; } Exif::StringExifElement::StringExifElement( const char* tag ) : m_tag( tag ) { } QString Exif::StringExifElement::columnName() const { return replaceDotWithUnderscore( m_tag ); } QString Exif::StringExifElement::createString() const { return QString::fromLatin1( "%1 string" ).arg( replaceDotWithUnderscore( m_tag ) ); } QString Exif::StringExifElement::queryString() const { return QString::fromLatin1( "?" ); } QVariant Exif::StringExifElement::valueFromExif(Exiv2::ExifData &data) const { return QVariant{ QLatin1String(data[m_tag].toString().c_str() ) }; } Exif::IntExifElement::IntExifElement( const char* tag ) : m_tag( tag ) { } QString Exif::IntExifElement::columnName() const { return replaceDotWithUnderscore( m_tag ); } QString Exif::IntExifElement::createString() const { return QString::fromLatin1( "%1 int" ).arg( replaceDotWithUnderscore( m_tag ) ); } QString Exif::IntExifElement::queryString() const { return QString::fromLatin1( "?" ); } QVariant Exif::IntExifElement::valueFromExif(Exiv2::ExifData &data) const { if (data[m_tag].count() > 0) return QVariant{ (int) data[m_tag].toLong() }; else return QVariant{ (int) 0 }; } Exif::RationalExifElement::RationalExifElement( const char* tag ) : m_tag( tag ) { } QString Exif::RationalExifElement::columnName() const { return replaceDotWithUnderscore( m_tag ); } QString Exif::RationalExifElement::createString() const { return QString::fromLatin1( "%1 float" ).arg( replaceDotWithUnderscore( m_tag ) ); } QString Exif::RationalExifElement::queryString() const { return QString::fromLatin1( "?" ); } QVariant Exif::RationalExifElement::valueFromExif(Exiv2::ExifData &data) const { double value; Exiv2::Exifdatum &tagDatum = data[m_tag]; switch ( tagDatum.count() ) { case 0: // empty value = -1.0; break; case 1: // "normal" rational value = 1.0 * tagDatum.toRational().first / tagDatum.toRational().second; break; case 3: // GPS lat/lon data: { value = 0.0; double divisor = 1.0; - // hour / minute / second: - for (int i=0 ; i < 4 ; i++ ) + // degree / minute / second: + for (int i=0 ; i < 3 ; i++ ) { double nom = tagDatum.toRational(i).first; double denom = tagDatum.toRational(i).second; - if ( denom == 0 ) - value += 0; - else + if ( denom != 0 ) value += (nom / denom)/ divisor; divisor *= 60.0; } } break; default: // FIXME: there are at least the following other rational types: // whitepoints -> 2 components // YCbCrCoefficients -> 3 components (Coefficients for transformation from RGB to YCbCr image data. ) // chromaticities -> 6 components qCWarning(ExifLog) << "Exif rational data with " << tagDatum.count() << " components is not handled, yet!"; return QVariant{}; } return QVariant{value}; } Exif::LensExifElement::LensExifElement() : m_tag("Exif.Photo.LensModel") { } QString Exif::LensExifElement::columnName() const { return replaceDotWithUnderscore( m_tag ); } QString Exif::LensExifElement::createString() const { return QString::fromLatin1( "%1 string" ).arg( replaceDotWithUnderscore( m_tag ) ); } QString Exif::LensExifElement::queryString() const { return QString::fromLatin1( "?" ); } QVariant Exif::LensExifElement::valueFromExif(Exiv2::ExifData &data) const { QString value; bool canonHack = false; for (Exiv2::ExifData::const_iterator it = data.begin(); it != data.end(); ++it) { const QString datum = QString::fromLatin1(it->key().c_str()); // Exif.Photo.LensModel [Ascii] // Exif.Canon.LensModel [Ascii] // Exif.OlympusEq.LensModel [Ascii] if (datum.endsWith(QString::fromLatin1(".LensModel"))) { qCDebug(ExifLog) << datum << ": " << it->toString().c_str(); canonHack = false; value = QString::fromUtf8(it->toString().c_str()); // we can break here since Exif.Photo.LensModel should be bound first break; } // Exif.NikonLd3.LensIDNumber [Byte] // on Nikon cameras, this seems to provide better results than .Lens and .LensType // (i.e. it includes the lens manufacturer). if (datum.endsWith(QString::fromLatin1(".LensIDNumber"))) { // ExifDatum::print() returns the interpreted value qCDebug(ExifLog) << datum << ": " << it->print(&data).c_str(); canonHack = false; value = QString::fromUtf8(it->print(&data).c_str()); continue; } // Exif.Nikon3.LensType [Byte] // Exif.OlympusEq.LensType [Byte] // Exif.Panasonic.LensType [Ascii] // Exif.Pentax.LensType [Byte] // Exif.Samsung2.LensType [Short] if (datum.endsWith(QString::fromLatin1(".LensType"))) { // ExifDatum::print() returns the interpreted value qCDebug(ExifLog) << datum << ": " << it->print(&data).c_str(); // make sure this cannot overwrite LensIDNumber if (value.isEmpty()) { canonHack = (datum == QString::fromLatin1("Exif.CanonCs.LensType")); value = QString::fromUtf8(it->print(&data).c_str()); } } } // some canon lenses have a dummy value as LensType: if (canonHack && value == QString::fromLatin1("(65535)")) { value = QString::fromLatin1("Canon generic"); const auto datum = data.findKey(Exiv2::ExifKey("Exif.CanonCs.Lens")); if (datum != data.end()) { value += QString::fromLatin1(" "); value += QString::fromUtf8(datum->print(&data).c_str()); } } qCDebug(ExifLog) << "final lens value " << value; return QVariant{ value }; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/Export.cpp b/ImportExport/Export.cpp index b1ed89a4..17c72f24 100644 --- a/ImportExport/Export.cpp +++ b/ImportExport/Export.cpp @@ -1,410 +1,409 @@ /* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Export.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include "XMLHandler.h" using namespace ImportExport; void Export::imageExport(const DB::FileNameList& list) { ExportConfig config; if ( config.exec() == QDialog::Rejected ) return; int maxSize = -1; if ( config.mp_enforeMaxSize->isChecked() ) maxSize = config.mp_maxSize->value(); // Ask for zip file name QString zipFile = QFileDialog::getSaveFileName( nullptr, /* parent */ i18n("Save an export file"), /* caption */ QString(), /* directory */ i18n("KPhotoAlbum import files") + QString::fromLatin1( "(*.kim)" ) /*filter*/ ); if ( zipFile.isNull() ) return; bool ok; Export* exp = new Export( list, zipFile, config.mp_compress->isChecked(), maxSize, config.imageFileLocation(), QString::fromLatin1( "" ), config.mp_generateThumbnails->isChecked(), &ok); delete exp; // It will not return before done - we still need a class to connect slots etc. if ( ok ) showUsageDialog(); } // PENDING(blackie) add warning if images are to be copied into a non empty directory. ExportConfig::ExportConfig() { setWindowTitle( i18n("Export Configuration / Copy Images") ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::Help); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QWidget* top = new QWidget; mainLayout->addWidget(top); QVBoxLayout* lay1 = new QVBoxLayout( top ); // Include images QGroupBox* grp = new QGroupBox( i18n("How to Handle Images") ); lay1->addWidget( grp ); QVBoxLayout* boxLay = new QVBoxLayout( grp ); m_include = new QRadioButton( i18n("Include in .kim file"), grp ); m_manually = new QRadioButton( i18n("Do not copy files, only generate .kim file"), grp ); m_auto = new QRadioButton( i18n("Automatically copy next to .kim file"), grp ); m_link = new QRadioButton( i18n("Hard link next to .kim file"), grp ); m_symlink = new QRadioButton( i18n("Symbolic link next to .kim file"), grp ); m_auto->setChecked( true ); boxLay->addWidget( m_include ); boxLay->addWidget( m_manually ); boxLay->addWidget( m_auto ); boxLay->addWidget( m_link ); boxLay->addWidget( m_symlink ); // Compress mp_compress = new QCheckBox( i18n("Compress export file"), top ); lay1->addWidget( mp_compress ); // Generate thumbnails mp_generateThumbnails = new QCheckBox( i18n("Generate thumbnails"), top ); mp_generateThumbnails->setChecked( false ); lay1->addWidget( mp_generateThumbnails ); // Enforece max size QHBoxLayout* hlay = new QHBoxLayout; lay1->addLayout( hlay ); mp_enforeMaxSize = new QCheckBox( i18n( "Limit maximum image dimensions to: " ) ); hlay->addWidget( mp_enforeMaxSize ); mp_maxSize = new QSpinBox; mp_maxSize->setRange( 100,4000 ); hlay->addWidget( mp_maxSize ); mp_maxSize->setValue( 800 ); connect(mp_enforeMaxSize, &QCheckBox::toggled, mp_maxSize, &QSpinBox::setEnabled); mp_maxSize->setEnabled( false ); QString txt = i18n( "

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

" "

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

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

Generate thumbnail images

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

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

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

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

" "

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

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

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

" "

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

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

" ); KMessageBox::information( nullptr, txt, i18n("How to Use the Export File"), QString::fromLatin1("export_how_to_use_the_export_file") ); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/Import.cpp b/ImportExport/Import.cpp index 2d56433c..8403dae4 100644 --- a/ImportExport/Import.cpp +++ b/ImportExport/Import.cpp @@ -1,125 +1,124 @@ /* 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 "Import.h" #include #include -#include #include #include #include #include #include #include "ImportHandler.h" #include "KimFileReader.h" #include "ImportDialog.h" using namespace ImportExport; void Import::imageImport() { QUrl url = QFileDialog::getOpenFileUrl( nullptr, /*parent*/ i18n("KPhotoAlbum Export Files" ), /*caption*/ QUrl(), /* directory */ i18n("KPhotoAlbum import files") + QString::fromLatin1( "(*.kim)" ) /*filter*/ ); if ( url.isEmpty() ) return; imageImport( url ); // This instance will delete itself when done. } void Import::imageImport( const QUrl &url ) { Import* import = new Import; import->m_kimFileUrl = url; if ( !url.isLocalFile() ) import->downloadUrl(url); else import->exec(url.path()); // This instance will delete itself when done. } ImportExport::Import::Import() :m_tmp(nullptr) { } void ImportExport::Import::downloadUrl( const QUrl &url ) { m_tmp = new QTemporaryFile; m_tmp->setFileTemplate(QString::fromLatin1("XXXXXX.kim")); if ( !m_tmp->open() ) { KMessageBox::error( MainWindow::Window::theMainWindow(), i18n("Unable to create temporary file") ); delete this; return; } KIO::TransferJob* job = KIO::get( url ); connect(job, &KIO::TransferJob::result, this, &Import::downloadKimJobCompleted); connect(job, &KIO::TransferJob::data, this, &Import::data); } void ImportExport::Import::downloadKimJobCompleted( KJob* job ) { if ( job->error() ) { job->uiDelegate()->showErrorMessage(); delete this; } else { QString path = m_tmp->fileName(); m_tmp->close(); exec( path ); } } void ImportExport::Import::exec(const QString& fileName ) { ImportDialog dialog(MainWindow::Window::theMainWindow()); KimFileReader kimFileReader; if ( !kimFileReader.open( fileName ) ) { delete this; return; } bool ok = dialog.exec( &kimFileReader, m_kimFileUrl ); if ( ok ) { ImportHandler handler; handler.exec(dialog.settings(), &kimFileReader); } delete this; } void ImportExport::Import::data( KIO::Job*, const QByteArray& data ) { m_tmp->write( data ); } ImportExport::Import::~Import() { delete m_tmp; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportHandler.cpp b/ImportExport/ImportHandler.cpp index 34c6ee65..dc90cc2f 100644 --- a/ImportExport/ImportHandler.cpp +++ b/ImportExport/ImportHandler.cpp @@ -1,358 +1,357 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImportHandler.h" #include "Logging.h" #include #include #include #include #include #include #include #include -#include #include #include "Utilities/Util.h" #include "KimFileReader.h" #include "ImportSettings.h" #include "MainWindow/Window.h" #include "DB/ImageDB.h" #include "Browser/BrowserWidget.h" #include "DB/MD5Map.h" #include "DB/Category.h" #include "DB/CategoryCollection.h" #include "Utilities/UniqFilenameMapper.h" #include "kio/job.h" #include using namespace ImportExport; ImportExport::ImportHandler::ImportHandler() : m_fileMapper(nullptr), m_finishedPressed(false), m_progress(0), m_reportUnreadableFiles( true ) , m_eventLoop( new QEventLoop ) { } ImportHandler::~ImportHandler() { delete m_fileMapper; delete m_eventLoop; } bool ImportExport::ImportHandler::exec( const ImportSettings& settings, KimFileReader* kimFileReader ) { m_settings = settings; m_kimFileReader = kimFileReader; m_finishedPressed = true; delete m_fileMapper; m_fileMapper = new Utilities::UniqFilenameMapper(m_settings.destination()); bool ok; // copy images if ( m_settings.externalSource() ) { copyFromExternal(); // If none of the images were to be copied, then we flushed the loop before we got started, in that case, don't start the loop. qCDebug(ImportExportLog) << "Copying" << m_pendingCopies.count() << "files from external source..."; if ( m_pendingCopies.count() > 0 ) ok = m_eventLoop->exec(); else ok = false; } else { ok = copyFilesFromZipFile(); if ( ok ) updateDB(); } if ( m_progress ) delete m_progress; return ok; } void ImportExport::ImportHandler::copyFromExternal() { m_pendingCopies = m_settings.selectedImages(); m_totalCopied = 0; m_progress = new QProgressDialog( MainWindow::Window::theMainWindow()); m_progress->setWindowTitle(i18n("Copying Images") ); m_progress->setMinimum( 0 ); m_progress->setMaximum( 2 * m_pendingCopies.count() ); m_progress->show(); connect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages); copyNextFromExternal(); } void ImportExport::ImportHandler::copyNextFromExternal() { DB::ImageInfoPtr info = m_pendingCopies[0]; if ( isImageAlreadyInDB( info ) ) { qCDebug(ImportExportLog) << info->fileName().relative() << "is already in database."; aCopyJobCompleted(0); return; } const DB::FileName fileName = info->fileName(); bool succeeded = false; QStringList tried; // First search for images next to the .kim file // Second search for images base on the image root as specified in the .kim file QList searchUrls { m_settings.kimFile().adjusted(QUrl::RemoveFilename) , m_settings.baseURL().adjusted(QUrl::RemoveFilename) }; Q_FOREACH(const QUrl& url, searchUrls) { QUrl src (url); src.setPath(src.path() + fileName.relative() ); - std::unique_ptr statJob { KIO::stat(src, KIO::StatJob::SourceSide, 0 /* just query for existance */ ) }; + std::unique_ptr statJob { KIO::stat(src, KIO::StatJob::SourceSide, 0 /* just query for existence */ ) }; KJobWidgets::setWindow(statJob.get(), MainWindow::Window::theMainWindow()); if ( statJob->exec() ) { QUrl dest = QUrl::fromLocalFile( m_fileMapper->uniqNameFor(fileName) ); m_job = KIO::file_copy( src, dest, -1, KIO::HideProgressInfo ); connect(m_job, &KIO::FileCopyJob::result, this, &ImportHandler::aCopyJobCompleted); succeeded = true; qCDebug(ImportExportLog) << "Copying" << src << "to" << dest; break; } else tried << src.toDisplayString(); } if (!succeeded) aCopyFailed( tried ); } bool ImportExport::ImportHandler::copyFilesFromZipFile() { DB::ImageInfoList images = m_settings.selectedImages(); m_totalCopied = 0; m_progress = new QProgressDialog( MainWindow::Window::theMainWindow()); m_progress->setWindowTitle(i18n("Copying Images") ); m_progress->setMinimum( 0 ); m_progress->setMaximum( 2 * m_pendingCopies.count() ); m_progress->show(); for( DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it ) { if ( !isImageAlreadyInDB( *it ) ) { const DB::FileName fileName = (*it)->fileName(); QByteArray data = m_kimFileReader->loadImage( fileName.relative() ); if ( data.isNull() ) return false; QString newName = m_fileMapper->uniqNameFor(fileName); QFile out( newName ); if ( !out.open( QIODevice::WriteOnly ) ) { KMessageBox::error( MainWindow::Window::theMainWindow(), i18n("Error when writing image %1", newName ) ); return false; } out.write( data.constData(), data.size() ); out.close(); } qApp->processEvents(); m_progress->setValue( ++m_totalCopied ); if ( m_progress->wasCanceled() ) { return false; } } return true; } void ImportExport::ImportHandler::updateDB() { disconnect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages); m_progress->setLabelText( i18n("Updating Database") ); int len = Settings::SettingsData::instance()->imageDirectory().length(); // image directory is always a prefix of destination if ( len == m_settings.destination().length() ) len = 0; else qCDebug(ImportExportLog) << "Re-rooting of ImageInfos from " << Settings::SettingsData::instance()->imageDirectory() << " to " << m_settings.destination(); // Run though all images DB::ImageInfoList images = m_settings.selectedImages(); for( DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it ) { DB::ImageInfoPtr info = *it; if ( len != 0) { // exchange prefix: QString name = m_settings.destination() + info->fileName().absolute().mid(len); qCDebug(ImportExportLog) << info->fileName().absolute() << " -> " << name; info->setFileName( DB::FileName::fromAbsolutePath(name) ); } if ( isImageAlreadyInDB( info ) ) { qCDebug(ImportExportLog) << "Updating ImageInfo for " << info->fileName().absolute(); updateInfo( matchingInfoFromDB( info ), info ); } else { qCDebug(ImportExportLog) << "Adding ImageInfo for " << info->fileName().absolute(); addNewRecord( info ); } m_progress->setValue( ++m_totalCopied ); if ( m_progress->wasCanceled() ) break; } Browser::BrowserWidget::instance()->home(); } void ImportExport::ImportHandler::stopCopyingImages() { m_job->kill(); } void ImportExport::ImportHandler::aCopyFailed( QStringList files ) { int result = m_reportUnreadableFiles ? KMessageBox::warningYesNoCancelList( m_progress, i18n("Cannot copy from any of the following locations:"), files, QString(), KStandardGuiItem::cont(), KGuiItem( i18n("Continue without Asking") )) : KMessageBox::Yes; switch (result) { case KMessageBox::Cancel: // This might be late -- if we managed to copy some files, we will // just throw away any changes to the DB, but some new image files // might be in the image directory... m_eventLoop->exit(false); m_pendingCopies.pop_front(); break; case KMessageBox::No: m_reportUnreadableFiles = false; // fall through default: aCopyJobCompleted( 0 ); } } void ImportExport::ImportHandler::aCopyJobCompleted( KJob* job ) { qCDebug(ImportExportLog) << "CopyJob" << job << "completed."; m_pendingCopies.pop_front(); if ( job && job->error() ) { job->uiDelegate()->showErrorMessage(); m_eventLoop->exit(false); } else if ( m_pendingCopies.count() == 0 ) { updateDB(); m_eventLoop->exit(true); } else if ( m_progress->wasCanceled() ) { m_eventLoop->exit(false); } else { m_progress->setValue( ++m_totalCopied ); copyNextFromExternal(); } } bool ImportExport::ImportHandler::isImageAlreadyInDB( const DB::ImageInfoPtr& info ) { return DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum()); } DB::ImageInfoPtr ImportExport::ImportHandler::matchingInfoFromDB( const DB::ImageInfoPtr& info ) { const DB::FileName name = DB::ImageDB::instance()->md5Map()->lookup(info->MD5Sum()); return DB::ImageDB::instance()->info(name); } /** * Merge the ImageInfo data from the kim file into the existing ImageInfo. */ void ImportExport::ImportHandler::updateInfo( DB::ImageInfoPtr dbInfo, DB::ImageInfoPtr newInfo ) { if ( dbInfo->label() != newInfo->label() && m_settings.importAction(QString::fromLatin1("*Label*")) == ImportSettings::Replace ) dbInfo->setLabel( newInfo->label() ); if ( dbInfo->description().simplified() != newInfo->description().simplified() ) { if ( m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Replace ) dbInfo->setDescription( newInfo->description() ); else if ( m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Merge ) dbInfo->setDescription( dbInfo->description() + QString::fromLatin1("

") + newInfo->description() ); } if (dbInfo->angle() != newInfo->angle() && m_settings.importAction(QString::fromLatin1("*Orientation*")) == ImportSettings::Replace ) dbInfo->setAngle( newInfo->angle() ); if (dbInfo->date() != newInfo->date() && m_settings.importAction(QString::fromLatin1("*Date*")) == ImportSettings::Replace ) dbInfo->setDate( newInfo->date() ); updateCategories( newInfo, dbInfo, false ); } void ImportExport::ImportHandler::addNewRecord( DB::ImageInfoPtr info ) { const DB::FileName importName = info->fileName(); DB::ImageInfoPtr updateInfo(new DB::ImageInfo(importName, info->mediaType(), false /*don't read exif */)); updateInfo->setLabel( info->label() ); updateInfo->setDescription( info->description() ); updateInfo->setDate( info->date() ); updateInfo->setAngle( info->angle() ); updateInfo->setMD5Sum( Utilities::MD5Sum( updateInfo->fileName() ) ); DB::ImageInfoList list; list.append(updateInfo); DB::ImageDB::instance()->addImages( list ); updateCategories( info, updateInfo, true ); } void ImportExport::ImportHandler::updateCategories( DB::ImageInfoPtr XMLInfo, DB::ImageInfoPtr DBInfo, bool forceReplace ) { // Run though the categories const QList matches = m_settings.categoryMatchSetting(); for ( const CategoryMatchSetting& match : matches ) { QString XMLCategoryName = match.XMLCategoryName(); QString DBCategoryName = match.DBCategoryName(); ImportSettings::ImportAction action = m_settings.importAction(DBCategoryName); const Utilities::StringSet items = XMLInfo->itemsOfCategory(XMLCategoryName); DB::CategoryPtr DBCategoryPtr = DB::ImageDB::instance()->categoryCollection()->categoryForName( DBCategoryName ); if ( !forceReplace && action == ImportSettings::Replace ) DBInfo->setCategoryInfo( DBCategoryName, Utilities::StringSet() ); if ( action == ImportSettings::Merge || action == ImportSettings::Replace || forceReplace ) { for ( const QString& item : items ) { if (match.XMLtoDB().contains( item ) ) { DBInfo->addCategoryInfo( DBCategoryName, match.XMLtoDB()[item] ); DBCategoryPtr->addItem( match.XMLtoDB()[item] ); } } } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/TokenEditor.cpp b/MainWindow/TokenEditor.cpp index 525fd633..2508a58c 100644 --- a/MainWindow/TokenEditor.cpp +++ b/MainWindow/TokenEditor.cpp @@ -1,139 +1,138 @@ /* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "TokenEditor.h" #include #include #include #include #include #include #include #include #include -#include #include "DB/ImageDB.h" #include "DB/CategoryCollection.h" #include "DB/Category.h" #include "DB/ImageSearchInfo.h" #include "Settings/SettingsData.h" using namespace MainWindow; TokenEditor::TokenEditor( QWidget* parent ) :QDialog( parent ) { setWindowTitle( i18n( "Remove Tokens" ) ); QVBoxLayout *dialogLayout = new QVBoxLayout(this); QWidget* mainContents = new QWidget; QVBoxLayout* vlay = new QVBoxLayout( mainContents ); QLabel* label = new QLabel( i18n("Select tokens to remove from all images and videos:") ); vlay->addWidget( label ); QGridLayout* grid = new QGridLayout; vlay->addLayout( grid ); int index = 0; for ( int ch = 'A'; ch <= 'Z'; ch++, index++ ) { QChar token = QChar::fromLatin1( (char) ch ); QCheckBox* box = new QCheckBox( token ); grid->addWidget( box, index/5, index % 5 ); m_checkBoxes.append( box ); } QHBoxLayout* hlay = new QHBoxLayout; vlay->addLayout( hlay ); hlay->addStretch( 1 ); QPushButton* selectAll = new QPushButton( i18n("Select All") ); QPushButton* selectNone = new QPushButton( i18n("Select None") ); hlay->addWidget( selectAll ); hlay->addWidget( selectNone ); connect(selectAll, &QPushButton::clicked, this, &TokenEditor::selectAll); connect(selectNone, &QPushButton::clicked, this, &TokenEditor::selectNone); dialogLayout->addWidget(mainContents); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &TokenEditor::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &TokenEditor::reject); dialogLayout->addWidget(buttonBox); } void TokenEditor::show() { QStringList tokens = tokensInUse(); Q_FOREACH( QCheckBox *box, m_checkBoxes ) { box->setChecked( false ); QString txt = box->text().remove( QString::fromLatin1("&") ); box->setEnabled( tokens.contains( txt ) ); } QDialog::show(); } void TokenEditor::selectAll() { Q_FOREACH( QCheckBox *box, m_checkBoxes ) { box->setChecked( true ); } } void TokenEditor::selectNone() { Q_FOREACH( QCheckBox *box, m_checkBoxes ) { box->setChecked( false ); } } /** I would love to use Settings::optionValue, but that method does not forget about an item once it has seen it, which is really what it should do anyway, otherwise it would be way to expensive in use. */ QStringList TokenEditor::tokensInUse() { QStringList res; DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); QMap map = DB::ImageDB::instance()->classify( DB::ImageSearchInfo(), tokensCategory->name(), DB::anyMediaType ); for( QMap::Iterator it = map.begin(); it != map.end(); ++it ) { if ( it.value() > 0 ) res.append( it.key() ); } return res; } void TokenEditor::accept() { DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); Q_FOREACH( const QCheckBox *box, m_checkBoxes ) { if ( box->isChecked() && box->isEnabled() ) { QString txt = box->text().remove( QString::fromLatin1("&") ); tokensCategory->removeItem( txt ); } } QDialog::accept(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/WelcomeDialog.cpp b/MainWindow/WelcomeDialog.cpp index 8aded087..1acaad93 100644 --- a/MainWindow/WelcomeDialog.cpp +++ b/MainWindow/WelcomeDialog.cpp @@ -1,211 +1,210 @@ /* 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 "WelcomeDialog.h" #include "FeatureDialog.h" #include "Window.h" #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include using namespace MainWindow; WelcomeDialog::WelcomeDialog( QWidget* parent ) : QDialog( parent ) { QVBoxLayout* lay1 = new QVBoxLayout( this ); QHBoxLayout* lay2 = new QHBoxLayout; lay1->addLayout( lay2 ); QLabel* image = new QLabel( this ); image->setMinimumSize( QSize( 273, 204 ) ); image->setMaximumSize( QSize( 273, 204 ) ); image->setPixmap(Utilities::locateDataFile(QString::fromLatin1("pics/splash.png"))); lay2->addWidget( image ); QLabel* textLabel2 = new QLabel( this ); lay2->addWidget( textLabel2 ); textLabel2->setText( i18n( "

Welcome to KPhotoAlbum

" "

KPhotoAlbum is a powerful free tool to archive, tag and manage your photos and " "videos. It will not modify or change your precious files, it only indexes them " "and lets you easily find and manage your photos and videos.

" "

Start by showing KPhotoAlbum where your photos are by pressing on Create My Own " "Database. Select this button also if you have an existing KPhotoAlbum database " "that you want to start using again.

" "

If you feel safer first trying out KPhotoAlbum with prebuilt set of images, " "press the Load Demo button.

" ) ); textLabel2->setWordWrap( true ); QHBoxLayout* lay3 = new QHBoxLayout; lay1->addLayout( lay3 ); lay3->addStretch( 1 ); QPushButton* createSetup = new QPushButton( i18n("Create My Own Database..."), this ); lay3->addWidget( createSetup ); QPushButton* loadDemo = new QPushButton( i18n("Load Demo") ); lay3->addWidget( loadDemo ); QPushButton* checkFeatures = new QPushButton( i18n("Check My Feature Set") ); lay3->addWidget( checkFeatures ); connect(loadDemo, &QPushButton::clicked, this, &WelcomeDialog::slotLoadDemo); connect(createSetup, &QPushButton::clicked, this, &WelcomeDialog::createSetup); connect(checkFeatures, &QPushButton::clicked, this, &WelcomeDialog::checkFeatures); } void WelcomeDialog::slotLoadDemo() { // rerun KPA with "--demo" MainWindow::Window::theMainWindow()->runDemo(); // cancel the dialog (and exit this instance of KPA) reject(); } void WelcomeDialog::createSetup() { FileDialog dialog( this ); m_configFile = dialog.getFileName(); if ( !m_configFile.isNull() ) accept(); } QString WelcomeDialog::configFileName() const { return m_configFile; } FileDialog::FileDialog( QWidget* parent ) :QDialog( parent ) { QVBoxLayout *mainLayout = new QVBoxLayout (this); QLabel* label = new QLabel( i18n("

KPhotoAlbum database creation

" "

You need to show where the photos and videos are for KPhotoAlbum to " "find them. They all need to be under one root directory, for example " "/home/user/Images. In this directory you can have as many subdirectories as you " "want, KPhotoAlbum will find them all for you.

" "

Feel safe, KPhotoAlbum will not modify or edit any of your images, so you can " "simply point KPhotoAlbum to the directory where you already have all your " "images.

" "

If you have an existing KPhotoAlbum database and root directory somewhere, " "point KPhotoAlbum to that directory to start using it again.

" ), this ); label->setWordWrap( true ); mainLayout->addWidget( label ); QHBoxLayout* lay2 = new QHBoxLayout; label = new QLabel( i18n("Image/Video root directory: "), this ); lay2->addWidget( label ); m_lineEdit = new QLineEdit( this ); m_lineEdit->setText( QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) ); lay2->addWidget( m_lineEdit ); QPushButton* button = new QPushButton( QString::fromLatin1("..."), this ); button->setMaximumWidth( 30 ); lay2->addWidget( button ); connect(button, &QPushButton::clicked, this, &FileDialog::slotBrowseForDirecory); mainLayout->addLayout( lay2 ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &FileDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &FileDialog::reject); mainLayout->addWidget(buttonBox); } void FileDialog::slotBrowseForDirecory() { QString dir = QFileDialog::getExistingDirectory(this , QString(), m_lineEdit->text()); if ( ! dir.isNull() ) m_lineEdit->setText( dir ); } QString FileDialog::getFileName() { bool ok = false; QString dir; while ( !ok ) { if ( exec() == Rejected ) return QString(); dir = KShell::tildeExpand( m_lineEdit->text() ); if ( !QFileInfo( dir ).exists() ) { int create = KMessageBox::questionYesNo( this, i18n("Directory does not exist, create it?") ); if ( create == KMessageBox::Yes ) { bool ok2 = QDir().mkdir( dir ); if ( !ok2 ) { KMessageBox::sorry( this, i18n("Could not create directory %1",dir) ); } else ok = true; } } else if ( !QFileInfo( dir ).isDir() ) { KMessageBox::sorry( this, i18n("%1 exists, but is not a directory",dir) ); } else ok = true; } QString file = dir + QString::fromLatin1("/index.xml"); KConfigGroup group = KSharedConfig::openConfig()->group(QString::fromUtf8("General")); group.writeEntry( QString::fromLatin1("imageDBFile"), file ); group.sync(); return file; } void MainWindow::WelcomeDialog::checkFeatures() { if ( !FeatureDialog::hasAllFeaturesAvailable() ) { const QString msg = i18n("

KPhotoAlbum does not seem to be build with support for all its features. The following is a list " "indicating to you what you may miss:

    %1

" "

For details on how to solve this problem, please choose Help|KPhotoAlbum Feature Status " "from the menus.

", FeatureDialog::featureString() ); KMessageBox::information( this, msg, i18n("Feature Check") ); } else { KMessageBox::information( this, i18n("Congratulations: all dynamic features have been enabled."), i18n("Feature Check" ) ); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Window.cpp b/MainWindow/Window.cpp index 9b3f00c4..78c791da 100644 --- a/MainWindow/Window.cpp +++ b/MainWindow/Window.cpp @@ -1,1990 +1,1987 @@ /* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "Window.h" #include #ifdef HAVE_STDLIB_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for #if KIO_VERSION... #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HASKIPI # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HASKIPI # include #endif #include #include #include #include #include #include #include #include #include #include "AutoStackImages.h" #include "BreadcrumbViewer.h" #include "CopyPopup.h" #include "DeleteDialog.h" #include "DirtyIndicator.h" #include "DuplicateMerger/DuplicateMerger.h" #include "ExternalPopup.h" #include "FeatureDialog.h" #include "ImageCounter.h" #include "InvalidDateFinder.h" #include "Logging.h" #include "Options.h" #include "SearchBar.h" #include "SplashScreen.h" #include "StatisticsDialog.h" #include "StatusBar.h" #include "TokenEditor.h" #include "UpdateVideoThumbnail.h" #include "WelcomeDialog.h" using namespace DB; MainWindow::Window* MainWindow::Window::s_instance = nullptr; MainWindow::Window::Window( QWidget* parent ) :KXmlGuiWindow( parent ), m_annotationDialog(nullptr), m_deleteDialog( nullptr ), m_htmlDialog(nullptr), m_tokenEditor( nullptr ) { #ifdef HAVE_KGEOMAP m_positionBrowser = 0; #endif qCDebug(MainWindowLog) << "Using icon theme: " << QIcon::themeName(); qCDebug(MainWindowLog) << "Icon search paths: " << QIcon::themeSearchPaths(); QElapsedTimer timer; timer.start(); SplashScreen::instance()->message( i18n("Loading Database") ); s_instance = this; bool gotConfigFile = load(); if ( !gotConfigFile ) throw 0; qCInfo(TimingLog) << "MainWindow: Loading Database: " << timer.restart() << "ms."; SplashScreen::instance()->message( i18n("Loading Main Window") ); QWidget* top = new QWidget( this ); QVBoxLayout* lay = new QVBoxLayout( top ); lay->setSpacing(2); lay->setContentsMargins(2,2,2,2); setCentralWidget( top ); m_stack = new QStackedWidget( top ); lay->addWidget( m_stack, 1 ); m_dateBar = new DateBar::DateBarWidget( top ); lay->addWidget( m_dateBar ); m_dateBarLine = new QFrame( top ); m_dateBarLine->setFrameStyle( QFrame::HLine | QFrame::Plain ); m_dateBarLine->setLineWidth(0); m_dateBarLine->setMidLineWidth(0); QPalette pal = m_dateBarLine->palette(); pal.setColor( QPalette::WindowText, QColor("#c4c1bd") ); m_dateBarLine->setPalette( pal ); lay->addWidget( m_dateBarLine ); setHistogramVisibilty(Settings::SettingsData::instance()->showHistogram()); m_browser = new Browser::BrowserWidget( m_stack ); m_thumbnailView = new ThumbnailView::ThumbnailFacade(); m_stack->addWidget( m_browser ); m_stack->addWidget( m_thumbnailView->gui() ); m_stack->setCurrentWidget( m_browser ); m_settingsDialog = nullptr; qCInfo(TimingLog) << "MainWindow: Loading MainWindow: " << timer.restart() << "ms."; setupMenuBar(); qCInfo(TimingLog) << "MainWindow: setupMenuBar: " << timer.restart() << "ms."; createSarchBar(); qCInfo(TimingLog) << "MainWindow: createSearchBar: " << timer.restart() << "ms."; setupStatusBar(); qCInfo(TimingLog) << "MainWindow: setupStatusBar: " << timer.restart() << "ms."; // Misc m_autoSaveTimer = new QTimer( this ); connect(m_autoSaveTimer, &QTimer::timeout, this, &Window::slotAutoSave); startAutoSaveTimer(); connect(m_browser, &Browser::BrowserWidget::showingOverview, this, &Window::showBrowser); connect( m_browser, SIGNAL(pathChanged(Browser::BreadcrumbList)), m_statusBar->mp_pathIndicator, SLOT(setBreadcrumbs(Browser::BreadcrumbList)) ); connect( m_statusBar->mp_pathIndicator, SIGNAL(widenToBreadcrumb(Browser::Breadcrumb)), m_browser, SLOT(widenToBreadcrumb(Browser::Breadcrumb)) ); connect( m_browser, SIGNAL(pathChanged(Browser::BreadcrumbList)), this, SLOT(updateDateBar(Browser::BreadcrumbList)) ); connect(m_dateBar, &DateBar::DateBarWidget::dateSelected, m_thumbnailView, &ThumbnailView::ThumbnailFacade::gotoDate); connect(m_dateBar, &DateBar::DateBarWidget::toolTipInfo, this, &Window::showDateBarTip); connect( Settings::SettingsData::instance(), SIGNAL(histogramSizeChanged(QSize)), m_dateBar, SLOT(setHistogramBarSize(QSize)) ); connect( Settings::SettingsData::instance(), SIGNAL(actualThumbnailSizeChanged(int)), this, SLOT(slotThumbnailSizeChanged()) ); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeChange, this, &Window::setDateRange); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeCleared, this, &Window::clearDateRange); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::currentDateChanged, m_dateBar, &DateBar::DateBarWidget::setDate); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::showImage, this, &Window::showImage); connect( m_thumbnailView, SIGNAL(showSelection()), this, SLOT(slotView()) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::fileIdUnderCursorChanged, this, &Window::slotSetFileName); connect( DB::ImageDB::instance(), SIGNAL(totalChanged(uint)), this, SLOT(updateDateBar()) ); connect( DB::ImageDB::instance()->categoryCollection(), SIGNAL(categoryCollectionChanged()), this, SLOT(slotOptionGroupChanged()) ); connect( m_browser, SIGNAL(imageCount(uint)), m_statusBar->mp_partial, SLOT(showBrowserMatches(uint)) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, this, &Window::updateContextMenuFromSelectionSize); + checkIfMplayerIsInstalled(); + executeStartupActions(); + + qCInfo(TimingLog) << "MainWindow: executeStartupActions " << timer.restart() << "ms."; QTimer::singleShot( 0, this, SLOT(delayedInit()) ); updateContextMenuFromSelectionSize(0); // Automatically save toolbar settings setAutoSaveSettings(); - checkIfMplayerIsInstalled(); - qCInfo(TimingLog) << "MainWindow: misc setup time: " << timer.restart() << "ms."; - executeStartupActions(); - qCInfo(TimingLog) << "MainWindow: executeStartupActions " << 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* exifDB = Exif::Database::instance(); // Load the database - if ( exifDB->isAvailable() && !exifDB->isOpen() ) { - KMessageBox::sorry( this, i18n("EXIF database cannot be opened. Check that the image root directory is writable.") ); - } + 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 = Utilities::MD5Sum( fileName ); ImageInfoPtr originalInfo; if ( DB::ImageDB::instance()->md5Map()->contains( originalSum ) ) { originalInfo = DB::ImageDB::instance()->info( fileName ); } else { originalInfo = fileName.info(); } // fail silent if there is no info for the file. if (!originalInfo) return; Q_FOREACH(const DB::FileName& newFile, selected()) { newFile.info()->copyExtraData(*originalInfo, false); } DirtyIndicator::markDirty(); } void MainWindow::Window::slotReReadExifInfo() { DB::FileNameList files = selectedOnDisk(); static Exif::ReReadDialog* dialog = nullptr; if ( ! dialog ) dialog = new Exif::ReReadDialog( this ); if ( dialog->exec( files ) == QDialog::Accepted ) DirtyIndicator::markDirty(); } void MainWindow::Window::slotAutoStackImages() { const DB::FileNameList list = selected(); if (list.isEmpty()) { KMessageBox::sorry( this, i18n("No item is selected."), i18n("No Selection") ); return; } QPointer stacker = new AutoStackImages( this, list ); if ( stacker->exec() == QDialog::Accepted ) showThumbNails(); delete stacker; } /** * In thumbnail mode, return a list of files that are selected. * Otherwise, return all images in the current scope/context. */ DB::FileNameList MainWindow::Window::selected( ThumbnailView::SelectionMode mode) const { if ( m_thumbnailView->gui() == m_stack->currentWidget() ) return m_thumbnailView->selection(mode); else // return all images in the current scope (parameter false: include images not on disk) return DB::ImageDB::instance()->currentScope(false); } void MainWindow::Window::slotViewNewWindow() { slotView( false, false ); } /* * Returns a list of files that are both selected and on disk. If there are no * selected files, returns all files form current context that are on disk. * Note: On some setups (NFS), this can be a very time-consuming method! * */ DB::FileNameList MainWindow::Window::selectedOnDisk() { const DB::FileNameList list = selected(ThumbnailView::NoExpandCollapsedStacks); DB::FileNameList listOnDisk; Q_FOREACH(const DB::FileName& fileName, list) { if (DB::ImageInfo::imageOnDisk(fileName)) listOnDisk.append(fileName); } return listOnDisk; } void MainWindow::Window::slotView( bool reuse, bool slideShow, bool random ) { launchViewer(selected(ThumbnailView::NoExpandCollapsedStacks), reuse, slideShow, random ); } void MainWindow::Window::launchViewer(const DB::FileNameList& inputMediaList, bool reuse, bool slideShow, bool random) { DB::FileNameList mediaList = inputMediaList; int seek = -1; if (mediaList.isEmpty()) { mediaList = m_thumbnailView->imageList( ThumbnailView::ViewOrder ); } else if (mediaList.size() == 1) { // we fake it so it appears the user has selected all images // and magically scrolls to the originally selected one const DB::FileName first = mediaList.at(0); mediaList = m_thumbnailView->imageList( ThumbnailView::ViewOrder ); seek = mediaList.indexOf(first); } if (mediaList.isEmpty()) mediaList = DB::ImageDB::instance()->currentScope( false ); if (mediaList.isEmpty()) { KMessageBox::sorry( this, i18n("There are no images to be shown.") ); return; } if (random) { mediaList = DB::FileNameList(Utilities::shuffleList(mediaList)); } Viewer::ViewerWidget* viewer; if ( reuse && Viewer::ViewerWidget::latest() ) { viewer = Viewer::ViewerWidget::latest(); viewer->raise(); viewer->activateWindow(); } else viewer = new Viewer::ViewerWidget(Viewer::ViewerWidget::ViewerWindow, &m_viewerInputMacros); connect(viewer, &Viewer::ViewerWidget::soughtTo, m_thumbnailView, &ThumbnailView::ThumbnailFacade::changeSingleSelection); connect(viewer, &Viewer::ViewerWidget::imageRotated, this, &Window::slotImageRotated); viewer->show( slideShow ); viewer->load( mediaList, seek < 0 ? 0 : seek ); viewer->raise(); } void MainWindow::Window::slotSortByDateAndTime() { DB::ImageDB::instance()->sortAndMergeBackIn(selected()); showThumbNails( DB::ImageDB::instance()->search( Browser::BrowserWidget::instance()->currentContext())); DirtyIndicator::markDirty(); } void MainWindow::Window::slotSortAllByDateAndTime() { DB::ImageDB::instance()->sortAndMergeBackIn(DB::ImageDB::instance()->images()); if ( m_thumbnailView->gui() == m_stack->currentWidget() ) showThumbNails( DB::ImageDB::instance()->search( Browser::BrowserWidget::instance()->currentContext())); DirtyIndicator::markDirty(); } QString MainWindow::Window::welcome() { QString configFileName; QPointer dialog = new WelcomeDialog( this ); // exit if the user dismissed the welcome dialog if (!dialog->exec()) { qApp->quit(); } configFileName = dialog->configFileName(); delete dialog; return configFileName; } void MainWindow::Window::closeEvent( QCloseEvent* e ) { bool quit = true; quit = slotExit(); // If I made it here, then the user canceled if ( !quit ) e->ignore(); else e->setAccepted(true); } void MainWindow::Window::slotLimitToSelected() { Utilities::ShowBusyCursor dummy; showThumbNails( selected() ); } void MainWindow::Window::setupMenuBar() { // File menu KStandardAction::save( this, SLOT(slotSave()), actionCollection() ); KStandardAction::quit( this, SLOT(slotExit()), actionCollection() ); m_generateHtml = actionCollection()->addAction( QString::fromLatin1("exportHTML") ); m_generateHtml->setText( i18n("Generate HTML...") ); connect(m_generateHtml, &QAction::triggered, this, &Window::slotExportToHTML); QAction* a = actionCollection()->addAction( QString::fromLatin1("import"), this, SLOT(slotImport()) ); a->setText( i18n( "Import...") ); a = actionCollection()->addAction( QString::fromLatin1("export"), this, SLOT(slotExport()) ); a->setText( i18n( "Export/Copy Images...") ); // Go menu a = KStandardAction::back( m_browser, SLOT(back()), actionCollection() ); connect(m_browser, &Browser::BrowserWidget::canGoBack, a, &QAction::setEnabled); a->setEnabled( false ); a = KStandardAction::forward( m_browser, SLOT(forward()), actionCollection() ); connect(m_browser, &Browser::BrowserWidget::canGoForward, a, &QAction::setEnabled); a->setEnabled( false ); a = KStandardAction::home( m_browser, SLOT(home()), actionCollection() ); actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_Home); connect(a, &QAction::triggered, m_dateBar, &DateBar::DateBarWidget::clearSelection); KStandardAction::redisplay( m_browser, SLOT(go()), actionCollection() ); // The Edit menu m_copy = KStandardAction::copy( this, SLOT(slotCopySelectedURLs()), actionCollection() ); m_paste = KStandardAction::paste( this, SLOT(slotPasteInformation()), actionCollection() ); m_paste->setEnabled(false); m_selectAll = KStandardAction::selectAll( m_thumbnailView, SLOT(selectAll()), actionCollection() ); KStandardAction::find( this, SLOT(slotSearch()), actionCollection() ); m_deleteSelected = actionCollection()->addAction(QString::fromLatin1("deleteSelected")); m_deleteSelected->setText( i18nc("Delete selected images", "Delete Selected" ) ); m_deleteSelected->setIcon( QIcon::fromTheme( QString::fromLatin1("edit-delete") ) ); actionCollection()->setDefaultShortcut(m_deleteSelected, Qt::Key_Delete); connect(m_deleteSelected, &QAction::triggered, this, &Window::slotDeleteSelected); a = actionCollection()->addAction(QString::fromLatin1("removeTokens"), this, SLOT(slotRemoveTokens())); a->setText( i18n("Remove Tokens...") ); a = actionCollection()->addAction(QString::fromLatin1("showListOfFiles"), this, SLOT(slotShowListOfFiles())); a->setText( i18n("Open List of Files...")) ; m_configOneAtATime = actionCollection()->addAction( QString::fromLatin1("oneProp"), this, SLOT(slotConfigureImagesOneAtATime()) ); m_configOneAtATime->setText( i18n( "Annotate Individual Items" ) ); actionCollection()->setDefaultShortcut(m_configOneAtATime, Qt::CTRL + Qt::Key_1); m_configAllSimultaniously = actionCollection()->addAction( QString::fromLatin1("allProp"), this, SLOT(slotConfigureAllImages()) ); m_configAllSimultaniously->setText( i18n( "Annotate Multiple Items at a Time" ) ); actionCollection()->setDefaultShortcut(m_configAllSimultaniously, Qt::CTRL + Qt::Key_2); m_createImageStack = actionCollection()->addAction( QString::fromLatin1("createImageStack"), this, SLOT(slotCreateImageStack()) ); m_createImageStack->setText( i18n("Merge Images into a Stack") ); actionCollection()->setDefaultShortcut(m_createImageStack, Qt::CTRL + Qt::Key_3); m_unStackImages = actionCollection()->addAction( QString::fromLatin1("unStackImages"), this, SLOT(slotUnStackImages()) ); m_unStackImages->setText( i18n("Remove Images from Stack") ); m_setStackHead = actionCollection()->addAction( QString::fromLatin1("setStackHead"), this, SLOT(slotSetStackHead()) ); m_setStackHead->setText( i18n("Set as First Image in Stack") ); actionCollection()->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4); m_rotLeft = actionCollection()->addAction( QString::fromLatin1("rotateLeft"), this, SLOT(slotRotateSelectedLeft()) ); m_rotLeft->setText( i18n( "Rotate counterclockwise" ) ); actionCollection()->setDefaultShortcut(m_rotLeft, Qt::Key_7); m_rotRight = actionCollection()->addAction( QString::fromLatin1("rotateRight"), this, SLOT(slotRotateSelectedRight()) ); m_rotRight->setText( i18n( "Rotate clockwise" ) ); actionCollection()->setDefaultShortcut(m_rotRight, Qt::Key_9); // The Images menu m_view = actionCollection()->addAction( QString::fromLatin1("viewImages"), this, SLOT(slotView()) ); m_view->setText( i18n("View") ); actionCollection()->setDefaultShortcut(m_view, Qt::CTRL + Qt::Key_I); m_viewInNewWindow = actionCollection()->addAction( QString::fromLatin1("viewImagesNewWindow"), this, SLOT(slotViewNewWindow()) ); m_viewInNewWindow->setText( i18n("View (In New Window)") ); m_runSlideShow = actionCollection()->addAction( QString::fromLatin1("runSlideShow"), this, SLOT(slotRunSlideShow()) ); m_runSlideShow->setText( i18n("Run Slide Show") ); m_runSlideShow->setIcon( QIcon::fromTheme( QString::fromLatin1("view-presentation") ) ); actionCollection()->setDefaultShortcut(m_runSlideShow, Qt::CTRL + Qt::Key_R); m_runRandomSlideShow = actionCollection()->addAction( QString::fromLatin1("runRandomizedSlideShow"), this, SLOT(slotRunRandomizedSlideShow()) ); m_runRandomSlideShow->setText( i18n( "Run Randomized Slide Show" ) ); a = actionCollection()->addAction( QString::fromLatin1("collapseAllStacks"), m_thumbnailView, SLOT(collapseAllStacks()) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::collapseAllStacksEnabled, a, &QAction::setEnabled); a->setEnabled(false); a->setText( i18n("Collapse all stacks" )); a = actionCollection()->addAction( QString::fromLatin1("expandAllStacks"), m_thumbnailView, SLOT(expandAllStacks()) ); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::expandAllStacksEnabled, a, &QAction::setEnabled); a->setEnabled(false); a->setText( i18n("Expand all stacks" )); QActionGroup* grp = new QActionGroup( this ); a = actionCollection()->add( QString::fromLatin1("orderIncr"), this, SLOT(slotOrderIncr()) ); a->setText( i18n("Show &Oldest First") ) ; a->setActionGroup(grp); a->setChecked( !Settings::SettingsData::instance()->showNewestThumbnailFirst() ); a = actionCollection()->add( QString::fromLatin1("orderDecr"), this, SLOT(slotOrderDecr()) ); a->setText( i18n("Show &Newest First") ); a->setActionGroup(grp); a->setChecked( Settings::SettingsData::instance()->showNewestThumbnailFirst() ); m_sortByDateAndTime = actionCollection()->addAction( QString::fromLatin1("sortImages"), this, SLOT(slotSortByDateAndTime()) ); m_sortByDateAndTime->setText( i18n("Sort Selected by Date && Time") ); m_limitToMarked = actionCollection()->addAction( QString::fromLatin1("limitToMarked"), this, SLOT(slotLimitToSelected()) ); m_limitToMarked->setText( i18n("Limit View to Selection") ); m_jumpToContext = actionCollection()->addAction( QString::fromLatin1("jumpToContext"), this, SLOT(slotJumpToContext()) ); m_jumpToContext->setText( i18n("Jump to Context") ); actionCollection()->setDefaultShortcut(m_jumpToContext, Qt::CTRL + Qt::Key_J); m_jumpToContext->setIcon( QIcon::fromTheme( QString::fromLatin1( "kphotoalbum" ) ) ); // icon suggestion: go-jump (don't know the exact meaning though, so I didn't replace it right away m_lock = actionCollection()->addAction( QString::fromLatin1("lockToDefaultScope"), this, SLOT(lockToDefaultScope()) ); m_lock->setText( i18n("Lock Images") ); m_unlock = actionCollection()->addAction( QString::fromLatin1("unlockFromDefaultScope"), this, SLOT(unlockFromDefaultScope()) ); m_unlock->setText( i18n("Unlock") ); a = actionCollection()->addAction( QString::fromLatin1("changeScopePasswd"), this, SLOT(changePassword()) ); a->setText( i18n("Change Password...") ); actionCollection()->setDefaultShortcut(a, 0); m_setDefaultPos = actionCollection()->addAction( QString::fromLatin1("setDefaultScopePositive"), this, SLOT(setDefaultScopePositive()) ); m_setDefaultPos->setText( i18n("Lock Away All Other Items") ); m_setDefaultNeg = actionCollection()->addAction( QString::fromLatin1("setDefaultScopeNegative"), this, SLOT(setDefaultScopeNegative()) ); m_setDefaultNeg->setText( i18n("Lock Away Current Set of Items") ); // Maintenance a = actionCollection()->addAction( QString::fromLatin1("findUnavailableImages"), this, SLOT(slotShowNotOnDisk()) ); a->setText( i18n("Display Images and Videos Not on Disk") ); a = actionCollection()->addAction( QString::fromLatin1("findImagesWithInvalidDate"), this, SLOT(slotShowImagesWithInvalidDate()) ); a->setText( i18n("Display Images and Videos with Incomplete Dates...") ); #ifdef DOES_STILL_NOT_WORK_IN_KPA4 a = actionCollection()->addAction( QString::fromLatin1("findImagesWithChangedMD5Sum"), this, SLOT(slotShowImagesWithChangedMD5Sum()) ); a->setText( i18n("Display Images and Videos with Changed MD5 Sum") ); #endif //DOES_STILL_NOT_WORK_IN_KPA4 a = actionCollection()->addAction( QLatin1String("mergeDuplicates"), this, SLOT(mergeDuplicates())); a->setText(i18n("Merge duplicates")); a = actionCollection()->addAction( QString::fromLatin1("rebuildMD5s"), this, SLOT(slotRecalcCheckSums()) ); a->setText( i18n("Recalculate Checksum") ); a = actionCollection()->addAction( QString::fromLatin1("rescan"), DB::ImageDB::instance(), SLOT(slotRescan()) ); a->setText( i18n("Rescan for Images and Videos") ); QAction* recreateExif = actionCollection()->addAction( QString::fromLatin1( "recreateExifDB" ), this, SLOT(slotRecreateExifDB()) ); recreateExif->setText( i18n("Recreate Exif Search Database") ); QAction* rereadExif = actionCollection()->addAction( QString::fromLatin1("reReadExifInfo"), this, SLOT(slotReReadExifInfo()) ); rereadExif->setText( i18n("Read EXIF Info From Files...") ); m_sortAllByDateAndTime = actionCollection()->addAction( QString::fromLatin1("sortAllImages"), this, SLOT(slotSortAllByDateAndTime()) ); m_sortAllByDateAndTime->setText( i18n("Sort All by Date && Time") ); m_sortAllByDateAndTime->setEnabled(true); m_AutoStackImages = actionCollection()->addAction( QString::fromLatin1( "autoStack" ), this, SLOT (slotAutoStackImages()) ); m_AutoStackImages->setText( i18n("Automatically Stack Selected Images...") ); a = actionCollection()->addAction( QString::fromLatin1("buildThumbs"), this, SLOT(slotBuildThumbnails()) ); a->setText( i18n("Build Thumbnails") ); a->setText( i18n("Statistics...") ); m_markUntagged = actionCollection()->addAction(QString::fromUtf8("markUntagged"), this, SLOT(slotMarkUntagged())); m_markUntagged->setText(i18n("Mark As Untagged")); // Settings KStandardAction::preferences( this, SLOT(slotOptions()), actionCollection() ); KStandardAction::keyBindings( this, SLOT(slotConfigureKeyBindings()), actionCollection() ); KStandardAction::configureToolbars( this, SLOT(slotConfigureToolbars()), actionCollection() ); a = actionCollection()->addAction( QString::fromLatin1("readdAllMessages"), this, SLOT(slotReenableMessages()) ); a->setText( i18n("Enable All Messages") ); m_viewMenu = actionCollection()->add( QString::fromLatin1("configureView") ); m_viewMenu->setText( i18n("Configure Current View") ); m_viewMenu->setIcon( QIcon::fromTheme( QString::fromLatin1( "view-list-details" ) ) ); m_viewMenu->setDelayed( false ); QActionGroup* viewGrp = new QActionGroup( this ); viewGrp->setExclusive( true ); m_smallListView = actionCollection()->add( QString::fromLatin1("smallListView"), m_browser, SLOT(slotSmallListView()) ); m_smallListView->setText( i18n("Tree") ); m_viewMenu->addAction( m_smallListView ); m_smallListView->setActionGroup( viewGrp ); m_largeListView = actionCollection()->add( QString::fromLatin1("largelistview"), m_browser, SLOT(slotLargeListView()) ); m_largeListView->setText( i18n("Tree with User Icons") ); m_viewMenu->addAction( m_largeListView ); m_largeListView->setActionGroup( viewGrp ); m_largeIconView = actionCollection()->add( QString::fromLatin1("largeiconview"), m_browser, SLOT(slotLargeIconView()) ); m_largeIconView->setText( i18n("Icons") ); m_viewMenu->addAction( m_largeIconView ); m_largeIconView->setActionGroup( viewGrp ); connect(m_browser, &Browser::BrowserWidget::isViewChangeable, viewGrp, &QActionGroup::setEnabled); connect(m_browser, &Browser::BrowserWidget::currentViewTypeChanged, this, &Window::slotUpdateViewMenu); // The help menu KStandardAction::tipOfDay( this, SLOT(showTipOfDay()), actionCollection() ); a = actionCollection()->add( QString::fromLatin1("showToolTipOnImages") ); a->setText( i18n("Show Tooltips in Thumbnails Window") ); actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_T); connect(a, &QAction::toggled, m_thumbnailView, &ThumbnailView::ThumbnailFacade::showToolTipsOnImages); a = actionCollection()->addAction( QString::fromLatin1("runDemo"), this, SLOT(runDemo()) ); a->setText( i18n("Run KPhotoAlbum Demo") ); a = actionCollection()->addAction( QString::fromLatin1("features"), this, SLOT(showFeatures()) ); a->setText( i18n("KPhotoAlbum Feature Status") ); a = actionCollection()->addAction( QString::fromLatin1("showVideo"), this, SLOT(showVideos()) ); a->setText( i18n( "Show Demo Videos") ); // Context menu actions m_showExifDialog = actionCollection()->addAction( QString::fromLatin1("showExifInfo"), this, SLOT(slotShowExifInfo()) ); m_showExifDialog->setText( i18n("Show Exif Info") ); m_recreateThumbnails = actionCollection()->addAction( QString::fromLatin1("recreateThumbnails"), m_thumbnailView, SLOT(slotRecreateThumbnail()) ); m_recreateThumbnails->setText( i18n("Recreate Selected Thumbnails") ); m_useNextVideoThumbnail = actionCollection()->addAction( QString::fromLatin1("useNextVideoThumbnail"), this, SLOT(useNextVideoThumbnail())); m_useNextVideoThumbnail->setText(i18n("Use next video thumbnail")); actionCollection()->setDefaultShortcut(m_useNextVideoThumbnail, Qt::CTRL + Qt::Key_Plus); m_usePreviousVideoThumbnail = actionCollection()->addAction( QString::fromLatin1("usePreviousVideoThumbnail"), this, SLOT(usePreviousVideoThumbnail())); m_usePreviousVideoThumbnail->setText(i18n("Use previous video thumbnail")); actionCollection()->setDefaultShortcut(m_usePreviousVideoThumbnail, Qt::CTRL + Qt::Key_Minus); createGUI( QString::fromLatin1( "kphotoalbumui.rc" ) ); } void MainWindow::Window::slotExportToHTML() { if ( ! m_htmlDialog ) m_htmlDialog = new HTMLGenerator::HTMLDialog( this ); m_htmlDialog->exec(selectedOnDisk()); } void MainWindow::Window::startAutoSaveTimer() { int i = Settings::SettingsData::instance()->autoSave(); m_autoSaveTimer->stop(); if ( i != 0 ) { m_autoSaveTimer->start( i * 1000 * 60 ); } } void MainWindow::Window::slotAutoSave() { if ( m_statusBar->mp_dirtyIndicator->isAutoSaveDirty() ) { Utilities::ShowBusyCursor dummy; m_statusBar->showMessage(i18n("Auto saving....")); DB::ImageDB::instance()->save( Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1(".#index.xml"), true ); ImageManager::ThumbnailCache::instance()->save(); m_statusBar->showMessage(i18n("Auto saving.... Done"), 5000); m_statusBar->mp_dirtyIndicator->autoSaved(); } } void MainWindow::Window::showThumbNails() { m_statusBar->showThumbnailSlider(); reloadThumbnails( ThumbnailView::ClearSelection ); m_stack->setCurrentWidget( m_thumbnailView->gui() ); m_thumbnailView->gui()->setFocus(); updateStates( true ); } void MainWindow::Window::showBrowser() { m_statusBar->clearMessage(); m_statusBar->hideThumbnailSlider(); m_stack->setCurrentWidget( m_browser ); m_browser->setFocus(); updateContextMenuFromSelectionSize( 0 ); updateStates( false ); } void MainWindow::Window::slotOptionGroupChanged() { // FIXME: What if annotation dialog is open? (if that's possible) delete m_annotationDialog; m_annotationDialog = nullptr; DirtyIndicator::markDirty(); } void MainWindow::Window::showTipOfDay() { KTipDialog::showTip( this, QString(), true ); } void MainWindow::Window::runDemo() { KProcess* process = new KProcess; *process << QLatin1String("kphotoalbum") << QLatin1String("--demo"); process->startDetached(); } bool MainWindow::Window::load() { // Let first try to find a config file. QString configFile; QUrl dbFileUrl = Options::the()->dbFile(); if ( !dbFileUrl.isEmpty() && dbFileUrl.isLocalFile() ) { configFile = dbFileUrl.toLocalFile(); } else if ( Options::the()->demoMode() ) { configFile = Utilities::setupDemo(); } else { bool showWelcome = false; KConfigGroup config = KSharedConfig::openConfig()->group(QString::fromUtf8("General")); if ( config.hasKey( QString::fromLatin1("imageDBFile") ) ) { configFile = config.readEntry( QString::fromLatin1("imageDBFile"), QString() ); if ( !QFileInfo( configFile ).exists() ) showWelcome = true; } else showWelcome = true; if ( showWelcome ) { SplashScreen::instance()->hide(); configFile = welcome(); } } if ( configFile.isNull() ) return false; if (configFile.startsWith( QString::fromLatin1( "~" ) ) ) configFile = QDir::home().path() + QString::fromLatin1( "/" ) + configFile.mid(1); // To avoid a race conditions where both the image loader thread creates an instance of // Settings, and where the main thread crates an instance, we better get it created now. Settings::SettingsData::setup( QFileInfo( configFile ).absolutePath() ); if ( Settings::SettingsData::instance()->showSplashScreen() ) { SplashScreen::instance()->show(); qApp->processEvents(); } // Doing some validation on user provided index file if ( Options::the()->dbFile().isValid() ) { QFileInfo fi( configFile ); if ( !fi.dir().exists() ) { KMessageBox::error( this, i18n("

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

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

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

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

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

" "

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

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

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

" "

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

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

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

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

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

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

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

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

" "

Please follow these steps to do so:" "

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

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

Unable to find ffmpeg or MPlayer on the system.

" "

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

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

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

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

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

" "

See youtube video or " "install from google play

" ); KMessageBox::information( this, txt, QString(), doNotShowKey, KMessageBox::AllowLink ); } void MainWindow::Window::setHistogramVisibilty( bool visible ) const { if (visible) { m_dateBar->show(); m_dateBarLine->show(); } else { m_dateBar->hide(); m_dateBarLine->hide(); } } void MainWindow::Window::slotImageRotated(const DB::FileName& fileName) { // An image has been rotated by the annotation dialog or the viewer. // We have to reload the respective thumbnail to get it in the right angle ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } bool MainWindow::Window::dbIsDirty() const { return m_statusBar->mp_dirtyIndicator->isSaveDirty(); } #ifdef HAVE_KGEOMAP void MainWindow::Window::showPositionBrowser() { Browser::PositionBrowserWidget *positionBrowser = positionBrowserWidget(); m_stack->setCurrentWidget(positionBrowser); updateStates( false ); } Browser::PositionBrowserWidget* MainWindow::Window::positionBrowserWidget() { if (m_positionBrowser == 0) { m_positionBrowser = createPositionBrowser(); } return m_positionBrowser; } Browser::PositionBrowserWidget* MainWindow::Window::createPositionBrowser() { Browser::PositionBrowserWidget* widget = new Browser::PositionBrowserWidget(m_stack); m_stack->addWidget(widget); return widget; } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/CategoryPage.cpp b/Settings/CategoryPage.cpp index 6a20653d..7fee9121 100644 --- a/Settings/CategoryPage.cpp +++ b/Settings/CategoryPage.cpp @@ -1,563 +1,563 @@ /* Copyright (C) 2003-2014 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "CategoryPage.h" // Qt includes #include #include #include #include #include #include #include #include #include #include // KDE includes #include #include #include // Local includes #include "DB/ImageDB.h" #include "DB/CategoryCollection.h" #include "DB/MemberMap.h" #include "MainWindow/Window.h" #include "MainWindow/DirtyIndicator.h" #include "UntaggedGroupBox.h" #include "SettingsDialog.h" #include "CategoryItem.h" Settings::CategoryPage::CategoryPage(QWidget* parent) : QWidget(parent) { QVBoxLayout* mainLayout = new QVBoxLayout(this); // The category settings QGroupBox* categoryGroupBox = new QGroupBox; mainLayout->addWidget(categoryGroupBox); categoryGroupBox->setTitle(i18n("Category Settings")); QHBoxLayout* categoryLayout = new QHBoxLayout(categoryGroupBox); // Category list QVBoxLayout* categorySideLayout = new QVBoxLayout; categoryLayout->addLayout(categorySideLayout); m_categoriesListWidget = new QListWidget; connect(m_categoriesListWidget, &QListWidget::itemClicked, this, &CategoryPage::editCategory); connect(m_categoriesListWidget, &QListWidget::itemSelectionChanged, this, &CategoryPage::editSelectedCategory); connect(m_categoriesListWidget, &QListWidget::itemChanged, this, &CategoryPage::categoryNameChanged); // This is needed to fix some odd behavior if the "New" button is double clicked connect(m_categoriesListWidget, &QListWidget::itemDoubleClicked, this, &CategoryPage::categoryDoubleClicked); connect(m_categoriesListWidget->itemDelegate(), SIGNAL(closeEditor(QWidget*,QAbstractItemDelegate::EndEditHint)), this, SLOT(listWidgetEditEnd(QWidget*,QAbstractItemDelegate::EndEditHint))); categorySideLayout->addWidget(m_categoriesListWidget); // New, Delete, and buttons QHBoxLayout* newDeleteRenameLayout = new QHBoxLayout; categorySideLayout->addLayout(newDeleteRenameLayout); m_newCategoryButton = new QPushButton(i18n("New")); connect(m_newCategoryButton, &QPushButton::clicked, this, &CategoryPage::newCategory); newDeleteRenameLayout->addWidget(m_newCategoryButton); m_delItem = new QPushButton(i18n("Delete")); connect(m_delItem, &QPushButton::clicked, this, &CategoryPage::deleteCurrentCategory); newDeleteRenameLayout->addWidget(m_delItem); m_renameItem = new QPushButton(i18n("Rename")); connect(m_renameItem, &QPushButton::clicked, this, &CategoryPage::renameCurrentCategory); newDeleteRenameLayout->addWidget(m_renameItem); // Category settings QVBoxLayout* rightSideLayout = new QVBoxLayout; categoryLayout->addLayout(rightSideLayout); // Header m_categoryLabel = new QLabel; m_categoryLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); rightSideLayout->addWidget(m_categoryLabel); // Pending rename label m_renameLabel = new QLabel; m_renameLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); rightSideLayout->addWidget(m_renameLabel); QDialog *parentDialog = qobject_cast(parent); connect( parentDialog, &QDialog::rejected, m_renameLabel, &QLabel::clear); // Some space looks better here :-) QLabel* spacer = new QLabel; rightSideLayout->addWidget(spacer); // Here we start with the actual settings QGridLayout* settingsLayout = new QGridLayout; rightSideLayout->addLayout(settingsLayout); int row = 0; // Positionable m_positionableLabel = new QLabel(i18n("Positionable tags:")); settingsLayout->addWidget(m_positionableLabel, row, 0); m_positionable = new QCheckBox(i18n("Tags in this category can be associated with areas within images")); settingsLayout->addWidget(m_positionable, row, 1); connect(m_positionable, &QCheckBox::clicked, this, &CategoryPage::positionableChanged); row++; // Icon m_iconLabel = new QLabel(i18n("Icon:")); settingsLayout->addWidget(m_iconLabel, row, 0); m_icon = new KIconButton; settingsLayout->addWidget(m_icon, row, 1); m_icon->setIconSize(32); m_icon->setIcon(QString::fromUtf8("personsIcon")); connect(m_icon, &KIconButton::iconChanged, this, &CategoryPage::iconChanged); row++; // Thumbnail size m_thumbnailSizeInCategoryLabel = new QLabel(i18n("Thumbnail Size:")); settingsLayout->addWidget(m_thumbnailSizeInCategoryLabel, row, 0); m_thumbnailSizeInCategory = new QSpinBox; m_thumbnailSizeInCategory->setRange(32, 512); m_thumbnailSizeInCategory->setSingleStep(32); settingsLayout->addWidget(m_thumbnailSizeInCategory, row, 1); connect(m_thumbnailSizeInCategory, static_cast(&QSpinBox::valueChanged), this, &CategoryPage::thumbnailSizeChanged); row++; // Preferred View m_preferredViewLabel = new QLabel(i18n("Preferred view:")); settingsLayout->addWidget(m_preferredViewLabel, row, 0); m_preferredView = new QComboBox; settingsLayout->addWidget(m_preferredView, row, 1); m_preferredView->addItems(QStringList() << i18n("List View") << i18n("List View with Custom Thumbnails") << i18n("Icon View") << i18n("Icon View with Custom Thumbnails")); connect(m_preferredView, static_cast(&QComboBox::activated), this, &CategoryPage::preferredViewChanged); rightSideLayout->addStretch(); // Info about the database not being saved QHBoxLayout* dbNotSavedLayout = new QHBoxLayout; mainLayout->addLayout(dbNotSavedLayout); m_dbNotSavedLabel = new QLabel( i18n("" "The database has unsaved changes. As long as those are " "not saved,
the names of categories can't be changed " "and new ones can't be added." "
")); m_dbNotSavedLabel->setWordWrap(true); dbNotSavedLayout->addWidget(m_dbNotSavedLabel); m_saveDbNowButton = new QPushButton(i18n("Save the DB now")); m_saveDbNowButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum); connect(m_saveDbNowButton, &QPushButton::clicked, this, &CategoryPage::saveDbNow); dbNotSavedLayout->addWidget(m_saveDbNowButton); resetInterface(); // Untagged images m_untaggedBox = new UntaggedGroupBox; mainLayout->addWidget(m_untaggedBox); m_currentCategory = 0; // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = false; m_categoryNamesChanged = false; } void Settings::CategoryPage::resetInterface() { enableDisable(false); m_categoriesListWidget->setItemSelected(m_categoriesListWidget->currentItem(), false); resetCategoryLabel(); m_renameLabel->hide(); } void Settings::CategoryPage::editSelectedCategory() { editCategory(m_categoriesListWidget->currentItem()); } void Settings::CategoryPage::editCategory(QListWidgetItem* i) { if (i == 0) { return; } m_categoryNameBeforeEdit = i->text(); Settings::CategoryItem* item = static_cast(i); m_currentCategory = item; m_categoryLabel->setText(i18n("Settings for category %1", item->originalName())); if (m_currentCategory->originalName() != m_categoryNameBeforeEdit) { m_renameLabel->setText(i18n("Pending change: rename to \"%1\"", m_categoryNameBeforeEdit)); m_renameLabel->show(); } else { m_renameLabel->clear(); m_renameLabel->hide(); } m_positionable->setChecked(item->positionable()); m_icon->setIcon(item->icon()); m_thumbnailSizeInCategory->setValue(item->thumbnailSize()); m_preferredView->setCurrentIndex(static_cast(item->viewType())); enableDisable(true); if (item->originalName() == DB::ImageDB::instance()->categoryCollection() ->categoryForSpecial(DB::Category::TokensCategory)->name()) { m_delItem->setEnabled(false); m_positionableLabel->setEnabled(false); m_positionable->setEnabled(false); m_thumbnailSizeInCategoryLabel->setEnabled(false); m_thumbnailSizeInCategory->setEnabled(false); m_preferredViewLabel->setEnabled(false); m_preferredView->setEnabled(false); } } void Settings::CategoryPage::categoryNameChanged(QListWidgetItem* item) { QString newCategoryName = item->text().simplified(); m_categoriesListWidget->blockSignals(true); item->setText(QString()); m_categoriesListWidget->blockSignals(false); // Now let's check if the new name is valid :-) // If it's empty, we're done here. The new name can't be empty. if (newCategoryName.isEmpty()) { resetCategory(item); return; } // We don't want to have special category names. // We do have to search both for the localized version and the C locale version, because a user // could start KPA e. g. with a German locale and create a "Folder" category (which would not - // be catched by i18n("Folder")), and then start KPA with the C locale, which would produce a + // be caught by i18n("Folder")), and then start KPA with the C locale, which would produce a // doubled "Folder" category. if (newCategoryName == i18n("Folder") || newCategoryName == QString::fromUtf8("Folder") || newCategoryName == i18n("Media Type") || newCategoryName == QString::fromUtf8("Media Type")) { resetCategory(item); KMessageBox::sorry(this, i18n("

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

" "

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

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

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

" "

A category with this name already exists.

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

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

" "

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

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

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

" "

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

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

Really delete category \"%1\"?

", m_currentCategory->text())); if (answer == KMessageBox::No) { return; } m_untaggedBox->categoryDeleted(m_currentCategory->text()); m_deletedCategories.append(m_currentCategory); m_categoriesListWidget->takeItem(m_categoriesListWidget->row(m_currentCategory)); m_currentCategory = 0; m_positionable->setChecked(false); m_icon->setIcon(QIcon()); m_thumbnailSizeInCategory->setValue(64); enableDisable(false); resetCategoryLabel(); editCategory(m_categoriesListWidget->currentItem()); emit categoryChangesPending(); } void Settings::CategoryPage::renameCurrentCategory() { // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = true; m_categoriesListWidget->editItem(m_currentCategory); } void Settings::CategoryPage::enableDisable(bool b) { m_delItem->setEnabled(b); m_positionableLabel->setEnabled(b); m_positionable->setEnabled(b); m_icon->setEnabled(b); m_iconLabel->setEnabled(b); m_thumbnailSizeInCategoryLabel->setEnabled(b); m_thumbnailSizeInCategory->setEnabled(b); m_preferredViewLabel->setEnabled(b); m_preferredView->setEnabled(b); m_categoriesListWidget->blockSignals(true); if (MainWindow::Window::theMainWindow()->dbIsDirty()) { m_dbNotSavedLabel->show(); m_saveDbNowButton->show(); m_renameItem->setEnabled(false); m_newCategoryButton->setEnabled(false); for (int i = 0; i < m_categoriesListWidget->count(); i++) { QListWidgetItem* currentItem = m_categoriesListWidget->item(i); currentItem->setFlags(currentItem->flags() & ~Qt::ItemIsEditable); } } else { m_dbNotSavedLabel->hide(); m_saveDbNowButton->hide(); m_renameItem->setEnabled(b); m_newCategoryButton->setEnabled(true); for (int i = 0; i < m_categoriesListWidget->count(); i++) { QListWidgetItem* currentItem = m_categoriesListWidget->item(i); currentItem->setFlags(currentItem->flags() | Qt::ItemIsEditable); } } m_categoriesListWidget->blockSignals(false); } void Settings::CategoryPage::saveSettings(Settings::SettingsData* opt, DB::MemberMap* memberMap) { // Delete items Q_FOREACH( CategoryItem *item, m_deletedCategories ) { item->removeFromDatabase(); } // Created or Modified items for (int i = 0; i < m_categoriesListWidget->count(); ++i) { CategoryItem* item = static_cast(m_categoriesListWidget->item(i)); item->submit(memberMap); } DB::ImageDB::instance()->memberMap() = *memberMap; m_untaggedBox->saveSettings(opt); if (m_categoryNamesChanged) { // Probably, one or more category names have been edited. Save the database so that // all thumbnails are referenced with the correct name. MainWindow::Window::theMainWindow()->slotSave(); m_categoryNamesChanged = false; } } void Settings::CategoryPage::loadSettings(Settings::SettingsData* opt) { m_categoriesListWidget->blockSignals(true); m_categoriesListWidget->clear(); QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH( const DB::CategoryPtr category, categories ) { if (category->type() == DB::Category::PlainCategory || category->type() == DB::Category::TokensCategory) { Settings::CategoryItem *item = new CategoryItem(category->name(), category->iconName(), category->viewType(), category->thumbnailSize(), m_categoriesListWidget, category->positionable()); Q_UNUSED(item); } } m_categoriesListWidget->blockSignals(false); m_untaggedBox->loadSettings(opt); } void Settings::CategoryPage::categoryDoubleClicked(QListWidgetItem*) { // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = true; } void Settings::CategoryPage::listWidgetEditEnd(QWidget*, QAbstractItemDelegate::EndEditHint) { // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = false; } void Settings::CategoryPage::resetCategoryLabel() { m_categoryLabel->setText(i18n("choose a category to edit it")); } void Settings::CategoryPage::saveDbNow() { MainWindow::Window::theMainWindow()->slotSave(); resetInterface(); enableDisable(false); } void Settings::CategoryPage::resetCategoryNamesChanged() { m_categoryNamesChanged = false; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/SettingsData.cpp b/Settings/SettingsData.cpp index 41fa9260..1db67495 100644 --- a/Settings/SettingsData.cpp +++ b/Settings/SettingsData.cpp @@ -1,549 +1,549 @@ /* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "SettingsData.h" #include #include #include #include #include #include #include #include #include #include "DB/CategoryCollection.h" #include "DB/ImageDB.h" #define STR(x) QString::fromLatin1(x) #define value( GROUP, OPTION, DEFAULT ) \ KSharedConfig::openConfig()->group( GROUP ).readEntry( OPTION, DEFAULT ) \ #define setValue( GROUP, OPTION, VALUE ) \ { \ KConfigGroup group = KSharedConfig::openConfig()->group( GROUP ); \ group.writeEntry( OPTION, VALUE ); \ group.sync(); \ } #define getValueFunc_( TYPE,FUNC, GROUP,OPTION,DEFAULT ) \ TYPE SettingsData::FUNC() const \ { return (TYPE) value( GROUP, OPTION, DEFAULT ); } #define setValueFunc_( FUNC,TYPE, GROUP,OPTION,VALUE ) \ void SettingsData::FUNC( const TYPE v ) \ { setValue( GROUP, OPTION, VALUE ); } #define getValueFunc( TYPE,FUNC, GROUP,DEFAULT ) getValueFunc_( TYPE,FUNC, #GROUP,#FUNC,DEFAULT ) #define setValueFunc( FUNC,TYPE, GROUP,OPTION ) setValueFunc_( FUNC,TYPE, #GROUP,#OPTION,v ) // TODO(mfwitten): document parameters. #define property_( GET_TYPE,GET_FUNC,GET_VALUE, SET_FUNC,SET_TYPE,SET_VALUE, GROUP,OPTION,GET_DEFAULT_1,GET_DEFAULT_2,GET_DEFAULT_2_TYPE ) \ GET_TYPE SettingsData::GET_FUNC() const \ { \ KConfigGroup g = KSharedConfig::openConfig()->group(GROUP); \ \ if ( !g.hasKey(OPTION) ) \ return GET_DEFAULT_1; \ \ GET_DEFAULT_2_TYPE v = g.readEntry( OPTION, (GET_DEFAULT_2_TYPE)GET_DEFAULT_2 ); \ return (GET_TYPE) GET_VALUE; \ } \ setValueFunc_( SET_FUNC,SET_TYPE, GROUP,OPTION,SET_VALUE ) #define property( GET_TYPE,GET_FUNC, SET_FUNC,SET_TYPE,SET_VALUE, GROUP,OPTION,GET_DEFAULT ) \ getValueFunc_( GET_TYPE,GET_FUNC, GROUP,OPTION,GET_DEFAULT) \ setValueFunc_( SET_FUNC,SET_TYPE, GROUP,OPTION,SET_VALUE ) #define property_copy( GET_FUNC,SET_FUNC, TYPE,GROUP,GET_DEFAULT ) \ property( TYPE,GET_FUNC, SET_FUNC,TYPE,v, #GROUP,#GET_FUNC,GET_DEFAULT ) #define property_ref_( GET_FUNC,SET_FUNC, TYPE,GROUP,GET_DEFAULT ) \ property( TYPE,GET_FUNC, SET_FUNC,TYPE&,v, GROUP,#GET_FUNC,GET_DEFAULT ) #define property_ref( GET_FUNC,SET_FUNC, TYPE,GROUP,GET_DEFAULT ) \ property( TYPE,GET_FUNC, SET_FUNC,TYPE&,v, #GROUP,#GET_FUNC,GET_DEFAULT ) #define property_enum( GET_FUNC,SET_FUNC, TYPE,GROUP,GET_DEFAULT ) \ property( TYPE,GET_FUNC, SET_FUNC,TYPE,(int)v, #GROUP,#GET_FUNC,(int)GET_DEFAULT ) #define property_sset( GET_FUNC,SET_FUNC, GROUP,GET_DEFAULT ) \ property_( StringSet,GET_FUNC,v.toSet(), SET_FUNC,StringSet&,v.toList(), #GROUP,#GET_FUNC,GET_DEFAULT,QStringList(),QStringList ) /** * smoothScale() is called from the image loading thread, therefore we need * to cache it this way, rather than going to KConfig. */ static bool _smoothScale = true; using namespace Settings; const WindowType Settings::MainWindow = "MainWindow"; const WindowType Settings::AnnotationDialog = "AnnotationDialog"; SettingsData* SettingsData::s_instance = nullptr; SettingsData* SettingsData::instance() { if ( ! s_instance ) qFatal("instance called before loading a setup!"); return s_instance; } bool SettingsData::ready() { return s_instance; } void SettingsData::setup( const QString& imageDirectory ) { if ( !s_instance ) s_instance = new SettingsData( imageDirectory ); } SettingsData::SettingsData( const QString& imageDirectory ) { m_hasAskedAboutTimeStamps = false; QString s = STR( "/" ); m_imageDirectory = imageDirectory.endsWith(s) ? imageDirectory : imageDirectory + s; _smoothScale = value( "Viewer", "smoothScale", true ); // Split the list of EXIF comments that should be stripped automatically to a list QStringList commentsToStrip = value( "General", "commentsToStrip", QString::fromLatin1("") ).split(QString::fromLatin1("-,-"), QString::SkipEmptyParts ); for (QString &comment : commentsToStrip ) comment.replace( QString::fromLatin1(",,"), QString::fromLatin1(",") ); m_EXIFCommentsToStrip = commentsToStrip; } ///////////////// //// General //// ///////////////// property_copy( useEXIFRotate , setUseEXIFRotate , bool , General, true ) property_copy( useEXIFComments , setUseEXIFComments , bool , General, true ) property_copy( stripEXIFComments , setStripEXIFComments , bool , General, false ) property_copy( commentsToStrip , setCommentsToStrip , QString , General, QString::fromLatin1("") ) property_copy( searchForImagesOnStart, setSearchForImagesOnStart, bool , General, true ) property_copy( ignoreFileExtension , setIgnoreFileExtension , bool , General, false ) property_copy( skipSymlinks, setSkipSymlinks , bool , General, false ) property_copy( skipRawIfOtherMatches , setSkipRawIfOtherMatches , bool , General, false ) -property_copy( useRawThumbnail , setUseRawThumbnail , bool , General, false ) +property_copy( useRawThumbnail , setUseRawThumbnail , bool , General, true ) property_copy( useRawThumbnailSize , setUseRawThumbnailSize , QSize , General, QSize(1024,768) ) property_copy( useCompressedIndexXML , setUseCompressedIndexXML , bool , General, false ) property_copy( compressBackup , setCompressBackup , bool , General, true ) property_copy( showSplashScreen , setShowSplashScreen , bool , General, true ) property_copy( showHistogram , setShowHistogram , bool , General, true ) property_copy( autoSave , setAutoSave , int , General, 5 ) property_copy( backupCount , setBackupCount , int , General, 5 ) property_enum( tTimeStamps , setTTimeStamps , TimeStampTrust, General, Always ) property_copy( excludeDirectories , setExcludeDirectories , QString , General, QString::fromLatin1("xml,ThumbNails,.thumbs") ) property_copy( recentAndroidAddress , setRecentAndroidAddress , QString , General, QString() ) property_copy( listenForAndroidDevicesOnStartup, setListenForAndroidDevicesOnStartup, bool, General, false) getValueFunc( QSize,histogramSize, General,QSize(15,30) ) getValueFunc( ViewSortType,viewSortType, General,(int)SortLastUse ) getValueFunc( AnnotationDialog::MatchType, matchType, General,(int)AnnotationDialog::MatchFromWordStart ) void SettingsData::setHistogramSize( const QSize& size ) { if ( size == histogramSize() ) return; setValue( "General", "histogramSize", size ); emit histogramSizeChanged( size ); } void SettingsData::setViewSortType( const ViewSortType tp ) { if ( tp == viewSortType() ) return; setValue( "General", "viewSortType", (int)tp ); emit viewSortTypeChanged( tp ); } void SettingsData::setMatchType( const AnnotationDialog::MatchType mt ) { if ( mt == matchType() ) return; setValue( "General", "matchType", (int)mt ); emit matchTypeChanged( mt ); } bool SettingsData::trustTimeStamps() { if ( tTimeStamps() == Always ) return true; else if ( tTimeStamps() == Never ) return false; else { if (!m_hasAskedAboutTimeStamps ) { QApplication::setOverrideCursor( Qt::ArrowCursor ); QString txt = i18n("When reading time information of images, their EXIF info is used. " "Exif info may, however, not be supported by your KPhotoAlbum installation, " "or no valid information may be in the file. " "As a backup, KPhotoAlbum may use the timestamp of the image - this may, " "however, not be valid in case the image is scanned in. " "So the question is, should KPhotoAlbum trust the time stamp on your images?" ); int answer = KMessageBox::questionYesNo( nullptr, txt, i18n("Trust Time Stamps?") ); QApplication::restoreOverrideCursor(); if ( answer == KMessageBox::Yes ) m_trustTimeStamps = true; else m_trustTimeStamps = false; m_hasAskedAboutTimeStamps = true; } return m_trustTimeStamps; } } //////////////////////////////// //// File Version Detection //// //////////////////////////////// property_copy( detectModifiedFiles , setDetectModifiedFiles , bool , FileVersionDetection, false ) property_copy( modifiedFileComponent , setModifiedFileComponent , QString , FileVersionDetection, QString() ) property_copy( originalFileComponent , setOriginalFileComponent , QString , FileVersionDetection, QString() ) property_copy( moveOriginalContents , setMoveOriginalContents , bool , FileVersionDetection, false ) property_copy( autoStackNewFiles , setAutoStackNewFiles , bool , FileVersionDetection, true ) property_copy( copyFileComponent , setCopyFileComponent , QString , FileVersionDetection, "(.[^.]+)$" ) property_copy( copyFileReplacementComponent , setCopyFileReplacementComponent , QString , FileVersionDetection, "-edited\\1") //////////////////// //// Thumbnails //// //////////////////// property_copy( displayLabels , setDisplayLabels , bool , Thumbnails, true ) property_copy( displayCategories , setDisplayCategories , bool , Thumbnails, false ) property_copy( autoShowThumbnailView , setAutoShowThumbnailView , int , Thumbnails, 0 ) property_copy( showNewestThumbnailFirst, setShowNewestFirst , bool , Thumbnails, false ) property_copy( thumbnailDisplayGrid , setThumbnailDisplayGrid , bool , Thumbnails, false ) property_copy( previewSize , setPreviewSize , int , Thumbnails, 256 ) property_copy( thumbnailSpace , setThumbnailSpace , int , Thumbnails, 4 ) // not available via GUI, but should be consistent (and maybe confgurable for powerusers): property_copy( minimumThumbnailSize , setMinimumThumbnailSize , int , Thumbnails, 32 ) property_copy( maximumThumbnailSize , setMaximumThumbnailSize , int , Thumbnails, 4096 ) property_enum( thumbnailAspectRatio , setThumbnailAspectRatio , ThumbnailAspectRatio, Thumbnails, Aspect_4_3 ) property_ref( backgroundColor , setBackgroundColor , QString , Thumbnails, QColor(Qt::darkGray).name() ) property_copy( incrementalThumbnails , setIncrementalThumbnails , bool , Thumbnails, true ) // database specific so that changing it doesn't invalidate the thumbnail cache for other databases: getValueFunc_( int, thumbnailSize, groupForDatabase("Thumbnails"), "thumbSize", 150) void SettingsData::setThumbnailSize( int value ) { // enforce limits: value = qBound( minimumThumbnailSize(), value, maximumThumbnailSize()); if ( value != thumbnailSize() ) emit thumbnailSizeChanged(value); setValue( groupForDatabase("Thumbnails"), "thumbSize", value ); } int SettingsData::actualThumbnailSize() const \ { // this is database specific since it's a derived value of thumbnailSize int retval = value( groupForDatabase("Thumbnails"), "actualThumbSize", 0 ); // if no value has been set, use thumbnailSize if ( retval == 0 ) retval = thumbnailSize(); return retval; } void SettingsData::setActualThumbnailSize( int value ) { QPixmapCache::clear(); // enforce limits: value = qBound( minimumThumbnailSize(), value, thumbnailSize()); if ( value != actualThumbnailSize()) { setValue( groupForDatabase("Thumbnails"), "actualThumbSize", value ); emit actualThumbnailSizeChanged(value); } } //////////////// //// Viewer //// //////////////// property_ref ( viewerSize , setViewerSize , QSize , Viewer, QSize(1024,768) ) property_ref ( slideShowSize , setSlideShowSize , QSize , Viewer, QSize(1024,768) ) property_copy( launchViewerFullScreen , setLaunchViewerFullScreen , bool , Viewer, false ) property_copy( launchSlideShowFullScreen, setLaunchSlideShowFullScreen, bool , Viewer, false ) property_copy( showInfoBox , setShowInfoBox , bool , Viewer, true ) property_copy( showLabel , setShowLabel , bool , Viewer, true ) property_copy( showDescription , setShowDescription , bool , Viewer, true ) property_copy( showDate , setShowDate , bool , Viewer, true ) property_copy( showImageSize , setShowImageSize , bool , Viewer, true ) property_copy( showRating , setShowRating , bool , Viewer, true ) property_copy( showTime , setShowTime , bool , Viewer, true ) property_copy( showFilename , setShowFilename , bool , Viewer, false ) property_copy( showEXIF , setShowEXIF , bool , Viewer, true ) property_copy( slideShowInterval , setSlideShowInterval , int , Viewer, 5 ) property_copy( viewerCacheSize , setViewerCacheSize , int , Viewer, 195 ) property_copy( infoBoxWidth , setInfoBoxWidth , int , Viewer, 400 ) property_copy( infoBoxHeight , setInfoBoxHeight , int , Viewer, 300 ) property_enum( infoBoxPosition , setInfoBoxPosition , Position , Viewer, Bottom ) property_enum( viewerStandardSize , setViewerStandardSize , StandardViewSize, Viewer, FullSize ) bool SettingsData::smoothScale() const { return _smoothScale; } void SettingsData::setSmoothScale( bool b ) { _smoothScale = b; setValue( "Viewer", "smoothScale", b ); } //////////////////// //// Categories //// //////////////////// setValueFunc( setAlbumCategory,QString&, General,albumCategory ) QString SettingsData::albumCategory() const { QString category = value( "General", "albumCategory", STR("") ); if ( !DB::ImageDB::instance()->categoryCollection()->categoryNames().contains( category ) ) { category = DB::ImageDB::instance()->categoryCollection()->categoryNames()[0]; const_cast(this)->setAlbumCategory( category ); } return category; } property_ref( untaggedCategory, setUntaggedCategory, QString, General, i18n("Events")) property_ref( untaggedTag, setUntaggedTag, QString, General, i18n("untagged")) property_copy( untaggedImagesTagVisible, setUntaggedImagesTagVisible, bool, General, false) ////////////// //// Exif //// ////////////// property_sset( exifForViewer, setExifForViewer, Exif, StringSet() ) property_sset( exifForDialog, setExifForDialog, Exif, Exif::Info::instance()->standardKeys() ) property_ref ( iptcCharset , setIptcCharset , QString, Exif, QString() ) ///////////////////// //// Exif Import //// ///////////////////// property_copy( updateExifData , setUpdateExifData , bool , ExifImport, true ) property_copy( updateImageDate , setUpdateImageDate , bool , ExifImport, false ) property_copy( useModDateIfNoExif , setUseModDateIfNoExif , bool , ExifImport, true ) property_copy( updateOrientation , setUpdateOrientation , bool , ExifImport, false ) property_copy( updateDescription , setUpdateDescription , bool , ExifImport, false ) /////////////////////// //// Miscellaneous //// /////////////////////// property_copy( delayLoadingPlugins, setDelayLoadingPlugins, bool, Plug-ins, true ) property_ref_( HTMLBaseDir, setHTMLBaseDir, QString, groupForDatabase( "HTML Settings" ), QString::fromLocal8Bit(qgetenv( "HOME" )) + STR( "/public_html" ) ) property_ref_( HTMLBaseURL, setHTMLBaseURL, QString, groupForDatabase( "HTML Settings" ), STR( "file://" ) + HTMLBaseDir() ) property_ref_( HTMLDestURL, setHTMLDestURL, QString, groupForDatabase( "HTML Settings" ), STR( "file://" ) + HTMLBaseDir() ) property_ref_( HTMLCopyright, setHTMLCopyright, QString, groupForDatabase( "HTML Settings" ), STR( "" ) ) property_ref_( HTMLDate, setHTMLDate, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTMLTheme, setHTMLTheme, int, groupForDatabase( "HTML Settings" ), -1 ) property_ref_( HTMLKimFile, setHTMLKimFile, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTMLInlineMovies, setHTMLInlineMovies, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTML5Video, setHTML5Video, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTML5VideoGenerate, setHTML5VideoGenerate, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTMLThumbSize, setHTMLThumbSize, int, groupForDatabase( "HTML Settings" ), 128 ) property_ref_( HTMLNumOfCols, setHTMLNumOfCols, int, groupForDatabase( "HTML Settings" ), 5 ) property_ref_( HTMLSizes, setHTMLSizes, QString, groupForDatabase( "HTML Settings" ), STR("") ) property_ref_( HTMLIncludeSelections, setHTMLIncludeSelections, QString, groupForDatabase( "HTML Settings" ), STR("") ) property_ref_( password, setPassword, QString, groupForDatabase( "Privacy Settings" ), STR("") ) QDate SettingsData::fromDate() const { QString date = value( "Miscellaneous", "fromDate", STR("") ); return date.isEmpty() ? QDate( QDate::currentDate().year(), 1, 1 ) : QDate::fromString( date, Qt::ISODate ); } void SettingsData::setFromDate( const QDate& date) { if (date.isValid()) setValue( "Miscellaneous", "fromDate", date.toString( Qt::ISODate ) ); } QDate SettingsData::toDate() const { QString date = value( "Miscellaneous", "toDate", STR("") ); return date.isEmpty() ? QDate( QDate::currentDate().year()+1, 1, 1 ) : QDate::fromString( date, Qt::ISODate ); } void SettingsData::setToDate( const QDate& date) { if (date.isValid()) setValue( "Miscellaneous", "toDate", date.toString( Qt::ISODate ) ); } QString SettingsData::imageDirectory() const { return m_imageDirectory; } QString SettingsData::groupForDatabase( const char* setting ) const { return STR("%1 - %2").arg( STR(setting) ).arg( imageDirectory() ); } DB::ImageSearchInfo SettingsData::currentLock() const { return DB::ImageSearchInfo::loadLock(); } void SettingsData::setCurrentLock( const DB::ImageSearchInfo& info, bool exclude ) { info.saveLock(); setValue( groupForDatabase( "Privacy Settings" ), "exclude", exclude ); } bool SettingsData::lockExcludes() const { return value( groupForDatabase( "Privacy Settings" ), "exclude", false ); } getValueFunc_( bool,locked, groupForDatabase("Privacy Settings"),"locked",false ) void SettingsData::setLocked( bool lock, bool force ) { if ( lock == locked() && !force ) return; setValue( groupForDatabase( "Privacy Settings" ), "locked", lock ); emit locked( lock, lockExcludes() ); } void SettingsData::setWindowGeometry( WindowType win, const QRect& geometry ) { setValue( "Window Geometry", win, geometry ); } QRect SettingsData::windowGeometry( WindowType win ) const { return value( "Window Geometry", win, QRect(0,0,800,600) ); } bool Settings::SettingsData::hasUntaggedCategoryFeatureConfigured() const { return DB::ImageDB::instance()->categoryCollection()->categoryNames().contains( untaggedCategory() ) && DB::ImageDB::instance()->categoryCollection()->categoryForName( untaggedCategory())->items().contains( untaggedTag() ); } double Settings::SettingsData::getThumbnailAspectRatio() const { double ratio = 1.0; switch (Settings::SettingsData::instance()->thumbnailAspectRatio()) { case Settings::Aspect_16_9: ratio = 9.0 / 16; break; case Settings::Aspect_4_3: ratio = 3.0 / 4; break; case Settings::Aspect_3_2: ratio = 2.0 / 3; break; case Settings::Aspect_9_16: ratio = 16 / 9.0; break; case Settings::Aspect_3_4: ratio = 4 / 3.0; break; case Settings::Aspect_2_3: ratio = 3 / 2.0; break; case Settings::Aspect_1_1: ratio = 1.0; break; } return ratio; } QStringList Settings::SettingsData::EXIFCommentsToStrip() { return m_EXIFCommentsToStrip; } void Settings::SettingsData::setEXIFCommentsToStrip(QStringList EXIFCommentsToStrip) { m_EXIFCommentsToStrip = EXIFCommentsToStrip; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/Util.cpp b/Utilities/Util.cpp index 3689c892..22fa69d6 100644 --- a/Utilities/Util.cpp +++ b/Utilities/Util.cpp @@ -1,854 +1,854 @@ /* 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 "Util.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include "JpeglibWithFix.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include #include #include #include #include #include #include #include } namespace { // Determined experimentally to yield best results (on Seagate 2TB 2.5" disk, // 5400 RPM). Performance is very similar at 524288. Above that, performance // was significantly worse. Below that, performance also deteriorated. // This assumes use of one image scout thread (see DB/ImageScout.cpp). Without // a scout thread, performance was about 10-15% worse. constexpr int MD5_BUFFER_SIZE = 262144; } /** * Add a line label + info text to the result text if info is not empty. * If the result already contains something, a HTML newline is added first. * To be used in createInfoText(). */ static void AddNonEmptyInfo(const QString &label, const QString &info, QString *result) { if (info.isEmpty()) return; if (!result->isEmpty()) *result += QString::fromLatin1("
"); result->append(label).append(info); } /** * Given an ImageInfoPtr this function will create an HTML blob about the * image. The blob is used in the viewer and in the tool tip box from the * thumbnail view. * * As the HTML text is created, the parameter linkMap is filled with * information about hyperlinks. The map maps from an index to a pair of * (categoryName, categoryItem). This linkMap is used when the user selects * one of the hyberlinks. */ QString Utilities::createInfoText( DB::ImageInfoPtr info, QMap< int,QPair >* linkMap ) { Q_ASSERT( info ); QString result; if ( Settings::SettingsData::instance()->showFilename() ) { AddNonEmptyInfo(i18n("File Name: "), info->fileName().relative(), &result); } if ( Settings::SettingsData::instance()->showDate() ) { AddNonEmptyInfo(i18n("Date: "), info->date().toString( Settings::SettingsData::instance()->showTime() ? true : false ), &result); } /* XXX */ if ( Settings::SettingsData::instance()->showImageSize() && info->mediaType() == DB::Image) { const QSize imageSize = info->size(); // Do not add -1 x -1 text if (imageSize.width() >= 0 && imageSize.height() >= 0) { const double megapix = imageSize.width() * imageSize.height() / 1000000.0; QString info = i18nc("width x height","%1x%2" ,QString::number(imageSize.width()) ,QString::number(imageSize.height())); if (megapix > 0.05) { info += i18nc("short for: x megapixels"," (%1MP)" ,QString::number(megapix, 'f', 1)); } const double aspect = (double) imageSize.width() / (double) imageSize.height(); if (aspect > 1) info += i18nc("aspect ratio"," (%1:1)" ,QLocale::system().toString(aspect, 'f', 2)); else if (aspect >= 0.995 && aspect < 1.005) info += i18nc("aspect ratio"," (1:1)"); else info += i18nc("aspect ratio"," (1:%1)" ,QLocale::system().toString(1.0/aspect, 'f', 2)); AddNonEmptyInfo(i18n("Image Size: "), info, &result); } } if ( Settings::SettingsData::instance()->showRating() ) { if ( info->rating() != -1 ) { if ( ! result.isEmpty() ) result += QString::fromLatin1("
"); QUrl rating; rating.setScheme(QString::fromLatin1("kratingwidget")); // we don't use the host part, but if we don't set it, we can't use port: rating.setHost(QString::fromLatin1("int")); rating.setPort(qMin( qMax( static_cast(0), info->rating() ), static_cast(10))); result += QString::fromLatin1("").arg( rating.toString(QUrl::None) ); } } QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); int link = 0; Q_FOREACH( const DB::CategoryPtr category, categories ) { const QString categoryName = category->name(); if ( category->doShow() ) { StringSet items = info->itemsOfCategory( categoryName ); if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && ! Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (categoryName == Settings::SettingsData::instance()->untaggedCategory()) { if (items.contains(Settings::SettingsData::instance()->untaggedTag())) { items.remove(Settings::SettingsData::instance()->untaggedTag()); } } } if (!items.empty()) { QString title = QString::fromUtf8("%1: ").arg(category->name()); QString infoText; bool first = true; Q_FOREACH( const QString &item, items) { if ( first ) first = false; else infoText += QString::fromLatin1( ", " ); if ( linkMap ) { ++link; (*linkMap)[link] = QPair( categoryName, item ); infoText += QString::fromLatin1( "%2").arg( link ).arg( item ); infoText += formatAge(category, item, info); } else infoText += item; } AddNonEmptyInfo(title, infoText, &result); } } } if ( Settings::SettingsData::instance()->showLabel()) { AddNonEmptyInfo(i18n("Label: "), info->label(), &result); } if ( Settings::SettingsData::instance()->showDescription() && !info->description().trimmed().isEmpty() ) { AddNonEmptyInfo(i18n("Description: "), info->description(), &result); } QString exifText; if ( Settings::SettingsData::instance()->showEXIF() ) { typedef QMap ExifMap; typedef ExifMap::const_iterator ExifMapIterator; ExifMap exifMap = Exif::Info::instance()->infoForViewer( info->fileName(), Settings::SettingsData::instance()->iptcCharset() ); for( ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt ) { if ( exifIt.key().startsWith( QString::fromLatin1( "Exif." ) ) ) for ( QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt ) { QString exifName = exifIt.key().split( QChar::fromLatin1('.') ).last(); AddNonEmptyInfo(QString::fromLatin1( "%1: ").arg(exifName), *valuesIt, &exifText); } } QString iptcText; for( ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt ) { if ( !exifIt.key().startsWith( QString::fromLatin1( "Exif." ) ) ) for ( QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt ) { QString iptcName = exifIt.key().split( QChar::fromLatin1('.') ).last(); AddNonEmptyInfo(QString::fromLatin1( "%1: ").arg(iptcName), *valuesIt, &iptcText); } } if ( !iptcText.isEmpty() ) { if ( exifText.isEmpty() ) exifText = iptcText; else exifText += QString::fromLatin1( "
" ) + iptcText; } } if ( !result.isEmpty() && !exifText.isEmpty() ) result += QString::fromLatin1( "
" ); result += exifText; return result; } using DateSpec = QPair; DateSpec dateDiff(const QDate& birthDate, const QDate& imageDate) { const int bday = birthDate.day(); const int iday = imageDate.day(); const int bmonth = birthDate.month(); const int imonth = imageDate.month(); const int byear = birthDate.year(); const int iyear = imageDate.year(); // Image before birth const int diff = birthDate.daysTo(imageDate); if (diff < 0) return qMakePair(0, 'I'); if (diff < 31) return qMakePair(diff, 'D'); int months = (iyear-byear)*12; months += (imonth-bmonth); months += (iday >= bday) ? 0 : -1; if ( months < 24) return qMakePair(months, 'M'); else return qMakePair(months/12, 'Y'); } QString formatDate(const DateSpec& date) { if (date.second == 'I') return {}; else if (date.second == 'D') return i18np("1 day", "%1 days", date.first); else if (date.second == 'M') return i18np("1 month", "%1 months", date.first); else return i18np("1 year", "%1 years", date.first); } void test() { Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,7,11))) == QString::fromLatin1("0 days")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,8,10))) == QString::fromLatin1("30 days")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,8,11))) == QString::fromLatin1("1 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,8,12))) == QString::fromLatin1("1 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,9,10))) == QString::fromLatin1("1 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1971,9,11))) == QString::fromLatin1("2 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,6,10))) == QString::fromLatin1("10 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,6,11))) == QString::fromLatin1("11 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,6,12))) == QString::fromLatin1("11 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,7,10))) == QString::fromLatin1("11 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,7,11))) == QString::fromLatin1("12 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,7,12))) == QString::fromLatin1("12 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1972,12,11))) == QString::fromLatin1("17 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971,7,11), QDate(1973,7,11))) == QString::fromLatin1("2 years")); } QString Utilities::formatAge(DB::CategoryPtr category, const QString &item, DB::ImageInfoPtr info) { // test(); // I wish I could get my act together to set up a test suite. const QDate birthDate = category->birthDate(item); const QDate start = info->date().start().date(); const QDate end = info->date().end().date(); if (birthDate.isNull() || start.isNull()) return {}; if ( start == end) return QString::fromUtf8(" (%1)").arg(formatDate(dateDiff(birthDate, start))); else { DateSpec lower = dateDiff(birthDate,start); DateSpec upper = dateDiff(birthDate,end); if (lower == upper) return QString::fromUtf8(" (%1)").arg(formatDate(lower)); else if (lower.second == 'I') return QString::fromUtf8(" (< %1)").arg(formatDate(upper)); else { if (lower.second == upper.second) return QString::fromUtf8(" (%1-%2)").arg(lower.first).arg(formatDate(upper)); else return QString::fromUtf8(" (%1-%2)").arg(formatDate(lower)).arg(formatDate(upper)); } } } void Utilities::checkForBackupFile( const QString& fileName, const QString& message ) { QString backupName = QFileInfo( fileName ).absolutePath() + QString::fromLatin1("/.#") + QFileInfo( fileName ).fileName(); QFileInfo backUpFile( backupName); QFileInfo indexFile( fileName ); if ( !backUpFile.exists() || indexFile.lastModified() > backUpFile.lastModified() || backUpFile.size() == 0 ) if ( !( backUpFile.exists() && !message.isNull() ) ) return; int code; if ( message.isNull() ) code = KMessageBox::questionYesNo( nullptr, i18n("Autosave file '%1' exists (size %3 KB) and is newer than '%2'. " "Should the autosave file be used?", backupName, fileName, backUpFile.size() >> 10 ), i18n("Found Autosave File") ); else if ( backUpFile.size() > 0 ) code = KMessageBox::warningYesNo( nullptr,i18n( "

Error: Cannot use current database file '%1':

%2

" "

Do you want to use autosave (%3 - size %4 KB) instead of exiting?

" "

(Manually verifying and copying the file might be a good idea)

", fileName, message, backupName, backUpFile.size() >> 10 ), i18n("Recover from Autosave?") ); else { KMessageBox::error( nullptr, i18n( "

Error: %1

Also autosave file is empty, check manually " "if numbered backup files exist and can be used to restore index.xml.

", message ) ); exit(-1); } if ( code == KMessageBox::Yes ) { QFile in( backupName ); if ( in.open( QIODevice::ReadOnly ) ) { QFile out( fileName ); if (out.open( QIODevice::WriteOnly ) ) { char data[1024]; int len; while ( (len = in.read( data, 1024 ) ) ) out.write( data, len ); } } } else if ( !message.isNull() ) exit(-1); } bool Utilities::ctrlKeyDown() { return QApplication::keyboardModifiers() & Qt::ControlModifier; } void Utilities::copyList( const QStringList& from, const QString& directoryTo ) { for( QStringList::ConstIterator it = from.constBegin(); it != from.constEnd(); ++it ) { QString destFile = directoryTo + QString::fromLatin1( "/" ) + QFileInfo(*it).fileName(); if ( ! QFileInfo( destFile ).exists() ) { const bool ok = copy( *it, destFile ); if ( !ok ) { KMessageBox::error( nullptr, i18n("Unable to copy '%1' to '%2'.", *it , destFile ), i18n("Error Running Demo") ); exit(-1); } } } } QString Utilities::setupDemo() { QString demoDir = QString::fromLatin1( "%1/kphotoalbum-demo-%2" ).arg(QDir::tempPath()).arg(QString::fromLocal8Bit( qgetenv( "LOGNAME" ) )); QFileInfo fi(demoDir); if ( ! fi.exists() ) { bool ok = QDir().mkdir( demoDir ); if ( !ok ) { KMessageBox::error( nullptr, i18n("Unable to create directory '%1' needed for demo.", demoDir ), i18n("Error Running Demo") ); exit(-1); } } // index.xml QString demoDB = locateDataFile(QString::fromLatin1("demo/index.xml")); if ( demoDB.isEmpty() ) { qCDebug(UtilitiesLog) << "No demo database in standard locations:" << QStandardPaths::standardLocations(QStandardPaths::DataLocation); exit(-1); } QString configFile = demoDir + QString::fromLatin1( "/index.xml" ); copy(demoDB, configFile); // Images const QStringList kpaDemoDirs = QStandardPaths::locateAll( QStandardPaths::DataLocation, QString::fromLatin1("demo"), QStandardPaths::LocateDirectory); QStringList images; Q_FOREACH(const QString &dir, kpaDemoDirs) { QDirIterator it(dir, QStringList() << QStringLiteral("*.jpg") << QStringLiteral("*.avi")); while (it.hasNext()) { images.append(it.next()); } } copyList( images, demoDir ); // CategoryImages QString catDir = demoDir + QString::fromLatin1("/CategoryImages"); fi = QFileInfo(catDir); if ( ! fi.exists() ) { bool ok = QDir().mkdir( catDir ); if ( !ok ) { KMessageBox::error( nullptr, i18n("Unable to create directory '%1' needed for demo.", catDir ), i18n("Error Running Demo") ); exit(-1); } } const QStringList kpaDemoCatDirs = QStandardPaths::locateAll( QStandardPaths::DataLocation, QString::fromLatin1("demo/CategoryImages"), QStandardPaths::LocateDirectory); QStringList catImages; Q_FOREACH(const QString &dir, kpaDemoCatDirs) { QDirIterator it(dir, QStringList() << QStringLiteral("*.jpg")); while (it.hasNext()) { catImages.append(it.next()); } } copyList( catImages, catDir ); return configFile; } bool Utilities::copy( const QString& from, const QString& to ) { if ( QFileInfo(to).exists()) QDir().remove(to); return QFile::copy(from,to); } bool Utilities::makeHardLink( const QString& from, const QString& to ) { if (link(from.toLocal8Bit().constData(), to.toLocal8Bit().constData()) != 0) return false; else return true; } bool Utilities::makeSymbolicLink( const QString& from, const QString& to ) { if (symlink(from.toLocal8Bit().constData(), to.toLocal8Bit().constData()) != 0) return false; else return true; } bool Utilities::canReadImage( const DB::FileName& fileName ) { bool fastMode = !Settings::SettingsData::instance()->ignoreFileExtension(); QMimeDatabase::MatchMode mode = fastMode ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault; QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile( fileName.absolute(), mode ); return QImageReader::supportedMimeTypes().contains( mimeType.name().toUtf8() ) || ImageManager::ImageDecoder::mightDecode( fileName ); } QString Utilities::locateDataFile(const QString& fileName) { return QStandardPaths::locate(QStandardPaths::DataLocation, fileName); } QString Utilities::readFile( const QString& fileName ) { if ( fileName.isEmpty() ) { KMessageBox::error( nullptr, i18n("

No file name given!

") ); return QString(); } QFile file( fileName ); if ( !file.open( QIODevice::ReadOnly ) ) { //KMessageBox::error( nullptr, i18n("Could not open file %1").arg( fileName ) ); return QString(); } QTextStream stream( &file ); QString content = stream.readAll(); file.close(); return content; } struct myjpeg_error_mgr : public jpeg_error_mgr { jmp_buf setjmp_buffer; }; extern "C" { static void myjpeg_error_exit(j_common_ptr cinfo) { myjpeg_error_mgr* myerr = (myjpeg_error_mgr*) cinfo->err; char buffer[JMSG_LENGTH_MAX]; (*cinfo->err->format_message)(cinfo, buffer); //kWarning() << buffer; longjmp(myerr->setjmp_buffer, 1); } } namespace Utilities { static bool loadJPEGInternal(QImage *img, FILE* inputFile, QSize* fullSize, int dim, const char *membuf, size_t membuf_size ); } bool Utilities::loadJPEG(QImage *img, const DB::FileName& imageFile, QSize* fullSize, int dim, char *membuf, size_t membufSize) { bool ok; struct stat statbuf; if ( stat( QFile::encodeName(imageFile.absolute()).constData(), &statbuf ) == -1 ) return false; if ( ! membuf || statbuf.st_size > (int) membufSize ) { qCDebug(UtilitiesLog) << "loadJPEG (slow path) " << imageFile.relative() << " " << statbuf.st_size << " " << membufSize; FILE* inputFile=fopen( QFile::encodeName(imageFile.absolute()).constData(), "rb"); if(!inputFile) return false; ok = loadJPEGInternal( img, inputFile, fullSize, dim, NULL, 0 ); fclose(inputFile); } else { // May be more than is needed, but less likely to fragment memory this // way. int inputFD = open( QFile::encodeName(imageFile.absolute()).constData(), O_RDONLY ); if ( inputFD == -1 ) { return false; } unsigned bytesLeft = statbuf.st_size; unsigned offset = 0; while ( bytesLeft > 0 ) { int bytes = read(inputFD, membuf + offset, bytesLeft); if (bytes <= 0) { (void) close(inputFD); return false; } offset += bytes; bytesLeft -= bytes; } ok = loadJPEGInternal( img, NULL, fullSize, dim, membuf, statbuf.st_size ); (void) close(inputFD); } return ok; } bool Utilities::loadJPEG(QImage *img, const DB::FileName& imageFile, QSize* fullSize, int dim) { return loadJPEG( img, imageFile, fullSize, dim, NULL, 0 ); } bool Utilities::loadJPEG(QImage *img, const QByteArray &data, QSize* fullSize, int dim) { return loadJPEGInternal(img, nullptr, fullSize, dim, data.data(), data.size()); } bool Utilities::loadJPEGInternal(QImage *img, FILE* inputFile, QSize* fullSize, int dim, const char *membuf, size_t membuf_size ) { struct jpeg_decompress_struct cinfo; struct myjpeg_error_mgr jerr; // JPEG error handling - thanks to Marcus Meissner cinfo.err = jpeg_std_error(&jerr); cinfo.err->error_exit = myjpeg_error_exit; if (setjmp(jerr.setjmp_buffer)) { jpeg_destroy_decompress(&cinfo); return false; } jpeg_create_decompress(&cinfo); if (inputFile) jpeg_stdio_src(&cinfo, inputFile); else jpeg_mem_src(&cinfo, (unsigned char *) membuf, membuf_size); jpeg_read_header(&cinfo, TRUE); *fullSize = QSize( cinfo.image_width, cinfo.image_height ); int imgSize = qMax(cinfo.image_width, cinfo.image_height); //libjpeg supports a sort of scale-while-decoding which speeds up decoding int scale=1; if (dim != -1) { while(dim*scale*2<=imgSize) { scale*=2; } if(scale>8) scale=8; } cinfo.scale_num=1; cinfo.scale_denom=scale; // Create QImage jpeg_start_decompress(&cinfo); switch(cinfo.output_components) { case 3: case 4: *img = QImage( cinfo.output_width, cinfo.output_height, QImage::Format_RGB32); if (img->isNull()) return false; break; case 1: // B&W image *img = QImage( cinfo.output_width, cinfo.output_height, QImage::Format_Indexed8); if (img->isNull()) return false; img->setColorCount(256); for (int i=0; i<256; i++) img->setColor(i, qRgb(i,i,i)); break; default: return false; } QVector linesVector; linesVector.reserve(img->height()); for (int i = 0; i < img->height(); ++i) linesVector.push_back(img->scanLine(i)); uchar** lines = linesVector.data(); while (cinfo.output_scanline < cinfo.output_height) jpeg_read_scanlines(&cinfo, lines + cinfo.output_scanline, cinfo.output_height); jpeg_finish_decompress(&cinfo); // Expand 24->32 bpp if ( cinfo.output_components == 3 ) { for (uint j=0; jscanLine(j) + cinfo.output_width*3; - QRgb *out = (QRgb*)( img->scanLine(j) ); + QRgb *out = reinterpret_cast( img->scanLine(j) ); for (uint i=cinfo.output_width; i--; ) { in-=3; out[i] = qRgb(in[0], in[1], in[2]); } } } /*int newMax = qMax(cinfo.output_width, cinfo.output_height); int newx = size_*cinfo.output_width / newMax; int newy = size_*cinfo.output_height / newMax;*/ jpeg_destroy_decompress(&cinfo); //image = img.smoothScale(newx,newy); return true; } bool Utilities::isJPEG( const DB::FileName& fileName ) { QString format= QString::fromLocal8Bit( QImageReader::imageFormat( fileName.absolute() ) ); return format == QString::fromLocal8Bit( "jpeg" ); } namespace Utilities { QString normalizedFileName( const QString& fileName ) { return QFileInfo(fileName).absoluteFilePath(); } QString dereferenceSymLinks( const QString& fileName ) { QFileInfo fi(fileName); int rounds = 256; while (fi.isSymLink() && --rounds > 0) fi = QFileInfo(fi.readLink()); if (rounds == 0) return QString(); return fi.filePath(); } } QString Utilities::stripEndingForwardSlash( const QString& fileName ) { static QString slash = QString::fromLatin1("/"); if ( fileName.endsWith( slash ) ) return fileName.left( fileName.length()-1); else return fileName; } QString Utilities::relativeFolderName( const QString& fileName) { int index= fileName.lastIndexOf( QChar::fromLatin1('/'), -1); if (index == -1) return QString(); else return fileName.left( index ); } void Utilities::deleteDemo() { QString dir = QString::fromLatin1( "%1/kphotoalbum-demo-%2" ).arg(QDir::tempPath()).arg(QString::fromLocal8Bit( qgetenv( "LOGNAME" ) ) ); QUrl demoUrl = QUrl::fromLocalFile( dir ); KJob *delDemoJob = KIO::del( demoUrl ); KJobWidgets::setWindow( delDemoJob, MainWindow::Window::theMainWindow()); delDemoJob->exec(); } QString Utilities::absoluteImageFileName( const QString& relativeName ) { return stripEndingForwardSlash( Settings::SettingsData::instance()->imageDirectory() ) + QString::fromLatin1( "/" ) + relativeName; } QString Utilities::imageFileNameToAbsolute( const QString& fileName ) { if ( fileName.startsWith( Settings::SettingsData::instance()->imageDirectory() ) ) return fileName; else if ( fileName.startsWith( QString::fromLatin1("file://") ) ) return imageFileNameToAbsolute( fileName.mid( 7 ) ); // 7 == length("file://") else if ( fileName.startsWith( QString::fromLatin1("/") ) ) return QString(); // Not within our image root else return absoluteImageFileName( fileName ); } bool operator>( const QPoint& p1, const QPoint& p2) { return p1.y() > p2.y() || (p1.y() == p2.y() && p1.x() > p2.x() ); } bool operator<( const QPoint& p1, const QPoint& p2) { return p1.y() < p2.y() || ( p1.y() == p2.y() && p1.x() < p2.x() ); } const QSet& Utilities::supportedVideoExtensions() { static QSet videoExtensions; if ( videoExtensions.empty() ) { videoExtensions.insert( QString::fromLatin1( "3gp" ) ); videoExtensions.insert( QString::fromLatin1( "avi" ) ); videoExtensions.insert( QString::fromLatin1( "mp4" ) ); videoExtensions.insert( QString::fromLatin1( "m4v" ) ); videoExtensions.insert( QString::fromLatin1( "mpeg" ) ); videoExtensions.insert( QString::fromLatin1( "mpg" ) ); videoExtensions.insert( QString::fromLatin1( "qt" ) ); videoExtensions.insert( QString::fromLatin1( "mov" ) ); videoExtensions.insert( QString::fromLatin1( "moov" ) ); videoExtensions.insert( QString::fromLatin1( "qtvr" ) ); videoExtensions.insert( QString::fromLatin1( "rv" ) ); videoExtensions.insert( QString::fromLatin1( "3g2" ) ); videoExtensions.insert( QString::fromLatin1( "fli" ) ); videoExtensions.insert( QString::fromLatin1( "flc" ) ); videoExtensions.insert( QString::fromLatin1( "mkv" ) ); videoExtensions.insert( QString::fromLatin1( "mng" ) ); videoExtensions.insert( QString::fromLatin1( "asf" ) ); videoExtensions.insert( QString::fromLatin1( "asx" ) ); videoExtensions.insert( QString::fromLatin1( "wmp" ) ); videoExtensions.insert( QString::fromLatin1( "wmv" ) ); videoExtensions.insert( QString::fromLatin1( "ogm" ) ); videoExtensions.insert( QString::fromLatin1( "rm" ) ); videoExtensions.insert( QString::fromLatin1( "flv" ) ); videoExtensions.insert( QString::fromLatin1( "webm" ) ); videoExtensions.insert( QString::fromLatin1( "mts" ) ); videoExtensions.insert( QString::fromLatin1( "ogg" ) ); videoExtensions.insert( QString::fromLatin1( "ogv" ) ); videoExtensions.insert( QString::fromLatin1( "m2ts" ) ); } return videoExtensions; } bool Utilities::isVideo( const DB::FileName& fileName ) { QFileInfo fi( fileName.relative() ); QString ext = fi.suffix().toLower(); return supportedVideoExtensions().contains( ext ); } bool Utilities::isRAW( const DB::FileName& fileName ) { return ImageManager::RAWImageDecoder::isRAW( fileName ); } QImage Utilities::scaleImage(const QImage &image, int w, int h, Qt::AspectRatioMode mode ) { return image.scaled( w, h, mode, Settings::SettingsData::instance()->smoothScale() ? Qt::SmoothTransformation : Qt::FastTransformation ); } QImage Utilities::scaleImage(const QImage &image, const QSize& s, Qt::AspectRatioMode mode ) { return scaleImage( image, s.width(), s.height(), mode ); } QString Utilities::cStringWithEncoding( const char *c_str, const QString& charset ) { QTextCodec* codec = QTextCodec::codecForName( charset.toLatin1() ); if (!codec) codec = QTextCodec::codecForLocale(); return codec->toUnicode( c_str ); } DB::MD5 Utilities::MD5Sum( const DB::FileName& fileName ) { DB::MD5 checksum; QFile file( fileName.absolute() ); if ( file.open( QIODevice::ReadOnly ) ) { QCryptographicHash md5calculator(QCryptographicHash::Md5); while ( !file.atEnd() ) { QByteArray md5Buffer( file.read( MD5_BUFFER_SIZE ) ); md5calculator.addData( md5Buffer ); } file.close(); checksum = DB::MD5(QString::fromLatin1(md5calculator.result().toHex())); } return checksum; } QColor Utilities::contrastColor( const QColor& col ) { if ( col.red() < 127 && col.green() < 127 && col.blue() < 127 ) return Qt::white; else return Qt::black; } void Utilities::saveImage( const DB::FileName& fileName, const QImage& image, const char* format ) { const QFileInfo info(fileName.absolute()); QDir().mkpath(info.path()); const bool ok = image.save(fileName.absolute(),format); Q_ASSERT(ok); Q_UNUSED(ok); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/org.kde.kphotoalbum-import.desktop b/org.kde.kphotoalbum-import.desktop index 127cc32a..af82a687 100644 --- a/org.kde.kphotoalbum-import.desktop +++ b/org.kde.kphotoalbum-import.desktop @@ -1,119 +1,120 @@ [Desktop Entry] Name=KPhotoAlbum Name[ast]=KPhotoAlbum Name[be]=KPhotoAlbum Name[bg]=KPhotoAlbum Name[bs]=KPhotoAlbum Name[ca]=KPhotoAlbum Name[ca@valencia]=KPhotoAlbum Name[cs]=KPhotoAlbum Name[da]=KPhotoAlbum Name[de]=KPhotoAlbum Name[el]=KPhotoAlbum Name[en_GB]=KPhotoAlbum Name[eo]=KPhotoAlbum Name[es]=KPhotoAlbum Name[et]=KPhotoAlbum +Name[eu]=KPhotoAlbum Name[fi]=KPhotoAlbum Name[fr]=KPhotoAlbum Name[ga]=KPhotoAlbum Name[gl]=KPhotoAlbum Name[he]=KPhotoAlbum Name[hi]=केफोटोएलबम Name[hne]=केफोटोएलबम Name[hr]=KPhotoAlbum Name[hu]=KPhotoAlbum Name[is]=KPhotoAlbum Name[it]=KPhotoAlbum Name[ja]=KPhotoAlbum Name[km]=KPhotoAlbum Name[ko]=KPhotoAlbum Name[ku]=KPhotoAlbum Name[lt]=KPhotoAlbum Name[lv]=KPhotoAlbum Name[ml]=കെഫോട്ടോആല്‍ബം Name[mr]=के-फोटो-अल्बम Name[ms]=KPhotoAlbum Name[nb]=KPhotoAlbum Name[nds]=KPhotoAlbum Name[nl]=KPhotoAlbum Name[nn]=KPhotoAlbum Name[pa]=ਕੇ-ਫੋਟੋ-ਐਲਬਮ Name[pl]=KPhotoAlbum Name[pt]=KPhotoAlbum Name[pt_BR]=KPhotoAlbum Name[ro]=KAlbumFoto Name[ru]=KPhotoAlbum Name[sk]=KPhotoAlbum Name[sl]=KPhotoAlbum Name[sv]=Kfotoalbum Name[tr]=KPhotoAlbum Name[ug]=KPhotoAlbum Name[uk]=KPhotoAlbum Name[vi]=KPhotoAlbum Name[x-test]=xxKPhotoAlbumxx Name[zh_CN]=KPhotoAlbum Name[zh_TW]=KPhotoAlbum Exec=kphotoalbum --import %u GenericName=Photo Album GenericName[be]=Фотаальбом GenericName[bg]=KPhotoAlbum GenericName[bs]=Foto album GenericName[ca]=Àlbum de fotografies GenericName[ca@valencia]=Àlbum de fotografies GenericName[cs]=Foto album GenericName[da]=Fotoalbum GenericName[de]=Fotoalbum GenericName[el]=Φωτογραφικό άλμπουμ GenericName[en_GB]=Photo Album GenericName[eo]=Albumo de Fotoj GenericName[es]=Álbum de fotos GenericName[et]=Fotoalbum -GenericName[eu]=Argazkien albuma +GenericName[eu]=Argazki albuma GenericName[fi]=Valokuva-albumi GenericName[fr]=Album de photos GenericName[ga]=Albam Grianghraf GenericName[gl]=Álbum de fotos GenericName[he]=אלבום תמונות GenericName[hi]=फोटो एलबम GenericName[hne]=फोटो एलबम GenericName[hr]=Fotografski album GenericName[hu]=Fényképalbum GenericName[is]=Myndaalbúm GenericName[it]=Album fotografico GenericName[ja]=フォトアルバム GenericName[km]=អាល់ប៊ុម​រូបថត GenericName[ko]=사진 앨범 GenericName[ku]=Albûma Wêneyan GenericName[lt]=Nuotraukų albumas GenericName[lv]=Foto albums GenericName[ml]=ഫോട്ടോ ആല്‍ബം GenericName[mr]=फोटो अल्बम GenericName[ms]=Album Gambar GenericName[nb]=Fotoalbum GenericName[nds]=Fotoalbum GenericName[nl]=Fotoalbum GenericName[nn]=Fotoalbum GenericName[pa]=ਫੋਟੋ ਐਲਬਮ GenericName[pl]=Album na zdjęcia GenericName[pt]=Álbum de Fotografias GenericName[pt_BR]=Álbum de fotografias GenericName[ro]=Album foto GenericName[ru]=Работа с фотоальбомом GenericName[sk]=Album fotiek GenericName[sl]=Album fotografij GenericName[sv]=Fotoalbum GenericName[tr]=Fotoğraf Albümü GenericName[ug]=سۈرەت ئالبومى GenericName[uk]=Фотоальбом GenericName[vi]=Tập ảnh chụp GenericName[x-test]=xxPhoto Albumxx GenericName[zh_CN]=相册 GenericName[zh_TW]=相簿 Terminal=false Type=Application X-KDE-StartupNotify=true Icon=kphotoalbum Categories=Qt;KDE;Graphics; NoDisplay=true MimeType=application/vnd.kde.kphotoalbum-import; diff --git a/org.kde.kphotoalbum.appdata.xml b/org.kde.kphotoalbum.appdata.xml index c4df1140..3e6e3cdd 100644 --- a/org.kde.kphotoalbum.appdata.xml +++ b/org.kde.kphotoalbum.appdata.xml @@ -1,95 +1,94 @@ 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 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 citra 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 screen in KPhotoAlbum + The main screen in KPhotoAlbum La pantalla principal del KPhotoAlbum La pantalla principal del KPhotoAlbum - The main screen screen in 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 - Layar layar utama dalam KPhotoAlbum Het hoofdscherm in KPhotoAlbum - O ecrã principal do KPhotoAlbum + O ecrã principal no KPhotoAlbum A tela principal do KPhotoAlbum Huvudskärmen i Kfotoalbum Знімок головного вікна KPhotoAlbum - xxThe main screen screen in KPhotoAlbumxx + 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/org.kde.kphotoalbum.desktop b/org.kde.kphotoalbum.desktop index b64a7dff..cc7dfae3 100644 --- a/org.kde.kphotoalbum.desktop +++ b/org.kde.kphotoalbum.desktop @@ -1,118 +1,119 @@ [Desktop Entry] Name=KPhotoAlbum Name[ast]=KPhotoAlbum Name[be]=KPhotoAlbum Name[bg]=KPhotoAlbum Name[bs]=KPhotoAlbum Name[ca]=KPhotoAlbum Name[ca@valencia]=KPhotoAlbum Name[cs]=KPhotoAlbum Name[da]=KPhotoAlbum Name[de]=KPhotoAlbum Name[el]=KPhotoAlbum Name[en_GB]=KPhotoAlbum Name[eo]=KPhotoAlbum Name[es]=KPhotoAlbum Name[et]=KPhotoAlbum +Name[eu]=KPhotoAlbum Name[fi]=KPhotoAlbum Name[fr]=KPhotoAlbum Name[ga]=KPhotoAlbum Name[gl]=KPhotoAlbum Name[he]=KPhotoAlbum Name[hi]=केफोटोएलबम Name[hne]=केफोटोएलबम Name[hr]=KPhotoAlbum Name[hu]=KPhotoAlbum Name[is]=KPhotoAlbum Name[it]=KPhotoAlbum Name[ja]=KPhotoAlbum Name[km]=KPhotoAlbum Name[ko]=KPhotoAlbum Name[ku]=KPhotoAlbum Name[lt]=KPhotoAlbum Name[lv]=KPhotoAlbum Name[ml]=കെഫോട്ടോആല്‍ബം Name[mr]=के-फोटो-अल्बम Name[ms]=KPhotoAlbum Name[nb]=KPhotoAlbum Name[nds]=KPhotoAlbum Name[nl]=KPhotoAlbum Name[nn]=KPhotoAlbum Name[pa]=ਕੇ-ਫੋਟੋ-ਐਲਬਮ Name[pl]=KPhotoAlbum Name[pt]=KPhotoAlbum Name[pt_BR]=KPhotoAlbum Name[ro]=KAlbumFoto Name[ru]=KPhotoAlbum Name[sk]=KPhotoAlbum Name[sl]=KPhotoAlbum Name[sv]=Kfotoalbum Name[tr]=KPhotoAlbum Name[ug]=KPhotoAlbum Name[uk]=KPhotoAlbum Name[vi]=KPhotoAlbum Name[x-test]=xxKPhotoAlbumxx Name[zh_CN]=KPhotoAlbum Name[zh_TW]=KPhotoAlbum Exec=kphotoalbum GenericName=Photo Album GenericName[be]=Фотаальбом GenericName[bg]=KPhotoAlbum GenericName[bs]=Foto album GenericName[ca]=Àlbum de fotografies GenericName[ca@valencia]=Àlbum de fotografies GenericName[cs]=Foto album GenericName[da]=Fotoalbum GenericName[de]=Fotoalbum GenericName[el]=Φωτογραφικό άλμπουμ GenericName[en_GB]=Photo Album GenericName[eo]=Albumo de Fotoj GenericName[es]=Álbum de fotos GenericName[et]=Fotoalbum -GenericName[eu]=Argazkien albuma +GenericName[eu]=Argazki albuma GenericName[fi]=Valokuva-albumi GenericName[fr]=Album de photos GenericName[ga]=Albam Grianghraf GenericName[gl]=Álbum de fotos GenericName[he]=אלבום תמונות GenericName[hi]=फोटो एलबम GenericName[hne]=फोटो एलबम GenericName[hr]=Fotografski album GenericName[hu]=Fényképalbum GenericName[is]=Myndaalbúm GenericName[it]=Album fotografico GenericName[ja]=フォトアルバム GenericName[km]=អាល់ប៊ុម​រូបថត GenericName[ko]=사진 앨범 GenericName[ku]=Albûma Wêneyan GenericName[lt]=Nuotraukų albumas GenericName[lv]=Foto albums GenericName[ml]=ഫോട്ടോ ആല്‍ബം GenericName[mr]=फोटो अल्बम GenericName[ms]=Album Gambar GenericName[nb]=Fotoalbum GenericName[nds]=Fotoalbum GenericName[nl]=Fotoalbum GenericName[nn]=Fotoalbum GenericName[pa]=ਫੋਟੋ ਐਲਬਮ GenericName[pl]=Album na zdjęcia GenericName[pt]=Álbum de Fotografias GenericName[pt_BR]=Álbum de fotografias GenericName[ro]=Album foto GenericName[ru]=Работа с фотоальбомом GenericName[sk]=Album fotiek GenericName[sl]=Album fotografij GenericName[sv]=Fotoalbum GenericName[tr]=Fotoğraf Albümü GenericName[ug]=سۈرەت ئالبومى GenericName[uk]=Фотоальбом GenericName[vi]=Tập ảnh chụp GenericName[x-test]=xxPhoto Albumxx GenericName[zh_CN]=相册 GenericName[zh_TW]=相簿 Terminal=false Type=Application X-KDE-StartupNotify=true Icon=kphotoalbum X-DocPath=kphotoalbum/index.html Categories=Qt;KDE;Graphics;Photography; diff --git a/scripts/org.kde.kphotoalbum.open-raw.desktop b/scripts/org.kde.kphotoalbum.open-raw.desktop index fd03147c..d0fafec0 100644 --- a/scripts/org.kde.kphotoalbum.open-raw.desktop +++ b/scripts/org.kde.kphotoalbum.open-raw.desktop @@ -1,86 +1,88 @@ # Copyright 2012 Miika Turkia # # Copy this file to /usr/local/share/applications/ # Update the Exec and Icon variables as appropriate # Default icon comes from kipi-plugins-common package [Desktop Entry] Version=1.0 Name=Open in RAW editor Name[bs]=Otvori RAW editor Name[ca]=Obre en un editor RAW Name[ca@valencia]=Obri en un editor RAW Name[cs]=Otevřít v editoru RAW Name[da]=Åbn i RAW-editor Name[de]=Im RAW-Editor öffnen Name[el]=Άνοιγμα σε επεξεργαστή RAW Name[en_GB]=Open in RAW editor Name[es]=Abrir en el editor RAW Name[et]=Avamine toorfailide redaktoris +Name[eu]=Ireki RAW editorean Name[fi]=Avaa RAW-ohjelmassa Name[fr]=Ouvrir dans un éditeur de fichiers bruts Name[ga]=Oscail in eagarthóir RAW Name[gl]=Abrir nun editor de RAW Name[hu]=Megnyitás RAW szerkesztőben Name[is]=Opna í RAW-myndvinnsluforriti Name[it]=Apri nell'editor RAW Name[km]=បើក​​ក្នុង​កម្មវិធី​កែសម្រួល RAW Name[ko]=RAW 편집기로 열기 Name[lt]=Atverti RAW redaktoriuje Name[mr]=RAW संपादकात उघडा Name[nb]=Åpne i RAW-redigering Name[nds]=Mit RAW-Editor opmaken Name[nl]=In RAW-bewerker openen Name[nn]=Opna i program for råformatfiler Name[pl]=Otwieranie w edytorze RAW Name[pt]=Abrir no editor RAW Name[pt_BR]=Abrir no editor RAW Name[sk]=Otvoriť v RAW editore Name[sl]=Odpri v urejevalniku RAW Name[sv]=Öppna i editor för obehandlade bilder Name[tr]=RAW resim düzenleyici ile aç Name[uk]=Відкрити у редакторі цифрових негативів Name[x-test]=xxOpen in RAW editorxx Name[zh_TW]=在 RAW 編輯器開啟 Comment=Send RAW files to external editor on command line Comment[bs]=Šalje RAW datoteke vanjskom uređivaču na komandnoj liniji Comment[ca]=Envia els fitxers RAW a un editor extern a través de la línia d'ordres Comment[ca@valencia]=Envia els fitxers RAW a un editor extern a través de la línia d'ordres Comment[cs]=Poslat soubory RAW do externího editoru na příkazovém řádku Comment[da]=Send RAW-filer til ekstern editor på kommandolinjen Comment[de]=RAW-Dateien zu einem externen Editor auf der Befehlszeile senden Comment[el]=Αποστολή RAW αρχείων σε εξωτερικό επεξεργαστή από τη γραμμή εντολών Comment[en_GB]=Send RAW files to external editor on command line Comment[es]=Enviar los archivos RAW a un editor externo por la línea de órdenes Comment[et]=Toorfailide saatmine välisesse redaktorisse käsureal +Comment[eu]=Bidali RAW fitxategiak kanpoko editorera komando lerroaren bidez Comment[fi]=Avaa RAW-tiedostot toisella ohjelmalla Comment[fr]=Envoie les fichiers bruts dans un éditeur externe en ligne de commandes Comment[ga]=Seol comhaid RAW chuig eagarthóir seachtrach ar líne na n-orduithe Comment[gl]=Enviar ficheiros en RAW a un editor externo na liña de ordes Comment[hu]=RAW-fájlok küldése külső szerkesztőbe parancssorból Comment[is]=Opnaðu RAW-skrár í utanaðkomandi myndvinnsluforriti frá skipanalínu Comment[it]=Invia tutti i file RAW all'editor esterno sulla riga di comando Comment[km]=ផ្ញើ​ឯកសារ RAW ទៅ​កម្មវិធី​កែសម្រួល​ខាង​ក្រៅ​នៅ​លើ​បន្ទាត់​ពាក្យ​បញ្ជា Comment[ko]=RAW 파일을 명령행 외부 편집기로 보내기 Comment[lt]=Siųsti RAW failus į Išorinį redaktorių komandos eilutėje Comment[mr]=RAW फाईल्स आदेश ओळीवरील बाहेरच्या संपादकाकडे पाठवा Comment[nb]=Send RAW-filer til ekstern redigerer på kommandolinja Comment[nds]=RAW-Dateien op de Befehlsreeg en extern Editor tostüern Comment[nl]=RAW-bestanden verzenden naar de externe bewerker op de opdrachtregel Comment[nn]=Send råformatfiler til eksternt redigeringsprogram på kommandolinja Comment[pl]=Wyślij pliki RAW do zewnętrznego edytora na wiersz poleceń Comment[pt]=Enviar os ficheiros RAW para o editor externo na linha de comandos Comment[pt_BR]=Enviar os arquivos RAW para o editor externo na linha de comando Comment[sk]=Poslať RAW súbory do externého editora alebo príkazového riadku Comment[sl]=Pošljite datoteke RAW v zunanji urejevalnik preko ukazne vrstice Comment[sv]=Skicka obehandlade bilder till en extern editor på kommandoraden Comment[tr]=RAW dosyaları komut satırından bir dış düzenleyiciye gönderin Comment[uk]=Надіслати файли цифрових негативів до зовнішнього редактора за допомогою командного рядка Comment[x-test]=xxSend RAW files to external editor on command linexx Comment[zh_TW]=以命令列傳送 RAW 檔案到外部編輯器 Exec=open-raw.pl %F Terminal=false Icon=open-raw Type=Application Categories=Graphics;Photography; MimeType=image/x-canon-crw;image/x-canon-cr2;image/x-sony-arw;image/x-nikon-nef;image/x-olympus-orf;image/x-pentax-pef;image/x-adobe-dng;image/x-raw;image/gif;image/tiff;image/jpeg;