diff --git a/AnnotationDialog/DateEdit.cpp b/AnnotationDialog/DateEdit.cpp index 0be6aa81..1ed18372 100644 --- a/AnnotationDialog/DateEdit.cpp +++ b/AnnotationDialog/DateEdit.cpp @@ -1,362 +1,361 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + /** * A date editing widget that consists of an editable combo box. * The combo box contains the date in text form, and clicking the combo * box arrow will display a 'popup' style date picker. * * This widget also supports advanced features like allowing the user * to type in the day name to get the date. The following keywords * are supported (in the native language): tomorrow, yesterday, today, * monday, tuesday, wednesday, thursday, friday, saturday, sunday. * * @author Cornelius Schumacher * @author Mike Pilone * @author David Jarvie * @author Jesper Pedersen */ #include "DateEdit.h" #include #include #include #include #include #include #include #include #include #include #include -#include "DateEdit.moc" - AnnotationDialog::DateEdit::DateEdit( bool isStartEdit, QWidget *parent ) : QComboBox( parent ), m_defaultValue( QDate::currentDate() ), m_ReadOnly(false), m_DiscardNextMousePress(false), m_IsStartEdit( isStartEdit ) { setEditable(true); setMaxCount(1); // need at least one entry for popup to work m_value = m_defaultValue; QString today = QDate::currentDate().toString( QString::fromLatin1("dd. MMM yyyy") ); addItem(QString::fromLatin1( "" ) ); setCurrentIndex(0); setItemText( 0, QString::fromLatin1( "" )); setMinimumSize(sizeHint()); m_DateFrame = new QFrame; m_DateFrame->setWindowFlags(Qt::Popup); QVBoxLayout* layout = new QVBoxLayout(m_DateFrame); m_DateFrame->setFrameStyle(QFrame::StyledPanel | QFrame::Raised); m_DateFrame->setLineWidth(3); m_DateFrame->hide(); m_DateFrame->installEventFilter(this); m_DatePicker = new KDatePicker(m_value, m_DateFrame); layout->addWidget(m_DatePicker); connect(lineEdit(),&QLineEdit::editingFinished,this,&DateEdit::lineEnterPressed); connect(this,&QComboBox::currentTextChanged,this,&DateEdit::slotTextChanged); connect(m_DatePicker, &KDatePicker::dateEntered, this, &DateEdit::dateEntered); connect(m_DatePicker, &KDatePicker::dateSelected, this, &DateEdit::dateSelected); // Create the keyword list. This will be used to match against when the user // enters information. m_KeywordMap[i18n("tomorrow")] = 1; m_KeywordMap[i18n("today")] = 0; m_KeywordMap[i18n("yesterday")] = -1; QString dayName; for (int i = 1; i <= 7; ++i) { dayName = QDate::longDayName(i).toLower(); m_KeywordMap[dayName] = i + 100; } lineEdit()->installEventFilter(this); // handle keyword entry m_TextChanged = false; m_HandleInvalid = false; } AnnotationDialog::DateEdit::~DateEdit() { } void AnnotationDialog::DateEdit::setDate(const QDate& newDate) { QString dateString = QString::fromLatin1(""); if(newDate.isValid()) dateString = DB::ImageDate( newDate ).toString( false ); m_TextChanged = false; // We do not want to generate a signal here, since we explicitly setting // the date bool b = signalsBlocked(); blockSignals(true); setItemText(0, dateString); blockSignals(b); m_value = newDate; } void AnnotationDialog::DateEdit::setHandleInvalid(bool handleInvalid) { m_HandleInvalid = handleInvalid; } bool AnnotationDialog::DateEdit::handlesInvalid() const { return m_HandleInvalid; } void AnnotationDialog::DateEdit::setReadOnly(bool readOnly) { m_ReadOnly = readOnly; lineEdit()->setReadOnly(readOnly); } bool AnnotationDialog::DateEdit::isReadOnly() const { return m_ReadOnly; } bool AnnotationDialog::DateEdit::validate( const QDate & ) { return true; } QDate AnnotationDialog::DateEdit::date() const { QDate dt; readDate(dt, 0); return dt; } QDate AnnotationDialog::DateEdit::defaultDate() const { return m_defaultValue; } void AnnotationDialog::DateEdit::setDefaultDate(const QDate& date) { m_defaultValue = date; } void AnnotationDialog::DateEdit::showPopup() { if (m_ReadOnly) return; QRect desk = QApplication::desktop()->availableGeometry(this); // ensure that the popup is fully visible even when the DateEdit is off-screen QPoint popupPoint = mapToGlobal( QPoint( 0,0 ) ); if ( popupPoint.x() < desk.left() ) { popupPoint.setX( desk.x() ); } else if ( popupPoint.x() + width() > desk.right()) { popupPoint.setX( desk.right() - width()); } int dateFrameHeight = m_DateFrame->sizeHint().height(); if ( popupPoint.y() + height() + dateFrameHeight > desk.bottom() ) { popupPoint.setY( popupPoint.y() - dateFrameHeight ); } else { popupPoint.setY( popupPoint.y() + height() ); } m_DateFrame->move( popupPoint ); QDate date; readDate(date, 0); if (date.isValid()) { m_DatePicker->setDate( date ); } else { m_DatePicker->setDate( m_defaultValue ); } m_DateFrame->show(); } void AnnotationDialog::DateEdit::dateSelected(QDate newDate) { if ((m_HandleInvalid || newDate.isValid()) && validate(newDate)) { setDate(newDate); emit dateChanged(newDate); emit dateChanged( DB::ImageDate( QDateTime(newDate), QDateTime(newDate) ) ); m_DateFrame->hide(); } } void AnnotationDialog::DateEdit::dateEntered(QDate newDate) { if ((m_HandleInvalid || newDate.isValid()) && validate(newDate)) { setDate(newDate); emit dateChanged(newDate); emit dateChanged( DB::ImageDate( QDateTime(newDate), QDateTime(newDate) ) ); } } void AnnotationDialog::DateEdit::lineEnterPressed() { if ( !m_TextChanged ) return; QDate date; QDate end; if (readDate(date, &end) && (m_HandleInvalid || date.isValid()) && validate(date)) { // Update the edit. This is needed if the user has entered a // word rather than the actual date. setDate(date); emit(dateChanged(date)); emit dateChanged( DB::ImageDate( QDateTime(date), QDateTime(end) ) ); } else { // Invalid or unacceptable date - revert to previous value setDate(m_value); emit invalidDateEntered(); } } bool AnnotationDialog::DateEdit::inputIsValid() const { QDate date; return readDate(date, 0) && date.isValid(); } /* Reads the text from the line edit. If the text is a keyword, the * word will be translated to a date. If the text is not a keyword, the * text will be interpreted as a date. * Returns true if the date text is blank or valid, false otherwise. */ bool AnnotationDialog::DateEdit::readDate(QDate& result, QDate* end) const { QString text = currentText(); if (text.isEmpty()) { result = QDate(); } else if (m_KeywordMap.contains(text.toLower())) { QDate today = QDate::currentDate(); int i = m_KeywordMap[text.toLower()]; if (i >= 100) { /* A day name has been entered. Convert to offset from today. * This uses some math tricks to figure out the offset in days * to the next date the given day of the week occurs. There * are two cases, that the new day is >= the current day, which means * the new day has not occurred yet or that the new day < the current day, * which means the new day is already passed (so we need to find the * day in the next week). */ i -= 100; int currentDay = today.dayOfWeek(); if (i >= currentDay) i -= currentDay; else i += 7 - currentDay; } result = today.addDays(i); } else { result = DB::ImageDate::parseDate( text, m_IsStartEdit ); if ( end ) *end = DB::ImageDate::parseDate( text, false ); return result.isValid(); } return true; } void AnnotationDialog::DateEdit::keyPressEvent( QKeyEvent *event ) { int step = 0; if ( event->key() == Qt::Key_Up ) step = 1; else if ( event->key() == Qt::Key_Down ) step = -1; setDate( m_value.addDays(step) ); QComboBox::keyPressEvent( event ); } /* Checks for a focus out event. The display of the date is updated * to display the proper date when the focus leaves. */ bool AnnotationDialog::DateEdit::eventFilter(QObject *obj, QEvent *e) { if (obj == lineEdit()) { if (e->type() == QEvent::Wheel) { // Up and down arrow keys step the date QWheelEvent* we = dynamic_cast(e); Q_ASSERT( we != nullptr ); int step = 0; step = we->delta() > 0 ? 1 : -1; if (we->orientation() == Qt::Vertical) { setDate( m_value.addDays(step) ); } } } else { // It's a date picker event switch (e->type()) { case QEvent::MouseButtonDblClick: case QEvent::MouseButtonPress: { QMouseEvent *me = (QMouseEvent*)e; if (!m_DateFrame->rect().contains(me->pos())) { QPoint globalPos = m_DateFrame->mapToGlobal(me->pos()); if (QApplication::widgetAt(globalPos) == this) { // The date picker is being closed by a click on the // DateEdit widget. Avoid popping it up again immediately. m_DiscardNextMousePress = true; } } break; } default: break; } } return false; } void AnnotationDialog::DateEdit::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton && m_DiscardNextMousePress) { m_DiscardNextMousePress = false; return; } QComboBox::mousePressEvent(e); } void AnnotationDialog::DateEdit::slotTextChanged(const QString &) { m_TextChanged = true; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/DescriptionEdit.cpp b/AnnotationDialog/DescriptionEdit.cpp index dd746fa2..3658ad0e 100644 --- a/AnnotationDialog/DescriptionEdit.cpp +++ b/AnnotationDialog/DescriptionEdit.cpp @@ -1,39 +1,39 @@ -/* Copyright (C) 2014 Tobias Leupold +/* Copyright (C) 2014-2018 Tobias Leupold This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + #include "DescriptionEdit.h" #include AnnotationDialog::DescriptionEdit::DescriptionEdit(QWidget *parent) : KTextEdit(parent) { } AnnotationDialog::DescriptionEdit::~DescriptionEdit() { } void AnnotationDialog::DescriptionEdit::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_PageUp || event->key() == Qt::Key_PageDown) { emit pageUpDownPressed(event); } else { QTextEdit::keyPressEvent(event); } } -#include "DescriptionEdit.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/Dialog.cpp b/AnnotationDialog/Dialog.cpp index dcfffdfc..642164a5 100644 --- a/AnnotationDialog/Dialog.cpp +++ b/AnnotationDialog/Dialog.cpp @@ -1,1756 +1,1754 @@ -/* Copyright (C) 2003-2017 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Dialog.h" #include "DescriptionEdit.h" #include "enums.h" #include "ImagePreviewWidget.h" #include "DateEdit.h" #include "ListSelect.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 #include #ifdef HAVE_KGEOMAP #include #include #include #endif #include #include #include #include #include #ifdef DEBUG_AnnotationDialog #define Debug qDebug #else #define Debug if(0) qDebug #endif using Utilities::StringSet; /** * \class AnnotationDialog::Dialog * \brief QDialog subclass used for tagging images */ AnnotationDialog::Dialog::Dialog( QWidget* parent ) : QDialog( parent ) , m_ratingChanged( false ) , m_conflictText( i18n("(You have differing descriptions on individual images, setting text here will override them all)" ) ) { Utilities::ShowBusyCursor dummy; ShortCutManager shortCutManager; // The widget stack QWidget *mainWidget = new QWidget(this); QVBoxLayout* layout = new QVBoxLayout(mainWidget); setLayout(layout); layout->addWidget(mainWidget); m_stack = new QStackedWidget(mainWidget); layout->addWidget( m_stack ); // The Viewer m_fullScreenPreview = new Viewer::ViewerWidget( Viewer::ViewerWidget::InlineViewer ); m_stack->addWidget( m_fullScreenPreview ); // The dock widget m_dockWindow = new QMainWindow; m_stack->addWidget( m_dockWindow ); m_dockWindow->setDockNestingEnabled( true ); // -------------------------------------------------- Dock widgets m_generalDock = createDock( i18n("Label and Dates"), QString::fromLatin1("Label and Dates"), Qt::TopDockWidgetArea, createDateWidget(shortCutManager) ); m_previewDock = createDock( i18n("Image Preview"), QString::fromLatin1("Image Preview"), Qt::TopDockWidgetArea, createPreviewWidget() ); m_description = new DescriptionEdit(this); m_description->setProperty( "WantsFocus", true ); m_description->setObjectName( i18n("Description") ); m_description->setCheckSpellingEnabled( true ); m_description->setTabChangesFocus( true ); // this allows tabbing to the next item in the tab order. m_description->setWhatsThis( i18nc( "@info:whatsthis", "A descriptive text of the image." "If Use EXIF description is enabled under " "Settings|Configure KPhotoAlbum...|General, a description " "embedded in the image EXIF information is imported to this field if available." )); m_descriptionDock = createDock( i18n("Description"), QString::fromLatin1("description"), Qt::LeftDockWidgetArea, m_description ); shortCutManager.addDock( m_descriptionDock, m_description ); connect( m_description, SIGNAL(pageUpDownPressed(QKeyEvent*)), this, SLOT(descriptionPageUpDownPressed(QKeyEvent*)) ); #ifdef HAVE_KGEOMAP // -------------------------------------------------- Map representation m_annotationMapContainer = new QWidget(this); QVBoxLayout *annotationMapContainerLayout = new QVBoxLayout(m_annotationMapContainer); m_annotationMap = new Map::MapView(this); annotationMapContainerLayout->addWidget(m_annotationMap); QHBoxLayout *mapLoadingProgressLayout = new QHBoxLayout(); annotationMapContainerLayout->addLayout(mapLoadingProgressLayout); m_mapLoadingProgress = new QProgressBar(this); mapLoadingProgressLayout->addWidget(m_mapLoadingProgress); m_mapLoadingProgress->hide(); m_cancelMapLoadingButton = new QPushButton(i18n("Cancel")); mapLoadingProgressLayout->addWidget(m_cancelMapLoadingButton); m_cancelMapLoadingButton->hide(); connect(m_cancelMapLoadingButton, SIGNAL(clicked()), this, SLOT(setCancelMapLoading())); m_annotationMapContainer->setObjectName(i18n("Map")); m_mapDock = createDock( i18n("Map"), QString::fromLatin1("map"), Qt::LeftDockWidgetArea, m_annotationMapContainer ); shortCutManager.addDock(m_mapDock, m_annotationMapContainer); connect(m_mapDock, SIGNAL(visibilityChanged(bool)), this, SLOT(annotationMapVisibilityChanged(bool))); m_mapDock->setWhatsThis( i18nc( "@info:whatsthis", "The map widget allows you to view the location of images if GPS coordinates are found in the EXIF information." )); #endif // -------------------------------------------------- Categories QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); // Let's first assume we don't have positionable categories m_positionableCategories = false; for( QList::ConstIterator categoryIt = categories.constBegin(); categoryIt != categories.constEnd(); ++categoryIt ) { ListSelect* sel = createListSel( *categoryIt ); // Create a QMap of all ListSelect instances, so that we can easily // check if a specific (positioned) tag is (still) selected later m_listSelectList[(*categoryIt)->name()] = sel; QDockWidget* dock = createDock((*categoryIt)->name(), (*categoryIt)->name(), Qt::BottomDockWidgetArea, sel); shortCutManager.addDock( dock, sel->lineEdit() ); if ( (*categoryIt)->isSpecialCategory() ) dock->hide(); // Pass the positionable selection to the object sel->setPositionable( (*categoryIt)->positionable() ); if ( sel->positionable() ) { connect( sel, SIGNAL(positionableTagSelected(QString,QString)), this, SLOT(positionableTagSelected(QString,QString)) ); connect( sel, SIGNAL(positionableTagDeselected(QString,QString)), this, SLOT(positionableTagDeselected(QString,QString)) ); connect( sel, SIGNAL(positionableTagRenamed(QString,QString,QString)), this, SLOT(positionableTagRenamed(QString,QString,QString)) ); connect(m_preview->preview(), SIGNAL(proposedTagSelected(QString,QString)), sel, SLOT(ensureTagIsSelected(QString,QString))); // We have at least one positionable category m_positionableCategories = true; } } // -------------------------------------------------- The buttons. // don't use default buttons (Ok, Cancel): QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::NoButton); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); QHBoxLayout* lay1 = new QHBoxLayout; layout->addLayout( lay1 ); m_revertBut = new QPushButton( i18n("Revert This Item") ); KAcceleratorManager::setNoAccel(m_revertBut); lay1->addWidget( m_revertBut ); m_clearBut = new QPushButton(); KGuiItem::assign(m_clearBut, KGuiItem( i18n("Clear Form"),QApplication::isRightToLeft() ? QString::fromLatin1("clear_left") : QString::fromLatin1("locationbar_erase")) ) ; KAcceleratorManager::setNoAccel(m_clearBut); lay1->addWidget( m_clearBut ); QPushButton* optionsBut = new QPushButton( i18n("Options..." ) ); KAcceleratorManager::setNoAccel(optionsBut); lay1->addWidget( optionsBut ); lay1->addStretch(1); m_okBut = new QPushButton( i18n("&Done") ); lay1->addWidget( m_okBut ); m_continueLaterBut = new QPushButton( i18n("Continue &Later") ); lay1->addWidget( m_continueLaterBut ); QPushButton* cancelBut = new QPushButton(); KGuiItem::assign( cancelBut, KStandardGuiItem::cancel() ); lay1->addWidget( cancelBut ); // It is unfortunately not possible to ask KAcceleratorManager not to setup the OK and cancel keys. shortCutManager.addTaken( i18nc("@action:button","&Search") ); shortCutManager.addTaken( m_okBut->text() ); shortCutManager.addTaken( m_continueLaterBut->text()); shortCutManager.addTaken( cancelBut->text() ); connect( m_revertBut, SIGNAL(clicked()), this, SLOT(slotRevert()) ); connect( m_okBut, SIGNAL(clicked()), this, SLOT(doneTagging()) ); connect( m_continueLaterBut, SIGNAL(clicked()), this, SLOT(continueLater()) ); connect( cancelBut, SIGNAL(clicked()), this, SLOT(reject()) ); connect( m_clearBut, SIGNAL(clicked()), this, SLOT(slotClear()) ); connect( optionsBut, SIGNAL(clicked()), this, SLOT(slotOptions()) ); connect( m_preview, SIGNAL(imageRotated(int)), this, SLOT(rotate(int)) ); connect( m_preview, SIGNAL(indexChanged(int)), this, SLOT(slotIndexChanged(int)) ); connect( m_preview, SIGNAL(imageDeleted(DB::ImageInfo)), this, SLOT(slotDeleteImage()) ); connect( m_preview, SIGNAL(copyPrevClicked()), this, SLOT(slotCopyPrevious()) ); connect( m_preview, SIGNAL(areaVisibilityChanged(bool)), this, SLOT(slotShowAreas(bool)) ); connect( m_preview->preview(), SIGNAL(areaCreated(ResizableFrame*)), this, SLOT(slotNewArea(ResizableFrame*)) ); // Disable so no button accept return (which would break with the line edits) m_revertBut->setAutoDefault( false ); m_okBut->setAutoDefault( false ); m_continueLaterBut->setAutoDefault( false ); cancelBut->setAutoDefault( false ); m_clearBut->setAutoDefault( false ); optionsBut->setAutoDefault( false ); m_dockWindowCleanState = m_dockWindow->saveState(); loadWindowLayout(); m_current = -1; setGeometry( Settings::SettingsData::instance()->windowGeometry( Settings::AnnotationDialog ) ); setupActions(); shortCutManager.setupShortCuts(); // WARNING layout->addWidget(buttonBox) must be last item in layout layout->addWidget(buttonBox); } QDockWidget* AnnotationDialog::Dialog::createDock( const QString& title, const QString& name, Qt::DockWidgetArea location, QWidget* widget ) { QDockWidget* dock = new QDockWidget( title ); KAcceleratorManager::setNoAccel(dock); dock->setObjectName( name ); dock->setAllowedAreas( Qt::AllDockWidgetAreas ); dock->setWidget( widget ); m_dockWindow->addDockWidget( location, dock ); m_dockWidgets.append( dock ); return dock; } QWidget* AnnotationDialog::Dialog::createDateWidget(ShortCutManager& shortCutManager) { QWidget* top = new QWidget; QVBoxLayout* lay2 = new QVBoxLayout( top ); // Image Label QHBoxLayout* lay3 = new QHBoxLayout; lay2->addLayout( lay3 ); QLabel* label = new QLabel( i18n("Label: " ) ); lay3->addWidget( label ); m_imageLabel = new KLineEdit; m_imageLabel->setProperty( "WantsFocus", true ); m_imageLabel->setObjectName( i18n("Label") ); lay3->addWidget( m_imageLabel ); shortCutManager.addLabel( label ); label->setBuddy( m_imageLabel ); // Date QHBoxLayout* lay4 = new QHBoxLayout; lay2->addLayout( lay4 ); label = new QLabel( i18n("Date: ") ); lay4->addWidget( label ); m_startDate = new ::AnnotationDialog::DateEdit( true ); lay4->addWidget( m_startDate, 1 ); connect( m_startDate, SIGNAL(dateChanged(DB::ImageDate)), this, SLOT(slotStartDateChanged(DB::ImageDate)) ); shortCutManager.addLabel(label ); label->setBuddy( m_startDate); m_endDateLabel = new QLabel( QString::fromLatin1( "-" ) ); lay4->addWidget( m_endDateLabel ); m_endDate = new ::AnnotationDialog::DateEdit( false ); lay4->addWidget( m_endDate, 1 ); // Time m_timeLabel = new QLabel( i18n("Time: ") ); lay4->addWidget( m_timeLabel ); m_time= new QTimeEdit; lay4->addWidget( m_time ); m_isFuzzyDate = new QCheckBox( i18n("Use Fuzzy Date") ); m_isFuzzyDate->setWhatsThis( i18nc("@info", "In KPhotoAlbum, images can either have an exact date and time" ", or a fuzzy date which happened any time during" " a specified time interval. Images produced by digital cameras" " do normally have an exact date." "If you don't know exactly when a photo was taken" " (e.g. if the photo comes from an analog camera), then you should set" " Use Fuzzy Date.") ); m_isFuzzyDate->setToolTip( m_isFuzzyDate->whatsThis() ); lay4->addWidget( m_isFuzzyDate ); lay4->addStretch(1); connect(m_isFuzzyDate,SIGNAL(stateChanged(int)),this,SLOT(slotSetFuzzyDate())); QHBoxLayout* lay8 = new QHBoxLayout; lay2->addLayout( lay8 ); m_megapixelLabel = new QLabel( i18n("Minimum megapixels:") ); lay8->addWidget( m_megapixelLabel ); m_megapixel = new QSpinBox; m_megapixel->setRange( 0, 99 ); m_megapixel->setSingleStep( 1 ); m_megapixelLabel->setBuddy( m_megapixel ); lay8->addWidget( m_megapixel ); lay8->addStretch( 1 ); m_max_megapixelLabel = new QLabel( i18n("Maximum megapixels:") ); lay8->addWidget( m_max_megapixelLabel ); m_max_megapixel = new QSpinBox; m_max_megapixel->setRange( 0, 99 ); m_max_megapixel->setSingleStep( 1 ); m_max_megapixelLabel->setBuddy( m_max_megapixel ); lay8->addWidget( m_max_megapixel ); lay8->addStretch( 1 ); QHBoxLayout* lay9 = new QHBoxLayout; lay2->addLayout( lay9 ); label = new QLabel( i18n("Rating:") ); lay9->addWidget( label ); m_rating = new KRatingWidget; m_rating->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); lay9->addWidget( m_rating, 0, Qt::AlignCenter ); connect( m_rating, SIGNAL(ratingChanged(uint)), this, SLOT(slotRatingChanged(uint)) ); m_ratingSearchLabel = new QLabel( i18n("Rating search mode:") ); lay9->addWidget( m_ratingSearchLabel ); m_ratingSearchMode = new KComboBox( lay9 ); m_ratingSearchMode->addItems( QStringList() << i18n("==") << i18n(">=") << i18n("<=") << i18n("!=") ); m_ratingSearchLabel->setBuddy( m_ratingSearchMode ); lay9->addWidget( m_ratingSearchMode ); // File name search pattern QHBoxLayout* lay10 = new QHBoxLayout; lay2->addLayout( lay10 ); m_imageFilePatternLabel = new QLabel( i18n("File Name Pattern: " ) ); lay10->addWidget( m_imageFilePatternLabel ); m_imageFilePattern = new KLineEdit; m_imageFilePattern->setObjectName( i18n("File Name Pattern") ); lay10->addWidget( m_imageFilePattern ); shortCutManager.addLabel( m_imageFilePatternLabel ); m_imageFilePatternLabel->setBuddy( m_imageFilePattern ); m_searchRAW = new QCheckBox( i18n("Search only for RAW files") ); lay2->addWidget( m_searchRAW ); lay9->addStretch( 1 ); lay2->addStretch(1); return top; } QWidget* AnnotationDialog::Dialog::createPreviewWidget() { m_preview = new ImagePreviewWidget(); connect(m_preview, &ImagePreviewWidget::togglePreview, this, &Dialog::togglePreview); return m_preview; } void AnnotationDialog::Dialog::slotRevert() { if ( m_setup == InputSingleImageConfigMode ) load(); } void AnnotationDialog::Dialog::slotIndexChanged( int index ) { if ( m_setup != InputSingleImageConfigMode ) return; if(m_current >= 0 ) writeToInfo(); m_current = index; load(); } void AnnotationDialog::Dialog::doneTagging() { saveAndClose(); if ( Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() ) { for( DB::ImageInfoListIterator it = m_origList.begin(); it != m_origList.end(); ++it ) { (*it)->removeCategoryInfo( Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag() ); } } } /* * Copy tags (only tags/categories, not description/label/...) from previous image to the currently showed one */ void AnnotationDialog::Dialog::slotCopyPrevious() { if ( m_setup != InputSingleImageConfigMode ) return; if ( m_current < 1 ) return; // FIXME: it would be better to compute the "previous image" in a better way, but let's stick with this for now... DB::ImageInfo& old_info = m_editList[ m_current - 1 ]; m_positionableTagCandidates.clear(); m_lastSelectedPositionableTag.first = QString(); m_lastSelectedPositionableTag.second = QString(); Q_FOREACH( ListSelect *ls, m_optionList ) { ls->setSelection( old_info.itemsOfCategory( ls->category() ) ); // Also set all positionable tag candidates if ( ls->positionable() ) { QString category = ls->category(); QSet selectedTags = old_info.itemsOfCategory( category ); QSet positionedTagSet = positionedTags( category ); // Add the tag to the positionable candiate list, if no area is already associated with it Q_FOREACH(const auto &tag, selectedTags) { if (!positionedTagSet.contains(tag)) { addTagToCandidateList(category, tag); } } // Check all areas for a linked tag in this category that is probably not selected anymore for(ResizableFrame *area : areas()) { QPair tagData = area->tagData(); if (tagData.first == category) { if (! selectedTags.contains(tagData.second)) { // The linked tag is not selected anymore, so remove it area->removeTagData(); } } } } } } void AnnotationDialog::Dialog::load() { // Remove all areas tidyAreas(); // No areas have been changed m_areasChanged = false; // Empty the positionable tag candidate list and the last selected positionable tag m_positionableTagCandidates.clear(); m_lastSelectedPositionableTag = QPair(); DB::ImageInfo& info = m_editList[ m_current ]; m_startDate->setDate( info.date().start().date() ); if( info.date().hasValidTime() ) { m_time->show(); m_time->setTime( info.date().start().time()); m_isFuzzyDate->setChecked(false); } else { m_time->hide(); m_isFuzzyDate->setChecked(true); } if ( info.date().start().date() == info.date().end().date() ) m_endDate->setDate( QDate() ); else m_endDate->setDate( info.date().end().date() ); m_imageLabel->setText( info.label() ); m_description->setPlainText( info.description() ); if ( m_setup == InputSingleImageConfigMode ) m_rating->setRating( qMax( static_cast(0), info.rating() ) ); m_ratingChanged = false; // A category areas have been linked against could have been deleted // or un-marked as positionable in the meantime, so ... QMap categoryIsPositionable; QList positionableCategories; Q_FOREACH( ListSelect *ls, m_optionList ) { ls->setSelection( info.itemsOfCategory( ls->category() ) ); ls->rePopulate(); // Get all selected positionable tags and add them to the candidate list if (ls->positionable()) { QSet selectedTags = ls->itemsOn(); Q_FOREACH( const QString &tagName, selectedTags ) { addTagToCandidateList( ls->category(), tagName ); } } // ... create a list of all categories and their positionability ... categoryIsPositionable[ls->category()] = ls->positionable(); if (ls->positionable()) { positionableCategories << ls->category(); } } // Create all tagged areas QMap> taggedAreas = info.taggedAreas(); QMapIterator> areasInCategory(taggedAreas); while (areasInCategory.hasNext()) { areasInCategory.next(); QString category = areasInCategory.key(); // ... and check if the respective category is actually there yet and still positionable // (operator[] will insert an empty item if the category has been deleted // and is thus missing in the QMap, but the respective key won't be true) if (categoryIsPositionable[category]) { QMapIterator areaData(areasInCategory.value()); while (areaData.hasNext()) { areaData.next(); QString tag = areaData.key(); // Be sure that the corresponding tag is still checked. The category could have // been un-marked as positionable in the meantime and the tag could have been // deselected, without triggering positionableTagDeselected and the area thus // still remaining. If the category is then re-marked as positionable, the area would // show up without the tag being selected. if(m_listSelectList[category]->tagIsChecked(tag)) { m_preview->preview()->createTaggedArea(category, tag, areaData.value(), m_preview->showAreas()); } } } } if (m_setup == InputSingleImageConfigMode) { setWindowTitle(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); 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() ) { Debug() << "Hiding dock: " << dock->objectName(); dock->hide(); } } } void AnnotationDialog::Dialog::showTornOfWindows() { for (QDockWidget* dock: m_dockWidgets ) { if ( dock->isFloating() ) { Debug() << "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 -#include "Dialog.moc" - // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ImagePreview.cpp b/AnnotationDialog/ImagePreview.cpp index 34d90b62..4bcd3d55 100644 --- a/AnnotationDialog/ImagePreview.cpp +++ b/AnnotationDialog/ImagePreview.cpp @@ -1,705 +1,704 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImagePreview.h" #include #include #include #include #include "ResizableFrame.h" #include #include #include #include #include #include #include #include #ifdef DEBUG_AnnotationDialog #define Debug qDebug #else #define Debug if(0) qDebug #endif using namespace AnnotationDialog; ImagePreview::ImagePreview( QWidget* parent ) : QLabel( parent ) , m_selectionRect(0) , m_aspectRatio(1) , m_reloadTimer( new QTimer(this) ) , m_areaCreationEnabled( false ) { setAlignment( Qt::AlignCenter ); setMinimumSize( 64, 64 ); // "the widget can make use of extra space, so it should get as much space as possible" setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_reloadTimer->setSingleShot(true); connect(m_reloadTimer, &QTimer::timeout, this, &ImagePreview::resizeFinished); } void ImagePreview::resizeEvent( QResizeEvent* ev ) { Debug() << "Resizing from" << ev->oldSize() <<"to"<size(); // during resizing, a scaled image will do QImage scaledImage = m_currentImage.getImage().scaled(size(),Qt::KeepAspectRatio); setPixmap(QPixmap::fromImage(scaledImage)); updateScaleFactors(); // (re)start the timer to do a full reload m_reloadTimer->start(200); QLabel::resizeEvent(ev); } int ImagePreview::heightForWidth(int width) const { int height = width * m_aspectRatio; return height; } QSize ImagePreview::sizeHint() const { QSize hint = m_info.size(); Debug() << "Preview size hint is" << hint; return hint; } void ImagePreview::rotate(int angle) { if (! m_info.isNull()) { m_currentImage.setAngle( m_info.angle() ); m_info.rotate( angle, DB::RotateImageInfoOnly ); } else { // Can this really happen? m_angle += angle; } m_preloader.cancelPreload(); m_lastImage.reset(); reload(); rotateAreas(angle); } void ImagePreview::setImage( const DB::ImageInfo& info ) { m_info = info; reload(); } /** This method should only be used for the non-user images. Currently this includes two images: the search image and the configure several images at a time image. */ void ImagePreview::setImage( const QString& fileName ) { m_fileName = fileName; m_info = DB::ImageInfo(); m_angle = 0; // Set the current angle that will be passed to m_lastImage m_currentImage.setAngle( m_info.angle() ); reload(); } void ImagePreview::reload() { m_aspectRatio = 1; if ( !m_info.isNull() ) { if (m_preloader.has(m_info.fileName(), m_info.angle())) { Debug() << "reload(): set preloader image"; setCurrentImage(m_preloader.getImage()); } else if (m_lastImage.has(m_info.fileName(), m_info.angle())) { Debug() << "reload(): set last image"; //don't pass by reference, the additional constructor is needed here //see setCurrentImage for the reason (where m_lastImage is changed...) setCurrentImage(QImage(m_lastImage.getImage())); } else { if (!m_currentImage.has(m_info.fileName(), m_info.angle())) { // erase old image to prevent a laggy feel, // but only erase old image if it is a different image // (otherwise we get flicker when resizing) setPixmap(QPixmap()); } Debug() << "reload(): set another image"; ImageManager::AsyncLoader::instance()->stop(this); ImageManager::ImageRequest* request = new ImageManager::ImageRequest( m_info.fileName(), size(), m_info.angle(), this ); request->setPriority( ImageManager::Viewer ); ImageManager::AsyncLoader::instance()->load( request ); } } else { Debug() << "reload(): set image from file"; QImage img( m_fileName ); img = rotateAndScale( img, width(), height(), m_angle ); setPixmap( QPixmap::fromImage(img) ); } } int ImagePreview::angle() const { Q_ASSERT( !m_info.isNull() ); return m_angle; } QSize ImagePreview::getActualImageSize() { if (! m_info.size().isValid()) { // We have to fetch the size from the image m_info.setSize(QImageReader(m_info.fileName().absolute()).size()); m_aspectRatio = m_info.size().height() / m_info.size().width(); } return m_info.size(); } void ImagePreview::setCurrentImage(const QImage &image) { // Cache the current image as the last image before changing it m_lastImage.set(m_currentImage); m_currentImage.set(m_info.fileName(), image, m_info.angle()); setPixmap(QPixmap::fromImage(image)); if (!m_anticipated.m_fileName.isNull()) m_preloader.preloadImage(m_anticipated.m_fileName, width(), height(), m_anticipated.m_angle); updateScaleFactors(); // Clear the full size image (if we have loaded one) m_fullSizeImage = QImage(); } void ImagePreview::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& image) { const DB::FileName fileName = request->databaseFileName(); const bool loadedOK = request->loadedOK(); if ( loadedOK && !m_info.isNull() ) { if (m_info.fileName() == fileName) setCurrentImage(image); } } void ImagePreview::anticipate(DB::ImageInfo &info1) { //We cannot call m_preloader.preloadImage right here: //this function is called before reload(), so if we preload here, //the preloader will always be loading the image after the next image. m_anticipated.set(info1.fileName(), info1.angle()); } ImagePreview::PreloadInfo::PreloadInfo() : m_angle(0) { } void ImagePreview::PreloadInfo::set(const DB::FileName& fileName, int angle) { m_fileName=fileName; m_angle=angle; } bool ImagePreview::PreviewImage::has(const DB::FileName &fileName, int angle) const { return fileName==m_fileName && !m_image.isNull() && angle==m_angle; } QImage &ImagePreview::PreviewImage::getImage() { return m_image; } void ImagePreview::PreviewImage::set(const DB::FileName &fileName, const QImage &image, int angle) { m_fileName = fileName; m_image = image; m_angle = angle; } void ImagePreview::PreviewImage::set(const PreviewImage &other) { m_fileName = other.m_fileName; m_image = other.m_image; m_angle = other.m_angle; } void ImagePreview::PreviewImage::setAngle( int angle ) { m_angle = angle; } void ImagePreview::PreviewImage::reset() { m_fileName = DB::FileName(); m_image=QImage(); } void ImagePreview::PreviewLoader::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& image) { if ( request->loadedOK() ) { const DB::FileName fileName = request->databaseFileName(); set( fileName, image, request->angle() ); } } void ImagePreview::PreviewLoader::preloadImage(const DB::FileName &fileName, int width, int height, int angle) { //no need to worry about concurrent access: everything happens in the event loop thread reset(); ImageManager::AsyncLoader::instance()->stop(this); ImageManager::ImageRequest* request = new ImageManager::ImageRequest( fileName, QSize( width, height ), angle, this ); request->setPriority( ImageManager::ViewerPreload ); ImageManager::AsyncLoader::instance()->load( request ); } void ImagePreview::PreviewLoader::cancelPreload() { reset(); ImageManager::AsyncLoader::instance()->stop(this); } QImage ImagePreview::rotateAndScale(QImage img, int width, int height, int angle) const { if ( angle != 0 ) { QMatrix matrix; matrix.rotate( angle ); img = img.transformed( matrix ); } img = Utilities::scaleImage(img, width, height, Qt::KeepAspectRatio ); return img; } void ImagePreview::updateScaleFactors() { if (m_info.isNull()) return; // search mode // Calculate a scale factor from the original image's size and it's current preview QSize actualSize = getActualImageSize(); QSize previewSize = pixmap()->size(); m_scaleWidth = double(actualSize.width()) / double(previewSize.width()); m_scaleHeight = double(actualSize.height()) / double(previewSize.height()); // Calculate the min and max coordinates inside the preview widget int previewWidth = previewSize.width(); int previewHeight = previewSize.height(); int widgetWidth = this->frameGeometry().width(); int widgetHeight = this->frameGeometry().height(); m_minX = (widgetWidth - previewWidth) / 2; m_maxX = m_minX + previewWidth - 1; m_minY = (widgetHeight - previewHeight) / 2; m_maxY = m_minY + previewHeight - 1; // Put all areas to their respective position on the preview remapAreas(); } void ImagePreview::mousePressEvent(QMouseEvent *event) { if (! m_areaCreationEnabled) { return; } if (event->button() & Qt::LeftButton) { if (! m_selectionRect) { m_selectionRect = new QRubberBand(QRubberBand::Rectangle, this); } m_areaStart = event->pos(); if (m_areaStart.x() < m_minX || m_areaStart.x() > m_maxX || m_areaStart.y() < m_minY || m_areaStart.y() > m_maxY) { // Dragging started outside of the preview image return; } m_selectionRect->setGeometry(QRect(m_areaStart, QSize())); m_selectionRect->show(); } } void ImagePreview::mouseMoveEvent(QMouseEvent *event) { if (! m_areaCreationEnabled) { return; } if (m_selectionRect && m_selectionRect->isVisible()) { m_currentPos = event->pos(); // Restrict the coordinates to the preview images's size if (m_currentPos.x() < m_minX) { m_currentPos.setX(m_minX); } if (m_currentPos.y() < m_minY) { m_currentPos.setY(m_minY); } if (m_currentPos.x() > m_maxX) { m_currentPos.setX(m_maxX); } if (m_currentPos.y() > m_maxY) { m_currentPos.setY(m_maxY); } m_selectionRect->setGeometry(QRect( m_areaStart, m_currentPos ).normalized()); } } void ImagePreview::mouseReleaseEvent(QMouseEvent *event) { if (! m_areaCreationEnabled) { return; } if (event->button() & Qt::LeftButton && m_selectionRect->isVisible()) { m_areaEnd = event->pos(); processNewArea(); m_selectionRect->hide(); } } QPixmap ImagePreview::grabAreaImage(QRect area) { return QPixmap::fromImage(m_currentImage.getImage().copy(area.left() - m_minX, area.top() - m_minY, area.width(), area.height())); } QRect ImagePreview::areaPreviewToActual(QRect area) const { return QRect(QPoint(int(double(area.left() - m_minX) * m_scaleWidth), int(double(area.top() - m_minY) * m_scaleHeight)), QPoint(int(double(area.right() - m_minX) * m_scaleWidth), int(double(area.bottom() - m_minY) * m_scaleHeight))); } QRect ImagePreview::areaActualToPreview(QRect area) const { return QRect(QPoint(int(double(area.left() / m_scaleWidth)) + m_minX, int(double(area.top() / m_scaleHeight)) + m_minY), QPoint(int(double(area.right() / m_scaleWidth)) + m_minX, int(double(area.bottom() / m_scaleHeight)) + m_minY)); } void ImagePreview::createNewArea(QRect geometry, QRect actualGeometry) { // Create a ResizableFrame (cleaned up in Dialog::tidyAreas()) ResizableFrame *newArea = new ResizableFrame(this); newArea->setGeometry(geometry); // Be sure not to create an invisible area newArea->checkGeometry(); // In case the geometry has been changed by checkGeometry() actualGeometry = areaPreviewToActual(newArea->geometry()); // Store the coordinates on the real image (not on the preview) newArea->setActualCoordinates(actualGeometry); emit areaCreated(newArea); newArea->show(); newArea->showContextMenu(); } void ImagePreview::processNewArea() { if (m_areaStart == m_areaEnd) { // It was just a click, no area has been dragged return; } QRect newAreaPreview = QRect(m_areaStart, m_currentPos).normalized(); createNewArea(newAreaPreview, areaPreviewToActual(newAreaPreview)); } void ImagePreview::remapAreas() { QList allAreas = this->findChildren(); if (allAreas.isEmpty()) { return; } foreach (ResizableFrame *area, allAreas) { area->setGeometry(areaActualToPreview(area->actualCoordinates())); } } QRect ImagePreview::rotateArea(QRect originalAreaGeometry, int angle) { // This is the current state of the image. We need the state before, so ... QSize unrotatedOriginalImageSize = getActualImageSize(); // ... un-rotate it unrotatedOriginalImageSize.transpose(); QRect rotatedAreaGeometry; rotatedAreaGeometry.setWidth(originalAreaGeometry.height()); rotatedAreaGeometry.setHeight(originalAreaGeometry.width()); if (angle == 90) { rotatedAreaGeometry.moveTo( unrotatedOriginalImageSize.height() - (originalAreaGeometry.height() + originalAreaGeometry.y()), originalAreaGeometry.x() ); } else { rotatedAreaGeometry.moveTo( originalAreaGeometry.y(), unrotatedOriginalImageSize.width() - (originalAreaGeometry.width() + originalAreaGeometry.x()) ); } return rotatedAreaGeometry; } void ImagePreview::rotateAreas(int angle) { // Map all areas to their respective coordinates on the rotated actual image QList allAreas = this->findChildren(); foreach (ResizableFrame *area, allAreas) { area->setActualCoordinates(rotateArea(area->actualCoordinates(), angle)); } } void ImagePreview::resizeFinished() { Debug() << "Reloading image after resize"; m_preloader.cancelPreload(); m_lastImage.reset(); reload(); } QRect ImagePreview::minMaxAreaPreview() const { return QRect(m_minX, m_minY, m_maxX, m_maxY); } void ImagePreview::createTaggedArea(QString category, QString tag, QRect geometry, bool showArea) { // Create a ResizableFrame (cleaned up in Dialog::tidyAreas()) ResizableFrame *newArea = new ResizableFrame(this); emit areaCreated(newArea); newArea->setGeometry(areaActualToPreview(geometry)); newArea->setActualCoordinates(geometry); newArea->setTagData(category, tag, AutomatedChange); newArea->setVisible(showArea); } void ImagePreview::setAreaCreationEnabled(bool state) { m_areaCreationEnabled = state; } // Currently only called when face detection/recognition is used void ImagePreview::fetchFullSizeImage() { if (m_fullSizeImage.isNull()) { m_fullSizeImage = QImage(m_info.fileName().absolute()); } if (m_angle != m_info.angle()) { QMatrix matrix; matrix.rotate(m_info.angle()); m_fullSizeImage = m_fullSizeImage.transformed(matrix); } } void ImagePreview::acceptProposedTag(QPair tagData, ResizableFrame *area) { // Be sure that we do have the category the proposed tag belongs to bool categoryFound = false; // Any warnings should only happen when the recognition database is e. g. copied from another // database location or has been changed outside of KPA. Anyways, this m_can_ happen, so we // have to handle it. QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); for(QList::ConstIterator categoryIt = categories.constBegin(); categoryIt != categories.constEnd(); ++categoryIt) { if ((*categoryIt)->name() == tagData.first) { if (! (*categoryIt)->positionable()) { KMessageBox::sorry(this, i18n( "

Can't associate tag \"%2\"

" "

The category \"%1\" the tag \"%2\" belongs to is not positionable.

" "

If you want to use this tag, change this in the settings dialog. " "If this tag shouldn't be in the recognition database anymore, it can " "be deleted in the settings.

", tagData.first, tagData.second )); return; } categoryFound = true; break; } } if (! categoryFound) { KMessageBox::sorry(this, i18n( "

Can't associate tag \"%2\"

" "

The category \"%1\" the tag \"%2\" belongs to does not exist.

" "

If you want to use this tag, add this category and mark it as positionable. " "If this tag shouldn't be in the recognition database anymore, it can " "be deleted in the settings dialog.

", tagData.first, tagData.second )); return; } // Tell all ListSelects that we accepted a proposed tag, so that the ListSelect // holding the respective category can ensure that the tag is checked emit proposedTagSelected(tagData.first, tagData.second); // Associate the area with the proposed tag area->setTagData(tagData.first, tagData.second); } bool ImagePreview::fuzzyAreaExists(QList &existingAreas, QRect area) { float maximumDeviation; for (int i = 0; i < existingAreas.size(); ++i) { // maximumDeviation is 15% of the mean value of the width and height of each area maximumDeviation = float(existingAreas.at(i).width() + existingAreas.at(i).height()) * 0.075; if ( distance(existingAreas.at(i).topLeft(), area.topLeft()) < maximumDeviation && distance(existingAreas.at(i).topRight(), area.topRight()) < maximumDeviation && distance(existingAreas.at(i).bottomLeft(), area.bottomLeft()) < maximumDeviation && distance(existingAreas.at(i).bottomRight(), area.bottomRight()) < maximumDeviation ) { return true; } } return false; } float ImagePreview::distance(QPoint point1, QPoint point2) { QPoint difference = point1 - point2; return sqrt(pow(difference.x(), 2) + pow(difference.y(), 2)); } #ifdef HAVE_KFACE void ImagePreview::detectFaces() { m_detector = FaceManagement::Detector::instance(); m_recognizer = FaceManagement::Recognizer::instance(); ImagePreviewWidget *parent = dynamic_cast(parentWidget()); parent->setFacedetectButEnabled(false); // We need the whole image, not only the preview. fetchFullSizeImage(); // Search for faces QList faces = m_detector->detectFaces(m_fullSizeImage); if (faces.size() == 0) { // No faces found, so we can stop here parent->setFacedetectButEnabled(true); return; } // Get all geometries and all tags of all areas we have QList existingAreaGeometries; QList> existingAreaTagData; foreach (ResizableFrame *area, findChildren()) { existingAreaGeometries << area->geometry(); existingAreaTagData << area->tagData(); } QPair proposedTagData; for (int i = 0; i < faces.size(); ++i) { // Check if we already have the area (the button has already been pressed) if (fuzzyAreaExists(existingAreaGeometries, areaActualToPreview(faces.at(i)))) { continue; } // Create a new area for the found face ResizableFrame *newArea = new ResizableFrame(this); newArea->setActualCoordinates(faces.at(i)); newArea->setGeometry(areaActualToPreview(faces.at(i))); // allow auto-training of the face: newArea->markAsFace(); newArea->show(); emit areaCreated(newArea); // Check the recognition database for a matching person QPair proposedTagData = m_recognizer->recognizeFace(m_fullSizeImage.copy(faces.at(i))); if (! proposedTagData.first.isEmpty()) { // Check if the matching person is not alreday associated to another area if (! existingAreaTagData.contains(proposedTagData)) { // Propose the found tag newArea->setProposedTagData(proposedTagData); } } } parent->setFacedetectButEnabled(true); } void ImagePreview::trainRecognitionDatabase(QRect geometry, QPair tagData) { ImagePreviewWidget* parent = dynamic_cast(parentWidget()); parent->setFacedetectButEnabled(false); // Be sure to have the full size image fetchFullSizeImage(); // Train the database m_recognizer->trainRecognitionDatabase(tagData, m_fullSizeImage.copy(geometry)); parent->setFacedetectButEnabled(true); } void ImagePreview::recognizeArea(ResizableFrame *area) { // Be sure to actually have a recognizer instance m_recognizer = FaceManagement::Recognizer::instance(); // Be sure to have the full size image fetchFullSizeImage(); // Check the recognition database for a matching person QPair proposedTagData = m_recognizer->recognizeFace(m_fullSizeImage.copy(area->actualCoordinates())); if (! proposedTagData.first.isEmpty()) { // Let's see if an area is already associated with this tag foreach (ResizableFrame *area, findChildren()) { if (area->tagData() == proposedTagData) { return; } } // Set the proposed data area->setProposedTagData(proposedTagData); } } #endif // HAVE_KFACE -#include "ImagePreview.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ListSelect.cpp b/AnnotationDialog/ListSelect.cpp index 45c87e78..c3bd1945 100644 --- a/AnnotationDialog/ListSelect.cpp +++ b/AnnotationDialog/ListSelect.cpp @@ -1,878 +1,877 @@ -/* Copyright (C) 2003-2015 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ListSelect.h" #include "CompletableLineEdit.h" #include "Dialog.h" #include "ListViewItemHider.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 using namespace AnnotationDialog; using CategoryListView::CheckDropItem; AnnotationDialog::ListSelect::ListSelect( const DB::CategoryPtr& category, QWidget* parent ) : QWidget( parent ), m_category( category ), m_baseTitle( ) { QVBoxLayout* layout = new QVBoxLayout( this ); m_lineEdit = new CompletableLineEdit( this ); m_lineEdit->setProperty( "FocusCandidate", true ); m_lineEdit->setProperty( "WantsFocus", true ); layout->addWidget( m_lineEdit ); // PENDING(blackie) rename instance variable to something better than _listView m_treeWidget = new CategoryListView::DragableTreeWidget( m_category, this ); m_treeWidget->setHeaderLabel( QString::fromLatin1( "items" ) ); m_treeWidget->header()->hide(); connect( m_treeWidget, SIGNAL(itemClicked(QTreeWidgetItem*,int)), this, SLOT(itemSelected(QTreeWidgetItem*)) ); m_treeWidget->setContextMenuPolicy(Qt::CustomContextMenu); connect( m_treeWidget, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showContextMenu(QPoint))); connect( m_treeWidget, SIGNAL(itemsChanged()), this, SLOT(rePopulate()) ); connect( m_treeWidget, SIGNAL(itemClicked(QTreeWidgetItem*,int)), this, SLOT(updateSelectionCount()) ); layout->addWidget( m_treeWidget ); // Merge CheckBox QHBoxLayout* lay2 = new QHBoxLayout; layout->addLayout( lay2 ); m_or = new QRadioButton( i18n("or"), this ); m_and = new QRadioButton( i18n("and"), this ); lay2->addWidget( m_or ); lay2->addWidget( m_and ); lay2->addStretch(1); // Sorting tool button QButtonGroup* grp = new QButtonGroup( this ); grp->setExclusive( true ); m_alphaTreeSort = new QToolButton; m_alphaTreeSort->setIcon( SmallIcon( QString::fromLatin1( "view-list-tree" ) ) ); m_alphaTreeSort->setCheckable( true ); m_alphaTreeSort->setToolTip( i18n("Sort Alphabetically (Tree)") ); grp->addButton( m_alphaTreeSort ); m_alphaFlatSort = new QToolButton; m_alphaFlatSort->setIcon( SmallIcon( QString::fromLatin1( "draw-text" ) ) ); m_alphaFlatSort->setCheckable( true ); m_alphaFlatSort->setToolTip( i18n("Sort Alphabetically (Flat)") ); grp->addButton( m_alphaFlatSort ); m_dateSort = new QToolButton; m_dateSort->setIcon( SmallIcon( QString::fromLatin1( "x-office-calendar" ) ) ); m_dateSort->setCheckable( true ); m_dateSort->setToolTip( i18n("Sort by date") ); grp->addButton( m_dateSort ); m_showSelectedOnly = new QToolButton; m_showSelectedOnly->setIcon( SmallIcon( QString::fromLatin1( "view-filter" ) ) ); m_showSelectedOnly->setCheckable( true ); m_showSelectedOnly->setToolTip( i18n("Show only selected Ctrl+S") ); m_showSelectedOnly->setChecked( ShowSelectionOnlyManager::instance().selectionIsLimited() ); m_alphaTreeSort->setChecked( Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree ); m_alphaFlatSort->setChecked( Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaFlat ); m_dateSort->setChecked( Settings::SettingsData::instance()->viewSortType() == Settings::SortLastUse ); connect( m_dateSort, SIGNAL(clicked()), this, SLOT(slotSortDate()) ); connect( m_alphaTreeSort, SIGNAL(clicked()), this, SLOT(slotSortAlphaTree()) ); connect( m_alphaFlatSort, SIGNAL(clicked()), this, SLOT(slotSortAlphaFlat()) ); connect( m_showSelectedOnly, SIGNAL(clicked()), &ShowSelectionOnlyManager::instance(), SLOT(toggle()) ); lay2->addWidget( m_alphaTreeSort ); lay2->addWidget( m_alphaFlatSort ); lay2->addWidget( m_dateSort ); lay2->addWidget( m_showSelectedOnly ); connectLineEdit(m_lineEdit); populate(); connect( Settings::SettingsData::instance(), SIGNAL(viewSortTypeChanged(Settings::ViewSortType)), this, SLOT(setViewSortType(Settings::ViewSortType)) ); connect( Settings::SettingsData::instance(), SIGNAL(matchTypeChanged(AnnotationDialog::MatchType)), this, SLOT(updateListview()) ); connect( &ShowSelectionOnlyManager::instance(), SIGNAL(limitToSelected()), this, SLOT(limitToSelection()) ); connect( &ShowSelectionOnlyManager::instance(), SIGNAL(broaden()), this, SLOT(showAllChildren()) ); } void AnnotationDialog::ListSelect::slotReturn() { if (isInputMode()) { QString enteredText = m_lineEdit->text().trimmed(); if (enteredText.isEmpty()) { return; } if (searchForUntaggedImagesTagNeeded()) { if (enteredText == Settings::SettingsData::instance()->untaggedTag()) { KMessageBox::information( this, i18n("The tag you entered is the tag that is set automatically for newly " "found, untagged images (cf. Settings|Configure KPhotoAlbum..." "|Categories|Untagged Images). It will not show up here as " "long as it is selected for this purpose.") ); m_lineEdit->setText(QString()); return; } } m_category->addItem(enteredText); rePopulate(); QList items = m_treeWidget->findItems(enteredText, Qt::MatchExactly|Qt::MatchRecursive, 0); if (! items.isEmpty()) { items.at(0)->setCheckState(0, Qt::Checked); if (m_positionable) { emit positionableTagSelected(m_category->name(), items.at(0)->text(0)); } } else { Q_ASSERT(false); } m_lineEdit->clear(); } updateSelectionCount(); } void ListSelect::slotExternalReturn(const QString &text) { m_lineEdit->setText(text); slotReturn(); } QString AnnotationDialog::ListSelect::category() const { return m_category->name(); } void AnnotationDialog::ListSelect::setSelection( const StringSet& on, const StringSet& partiallyOn ) { for ( QTreeWidgetItemIterator itemIt( m_treeWidget ); *itemIt; ++itemIt ) { if ( partiallyOn.contains( (*itemIt)->text(0) ) ) (*itemIt)->setCheckState( 0, Qt::PartiallyChecked ); else (*itemIt)->setCheckState( 0, on.contains( (*itemIt)->text(0) ) ? Qt::Checked : Qt::Unchecked ); } m_lineEdit->clear(); updateSelectionCount(); } bool AnnotationDialog::ListSelect::isAND() const { return m_and->isChecked(); } void AnnotationDialog::ListSelect::setMode( UsageMode mode ) { m_mode = mode; m_lineEdit->setMode( mode ); if ( mode == SearchMode ) { // "0" below is sorting key which ensures that None is always at top. CheckDropItem* item = new CheckDropItem( m_treeWidget, DB::ImageDB::NONE(), QString::fromLatin1("0") ); configureItem( item ); m_and->show(); m_or->show(); m_or->setChecked( true ); m_showSelectedOnly->hide(); } else { m_and->hide(); m_or->hide(); m_showSelectedOnly->show(); } for ( QTreeWidgetItemIterator itemIt( m_treeWidget ); *itemIt; ++itemIt ) configureItem( dynamic_cast(*itemIt) ); // ensure that the selection count indicator matches the current mode: updateSelectionCount(); } void AnnotationDialog::ListSelect::setViewSortType( Settings::ViewSortType tp ) { showAllChildren(); // set sortType and redisplay with new sortType QString text = m_lineEdit->text(); rePopulate(); m_lineEdit->setText( text ); setMode( m_mode ); // generate the ***NONE*** entry if in search mode m_alphaTreeSort->setChecked( tp == Settings::SortAlphaTree ); m_alphaFlatSort->setChecked( tp == Settings::SortAlphaFlat ); m_dateSort->setChecked( tp == Settings::SortLastUse ); } QString AnnotationDialog::ListSelect::text() const { return m_lineEdit->text(); } void AnnotationDialog::ListSelect::setText( const QString& text ) { m_lineEdit->setText( text ); m_treeWidget->clearSelection(); } void AnnotationDialog::ListSelect::itemSelected(QTreeWidgetItem *item ) { if ( !item ) { // click outside any item return; } if ( m_mode == SearchMode ) { QString txt = item->text(0); QString res; QRegExp regEnd( QString::fromLatin1("\\s*[&|!]\\s*$") ); QRegExp regStart( QString::fromLatin1("^\\s*[&|!]\\s*") ); if (item->checkState(0) == Qt::Checked) { int matchPos = m_lineEdit->text().indexOf(txt); if (matchPos != -1) { return; } int index = m_lineEdit->cursorPosition(); QString start = m_lineEdit->text().left(index); QString end = m_lineEdit->text().mid(index); res = start; if (! start.isEmpty() && ! start.contains(regEnd)) { res += isAND() ? QString::fromLatin1(" & ") : QString::fromLatin1(" | ") ; } res += txt; if (! end.isEmpty() && ! end.contains(regStart)) { res += isAND() ? QString::fromLatin1(" & ") : QString::fromLatin1(" | ") ; } res += end; } else { int index = m_lineEdit->text().indexOf( txt ); if ( index == -1 ) return; QString start = m_lineEdit->text().left(index); QString end = m_lineEdit->text().mid(index + txt.length() ); if ( start.contains( regEnd ) ) start.replace( regEnd, QString::fromLatin1("") ); else end.replace( regStart, QString::fromLatin1("") ); res = start + end; } m_lineEdit->setText( res ); } else { if (m_positionable) { if (item->checkState(0) == Qt::Checked) { emit positionableTagSelected(m_category->name(), item->text(0)); } else { emit positionableTagDeselected(m_category->name(), item->text(0)); } } m_lineEdit->clear(); showAllChildren(); ensureAllInstancesAreStateChanged( item ); } } void AnnotationDialog::ListSelect::showContextMenu(const QPoint& pos) { QMenu* menu = new QMenu( this ); QTreeWidgetItem* item = m_treeWidget->itemAt(pos); // click on any item QString title = i18n("No Item Selected"); if ( item ) title = item->text(0); QLabel* label = new QLabel( i18n("%1",title), menu ); label->setAlignment( Qt::AlignCenter ); QWidgetAction* action = new QWidgetAction(menu); action->setDefaultWidget( label ); menu->addAction(action); QAction* deleteAction = menu->addAction( SmallIcon(QString::fromLatin1("edit-delete")), i18n("Delete") ); QAction* renameAction = menu->addAction( i18n("Rename...") ); QLabel* categoryTitle = new QLabel( i18n("Tag Groups"), menu ); categoryTitle->setAlignment( Qt::AlignCenter ); action = new QWidgetAction( menu ); action->setDefaultWidget( categoryTitle ); menu->addAction( action ); // -------------------------------------------------- Add/Remove member group DB::MemberMap& memberMap = DB::ImageDB::instance()->memberMap(); QMenu* members = new QMenu( i18n( "Tag groups" ) ); menu->addMenu( members ); QAction* newCategoryAction = nullptr; if ( item ) { QStringList grps = memberMap.groups( m_category->name() ); for( QStringList::ConstIterator it = grps.constBegin(); it != grps.constEnd(); ++it ) { if (!memberMap.canAddMemberToGroup(m_category->name(), *it, item->text(0))) continue; QAction* action = members->addAction( *it ); action->setCheckable(true); action->setChecked( (bool) memberMap.members( m_category->name(), *it, false ).contains( item->text(0) ) ); action->setData( *it ); } if ( !grps.isEmpty() ) members->addSeparator(); newCategoryAction = members->addAction( i18n("Add this tag to a new tag group..." ) ); } QAction* newSubcategoryAction = menu->addAction( i18n( "Make this tag a tag group and add a tag..." ) ); // -------------------------------------------------- Take item out of category QTreeWidgetItem* parent = item ? item->parent() : nullptr; QAction* takeAction = nullptr; if ( parent ) takeAction = menu->addAction( i18n( "Remove from tag group %1", parent->text(0) ) ); // -------------------------------------------------- sort QLabel* sortTitle = new QLabel( i18n("Sorting") ); sortTitle->setAlignment( Qt::AlignCenter ); action = new QWidgetAction( menu ); action->setDefaultWidget( sortTitle ); menu->addAction( action ); QAction* usageAction = menu->addAction( i18n("Usage") ); QAction* alphaFlatAction = menu->addAction( i18n("Alphabetical (Flat)") ); QAction* alphaTreeAction = menu->addAction( i18n("Alphabetical (Tree)") ); usageAction->setCheckable(true); usageAction->setChecked( Settings::SettingsData::instance()->viewSortType() == Settings::SortLastUse); alphaFlatAction->setCheckable(true); alphaFlatAction->setChecked( Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaFlat); alphaTreeAction->setCheckable(true); alphaTreeAction->setChecked( Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree); if ( !item ) { deleteAction->setEnabled( false ); renameAction->setEnabled( false ); members->setEnabled( false ); newSubcategoryAction->setEnabled( false ); } // -------------------------------------------------- exec QAction* which = menu->exec( m_treeWidget->mapToGlobal(pos)); if ( which == nullptr ) return; else if ( which == deleteAction ) { Q_ASSERT( item ); int code = KMessageBox::warningContinueCancel( this, i18n("

Do you really want to delete \"%1\"?
" "Deleting the item will remove any information " "about it from any image containing the item.

" ,title), i18n("Really Delete %1?",item->text(0)), KGuiItem(i18n("&Delete"),QString::fromLatin1("editdelete")) ); if ( code == KMessageBox::Continue ) { if (item->checkState(0) == Qt::Checked && m_positionable) { // An area could be linked against this. We can use positionableTagDeselected // here, as the procedure is the same as if the tag had been deselected. emit positionableTagDeselected(m_category->name(), item->text(0)); } m_category->removeItem( item->text(0) ); rePopulate(); } } else if ( which == renameAction ) { Q_ASSERT( item ); bool ok; QString newStr = QInputDialog::getText( this, i18n("Rename Item"), i18n("Enter new name:"), QLineEdit::Normal, item->text(0), &ok ); if ( ok && !newStr.isEmpty() && newStr != item->text(0) ) { int code = KMessageBox::questionYesNo( this, i18n("

Do you really want to rename \"%1\" to \"%2\"?
" "Doing so will rename \"%3\" " "on any image containing it.

" ,item->text(0),newStr,item->text(0)), i18n("Really Rename %1?",item->text(0)) ); if ( code == KMessageBox::Yes ) { QString oldStr = item->text(0); m_category->renameItem( oldStr, newStr ); bool checked = item->checkState(0) == Qt::Checked; rePopulate(); // rePopuldate doesn't ask the backend if the item should be checked, so we need to do that. checkItem( newStr, checked ); // rename the category image too QString oldFile = m_category->fileForCategoryImage( category(), oldStr ); QString newFile = m_category->fileForCategoryImage( category(), newStr ); KIO::move( QUrl(oldFile), QUrl(newFile) ); if (m_positionable) { // Also take care of areas that could be linked against this emit positionableTagRenamed(m_category->name(), oldStr, newStr); } } } } else if ( which == usageAction ) { Settings::SettingsData::instance()->setViewSortType( Settings::SortLastUse ); } else if ( which == alphaTreeAction ) { Settings::SettingsData::instance()->setViewSortType( Settings::SortAlphaTree ); } else if ( which == alphaFlatAction ) { Settings::SettingsData::instance()->setViewSortType( Settings::SortAlphaFlat ); } else if ( which == newCategoryAction ) { Q_ASSERT( item ); QString superCategory = QInputDialog::getText( this, i18n("New tag group"), i18n("Name for the new tag group the tag will be added to:") ); if ( superCategory.isEmpty() ) return; memberMap.addGroup( m_category->name(), superCategory ); memberMap.addMemberToGroup( m_category->name(), superCategory, item->text(0) ); //DB::ImageDB::instance()->setMemberMap( memberMap ); rePopulate(); } else if ( which == newSubcategoryAction ) { Q_ASSERT( item ); QString subCategory = QInputDialog::getText( this, i18n("Add a tag"), i18n("Name for the tag to be added to this tag group:") ); if ( subCategory.isEmpty() ) return; m_category->addItem( subCategory ); memberMap.addGroup( m_category->name(), item->text(0) ); memberMap.addMemberToGroup( m_category->name(), item->text(0), subCategory ); //DB::ImageDB::instance()->setMemberMap( memberMap ); if ( isInputMode() ) m_category->addItem( subCategory ); rePopulate(); if ( isInputMode() ) checkItem( subCategory, true ); } else if ( which == takeAction ) { Q_ASSERT( item ); memberMap.removeMemberFromGroup( m_category->name(), parent->text(0), item->text(0) ); rePopulate(); } else { Q_ASSERT( item ); QString checkedItem = which->data().value(); if ( which->isChecked() ) // choosing the item doesn't check it, so this is the value before. memberMap.addMemberToGroup( m_category->name(), checkedItem, item->text(0) ); else memberMap.removeMemberFromGroup( m_category->name(), checkedItem, item->text(0) ); rePopulate(); } delete menu; } void AnnotationDialog::ListSelect::addItems( DB::CategoryItem* item, QTreeWidgetItem* parent ) { for( QList::ConstIterator subcategoryIt = item->mp_subcategories.constBegin(); subcategoryIt != item->mp_subcategories.constEnd(); ++subcategoryIt ) { CheckDropItem* newItem = nullptr; if ( parent == nullptr ) newItem = new CheckDropItem( m_treeWidget, (*subcategoryIt)->mp_name, QString() ); else newItem = new CheckDropItem( m_treeWidget, parent, (*subcategoryIt)->mp_name, QString() ); newItem->setExpanded(true); configureItem( newItem ); addItems( *subcategoryIt, newItem ); } } void AnnotationDialog::ListSelect::populate() { m_treeWidget->clear(); if ( Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree ) populateAlphaTree(); else if ( Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaFlat ) populateAlphaFlat(); else populateMRU(); hideUntaggedImagesTag(); } bool AnnotationDialog::ListSelect::searchForUntaggedImagesTagNeeded() { if (!Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() || Settings::SettingsData::instance()->untaggedImagesTagVisible()) { return false; } if (Settings::SettingsData::instance()->untaggedCategory() != category()) { return false; } return true; } void AnnotationDialog::ListSelect::hideUntaggedImagesTag() { if (! searchForUntaggedImagesTagNeeded()) { return; } QTreeWidgetItem* untaggedImagesTag = getUntaggedImagesTag(); if (untaggedImagesTag) { untaggedImagesTag->setHidden(true); } } void AnnotationDialog::ListSelect::slotSortDate() { Settings::SettingsData::instance()->setViewSortType( Settings::SortLastUse ); } void AnnotationDialog::ListSelect::slotSortAlphaTree() { Settings::SettingsData::instance()->setViewSortType( Settings::SortAlphaTree ); } void AnnotationDialog::ListSelect::slotSortAlphaFlat() { Settings::SettingsData::instance()->setViewSortType( Settings::SortAlphaFlat ); } void AnnotationDialog::ListSelect::rePopulate() { const StringSet on = itemsOn(); const StringSet noChange = itemsUnchanged(); populate(); setSelection( on, noChange ); if( ShowSelectionOnlyManager::instance().selectionIsLimited() ) limitToSelection(); } void AnnotationDialog::ListSelect::showOnlyItemsMatching( const QString& text ) { ListViewTextMatchHider dummy( text, Settings::SettingsData::instance()->matchType(), m_treeWidget ); ShowSelectionOnlyManager::instance().unlimitFromSelection(); } void AnnotationDialog::ListSelect::populateAlphaTree() { DB::CategoryItemPtr item = m_category->itemsCategories(); m_treeWidget->setRootIsDecorated( true ); addItems( item.data(), 0 ); m_treeWidget->sortByColumn(0, Qt::AscendingOrder); m_treeWidget->setSortingEnabled(true); } void AnnotationDialog::ListSelect::populateAlphaFlat() { QStringList items = m_category->itemsInclCategories(); items.sort(); m_treeWidget->setRootIsDecorated( false ); for( QStringList::ConstIterator itemIt = items.constBegin(); itemIt != items.constEnd(); ++itemIt ) { CheckDropItem* item = new CheckDropItem( m_treeWidget, *itemIt, *itemIt ); configureItem( item ); } m_treeWidget->sortByColumn(1, Qt::AscendingOrder); m_treeWidget->setSortingEnabled(true); } void AnnotationDialog::ListSelect::populateMRU() { QStringList items = m_category->itemsInclCategories(); m_treeWidget->setRootIsDecorated( false ); int index = 100000; // This counter will be converted to a string, and compared, and we don't want "1111" to be less than "2" for( QStringList::ConstIterator itemIt = items.constBegin(); itemIt != items.constEnd(); ++itemIt ) { ++index; CheckDropItem* item = new CheckDropItem( m_treeWidget, *itemIt, QString::number( index ) ); configureItem( item ); } m_treeWidget->sortByColumn(1, Qt::AscendingOrder); m_treeWidget->setSortingEnabled(true); } void AnnotationDialog::ListSelect::toggleSortType() { Settings::SettingsData* data = Settings::SettingsData::instance(); if ( data->viewSortType() == Settings::SortLastUse ) data->setViewSortType( Settings::SortAlphaTree ); else if ( data->viewSortType() == Settings::SortAlphaTree ) data->setViewSortType( Settings::SortAlphaFlat ); else data->setViewSortType( Settings::SortLastUse ); } void AnnotationDialog::ListSelect::updateListview() { // update item list (e.g. when MatchType changes): showOnlyItemsMatching( text() ); } void AnnotationDialog::ListSelect::limitToSelection() { if ( !isInputMode() ) return; m_showSelectedOnly->setChecked( true ); ListViewCheckedHider dummy( m_treeWidget ); hideUntaggedImagesTag(); } void AnnotationDialog::ListSelect::showAllChildren() { m_showSelectedOnly->setChecked( false ); showOnlyItemsMatching( QString() ); hideUntaggedImagesTag(); } QTreeWidgetItem* AnnotationDialog::ListSelect::getUntaggedImagesTag() { QList matchingTags = m_treeWidget->findItems( Settings::SettingsData::instance()->untaggedTag(), Qt::MatchExactly | Qt::MatchRecursive, 0 ); // Be sure not to crash here in case the config points to a non-existent tag if (matchingTags.at(0) == nullptr) { return 0; } else { return matchingTags.at(0); } } void AnnotationDialog::ListSelect::updateSelectionCount() { if (m_baseTitle.isEmpty() /* --> first time */ || ! parentWidget()->windowTitle().startsWith(m_baseTitle) /* --> title has changed */) { // save the original parentWidget title m_baseTitle = parentWidget()->windowTitle(); } int itemsOnCount = itemsOn().size(); // Don't count the untagged images tag: if (searchForUntaggedImagesTagNeeded()) { QTreeWidgetItem* untaggedImagesTag = getUntaggedImagesTag(); if (untaggedImagesTag) { if (untaggedImagesTag->checkState(0) != Qt::Unchecked) { itemsOnCount--; } } } switch(m_mode) { case InputMultiImageConfigMode: if (itemsUnchanged().size() > 0) { // if min != max // tri-state selection -> show min-max (selected items vs. partially selected items): parentWidget()->setWindowTitle(i18nc( "Category name, then min-max of selected tags across several images. E.g. 'People (1-2)'", "%1 (%2-%3)", m_baseTitle, itemsOnCount, itemsOnCount + itemsUnchanged().size() ) ); break; } // else fall through and only show one number: /* FALLTHROUGH */ case InputSingleImageConfigMode: if (itemsOnCount > 0) { // if any tags have been selected // "normal" on/off states -> show selected items parentWidget()->setWindowTitle( i18nc("Category name, then number of selected tags. E.g. 'People (1)'", "%1 (%2)", m_baseTitle, itemsOnCount)); break; } // else fall through and only show category /* FALLTHROUGH */ case SearchMode: // no indicator while searching parentWidget()->setWindowTitle(m_baseTitle); break; } } void AnnotationDialog::ListSelect::configureItem( CategoryListView::CheckDropItem* item ) { bool isDNDAllowed = Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree; item->setDNDEnabled( isDNDAllowed && ! m_category->isSpecialCategory() ); } bool AnnotationDialog::ListSelect::isInputMode() const { return m_mode != SearchMode; } StringSet AnnotationDialog::ListSelect::itemsOn() const { return itemsOfState( Qt::Checked ); } StringSet AnnotationDialog::ListSelect::itemsOff() const { return itemsOfState( Qt::Unchecked ); } StringSet AnnotationDialog::ListSelect::itemsOfState(Qt::CheckState state ) const { StringSet res; for ( QTreeWidgetItemIterator itemIt( m_treeWidget ); *itemIt; ++itemIt ) { if ( (*itemIt)->checkState(0) == state ) res.insert( (*itemIt)->text(0) ); } return res; } StringSet AnnotationDialog::ListSelect::itemsUnchanged() const { return itemsOfState( Qt::PartiallyChecked ); } void AnnotationDialog::ListSelect::checkItem( const QString itemText, bool b ) { QList items = m_treeWidget->findItems( itemText, Qt::MatchExactly | Qt::MatchRecursive ); if ( !items.isEmpty() ) items.at(0)->setCheckState(0, b ? Qt::Checked : Qt::Unchecked ); else Q_ASSERT( false ); } /** * An item may be member of a number of categories. Mike may be a member of coworkers and friends. * Selecting the item in one subcategory, should select him in all. */ void AnnotationDialog::ListSelect::ensureAllInstancesAreStateChanged(QTreeWidgetItem *item ) { const bool on = item->checkState(0) == Qt::Checked; for ( QTreeWidgetItemIterator itemIt( m_treeWidget ); *itemIt; ++itemIt ) { if ( (*itemIt) != item && (*itemIt)->text(0) == item->text(0) ) (*itemIt)->setCheckState(0, on ? Qt::Checked : Qt::Unchecked); } } QWidget* AnnotationDialog::ListSelect::lineEdit() const { return m_lineEdit; } void AnnotationDialog::ListSelect::setPositionable(bool positionableState) { m_positionable = positionableState; } bool AnnotationDialog::ListSelect::positionable() const { return m_positionable; } bool AnnotationDialog::ListSelect::tagIsChecked(QString tag) const { QList matchingTags = m_treeWidget->findItems(tag, Qt::MatchExactly | Qt::MatchRecursive, 0); if(matchingTags.isEmpty()) { return false; } return (bool) matchingTags.first()->checkState(0); } /** * @brief ListSelect::connectLineEdit associates a CompletableLineEdit with this ListSelect * This method also allows to connect an external CompletableLineEdit to work with this ListSelect. * @param le */ void ListSelect::connectLineEdit(CompletableLineEdit *le) { le->setObjectName( m_category->name() ); le->setListView( m_treeWidget ); connect( le, &KLineEdit::returnPressed, this, &ListSelect::slotExternalReturn ); } void AnnotationDialog::ListSelect::ensureTagIsSelected(QString category, QString tag) { if (category != m_lineEdit->objectName()) { // The selected tag's category does not belong to this ListSelect return; } // Be sure that tag is actually checked QList matchingTags = m_treeWidget->findItems(tag, Qt::MatchExactly | Qt::MatchRecursive, 0); // If we have the requested category, but not this tag, add it. // This should only happen if the recognition database is copied from another database // or has been changed outside of KPA. But this _can_ happen and simply adding a // missing tag does not hurt ;-) if(matchingTags.isEmpty()) { m_category->addItem(tag); rePopulate(); // Now, we find it matchingTags = m_treeWidget->findItems(tag, Qt::MatchExactly | Qt::MatchRecursive, 0); } matchingTags.first()->setCheckState(0, Qt::Checked); } void AnnotationDialog::ListSelect::deselectTag(QString tag) { QList matchingTags = m_treeWidget->findItems(tag, Qt::MatchExactly | Qt::MatchRecursive, 0); matchingTags.first()->setCheckState(0, Qt::Unchecked); } -#include "ListSelect.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ShowSelectionOnlyManager.cpp b/AnnotationDialog/ShowSelectionOnlyManager.cpp index a2307026..8bd691a5 100644 --- a/AnnotationDialog/ShowSelectionOnlyManager.cpp +++ b/AnnotationDialog/ShowSelectionOnlyManager.cpp @@ -1,54 +1,54 @@ -/* Copyright (C) 2003-2015 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + #include "ShowSelectionOnlyManager.h" AnnotationDialog::ShowSelectionOnlyManager& AnnotationDialog::ShowSelectionOnlyManager::instance() { static ShowSelectionOnlyManager instance; return instance; } AnnotationDialog::ShowSelectionOnlyManager::ShowSelectionOnlyManager() :m_limit(false) { } bool AnnotationDialog::ShowSelectionOnlyManager::selectionIsLimited() const { return m_limit; } void AnnotationDialog::ShowSelectionOnlyManager::toggle() { m_limit = !m_limit; if ( m_limit ) emit limitToSelected(); else emit broaden(); } void AnnotationDialog::ShowSelectionOnlyManager::unlimitFromSelection() { if ( m_limit ) { m_limit = false; emit broaden(); } } -#include "ShowSelectionOnlyManager.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/JobInterface.cpp b/BackgroundTaskManager/JobInterface.cpp index 7739482c..254c9072 100644 --- a/BackgroundTaskManager/JobInterface.cpp +++ b/BackgroundTaskManager/JobInterface.cpp @@ -1,69 +1,68 @@ -/* Copyright (C) 2012 Jesper K. Pedersen +/* Copyright (C) 2012-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 "JobInterface.h" #include "JobManager.h" #include #ifdef DEBUG_JOBMANAGER #define Debug qDebug #else #define Debug if (0) qDebug #endif /** \class BackgroundTaskManager::JobInterface \brief Interfaces for jobs to be executed using \ref BackgroundTaskManager::JobManager Each job must override \ref execute, and must emit the signal completed. Emitting the signal is crusial, as the JobManager will otherwise stall. */ BackgroundTaskManager::JobInterface::JobInterface(BackgroundTaskManager::Priority priority) : JobInfo(priority), m_dependencies(0) { Debug() << "Created Job #" << jobIndex(); connect( this, SIGNAL(completed()), this, SLOT(stop())); } BackgroundTaskManager::JobInterface::~JobInterface() { } void BackgroundTaskManager::JobInterface::start() { Debug("Starting Job (#%d): %s %s", jobIndex(), qPrintable(title()), qPrintable(details())); JobInfo::start(); execute(); } void BackgroundTaskManager::JobInterface::addDependency(BackgroundTaskManager::JobInterface *job) { m_dependencies++; connect(job,SIGNAL(completed()),this, SLOT(dependedJobCompleted())); } void BackgroundTaskManager::JobInterface::dependedJobCompleted() { m_dependencies--; if ( m_dependencies == 0 ) BackgroundTaskManager::JobManager::instance()->addJob(this); } -#include "JobInterface.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/BrowserWidget.cpp b/Browser/BrowserWidget.cpp index 721d9e1e..c82e67c5 100644 --- a/Browser/BrowserWidget.cpp +++ b/Browser/BrowserWidget.cpp @@ -1,482 +1,480 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "BrowserWidget.h" #include #include #include #include #include #include "ImageViewPage.h" #include "CategoryPage.h" #include "TreeFilter.h" #include #include #include "OverviewPage.h" #include "enums.h" #include #include "Settings/SettingsData.h" #include #include #include "Utilities/Util.h" #include "Utilities/ShowBusyCursor.h" #include #include "DB/CategoryCollection.h" #include "TreeCategoryModel.h" Browser::BrowserWidget* Browser::BrowserWidget::s_instance = nullptr; bool Browser::BrowserWidget::s_isResizing = false; Browser::BrowserWidget::BrowserWidget( QWidget* parent ) :QWidget( parent ), m_current(-1) { Q_ASSERT( !s_instance ); s_instance = this; createWidgets(); connect( DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::categoryCollectionChanged, this, &BrowserWidget::reload); connect( this, &BrowserWidget::viewChanged, this, &BrowserWidget::resetIconViewSearch); m_filterProxy = new TreeFilter(this); m_filterProxy->setFilterKeyColumn(0); m_filterProxy->setFilterCaseSensitivity( Qt::CaseInsensitive ); m_filterProxy->setSortRole( ValueRole ); m_filterProxy->setSortCaseSensitivity( Qt::CaseInsensitive ); addAction( new OverviewPage( Breadcrumb::home(), DB::ImageSearchInfo(), this ) ); QTimer::singleShot( 0, this, SLOT(emitSignals()) ); } void Browser::BrowserWidget::forward() { int targetIndex = m_current; while ( targetIndex < m_list.count()-1 ) { targetIndex++; if ( m_list[targetIndex]->showDuringMovement() ) { break; } } activatePage(targetIndex); } void Browser::BrowserWidget::back() { int targetIndex = m_current; while ( targetIndex > 0 ) { targetIndex--; if ( m_list[targetIndex]->showDuringMovement() ) break; } activatePage(targetIndex); } void Browser::BrowserWidget::activatePage(int pageIndex) { if (pageIndex != m_current) { if (currentAction() != 0) { currentAction()->deactivate(); } m_current = pageIndex; go(); } } void Browser::BrowserWidget::go() { switchToViewType( currentAction()->viewType() ); currentAction()->activate(); setBranchOpen(QModelIndex(), true); adjustTreeViewColumnSize(); emitSignals(); } void Browser::BrowserWidget::addSearch( DB::ImageSearchInfo& info ) { addAction( new OverviewPage( Breadcrumb::empty(), info, this ) ); } void Browser::BrowserWidget::addImageView( const DB::FileName& context ) { addAction( new ImageViewPage( context, this ) ); } void Browser::BrowserWidget::addAction( Browser::BrowserPage* action ) { // remove actions which would go forward in the breadcrumbs while ( (int) m_list.count() > m_current+1 ) { BrowserPage* m = m_list.back(); m_list.pop_back(); delete m; } m_list.append(action); activatePage(m_list.count() - 1); } void Browser::BrowserWidget::emitSignals() { emit canGoBack( m_current > 0 ); emit canGoForward( m_current < (int)m_list.count()-1 ); if ( currentAction()->viewer() == ShowBrowser ) emit showingOverview(); emit isSearchable( currentAction()->isSearchable() ); emit isViewChangeable( currentAction()->isViewChangeable() ); bool isCategoryAction = (dynamic_cast( currentAction() ) != 0); if ( isCategoryAction ) { DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName( currentCategory() ); Q_ASSERT( category.data() ); emit currentViewTypeChanged( category->viewType()); } emit pathChanged( createPath() ); emit viewChanged(); emit imageCount( DB::ImageDB::instance()->count(currentAction()->searchInfo()).total() ); } void Browser::BrowserWidget::home() { addAction( new OverviewPage( Breadcrumb::home(), DB::ImageSearchInfo(), this ) ); } void Browser::BrowserWidget::reload() { currentAction()->activate(); } Browser::BrowserWidget* Browser::BrowserWidget::instance() { Q_ASSERT( s_instance ); return s_instance; } void Browser::BrowserWidget::load( const QString& category, const QString& value ) { DB::ImageSearchInfo info; info.addAnd( category, value ); DB::MediaCount counts = DB::ImageDB::instance()->count( info ); bool loadImages = (counts.total() < Settings::SettingsData::instance()->autoShowThumbnailView()); if ( Utilities::ctrlKeyDown() ) loadImages = !loadImages; if ( loadImages ) addAction( new ImageViewPage( info, this ) ); else addAction( new OverviewPage( Breadcrumb(value, true) , info, this ) ); go(); topLevelWidget()->raise(); activateWindow(); } DB::ImageSearchInfo Browser::BrowserWidget::currentContext() { return currentAction()->searchInfo(); } void Browser::BrowserWidget::slotSmallListView() { changeViewTypeForCurrentView( DB::Category::TreeView ); } void Browser::BrowserWidget::slotLargeListView() { changeViewTypeForCurrentView( DB::Category::ThumbedTreeView ); } void Browser::BrowserWidget::slotSmallIconView() { changeViewTypeForCurrentView( DB::Category::IconView ); } void Browser::BrowserWidget::slotLargeIconView() { changeViewTypeForCurrentView( DB::Category::ThumbedIconView ); } void Browser::BrowserWidget::changeViewTypeForCurrentView( DB::Category::ViewType type ) { Q_ASSERT( m_list.size() > 0 ); DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName( currentCategory() ); Q_ASSERT( category.data() ); category->setViewType( type ); switchToViewType( type ); reload(); } void Browser::BrowserWidget::setFocus() { m_curView->setFocus(); } QString Browser::BrowserWidget::currentCategory() const { if ( CategoryPage* action = dynamic_cast( currentAction() ) ) return action->category()->name(); else return QString(); } void Browser::BrowserWidget::slotLimitToMatch( const QString& str ) { m_filterProxy->resetCache(); m_filterProxy->setFilterFixedString( str ); setBranchOpen(QModelIndex(), true); adjustTreeViewColumnSize(); } void Browser::BrowserWidget::resetIconViewSearch() { m_filterProxy->resetCache(); m_filterProxy->setFilterRegExp( QString() ); adjustTreeViewColumnSize(); } void Browser::BrowserWidget::slotInvokeSeleted() { if ( !m_curView->currentIndex().isValid() ) { if ( m_filterProxy->rowCount( QModelIndex() ) == 0 ) { // Absolutely nothing to see here :-) return; } else { // Use the first item itemClicked( m_filterProxy->index( 0,0,QModelIndex() ) ); } } else itemClicked( m_curView->currentIndex() ); } void Browser::BrowserWidget::itemClicked( const QModelIndex& index ) { Utilities::ShowBusyCursor busy; BrowserPage* action = currentAction()->activateChild( m_filterProxy->mapToSource( index ) ); if ( action ) addAction( action ); } Browser::BrowserPage* Browser::BrowserWidget::currentAction() const { return m_current >= 0? m_list[m_current] : 0; } void Browser::BrowserWidget::setModel( QAbstractItemModel* model) { m_filterProxy->setSourceModel( model ); // make sure the view knows about the source model change: m_curView->setModel( m_filterProxy ); if (qobject_cast(model)) { // FIXME: The new-style connect here does not work, reload() is not triggered //connect(model, &QAbstractItemModel::dataChanged, this, &BrowserWidget::reload); // The old-style one triggers reload() correctly connect(model, SIGNAL(dataChanged()), this, SLOT(reload())); } } void Browser::BrowserWidget::switchToViewType( DB::Category::ViewType type ) { if ( m_curView ) { m_curView->setModel(0); disconnect( m_curView, &QAbstractItemView::activated, this, &BrowserWidget::itemClicked); } if ( type == DB::Category::TreeView || type == DB::Category::ThumbedTreeView ) { m_curView = m_treeView; } else { m_curView =m_listView; m_filterProxy->invalidate(); m_filterProxy->sort( 0, Qt::AscendingOrder ); m_listView->setViewMode(dynamic_cast(currentAction()) == 0 ? CenteringIconView::NormalIconView : CenteringIconView::CenterView ); } if ( CategoryPage* action = dynamic_cast( currentAction() ) ) { const int size = action->category()->thumbnailSize(); m_curView->setIconSize( QSize(size,size) ); // m_curView->setGridSize( QSize( size+10, size+10 ) ); } // Hook up the new view m_curView->setModel( m_filterProxy ); connect( m_curView, &QAbstractItemView::activated, this, &BrowserWidget::itemClicked); m_stack->setCurrentWidget( m_curView ); adjustTreeViewColumnSize(); } void Browser::BrowserWidget::setBranchOpen( const QModelIndex& parent, bool open ) { if ( m_curView != m_treeView ) return; const int count = m_filterProxy->rowCount(parent); if ( count > 5 ) open = false; m_treeView->setExpanded( parent, open ); for ( int row = 0; row < count; ++row ) setBranchOpen( m_filterProxy->index( row, 0 ,parent ), open ); } Browser::BreadcrumbList Browser::BrowserWidget::createPath() const { BreadcrumbList result; for ( int i = 0; i <= m_current; ++i ) result.append(m_list[i]->breadcrumb() ); return result; } void Browser::BrowserWidget::widenToBreadcrumb( const Browser::Breadcrumb& breadcrumb ) { while ( currentAction()->breadcrumb() != breadcrumb ) m_current--; go(); } void Browser::BrowserWidget::adjustTreeViewColumnSize() { m_treeView->header()->resizeSections(QHeaderView::ResizeToContents); } void Browser::BrowserWidget::createWidgets() { m_stack = new QStackedWidget; QHBoxLayout* layout = new QHBoxLayout( this ); layout->setContentsMargins(0,0,0,0); layout->addWidget( m_stack ); m_listView = new CenteringIconView ( m_stack ); m_listView->setIconSize( QSize(100,75) ); m_listView->setSelectionMode( QListView::SingleSelection ); m_listView->setSpacing(10); m_listView->setUniformItemSizes(true); m_listView->setResizeMode( QListView::Adjust ); m_stack->addWidget( m_listView ); m_treeView = new QTreeView( m_stack ); m_treeView->setDragEnabled(true); m_treeView->setAcceptDrops(true); m_treeView->setDropIndicatorShown(true); m_treeView->setDefaultDropAction( Qt::MoveAction ); QPalette pal = m_treeView->palette(); pal.setBrush( QPalette::Base, QApplication::palette().color( QPalette::Background ) ); m_treeView->setPalette( pal ); m_treeView->header()->setStretchLastSection(false); m_treeView->header()->setSortIndicatorShown(true); m_treeView->setSortingEnabled(true); m_treeView->sortByColumn( 0, Qt::AscendingOrder ); m_stack->addWidget( m_treeView ); // Do not give focus to the widgets when they are scrolled with the wheel. m_listView->setFocusPolicy( Qt::StrongFocus ); m_treeView->setFocusPolicy( Qt::StrongFocus ); m_treeView->installEventFilter( this ); m_treeView->viewport()->installEventFilter( this ); m_listView->installEventFilter( this ); m_listView->viewport()->installEventFilter( this ); connect( m_treeView, &QTreeView::expanded, this, &BrowserWidget::adjustTreeViewColumnSize); m_curView = nullptr; } bool Browser::BrowserWidget::eventFilter( QObject* /* obj */, QEvent* event) { if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonRelease ) { QMouseEvent* me = static_cast( event ); if ( me->buttons() & Qt::MidButton || me->button() & Qt::MidButton) { handleResizeEvent( me ); return true; } } return false; } void Browser::BrowserWidget::scrollKeyPressed( QKeyEvent* event ) { QApplication::sendEvent(m_curView, event ); } void Browser::BrowserWidget::handleResizeEvent( QMouseEvent* event ) { static int offset; CategoryPage* action = dynamic_cast( currentAction() ); if ( !action ) return; DB::CategoryPtr category = action->category(); if ( !action ) return; if ( event->type() == QEvent::MouseButtonPress ) { m_resizePressPos = event->pos(); offset = category->thumbnailSize(); s_isResizing = true; } else if ( event->type() == QEvent::MouseMove ) { int distance = (event->pos() - m_resizePressPos).x() + (event->pos() - m_resizePressPos).y() / 3; int size = distance + offset; size = qMax( qMin( 512, size ), 32 ); action->category()->setThumbnailSize( size ); m_curView->setIconSize( QSize(size,size) ); m_filterProxy->invalidate(); adjustTreeViewColumnSize(); } else if ( event->type() == QEvent::MouseButtonRelease ) { s_isResizing = false; update(); } } -#include "BrowserWidget.moc" - // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/CategoryListView/DragableTreeWidget.cpp b/CategoryListView/DragableTreeWidget.cpp index f5e4ee4f..34097bc8 100644 --- a/CategoryListView/DragableTreeWidget.cpp +++ b/CategoryListView/DragableTreeWidget.cpp @@ -1,94 +1,93 @@ -/* Copyright (C) 2003-2015 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + #include "DragableTreeWidget.h" #include "DB/Category.h" #include "CheckDropItem.h" #include #include CategoryListView::DragableTreeWidget::DragableTreeWidget( const DB::CategoryPtr& category, QWidget* parent ) :QTreeWidget( parent ), m_category( category ) { setDragEnabled(true); setDragDropMode(DragDrop); viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setSelectionMode(ExtendedSelection); } DB::CategoryPtr CategoryListView::DragableTreeWidget::category() const { return m_category; } void CategoryListView::DragableTreeWidget::emitItemsChanged() { emit itemsChanged(); } QMimeData *CategoryListView::DragableTreeWidget::mimeData(const QList items) const { CategoryListView::DragItemInfoSet selected; for( QTreeWidgetItem* item: items ) { QTreeWidgetItem* parent = item->parent(); QString parentText = parent ? parent->text(0) : QString(); selected.insert( CategoryListView::DragItemInfo( parentText, item->text(0) ) ); } QByteArray data; QDataStream stream( &data, QIODevice::WriteOnly ); stream << selected; QMimeData* mime = new QMimeData; mime->setData(QString::fromUtf8("x-kphotoalbum/x-categorydrag"), data); return mime; } QStringList CategoryListView::DragableTreeWidget::mimeTypes() const { return QStringList(QString::fromUtf8("x-kphotoalbum/x-categorydrag")); } bool CategoryListView::DragableTreeWidget::dropMimeData(QTreeWidgetItem *parent, int, const QMimeData *data, Qt::DropAction ) { CheckDropItem* targetItem = static_cast(parent); if (targetItem == nullptr) { // This can happen when an item is dropped between two other items and not // onto an item, which leads to a crash when calling dataDropped(data). return false; } else { return targetItem->dataDropped(data); } } void CategoryListView::DragableTreeWidget::dragMoveEvent(QDragMoveEvent *event) { // Call super class in any case as it may scroll, which we want even if we reject QTreeWidget::dragMoveEvent(event); if ( event->source() != this ) event->ignore(); QTreeWidgetItem* item = itemAt(event->pos()); if ( item && static_cast(item)->isSelfDrop(event->mimeData())) event->ignore(); } - -#include "DragableTreeWidget.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/Category.cpp b/DB/Category.cpp index db53a35a..8f343a1f 100644 --- a/DB/Category.cpp +++ b/DB/Category.cpp @@ -1,191 +1,190 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + #include "Category.h" #include #include #include #include #include #include #include #include "DB/ImageDB.h" #include "DB/MemberMap.h" #include "CategoryItem.h" #include #include #include using Utilities::StringSet; QPixmap DB::Category::icon( int size, KIconLoader::States state ) const { QPixmap pixmap = KIconLoader::global()->loadIcon( iconName(), KIconLoader::Desktop, size, state, QStringList(), 0L, true); DB::Category* This = const_cast(this); if ( pixmap.isNull() ) { This->blockSignals(true); This->setIconName(defaultIconName()); This->blockSignals(false); pixmap = QIcon::fromTheme(iconName()).pixmap(size); } return pixmap; } QStringList DB::Category::itemsInclCategories() const { // values including member groups QStringList items = this->items(); // add the groups to the list too, but only if the group is not there already, which will be the case // if it has ever been selected once. QStringList groups = DB::ImageDB::instance()->memberMap().groups( name() ); for( QStringList::ConstIterator it = groups.constBegin(); it != groups.constEnd(); ++it ) { if ( ! items.contains( *it ) ) items << *it ; }; return items; } DB::CategoryItem* createItem( const QString& categoryName, const QString& itemName, StringSet handledItems, QMap& categoryItems, QMap& potentialToplevelItems ) { handledItems.insert( itemName ); DB::CategoryItem* result = new DB::CategoryItem( itemName ); const QStringList members = DB::ImageDB::instance()->memberMap().members( categoryName, itemName, false ); for( QStringList::ConstIterator memberIt = members.constBegin(); memberIt != members.constEnd(); ++memberIt ) { if ( !handledItems.contains( *memberIt ) ) { DB::CategoryItem* child; if ( categoryItems.contains( *memberIt ) ) child = categoryItems[*memberIt]->clone(); else child = createItem( categoryName, *memberIt, handledItems, categoryItems, potentialToplevelItems ); potentialToplevelItems.remove( *memberIt ); result->mp_subcategories.append( child ); } } categoryItems.insert( itemName, result ); return result; } DB::CategoryItemPtr DB::Category::itemsCategories() const { const MemberMap& map = ImageDB::instance()->memberMap(); const QStringList groups = map.groups( name() ); QMap categoryItems; QMap potentialToplevelItems; for( QStringList::ConstIterator groupIt = groups.constBegin(); groupIt != groups.constEnd(); ++groupIt ) { if ( !categoryItems.contains( *groupIt ) ) { StringSet handledItems; DB::CategoryItem* child = createItem( name(), *groupIt, handledItems, categoryItems, potentialToplevelItems ); potentialToplevelItems.insert( *groupIt, child ); } } CategoryItem* result = new CategoryItem( QString::fromLatin1("top"), true ); for( QMap::ConstIterator toplevelIt = potentialToplevelItems.constBegin(); toplevelIt != potentialToplevelItems.constEnd(); ++toplevelIt ) { result->mp_subcategories.append( *toplevelIt ); } // Add items not found yet. QStringList elms = items(); for( QStringList::ConstIterator elmIt = elms.constBegin(); elmIt != elms.constEnd(); ++elmIt ) { if ( !categoryItems.contains( *elmIt ) ) result->mp_subcategories.append( new DB::CategoryItem( *elmIt ) ); } return CategoryItemPtr( result ); } QString DB::Category::defaultIconName() const { const QString nm = name().toLower(); if ( nm == QString::fromLatin1("people") ) return QString::fromLatin1("system-users"); if ( nm == QString::fromLatin1("places") || nm == QString::fromLatin1("locations") ) return QString::fromLatin1("network-workgroup"); if ( nm == QString::fromLatin1("events") || nm == QString::fromLatin1("keywords") ) return QString::fromLatin1("dialog-password"); if ( nm == QString::fromLatin1("tokens") ) return QString::fromLatin1("preferences-other"); if ( nm == QString::fromLatin1("folder") ) return QString::fromLatin1("folder"); if ( nm == QString::fromLatin1("media type") ) return QString::fromLatin1("video"); return QString(); } QPixmap DB::Category::categoryImage( const QString& category, QString member, int width, int height ) const { QString fileName = fileForCategoryImage( category, member ); QString key = QString::fromLatin1( "%1-%2" ).arg(width).arg(fileName); QPixmap res; if ( QPixmapCache::find( key, res ) ) return res; QImage img; bool ok = img.load( fileName, "JPEG" ); if ( ! ok ) { if ( DB::ImageDB::instance()->memberMap().isGroup( category, member ) ) img = KIconLoader::global()->loadIcon( QString::fromLatin1( "kuser" ), KIconLoader::Desktop, qMax(width,height) ).toImage(); else img = icon( qMax(width,height) ).toImage(); } res = QPixmap::fromImage( Utilities::scaleImage(img, width, height, Qt::KeepAspectRatio) ); QPixmapCache::insert( key, res ); return res; } void DB::Category::setCategoryImage( const QString& category, QString member, const QImage& image ) { QString dir = Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("CategoryImages" ); QFileInfo fi( dir ); bool ok; if ( !fi.exists() ) { bool ok = QDir().mkdir( dir ); if ( !ok ) { KMessageBox::error( nullptr, i18n("Unable to create directory '%1'.", dir ), i18n("Unable to Create Directory") ); return; } } QString fileName = fileForCategoryImage( category, member ); ok = image.save( fileName, "JPEG" ); if ( !ok ) { KMessageBox::error( nullptr, i18n("Error when saving image '%1'.",fileName), i18n("Error Saving Image") ); return; } QPixmapCache::clear(); } QString DB::Category::fileForCategoryImage( const QString& category, QString member ) const { QString dir = Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("CategoryImages" ); member.replace( QChar::fromLatin1(' '), QChar::fromLatin1('_') ); member.replace( QChar::fromLatin1('/'), QChar::fromLatin1('_') ); QString fileName = dir + QString::fromLatin1("/%1-%2.jpg").arg( category ).arg( member ); return fileName; } - -#include "Category.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/CategoryCollection.cpp b/DB/CategoryCollection.cpp index 5065d608..bd8a81af 100644 --- a/DB/CategoryCollection.cpp +++ b/DB/CategoryCollection.cpp @@ -1,33 +1,32 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "CategoryCollection.h" using namespace DB; void CategoryCollection::itemRenamed( const QString& oldName, const QString& newName ) { emit itemRenamed( static_cast( const_cast( sender() ) ), oldName, newName ); } void CategoryCollection::itemRemoved( const QString& item ) { emit itemRemoved( static_cast( const_cast( sender() ) ), item ); } -#include "CategoryCollection.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageDB.cpp b/DB/ImageDB.cpp index 8d217c95..6262d8b4 100644 --- a/DB/ImageDB.cpp +++ b/DB/ImageDB.cpp @@ -1,195 +1,194 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageDB.h" #include "XMLDB/Database.h" #include #include #include "Browser/BrowserWidget.h" #include "DB/CategoryCollection.h" #include #include "NewImageFinder.h" #include #include #include using namespace DB; ImageDB* ImageDB::s_instance = nullptr; ImageDB* DB::ImageDB::instance() { if ( s_instance == nullptr ) exit(0); // Either we are closing down or ImageDB::instance was called before ImageDB::setup return s_instance; } void ImageDB::setupXMLDB( const QString& configFile ) { if (s_instance) qFatal("ImageDB::setupXMLDB: Setup must be called only once."); s_instance = new XMLDB::Database( configFile ); connectSlots(); } void ImageDB::deleteInstance() { delete s_instance; s_instance = nullptr; } void ImageDB::connectSlots() { connect( Settings::SettingsData::instance(), SIGNAL(locked(bool,bool)), s_instance, SLOT(lockDB(bool,bool)) ); connect( &s_instance->memberMap(), SIGNAL(dirty()), s_instance, SLOT(markDirty())); } QString ImageDB::NONE() { static QString none = QString::fromLatin1("**NONE**"); return none; } DB::FileNameList ImageDB::currentScope(bool requireOnDisk) const { return search( Browser::BrowserWidget::instance()->currentContext(), requireOnDisk ); } void ImageDB::markDirty() { emit dirty(); } void ImageDB::setDateRange( const ImageDate& range, bool includeFuzzyCounts ) { m_selectionRange = range; m_includeFuzzyCounts = includeFuzzyCounts; } void ImageDB::clearDateRange() { m_selectionRange = ImageDate(); } void ImageDB::slotRescan() { bool newImages = NewImageFinder().findImages(); if ( newImages ) markDirty(); emit totalChanged( totalCount() ); } void ImageDB::slotRecalcCheckSums(const DB::FileNameList& inputList) { DB::FileNameList list = inputList; if (list.isEmpty()) { list = images(); md5Map()->clear(); } bool d = NewImageFinder().calculateMD5sums( list, md5Map() ); if ( d ) markDirty(); emit totalChanged( totalCount() ); } DB::FileNameSet DB::ImageDB::imagesWithMD5Changed() { MD5Map map; bool wasCanceled; NewImageFinder().calculateMD5sums(images(), &map, &wasCanceled); if ( wasCanceled ) return DB::FileNameSet(); return md5Map()->diff( map ); } ImageDB::ImageDB() { } DB::MediaCount ImageDB::count( const ImageSearchInfo& searchInfo ) { uint images = 0; uint videos = 0; for (const DB::FileName& fileName : search(searchInfo)) { if ( info(fileName)->mediaType() == Image ) ++images; else ++videos; } return MediaCount( images, videos ); } void ImageDB::slotReread( const DB::FileNameList& list, DB::ExifMode mode) { // Do here a reread of the exif info and change the info correctly in the database without loss of previous added data QProgressDialog dialog( i18n("Loading information from images"), i18n("Cancel"), 0, list.count() ); uint count=0; for( DB::FileNameList::ConstIterator it = list.begin(); it != list.end(); ++it, ++count ) { if ( count % 10 == 0 ) { dialog.setValue( count ); // ensure to call setProgress(0) qApp->processEvents( QEventLoop::AllEvents ); if ( dialog.wasCanceled() ) return; } QFileInfo fi( (*it).absolute() ); if (fi.exists()) info(*it)->readExif(*it, mode); markDirty(); } } DB::FileName ImageDB::findFirstItemInRange(const DB::FileNameList& images, const ImageDate& range, bool includeRanges) const { DB::FileName candidate; QDateTime candidateDateStart; for (const DB::FileName& fileName : images) { ImageInfoPtr iInfo = info(fileName); ImageDate::MatchType match = iInfo->date().isIncludedIn(range); if (match == DB::ImageDate::ExactMatch || (includeRanges && match == DB::ImageDate::RangeMatch)) { if (candidate.isNull() || iInfo->date().start() < candidateDateStart) { candidate = fileName; // Looking at this, can't this just be iInfo->date().start()? // Just in the middle of refactoring other stuff, so leaving // this alone now. TODO(hzeller): revisit. candidateDateStart = info(candidate)->date().start(); } } } return candidate; } /** \fn void ImageDB::renameCategory( const QString& oldName, const QString newName ) * \brief Rename category in media items stored in database. */ -#include "ImageDB.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/MemberMap.cpp b/DB/MemberMap.cpp index 83747e59..bf09a59d 100644 --- a/DB/MemberMap.cpp +++ b/DB/MemberMap.cpp @@ -1,343 +1,342 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "MemberMap.h" #include "DB/Category.h" #include using namespace DB; MemberMap::MemberMap() :QObject(nullptr), m_dirty( true ), m_loading( false ) { } /** returns the groups directly available from category (non closure that is) */ QStringList MemberMap::groups( const QString& category ) const { return QStringList( m_members[ category ].keys() ); } void MemberMap::deleteGroup( const QString& category, const QString& name ) { m_members[category].remove(name); m_dirty = true; if ( !m_loading ) emit dirty(); } /** return all the members of memberGroup */ QStringList MemberMap::members( const QString& category, const QString& memberGroup, bool closure ) const { if ( closure ) { if ( m_dirty ) calculate(); return m_closureMembers[category][memberGroup].toList(); } else return m_members[category][memberGroup].toList(); } void MemberMap::setMembers( const QString& category, const QString& memberGroup, const QStringList& members ) { StringSet allowedMembers = members.toSet(); for (QStringList::const_iterator i = members.begin(); i != members.end(); ++i) if (!canAddMemberToGroup(category, memberGroup, *i)) allowedMembers.remove(*i); m_members[category][memberGroup] = allowedMembers; m_dirty = true; if ( !m_loading ) emit dirty(); } bool MemberMap::isEmpty() const { return m_members.empty(); } /** returns true if item is a group for category. */ bool MemberMap::isGroup( const QString& category, const QString& item ) const { return m_members[category].find(item) != m_members[category].end(); } /** return a map from groupName to list of items for category example: { USA |-> [Chicago, Grand Canyon, Santa Clara], Denmark |-> [Esbjerg, Odense] } */ QMap MemberMap::groupMap( const QString& category ) const { if ( m_dirty ) calculate(); return m_closureMembers[category]; } /** Calculates the closure for group, that is finds all members for group. Imagine there is a group called USA, and that this groups has a group inside it called Califonia, Califonia consists of members San Fransisco and Los Angeless. This function then maps USA to include Califonia, San Fransisco and Los Angeless. */ QStringList MemberMap::calculateClosure( QMap& resultSoFar, const QString& category, const QString& group ) const { resultSoFar[group] = StringSet(); // Prevent against cykles. StringSet members = m_members[category][group]; StringSet result = members; for( StringSet::const_iterator it = members.begin(); it != members.end(); ++it ) { if ( resultSoFar.contains( *it ) ) { result += resultSoFar[*it]; } else if ( isGroup(category, *it ) ) { result += calculateClosure( resultSoFar, category, *it ).toSet(); } } resultSoFar[group] = result; return result.toList(); } /** This methods create the map _closureMembers from _members This is simply to avoid finding the closure each and every time it is needed. */ void MemberMap::calculate() const { m_closureMembers.clear(); // run through all categories for( QMap< QString,QMap >::ConstIterator categoryIt= m_members.begin(); categoryIt != m_members.end(); ++categoryIt ) { QString category = categoryIt.key(); QMap groupMap = categoryIt.value(); // Run through each of the groups for the given categories for( QMap::const_iterator groupIt= groupMap.constBegin() ; groupIt != groupMap.constEnd() ; ++groupIt ) { QString group = groupIt.key(); if ( m_closureMembers[category].find( group ) == m_closureMembers[category].end() ) { (void) calculateClosure( m_closureMembers[category], category, group ); } } } m_dirty = false; } void MemberMap::renameGroup( const QString& category, const QString& oldName, const QString& newName ) { // Don't allow overwriting to avoid creating cycles if (m_members[category].contains(newName)) return; m_dirty = true; if ( !m_loading ) emit dirty(); QMap& groupMap = m_members[category]; groupMap.insert(newName,m_members[category][oldName] ); groupMap.remove( oldName ); for( StringSet &set: groupMap ) { if ( set.contains( oldName ) ) { set.remove( oldName ); set.insert( newName ); } } } MemberMap::MemberMap( const MemberMap& other ) : QObject( nullptr ), m_members( other.memberMap() ), m_dirty( true ), m_loading( false ) { } void MemberMap::deleteItem( DB::Category* category, const QString& name) { QMap& groupMap = m_members[category->name()]; for( StringSet &items: groupMap ) { items.remove( name ); } m_members[category->name()].remove(name); m_dirty = true; if ( !m_loading ) emit dirty(); } void MemberMap::renameItem( DB::Category* category, const QString& oldName, const QString& newName ) { if (oldName == newName) return; QMap& groupMap = m_members[category->name()]; for( StringSet &items: groupMap ) { if (items.contains( oldName ) ) { items.remove( oldName ); items.insert( newName ); } } if ( groupMap.contains( oldName ) ) { groupMap[newName] = groupMap[oldName]; groupMap.remove(oldName); } m_dirty = true; if ( !m_loading ) emit dirty(); } MemberMap& MemberMap::operator=( const MemberMap& other ) { if ( this != &other ) { m_members = other.memberMap(); m_dirty = true; } return *this; } void MemberMap::addMemberToGroup( const QString& category, const QString& group, const QString& item ) { // Only test for cycles after database is already loaded if (!m_loading && !canAddMemberToGroup(category, group, item)) return; if ( item.isEmpty() ) { qWarning( "Null item tried inserted into group %s", qPrintable(group)); return; } m_members[category][group].insert( item ); if (m_loading) { m_dirty = true; } else if (!m_dirty) { // Update _closureMembers to avoid marking it dirty QMap& categoryClosure = m_closureMembers[category]; categoryClosure[group].insert(item); QMap::const_iterator closureOfItem = categoryClosure.constFind(item); const StringSet* closureOfItemPtr(nullptr); if (closureOfItem != categoryClosure.constEnd()) { closureOfItemPtr = &(*closureOfItem); categoryClosure[group] += *closureOfItem; } for (QMap::iterator i = categoryClosure.begin(); i != categoryClosure.end(); ++i) if ((*i).contains(group)) { (*i).insert(item); if (closureOfItemPtr) (*i) += *closureOfItemPtr; } } if ( !m_loading ) emit dirty(); } void MemberMap::removeMemberFromGroup( const QString& category, const QString& group, const QString& item ) { Q_ASSERT( m_members.contains(category) ); if ( m_members[category].contains( group ) ) m_members[category][group].remove( item ); m_dirty = true; if ( !m_loading ) emit dirty(); } void MemberMap::addGroup( const QString& category, const QString& group ) { if ( ! m_members[category].contains( group ) ) { m_members[category].insert( group, StringSet() ); } if ( !m_loading ) emit dirty(); } void MemberMap::renameCategory( const QString& oldName, const QString& newName ) { if (oldName == newName) return; m_members[newName] = m_members[oldName]; m_members.remove(oldName); m_closureMembers[newName] = m_closureMembers[oldName]; m_closureMembers.remove(oldName); if ( !m_loading ) emit dirty(); } void MemberMap::deleteCategory(const QString &category) { m_members.remove(category); m_closureMembers.remove(category); if ( !m_loading ) emit dirty(); } QMap DB::MemberMap::inverseMap( const QString& category ) const { QMap res; const QMap& map = m_members[category]; for( QMap::ConstIterator mapIt = map.begin(); mapIt != map.end(); ++mapIt ) { QString group = mapIt.key(); StringSet members = mapIt.value(); for( StringSet::const_iterator memberIt = members.begin(); memberIt != members.end(); ++memberIt ) { res[*memberIt].insert( group ); } } return res; } bool DB::MemberMap::hasPath( const QString& category, const QString& from, const QString& to ) const { if (from == to) return true; else if (!m_members[category].contains(from)) // Try to avoid calculate(), which is quite time consuming. return false; else { // return members(category, from, true).contains(to); if ( m_dirty ) calculate(); return m_closureMembers[category][from].contains(to); } } void DB::MemberMap::setLoading( bool b ) { if (m_loading && !b) { // TODO: Remove possible loaded cycles. } m_loading = b; } -#include "MemberMap.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DateBar/DateBarWidget.cpp b/DateBar/DateBarWidget.cpp index 8ad333d9..aa335cdb 100644 --- a/DateBar/DateBarWidget.cpp +++ b/DateBar/DateBarWidget.cpp @@ -1,850 +1,849 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "DateBarWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "MouseHandler.h" #include "Settings/SettingsData.h" const int borderAboveHistogram = 4; const int borderArroundWidget = 0; const int buttonWidth = 22; const int arrowLength = 20; /** * \class DateBar::DateBarWidget * \brief This class represents the date bar at the bottom of the main window. * * The mouse interaction is handled by the classes which inherits \ref DateBar::MouseHandler, while the logic for * deciding the length (in minutes, hours, days, etc) are handled by subclasses of \ref DateBar::ViewHandler. */ DateBar::DateBarWidget::DateBarWidget( QWidget* parent ) :QWidget( parent ), m_currentHandler( &m_yearViewHandler ),m_tp(YearView), m_currentMouseHandler(nullptr), m_currentUnit(0), m_currentDate( QDateTime::currentDateTime() ),m_includeFuzzyCounts( true ), m_contextMenu(nullptr), m_showResolutionIndicator( true ), m_doAutomaticRangeAdjustment( true ) { setMouseTracking( true ); setFocusPolicy( Qt::StrongFocus ); m_barWidth = Settings::SettingsData::instance()->histogramSize().width(); m_barHeight = Settings::SettingsData::instance()->histogramSize().height(); m_rightArrow = new QToolButton( this ); m_rightArrow->setArrowType( Qt::RightArrow ); m_rightArrow->setAutoRepeat( true ); connect( m_rightArrow, SIGNAL(clicked()), this, SLOT(scrollRight()) ); m_leftArrow = new QToolButton( this ); m_leftArrow->setArrowType( Qt::LeftArrow ); m_leftArrow->setAutoRepeat( true ); connect( m_leftArrow, SIGNAL(clicked()), this, SLOT(scrollLeft()) ); m_zoomIn = new QToolButton( this ); m_zoomIn->setIcon( QIcon::fromTheme( QStringLiteral( "zoom-in" ) ) ); connect( m_zoomIn, SIGNAL(clicked()), this, SLOT(zoomIn()) ); connect( this, SIGNAL(canZoomIn(bool)), m_zoomIn, SLOT(setEnabled(bool)) ); m_zoomOut = new QToolButton( this ); m_zoomOut->setIcon( QIcon::fromTheme( QStringLiteral( "zoom-out" ) ) ); connect( m_zoomOut, SIGNAL(clicked()), this, SLOT(zoomOut()) ); connect( this, SIGNAL(canZoomOut(bool)), m_zoomOut, SLOT(setEnabled(bool)) ); m_cancelSelection = new QToolButton( this ); m_cancelSelection->setIcon( QIcon( QStringLiteral( "dialog-close" ) ) ); connect( m_cancelSelection, SIGNAL(clicked()), this, SLOT(clearSelection()) ); m_cancelSelection->setEnabled( false ); m_cancelSelection->setToolTip( i18n("Widen selection to include all images and videos again") ); placeAndSizeButtons(); m_focusItemDragHandler = new FocusItemDragHandler( this ); m_barDragHandler = new BarDragHandler( this ); m_selectionHandler = new SelectionHandler( this ); setWhatsThis( i18nc( "@info", "The date bar" "The date bar gives you an overview of the approximate number of images taken in a given time frame." "Time units are shown on the timeline. Above it, a histogram indicates the number of images for that time range." "You can interact with the date bar in several ways:" "Zoom in or out by using the +/- buttons or Ctrl + scrollwheel." "Scroll the timeline either by using the arrow buttons or the scroll wheel, or by dragging it using the middle mouse button." "Restrict the current view to a given time frame by clicking below the timeline and marking the time frame." "Clicking on the timeline sets the focus for the thumbnail view, i.e. jumps to the first thumbnail of the time unit in focus." "") ); setToolTip( whatsThis() ); } QSize DateBar::DateBarWidget::sizeHint() const { int height = qMax( dateAreaGeometry().bottom() + borderArroundWidget, m_barHeight+ buttonWidth + 2* borderArroundWidget + 7 ); return QSize( 800, height ); } QSize DateBar::DateBarWidget::minimumSizeHint() const { int height = qMax( dateAreaGeometry().bottom() + borderArroundWidget, m_barHeight + buttonWidth + 2* borderArroundWidget + 7 ); return QSize( 200, height ); } void DateBar::DateBarWidget::paintEvent( QPaintEvent* /*event*/ ) { QPainter painter( this ); painter.drawPixmap( 0,0, m_buffer ); } void DateBar::DateBarWidget::redraw() { if ( m_buffer.isNull() ) return; QPainter p( &m_buffer ); p.setRenderHint( QPainter::Antialiasing ); p.setFont( font() ); // Fill with background pixels p.save(); p.setPen( Qt::NoPen ); p.setBrush( palette().brush( QPalette::Background ) ); p.drawRect( rect() ); if (!m_dates ) return; // Draw the area with histograms QRect barArea = barAreaGeometry(); p.setPen( palette().color( QPalette::Dark ) ); p.setBrush( palette().brush( QPalette::Base ) ); p.drawRect( barArea ); p.restore(); m_currentHandler->init( dateForUnit( -m_currentUnit, m_currentDate ) ); int right; drawResolutionIndicator( p, &right ); QRect rect = dateAreaGeometry(); rect.setRight( right ); rect.setLeft( rect.left() + buttonWidth + 2 ); drawTickMarks( p, rect ); drawHistograms( p ); drawFocusRectagle( p ); updateArrowState(); repaint(); } void DateBar::DateBarWidget::resizeEvent( QResizeEvent* event ) { placeAndSizeButtons(); m_buffer = QPixmap( event->size() ); m_currentUnit = numberOfUnits()/2; redraw(); } void DateBar::DateBarWidget::drawTickMarks( QPainter& p, const QRect& textRect ) { QRect rect = tickMarkGeometry(); p.save(); p.setPen( QPen( palette().color( QPalette::Text) , 1 ) ); QFont f( font() ); QFontMetrics fm(f); int fontHeight = fm.height(); int unit = 0; QRect clip = rect; clip.setHeight( rect.height() + 2 + fontHeight ); clip.setLeft( clip.left() + 2 ); clip.setRight( clip.right() -2 ); p.setClipRect( clip ); for ( int x = rect.x(); x < rect.right(); x+=m_barWidth, unit += 1 ) { // draw selection indication p.save(); p.setPen( Qt::NoPen ); p.setBrush( palette().brush( QPalette::Highlight ) ); QDateTime date = dateForUnit( unit ); if ( isUnitSelected( unit ) ) p.drawRect( QRect( x, rect.top(), m_barWidth, rect.height() ) ); p.restore(); // draw tickmarks int h = rect.height(); if ( m_currentHandler->isMajorUnit( unit ) ) { QString text = m_currentHandler->text( unit ); int w = fm.width( text ); p.setFont( f ); if ( textRect.right() > x + w/2 && textRect.left() < x - w/2) p.drawText( x - w/2, textRect.top(), w, fontHeight, Qt::TextSingleLine, text ); } else if ( m_currentHandler->isMidUnit( unit ) ) h = (int) ( 2.0/3*rect.height()); else h = (int) ( 1.0/3*rect.height()); p.drawLine( x, rect.top(), x, rect.top() + h ); } p.restore(); } void DateBar::DateBarWidget::setViewType( ViewType tp ) { setViewHandlerForType(tp); redraw(); m_tp = tp; } void DateBar::DateBarWidget::setViewHandlerForType( ViewType tp ) { switch ( tp ) { case DecadeView: m_currentHandler = &m_decadeViewHandler; break; case YearView: m_currentHandler = &m_yearViewHandler; break; case MonthView: m_currentHandler = &m_monthViewHandler; break; case WeekView: m_currentHandler = &m_weekViewHandler; break; case DayView: m_currentHandler = &m_dayViewHandler; break; case HourView: m_currentHandler = &m_hourViewHandler; break; } } void DateBar::DateBarWidget::setDate( const QDateTime& date ) { m_currentDate = date; if ( hasSelection() ) { if ( currentSelection().start() > m_currentDate ) m_currentDate = currentSelection().start(); if ( currentSelection().end() < m_currentDate ) m_currentDate = currentSelection().end(); } if ( unitForDate( m_currentDate ) != -1 ) m_currentUnit = unitForDate( m_currentDate ); redraw(); } void DateBar::DateBarWidget::setImageDateCollection( const QExplicitlySharedDataPointer& dates ) { m_dates = dates; if ( m_doAutomaticRangeAdjustment && m_dates && ! m_dates->lowerLimit().isNull()) { QDateTime start = m_dates->lowerLimit(); QDateTime end = m_dates->upperLimit(); if ( end.isNull() ) end = QDateTime::currentDateTime(); m_currentDate = start; m_currentUnit = 0; // select suitable timeframe: setViewType( HourView ); m_currentHandler->init(start); while ( m_tp != DecadeView && end > dateForUnit( numberOfUnits() ) ) { m_tp = (ViewType) (m_tp-1); setViewHandlerForType( m_tp ); m_currentHandler->init(start); } // center range in datebar: int units = unitForDate( end ); if ( units != -1 ) { m_currentUnit = (numberOfUnits() - units )/2; } } redraw(); } void DateBar::DateBarWidget::drawHistograms( QPainter& p) { QRect rect = barAreaGeometry(); p.save(); p.setClipping( true ); p.setClipRect( rect ); p.setPen( Qt::NoPen ); int unit = 0; int max = 0; for ( int x = rect.x(); x + m_barWidth < rect.right(); x+=m_barWidth, unit += 1 ) { DB::ImageCount count = m_dates->count( rangeForUnit(unit) ); int cnt = count.mp_exact; if ( m_includeFuzzyCounts ) cnt += count.mp_rangeMatch; max = qMax( max, cnt ); } // Calculate the font size for the largest number. QFont f = font(); bool fontFound = false; for ( int i = f.pointSize(); i >= 6; i-=2 ) { f.setPointSize( i ); int w = QFontMetrics(f).width( QString::number( max ) ); if ( w < rect.height() - 6 ) { p.setFont(f); fontFound = true; break; } } unit = 0; for ( int x = rect.x(); x + m_barWidth < rect.right(); x+=m_barWidth, unit += 1 ) { DB::ImageCount count = m_dates->count( rangeForUnit(unit) ); int exact = 0; if ( max != 0 ) exact = (int) ((double) (rect.height()-2) * count.mp_exact / max ); int range = 0; if ( m_includeFuzzyCounts && max != 0 ) range = (int) ((double) (rect.height()-2) * count.mp_rangeMatch / max ); Qt::BrushStyle style = Qt::SolidPattern; if ( !isUnitSelected( unit ) && hasSelection() ) style= Qt::Dense5Pattern; p.setBrush( QBrush( Qt::yellow, style ) ); p.drawRect( x+1, rect.bottom()-range, m_barWidth-2, range ); p.setBrush( QBrush( Qt::green, style ) ); p.drawRect( x+1, rect.bottom()-range-exact, m_barWidth-2, exact ); // Draw the numbers, if they fit. if (fontFound) { int tot = count.mp_exact; if ( m_includeFuzzyCounts ) tot += count.mp_rangeMatch; p.save(); p.translate( x+m_barWidth-3, rect.bottom()-2 ); p.rotate( -90 ); int w = QFontMetrics(f).width( QString::number( tot ) ); if ( w < exact+range-2 ) { p.setPen( Qt::black ); p.drawText( 0,0, QString::number( tot ) ); } p.restore(); } } p.restore(); } void DateBar::DateBarWidget::scrollLeft() { scroll( -1 ); } void DateBar::DateBarWidget::scrollRight() { scroll( 1 ); } void DateBar::DateBarWidget::scroll( int units ) { m_currentDate = dateForUnit( units, m_currentDate ); redraw(); emit dateSelected( currentDateRange(), includeFuzzyCounts() ); } void DateBar::DateBarWidget::drawFocusRectagle( QPainter& p) { QRect rect = barAreaGeometry(); p.save(); int x = rect.left() + m_currentUnit*m_barWidth; QRect inner( QPoint(x-1, borderAboveHistogram), QPoint( x + m_barWidth, borderAboveHistogram + m_barHeight - 1 ) ); p.setPen( QPen( palette().color( QPalette::Dark ), 1 ) ); // Inner rect p.drawRect( inner ); QRect outer = inner; outer.adjust( -2, -2, 2, 2 ); // Outer rect QRegion region = outer; region -= inner; p.setClipping( true ); p.setClipRegion( region ); QColor col = Qt::gray; if ( !hasFocus() ) col = Qt::white; p.setBrush( col ); p.setPen( col ); p.drawRect( outer ); // Shadow below QRect shadow = outer; shadow.adjust( -1,-1, 1, 1 ); region = shadow; region -= outer; p.setPen( palette().color( QPalette::Shadow ) ); p.setClipRegion( region ); p.drawRect( shadow ); // Light above QRect hide = shadow; hide.translate( 1, 1 ); region = shadow; region -= hide; p.setPen( palette().color( QPalette::Light ) ); p.setClipRegion( region ); p.drawRect( shadow ); p.restore(); } void DateBar::DateBarWidget::zoomIn() { if ( m_tp == HourView ) return; zoom(+1); } void DateBar::DateBarWidget::zoomOut() { if ( m_tp == DecadeView ) return; zoom(-1); } void DateBar::DateBarWidget::zoom( int factor ) { ViewType tp = (ViewType) (m_tp+factor); setViewType( tp ); emit canZoomIn( tp != HourView ); emit canZoomOut( tp != DecadeView ); } void DateBar::DateBarWidget::mousePressEvent( QMouseEvent* event ) { if ( (event->button() & ( Qt::MidButton | Qt::LeftButton)) == 0 || event->x() > barAreaGeometry().right() || event->x() < barAreaGeometry().left() ) return; if ( (event->button() & Qt::MidButton) || event->modifiers() & Qt::ControlModifier ) { m_currentMouseHandler = m_barDragHandler; } else { bool onBar = event->y() > barAreaGeometry().bottom(); if ( onBar ) m_currentMouseHandler = m_selectionHandler; else { m_currentMouseHandler= m_focusItemDragHandler; } } m_currentMouseHandler->mousePressEvent( event->x() ); m_cancelSelection->setEnabled( hasSelection() ); emit dateSelected( currentDateRange(), includeFuzzyCounts() ); showStatusBarTip( event->pos() ); redraw(); } void DateBar::DateBarWidget::mouseReleaseEvent( QMouseEvent* ) { if ( m_currentMouseHandler == nullptr ) return; m_currentMouseHandler->endAutoScroll(); m_currentMouseHandler->mouseReleaseEvent(); m_currentMouseHandler = nullptr; } void DateBar::DateBarWidget::mouseMoveEvent( QMouseEvent* event ) { if ( m_currentMouseHandler == nullptr) return; showStatusBarTip( event->pos() ); if ( (event->buttons() & ( Qt::MidButton | Qt::LeftButton)) == 0 ) return; m_currentMouseHandler->endAutoScroll(); m_currentMouseHandler->mouseMoveEvent( event->pos().x() ); } QRect DateBar::DateBarWidget::barAreaGeometry() const { QRect barArea; barArea.setTopLeft( QPoint( borderArroundWidget, borderAboveHistogram ) ); barArea.setRight( width() - borderArroundWidget - 2 * buttonWidth - 2*3 ); // 2 pixels between button and bar + 1 pixel as the pen is one pixel barArea.setHeight( m_barHeight ); return barArea; } int DateBar::DateBarWidget::numberOfUnits() const { return barAreaGeometry().width() / m_barWidth -1 ; } void DateBar::DateBarWidget::setHistogramBarSize( const QSize& size ) { m_barWidth = size.width(); m_barHeight = size.height(); m_currentUnit = numberOfUnits()/2; Q_ASSERT( parentWidget() ); updateGeometry(); Q_ASSERT( parentWidget() ); placeAndSizeButtons(); redraw(); } void DateBar::DateBarWidget::setIncludeFuzzyCounts( bool b ) { m_includeFuzzyCounts = b; redraw(); if ( hasSelection() ) emitRangeSelection( m_selectionHandler->dateRange() ); emit dateSelected( currentDateRange(), includeFuzzyCounts() ); } DB::ImageDate DateBar::DateBarWidget::rangeAt( const QPoint& p ) { int unit = (p.x() - barAreaGeometry().x())/ m_barWidth; return rangeForUnit( unit ); } DB::ImageDate DateBar::DateBarWidget::rangeForUnit( int unit ) { // Note on the use of setTimeSpec. // It came to my attention that addSec would create a QDateTime with internal type LocalStandard, while all the others would have type LocalUnknown, // this resulted in that QDateTime::operator<() would call getUTC(), which took 90% of the time for populating the datebar. QDateTime toUnit = dateForUnit(unit+1).addSecs(-1); toUnit.setTimeSpec( Qt::LocalTime); return DB::ImageDate( dateForUnit(unit), toUnit ); } bool DateBar::DateBarWidget::includeFuzzyCounts() const { return m_includeFuzzyCounts; } void DateBar::DateBarWidget::contextMenuEvent( QContextMenuEvent* event ) { if ( !m_contextMenu ) { m_contextMenu = new QMenu( this ); QAction* action = new QAction( i18n("Show Ranges"), this ); action->setCheckable( true ); m_contextMenu->addAction(action); action->setChecked( m_includeFuzzyCounts ); connect( action, SIGNAL(toggled(bool)), this, SLOT(setIncludeFuzzyCounts(bool)) ); action = new QAction( i18n("Show Resolution Indicator"), this ); action->setCheckable( true ); m_contextMenu->addAction(action); action->setChecked( m_showResolutionIndicator ); connect( action, SIGNAL(toggled(bool)), this, SLOT(setShowResolutionIndicator(bool)) ); } m_contextMenu->exec( event->globalPos()); event->setAccepted(true); } QRect DateBar::DateBarWidget::tickMarkGeometry() const { QRect rect; rect.setTopLeft( barAreaGeometry().bottomLeft() ); rect.setWidth( barAreaGeometry().width() ); rect.setHeight( 12 ); return rect; } void DateBar::DateBarWidget::drawResolutionIndicator( QPainter& p, int* leftEdge ) { QRect rect = dateAreaGeometry(); // For real small bars, we do not want to show the resolution. if ( rect.width() < 400 || !m_showResolutionIndicator ) { *leftEdge = rect.right(); return; } QString text = m_currentHandler->unitText(); int textWidth = QFontMetrics( font() ).width( text ); int height = QFontMetrics( font() ).height(); int endUnitPos = rect.right() - textWidth - arrowLength - 3; // Round to nearest unit mark endUnitPos = ( (endUnitPos-rect.left()) / m_barWidth) * m_barWidth + rect.left(); int startUnitPos = endUnitPos - m_barWidth; int midLine = rect.top() + height / 2; p.save(); p.setPen( Qt::red ); // draw arrows drawArrow( p, QPoint( startUnitPos - arrowLength, midLine ), QPoint( startUnitPos, midLine ) ); drawArrow( p, QPoint( endUnitPos + arrowLength, midLine ), QPoint( endUnitPos, midLine ) ); p.drawLine( startUnitPos, rect.top(), startUnitPos, rect.top()+height ); p.drawLine( endUnitPos, rect.top(), endUnitPos, rect.top()+height ); // draw text QFontMetrics fm( font() ); p.drawText( endUnitPos + arrowLength + 3, rect.top(), fm.width(text), fm.height(), Qt::TextSingleLine, text ); p.restore(); *leftEdge = startUnitPos - arrowLength - 3; } QRect DateBar::DateBarWidget::dateAreaGeometry() const { QRect rect = tickMarkGeometry(); rect.setTop( rect.bottom() + 2 ); rect.setHeight( QFontMetrics( font() ).height() ); return rect; } void DateBar::DateBarWidget::drawArrow( QPainter& p, const QPoint& start, const QPoint& end ) { p.save(); p.drawLine( start, end ); QPoint diff = QPoint( end.x() - start.x(), end.y() - start.y() ); double dx = diff.x(); double dy = diff.y(); if ( dx != 0 || dy != 0 ) { if( dy < 0 ) dx = -dx; double angle = acos(dx/sqrt( dx*dx+dy*dy ))*180./M_PI; if( dy < 0 ) angle += 180.; // angle is now the angle of the line. angle = angle + 180 - 15; p.translate( end.x(), end.y() ); p.rotate( angle ); p.drawLine( QPoint(0,0), QPoint( 10,0 ) ); p.rotate( 30 ); p.drawLine( QPoint(0,0), QPoint( 10,0 ) ); } p.restore(); } void DateBar::DateBarWidget::setShowResolutionIndicator( bool b ) { m_showResolutionIndicator = b; redraw(); } void DateBar::DateBarWidget::setAutomaticRangeAdjustment( bool b ) { m_doAutomaticRangeAdjustment = b; } void DateBar::DateBarWidget::updateArrowState() { m_leftArrow->setEnabled( m_dates->lowerLimit() <= dateForUnit( 0 ) ); m_rightArrow->setEnabled( m_dates->upperLimit() > dateForUnit( numberOfUnits() ) ); } DB::ImageDate DateBar::DateBarWidget::currentDateRange() const { return DB::ImageDate( dateForUnit( m_currentUnit ), dateForUnit( m_currentUnit+1 ) ); } void DateBar::DateBarWidget::showStatusBarTip( const QPoint& pos ) { DB::ImageDate range = rangeAt( pos ); DB::ImageCount count = m_dates->count( range ); QString cnt; if ( count.mp_rangeMatch != 0 && includeFuzzyCounts()) cnt = i18ncp("@info:status images that fall in the given date range" ,"1 exact", "%1 exact", count.mp_exact) + i18ncp("@info:status additional images captured in a date range that overlaps with the given date range," ," + 1 range", " + %1 ranges", count.mp_rangeMatch) + i18ncp("@info:status total image count"," = 1 total", " = %1 total", count.mp_exact + count.mp_rangeMatch ); else cnt = i18ncp("@info:status image count","%1 image/video","%1 images/videos", count.mp_exact ); QString res = i18nc("@info:status Time range vs. image count (e.g. 'Jun 2012 | 4 images/videos').","%1 | %2", range.toString(), cnt); static QString lastTip; if ( lastTip != res ) emit toolTipInfo( res ); lastTip = res; } void DateBar::DateBarWidget::placeAndSizeButtons() { m_zoomIn->setFixedSize( buttonWidth, buttonWidth ); m_zoomOut->setFixedSize( buttonWidth, buttonWidth ); m_rightArrow->setFixedSize( QSize( buttonWidth, m_barHeight ) ); m_leftArrow->setFixedSize( QSize( buttonWidth, m_barHeight ) ); m_rightArrow->move( size().width() - m_rightArrow->width() - borderArroundWidget, borderAboveHistogram ); m_leftArrow->move( m_rightArrow->pos().x() - m_leftArrow->width() -2 , borderAboveHistogram ); int x = m_leftArrow->pos().x(); int y = height() - buttonWidth; m_zoomOut->move( x, y ); x = m_rightArrow->pos().x(); m_zoomIn->move(x, y ); m_cancelSelection->setFixedSize( buttonWidth, buttonWidth ); m_cancelSelection->move( 0, y ); } void DateBar::DateBarWidget::keyPressEvent( QKeyEvent* event ) { int offset = 0; if ( event->key() == Qt::Key_Plus ) { if ( m_tp != HourView ) zoom(1); return; } if ( event->key() == Qt::Key_Minus ) { if ( m_tp != DecadeView ) zoom( -1 ); return; } if ( event->key() == Qt::Key_Left ) offset = -1; else if ( event->key() == Qt::Key_Right ) offset = 1; else if ( event->key() == Qt::Key_PageDown ) offset = -10; else if ( event->key() == Qt::Key_PageUp ) offset = 10; else return; QDateTime newDate =dateForUnit( offset, m_currentDate ); if ( (offset < 0 && newDate >= m_dates->lowerLimit()) || ( offset > 0 && newDate <= m_dates->upperLimit() ) ) { m_currentDate = newDate; m_currentUnit += offset; if ( m_currentUnit < 0 ) m_currentUnit = 0; if ( m_currentUnit > numberOfUnits() ) m_currentUnit = numberOfUnits(); if ( ! currentSelection().includes( m_currentDate ) ) clearSelection(); } redraw(); emit dateSelected( currentDateRange(), includeFuzzyCounts() ); } void DateBar::DateBarWidget::focusInEvent( QFocusEvent* ) { redraw(); } void DateBar::DateBarWidget::focusOutEvent( QFocusEvent* ) { redraw(); } int DateBar::DateBarWidget::unitAtPos( int x ) const { return ( x - barAreaGeometry().left() )/m_barWidth; } QDateTime DateBar::DateBarWidget::dateForUnit( int unit, const QDateTime& offset ) const { return m_currentHandler->date( unit, offset ); } bool DateBar::DateBarWidget::isUnitSelected( int unit ) const { QDateTime minDate = m_selectionHandler->min(); QDateTime maxDate = m_selectionHandler->max(); QDateTime date = dateForUnit( unit ); return ( minDate <= date && date < maxDate && !minDate.isNull() ); } bool DateBar::DateBarWidget::hasSelection() const { return !m_selectionHandler->min().isNull(); } DB::ImageDate DateBar::DateBarWidget::currentSelection() const { return DB::ImageDate(m_selectionHandler->min(), m_selectionHandler->max() ); } void DateBar::DateBarWidget::clearSelection() { if ( m_selectionHandler->hasSelection() ) { m_selectionHandler->clearSelection(); emit dateRangeCleared(); redraw(); } m_cancelSelection->setEnabled( false ); } void DateBar::DateBarWidget::emitRangeSelection( const DB::ImageDate& range ) { emit dateRangeChange( range ); } int DateBar::DateBarWidget::unitForDate( const QDateTime& date ) const { for ( int unit = 0; unit < numberOfUnits(); ++unit ) { if ( m_currentHandler->date( unit ) <= date && date < m_currentHandler->date( unit +1 ) ) return unit; } return -1; } void DateBar::DateBarWidget::emitDateSelected() { emit dateSelected( currentDateRange(), includeFuzzyCounts() ); } void DateBar::DateBarWidget::wheelEvent( QWheelEvent * e ) { if ( e->modifiers() & Qt::ControlModifier ) { if ( e->delta() > 0 ) zoomIn(); else zoomOut(); return; } if ( e->delta() > 0 ) scroll(1); else scroll(-1); } -#include "DateBarWidget.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DateBar/MouseHandler.cpp b/DateBar/MouseHandler.cpp index c1f531db..b8bf2b85 100644 --- a/DateBar/MouseHandler.cpp +++ b/DateBar/MouseHandler.cpp @@ -1,227 +1,226 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + #include "MouseHandler.h" #include #include "DateBarWidget.h" #include #include #include /** * \class DateBar::MouseHandler * \brief Base class for handling mouse events in the \ref DateBar * * The mouse events in the date bar are handled by subclasses of MouseHandler. * The subclasses are: * \li DateBar::BarDragHandler - used during dragging the bar (control left mouse button) * \li DateBar::FocusItemDragHandler - used during dragging the focus item (drag the top of the bar) * \li DateBar::SelectionHandler - used during range selection (drag on the bottom of the bar) */ /** * \class DateBar::BarDragHandler * \brief Mouse handler used when dragging the date bar (using control left mouse button on the bar) */ /** * \class DateBar::FocusItemDragHandler * \brief Handler used during dragging of the focus rectangle in the date bar (mouse button on upper part of the bar) */ /** * \class DateBar::SelectionHandler * \brief Handler used during range selection in the date bar (mouse button on lower part of the bar) */ DateBar::MouseHandler::MouseHandler( DateBarWidget* dateBar ) :QObject( dateBar ), m_dateBar( dateBar ) { m_autoScrollTimer = new QTimer( this ); connect( m_autoScrollTimer, SIGNAL(timeout()), this, SLOT(autoScroll()) ); } void DateBar::MouseHandler::autoScroll() { mouseMoveEvent( m_dateBar->mapFromGlobal( QCursor::pos() ).x() ); } void DateBar::MouseHandler::startAutoScroll() { m_autoScrollTimer->start( 100 ); } void DateBar::MouseHandler::endAutoScroll() { m_autoScrollTimer->stop(); } DateBar::SelectionHandler::SelectionHandler( DateBarWidget* dateBar ) :MouseHandler( dateBar ) { } void DateBar::SelectionHandler::mousePressEvent( int x ) { int unit = m_dateBar->unitAtPos( x ); m_start = m_dateBar->dateForUnit( unit ); m_end = m_dateBar->dateForUnit( unit + 1 ); } void DateBar::SelectionHandler::mouseMoveEvent( int x ) { int unit = m_dateBar->unitAtPos( x ); QDateTime date = m_dateBar->dateForUnit( unit ); if ( m_start < date ) m_end = m_dateBar->dateForUnit( unit + 1 ); else m_end = date; m_dateBar->redraw(); } DateBar::FocusItemDragHandler::FocusItemDragHandler( DateBarWidget* dateBar ) : MouseHandler( dateBar ) { } void DateBar::FocusItemDragHandler::mousePressEvent( int x ) { m_dateBar->m_currentUnit = m_dateBar->unitAtPos( x ); m_dateBar->m_currentDate = m_dateBar->dateForUnit( m_dateBar->m_currentUnit ); if ( m_dateBar->hasSelection() && ! m_dateBar->currentSelection().includes( m_dateBar->m_currentDate ) ) m_dateBar->clearSelection(); } void DateBar::FocusItemDragHandler::mouseMoveEvent( int x ) { int oldUnit = m_dateBar->m_currentUnit; int newUnit = ( x - m_dateBar->barAreaGeometry().left() )/m_dateBar->m_barWidth; // Don't scroll further down than the last image // We use oldUnit here, to ensure that we scroll all the way to the end // better scroll a bit over than not all the way. if ( (newUnit > oldUnit && m_dateBar->dateForUnit( oldUnit ) > m_dateBar->m_dates->upperLimit() ) || ( newUnit < oldUnit && m_dateBar->dateForUnit( oldUnit ) < m_dateBar->m_dates->lowerLimit() ) ) return; m_dateBar->m_currentUnit = newUnit; static double rest = 0; if ( m_dateBar->m_currentUnit < 0 || m_dateBar->m_currentUnit > m_dateBar->numberOfUnits() ) { // Slow down scrolling outside date bar. double newUnit = oldUnit + ( m_dateBar->m_currentUnit - oldUnit ) / 4.0 + rest; m_dateBar->m_currentUnit = (int) floor( newUnit ); rest = newUnit - m_dateBar->m_currentUnit; startAutoScroll(); } m_dateBar->m_currentDate = m_dateBar->dateForUnit( m_dateBar->m_currentUnit ); m_dateBar->m_currentUnit = qMax( m_dateBar->m_currentUnit, 0 ); m_dateBar->m_currentUnit = qMin( m_dateBar->m_currentUnit, m_dateBar->numberOfUnits() ); m_dateBar->redraw(); m_dateBar->emitDateSelected(); } DateBar::BarDragHandler::BarDragHandler( DateBarWidget* dateBar ) : MouseHandler( dateBar ) { } void DateBar::BarDragHandler::mousePressEvent( int x ) { m_movementOffset = m_dateBar->m_currentUnit * m_dateBar->m_barWidth - ( x - m_dateBar->barAreaGeometry().left() ); } void DateBar::BarDragHandler::mouseMoveEvent( int x ) { int oldUnit = m_dateBar->m_currentUnit; int newUnit = ( x + m_movementOffset - m_dateBar->barAreaGeometry().left() )/m_dateBar->m_barWidth; // Don't scroll further down than the last image // We use oldUnit here, to ensure that we scroll all the way to the end // better scroll a bit over than not all the way. if ( (newUnit > oldUnit && m_dateBar->dateForUnit( 0 ) < m_dateBar->m_dates->lowerLimit() ) || ( newUnit < oldUnit && m_dateBar->dateForUnit( m_dateBar->numberOfUnits() ) > m_dateBar->m_dates->upperLimit() ) ) return; m_dateBar->m_currentUnit = newUnit; if ( m_dateBar->m_currentUnit < 0 ) { m_dateBar->m_currentDate = m_dateBar->dateForUnit( - m_dateBar->m_currentUnit ); m_dateBar->m_currentUnit = 0; m_movementOffset = m_dateBar->barAreaGeometry().left() - x; } else if ( m_dateBar->m_currentUnit > m_dateBar->numberOfUnits() ) { int diff = m_dateBar->numberOfUnits() - m_dateBar->m_currentUnit; m_dateBar->m_currentDate = m_dateBar->dateForUnit( m_dateBar->numberOfUnits()+diff ); m_dateBar->m_currentUnit = m_dateBar->numberOfUnits(); m_movementOffset = (m_dateBar->numberOfUnits()*m_dateBar->m_barWidth ) - x + m_dateBar->m_barWidth/ 2 ; } m_dateBar->redraw(); m_dateBar->emitDateSelected(); } QDateTime DateBar::SelectionHandler::min() const { if ( m_start < m_end ) return m_start; else return m_end; } QDateTime DateBar::SelectionHandler::max() const { if ( m_start >= m_end ) return m_dateBar->dateForUnit( 1,m_start ); else return m_end; } void DateBar::SelectionHandler::clearSelection() { m_start = QDateTime(); m_end = QDateTime(); } void DateBar::SelectionHandler::mouseReleaseEvent() { m_dateBar->emitRangeSelection( dateRange() ); } DB::ImageDate DateBar::SelectionHandler::dateRange() const { return DB::ImageDate( min(), max() ); } bool DateBar::SelectionHandler::hasSelection() const { return min().isValid(); } - -#include "MouseHandler.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/InfoDialog.cpp b/Exif/InfoDialog.cpp index e6ee039f..5ac6c06f 100644 --- a/Exif/InfoDialog.cpp +++ b/Exif/InfoDialog.cpp @@ -1,126 +1,125 @@ -/* Copyright (C) 2003-2016 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include "DB/ImageDB.h" #include "Exif/InfoDialog.h" #include "Exif/Info.h" #include "ImageManager/AsyncLoader.h" #include "ImageManager/ImageRequest.h" #include "Settings/SettingsData.h" #include "Grid.h" using Utilities::StringSet; Exif::InfoDialog::InfoDialog(const DB::FileName& fileName, QWidget* parent) : QDialog(parent) { setWindowTitle( i18n("EXIF Information") ); setAttribute(Qt::WA_DeleteOnClose); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); buttonBox->button(QDialogButtonBox::Close)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QWidget* top = new QWidget(this); QVBoxLayout* vlay = new QVBoxLayout( top ); setLayout(vlay); vlay->addWidget(top); // -------------------------------------------------- File name and pixmap QHBoxLayout* hlay = new QHBoxLayout; vlay->addLayout(hlay); m_fileNameLabel = new QLabel( top ); QFont fnt = font(); fnt.setPointSize( (int) (fnt.pointSize() * 1.2) ); fnt.setWeight( QFont::Bold ); m_fileNameLabel->setFont( fnt ); m_fileNameLabel->setAlignment( Qt::AlignCenter ); hlay->addWidget( m_fileNameLabel, 1 ); m_pix = new QLabel( top ); hlay->addWidget( m_pix ); // -------------------------------------------------- Exif Grid m_grid = new Exif::Grid( top ); vlay->addWidget( m_grid ); // -------------------------------------------------- Current Search hlay = new QHBoxLayout; vlay->addLayout(hlay); QLabel* searchLabel = new QLabel( i18n( "EXIF Label Search: "), top ); hlay->addWidget( searchLabel ); m_searchBox = new QLineEdit( top ); hlay->addWidget( m_searchBox ); hlay->addStretch( 1 ); QLabel* iptcLabel = new QLabel( i18n("IPTC character set:"), top ); m_iptcCharset = new QComboBox( top ); QStringList charsets; QList charsetsBA = QTextCodec::availableCodecs(); for (QList::const_iterator it = charsetsBA.constBegin(); it != charsetsBA.constEnd(); ++it ) charsets << QLatin1String(*it); m_iptcCharset->insertItems( 0, charsets ); m_iptcCharset->setCurrentIndex( qMax( 0, QTextCodec::availableCodecs().indexOf( Settings::SettingsData::instance()->iptcCharset().toLatin1() ) ) ); hlay->addWidget( iptcLabel ); hlay->addWidget( m_iptcCharset ); connect( m_searchBox, SIGNAL(textChanged(QString)), m_grid, SLOT(updateSearchString(QString)) ); connect( m_iptcCharset, SIGNAL(activated(QString)), m_grid, SLOT(setupUI(QString)) ); setImage(fileName); vlay->addWidget(buttonBox); } QSize Exif::InfoDialog::sizeHint() const { return QSize( 800, 400 ); } void Exif::InfoDialog::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& image) { if ( request->loadedOK() ) m_pix->setPixmap( QPixmap::fromImage(image) ); } void Exif::InfoDialog::setImage(const DB::FileName& fileName ) { m_fileNameLabel->setText( fileName.relative() ); m_grid->setFileName( fileName ); ImageManager::ImageRequest* request = new ImageManager::ImageRequest( fileName, QSize( 128, 128 ), fileName.info()->angle(), this ); request->setPriority( ImageManager::Viewer ); ImageManager::AsyncLoader::instance()->load( request ); } void Exif::InfoDialog::enterEvent(QEvent *) { m_grid->setFocus(); } -#include "InfoDialog.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/RangeWidget.cpp b/Exif/RangeWidget.cpp index a13d8b15..8de5e669 100644 --- a/Exif/RangeWidget.cpp +++ b/Exif/RangeWidget.cpp @@ -1,99 +1,99 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen +/* Copyright (C) 2003-2018 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + #include "RangeWidget.h" #include #include #include Exif::RangeWidget::RangeWidget( const QString& text, const QString& searchTag, const ValueList& list, QGridLayout* layout, int row ) : QObject( layout->widget() ),m_searchTag ( searchTag ), m_list( list ) { int col = 0; // widget layout: <from_value> "to" <to_value> // register title text: QLabel* label = new QLabel( text ); layout->addWidget( label, row, col++); // register from-field: m_from = new KComboBox; layout->addWidget(m_from, row, col++); // register filler between from- and to-field: label = new QLabel( QString::fromLatin1( "to" ) ); layout->addWidget(label, row, col++); // register to-field: m_to = new KComboBox; layout->addWidget(m_to, row, col++); Q_ASSERT( list.count() > 2 ); ValueList::ConstIterator it = list.begin(); m_from->addItem( QString::fromLatin1( "< %1" ).arg( (*it).text ) ); for( ; it != list.end(); ++it ) { m_from->addItem( (*it).text ); } m_from->addItem( QString::fromLatin1( "> %1" ).arg( list.last().text ) ); slotUpdateTo( 0 ); m_to->setCurrentIndex( m_to->count()-1);// set range to be min->max connect( m_from, SIGNAL(activated(int)), this, SLOT(slotUpdateTo(int)) ); } void Exif::RangeWidget::slotUpdateTo( int fromIndex ) { m_to->clear(); if ( fromIndex == 0 ) m_to->addItem( QString::fromLatin1( "< %1" ).arg( m_list.first().text ) ); else fromIndex--; for ( int i = fromIndex; i < m_list.count(); ++i ) { m_to->addItem( m_list[i].text ); } m_to->addItem( QString::fromLatin1( "> %1" ).arg( m_list.last().text ) ); } Exif::SearchInfo::Range Exif::RangeWidget::range() const { SearchInfo::Range result( m_searchTag ); result.min = m_list.first().value; result.max = m_list.last().value; if ( m_from->currentIndex() == 0 ) result.isLowerMin = true; else if ( m_from->currentIndex() == m_from->count() -1 ) result.isLowerMax = true; else result.min = m_list[m_from->currentIndex()-1].value; if ( m_to->currentIndex() == 0 && m_from->currentIndex() == 0 ) result.isUpperMin = true; else if ( m_to->currentIndex() == m_to->count() -1 ) result.isUpperMax = true; else result.max = m_list[m_to->currentIndex() + m_from->currentIndex()-1].value; return result; } -#include "RangeWidget.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/ReReadDialog.cpp b/Exif/ReReadDialog.cpp index 67d402fe..532da2c8 100644 --- a/Exif/ReReadDialog.cpp +++ b/Exif/ReReadDialog.cpp @@ -1,147 +1,146 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ReReadDialog.h" #include <QCheckBox> #include <QGroupBox> #include <QLabel> #include <QListWidget> #include <QVBoxLayout> #include <QDialogButtonBox> #include <QPushButton> #include <KConfigGroup> #include <KLocalizedString> #include <KMessageBox> #include <KSharedConfig> #include <DB/ImageDB.h> #include <Exif/Database.h> #include <Settings/SettingsData.h> Exif::ReReadDialog::ReReadDialog(QWidget* parent) : QDialog(parent) { setWindowTitle( i18n("Read EXIF info from files") ); QWidget* top = new QWidget; QVBoxLayout* lay1 = new QVBoxLayout( top ); setLayout(lay1); lay1->addWidget(top); m_exifDB = new QCheckBox( i18n( "Update EXIF search database" ), top ); lay1->addWidget( m_exifDB ); if ( !Exif::Database::instance()->isUsable() ) { m_exifDB->hide(); } m_date = new QCheckBox( i18n( "Update image date" ), top ); lay1->addWidget( m_date ); m_force_date = new QCheckBox( i18n( "Use modification date if EXIF not found" ), top ); lay1->addWidget( m_force_date ); m_orientation = new QCheckBox( i18n( "Update image orientation from EXIF information" ), top ); lay1->addWidget( m_orientation ); m_description = new QCheckBox( i18n( "Update image description from EXIF information" ), top ); lay1->addWidget( m_description ); QGroupBox* box = new QGroupBox( i18n("Affected Files") ); lay1->addWidget( box ); QHBoxLayout* boxLayout = new QHBoxLayout( box ); m_fileList = new QListWidget; m_fileList->setSelectionMode( QAbstractItemView::NoSelection ); boxLayout->addWidget( m_fileList ); connect( m_date, SIGNAL(toggled(bool)), m_force_date, SLOT(setEnabled(bool)) ); connect( m_date, SIGNAL(toggled(bool)), this, SLOT(warnAboutDates(bool)) ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(buttonBox, &QDialogButtonBox::accepted, this, &ReReadDialog::readInfo); lay1->addWidget(buttonBox); } int Exif::ReReadDialog::exec( const DB::FileNameList& list ) { Settings::SettingsData *opt = Settings::SettingsData::instance(); m_exifDB->setChecked( opt->updateExifData() ); m_date->setChecked( opt->updateImageDate() ); m_force_date->setChecked( opt->useModDateIfNoExif() ); m_force_date->setEnabled( opt->updateImageDate() ); m_orientation->setChecked( opt->updateOrientation() ); m_description->setChecked( opt->updateDescription() ); m_list = list; m_fileList->clear(); m_fileList->addItems( list.toStringList(DB::RelativeToImageRoot) ); return QDialog::exec(); } void Exif::ReReadDialog::readInfo() { Settings::SettingsData *opt = Settings::SettingsData::instance(); opt->setUpdateExifData( m_exifDB->isChecked() ); opt->setUpdateImageDate( m_date->isChecked() ); opt->setUseModDateIfNoExif( m_force_date->isChecked() ); opt->setUpdateOrientation( m_orientation->isChecked() ); opt->setUpdateDescription( m_description->isChecked() ); KSharedConfig::openConfig()->sync(); DB::ExifMode mode = DB::EXIFMODE_FORCE; if ( m_exifDB->isChecked() ) mode |= DB::EXIFMODE_DATABASE_UPDATE; if ( m_date->isChecked() ) mode |= DB::EXIFMODE_DATE; if ( m_force_date->isChecked() ) mode |= DB::EXIFMODE_USE_IMAGE_DATE_IF_INVALID_EXIF_DATE; if ( m_orientation->isChecked() ) mode |= DB::EXIFMODE_ORIENTATION; if ( m_description->isChecked() ) mode |= DB::EXIFMODE_DESCRIPTION; accept(); DB::ImageDB::instance()->slotReread(m_list, mode); } void Exif::ReReadDialog::warnAboutDates( bool b ) { if ( !b ) return; int ret = KMessageBox::warningContinueCancel( this, i18n("<p>Be aware that setting the data from EXIF may " "<b>overwrite</b> data you have previously entered " "manually using the image configuration dialog.</p>" ), i18n( "Override image dates" ) ); if ( ret == KMessageBox::Cancel ) m_date->setChecked( false ); } -#include "ReReadDialog.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/SearchDialog.cpp b/Exif/SearchDialog.cpp index 56c0b8ab..610d53e4 100644 --- a/Exif/SearchDialog.cpp +++ b/Exif/SearchDialog.cpp @@ -1,433 +1,432 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + #include "SearchDialog.h" #include <KLocalizedString> #include <qlayout.h> #include <QGroupBox> #include <qcheckbox.h> #include <QHBoxLayout> #include <QVBoxLayout> #include "Exif/Database.h" #include <QGridLayout> #include <qlabel.h> #include <qspinbox.h> #include <QScrollArea> #include <KConfigGroup> #include <QDialogButtonBox> #include <QPushButton> using namespace Exif; Exif::SearchDialog::SearchDialog( QWidget* parent ) : KPageDialog( parent ) { setWindowTitle( i18n("EXIF Search") ); setFaceType( Tabbed ); QWidget* settings = new QWidget; KPageWidgetItem* page = new KPageWidgetItem( settings, i18n("Settings" ) ); addPage( page ); QVBoxLayout* vlay = new QVBoxLayout( settings ); // Iso, Exposure, Aperture, FNumber QHBoxLayout* hlay = new QHBoxLayout; vlay->addLayout( hlay ); QGridLayout* gridLayout = new QGridLayout; gridLayout->setSpacing( 6 ); hlay->addLayout( gridLayout ); hlay->addStretch( 1 ); makeISO( gridLayout ); makeExposureTime( gridLayout ); hlay->addSpacing(30); gridLayout = new QGridLayout; gridLayout->setSpacing( 6 ); hlay->addLayout( gridLayout ); hlay->addStretch( 1 ); m_apertureValue = makeApertureOrFNumber( i18n( "Aperture Value" ), QString::fromLatin1( "Exif_Photo_ApertureValue" ), gridLayout, 0 ); m_fNumber = makeApertureOrFNumber( i18n( "F Number" ), QString::fromLatin1( "Exif_Photo_FNumber" ), gridLayout, 1 ); hlay->addSpacing(30); // Focal length QHBoxLayout* focalLayout = new QHBoxLayout; focalLayout->setSpacing( 6 ); hlay->addLayout( focalLayout ); hlay->addStretch( 1 ); QLabel* label = new QLabel( i18n( "Focal Length" ) ); focalLayout->addWidget(label); m_fromFocalLength = new QSpinBox; focalLayout->addWidget(m_fromFocalLength); m_fromFocalLength->setRange( 0, 10000 ); m_fromFocalLength->setSingleStep( 10 ); label = new QLabel( i18nc("As in 'A range from x to y'","to")); focalLayout->addWidget(label); m_toFocalLength = new QSpinBox; focalLayout->addWidget(m_toFocalLength); m_toFocalLength->setRange( 0, 10000 ); m_toFocalLength->setSingleStep( 10 ); m_toFocalLength->setValue( 10000 ); QString suffix = i18nc( "This is millimeter for focal length, like 35mm", "mm" ); m_fromFocalLength->setSuffix( suffix ); m_toFocalLength->setSuffix( suffix ); connect( m_fromFocalLength, SIGNAL(valueChanged(int)), this, SLOT(fromFocalLengthChanged(int)) ); connect( m_toFocalLength, SIGNAL(valueChanged(int)), this, SLOT(toFocalLengthChanged(int)) ); // exposure program and Metring mode hlay = new QHBoxLayout; vlay->addLayout( hlay ); hlay->addWidget( makeExposureProgram( settings ) ); hlay->addWidget( makeMeteringMode( settings ) ); vlay->addStretch( 1 ); // ------------------------------------------------------------ Camera page = new KPageWidgetItem( makeCamera(), i18n("Camera") ); addPage( page ); // ------------------------------------------------------------ Lens page = new KPageWidgetItem( makeLens(), i18n("Lens") ); addPage( page ); // ------------------------------------------------------------ Misc QWidget* misc = new QWidget; addPage( new KPageWidgetItem( misc, i18n("Miscellaneous") ) ); vlay = new QVBoxLayout( misc ); vlay->addWidget( makeOrientation( misc ), 1 ); hlay = new QHBoxLayout; vlay->addLayout( hlay ); hlay->addWidget( makeContrast( misc ) ); hlay->addWidget( makeSharpness( misc ) ); hlay->addWidget( makeSaturation( misc ) ); vlay->addStretch( 1 ); } void Exif::SearchDialog::makeISO( QGridLayout* layout ) { Exif::RangeWidget::ValueList list; list << Exif::RangeWidget::Value( 100, QString::fromLatin1("100") ) << Exif::RangeWidget::Value( 200, QString::fromLatin1("200") ) << Exif::RangeWidget::Value( 400, QString::fromLatin1("400") ) << Exif::RangeWidget::Value( 800, QString::fromLatin1("800") ) << Exif::RangeWidget::Value( 1600, QString::fromLatin1("1600") ) << Exif::RangeWidget::Value( 3200, QString::fromLatin1("3200") ) << Exif::RangeWidget::Value( 6400, QString::fromLatin1("6400") ) << Exif::RangeWidget::Value( 12800, QString::fromLatin1("12800") ) << Exif::RangeWidget::Value( 25600, QString::fromLatin1("25600") ) << Exif::RangeWidget::Value( 51200, QString::fromLatin1("51200") ); m_iso = new RangeWidget( i18n("Iso setting" ), QString::fromLatin1( "Exif_Photo_ISOSpeedRatings" ), list, layout, 0 ); } void Exif::SearchDialog::makeExposureTime( QGridLayout* layout ) { QString secs = i18nc( "Example 1.6 secs (as in seconds)", "secs." ); Exif::RangeWidget::ValueList list; list << Exif::RangeWidget::Value( 1.0/4000, QString::fromLatin1( "1/4000" ) ) << Exif::RangeWidget::Value( 1.0/3200, QString::fromLatin1( "1/3200" ) ) << Exif::RangeWidget::Value( 1.0/2500, QString::fromLatin1( "1/2500" ) ) << Exif::RangeWidget::Value( 1.0/2000, QString::fromLatin1( "1/2000" ) ) << Exif::RangeWidget::Value( 1.0/1600, QString::fromLatin1( "1/1600" ) ) << Exif::RangeWidget::Value( 1.0/1250, QString::fromLatin1( "1/1250" ) ) << Exif::RangeWidget::Value( 1.0/1000, QString::fromLatin1( "1/1000" ) ) << Exif::RangeWidget::Value( 1.0/800, QString::fromLatin1( "1/800" ) ) << Exif::RangeWidget::Value( 1.0/640, QString::fromLatin1( "1/640" ) ) << Exif::RangeWidget::Value( 1.0/500, QString::fromLatin1( "1/500" ) ) << Exif::RangeWidget::Value( 1.0/400, QString::fromLatin1( "1/400" ) ) << Exif::RangeWidget::Value( 1.0/320, QString::fromLatin1( "1/320" ) ) << Exif::RangeWidget::Value( 1.0/250, QString::fromLatin1( "1/250" ) ) << Exif::RangeWidget::Value( 1.0/200, QString::fromLatin1( "1/200" ) ) << Exif::RangeWidget::Value( 1.0/160, QString::fromLatin1( "1/160" ) ) << Exif::RangeWidget::Value( 1.0/125, QString::fromLatin1( "1/125" ) ) << Exif::RangeWidget::Value( 1.0/100, QString::fromLatin1( "1/100" ) ) << Exif::RangeWidget::Value( 1.0/80, QString::fromLatin1( "1/80" ) ) << Exif::RangeWidget::Value( 1.0/60, QString::fromLatin1( "1/60" ) ) << Exif::RangeWidget::Value( 1.0/50, QString::fromLatin1( "1/50" ) ) << Exif::RangeWidget::Value( 1.0/40, QString::fromLatin1( "1/40" ) ) << Exif::RangeWidget::Value( 1.0/30, QString::fromLatin1( "1/30" ) ) << Exif::RangeWidget::Value( 1.0/25, QString::fromLatin1( "1/25" ) ) << Exif::RangeWidget::Value( 1.0/20, QString::fromLatin1( "1/20" ) ) << Exif::RangeWidget::Value( 1.0/15, QString::fromLatin1( "1/15" ) ) << Exif::RangeWidget::Value( 1.0/13, QString::fromLatin1( "1/13" ) ) << Exif::RangeWidget::Value( 1.0/10, QString::fromLatin1( "1/10" ) ) << Exif::RangeWidget::Value( 1.0/8, QString::fromLatin1( "1/8" ) ) << Exif::RangeWidget::Value( 1.0/6, QString::fromLatin1( "1/6" ) ) << Exif::RangeWidget::Value( 1.0/5, QString::fromLatin1( "1/5" ) ) << Exif::RangeWidget::Value( 1.0/4, QString::fromLatin1( "1/4" ) ) << Exif::RangeWidget::Value( 0.3, QString::fromLatin1( "0.3 %1" ).arg( secs ) ) << Exif::RangeWidget::Value( 0.4, QString::fromLatin1( "0.4 %1").arg(secs ) ) << Exif::RangeWidget::Value( 0.5, QString::fromLatin1( "0.5 %1").arg(secs ) ) << Exif::RangeWidget::Value( 0.6, QString::fromLatin1( "0.6 %1").arg(secs ) ) << Exif::RangeWidget::Value( 0.8, QString::fromLatin1( "0.8 %1").arg(secs ) ) << Exif::RangeWidget::Value( 1, i18n( "1 second" ) ) << Exif::RangeWidget::Value( 1.3, QString::fromLatin1( "1.3 %1").arg(secs ) ) << Exif::RangeWidget::Value( 1.6, QString::fromLatin1( "1.6 %1").arg(secs ) ) << Exif::RangeWidget::Value( 2, QString::fromLatin1( "2 %1").arg(secs ) ) << Exif::RangeWidget::Value( 2.5, QString::fromLatin1( "2.5 %1").arg(secs ) ) << Exif::RangeWidget::Value( 3.2, QString::fromLatin1( "3.2 %1").arg(secs ) ) << Exif::RangeWidget::Value( 4, QString::fromLatin1( "4 %1").arg(secs ) ) << Exif::RangeWidget::Value( 5, QString::fromLatin1( "5 %1").arg(secs ) ) << Exif::RangeWidget::Value( 6, QString::fromLatin1( "6 %1").arg(secs ) ) << Exif::RangeWidget::Value( 8, QString::fromLatin1( "8 %1").arg(secs ) ) << Exif::RangeWidget::Value( 10, QString::fromLatin1( "10 %1").arg(secs ) ) << Exif::RangeWidget::Value( 13, QString::fromLatin1( "13 %1").arg(secs ) ) << Exif::RangeWidget::Value( 15, QString::fromLatin1( "15 %1").arg(secs ) ) << Exif::RangeWidget::Value( 20, QString::fromLatin1( "20 %1").arg(secs ) ) << Exif::RangeWidget::Value( 25, QString::fromLatin1( "25 %1").arg(secs ) ) << Exif::RangeWidget::Value( 30, QString::fromLatin1( "30 %1").arg(secs ) ); m_exposureTime = new RangeWidget( i18n("Exposure time" ), QString::fromLatin1( "Exif_Photo_ExposureTime" ), list, layout, 1 ); } RangeWidget* Exif::SearchDialog::makeApertureOrFNumber( const QString& text, const QString& key, QGridLayout* layout, int row ) { Exif::RangeWidget::ValueList list; list << Exif::RangeWidget::Value( 1.4, QString::fromLatin1( "1.4" ) ) << Exif::RangeWidget::Value( 1.8, QString::fromLatin1( "1.8" ) ) << Exif::RangeWidget::Value( 2.0, QString::fromLatin1( "2.0" ) ) << Exif::RangeWidget::Value( 2.2, QString::fromLatin1( "2.2" ) ) << Exif::RangeWidget::Value( 2.5, QString::fromLatin1( "2.5" ) ) << Exif::RangeWidget::Value( 2.8, QString::fromLatin1( "2.8" ) ) << Exif::RangeWidget::Value( 3.2, QString::fromLatin1( "3.2" ) ) << Exif::RangeWidget::Value( 3.5, QString::fromLatin1( "3.5" ) ) << Exif::RangeWidget::Value( 4.0, QString::fromLatin1( "4.0" ) ) << Exif::RangeWidget::Value( 4.5, QString::fromLatin1( "4.5" ) ) << Exif::RangeWidget::Value( 5.0, QString::fromLatin1( "5.0" ) ) << Exif::RangeWidget::Value( 5.6, QString::fromLatin1( "5.6" ) ) << Exif::RangeWidget::Value( 6.3, QString::fromLatin1( "6.3" ) ) << Exif::RangeWidget::Value( 7.1, QString::fromLatin1( "7.1" ) ) << Exif::RangeWidget::Value( 8.0, QString::fromLatin1( "8.0" ) ) << Exif::RangeWidget::Value( 9.0, QString::fromLatin1( "9.0" ) ) << Exif::RangeWidget::Value( 10, QString::fromLatin1( "10" ) ) << Exif::RangeWidget::Value( 11, QString::fromLatin1( "11" ) ) << Exif::RangeWidget::Value( 13, QString::fromLatin1( "13" ) ) << Exif::RangeWidget::Value( 14, QString::fromLatin1( "14" ) ) << Exif::RangeWidget::Value( 16, QString::fromLatin1( "16" ) ) << Exif::RangeWidget::Value( 18, QString::fromLatin1( "18" ) ) << Exif::RangeWidget::Value( 20, QString::fromLatin1( "20" ) ) << Exif::RangeWidget::Value( 22, QString::fromLatin1( "22" ) ) << Exif::RangeWidget::Value( 25, QString::fromLatin1( "25" ) ) << Exif::RangeWidget::Value( 29, QString::fromLatin1( "29" ) ) << Exif::RangeWidget::Value( 32, QString::fromLatin1( "32" ) ) << Exif::RangeWidget::Value( 36, QString::fromLatin1( "36" ) ) << Exif::RangeWidget::Value( 40, QString::fromLatin1( "40" ) ) << Exif::RangeWidget::Value( 45, QString::fromLatin1( "45" ) ); return new RangeWidget( text, key, list, layout, row ); } #define addSetting(settings,text,num) \ { \ QCheckBox* cb = new QCheckBox( i18n( text ), box ); \ settings.append( Setting<int>( cb, num ) ); \ layout->addWidget(cb); \ } QWidget* Exif::SearchDialog::makeExposureProgram( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Exposure Program" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_exposureProgram, "Not defined", 0 ); addSetting( m_exposureProgram, "Manual", 1 ); addSetting( m_exposureProgram, "Normal program", 2 ); addSetting( m_exposureProgram, "Aperture priority", 3 ); addSetting( m_exposureProgram, "Shutter priority", 4 ); addSetting( m_exposureProgram, "Creative program (biased toward depth of field)", 5 ); addSetting( m_exposureProgram, "Action program (biased toward fast shutter speed)", 6 ); addSetting( m_exposureProgram, "Portrait mode (for closeup photos with the background out of focus)", 7 ); addSetting( m_exposureProgram, "Landscape mode (for landscape photos with the background in focus)", 8 ); return box; } QWidget* Exif::SearchDialog::makeOrientation( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Orientation" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_orientation, "Not rotated", 0); addSetting( m_orientation, "Rotated counterclockwise", 6 ); addSetting( m_orientation, "Rotated clockwise", 8 ); addSetting( m_orientation, "Rotated 180 degrees", 3 ); return box; } QWidget* Exif::SearchDialog::makeMeteringMode( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Metering Mode" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_meteringMode, "Unknown", 0 ); addSetting( m_meteringMode, "Average", 1 ); addSetting( m_meteringMode, "CenterWeightedAverage", 2 ); addSetting( m_meteringMode, "Spot", 3 ); addSetting( m_meteringMode, "MultiSpot", 4 ); addSetting( m_meteringMode, "Pattern", 5 ); addSetting( m_meteringMode, "Partial", 6 ); addSetting( m_meteringMode, "Other", 255 ); return box; } QWidget* Exif::SearchDialog::makeContrast( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Contrast" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_contrast, "Normal", 0 ); addSetting( m_contrast, "Soft", 1 ); addSetting( m_contrast, "Hard", 2 ); return box; } QWidget* Exif::SearchDialog::makeSharpness( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Sharpness" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_sharpness, "Normal", 0 ); addSetting( m_sharpness, "Soft", 1 ); addSetting( m_sharpness, "Hard", 2 ); return box; } QWidget* Exif::SearchDialog::makeSaturation( QWidget* parent ) { QGroupBox* box = new QGroupBox( i18n( "Saturation" ), parent ); QVBoxLayout* layout = new QVBoxLayout(box); addSetting( m_saturation, "Normal", 0 ); addSetting( m_saturation, "Low", 1 ); addSetting( m_saturation, "High", 2 ); return box; } Exif::SearchInfo Exif::SearchDialog::info() { Exif::SearchInfo result; result.addSearchKey( QString::fromLatin1( "Exif_Photo_MeteringMode" ), m_meteringMode.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Photo_ExposureProgram" ), m_exposureProgram.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Image_Orientation" ), m_orientation.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Photo_MeteringMode" ), m_meteringMode.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Photo_Contrast" ), m_contrast.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Photo_Sharpness" ), m_sharpness.selected() ); result.addSearchKey( QString::fromLatin1( "Exif_Photo_Saturation" ), m_saturation.selected() ); result.addCamera( m_cameras.selected() ); result.addLens( m_lenses.selected() ); result.addRangeKey( m_iso->range() ); result.addRangeKey( m_exposureTime->range() ); result.addRangeKey( m_apertureValue->range() ); result.addRangeKey( m_fNumber->range() ); SearchInfo::Range focalRange( QString::fromLatin1( "Exif_Photo_FocalLength" ) ); focalRange.min = m_fromFocalLength->value(); focalRange.max = m_toFocalLength->value(); result.addRangeKey( focalRange ); return result; } QWidget* Exif::SearchDialog::makeCamera() { QScrollArea* view = new QScrollArea; view->setWidgetResizable(true); QWidget* w = new QWidget; view->setWidget( w ); QVBoxLayout* layout = new QVBoxLayout( w ); QList< QPair<QString, QString> > cameras = Exif::Database::instance()->cameras(); std::sort(cameras.begin(),cameras.end()); for( QList< QPair<QString,QString> >::ConstIterator cameraIt = cameras.constBegin(); cameraIt != cameras.constEnd(); ++cameraIt ) { QCheckBox* cb = new QCheckBox( QString::fromUtf8( "%1 - %2" ).arg( (*cameraIt).first.trimmed() ).arg( (*cameraIt).second.trimmed() ) ); layout->addWidget( cb ); m_cameras.append( Setting< QPair<QString,QString> >( cb, *cameraIt ) ); } if ( cameras.isEmpty() ) { QLabel* label = new QLabel( i18n("No cameras found in the database") ); layout->addWidget( label ); } return view; } QWidget* Exif::SearchDialog::makeLens() { QScrollArea* view = new QScrollArea; view->setWidgetResizable(true); QWidget* w = new QWidget; view->setWidget( w ); QVBoxLayout* layout = new QVBoxLayout( w ); QList< QString > lenses = Exif::Database::instance()->lenses(); std::sort(lenses.begin(),lenses.end()); if ( lenses.isEmpty() ) { QLabel* label = new QLabel( i18n("No lenses found in the database") ); layout->addWidget( label ); } else { // add option "None" first lenses.prepend( i18nc("As in No persons, no locations etc.", "None") ); for( QList< QString >::ConstIterator lensIt = lenses.constBegin(); lensIt != lenses.constEnd(); ++lensIt ) { QCheckBox* cb = new QCheckBox( QString::fromUtf8( "%1" ).arg( (*lensIt).trimmed() ) ); layout->addWidget( cb ); m_lenses.append( Setting< QString >( cb, *lensIt ) ); } } if (Exif::Database::instance()->DBFileVersionGuaranteed() < 3) { QLabel* label = new QLabel( i18n("Not all images in the database have lens information. " "<note>Recreate the EXIF search database to ensure lens data for all images.</note>") ); layout->addWidget(label); } return view; } void Exif::SearchDialog::fromFocalLengthChanged( int val ) { if ( m_toFocalLength->value() < val ) m_toFocalLength->setValue( val ); } void Exif::SearchDialog::toFocalLengthChanged( int val ) { if ( m_fromFocalLength->value() > val ) m_fromFocalLength->setValue( val ); } - -#include "SearchDialog.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/TreeView.cpp b/Exif/TreeView.cpp index 30117f39..6667f30f 100644 --- a/Exif/TreeView.cpp +++ b/Exif/TreeView.cpp @@ -1,102 +1,102 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "Exif/TreeView.h" #include "Utilities/StringSet.h" #include <qmap.h> #include <qstringlist.h> #include "Exif/Info.h" #include <QDebug> using Utilities::StringSet; Exif::TreeView::TreeView( const QString& title, QWidget* parent) :QTreeWidget(parent) { setHeaderLabel( title ); reload(); connect( this, SIGNAL(itemClicked(QTreeWidgetItem*,int)), this, SLOT(toggleChildren(QTreeWidgetItem*)) ); } void Exif::TreeView::toggleChildren( QTreeWidgetItem* parent ) { if ( !parent ) return; bool on = parent->checkState(0) == Qt::Checked; for ( int index = 0; index < parent->childCount(); ++index ) { parent->child(index)->setCheckState(0,on ? Qt::Checked : Qt::Unchecked ); toggleChildren(parent->child(index)); } } StringSet Exif::TreeView::selected() { StringSet result; for ( QTreeWidgetItemIterator it( this ); *it; ++it ) { if ( (*it)->checkState(0) == Qt::Checked ) result.insert( (*it)->text( 1 ) ); } return result; } void Exif::TreeView::setSelectedExif( const StringSet& selected ) { for ( QTreeWidgetItemIterator it( this ); *it; ++it ) { bool on = selected.contains( (*it)->text(1) ); (*it)->setCheckState(0,on ? Qt::Checked : Qt::Unchecked ); } } void Exif::TreeView::reload() { clear(); setRootIsDecorated( true ); QStringList keys = Exif::Info::instance()->availableKeys().toList(); keys.sort(); QMap<QString, QTreeWidgetItem*> tree; for( QStringList::const_iterator keysIt = keys.constBegin(); keysIt != keys.constEnd(); ++keysIt ) { QStringList subKeys = (*keysIt).split(QLatin1String(".")); QTreeWidgetItem* parent = nullptr; QString path; Q_FOREACH( const QString &subKey, subKeys ) { if ( !path.isEmpty() ) path += QString::fromLatin1( "." ); path += subKey; if ( tree.contains( path ) ) parent = tree[path]; else { if ( parent == nullptr ) parent = new QTreeWidgetItem( this, QStringList(subKey) ); else parent = new QTreeWidgetItem( parent, QStringList(subKey) ); parent->setText( 1, path ); // This is simply to make the implementation of selected easier. parent->setFlags( Qt::ItemIsUserCheckable | Qt::ItemIsEnabled ); parent->setCheckState(0,Qt::Unchecked); tree.insert( path, parent ); } } } if ( QTreeWidgetItem* item = topLevelItem(0) ) item->setExpanded( true ); } -#include "TreeView.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/HTMLGenerator/Generator.cpp b/HTMLGenerator/Generator.cpp index 6d7ec45a..635c9e9c 100644 --- a/HTMLGenerator/Generator.cpp +++ b/HTMLGenerator/Generator.cpp @@ -1,790 +1,791 @@ -/* Copyright (C) 2003-2016 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "Generator.h" #include <sys/types.h> #include <sys/wait.h> #include <QFile> #include <QApplication> #include <QList> #include <QDir> #include <QDebug> #include <QDomDocument> #include <QMimeDatabase> #include <QStandardPaths> #include <KConfig> #include <KConfigGroup> #include <KIO/CopyJob> #include <KLocalizedString> #include <KMessageBox> #include <KRun> #include <DB/CategoryCollection.h> #include <DB/ImageDB.h> #include <DB/ImageInfo.h> #include <Exif/Info.h> #include <ImageManager/AsyncLoader.h> #include <ImportExport/Export.h> #include <Utilities/Util.h> #include "ImageSizeCheckBox.h" #include "Setup.h" #include "MainWindow/Window.h" #ifdef DEBUG_HTMLGENERATOR #define Debug qDebug #else #define Debug if(0) qDebug #endif HTMLGenerator::Generator::Generator( const Setup& setup, QWidget* parent ) : QProgressDialog( parent ) , m_tempDirHandle() , m_tempDir(m_tempDirHandle.path()) , m_hasEnteredLoop( false ) { setLabelText( i18n("Generating images for HTML page ") ); m_setup = setup; m_eventLoop = new QEventLoop; m_avconv = QStandardPaths::findExecutable(QString::fromUtf8("avconv")); if ( m_avconv.isNull() ) m_avconv = QStandardPaths::findExecutable(QString::fromUtf8("ffmpeg")); m_tempDirHandle.setAutoRemove(true); } HTMLGenerator::Generator::~Generator() { delete m_eventLoop; } void HTMLGenerator::Generator::generate() { Debug() << "Generating gallery" << m_setup.title() << "containing" << m_setup.imageList().size() << "entries in" << m_setup.baseDir(); // Generate .kim file if ( m_setup.generateKimFile() ) { Debug() << "Generating .kim file..."; bool ok; QString destURL = m_setup.destURL(); ImportExport::Export exp( m_setup.imageList(), kimFileName( false ), false, -1, ImportExport::ManualCopy, destURL + QDir::separator() + m_setup.outputDir(), true, &ok); if ( !ok ) { Debug() << ".kim file failed!"; return; } } // prepare the progress dialog m_total = m_waitCounter = calculateSteps(); setMaximum( m_total ); setValue( 0 ); connect(this, &QProgressDialog::canceled, this, &Generator::slotCancelGenerate); m_filenameMapper.reset(); Debug() << "Generating content pages..."; // Iterate over each of the image sizes needed. for( QList<ImageSizeCheckBox*>::ConstIterator sizeIt = m_setup.activeResolutions().begin(); sizeIt != m_setup.activeResolutions().end(); ++sizeIt ) { bool ok = generateIndexPage( (*sizeIt)->width(), (*sizeIt)->height() ); if ( !ok ) return; const DB::FileNameList imageList = m_setup.imageList(); for (int index = 0; index < imageList.size(); ++index) { DB::FileName current = imageList.at(index); DB::FileName prev; DB::FileName next; if ( index != 0 ) prev = imageList.at(index - 1); if (index != imageList.size() - 1) next = imageList.at(index + 1); ok = generateContentPage( (*sizeIt)->width(), (*sizeIt)->height(), prev, current, next ); if (!ok) return; } } // Now generate the thumbnail images Debug() << "Generating thumbnail images..."; for (const DB::FileName& fileName : m_setup.imageList()) { if ( wasCanceled() ) return; createImage(fileName, m_setup.thumbSize()); } if ( wasCanceled() ) return; if ( m_waitCounter > 0 ) { m_hasEnteredLoop = true; m_eventLoop->exec(); } if ( wasCanceled() ) return; Debug() << "Linking image file..."; bool ok = linkIndexFile(); if ( !ok ) return; Debug() << "Copying theme files..."; // Copy over the mainpage.css, imagepage.css QString themeDir, themeAuthor, themeName; getThemeInfo( &themeDir, &themeName, &themeAuthor ); QDir dir( themeDir ); QStringList files = dir.entryList( QDir::Files ); if( files.count() < 1 ) qWarning() << QString::fromLatin1("theme '%1' doesn't have enough files to be a theme").arg( themeDir ); for( QStringList::Iterator it = files.begin(); it != files.end(); ++it ) { if( *it == QString::fromLatin1("kphotoalbum.theme") || *it == QString::fromLatin1("mainpage.html") || *it == QString::fromLatin1("imagepage.html")) continue; QString from = QString::fromLatin1("%1%2").arg( themeDir ).arg(*it); QString to = m_tempDir.filePath(*it); ok = Utilities::copy( from, to ); if ( !ok ) { KMessageBox::error( this, i18n("Error copying %1 to %2", from , to ) ); return; } } // Copy files over to destination. QString outputDir = m_setup.baseDir() + QString::fromLatin1( "/" ) + m_setup.outputDir(); Debug() << "Copying files from" << m_tempDir.path() << "to final location"<< outputDir<<"..."; KIO::CopyJob* job = KIO::move( QUrl::fromLocalFile( m_tempDir.path() ), QUrl::fromUserInput(outputDir) ); connect(job, &KIO::CopyJob::result, this, &Generator::showBrowser); m_eventLoop->exec(); return; } bool HTMLGenerator::Generator::generateIndexPage( int width, int height ) { QString themeDir, themeAuthor, themeName; getThemeInfo( &themeDir, &themeName, &themeAuthor ); QString content = Utilities::readFile( QString::fromLatin1( "%1mainpage.html" ).arg( themeDir ) ); if ( content.isEmpty() ) return false; // Adding the copyright comment after DOCTYPE not before (HTML standard requires the DOCTYPE to be first within the document) QRegExp rx( QString::fromLatin1( "^(<!DOCTYPE[^>]*>)" ) ); int position; rx.setCaseSensitivity( Qt::CaseInsensitive ); position = rx.indexIn( content ); if ( ( position += rx.matchedLength () ) < 0 ) content = QString::fromLatin1("<!--\nMade with KPhotoAlbum. (http://www.kphotoalbum.org/)\nCopyright © Jesper K. Pedersen\nTheme %1 by %2\n-->\n").arg( themeName ).arg( themeAuthor ) + content; else content.insert( position, QString::fromLatin1("\n<!--\nMade with KPhotoAlbum. (http://www.kphotoalbum.org/)\nCopyright © Jesper K. Pedersen\nTheme %1 by %2\n-->\n").arg( themeName ).arg( themeAuthor ) ); content.replace( QString::fromLatin1( "**DESCRIPTION**" ), m_setup.description() ); content.replace( QString::fromLatin1( "**TITLE**" ), m_setup.title() ); QString copyright; if (!m_setup.copyright().isEmpty()) copyright = QString::fromLatin1( "© %1" ).arg( m_setup.copyright() ); else copyright = QString::fromLatin1( " " ); content.replace( QString::fromLatin1( "**COPYRIGHT**" ), copyright ); QString kimLink = QString::fromLatin1( "Share and Enjoy <a href=\"%1\">KPhotoAlbum export file</a>" ).arg( kimFileName( true ) ); if ( m_setup.generateKimFile() ) content.replace( QString::fromLatin1( "**KIMFILE**" ), kimLink ); else content.remove( QString::fromLatin1( "**KIMFILE**" ) ); QDomDocument doc; QDomElement elm; QDomElement col; // -------------------------------------------------- Thumbnails // Initially all of the HTML generation was done using QDom, but it turned out in the end // to be much less code simply concatenating strings. This part, however, is easier using QDom // so we keep it using QDom. int count = 0; int cols = m_setup.numOfCols(); int minWidth = 0; int minHeight = 0; int enableVideo = 0; QString first, last, images; images += QString::fromLatin1( "var gallery=new Array()\nvar width=%1\nvar height=%2\nvar tsize=%3\nvar inlineVideo=%4\nvar generatedVideo=%5\n" ).arg( width ).arg( height ).arg( m_setup.thumbSize () ).arg( m_setup.inlineMovies() ).arg( m_setup.html5VideoGenerate() ); minImageSize(minWidth, minHeight); if ( minWidth == 0 && minHeight == 0 ) { // full size only images += QString::fromLatin1( "var minPage=\"index-fullsize.html\"\n" ); } else { images += QString::fromLatin1( "var minPage=\"index-%1x%2.html\"\n" ).arg( minWidth ).arg( minHeight ); } QDomElement row; for (const DB::FileName& fileName : m_setup.imageList()) { const DB::ImageInfoPtr info = fileName.info(); if ( wasCanceled() ) return false; if ( count % cols == 0 ) { row = doc.createElement( QString::fromLatin1( "tr" ) ); row.setAttribute( QString::fromLatin1( "class" ), QString::fromLatin1( "thumbnail-row" ) ); doc.appendChild( row ); count = 0; } col = doc.createElement( QString::fromLatin1( "td" ) ); col.setAttribute( QString::fromLatin1( "class" ), QString::fromLatin1( "thumbnail-col" ) ); row.appendChild( col ); if (first.isEmpty()) first = namePage( width, height, fileName); else last = namePage( width, height, fileName); if (!Utilities::isVideo(fileName)) { QMimeDatabase db; images += QString::fromLatin1( "gallery.push([\"%1\", \"%2\", \"%3\", \"%4\", \"" ) .arg( nameImage( fileName, width ) ) .arg( nameImage( fileName, m_setup.thumbSize()) ) .arg( nameImage( fileName, maxImageSize()) ) .arg( db.mimeTypeForFile( nameImage( fileName, maxImageSize() ) ).name() ); } else { QMimeDatabase db; images += QString::fromLatin1( "gallery.push([\"%1\", \"%2\", \"%3\"" ) .arg( nameImage( fileName, m_setup.thumbSize() ) ) .arg( nameImage( fileName, m_setup.thumbSize() ) ) .arg( nameImage( fileName, maxImageSize() ) ); if ( m_setup.html5VideoGenerate() ) { images += QString::fromLatin1( ", \"%1\", \"" ) .arg( QString::fromLatin1( "video/ogg" ) ); } else { images += QString::fromLatin1( ", \"%1\", \"" ) .arg( db.mimeTypeForFile( fileName.relative(), QMimeDatabase::MatchExtension).name() ); } enableVideo = 1; } // -------------------------------------------------- Description if ( !info->description().isEmpty() && m_setup.includeCategory( QString::fromLatin1( "**DESCRIPTION**" )) ) { images += QString::fromLatin1( "%1\", \"" ) .arg( info->description() .replace( QString::fromLatin1( "\n$" ), QString::fromLatin1( "" ) ) .replace( QString::fromLatin1( "\n" ), QString::fromLatin1 ( " " ) ) .replace( QString::fromLatin1( "\"" ), QString::fromLatin1 ( "\\\"" ) ) ); } else { images += QString::fromLatin1( "\", \"" ); } QString description = populateDescription(DB::ImageDB::instance()->categoryCollection()->categories(), info); if ( !description.isEmpty() ) { description = QString::fromLatin1 ( "<ul>%1</ul>" ).arg ( description ); } else { description = QString::fromLatin1 ( "" ); } description.replace( QString::fromLatin1( "\n$" ), QString::fromLatin1 ( "" ) ); description.replace( QString::fromLatin1( "\n" ), QString::fromLatin1 ( " " ) ); description.replace( QString::fromLatin1( "\"" ), QString::fromLatin1 ( "\\\"" ) ); images += description; images += QString::fromLatin1( "\"]);\n" ); QDomElement href = doc.createElement( QString::fromLatin1( "a" ) ); href.setAttribute( QString::fromLatin1( "href" ), namePage( width, height, fileName)); col.appendChild( href ); QDomElement img = doc.createElement( QString::fromLatin1( "img" ) ); img.setAttribute( QString::fromLatin1( "src" ), nameImage( fileName, m_setup.thumbSize() ) ); img.setAttribute( QString::fromLatin1( "alt" ), nameImage( fileName, m_setup.thumbSize() ) ); href.appendChild( img ); ++count; } // Adding TD elements to match the selected column amount for valid HTML if ( count % cols != 0 ) { for ( int i = count; i % cols != 0; ++i ) { col = doc.createElement( QString::fromLatin1( "td" ) ); col.setAttribute( QString::fromLatin1( "class" ), QString::fromLatin1( "thumbnail-col" ) ); QDomText sp = doc.createTextNode( QString::fromLatin1( " " ) ); col.appendChild( sp ); row.appendChild( col ); } } content.replace( QString::fromLatin1( "**THUMBNAIL-TABLE**" ), doc.toString() ); images += QString::fromLatin1( "var enableVideo=%1\n" ).arg( enableVideo ? 1 : 0 ); content.replace( QString::fromLatin1( "**JSIMAGES**" ), images ); if (!first.isEmpty()) content.replace( QString::fromLatin1( "**FIRST**" ), first ); if (!last.isEmpty()) content.replace( QString::fromLatin1( "**LAST**" ), last ); // -------------------------------------------------- Resolutions QString resolutions; QList<ImageSizeCheckBox*> actRes = m_setup.activeResolutions(); std::sort(actRes.begin(),actRes.end()); if ( actRes.count() > 1 ) { resolutions += QString::fromLatin1( "Resolutions: " ); for( QList<ImageSizeCheckBox*>::ConstIterator sizeIt = actRes.constBegin(); sizeIt != actRes.constEnd(); ++sizeIt ) { int w = (*sizeIt)->width(); int h = (*sizeIt)->height(); QString page = QString::fromLatin1( "index-%1.html" ).arg( ImageSizeCheckBox::text( w, h, true ) ); QString text = (*sizeIt)->text(false); resolutions += QString::fromLatin1( " " ); if ( width == w && height == h ) { resolutions += text; } else { resolutions += QString::fromLatin1( "<a href=\"%1\">%2</a>" ).arg( page ).arg( text ); } } } content.replace( QString::fromLatin1( "**RESOLUTIONS**" ), resolutions ); if ( wasCanceled() ) return false; // -------------------------------------------------- write to file QString fileName = m_tempDir.filePath( QString::fromLatin1("index-%1.html" ) .arg(ImageSizeCheckBox::text(width,height,true)) ); bool ok = writeToFile( fileName, content ); if ( !ok ) return false; return true; } bool HTMLGenerator::Generator::generateContentPage( int width, int height, const DB::FileName& prev, const DB::FileName& current, const DB::FileName& next ) { QString themeDir, themeAuthor, themeName; getThemeInfo( &themeDir, &themeName, &themeAuthor ); QString content = Utilities::readFile( QString::fromLatin1( "%1imagepage.html" ).arg( themeDir )); if ( content.isEmpty() ) return false; DB::ImageInfoPtr info = current.info(); const DB::FileName currentFile = info->fileName(); // Adding the copyright comment after DOCTYPE not before (HTML standard requires the DOCTYPE to be first within the document) QRegExp rx( QString::fromLatin1( "^(<!DOCTYPE[^>]*>)" ) ); int position; rx.setCaseSensitivity( Qt::CaseInsensitive ); position = rx.indexIn( content ); if ( ( position += rx.matchedLength () ) < 0 ) content = QString::fromLatin1("<!--\nMade with KPhotoAlbum. (http://www.kphotoalbum.org/)\nCopyright © Jesper K. Pedersen\nTheme %1 by %2\n-->\n").arg( themeName ).arg( themeAuthor ) + content; else content.insert( position, QString::fromLatin1("\n<!--\nMade with KPhotoAlbum. (http://www.kphotoalbum.org/)\nCopyright © Jesper K. Pedersen\nTheme %1 by %2\n-->\n").arg( themeName ).arg( themeAuthor ) ); // TODO: Hardcoded non-standard category names is not good practice QString title = QString::fromLatin1(""); QString name = QString::fromLatin1( "Common Name" ); if ( !info->itemsOfCategory( name ).empty() ) { title += QStringList(info->itemsOfCategory( name ).toList()).join( QString::fromLatin1(" - ") ); } else { name = QString::fromLatin1( "Latin Name" ); if ( !info->itemsOfCategory( name ).empty() ) { title += QStringList(info->itemsOfCategory( name ).toList()).join( QString::fromLatin1(" - ") ); } else { title = info->label(); } } content.replace( QString::fromLatin1( "**TITLE**" ), title ); // Image or video content if (Utilities::isVideo(currentFile)) { QString videoFile = createVideo( currentFile ); QString videoBase = videoFile.replace( QRegExp( QString::fromLatin1("\\..*") ), QString::fromLatin1("") ); if ( m_setup.inlineMovies() ) if ( m_setup.html5Video() ) content.replace( QString::fromLatin1( "**IMAGE_OR_VIDEO**" ), QString::fromLatin1( "<video controls><source src=\"%4\" type=\"video/mp4\" /><source src=\"%5\" type=\"video/ogg\" /><object data=\"%1\"><img src=\"%2\" alt=\"download\"/></object></video><a href=\"%3\"><img src=\"download.png\" /></a>").arg( QString::fromLatin1("%1.mp4").arg( videoBase ) ).arg( createImage( current, 256 ) ).arg( QString::fromLatin1("%1.mp4").arg( videoBase ) ).arg( QString::fromLatin1("%1.mp4").arg( videoBase ) ).arg( QString::fromLatin1("%1.ogg").arg( videoBase ) ) ); else content.replace( QString::fromLatin1( "**IMAGE_OR_VIDEO**" ), QString::fromLatin1( "<object data=\"%1\"><img src=\"%2\"/></object>" "<a href=\"%3\"><img src=\"download.png\"/></a>").arg(videoFile).arg( createImage( current, 256 ) ).arg( videoFile ) ); else content.replace( QString::fromLatin1( "**IMAGE_OR_VIDEO**" ), QString::fromLatin1( "<a href=\"**NEXTPAGE**\"><img src=\"%2\"/></a>" "<a href=\"%1\"><img src=\"download.png\"/></a>").arg(videoFile).arg( createImage( current, 256 ) ) ); } else content.replace( QString::fromLatin1( "**IMAGE_OR_VIDEO**" ), QString::fromLatin1( "<a href=\"**NEXTPAGE**\"><img src=\"%1\" alt=\"%1\"/></a>") .arg(createImage( current, width ) ) ); // -------------------------------------------------- Links QString link; // prev link if ( !prev.isNull() ) link = i18n( "<a href=\"%1\">prev</a>", namePage( width, height, prev)); else link = i18n( "prev" ); content.replace( QString::fromLatin1( "**PREV**" ), link ); // PENDING(blackie) These next 5 line also exists exactly like that in HTMLGenerator::Generator::generateIndexPage. Please refactor. // prevfile if ( !prev.isNull() ) link = namePage( width, height, prev); else link = i18n( "prev" ); content.replace( QString::fromLatin1( "**PREVFILE**" ), link ); // index link link = i18n( "<a href=\"index-%1.html\">index</a>", ImageSizeCheckBox::text(width,height,true)); content.replace( QString::fromLatin1( "**INDEX**" ), link ); // indexfile link = QString::fromLatin1( "index-%1.html").arg(ImageSizeCheckBox::text(width,height,true)); content.replace( QString::fromLatin1( "**INDEXFILE**" ), link ); // Next Link if ( !next.isNull() ) link = i18n( "<a href=\"%1\">next</a>", namePage( width, height, next)); else link = i18n( "next" ); content.replace( QString::fromLatin1( "**NEXT**" ), link ); // Nextfile if ( !next.isNull() ) link = namePage(width, height, next); else link = i18n( "next" ); content.replace( QString::fromLatin1( "**NEXTFILE**" ), link ); if ( !next.isNull() ) link = namePage(width, height, next); else link = QString::fromLatin1( "index-%1.html" ).arg(ImageSizeCheckBox::text(width,height,true)); content.replace( QString::fromLatin1( "**NEXTPAGE**" ), link ); // -------------------------------------------------- Resolutions QString resolutions; const QList<ImageSizeCheckBox*>& actRes = m_setup.activeResolutions(); if ( actRes.count() > 1 ) { for( QList<ImageSizeCheckBox*>::ConstIterator sizeIt = actRes.begin(); sizeIt != actRes.end(); ++sizeIt ) { int w = (*sizeIt)->width(); int h = (*sizeIt)->height(); QString page = namePage( w, h, currentFile ); QString text = (*sizeIt)->text(false); resolutions += QString::fromLatin1( " " ); if ( width == w && height == h ) resolutions += text; else resolutions += QString::fromLatin1( "<a href=\"%1\">%2</a>" ).arg( page ).arg( text ); } } content.replace( QString::fromLatin1( "**RESOLUTIONS**" ), resolutions ); // -------------------------------------------------- Copyright QString copyright; if ( !m_setup.copyright().isEmpty() ) copyright = QString::fromLatin1( "© %1" ).arg( m_setup.copyright() ); else copyright = QString::fromLatin1( " " ); content.replace( QString::fromLatin1( "**COPYRIGHT**" ), QString::fromLatin1( "%1" ).arg( copyright ) ); // -------------------------------------------------- Description QString description = populateDescription(DB::ImageDB::instance()->categoryCollection()->categories(), info); if ( !description.isEmpty() ) content.replace( QString::fromLatin1( "**DESCRIPTION**" ), QString::fromLatin1( "<ul>\n%1\n</ul>" ).arg( description ) ); else content.replace( QString::fromLatin1( "**DESCRIPTION**" ), QString::fromLatin1( "" ) ); // -------------------------------------------------- write to file QString fileName = m_tempDir.filePath(namePage( width, height, currentFile )); bool ok = writeToFile( fileName, content ); if ( !ok ) return false; return true; } QString HTMLGenerator::Generator::namePage( int width, int height, const DB::FileName& fileName ) { QString name = m_filenameMapper.uniqNameFor(fileName); QString base = QFileInfo( name ).completeBaseName(); return QString::fromLatin1( "%1-%2.html" ).arg( base ).arg( ImageSizeCheckBox::text(width,height,true) ); } QString HTMLGenerator::Generator::nameImage( const DB::FileName& fileName, int size ) { QString name = m_filenameMapper.uniqNameFor(fileName); QString base = QFileInfo( name ).completeBaseName(); if ( size == maxImageSize() && !Utilities::isVideo( fileName ) ) { if ( name.endsWith( QString::fromLatin1(".jpg"), Qt::CaseSensitive ) || name.endsWith( QString::fromLatin1(".jpeg"), Qt::CaseSensitive ) ) return name; else return base + QString::fromLatin1(".jpg"); } else if ( size == maxImageSize() && Utilities::isVideo( fileName ) ) { return name; } else return QString::fromLatin1( "%1-%2.jpg" ).arg( base ).arg( size ); } QString HTMLGenerator::Generator::createImage( const DB::FileName& fileName, int size ) { DB::ImageInfoPtr info = fileName.info(); if ( m_generatedFiles.contains( qMakePair(fileName,size) ) ) { m_waitCounter--; } else { ImageManager::ImageRequest* request = new ImageManager::ImageRequest( fileName, QSize( size, size ), info->angle(), this ); request->setPriority( ImageManager::BatchTask ); ImageManager::AsyncLoader::instance()->load( request ); m_generatedFiles.insert( qMakePair( fileName, size ) ); } return nameImage( fileName, size ); } QString HTMLGenerator::Generator::createVideo( const DB::FileName& fileName ) { setValue( m_total - m_waitCounter ); qApp->processEvents(); QString baseName = nameImage( fileName, maxImageSize() ); QString destName = m_tempDir.filePath(baseName); if ( !m_copiedVideos.contains( fileName )) { if ( m_setup.html5VideoGenerate() ) { // TODO: shouldn't we use avconv library directly instead of KRun // TODO: should check that the avconv (ffmpeg takes the same parameters on older systems) and ffmpeg2theora exist // TODO: Figure out avconv parameters to get rid of ffmpeg2theora KRun::runCommand(QString::fromLatin1("%1 -y -i %2 -vcodec libx264 -b 250k -bt 50k -acodec libfaac -ab 56k -ac 2 -s %3 %4") .arg( m_avconv ) .arg( fileName.absolute() ).arg( QString::fromLatin1( "320x240" ) ) .arg( destName.replace( QRegExp( QString::fromLatin1("\\..*") ), QString::fromLatin1(".mp4") ) ), MainWindow::Window::theMainWindow() ); KRun::runCommand(QString::fromLatin1("ffmpeg2theora -v 7 -o %1 -x %2 %3") .arg( destName.replace( QRegExp( QString::fromLatin1("\\..*") ), QString::fromLatin1(".ogg") ) ) .arg( QString::fromLatin1( "320" ) ).arg( fileName.absolute() ), MainWindow::Window::theMainWindow() ); } else Utilities::copy( fileName.absolute(), destName ); m_copiedVideos.insert( fileName ); } return baseName; } QString HTMLGenerator::Generator::kimFileName( bool relative ) { if ( relative ) return QString::fromLatin1( "%2.kim" ).arg( m_setup.outputDir() ); else return m_tempDir.filePath( QString::fromLatin1("%2.kim").arg( m_setup.outputDir())); } bool HTMLGenerator::Generator::writeToFile( const QString& fileName, const QString& str ) { QFile file(fileName); if ( !file.open(QIODevice::WriteOnly) ) { KMessageBox::error( this, i18n("Could not create file '%1'.",fileName), i18n("Could Not Create File") ); return false; } QByteArray data = translateToHTML(str).toUtf8(); file.write( data ); file.close(); return true; } QString HTMLGenerator::Generator::translateToHTML( const QString& str ) { QString res; for ( int i = 0 ; i < str.length() ; ++i ) { if ( str[i].unicode() < 128 ) res.append( str[i] ); else { res.append( QString().sprintf("&#%u;", (unsigned int)str[i].unicode() ) ); } } return res; } bool HTMLGenerator::Generator::linkIndexFile() { ImageSizeCheckBox* resolution = m_setup.activeResolutions()[0]; QString fromFile = QString::fromLatin1("index-%1.html" ) .arg(resolution->text(true)); fromFile = m_tempDir.filePath(fromFile); QString destFile = m_tempDir.filePath( QString::fromLatin1("index.html") ); bool ok = Utilities::copy( fromFile, destFile ); if ( !ok ) { KMessageBox::error( this, i18n("<p>Unable to copy %1 to %2</p>" , fromFile , destFile ) ); return false; } return ok; } void HTMLGenerator::Generator::slotCancelGenerate() { ImageManager::AsyncLoader::instance()->stop( this ); m_waitCounter = 0; if ( m_hasEnteredLoop ) m_eventLoop->exit(); } void HTMLGenerator::Generator::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& image) { const DB::FileName fileName = request->databaseFileName(); const QSize imgSize = request->size(); const bool loadedOK = request->loadedOK(); setValue( m_total - m_waitCounter ); m_waitCounter--; int size = imgSize.width(); QString file = m_tempDir.filePath( nameImage( fileName, size ) ); bool success = loadedOK && image.save( file, "JPEG" ); if ( !success ) { // We better stop the imageloading. In case this is a full disk, we will just get all images loaded, while this // error box is showing, resulting in a bunch of error messages, and memory running out due to all the hanging // pixmapLoaded methods. slotCancelGenerate(); KMessageBox::error( this, i18n("Unable to write image '%1'.",file) ); } if ( !Utilities::isVideo( fileName ) ) { try { Exif::Info::instance()->writeInfoToFile( fileName, file ); } catch (...) { } } if ( m_waitCounter == 0 && m_hasEnteredLoop) { m_eventLoop->exit(); } } int HTMLGenerator::Generator::calculateSteps() { int count = m_setup.activeResolutions().count(); return m_setup.imageList().size() * (1 + count); // 1 thumbnail + 1 real image } void HTMLGenerator::Generator::getThemeInfo( QString* baseDir, QString* name, QString* author ) { *baseDir = m_setup.themePath(); KConfig themeconfig( QString::fromLatin1( "%1/kphotoalbum.theme").arg( *baseDir ), KConfig::SimpleConfig ); KConfigGroup config = themeconfig.group("theme"); *name = config.readEntry( "Name" ); *author = config.readEntry( "Author" ); } int HTMLGenerator::Generator::maxImageSize() { int res = 0; for( QList<ImageSizeCheckBox*>::ConstIterator sizeIt = m_setup.activeResolutions().begin(); sizeIt != m_setup.activeResolutions().end(); ++sizeIt ) { res = qMax( res, (*sizeIt)->width() ); } return res; } void HTMLGenerator::Generator::minImageSize(int& width, int& height) { width = height = 0; for( QList<ImageSizeCheckBox*>::ConstIterator sizeIt = m_setup.activeResolutions().begin(); sizeIt != m_setup.activeResolutions().end(); ++sizeIt ) { if ((width == 0) && ((*sizeIt)->width() > 0)) { width = (*sizeIt)->width(); height = (*sizeIt)->height(); } else if ((*sizeIt)->width() > 0) { width = qMin( width, (*sizeIt)->width() ); height = qMin( height, (*sizeIt)->height()); } } } void HTMLGenerator::Generator::showBrowser() { if ( m_setup.generateKimFile() ) ImportExport::Export::showUsageDialog(); if ( ! m_setup.baseURL().isEmpty() ) new KRun( QUrl::fromUserInput(QString::fromLatin1( "%1/%2/index.html" ).arg( m_setup.baseURL() ).arg( m_setup.outputDir()) ), MainWindow::Window::theMainWindow()); m_eventLoop->exit(); } QString HTMLGenerator::Generator::populateDescription( QList<DB::CategoryPtr> categories, const DB::ImageInfoPtr info ) { QString description; if (m_setup.includeCategory(QString::fromLatin1("**DATE**"))) description += QString::fromLatin1 ( "<li> <b>%1</b> %2</li>" ).arg ( i18n("Date") ).arg ( info->date().toString() ); for( QList<DB::CategoryPtr>::Iterator it = categories.begin(); it != categories.end(); ++it ) { if ( (*it)->isSpecialCategory() ) continue; QString name = (*it)->name(); if ( !info->itemsOfCategory( name ).empty() && m_setup.includeCategory(name) ) { QString val = QStringList(info->itemsOfCategory( name ).toList()).join( QString::fromLatin1(", ") ); description += QString::fromLatin1(" <li> <b>%1:</b> %2</li>").arg( name ).arg( val ); } } if ( !info->description().isEmpty() && m_setup.includeCategory( QString::fromLatin1( "**DESCRIPTION**" )) ) { description += QString::fromLatin1( " <li> <b>Description:</b> %1</li>" ).arg( info->description() ); } return description; } -#include "Generator.moc" + // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/HTMLGenerator/HTMLDialog.cpp b/HTMLGenerator/HTMLDialog.cpp index fe6d02cc..a569cd52 100644 --- a/HTMLGenerator/HTMLDialog.cpp +++ b/HTMLGenerator/HTMLDialog.cpp @@ -1,658 +1,657 @@ -/* Copyright (C) 2003-2016 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "HTMLDialog.h" #include <QCheckBox> #include <QComboBox> #include <QDebug> #include <QDialogButtonBox> #include <QFileDialog> #include <QGroupBox> #include <QHBoxLayout> #include <QLabel> #include <QLayout> #include <QLineEdit> #include <QPushButton> #include <QScopedPointer> #include <QSpinBox> #include <QStandardPaths> #include <QStringMatcher> #include <QVBoxLayout> #include <KConfig> #include <KConfigGroup> #include <KFileItem> #include <KIO/DeleteJob> #include <KIO/StatJob> #include <KJob> #include <KJobWidgets> #include <KLocalizedString> #include <KMessageBox> #include <KTextEdit> #include <DB/CategoryCollection.h> #include <DB/ImageDB.h> #include <MainWindow/Window.h> #include <Settings/SettingsData.h> #include "Generator.h" #include "ImageSizeCheckBox.h" using namespace HTMLGenerator; #ifdef DEBUG_HTMLGENERATOR #define Debug qDebug #else #define Debug if(0) qDebug #endif HTMLDialog::HTMLDialog( QWidget* parent ) : KPageDialog(parent) , m_list() { setWindowTitle( i18n("HTML Export") ); QWidget *mainWidget = new QWidget(this); this->layout()->addWidget(mainWidget); createContentPage(); createLayoutPage(); createDestinationPage(); // destUrl is only relevant for .kim file creation: connect(m_generateKimFile,&QCheckBox::toggled,m_destURL,&QLineEdit::setEnabled); // automatically fill in output directory: connect(m_title,&QLineEdit::editingFinished,this,&HTMLDialog::slotSuggestOutputDir); QDialogButtonBox *buttonBox = this->buttonBox(); connect(buttonBox, &QDialogButtonBox::accepted, this, &HTMLDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &HTMLDialog::reject); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); okButton->setEnabled( m_themes.size()>0 ); connect(okButton, &QPushButton::clicked, this, &HTMLDialog::slotOk); this->layout()->addWidget(buttonBox); } void HTMLDialog::createContentPage() { QWidget* contentPage = new QWidget; KPageWidgetItem* page = new KPageWidgetItem( contentPage, i18n("Content" ) ); page->setHeader( i18n("Content" ) ); page->setIcon( QIcon::fromTheme( QString::fromLatin1( "document-properties" ) ) ); addPage( page ); QVBoxLayout* lay1 = new QVBoxLayout( contentPage ); QGridLayout* lay2 = new QGridLayout; lay1->addLayout( lay2 ); QLabel* label = new QLabel( i18n("Page title:"), contentPage ); lay2->addWidget( label, 0, 0 ); m_title = new QLineEdit( contentPage ); label->setBuddy( m_title ); lay2->addWidget( m_title, 0, 1 ); // Copyright label = new QLabel( i18n("Copyright:"), contentPage ); label->setAlignment( Qt::AlignTop ); lay2->addWidget( label, 1, 0 ); m_copyright = new QLineEdit( contentPage ); m_copyright->setText( Settings::SettingsData::instance()->HTMLCopyright() ); label->setBuddy( m_copyright ); lay2->addWidget( m_copyright, 1, 1 ); // Description label = new QLabel( i18n("Description:"), contentPage ); label->setAlignment( Qt::AlignTop ); lay2->addWidget( label, 2, 0 ); m_description = new KTextEdit( contentPage ); label->setBuddy( m_description ); lay2->addWidget( m_description, 2, 1 ); m_generateKimFile = new QCheckBox( i18n("Create .kim export file"), contentPage ); m_generateKimFile->setChecked( Settings::SettingsData::instance()->HTMLKimFile() ); lay1->addWidget( m_generateKimFile ); m_inlineMovies = new QCheckBox( i18nc("Inline as a verb, i.e. 'please show movies right on the page, not as links'", "Inline Movies in pages" ), contentPage ); m_inlineMovies->setChecked( Settings::SettingsData::instance()->HTMLInlineMovies() ); lay1->addWidget( m_inlineMovies ); m_html5Video = new QCheckBox( i18nc("Tag as in HTML-tag, not as in image tag", "Use HTML5 video tag" ), contentPage ); m_html5Video->setChecked( Settings::SettingsData::instance()->HTML5Video() ); lay1->addWidget( m_html5Video ); QString avconv = QStandardPaths::findExecutable(QString::fromUtf8("avconv")); const QString ffmpeg2theora = QStandardPaths::findExecutable(QString::fromUtf8("ffmpeg2theora")); QStandardPaths::findExecutable(QString::fromUtf8("avconv")); if ( avconv.isNull() ) avconv = QStandardPaths::findExecutable(QString::fromUtf8("ffmpeg")); QString txt = i18n( "<p>This selection will generate video files suitable for displaying on web. " "avconv and ffmpeg2theora are required for video file generation.</p>" ); m_html5VideoGenerate = new QCheckBox( i18n( "Generate HTML5 video files (mp4 and ogg)" ), contentPage ); m_html5VideoGenerate->setChecked( Settings::SettingsData::instance()->HTML5VideoGenerate() ); lay1->addWidget( m_html5VideoGenerate ); m_html5VideoGenerate->setWhatsThis( txt ); if ( avconv.isNull() || ffmpeg2theora.isNull() ) m_html5VideoGenerate->setEnabled( false ); // What to include QGroupBox* whatToInclude = new QGroupBox( i18n( "What to Include" ), contentPage ); lay1->addWidget( whatToInclude ); QGridLayout* lay3 = new QGridLayout( whatToInclude ); QCheckBox* cb = new QCheckBox( i18n("Description"), whatToInclude ); m_whatToIncludeMap.insert( QString::fromLatin1("**DESCRIPTION**"), cb ); lay3->addWidget( cb, 0, 0 ); m_date = new QCheckBox( i18n("Date"), whatToInclude ); m_date->setChecked( Settings::SettingsData::instance()->HTMLDate() ); m_whatToIncludeMap.insert( QString::fromLatin1("**DATE**"), m_date ); lay3->addWidget( m_date, 0, 1 ); int row=1; int col=0; QString selectionsTmp = Settings::SettingsData::instance()->HTMLIncludeSelections(); QStringMatcher* pattern = new QStringMatcher(); pattern->setPattern(QString::fromLatin1("**DESCRIPTION**")); cb->setChecked( pattern->indexIn (selectionsTmp) >= 0 ? 1 : 0 ); QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); for( QList<DB::CategoryPtr>::Iterator it = categories.begin(); it != categories.end(); ++it ) { if ( ! (*it)->isSpecialCategory() ) { QCheckBox* cb = new QCheckBox((*it)->name(), whatToInclude); lay3->addWidget( cb, row, col%2 ); m_whatToIncludeMap.insert( (*it)->name(), cb ); pattern->setPattern((*it)->name()); cb->setChecked( pattern->indexIn (selectionsTmp) >= 0 ? 1 : 0 ); if ( ++col % 2 == 0 ) ++row; } } } void HTMLDialog::createLayoutPage() { QWidget* layoutPage = new QWidget; KPageWidgetItem* page = new KPageWidgetItem( layoutPage, i18n("Layout" ) ); page->setHeader( i18n("Layout" ) ); page->setIcon( QIcon::fromTheme( QString::fromLatin1( "configure" )) ); addPage(page); QVBoxLayout* lay1 = new QVBoxLayout( layoutPage ); QGridLayout* lay2 = new QGridLayout; lay1->addLayout( lay2 ); // Thumbnail size QLabel* label = new QLabel( i18n("Thumbnail size:"), layoutPage ); lay2->addWidget( label, 0, 0 ); QHBoxLayout* lay3 = new QHBoxLayout; lay2->addLayout( lay3, 0, 1 ); m_thumbSize = new QSpinBox; m_thumbSize->setRange( 16, 256 ); m_thumbSize->setValue( Settings::SettingsData::instance()->HTMLThumbSize() ); lay3->addWidget( m_thumbSize ); lay3->addStretch(1); label->setBuddy( m_thumbSize ); // Number of columns label = new QLabel( i18n("Number of columns:"), layoutPage ); lay2->addWidget( label, 1, 0 ); QHBoxLayout* lay4 = new QHBoxLayout; lay2->addLayout( lay4, 1, 1 ); m_numOfCols = new QSpinBox; m_numOfCols->setRange( 1, 10 ); label->setBuddy( m_numOfCols); m_numOfCols->setValue( Settings::SettingsData::instance()->HTMLNumOfCols() ); lay4->addWidget( m_numOfCols ); lay4->addStretch( 1 ); // Theme box label = new QLabel( i18n("Theme:"), layoutPage ); lay2->addWidget( label, 2, 0 ); lay4 = new QHBoxLayout; lay2->addLayout( lay4, 2, 1 ); m_themeBox = new KComboBox( layoutPage ); label->setBuddy( m_themeBox ); lay4->addWidget( m_themeBox ); lay4->addStretch( 1 ); m_themeInfo = new QLabel( i18n("Theme Description"), layoutPage ); m_themeInfo->setWordWrap(true); lay2->addWidget( m_themeInfo, 3, 1 ); connect(m_themeBox, static_cast<void (KComboBox::*)(int)>(&KComboBox::currentIndexChanged), this, &HTMLDialog::displayThemeDescription); populateThemesCombo(); // Image sizes QGroupBox* sizes = new QGroupBox( i18n("Image Sizes"), layoutPage ); lay1->addWidget( sizes ); QGridLayout* lay5 = new QGridLayout( sizes ); ImageSizeCheckBox* size320 = new ImageSizeCheckBox( 320, 200, sizes ); ImageSizeCheckBox* size640 = new ImageSizeCheckBox( 640, 480, sizes ); ImageSizeCheckBox* size800 = new ImageSizeCheckBox( 800, 600, sizes ); ImageSizeCheckBox* size1024 = new ImageSizeCheckBox( 1024, 768, sizes ); ImageSizeCheckBox* size1280 = new ImageSizeCheckBox( 1280, 1024, sizes ); ImageSizeCheckBox* size1600 = new ImageSizeCheckBox( 1600, 1200, sizes ); ImageSizeCheckBox* sizeOrig = new ImageSizeCheckBox( i18n("Full size"), sizes ); { int row = 0; int col = -1; lay5->addWidget( size320, row, ++col ); lay5->addWidget( size640, row, ++col ); lay5->addWidget( size800, row, ++col ); lay5->addWidget( size1024, row, ++col ); col =-1; lay5->addWidget( size1280, ++row, ++col ); lay5->addWidget( size1600, row, ++col ); lay5->addWidget( sizeOrig, row, ++col ); } QString tmp; if ((tmp = Settings::SettingsData::instance()->HTMLSizes()) != QString::fromLatin1("")) { QStringMatcher* pattern = new QStringMatcher(QString::fromLatin1("320")); size320->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); pattern->setPattern(QString::fromLatin1("640")); size640->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); pattern->setPattern(QString::fromLatin1("800")); size800->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0 ); pattern->setPattern(QString::fromLatin1("1024")); size1024->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); pattern->setPattern(QString::fromLatin1("1280")); size1280->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); pattern->setPattern(QString::fromLatin1("1600")); size1600->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); pattern->setPattern(QString::fromLatin1("-1")); sizeOrig->setChecked( pattern->indexIn (tmp) >= 0 ? 1 : 0); } else size800->setChecked( 1 ); m_sizeCheckBoxes << size800 << size1024 << size1280 << size640 << size1600 << size320 << sizeOrig; lay1->addStretch(1); QGridLayout* lay6 = new QGridLayout; lay1->addLayout( lay6 ); } void HTMLDialog::createDestinationPage() { QWidget* destinationPage = new QWidget; KPageWidgetItem* page = new KPageWidgetItem( destinationPage, i18n("Destination" ) ); page->setHeader( i18n("Destination" ) ); page->setIcon( QIcon::fromTheme( QString::fromLatin1( "drive-harddisk" ) ) ); addPage( page ); QVBoxLayout* lay1 = new QVBoxLayout( destinationPage ); QGridLayout* lay2 = new QGridLayout; lay1->addLayout( lay2 ); int row = -1; // Base Directory QLabel* label = new QLabel( i18n("Base directory:"), destinationPage ); lay2->addWidget( label, ++row, 0 ); QHBoxLayout* lay3 = new QHBoxLayout; lay2->addLayout( lay3, row, 1 ); m_baseDir = new QLineEdit( destinationPage ); lay3->addWidget( m_baseDir ); label->setBuddy( m_baseDir ); QPushButton* but = new QPushButton( QString::fromLatin1( ".." ), destinationPage ); lay3->addWidget( but ); but->setFixedWidth( 25 ); connect(but, &QPushButton::clicked, this, &HTMLDialog::selectDir); m_baseDir->setText( Settings::SettingsData::instance()->HTMLBaseDir() ); // Output Directory label = new QLabel( i18n("Gallery directory:"), destinationPage ); lay2->addWidget( label, ++row, 0 ); m_outputDir = new QLineEdit( destinationPage ); lay2->addWidget( m_outputDir, row, 1 ); label->setBuddy( m_outputDir ); // fully "Assembled" output Directory label = new QLabel( i18n("Output directory:"), destinationPage ); lay2->addWidget( label, ++row, 0 ); m_outputLabel = new QLabel( destinationPage ); lay2->addWidget( m_outputLabel, row, 1 ); label->setBuddy( m_outputLabel ); connect(m_baseDir, &QLineEdit::textChanged, this, &HTMLDialog::slotUpdateOutputLabel); connect(m_outputDir, &QLineEdit::textChanged, this, &HTMLDialog::slotUpdateOutputLabel); // initial text slotUpdateOutputLabel(); // Destination URL label = new QLabel( i18n("URL for final destination of .kim file:" ), destinationPage ); label->setToolTip( i18n( "<p>If you move the gallery to a remote location, set this to the destination URL.</p>" "<p>This only affects the generated <filename>.kim</filename> file.</p>" ) ); lay2->addWidget( label, ++row, 0 ); m_destURL = new QLineEdit( destinationPage ); m_destURL->setText( Settings::SettingsData::instance()->HTMLDestURL() ); lay2->addWidget( m_destURL, row, 1 ); label->setBuddy( m_destURL ); // Base URL label = new QLabel( i18n("Open gallery in browser:"), destinationPage ); lay2->addWidget( label, ++row, 0 ); m_openInBrowser = new QCheckBox(destinationPage); m_openInBrowser->setChecked(true); lay2->addWidget( m_openInBrowser, row, 1); label->setBuddy( m_openInBrowser ); lay1->addStretch( 1 ); } void HTMLDialog::slotOk() { if ( !checkVars() ) return; if( activeResolutions().count() < 1 ) { KMessageBox::sorry( nullptr, i18n( "You must select at least one resolution." ) ); return; } accept(); Settings::SettingsData::instance()->setHTMLBaseDir( m_baseDir->text() ); Settings::SettingsData::instance()->setHTMLDestURL( m_destURL->text() ); Settings::SettingsData::instance()->setHTMLCopyright( m_copyright->text() ); Settings::SettingsData::instance()->setHTMLDate( m_date->isChecked() ); Settings::SettingsData::instance()->setHTMLTheme( m_themeBox->currentIndex() ); Settings::SettingsData::instance()->setHTMLKimFile( m_generateKimFile->isChecked() ); Settings::SettingsData::instance()->setHTMLInlineMovies( m_inlineMovies->isChecked() ); Settings::SettingsData::instance()->setHTML5Video( m_html5Video->isChecked() ); Settings::SettingsData::instance()->setHTML5VideoGenerate( m_html5VideoGenerate->isChecked() ); Settings::SettingsData::instance()->setHTMLThumbSize( m_thumbSize->value() ); Settings::SettingsData::instance()->setHTMLNumOfCols( m_numOfCols->value() ); Settings::SettingsData::instance()->setHTMLSizes( activeSizes() ); Settings::SettingsData::instance()->setHTMLIncludeSelections( includeSelections() ); Generator generator( setup(), this ); generator.generate(); } void HTMLDialog::selectDir() { QString dir = QFileDialog::getExistingDirectory( this, i18n("Select base directory..."), m_baseDir->text() ); if ( !dir.isEmpty() ) m_baseDir->setText( dir ); } bool HTMLDialog::checkVars() { QString outputDir = m_baseDir->text() + QString::fromLatin1( "/" ) + m_outputDir->text(); // Ensure base dir is specified QString baseDir = m_baseDir->text(); if ( baseDir.isEmpty() ) { KMessageBox::error( this, i18n("<p>You did not specify a base directory. " "This is the topmost directory for your images. " "Under this directory you will find each generated collection " "in separate directories.</p>"), i18n("No Base Directory Specified") ); return false; } // ensure output directory is specified if ( m_outputDir->text().isEmpty() ) { KMessageBox::error( this, i18n("<p>You did not specify an output directory. " "This is a directory containing the actual images. " "The directory will be in the base directory specified above.</p>"), i18n("No Output Directory Specified") ); return false; } // ensure base dir exists QScopedPointer<KIO::StatJob> statJob( KIO::stat( QUrl::fromUserInput(baseDir), KIO::StatJob::DestinationSide, 1 /*only basic info*/)); KJobWidgets::setWindow( statJob.data(), MainWindow::Window::theMainWindow() ); if (!statJob->exec()) { KMessageBox::error( this, i18n("<p>Error while reading information about %1. " "This is most likely because the directory does not exist.</p>" "<p>The error message was: %2</p>", baseDir, statJob->errorString() ) ); return false; } KFileItem fileInfo( statJob->statResult(), QUrl::fromUserInput(baseDir) ); if ( !fileInfo.isDir() ) { KMessageBox::error( this, i18n("<p>%1 does not exist, is not a directory or " "cannot be written to.</p>", baseDir ) ); return false; } // test if destination directory exists. QScopedPointer<KIO::StatJob> existsJob( KIO::stat( QUrl::fromUserInput(outputDir), KIO::StatJob::DestinationSide, 0 /*only minimal info*/ )); KJobWidgets::setWindow( existsJob.data(), MainWindow::Window::theMainWindow() ); if ( existsJob->exec() ) { int answer = KMessageBox::warningYesNo( this, i18n("<p>Output directory %1 already exists. " "Usually, this means you should specify a new directory.</p>" "<p>Should %2 be deleted first?</p>", outputDir, outputDir ), i18n("Directory Exists"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QString::fromLatin1("html_export_delete_original_directory") ); if ( answer == KMessageBox::Yes ) { QScopedPointer<KJob> delJob (KIO::del(QUrl::fromUserInput(outputDir))); KJobWidgets::setWindow( delJob.data(), MainWindow::Window::theMainWindow() ); delJob->exec(); } else return false; } return true; } QList<ImageSizeCheckBox*> HTMLDialog::activeResolutions() const { QList<ImageSizeCheckBox*> res; for( QList<ImageSizeCheckBox*>::ConstIterator sizeIt = m_sizeCheckBoxes.begin(); sizeIt != m_sizeCheckBoxes.end(); ++sizeIt ) { if ( (*sizeIt)->isChecked() ) res << *sizeIt; } return res; } QString HTMLDialog::activeSizes() const { QString res; for( QList<ImageSizeCheckBox*>::ConstIterator sizeIt = m_sizeCheckBoxes.begin(); sizeIt != m_sizeCheckBoxes.end(); ++sizeIt ) { if ( (*sizeIt)->isChecked() ) { if (res.length() > 0) res.append(QString::fromLatin1(",")); res.append(QString::number((*sizeIt)->width())); } } return res; } QString HTMLDialog::includeSelections() const { QString sel; Setup setupChoices = setup(); for( QMap<QString,QCheckBox*>::ConstIterator it = m_whatToIncludeMap.begin() ; it != m_whatToIncludeMap.end() ; ++it ) { QString name = it.key(); if ( setupChoices.includeCategory(name) ) { if (sel.length() > 0) sel.append(QString::fromLatin1(",")); sel.append(name); } } return sel; } void HTMLDialog::populateThemesCombo() { QStringList dirs = QStandardPaths::locateAll( QStandardPaths::DataLocation, QString::fromLocal8Bit("themes/"), QStandardPaths::LocateDirectory ); int i = 0; int theme = 0; int defaultthemes = 0; Debug() << "Theme directories:"<<dirs; for(QStringList::Iterator it = dirs.begin(); it != dirs.end(); ++it) { QDir dir(*it); Debug() << "Searching themes in:"<<dir; QStringList themes = dir.entryList( QDir::Dirs | QDir::Readable ); for(QStringList::Iterator it = themes.begin(); it != themes.end(); ++it) { Debug() << " *" << *it; if(*it == QString::fromLatin1(".") || *it == QString::fromLatin1("..")) continue; QString themePath = QString::fromLatin1("%1/%2/").arg(dir.path()).arg(*it); KConfig themeconfig( QString::fromLatin1( "%1/kphotoalbum.theme").arg( themePath ), KConfig::SimpleConfig ); KConfigGroup config = themeconfig.group("theme"); QString themeName = config.readEntry( "Name" ); QString themeAuthor = config.readEntry( "Author" ); m_themeAuthors << themeAuthor; // save author to display later QString themeDefault = config.readEntry( "Default" ); QString themeDescription = config.readEntry( "Description" ); m_themeDescriptions << themeDescription; // save description to display later //m_themeBox->insertItem( i, i18n( "%1 (by %2)",themeName, themeAuthor ) ); // combined alternative m_themeBox->insertItem( i, i18n( "%1",themeName) ); m_themes.insert( i, themePath ); if (themeDefault == QString::fromLatin1("true")) { theme = i; defaultthemes++; } i++; } } if(m_themeBox->count() < 1) { KMessageBox::error( this, i18n("Could not find any themes - this is very likely an installation error" ) ); } if ( (Settings::SettingsData::instance()->HTMLTheme() >= 0) && (Settings::SettingsData::instance()->HTMLTheme() < m_themeBox->count()) ) m_themeBox->setCurrentIndex( Settings::SettingsData::instance()->HTMLTheme() ); else { m_themeBox->setCurrentIndex( theme ); if (defaultthemes > 1) KMessageBox::information( this, i18n("More than one theme is set as default, using theme %1", m_themeBox->currentText()) ); } } void HTMLDialog::displayThemeDescription(int themenr) { // SLOT: update m_themeInfo label whenever the m_theme QComboBox changes. QString outtxt = i18nc( "This is to show the author of the theme. E.g. copyright character (©) by itself will work fine on this context if no proper word is available in your language.", "by " ); outtxt.append( m_themeAuthors[themenr] ); outtxt.append( i18n( "\n " ) ); outtxt.append( m_themeDescriptions[themenr] ); m_themeInfo->setText( outtxt ); // Instead of two separate lists for authors and descriptions one could have a combined one by appending the text prior to storing within populateThemesCombo(), // however, storing author and descriptions separately might be cleaner. } void HTMLDialog::slotUpdateOutputLabel() { QString outputDir = QDir(m_baseDir->text()).filePath(m_outputDir->text()); // feedback on validity: if (outputDir == m_baseDir->text()) { m_outputLabel->setStyleSheet(QString::fromLatin1("QLabel { color : darkred; }")); outputDir.append(i18n("<p>Gallery directory cannot be empty.</p>")); } else if ( QDir(outputDir).exists()) { m_outputLabel->setStyleSheet(QString::fromLatin1("QLabel { color : darkorange; }")); outputDir.append(i18n("<p>The output directory already exists.</p>")); } else { m_outputLabel->setStyleSheet(QString::fromLatin1("QLabel { color : black; }")); } m_outputLabel->setText( outputDir ); } void HTMLDialog::slotSuggestOutputDir() { if (m_outputDir->text().isEmpty()) { // the title is often an adequate directory name: m_outputDir->setText( m_title->text() ); } } int HTMLDialog::exec(const DB::FileNameList& list) { if (list.empty()) { qWarning() << "HTMLDialog called without images for export"; return false; } m_list = list; return QDialog::exec(); } Setup HTMLGenerator::HTMLDialog::setup() const { Setup setup; setup.setTitle( m_title->text() ); setup.setBaseDir( m_baseDir->text() ); if (m_openInBrowser->isEnabled()) { setup.setBaseURL( m_baseDir->text() ); } setup.setDestURL( m_destURL->text() ); setup.setOutputDir( m_outputDir->text() ); setup.setThumbSize( m_thumbSize->value() ); setup.setCopyright( m_copyright->text() ); setup.setDate( m_date->isChecked() ); setup.setDescription( m_description->toPlainText() ); setup.setNumOfCols( m_numOfCols->value() ); setup.setGenerateKimFile( m_generateKimFile->isChecked() ); setup.setThemePath( m_themes[m_themeBox->currentIndex()] ); for( QMap<QString,QCheckBox*>::ConstIterator includeIt = m_whatToIncludeMap.begin(); includeIt != m_whatToIncludeMap.end(); ++includeIt ) { setup.setIncludeCategory( includeIt.key(), includeIt.value()->isChecked() ); } setup.setImageList(m_list); setup.setResolutions( activeResolutions() ); setup.setInlineMovies( m_inlineMovies->isChecked() ); setup.setHtml5Video( m_html5Video->isChecked() ); setup.setHtml5VideoGenerate( m_html5VideoGenerate->isChecked() ); return setup; } -#include "HTMLDialog.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ThumbnailBuilder.cpp b/ImageManager/ThumbnailBuilder.cpp index c9694b4a..9f58466b 100644 --- a/ImageManager/ThumbnailBuilder.cpp +++ b/ImageManager/ThumbnailBuilder.cpp @@ -1,132 +1,131 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "ThumbnailBuilder.h" #include <KLocalizedString> #include "ImageManager/ThumbnailCache.h" #include "MainWindow/StatusBar.h" #include "ThumbnailView/CellGeometry.h" #include "ImageManager/AsyncLoader.h" #include "DB/ImageDB.h" #include "PreloadRequest.h" #include <QTimer> #include <DB/ImageInfoPtr.h> ImageManager::ThumbnailBuilder* ImageManager::ThumbnailBuilder::s_instance = nullptr; ImageManager::ThumbnailBuilder::ThumbnailBuilder( MainWindow::StatusBar* statusBar, QObject* parent ) :QObject( parent ), m_statusBar( statusBar ), m_isBuilding( false ) { connect(m_statusBar, &MainWindow::StatusBar::cancelRequest, this, &ThumbnailBuilder::cancelRequests); s_instance = this; m_startBuildTimer = new QTimer(this); m_startBuildTimer->setSingleShot(true); connect(m_startBuildTimer, &QTimer::timeout, this, &ThumbnailBuilder::doThumbnailBuild); } void ImageManager::ThumbnailBuilder::cancelRequests() { ImageManager::AsyncLoader::instance()->stop( this, ImageManager::StopAll ); m_isBuilding = false; m_statusBar->setProgressBarVisible(false); m_startBuildTimer->stop(); } void ImageManager::ThumbnailBuilder::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& /*image*/) { const DB::FileName fileName = request->databaseFileName(); const QSize fullSize = request->fullSize(); if ( fullSize.width() != -1 ) { DB::ImageInfoPtr info = DB::ImageDB::instance()->info( fileName ); info->setSize( fullSize ); } m_statusBar->setProgress( ++m_count ); } void ImageManager::ThumbnailBuilder::buildAll( ThumbnailBuildStart when ) { ImageManager::ThumbnailCache::instance()->flush(); scheduleThumbnailBuild( DB::ImageDB::instance()->images(), when ); } ImageManager::ThumbnailBuilder* ImageManager::ThumbnailBuilder::instance() { Q_ASSERT( s_instance ); return s_instance; } void ImageManager::ThumbnailBuilder::buildMissing() { const DB::FileNameList images = DB::ImageDB::instance()->images(); DB::FileNameList needed; for ( const DB::FileName& fileName : images ) { if ( ! ImageManager::ThumbnailCache::instance()->contains( fileName ) ) needed.append( fileName ); } scheduleThumbnailBuild( needed, StartDelayed ); } void ImageManager::ThumbnailBuilder::scheduleThumbnailBuild( const DB::FileNameList& list, ThumbnailBuildStart when ) { if ( list.count() == 0 ) return; if ( m_isBuilding ) cancelRequests(); m_startBuildTimer->start( when == StartNow ? 0 : 5000 ); m_thumbnailsToBuild = list; } void ImageManager::ThumbnailBuilder::doThumbnailBuild() { m_isBuilding = true; int numberOfThumbnailsToBuild = 0; m_count = 0; m_statusBar->startProgress( i18n("Building thumbnails"), qMax( m_thumbnailsToBuild.size() - 1, 1 ) ); for (const DB::FileName& fileName : m_thumbnailsToBuild ) { DB::ImageInfoPtr info = fileName.info(); if ( info->isNull()) { m_count++; continue; } ImageManager::ImageRequest* request = new ImageManager::PreloadRequest( fileName, ThumbnailView::CellGeometry::preferredIconSize(), info->angle(), this ); request->setIsThumbnailRequest(true); request->setPriority( ImageManager::BuildThumbnails ); if (ImageManager::AsyncLoader::instance()->load( request )) ++numberOfThumbnailsToBuild; } if (numberOfThumbnailsToBuild == 0) m_statusBar->setProgressBarVisible(false); } void ImageManager::ThumbnailBuilder::requestCanceled() { m_statusBar->setProgress( ++m_count ); } -#include "ThumbnailBuilder.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/Export.cpp b/ImportExport/Export.cpp index 599f5f5e..b1ed89a4 100644 --- a/ImportExport/Export.cpp +++ b/ImportExport/Export.cpp @@ -1,414 +1,410 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 <QApplication> #include <QBuffer> #include <QCheckBox> #include <QFileInfo> #include <QGroupBox> #include <QLayout> #include <QProgressDialog> #include <QRadioButton> #include <QSpinBox> #include <QVBoxLayout> #include <QFileDialog> #include <KHelpClient> #include <KLocalizedString> #include <KMessageBox> #include <KZip> #include <DB/FileNameList.h> #include <DB/ImageInfo.h> #include <ImageManager/AsyncLoader.h> #include <Utilities/Util.h> #include <KConfigGroup> #include <QDialogButtonBox> #include <QPushButton> #include <QFileDialog> #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( "<p>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.</p>" "<p>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.</p>" ); mp_compress->setWhatsThis( txt ); txt = i18n( "<p>Generate thumbnail images</p>" ); mp_generateThumbnails->setWhatsThis( txt ); txt = i18n( "<p>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.</p>" ); mp_enforeMaxSize->setWhatsThis( txt ); mp_maxSize->setWhatsThis( txt ); txt = i18n("<p>When exporting images, bear in mind that there are two things the " "person importing these images again will need:<br/>" "1) meta information (image content, date etc.)<br/>" "2) the images themselves.</p>" "<p>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.</p>"); 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( "<p>Other KPhotoAlbum users may now load the import file into their database, by choosing <b>import</b> in " "the file menu.</p>" "<p>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 <b>/etc/httpd/mime.types</b> or similar:" "<pre>application/vnd.kde.kphotoalbum-import kim</pre>" "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.</p>" ); KMessageBox::information( nullptr, txt, i18n("How to Use the Export File"), QString::fromLatin1("export_how_to_use_the_export_file") ); } - - - -#include "Export.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportDialog.cpp b/ImportExport/ImportDialog.cpp index c1f179cf..a68e9b6f 100644 --- a/ImportExport/ImportDialog.cpp +++ b/ImportExport/ImportDialog.cpp @@ -1,405 +1,404 @@ -/* Copyright (C) 2003-2016 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "ImportDialog.h" #include <QCheckBox> #include <QComboBox> #include <QFileDialog> #include <QGridLayout> #include <QHBoxLayout> #include <QLabel> #include <QLineEdit> #include <QPixmap> #include <QPushButton> #include <QScrollArea> #include <QDir> #include <KLocalizedString> #include <KMessageBox> #include <KHelpClient> #include <DB/ImageInfo.h> #include <Settings/SettingsData.h> #include <XMLDB/Database.h> #include "ImageRow.h" #include "ImportMatcher.h" #include "KimFileReader.h" #include "MD5CheckPage.h" using Utilities::StringSet; class QPushButton; using namespace ImportExport; ImportDialog::ImportDialog( QWidget* parent ) :KAssistantDialog( parent ), m_hasFilled( false ), m_md5CheckPage(nullptr) { } bool ImportDialog::exec( KimFileReader* kimFileReader, const QUrl &kimFileURL ) { m_kimFileReader = kimFileReader; if (kimFileURL.isLocalFile()) { QDir cwd; // convert relative local path to absolute m_kimFile = QUrl::fromLocalFile(cwd.absoluteFilePath( kimFileURL.toLocalFile() ) ) .adjusted(QUrl::NormalizePathSegments); } else { m_kimFile = kimFileURL; } QByteArray indexXML = m_kimFileReader->indexXML(); if ( indexXML.isNull() ) return false; bool ok = readFile(indexXML); if ( !ok ) return false; setupPages(); return KAssistantDialog::exec() ; } bool ImportDialog::readFile(const QByteArray& data) { XMLDB::ReaderPtr reader = XMLDB::ReaderPtr(new XMLDB::XmlReader); reader->addData(data); XMLDB::ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KimDaBa-export")); if ( !info.isStartToken ) reader->complainStartElementExpected(QString::fromUtf8("KimDaBa-export")); // Read source QString source = reader->attribute( QString::fromUtf8( "location" ) ).toLower(); if ( source != QString::fromLatin1( "inline" ) && source != QString::fromLatin1( "external" ) ) { KMessageBox::error( this, i18n("<p>XML file did not specify the source of the images, " "this is a strong indication that the file is corrupted</p>" ) ); return false; } m_externalSource = ( source == QString::fromLatin1( "external" ) ); // Read base url m_baseUrl = QUrl::fromUserInput( reader->attribute( QString::fromLatin1( "baseurl" ) )); while ( reader->readNextStartOrStopElement(QString::fromUtf8("image")).isStartToken) { const DB::FileName fileName = DB::FileName::fromRelativePath(reader->attribute(QString::fromUtf8( "file" ))); DB::ImageInfoPtr info = XMLDB::Database::createImageInfo( fileName, reader ); m_images.append( info ); } // the while loop already read the end element, so we tell readEndElement to not read the next token: reader->readEndElement(false); return true; } void ImportDialog::setupPages() { createIntroduction(); createImagesPage(); createDestination(); createCategoryPages(); connect(this, &ImportDialog::currentPageChanged, this, &ImportDialog::updateNextButtonState); QPushButton *helpButton = buttonBox()->button(QDialogButtonBox::Help); connect(helpButton, &QPushButton::clicked, this, &ImportDialog::slotHelp); } void ImportDialog::createIntroduction() { QString txt = i18n( "<h1><font size=\"+2\">Welcome to KPhotoAlbum Import</font></h1>" "This wizard will take you through the steps of an import operation. The steps are: " "<ol><li>First you must select which images you want to import from the export file. " "You do so by selecting the checkbox next to the image.</li>" "<li>Next you must tell KPhotoAlbum in which directory to put the images. This directory must " "of course be below the directory root KPhotoAlbum uses for images. " "KPhotoAlbum will take care to avoid name clashes</li>" "<li>The next step is to specify which categories you want to import (People, Places, ... ) " "and also tell KPhotoAlbum how to match the categories from the file to your categories. " "Imagine you load from a file, where a category is called <b>Blomst</b> (which is the " "Danish word for flower), then you would likely want to match this with your category, which might be " "called <b>Blume</b> (which is the German word for flower) - of course given you are German.</li>" "<li>The final steps, is matching the individual tokens from the categories. I may call myself <b>Jesper</b> " "in my image database, while you want to call me by my full name, namely <b>Jesper K. Pedersen</b>. " "In this step non matches will be highlighted in red, so you can see which tokens was not found in your " "database, or which tokens was only a partial match.</li></ol>"); QLabel* intro = new QLabel( txt, this ); intro->setWordWrap(true); addPage( intro, i18nc("@title:tab introduction page","Introduction") ); } void ImportDialog::createImagesPage() { QScrollArea* top = new QScrollArea; top->setWidgetResizable(true); QWidget* container = new QWidget; QVBoxLayout* lay1 = new QVBoxLayout( container ); top->setWidget( container ); // Select all and Deselect All buttons QHBoxLayout* lay2 = new QHBoxLayout; lay1->addLayout(lay2); QPushButton* selectAll = new QPushButton( i18n("Select All"), container ); lay2->addWidget( selectAll ); QPushButton* selectNone = new QPushButton( i18n("Deselect All"), container ); lay2->addWidget( selectNone ); lay2->addStretch( 1 ); connect(selectAll, &QPushButton::clicked, this, &ImportDialog::slotSelectAll); connect(selectNone, &QPushButton::clicked, this, &ImportDialog::slotSelectNone); QGridLayout* lay3 = new QGridLayout; lay1->addLayout( lay3 ); lay3->setColumnStretch( 2, 1 ); int row = 0; for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it, ++row ) { DB::ImageInfoPtr info = *it; ImageRow* ir = new ImageRow( info, this, m_kimFileReader, container ); lay3->addWidget( ir->m_checkbox, row, 0 ); QPixmap pixmap = m_kimFileReader->loadThumbnail( info->fileName().relative() ); if ( !pixmap.isNull() ) { QPushButton* but = new QPushButton( container ); but->setIcon( pixmap ); but->setIconSize( pixmap.size() ); lay3->addWidget( but, row, 1 ); connect(but, &QPushButton::clicked, ir, &ImageRow::showImage); } else { QLabel* label = new QLabel( info->label() ); lay3->addWidget( label, row, 1 ); } QLabel* label = new QLabel( QString::fromLatin1("<p>%1</p>").arg(info->description()) ); lay3->addWidget( label, row, 2 ); m_imagesSelect.append( ir ); } addPage( top, i18n("Select Which Images to Import") ); } void ImportDialog::createDestination() { QWidget* top = new QWidget( this ); QVBoxLayout* topLay = new QVBoxLayout( top ); QHBoxLayout* lay = new QHBoxLayout; topLay->addLayout(lay); topLay->addStretch( 1 ); QLabel* label = new QLabel( i18n( "Destination of images: " ), top ); lay->addWidget( label ); m_destinationEdit = new QLineEdit( top ); lay->addWidget( m_destinationEdit, 1 ); QPushButton* but = new QPushButton( QString::fromLatin1("..." ), top ); but->setFixedWidth( 30 ); lay->addWidget( but ); m_destinationEdit->setText( Settings::SettingsData::instance()->imageDirectory()); connect(but, &QPushButton::clicked, this, &ImportDialog::slotEditDestination); connect(m_destinationEdit, &QLineEdit::textChanged, this, &ImportDialog::updateNextButtonState); m_destinationPage = addPage( top, i18n("Destination of Images" ) ); } void ImportDialog::slotEditDestination() { QString file = QFileDialog::getExistingDirectory(this , QString(), m_destinationEdit->text()); if ( !file.isNull() ) { if ( ! QFileInfo(file).absoluteFilePath().startsWith( QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath()) ) { KMessageBox::error( this, i18n("The directory must be a subdirectory of %1", Settings::SettingsData::instance()->imageDirectory() ) ); } else if ( QFileInfo(file).absoluteFilePath().startsWith( QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath() + QString::fromLatin1("CategoryImages")) ) { KMessageBox::error( this, i18n("This directory is reserved for category images." ) ); } else { m_destinationEdit->setText( file ); updateNextButtonState(); } } } void ImportDialog::updateNextButtonState() { bool enabled = true; if ( currentPage() == m_destinationPage ) { QString dest = m_destinationEdit->text(); if ( QFileInfo( dest ).isFile() ) enabled = false; else if ( ! QFileInfo(dest).absoluteFilePath().startsWith( QFileInfo(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath()) ) enabled = false; } setValid( currentPage(), enabled ); } void ImportDialog::createCategoryPages() { QStringList categories; DB::ImageInfoList images = selectedImages(); for( DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it ) { DB::ImageInfoPtr info = *it; QStringList categoriesForImage = info->availableCategories(); Q_FOREACH( const QString &category, categoriesForImage ) { if ( !categories.contains( category ) && category != i18n( "Folder" ) && category != i18n( "Tokens" ) && category != i18n( "Media Type" )) categories.append( category ); } } if ( !categories.isEmpty() ) { m_categoryMatcher = new ImportMatcher( QString(), QString(), categories, DB::ImageDB::instance()->categoryCollection()->categoryNames(), false, this ); m_categoryMatcherPage = addPage( m_categoryMatcher, i18n("Match Categories") ); QWidget* dummy = new QWidget; m_dummy = addPage( dummy, QString() ); } else { m_categoryMatcherPage = nullptr; possiblyAddMD5CheckPage(); } } ImportMatcher* ImportDialog::createCategoryPage( const QString& myCategory, const QString& otherCategory ) { StringSet otherItems; DB::ImageInfoList images = selectedImages(); for( DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it ) { otherItems += (*it)->itemsOfCategory( otherCategory ); } QStringList myItems = DB::ImageDB::instance()->categoryCollection()->categoryForName( myCategory )->itemsInclCategories(); myItems.sort(); ImportMatcher* matcher = new ImportMatcher( otherCategory, myCategory, otherItems.toList(), myItems, true, this ); addPage( matcher, myCategory ); return matcher; } void ImportDialog::next() { if ( currentPage() == m_destinationPage ) { QString dir = m_destinationEdit->text(); if ( !QFileInfo( dir ).exists() ) { int answer = KMessageBox::questionYesNo( this, i18n("Directory %1 does not exist. Should it be created?", dir ) ); if ( answer == KMessageBox::Yes ) { bool ok = QDir().mkpath(dir); if ( !ok ) { KMessageBox::error( this, i18n("Error creating directory %1", dir ) ); return; } } else return; } } if ( !m_hasFilled && currentPage() == m_categoryMatcherPage ) { m_hasFilled = true; m_categoryMatcher->setEnabled( false ); removePage(m_dummy); ImportMatcher* matcher = nullptr; Q_FOREACH( const CategoryMatch *match, m_categoryMatcher->m_matchers ) { if ( match->m_checkbox->isChecked() ) { matcher = createCategoryPage( match->m_combobox->currentText(), match->m_text ); m_matchers.append( matcher ); } } possiblyAddMD5CheckPage(); } KAssistantDialog::next(); } void ImportDialog::slotSelectAll() { selectImage( true ); } void ImportDialog::slotSelectNone() { selectImage( false ); } void ImportDialog::selectImage( bool on ) { Q_FOREACH( ImageRow* row, m_imagesSelect ) { row->m_checkbox->setChecked( on ); } } DB::ImageInfoList ImportDialog::selectedImages() const { DB::ImageInfoList res; for( QList<ImageRow*>::ConstIterator it = m_imagesSelect.begin(); it != m_imagesSelect.end(); ++it ) { if ( (*it)->m_checkbox->isChecked() ) res.append( (*it)->m_info ); } return res; } void ImportDialog::slotHelp() { KHelpClient::invokeHelp( QString::fromLatin1( "chp-importExport" ) ); } ImportSettings ImportExport::ImportDialog::settings() { ImportSettings settings; settings.setSelectedImages( selectedImages() ); settings.setDestination( m_destinationEdit->text() ); settings.setExternalSource( m_externalSource ); settings.setKimFile( m_kimFile ); settings.setBaseURL( m_baseUrl ); if ( m_md5CheckPage ) { settings.setImportActions( m_md5CheckPage->settings() ); } for ( ImportMatcher* match : m_matchers ) settings.addCategoryMatchSetting( match->settings() ); return settings; } void ImportExport::ImportDialog::possiblyAddMD5CheckPage() { if ( MD5CheckPage::pageNeeded( settings() ) ) { m_md5CheckPage = new MD5CheckPage( settings() ); addPage(m_md5CheckPage, i18n("How to resolve clashes") ); } } -#include "ImportDialog.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportMatcher.cpp b/ImportExport/ImportMatcher.cpp index 74562984..82b1a6ff 100644 --- a/ImportExport/ImportMatcher.cpp +++ b/ImportExport/ImportMatcher.cpp @@ -1,131 +1,130 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "ImportMatcher.h" #include <qcheckbox.h> #include <qcombobox.h> #include <qlabel.h> #include <QGridLayout> #include <QVBoxLayout> #include "ImportSettings.h" #include <KLocalizedString> using namespace ImportExport; ImportMatcher::ImportMatcher( const QString& otherCategory, const QString& myCategory, const QStringList& otherItems, const QStringList& myItems, bool allowNew, QWidget* parent ) : QScrollArea( parent ), m_otherCategory( otherCategory ), m_myCategory( myCategory ) { setWidgetResizable(true); QWidget* top = new QWidget( viewport() ); QVBoxLayout* layout = new QVBoxLayout( top ); QWidget* grid = new QWidget; layout->addWidget( grid ); layout->addStretch(1); QGridLayout* gridLay = new QGridLayout(grid); gridLay->setColumnStretch( 1, 1 ); setWidget( top ); QLabel* label = new QLabel( i18n("Key in file"), grid ); gridLay->addWidget( label, 0,0 ); QPalette pal = label->palette(); QColor col = pal.color( QPalette::Background); label->setAutoFillBackground(true); pal.setColor( QPalette::Background, pal.color( QPalette::Foreground ) ); pal.setColor( QPalette::Foreground, col ); label->setPalette( pal ); label->setAlignment( Qt::AlignCenter ); label = new QLabel( i18n("Key in your database"), grid ); label->setAutoFillBackground(true); gridLay->addWidget( label, 0, 1 ); label->setPalette( pal ); label->setAlignment( Qt::AlignCenter ); int row = 1; for( QStringList::ConstIterator it = otherItems.begin(); it != otherItems.end(); ++it ) { CategoryMatch* match = new CategoryMatch( allowNew, *it, myItems, grid, gridLay, row++ ); m_matchers.append( match ); } } CategoryMatch::CategoryMatch( bool allowNew, const QString& kimFileItem, QStringList myItems, QWidget* parent, QGridLayout* grid, int row ) { m_checkbox = new QCheckBox( kimFileItem, parent ); m_text = kimFileItem; // We can't just use QCheckBox::text() as Qt adds accelerators. m_checkbox->setChecked( true ); grid->addWidget( m_checkbox, row, 0 ); m_combobox = new QComboBox; m_combobox->setEditable( allowNew ); myItems.sort(); m_combobox->addItems( myItems ); QObject::connect(m_checkbox, &QCheckBox::toggled, m_combobox, &QComboBox::setEnabled); grid->addWidget( m_combobox, row, 1 ); if ( myItems.contains( kimFileItem ) ) { m_combobox->setCurrentIndex( myItems.indexOf(kimFileItem) ); } else { // This item was not in my database QString match; for( QStringList::ConstIterator it = myItems.constBegin(); it != myItems.constEnd(); ++it ) { if ( (*it).contains( kimFileItem ) || kimFileItem.contains( *it ) ) { // Either my item was a substring of the kim item or the other way around (Jesper is a substring of Jesper Pedersen) if ( match.isEmpty() ) match = *it; else { match.clear(); break; } } } if ( ! match.isEmpty() ) { // there was a single substring match m_combobox->setCurrentIndex( myItems.indexOf(match) ); } else { // Either none or multiple items matches if ( allowNew ) { m_combobox->addItem(kimFileItem); m_combobox->setCurrentIndex( m_combobox->count()-1 ); } else m_checkbox->setChecked( false ); } QPalette pal = m_checkbox->palette(); pal.setColor( QPalette::ButtonText, Qt::red ); m_checkbox->setPalette( pal ); } } ImportExport::CategoryMatchSetting ImportExport::ImportMatcher::settings() { CategoryMatchSetting res( m_myCategory, m_otherCategory ); for ( CategoryMatch* match : m_matchers ) { if ( match->m_checkbox->isChecked() ) res.add( match->m_combobox->currentText(),match->m_text ); } return res; } -#include "ImportMatcher.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/MiniViewer.cpp b/ImportExport/MiniViewer.cpp index c69f65a0..ccec1e78 100644 --- a/ImportExport/MiniViewer.cpp +++ b/ImportExport/MiniViewer.cpp @@ -1,74 +1,73 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "MiniViewer.h" #include <KLocalizedString> #include <qlabel.h> #include <qlayout.h> #include <qimage.h> #include "DB/ImageInfo.h" #include <qmatrix.h> #include <QDialogButtonBox> #include <QPushButton> using namespace ImportExport; MiniViewer* MiniViewer::s_instance = nullptr; void MiniViewer::show( QImage img, DB::ImageInfoPtr info, QWidget* parent ) { if ( !s_instance ) s_instance = new MiniViewer( parent ); if ( info->angle() != 0 ) { QMatrix matrix; matrix.rotate( info->angle() ); img = img.transformed( matrix ); } if ( img.width() > 800 || img.height() > 600 ) img = img.scaled( 800, 600, Qt::KeepAspectRatio ); s_instance->m_pixmap->setPixmap( QPixmap::fromImage(img) ); s_instance->QDialog::show(); s_instance->raise(); } void MiniViewer::closeEvent( QCloseEvent* ) { slotClose(); } void MiniViewer::slotClose() { s_instance = nullptr; deleteLater(); } MiniViewer::MiniViewer( QWidget* parent ): QDialog( parent ) { QVBoxLayout* vlay = new QVBoxLayout( this ); m_pixmap = new QLabel( this ); vlay->addWidget( m_pixmap ); QDialogButtonBox* box = new QDialogButtonBox( QDialogButtonBox::Close, this ); box->button(QDialogButtonBox::Close)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(box, &QDialogButtonBox::rejected, this, &MiniViewer::slotClose); vlay->addWidget( box ); } -#include "MiniViewer.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/AutoStackImages.cpp b/MainWindow/AutoStackImages.cpp index 01eecec0..71fcae06 100644 --- a/MainWindow/AutoStackImages.cpp +++ b/MainWindow/AutoStackImages.cpp @@ -1,328 +1,327 @@ -/* Copyright (C) 2010 Miika Turkia <miika.turkia@gmail.com> +/* Copyright (C) 2010-2018 Miika Turkia <miika.turkia@gmail.com> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "AutoStackImages.h" #include <QApplication> #include <QCheckBox> #include <QDebug> #include <QDialogButtonBox> #include <QEventLoop> #include <QGroupBox> #include <QLabel> #include <QLayout> #include <QPushButton> #include <QRadioButton> #include <QSpinBox> #include <QVBoxLayout> #include <KLocalizedString> #include <DB/FileInfo.h> #include <DB/ImageDate.h> #include <DB/ImageDB.h> #include <DB/ImageInfo.h> #include <DB/MD5Map.h> #include <MainWindow/Window.h> #include <Utilities/ShowBusyCursor.h> #include <Utilities/Util.h> using namespace MainWindow; AutoStackImages::AutoStackImages( QWidget* parent, const DB::FileNameList& list ) :QDialog( parent ), m_list( list ) { setWindowTitle( i18n("Automatically Stack Images" ) ); QWidget* top = new QWidget; QVBoxLayout* lay1 = new QVBoxLayout( top ); setLayout(lay1); QWidget* containerMd5 = new QWidget( this ); lay1->addWidget( containerMd5 ); QHBoxLayout* hlayMd5 = new QHBoxLayout( containerMd5 ); m_matchingMD5 = new QCheckBox( i18n( "Stack images with identical MD5 sum") ); m_matchingMD5->setChecked( false ); hlayMd5->addWidget( m_matchingMD5 ); QWidget* containerFile = new QWidget( this ); lay1->addWidget( containerFile ); QHBoxLayout* hlayFile = new QHBoxLayout( containerFile ); m_matchingFile = new QCheckBox( i18n( "Stack images based on file version detection") ); m_matchingFile->setChecked( true ); hlayFile->addWidget( m_matchingFile ); m_origTop = new QCheckBox( i18n( "Original to top") ); m_origTop ->setChecked( false ); hlayFile->addWidget( m_origTop ); QWidget* containerContinuous = new QWidget( this ); lay1->addWidget( containerContinuous ); QHBoxLayout* hlayContinuous = new QHBoxLayout( containerContinuous ); //FIXME: This is hard to translate because of the split sentence. It is better //to use a single sentence here like "Stack images that are (were?) shot //within this time:" and use the spin method setSuffix() to set the "seconds". //Also: Would minutes not be a more sane time unit here? (schwarzer) m_continuousShooting = new QCheckBox( i18nc( "The whole sentence should read: *Stack images that are shot within x seconds of each other*. So images that are shot in one burst are automatically stacked together. (This sentence is before the x.)", "Stack images that are shot within" ) ); m_continuousShooting->setChecked( false ); hlayContinuous->addWidget( m_continuousShooting ); m_continuousThreshold = new QSpinBox; m_continuousThreshold->setRange( 1, 999 ); m_continuousThreshold->setSingleStep( 1 ); m_continuousThreshold->setValue( 2 ); hlayContinuous->addWidget( m_continuousThreshold ); QLabel* sec = new QLabel( i18nc( "The whole sentence should read: *Stack images that are shot within x seconds of each other*. (This being the text after x.)", "seconds" ), containerContinuous ); hlayContinuous->addWidget( sec ); QGroupBox* grpOptions = new QGroupBox( i18n("AutoStacking Options") ); QVBoxLayout* grpLayOptions = new QVBoxLayout( grpOptions ); lay1->addWidget( grpOptions ); m_autostackDefault = new QRadioButton( i18n( "Include matching image to appropriate stack (if one exists)") ); m_autostackDefault->setChecked( true ); grpLayOptions->addWidget( m_autostackDefault ); m_autostackUnstack = new QRadioButton( i18n( "Unstack images from their current stack and create new one for the matches") ); m_autostackUnstack->setChecked( false ); grpLayOptions->addWidget( m_autostackUnstack ); m_autostackSkip = new QRadioButton( i18n( "Skip images that are already in a stack") ); m_autostackSkip->setChecked( false ); grpLayOptions->addWidget( m_autostackSkip ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &AutoStackImages::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &AutoStackImages::reject); lay1->addWidget(buttonBox); } /* * This function searches for images with matching MD5 sums * Matches are automatically stacked */ void AutoStackImages::matchingMD5( DB::FileNameList& toBeShown ) { QMap< DB::MD5, DB::FileNameList > tostack; DB::FileNameList showIfStacked; // Stacking all images that have the same MD5 sum // First make a map of MD5 sums with corresponding images Q_FOREACH(const DB::FileName& fileName, m_list) { DB::MD5 sum = fileName.info()->MD5Sum(); if ( DB::ImageDB::instance()->md5Map()->contains( sum ) ) { if (tostack[sum].isEmpty()) tostack.insert(sum, DB::FileNameList() << fileName); else tostack[sum].append(fileName); } } // Then add images to stack (depending on configuration options) for( QMap<DB::MD5, DB::FileNameList >::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it ) { if ( tostack[it.key()].count() > 1 ) { DB::FileNameList stack; for ( int i = 0; i < tostack[it.key()].count(); ++i ) { if ( !DB::ImageDB::instance()->getStackFor( tostack[it.key()][i]).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << tostack[it.key()][i]); else if ( m_autostackSkip->isChecked() ) continue; } showIfStacked.append( tostack[it.key()][i] ); stack.append( tostack[it.key()][i]); } if ( stack.size() > 1 ) { Q_FOREACH( const DB::FileName& a, showIfStacked ) { if ( !DB::ImageDB::instance()->getStackFor(a).isEmpty() ) Q_FOREACH( const DB::FileName& b, DB::ImageDB::instance()->getStackFor(a)) toBeShown.append( b ); else toBeShown.append(a); } DB::ImageDB::instance()->stack(stack); } showIfStacked.clear(); } } } /* * This function searches for images based on file version detection configuration. * Images that are detected to be versions of same file are stacked together. */ void AutoStackImages::matchingFile( DB::FileNameList& toBeShown ) { QMap< DB::MD5, DB::FileNameList > tostack; DB::FileNameList showIfStacked; QString modifiedFileCompString; QRegExp modifiedFileComponent; QStringList originalFileComponents; modifiedFileCompString = Settings::SettingsData::instance()->modifiedFileComponent(); modifiedFileComponent = QRegExp( modifiedFileCompString ); originalFileComponents << Settings::SettingsData::instance()->originalFileComponent(); originalFileComponents = originalFileComponents.at( 0 ).split( QString::fromLatin1(";") ); // Stacking all images based on file version detection // First round prepares the stacking Q_FOREACH( const DB::FileName& fileName, m_list ) { if ( modifiedFileCompString.length() >= 0 && fileName.relative().contains( modifiedFileComponent ) ) { for( QStringList::const_iterator it = originalFileComponents.constBegin(); it != originalFileComponents.constEnd(); ++it ) { QString tmp = fileName.relative(); tmp.replace( modifiedFileComponent, ( *it )); DB::FileName originalFileName = DB::FileName::fromRelativePath( tmp ); if ( originalFileName != fileName && m_list.contains( originalFileName ) ) { DB::MD5 sum = originalFileName.info()->MD5Sum(); if ( tostack[sum].isEmpty() ) { if ( m_origTop->isChecked() ) { tostack.insert( sum, DB::FileNameList() << originalFileName ); tostack[sum].append( fileName ); } else { tostack.insert( sum, DB::FileNameList() << fileName ); tostack[sum].append( originalFileName ); } } else tostack[sum].append(fileName); break; } } } } // Then add images to stack (depending on configuration options) for( QMap<DB::MD5, DB::FileNameList >::ConstIterator it = tostack.constBegin(); it != tostack.constEnd(); ++it ) { if ( tostack[it.key()].count() > 1 ) { DB::FileNameList stack; for ( int i = 0; i < tostack[it.key()].count(); ++i ) { if ( !DB::ImageDB::instance()->getStackFor( tostack[it.key()][i]).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << tostack[it.key()][i]); else if ( m_autostackSkip->isChecked() ) continue; } showIfStacked.append( tostack[it.key()][i] ); stack.append( tostack[it.key()][i]); } if ( stack.size() > 1 ) { Q_FOREACH( const DB::FileName& a, showIfStacked ) { if ( !DB::ImageDB::instance()->getStackFor(a).isEmpty() ) Q_FOREACH( const DB::FileName& b, DB::ImageDB::instance()->getStackFor(a)) toBeShown.append( b ); else toBeShown.append(a); } DB::ImageDB::instance()->stack(stack); } showIfStacked.clear(); } } } /* * This function searches for images that are shot within specified time frame */ void AutoStackImages::continuousShooting(DB::FileNameList &toBeShown ) { DB::ImageInfoPtr prev; Q_FOREACH(const DB::FileName& fileName, m_list) { DB::ImageInfoPtr info = fileName.info(); // Skipping images that do not have exact time stamp if ( info->date().start() != info->date().end() ) continue; if ( prev && ( prev->date().start().secsTo( info->date().start() ) < m_continuousThreshold->value() ) ) { DB::FileNameList stack; if ( !DB::ImageDB::instance()->getStackFor( prev->fileName() ).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << prev->fileName()); else if ( m_autostackSkip->isChecked() ) continue; } if ( !DB::ImageDB::instance()->getStackFor(fileName).isEmpty() ) { if ( m_autostackUnstack->isChecked() ) DB::ImageDB::instance()->unstack( DB::FileNameList() << fileName); else if ( m_autostackSkip->isChecked() ) continue; } stack.append(prev->fileName()); stack.append(info->fileName()); if ( !toBeShown.isEmpty() ) { if ( toBeShown.at( toBeShown.size() - 1 ).info()->fileName() != prev->fileName() ) toBeShown.append(prev->fileName()); } else { // if this is first insert, we have to include also the stacked images from previuous image if ( !DB::ImageDB::instance()->getStackFor( info->fileName() ).isEmpty() ) Q_FOREACH( const DB::FileName& a, DB::ImageDB::instance()->getStackFor( prev->fileName() ) ) toBeShown.append( a ); else toBeShown.append(prev->fileName()); } // Inserting stacked images from the current image if ( !DB::ImageDB::instance()->getStackFor( info->fileName() ).isEmpty() ) Q_FOREACH( const DB::FileName& a, DB::ImageDB::instance()->getStackFor(fileName)) toBeShown.append( a ); else toBeShown.append(info->fileName()); DB::ImageDB::instance()->stack(stack); } prev = info; } } void AutoStackImages::accept() { QDialog::accept(); Utilities::ShowBusyCursor dummy; DB::FileNameList toBeShown; if ( m_matchingMD5->isChecked() ) matchingMD5( toBeShown ); if ( m_matchingFile->isChecked() ) matchingFile( toBeShown ); if ( m_continuousShooting->isChecked() ) continuousShooting( toBeShown ); MainWindow::Window::theMainWindow()->showThumbNails(toBeShown); } -#include "AutoStackImages.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/CategoryImagePopup.cpp b/MainWindow/CategoryImagePopup.cpp index f2e79c3b..5841248c 100644 --- a/MainWindow/CategoryImagePopup.cpp +++ b/MainWindow/CategoryImagePopup.cpp @@ -1,87 +1,85 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "CategoryImagePopup.h" #include <KLocalizedString> #include <Utilities/StringSet.h> #include <qstringlist.h> #include "Window.h" #include "DB/CategoryCollection.h" #include "Viewer/CategoryImageConfig.h" - void MainWindow::CategoryImagePopup::populate( const QImage& image, const DB::FileName& imageName ) { clear(); m_image = image; m_imageInfo = DB::ImageDB::instance()->info( imageName ); // add the categories QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH( const DB::CategoryPtr category, categories ) { if ( !category->isSpecialCategory() ) { bool categoryMenuEnabled = false; const QString categoryName = category->name(); QMenu* categoryMenu = new QMenu(this); categoryMenu->setTitle( category->name() ); // add category members Utilities::StringSet members = m_imageInfo->itemsOfCategory( categoryName ); Q_FOREACH( const QString &member, members ) { QAction* action = categoryMenu->addAction( member ); action->setObjectName( categoryName ); action->setData( member ); categoryMenuEnabled = true; } categoryMenu->setEnabled(categoryMenuEnabled); addMenu( categoryMenu ); } } // Add the Category Editor menu item QAction* action = addAction( QString::fromLatin1("viewer-show-category-editor"), this, SLOT(makeCategoryImage()) ); action->setText( i18n("Show Category Editor") ); } void MainWindow::CategoryImagePopup::slotExecuteService( QAction* action ) { QString categoryName = action->objectName(); QString memberName = action->data().toString(); if (categoryName.isNull()) return; DB::ImageDB::instance()->categoryCollection()->categoryForName( categoryName )-> setCategoryImage(categoryName, memberName, m_image); } void MainWindow::CategoryImagePopup::makeCategoryImage() { Viewer::CategoryImageConfig::instance()->setCurrentImage( m_image, m_imageInfo ); Viewer::CategoryImageConfig::instance()->show(); } MainWindow::CategoryImagePopup::CategoryImagePopup( QWidget* parent ) :QMenu( parent ) { setTitle( i18n("Make Category Image") ); connect(this, &CategoryImagePopup::triggered, this, &CategoryImagePopup::slotExecuteService); } -#include "CategoryImagePopup.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DeleteDialog.cpp b/MainWindow/DeleteDialog.cpp index 589f4301..cd8b2cd1 100644 --- a/MainWindow/DeleteDialog.cpp +++ b/MainWindow/DeleteDialog.cpp @@ -1,110 +1,109 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "DeleteDialog.h" #include <QVBoxLayout> #include <KLocalizedString> #include <qcheckbox.h> #include <qlabel.h> #include <qlayout.h> #include <QDialogButtonBox> #include <QPushButton> #include "Utilities/DeleteFiles.h" using namespace MainWindow; DeleteDialog::DeleteDialog( QWidget* parent ) : QDialog(parent) , m_list() { setWindowTitle( i18n("Removing items") ); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); QWidget* top = new QWidget; QVBoxLayout* lay1 = new QVBoxLayout( top ); mainLayout->addWidget(top); m_label = new QLabel; lay1->addWidget( m_label ); m_useTrash = new QRadioButton; lay1->addWidget( m_useTrash ); m_deleteFile = new QRadioButton; lay1->addWidget( m_deleteFile ); m_deleteFromDb = new QRadioButton; lay1->addWidget( m_deleteFromDb ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &DeleteDialog::deleteImages); connect(buttonBox, &QDialogButtonBox::rejected, this, &DeleteDialog::reject); mainLayout->addWidget(buttonBox); } int DeleteDialog::exec(const DB::FileNameList& list) { if (!list.size()) return 0; bool someFileExists = false; Q_FOREACH(const DB::FileName& file, list) { if ( file.exists() ) { someFileExists = true; break; } } const QString msg1 = i18np( "Removing 1 item", "Removing %1 items", list.size() ); const QString msg2 = i18np( "Selected item will be removed from the database.<br/>What do you want to do with the file on disk?", "Selected %1 items will be removed from the database.<br/>What do you want to do with the files on disk?", list.size() ); const QString txt = QString::fromLatin1( "<p><b><center><font size=\"+3\">%1</font><br/>%2</center></b></p>" ).arg(msg1).arg(msg2); m_useTrash->setText( i18np("Move file to Trash", "Move %1 files to Trash", list.size() ) ); m_deleteFile->setText( i18np( "Delete file from disk", "Delete %1 files from disk", list.size() ) ); m_deleteFromDb->setText( i18np( "Only remove the item from database", "Only remove %1 items from database", list.size() ) ); m_label->setText( txt ); m_list = list; // disable trash/delete options if files don't exist m_useTrash->setChecked( someFileExists ); m_useTrash->setEnabled( someFileExists ); m_deleteFile->setEnabled( someFileExists ); m_deleteFromDb->setChecked( !someFileExists ); return QDialog::exec(); } void DeleteDialog::deleteImages() { bool anyDeleted = Utilities::DeleteFiles::deleteFiles(m_list, m_deleteFile->isChecked() ? Utilities::DeleteFromDisk : m_useTrash->isChecked() ? Utilities::MoveToTrash : Utilities::BlockFromDatabase ); if ( anyDeleted ) accept(); else reject(); } -#include "DeleteDialog.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DirtyIndicator.cpp b/MainWindow/DirtyIndicator.cpp index 37806eda..d0fe6bf7 100644 --- a/MainWindow/DirtyIndicator.cpp +++ b/MainWindow/DirtyIndicator.cpp @@ -1,94 +1,93 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "DirtyIndicator.h" #include <kiconloader.h> #include <QPixmap> #include <QLabel> static MainWindow::DirtyIndicator* _instance = nullptr; bool MainWindow::DirtyIndicator::s_autoSaveDirty = false; bool MainWindow::DirtyIndicator::s_saveDirty = false; bool MainWindow::DirtyIndicator::s_suppressMarkDirty = false; MainWindow::DirtyIndicator::DirtyIndicator( QWidget* parent ) :QLabel( parent ) { m_dirtyPix = QPixmap( SmallIcon( QString::fromLatin1( "media-floppy" ) ) ); setFixedWidth( m_dirtyPix.width() + 10); _instance = this; // Might have been marked dirty even before the indicator had been created, by the database searching during loading. if ( s_saveDirty ) markDirty(); } void MainWindow::DirtyIndicator::suppressMarkDirty(bool state) { MainWindow::DirtyIndicator::s_suppressMarkDirty = state; } void MainWindow::DirtyIndicator::markDirty() { if (MainWindow::DirtyIndicator::s_suppressMarkDirty) { return; } if ( _instance ) { _instance->markDirtySlot(); } else { s_saveDirty = true; s_autoSaveDirty = true; } } void MainWindow::DirtyIndicator::markDirtySlot() { if (MainWindow::DirtyIndicator::s_suppressMarkDirty) { return; } s_saveDirty = true; s_autoSaveDirty = true; setPixmap( m_dirtyPix ); emit dirty(); } void MainWindow::DirtyIndicator::autoSaved() { s_autoSaveDirty= false; } void MainWindow::DirtyIndicator::saved() { s_autoSaveDirty = false; s_saveDirty = false; setPixmap( QPixmap() ); } bool MainWindow::DirtyIndicator::isSaveDirty() const { return s_saveDirty; } bool MainWindow::DirtyIndicator::isAutoSaveDirty() const { return s_autoSaveDirty; } - -#include "DirtyIndicator.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DuplicateMerger/DuplicateMatch.cpp b/MainWindow/DuplicateMerger/DuplicateMatch.cpp index 2d6a7c51..9c3bfbe8 100644 --- a/MainWindow/DuplicateMerger/DuplicateMatch.cpp +++ b/MainWindow/DuplicateMerger/DuplicateMatch.cpp @@ -1,164 +1,163 @@ -/* Copyright 2012 Jesper K. Pedersen <blackie@kde.org> +/* Copyright 2012-2018 Jesper K. Pedersen <blackie@kde.org> 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 <http://www.gnu.org/licenses/>. */ #include "DuplicateMatch.h" #include <QCheckBox> #include <QEvent> #include <QImage> #include <QLabel> #include <QRadioButton> #include <QToolButton> #include <QVBoxLayout> #include <QVariant> #include <KLocalizedString> #include <DB/ImageDB.h> #include <DB/ImageInfo.h> #include <DB/ImageInfoPtr.h> #include <ImageManager/AsyncLoader.h> #include <Utilities/DeleteFiles.h> #include "MergeToolTip.h" namespace MainWindow { DuplicateMatch::DuplicateMatch(const DB::FileNameList& files ) { QVBoxLayout* topLayout = new QVBoxLayout(this); QHBoxLayout* horizontalLayout = new QHBoxLayout; topLayout->addLayout(horizontalLayout); m_image = new QLabel; horizontalLayout->addWidget(m_image); QVBoxLayout* rightSideLayout = new QVBoxLayout; horizontalLayout->addSpacing(20); horizontalLayout->addLayout(rightSideLayout); horizontalLayout->addStretch(1); rightSideLayout->addStretch(1); m_merge = new QCheckBox(i18n("Merge these images")); rightSideLayout->addWidget(m_merge); m_merge->setChecked(false); connect( m_merge, SIGNAL(toggled(bool)), this, SIGNAL(selectionChanged())); QWidget* options = new QWidget; rightSideLayout->addWidget(options); QVBoxLayout* optionsLayout = new QVBoxLayout(options); connect( m_merge, SIGNAL(toggled(bool)),options, SLOT(setEnabled(bool))); QLabel* label = new QLabel(i18n("Select target:")); optionsLayout->addWidget(label); bool first = true; Q_FOREACH(const DB::FileName& fileName, files) { QHBoxLayout* lay = new QHBoxLayout; optionsLayout->addLayout(lay); QRadioButton* button = new QRadioButton(fileName.relative()); button->setProperty("data",QVariant::fromValue(fileName)); lay->addWidget(button); if ( first ) { button->setChecked(true); first = false; } QToolButton* details = new QToolButton; details->setText(i18nc("i for info","i")); details->installEventFilter(this); details->setProperty("data",QVariant::fromValue(fileName)); lay->addWidget(details); m_buttons.append(button); } rightSideLayout->addStretch(1); QFrame* line = new QFrame; line->setFrameStyle(QFrame::HLine); topLayout->addWidget(line); const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(files.first()); const int angle = info->angle(); ImageManager::ImageRequest* request = new ImageManager::ImageRequest(files.first(), QSize(300,300), angle, this); ImageManager::AsyncLoader::instance()->load(request); } void DuplicateMatch::pixmapLoaded(ImageManager::ImageRequest* /*request*/, const QImage& image) { m_image->setPixmap(QPixmap::fromImage(image)); } void DuplicateMatch::setSelected(bool b) { m_merge->setChecked(b); } bool DuplicateMatch::selected() const { return m_merge->isChecked(); } void DuplicateMatch::execute(Utilities::DeleteMethod method) { if (!m_merge->isChecked()) return; DB::FileName destination; Q_FOREACH( QRadioButton* button, m_buttons ) { if ( button->isChecked() ) { destination = button->property("data").value<DB::FileName>(); break; } } DB::FileNameList deleteList, dupList; Q_FOREACH( QRadioButton* button, m_buttons ) { if (button->isChecked()) continue; DB::FileName fileName = button->property("data").value<DB::FileName>(); DB::ImageDB::instance()->copyData(fileName, destination); // can we safely delete the file? if ( fileName != destination ) deleteList.append(fileName); else dupList.append(fileName); } Utilities::DeleteFiles::deleteFiles(deleteList, method); // remove duplicate DB-entries without removing or blocking the file: DB::ImageDB::instance()->deleteList(dupList); } bool DuplicateMatch::eventFilter(QObject* obj, QEvent* event) { if ( event->type() != QEvent::Enter ) return false; QToolButton* but; if ( !(but = qobject_cast<QToolButton*>(obj)) ) return false; const DB::FileName fileName = but->property("data").value<DB::FileName>(); MergeToolTip::instance()->requestToolTip(fileName); return false; } } // namespace MainWindow -#include "DuplicateMatch.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DuplicateMerger/DuplicateMerger.cpp b/MainWindow/DuplicateMerger/DuplicateMerger.cpp index 55767f3d..2e04807c 100644 --- a/MainWindow/DuplicateMerger/DuplicateMerger.cpp +++ b/MainWindow/DuplicateMerger/DuplicateMerger.cpp @@ -1,207 +1,205 @@ -/* Copyright 2012-2016 Jesper K. Pedersen <blackie@kde.org> +/* Copyright 2012-2018 Jesper K. Pedersen <blackie@kde.org> 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 <http://www.gnu.org/licenses/>. */ #include <QScrollArea> #include <QVBoxLayout> #include <QLabel> #include <QRadioButton> #include <QDialogButtonBox> #include <QPushButton> #include <QDebug> #include <KLocalizedString> #include "DuplicateMerger.h" #include "DuplicateMatch.h" #include "MergeToolTip.h" #include "Utilities/ShowBusyCursor.h" #include "Utilities/DeleteFiles.h" #include "DB/ImageDB.h" #include "DB/FileName.h" #include "DB/FileNameList.h" #include "DB/ImageInfo.h" #include "DB/MD5.h" namespace MainWindow { DuplicateMerger::DuplicateMerger(QWidget *parent) : QDialog(parent) { setAttribute(Qt::WA_DeleteOnClose); resize(800,600); QWidget* top = new QWidget(this); QVBoxLayout* topLayout = new QVBoxLayout(top); setLayout(topLayout); topLayout->addWidget(top); QString txt = i18n("<p>Below is a list of all images that are duplicate in your database.<br/>" "Select which you want merged, and which of the duplicates should be kept.<br/>" "The tag and description from the deleted images will be transferred to the kept image</p>"); QLabel* label = new QLabel(txt); QFont fnt = font(); fnt.setPixelSize(18); label->setFont(fnt); topLayout->addWidget(label); m_trash = new QRadioButton(i18n("Move to &trash")); m_deleteFromDisk = new QRadioButton(i18n("&Delete from disk")); QRadioButton* blockFromDB = new QRadioButton(i18n("&Block from database")); m_trash->setChecked(true); topLayout->addSpacing(10); topLayout->addWidget(m_trash); topLayout->addWidget(m_deleteFromDisk); topLayout->addWidget(blockFromDB); topLayout->addSpacing(10); QScrollArea* scrollArea = new QScrollArea; topLayout->addWidget(scrollArea); scrollArea->setWidgetResizable(true); m_container = new QWidget(scrollArea); m_scrollLayout = new QVBoxLayout(m_container); scrollArea->setWidget(m_container); m_selectionCount = new QLabel; topLayout->addWidget(m_selectionCount); QDialogButtonBox* buttonBox = new QDialogButtonBox(); m_selectAllButton = buttonBox->addButton(i18n("Select &All"), QDialogButtonBox::YesRole); m_selectNoneButton = buttonBox->addButton(i18n("Select &None"), QDialogButtonBox::NoRole); m_okButton = buttonBox->addButton(QDialogButtonBox::Ok); m_cancelButton = buttonBox->addButton(QDialogButtonBox::Cancel); connect(m_selectAllButton, SIGNAL(clicked()), this, SLOT(selectAll())); connect(m_selectNoneButton, SIGNAL(clicked()), this, SLOT(selectNone())); connect(m_okButton, SIGNAL(clicked()), this, SLOT(go())); connect(m_cancelButton, SIGNAL(clicked()), this, SLOT(reject())); topLayout->addWidget(buttonBox); findDuplicates(); } MainWindow::DuplicateMerger::~DuplicateMerger() { MergeToolTip::destroy(); } void DuplicateMerger::selectAll() { selectAll(true); } void DuplicateMerger::selectNone() { selectAll(false); } void DuplicateMerger::go() { Utilities::DeleteMethod method = Utilities::BlockFromDatabase; if (m_trash->isChecked()) { method = Utilities::MoveToTrash; } else if (m_deleteFromDisk->isChecked()) { method = Utilities::DeleteFromDisk; } Q_FOREACH( DuplicateMatch* selector, m_selectors) { selector->execute(method); } accept(); } void DuplicateMerger::updateSelectionCount() { int total = 0; int selected = 0; Q_FOREACH( DuplicateMatch* selector, m_selectors) { ++total; if (selector->selected()) ++selected; } m_selectionCount->setText(i18n("%1 of %2 selected", selected, total)); m_okButton->setEnabled(selected > 0); } void DuplicateMerger::findDuplicates() { Utilities::ShowBusyCursor dummy; Q_FOREACH( const DB::FileName& fileName, DB::ImageDB::instance()->images() ) { const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); const DB::MD5 md5 = info->MD5Sum(); m_matches[md5].append(fileName); } bool anyFound = false; for (QMap<DB::MD5, DB::FileNameList>::const_iterator it = m_matches.constBegin(); it != m_matches.constEnd(); ++it) { if (it.value().count() > 1) { addRow(it.key()); anyFound = true; } } if (! anyFound) { tellThatNoDuplicatesWereFound(); } updateSelectionCount(); } void DuplicateMerger::addRow(const DB::MD5 &md5) { DuplicateMatch* match = new DuplicateMatch( m_matches[md5]); connect( match, SIGNAL(selectionChanged()), this, SLOT(updateSelectionCount())); m_scrollLayout->addWidget(match); m_selectors.append(match); } void DuplicateMerger::selectAll(bool b) { Q_FOREACH( DuplicateMatch* selector, m_selectors) { selector->setSelected(b); } } void DuplicateMerger::tellThatNoDuplicatesWereFound() { QLabel* label = new QLabel(i18n("No duplicates found")); QFont fnt = font(); fnt.setPixelSize(30); label->setFont(fnt); m_scrollLayout->addWidget(label); m_selectAllButton->setEnabled(false); m_selectNoneButton->setEnabled(false); m_okButton->setEnabled(false); } } // namespace MainWindow -#include "DuplicateMerger.moc" - // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DuplicateMerger/MergeToolTip.cpp b/MainWindow/DuplicateMerger/MergeToolTip.cpp index 0e9142c8..7267792f 100644 --- a/MainWindow/DuplicateMerger/MergeToolTip.cpp +++ b/MainWindow/DuplicateMerger/MergeToolTip.cpp @@ -1,52 +1,51 @@ -/* Copyright 2012 Jesper K. Pedersen <blackie@kde.org> +/* Copyright 2012-2018 Jesper K. Pedersen <blackie@kde.org> 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 <http://www.gnu.org/licenses/>. */ #include "MergeToolTip.h" namespace MainWindow { MergeToolTip* MergeToolTip::s_instance = nullptr; MainWindow::MergeToolTip *MainWindow::MergeToolTip::instance() { if ( !s_instance ) s_instance = new MergeToolTip; return s_instance; } void MergeToolTip::destroy() { delete s_instance; s_instance = nullptr; } MergeToolTip::MergeToolTip(QWidget *parent) : Utilities::ToolTip(parent, Qt::Window) { } void MergeToolTip::placeWindow() { raise(); } } // namespace MainWindow -#include "MergeToolTip.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/ExternalPopup.cpp b/MainWindow/ExternalPopup.cpp index 1dd79908..80967083 100644 --- a/MainWindow/ExternalPopup.cpp +++ b/MainWindow/ExternalPopup.cpp @@ -1,213 +1,211 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "ExternalPopup.h" #include <QDebug> #include <QFile> #include <QIcon> #include <QLabel> #include <QMimeDatabase> #include <QPixmap> #include <QStringList> #include <QUrl> #include <KFileItem> #include <KLocalizedString> #include <KMimeTypeTrader> #include <KRun> #include <KService> #include <KShell> #include <DB/FileNameList.h> #include <DB/ImageInfo.h> #include <Settings/SettingsData.h> #include "RunDialog.h" #include "Window.h" void MainWindow::ExternalPopup::populate( DB::ImageInfoPtr current, const DB::FileNameList& imageList ) { m_list = imageList; m_currentInfo = current; clear(); QAction *action; QStringList list = QStringList() << i18n("Current Item") << i18n("All Selected Items") << i18n("Copy and Open"); for ( int which = 0; which < 3; ++which ) { if ( which == 0 && !current ) continue; const bool multiple = (m_list.count() > 1); const bool enabled = (which != 1 && m_currentInfo ) || (which == 1 && multiple); // Submenu QMenu *submenu = addMenu( list[which] ); submenu->setEnabled(enabled); // Fetch set of offers OfferType offers; if ( which == 0 ) offers = appInfos( DB::FileNameList() << current->fileName() ); else offers = appInfos( imageList ); for ( OfferType::const_iterator offerIt = offers.begin(); offerIt != offers.end(); ++offerIt ) { action = submenu->addAction( (*offerIt).first ); action->setObjectName( (*offerIt).first ); // Notice this is needed to find the application later! action->setIcon( QIcon::fromTheme((*offerIt).second) ); action->setData( which ); action->setEnabled( enabled ); } // A personal command action = submenu->addAction( i18n("Open With...") ); action->setObjectName( i18n("Open With...") ); // Notice this is needed to find the application later! // XXX: action->setIcon( QIcon::fromTheme((*offerIt).second) ); action->setData( which ); action->setEnabled( enabled ); // A personal command // XXX: see kdialog.h for simple usage action = submenu->addAction( i18n("Your Command Line") ); action->setObjectName( i18n("Your Command Line") ); // Notice this is needed to find the application later! // XXX: action->setIcon( QIcon::fromTheme((*offerIt).second) ); action->setData( which ); action->setEnabled( enabled ); } } void MainWindow::ExternalPopup::slotExecuteService( QAction* action ) { QString name = action->objectName(); const StringSet apps =m_appToMimeTypeMap[name]; // get the list of arguments QList<QUrl> lst; if ( action->data() == -1 ) { return; //user clicked the title entry. (i.e: "All Selected Items") } else if ( action->data() == 1 ) { Q_FOREACH(const DB::FileName &file, m_list ) { if ( m_appToMimeTypeMap[name].contains( mimeType(file) ) ) lst.append( QUrl(file.absolute()) ); } } else if (action->data() == 2) { QString origFile = m_currentInfo->fileName().absolute(); QString newFile = origFile; QString origRegexpString = Settings::SettingsData::instance()->copyFileComponent(); QRegExp origRegexp = QRegExp(origRegexpString); QString copyFileReplacement = Settings::SettingsData::instance()->copyFileReplacementComponent(); if (origRegexpString.length() > 0) { newFile.replace(origRegexp, copyFileReplacement); QFile::copy(origFile, newFile); lst.append( QUrl::fromLocalFile(newFile) ); } else { qWarning("No settings were appropriate for modifying the file name (you must fill in the regexp field; Opening the original instead"); lst.append( QUrl::fromLocalFile(origFile) ); } } else { lst.append( QUrl(m_currentInfo->fileName().absolute())); } // get the program to run // check for the special entry for self-defined if (name == i18n("Your Command Line")) { static RunDialog* dialog = new RunDialog(MainWindow::Window::theMainWindow()); dialog->setImageList(m_list); dialog->show(); return; } // check for the special entry for self-defined if (name == i18n("Open With...")) { KRun::displayOpenWithDialog(lst, MainWindow::Window::theMainWindow()); return; } KService::List offers = KMimeTypeTrader::self()->query( *(apps.begin()), QString::fromLatin1("Application"), QString::fromLatin1("Name == '%1'").arg(name)); Q_ASSERT( offers.count() >= 1 ); KService::Ptr ptr = offers.first(); KRun::runService(*ptr, lst, MainWindow::Window::theMainWindow() ); } MainWindow::ExternalPopup::ExternalPopup( QWidget* parent ) :QMenu( parent ) { setTitle( i18n("Invoke External Program") ); connect(this, &ExternalPopup::triggered, this, &ExternalPopup::slotExecuteService); } QString MainWindow::ExternalPopup::mimeType( const DB::FileName& file ) { QMimeDatabase db; return db.mimeTypeForFile(file.absolute(), QMimeDatabase::MatchExtension).name(); } Utilities::StringSet MainWindow::ExternalPopup::mimeTypes( const DB::FileNameList& files ) { StringSet res; StringSet extensions; Q_FOREACH(const DB::FileName &file, files ) { const DB::FileName baseFileName = file; const int extStart = baseFileName.relative().lastIndexOf(QChar::fromLatin1('.')); const QString ext = baseFileName.relative().mid(extStart); if (! extensions.contains(ext)) { res.insert( mimeType( file ) ); extensions.insert( ext ); } } return res; } MainWindow::OfferType MainWindow::ExternalPopup::appInfos(const DB::FileNameList& files ) { StringSet types = mimeTypes( files ); OfferType res; Q_FOREACH(const QString &type, types) { KService::List offers = KMimeTypeTrader::self()->query( type, QLatin1String( "Application" )); Q_FOREACH( const KService::Ptr offer, offers ) { res.insert( qMakePair( offer->name(), offer->icon() ) ); m_appToMimeTypeMap[offer->name()].insert( type ); } } return res; } bool operator<( const QPair<QString,QPixmap>& a, const QPair<QString,QPixmap>& b ) { return a.first < b.first; } - -#include "ExternalPopup.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/FeatureDialog.cpp b/MainWindow/FeatureDialog.cpp index d76343da..9bccd3f1 100644 --- a/MainWindow/FeatureDialog.cpp +++ b/MainWindow/FeatureDialog.cpp @@ -1,246 +1,246 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + #include <config-kpa-kipi.h> #include <config-kpa-kgeomap.h> #include "FeatureDialog.h" #include <QDialogButtonBox> #include <QLayout> #include <QList> #include <QProcess> #include <QPushButton> #include <QStandardPaths> #include <QTextBrowser> #include <QVBoxLayout> #include <KLocalizedString> #include <phonon/backendcapabilities.h> #include "Exif/Database.h" using namespace MainWindow; FeatureDialog::FeatureDialog( QWidget* parent ) :QDialog( parent ) { setWindowTitle( i18n("KPhotoAlbum Feature Status") ); QTextBrowser* browser = new QTextBrowser( this ); QString text = i18n("<h1>Overview</h1>" "<p>Below you may see the list of compile- and runtime features KPhotoAlbum has, and their status:</p>" "%1", featureString() ); text += i18n( "<h1>What can I do if I miss a feature?</h1>" "<p>If you compiled KPhotoAlbum yourself, then please review the sections below to learn what to install " "to get the feature in question. If on the other hand you installed KPhotoAlbum from a binary package, please tell " "whoever made the package about this defect, eventually including the information from the section below.</p>" "<p>In case you are missing a feature and you did not compile KPhotoAlbum yourself, please do consider doing so. " "It really is not that hard. If you need help compiling KPhotoAlbum, feel free to ask on the " "<a href=\"http://mail.kdab.com/mailman/listinfo/kphotoalbum\">KPhotoAlbum mailing list</a></p>" "<p>The steps to compile KPhotoAlbum can be seen on <a href=\"http://www.kphotoalbum.org/index.php?page=compile\">" "the KPhotoAlbum home page</a>. If you have never compiled a KDE application, then please ensure that " "you have the developer packages installed, in most distributions they go under names like kdelibs<i>-devel</i></p>" ); text += i18n( "<h1><a name=\"kipi\">Plug-ins support</a></h1>" "<p>KPhotoAlbum has a plug-in system with lots of extensions. You may among other things find plug-ins for:" "<ul>" "<li>Writing images to cds or dvd's</li>" "<li>Adjusting timestamps on your images</li>" "<li>Making a calendar featuring your images</li>" "<li>Uploading your images to flickr</li>" "<li>Upload your images to facebook</li>" "</ul></p>" "<p>The plug-in library is called KIPI, and may be downloaded from the " "<a href=\"http://userbase.kde.org/KIPI\">KDE Userbase Wiki</a></p>" ); text += i18n( "<h1><a name=\"database\">SQLite database support</a></h1>" "<p>KPhotoAlbum allows you to search using a certain number of EXIF tags. For this KPhotoAlbum " "needs an Sqlite database. " "In addition the qt package for sqlite (e.g.qt-sql-sqlite) must be installed.</p>"); text += i18n("<h1><a name=\"geomap\">Map view for geotagged images</a></h1>" "<p>If KPhotoAlbum has been built with support for libkgeomap, " "KPhotoAlbum can show images with GPS information on a map." "</p>"); text += i18n("<h1><a name=\"video\">Video support</a></h1>" "<p>KPhotoAlbum relies on Qt's Phonon architecture for displaying videos; this in turn relies on GStreamer. " "If this feature is not enabled for you, have a look at the " "<a href=\"http://userbase.kde.org/KPhotoAlbum#Video_Support\">KPhotoAlbum wiki article on video support</a>.</p>"); QStringList mimeTypes = supportedVideoMimeTypes(); mimeTypes.sort(); if ( mimeTypes.isEmpty() ) text += i18n( "<p>No video mime types found, which indicates that either Qt was compiled without phonon support, or there were missing codecs</p>"); else text += i18n("<p>Phonon is capable of playing movies of these mime types:<ul><li>%1</li></ul></p>", mimeTypes.join(QString::fromLatin1( "</li><li>" ) ) ); text += i18n("<h1><a name=\"videoPreview\">Video thumbnail support</a></h1>" "<p>KPhotoAlbum can use <tt>ffmpeg</tt> or <tt>MPlayer</tt> to extract thumbnails from videos. These thumbnails are used to preview " "videos in the thumbnail viewer.</p>" "<p>In the past, MPlayer (in contrast to MPlayer2) often had problems extracting the length " "of videos and also often fails to extract the thumbnails used for cycling video thumbnails. " "For that reason, you should prefer ffmpeg or MPlayer2 over MPlayer, if possible.</p>" ); text += i18n("<h1><a name=\"videoInfo\">Video metadata support</a></h1>" "<p>KPhotoAlbum can use <tt>ffprobe</tt> or <tt>MPlayer</tt> to extract length information from videos." "</p>" "<p>Correct length information is also necessary for correct rendering of video thumbnails.</p>" ); browser->setText( text ); QVBoxLayout* layout = new QVBoxLayout; layout->addWidget(browser); this->setLayout(layout); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); layout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); } QSize FeatureDialog::sizeHint() const { return QSize(800,600); } bool MainWindow::FeatureDialog::hasKIPISupport() { #ifdef HASKIPI return true; #else return false; #endif } bool MainWindow::FeatureDialog::hasEXIV2DBSupport() { return Exif::Database::isAvailable(); } bool MainWindow::FeatureDialog::hasGeoMapSupport() { #ifdef HAVE_KGEOMAP return true; #else return false; #endif } QString FeatureDialog::mplayerBinary() { QString mplayer = QStandardPaths::findExecutable( QString::fromLatin1("mplayer2")); if ( mplayer.isNull() ) mplayer = QStandardPaths::findExecutable( QString::fromLatin1("mplayer")); return mplayer; } bool FeatureDialog::isMplayer2() { QProcess process; process.start( mplayerBinary(), QStringList() << QString::fromLatin1("--version")); process.waitForFinished(); const QString output = QString::fromLocal8Bit(process.readAllStandardOutput().data()); return output.contains(QString::fromLatin1("MPlayer2")); } QString FeatureDialog::ffmpegBinary() { QString ffmpeg = QStandardPaths::findExecutable( QString::fromLatin1("ffmpeg")); return ffmpeg; } QString FeatureDialog::ffprobeBinary() { QString ffprobe = QStandardPaths::findExecutable( QString::fromLatin1("ffprobe")); return ffprobe; } bool FeatureDialog::hasVideoThumbnailer() { return ! ( ffmpegBinary().isEmpty() && mplayerBinary().isEmpty()); } bool FeatureDialog::hasVideoProber() { return ! ( ffprobeBinary().isEmpty() && mplayerBinary().isEmpty()); } bool MainWindow::FeatureDialog::hasAllFeaturesAvailable() { // Only answer those that are compile time tests, otherwise we will pay a penalty each time we start up. return hasKIPISupport() && hasEXIV2DBSupport() && hasGeoMapSupport() && hasVideoThumbnailer() && hasVideoProber(); } struct Data { Data() {} Data( const QString& title, const QString tag, bool featureFound ) : title( title ), tag( tag ), featureFound( featureFound ) {} QString title; QString tag; bool featureFound; }; QString MainWindow::FeatureDialog::featureString() { QList<Data> features; features << Data( i18n("Plug-ins available"), QString::fromLatin1("#kipi"), hasKIPISupport() ); features << Data( i18n( "Sqlite database support (used for EXIF searches)" ), QString::fromLatin1("#database"), hasEXIV2DBSupport() ); features << Data( i18n( "Map view for geotagged images." ), QString::fromLatin1("#geomap"), hasGeoMapSupport() ); features << Data( i18n( "Video support" ), QString::fromLatin1("#video"), !supportedVideoMimeTypes().isEmpty() ); QString result = QString::fromLatin1("<p><table>"); const QString red = QString::fromLatin1("<font color=\"red\">%1</font>"); const QString yellow = QString::fromLatin1("<font color=\"yellow\">%1</font>"); const QString yes = i18nc("Feature available","Yes"); const QString no = red.arg( i18nc("Feature not available","No") ); const QString formatString = QString::fromLatin1( "<tr><td><a href=\"%1\">%2</a></td><td><b>%3</b></td></tr>" ); for( QList<Data>::ConstIterator featureIt = features.constBegin(); featureIt != features.constEnd(); ++featureIt ) { result += formatString .arg( (*featureIt).tag ).arg( (*featureIt).title ).arg( (*featureIt).featureFound ? yes : no ); } QString thumbnailSupport = hasVideoThumbnailer() ? ( !ffmpegBinary().isEmpty() || isMplayer2() ? yes : yellow.arg(i18n("Only with MPlayer1"))) : no ; result += formatString.arg(QString::fromLatin1("#videoPreview")).arg(i18n("Video thumbnail support")).arg(thumbnailSupport); QString videoinfoSupport = hasVideoProber() ? yes : no; result += formatString.arg(QString::fromLatin1("#videoInfo")).arg(i18n("Video metadata support")).arg(videoinfoSupport); result += QString::fromLatin1( "</table></p>" ); return result; } QStringList MainWindow::FeatureDialog::supportedVideoMimeTypes() { return Phonon::BackendCapabilities::availableMimeTypes(); } -#include "FeatureDialog.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/ImageCounter.cpp b/MainWindow/ImageCounter.cpp index 5c6a6ae1..2279ccb2 100644 --- a/MainWindow/ImageCounter.cpp +++ b/MainWindow/ImageCounter.cpp @@ -1,54 +1,53 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "ImageCounter.h" #include <KLocalizedString> #include <QLabel> MainWindow::ImageCounter::ImageCounter( QWidget* parent ) :QLabel( parent ) { setText( QString::fromLatin1( "---" ) ); setMargin( 5 ); } void MainWindow::ImageCounter::setMatchCount( uint matches ) { setText( i18np( "Showing 1 thumbnail", "Showing %1 thumbnails", matches ) ); } void MainWindow::ImageCounter::setSelectionCount( uint selected ) { if ( selected > 0 ) setText( i18n( "(%1 selected)", selected ) ); else setText( QString() ); } void MainWindow::ImageCounter::setTotal( uint c ) { setText( i18n( "Total: %1", c) ); } void MainWindow::ImageCounter::showBrowserMatches(uint matches ) { setText( i18np( "1 match", "%1 matches", matches ) ); } -#include "ImageCounter.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/InvalidDateFinder.cpp b/MainWindow/InvalidDateFinder.cpp index 7a9c07f5..5925dfce 100644 --- a/MainWindow/InvalidDateFinder.cpp +++ b/MainWindow/InvalidDateFinder.cpp @@ -1,150 +1,149 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "InvalidDateFinder.h" #include <qlayout.h> #include <qradiobutton.h> #include <KLocalizedString> #include "DB/ImageInfo.h" #include "DB/ImageDB.h" #include "DB/ImageDate.h" #include "DB/FileInfo.h" #include "MainWindow/Window.h" #include <qapplication.h> #include <qeventloop.h> #include "Utilities/ShowBusyCursor.h" #include <QGroupBox> #include <KTextEdit> #include <QProgressDialog> #include <QDialogButtonBox> #include <QPushButton> #include <QVBoxLayout> using namespace MainWindow; InvalidDateFinder::InvalidDateFinder( QWidget* parent ) :QDialog( parent ) { setWindowTitle( i18n("Search for Images and Videos with Missing Dates" ) ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QGroupBox* grp = new QGroupBox( i18n("Which Images and Videos to Display") ); QVBoxLayout* grpLay = new QVBoxLayout( grp ); mainLayout->addWidget( grp ); m_dateNotTime = new QRadioButton( i18n( "Search for images and videos with a valid date but an invalid time stamp") ); m_missingDate = new QRadioButton( i18n( "Search for images and videos missing date and time" ) ); m_partialDate = new QRadioButton( i18n( "Search for images and videos with only partial dates (like 1971 vs. 11/7-1971)") ); m_dateNotTime->setChecked( true ); grpLay->addWidget( m_dateNotTime ); grpLay->addWidget( m_missingDate ); grpLay->addWidget( m_partialDate ); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &InvalidDateFinder::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &InvalidDateFinder::reject); mainLayout->addWidget(buttonBox); } void InvalidDateFinder::accept() { QDialog::accept(); Utilities::ShowBusyCursor dummy; // create the info dialog QDialog* info = new QDialog; QVBoxLayout *mainLayout = new QVBoxLayout; info->setLayout(mainLayout); info->setWindowTitle( i18n("Image Info" ) ); KTextEdit* edit = new KTextEdit( info ); mainLayout->addWidget( edit ); edit->setText( i18n("<h1>Here you may see the date changes for the displayed items.</h1>") ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); info->connect(buttonBox, &QDialogButtonBox::accepted, info, &QDialog::accept); info->connect(buttonBox, &QDialogButtonBox::rejected, info, &QDialog::reject); mainLayout->addWidget(buttonBox); // Now search for the images. const DB::FileNameList list = DB::ImageDB::instance()->images(); DB::FileNameList toBeShown; QProgressDialog dialog( nullptr); dialog.setWindowTitle(i18n("Reading file properties")); dialog.setMaximum(list.size()); dialog.setValue(0); int progress = 0; Q_FOREACH(const DB::FileName& fileName, list) { dialog.setValue( ++progress ); qApp->processEvents( QEventLoop::AllEvents ); if ( dialog.wasCanceled() ) break; if ( fileName.info()->isNull() ) continue; DB::ImageDate date = fileName.info()->date(); bool show = false; if ( m_dateNotTime->isChecked() ) { DB::FileInfo fi = DB::FileInfo::read( fileName, DB::EXIFMODE_DATE ); if ( fi.dateTime().date() == date.start().date() ) show = ( fi.dateTime().time() != date.start().time() ); if ( show ) { edit->append( QString::fromLatin1("%1:<br/>existing = %2<br>new..... = %3" ) .arg(fileName.relative()) .arg(date.start().toString()) .arg(fi.dateTime().toString()) ); } } else if ( m_missingDate->isChecked() ) { show = !date.start().isValid(); } else if ( m_partialDate->isChecked() ) { show = ( date.start() != date.end() ); } if ( show ) toBeShown.append(fileName); } if ( m_dateNotTime->isChecked() ) { info->resize( 800, 600 ); edit->setReadOnly( true ); QFont f = edit->font(); f.setFamily( QString::fromLatin1( "fixed" ) ); edit->setFont( f ); info->show(); } else delete info; Window::theMainWindow()->showThumbNails( toBeShown ); } -#include "InvalidDateFinder.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/SearchBar.cpp b/MainWindow/SearchBar.cpp index e14d9905..f1f3be9a 100644 --- a/MainWindow/SearchBar.cpp +++ b/MainWindow/SearchBar.cpp @@ -1,91 +1,91 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "SearchBar.h" #include <QLineEdit> #include <kmainwindow.h> #include <qlabel.h> #include <QKeyEvent> #include <QEvent> #include <KLocalizedString> #include <qapplication.h> #include <kactioncollection.h> MainWindow::SearchBar::SearchBar( KMainWindow* parent ) : KToolBar( parent ) { QLabel* label = new QLabel( i18nc("@label:textbox label on the search bar","Search:") + QString::fromLatin1(" ") ); addWidget( label ); m_edit = new QLineEdit( this ); m_edit->setClearButtonEnabled(true); label->setBuddy( m_edit ); addWidget( m_edit ); connect(m_edit, &QLineEdit::textChanged, this, &SearchBar::textChanged); connect(m_edit, &QLineEdit::returnPressed, this, &SearchBar::returnPressed); m_edit->installEventFilter( this ); } bool MainWindow::SearchBar::eventFilter( QObject* , QEvent* e ) { if ( e->type() == QEvent::KeyPress ) { QKeyEvent* ke = static_cast<QKeyEvent*>( e ); if ( ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down || ke->key() == Qt::Key_Left || ke->key() == Qt::Key_Right || ke->key() == Qt::Key_PageDown || ke->key() == Qt::Key_PageUp || ke->key() == Qt::Key_Home || ke->key() == Qt::Key_End ) { emit keyPressed( ke ); return true; } else if ( ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return ) { // If I don't interpret return and enter here, but simply rely // on QLineEdit itself to emit the signal, then it will // propagate to the main window, and from there be delivered to // the central widget. emit returnPressed(); return true; } else if ( ke->key() == Qt::Key_Escape ) reset(); } return false; } void MainWindow::SearchBar::reset() { m_edit->clear(); } /** * This was originally just a call to setEnabled() on the SearchBar itself, * but due to a bug in either KDE or Qt, this resulted in the bar never * being enabled again after a disable. */ void MainWindow::SearchBar::setLineEditEnabled(bool b) { m_edit->setEnabled(b); m_edit->setFocus(); } -#include "SearchBar.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/SplashScreen.cpp b/MainWindow/SplashScreen.cpp index 722c491b..e923da20 100644 --- a/MainWindow/SplashScreen.cpp +++ b/MainWindow/SplashScreen.cpp @@ -1,76 +1,75 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "SplashScreen.h" #include <QDebug> #include <QPainter> #include <QRegExp> #include <KAboutData> #include <KLocalizedString> #include <Utilities/Util.h> MainWindow::SplashScreen* MainWindow::SplashScreen::s_instance = nullptr; MainWindow::SplashScreen::SplashScreen() :QSplashScreen(Utilities::locateDataFile(QString::fromLatin1("pics/splash-large.png"))) { s_instance = this; } MainWindow::SplashScreen* MainWindow::SplashScreen::instance() { return s_instance; } void MainWindow::SplashScreen::done() { s_instance = nullptr; (void) close(); deleteLater(); } void MainWindow::SplashScreen::message( const QString& message ) { m_message = message; repaint(); } void MainWindow::SplashScreen::drawContents( QPainter * painter ) { painter->save(); QFont font = painter->font(); font.setPointSize( 10 ); painter->setFont( font ); QRect r = QRect( QPoint(20, 265), QSize( 360, 25 )); // Version String QString txt; QString version = KAboutData::applicationData().version(); txt = i18n( "%1" , version ); painter->drawText( r, Qt::AlignRight | Qt::AlignTop, txt ); // Message painter->drawText( r, Qt::AlignLeft | Qt::AlignTop, m_message ); painter->restore(); } -#include "SplashScreen.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/TokenEditor.cpp b/MainWindow/TokenEditor.cpp index e8aeec91..525fd633 100644 --- a/MainWindow/TokenEditor.cpp +++ b/MainWindow/TokenEditor.cpp @@ -1,139 +1,139 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 <qlayout.h> #include <QGridLayout> #include <QHBoxLayout> #include <QVBoxLayout> #include <KLocalizedString> #include <QPushButton> #include <qcheckbox.h> #include <qlabel.h> #include <QDialogButtonBox> #include <QPushButton> #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<QString,uint> map = DB::ImageDB::instance()->classify( DB::ImageSearchInfo(), tokensCategory->name(), DB::anyMediaType ); for( QMap<QString,uint>::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(); } -#include "TokenEditor.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/WelcomeDialog.cpp b/MainWindow/WelcomeDialog.cpp index 52463800..8aded087 100644 --- a/MainWindow/WelcomeDialog.cpp +++ b/MainWindow/WelcomeDialog.cpp @@ -1,212 +1,211 @@ -/* Copyright (C) 2003-2010 Jesper K Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K Pedersen <blackie@kde.org> 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 <Utilities/Util.h> #include <KConfigGroup> #include <KLocalizedString> #include <KMessageBox> #include <KSharedConfig> #include <KShell> #include <QDialogButtonBox> #include <QFileDialog> #include <QHBoxLayout> #include <QLabel> #include <QLayout> #include <QLineEdit> #include <QPushButton> #include <QPushButton> #include <QStandardPaths> #include <QVBoxLayout> 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( "<h1>Welcome to KPhotoAlbum</h1>" "<p>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.</p>" "<p>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.</p>" "<p>If you feel safer first trying out KPhotoAlbum with prebuilt set of images, " "press the Load Demo button.</p>" ) ); 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("<h1>KPhotoAlbum database creation</h1>" "<p>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.</p>" "<p>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.</p>" "<p>If you have an existing KPhotoAlbum database and root directory somewhere, " "point KPhotoAlbum to that directory to start using it again.</p>" ), 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("<p>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:<ul>%1</ul></p>" "<p>For details on how to solve this problem, please choose <b>Help</b>|<b>KPhotoAlbum Feature Status</b> " "from the menus.</p>", FeatureDialog::featureString() ); KMessageBox::information( this, msg, i18n("Feature Check") ); } else { KMessageBox::information( this, i18n("Congratulations: all dynamic features have been enabled."), i18n("Feature Check" ) ); } } -#include "WelcomeDialog.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Window.cpp b/MainWindow/Window.cpp index 2877cce7..3cde687f 100644 --- a/MainWindow/Window.cpp +++ b/MainWindow/Window.cpp @@ -1,1985 +1,1983 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include <config-kpa-kipi.h> #include "Window.h" #include <stdexcept> #ifdef HAVE_STDLIB_H # include <stdlib.h> #endif #include <QApplication> #include <QClipboard> #include <QCloseEvent> #include <QContextMenuEvent> #include <QCursor> #include <QDebug> #include <QElapsedTimer> #include <QDir> #include <QFrame> #include <QInputDialog> #include <QLayout> #include <QLoggingCategory> #include <QMenu> #include <QMessageBox> #include <QMimeData> #include <QMoveEvent> #include <QObject> #include <QResizeEvent> #include <QStackedWidget> #include <QTimer> #include <QVBoxLayout> #include <KActionCollection> #include <KActionMenu> #include <KEditToolBar> #include <kio_version.h> // for #if KIO_VERSION... #include <KIconLoader> #include <KLocalizedString> #include <KMessageBox> #include <KPasswordDialog> #include <KProcess> #include <KRun> #include <KSharedConfig> #include <KShortcutsDialog> #include <KStandardAction> #include <ktip.h> #include <KToggleAction> #include <KConfigGroup> #ifdef HASKIPI # include <KIPI/PluginLoader> # include <KIPI/Plugin> #endif #include <AnnotationDialog/Dialog.h> #include <BackgroundJobs/SearchForVideosWithoutLengthInfo.h> #include <BackgroundJobs/SearchForVideosWithoutVideoThumbnailsJob.h> #include <BackgroundTaskManager/JobManager.h> #include <Browser/BrowserWidget.h> #include <DateBar/DateBarWidget.h> #include <DB/CategoryCollection.h> #include <DB/ImageDateCollection.h> #include <DB/ImageDB.h> #include <DB/ImageInfo.h> #include <DB/MD5.h> #include <DB/MD5Map.h> #include <Exif/Database.h> #include <Exif/InfoDialog.h> #include <Exif/Info.h> #include <Exif/ReReadDialog.h> #include <HTMLGenerator/HTMLDialog.h> #include <ImageManager/ThumbnailBuilder.h> #include <ImageManager/ThumbnailCache.h> #include <ImportExport/Export.h> #include <ImportExport/Import.h> #ifdef HASKIPI # include <Plugins/Interface.h> #endif #include <RemoteControl/RemoteInterface.h> #include <Settings/SettingsData.h> #include <Settings/SettingsDialog.h> #include <ThumbnailView/enums.h> #include <ThumbnailView/ThumbnailFacade.h> #include <Utilities/List.h> #include <Utilities/ShowBusyCursor.h> #include <Utilities/Util.h> #include <Viewer/ViewerWidget.h> #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 if (Options::the()->benchmark()) QLoggingCategory::setFilterRules(QStringLiteral("benchmark.startup = true")); SplashScreen::instance()->message( i18n("Loading Database") ); s_instance = this; QElapsedTimer timer; timer.start(); bool gotConfigFile = load(); qCInfo(startupTime) << "Loading Database: " << timer.elapsed() << "ms."; if ( !gotConfigFile ) throw 0; 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; timer.restart(); setupMenuBar(); qCInfo(startupTime) << "setupMenuBar: " << timer.elapsed() << "ms."; timer.restart(); createSarchBar(); qCInfo(startupTime) << "createSearchBar: " << timer.elapsed() << "ms."; timer.restart(); setupStatusBar(); qCInfo(startupTime) << "setupStatusBar: " << timer.elapsed() << "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); QTimer::singleShot( 0, this, SLOT(delayedInit()) ); updateContextMenuFromSelectionSize(0); // Automatically save toolbar settings setAutoSaveSettings(); checkIfMplayerIsInstalled(); executeStartupActions(); } MainWindow::Window::~Window() { DB::ImageDB::deleteInstance(); ImageManager::ThumbnailCache::deleteInstance(); Exif::Database::deleteInstance(); } void MainWindow::Window::delayedInit() { QElapsedTimer timer; SplashScreen* splash = SplashScreen::instance(); timer.start(); setupPluginMenu(); qCInfo(startupTime) << "setupPluginMenu: " << timer.elapsed() << "ms."; if ( Settings::SettingsData::instance()->searchForImagesOnStart() || Options::the()->searchForImagesOnStart() ) { timer.restart(); splash->message( i18n("Searching for New Files") ); qApp->processEvents(); DB::ImageDB::instance()->slotRescan(); qCInfo(startupTime) << "search for new images: " << timer.elapsed() << "ms."; } if ( !Settings::SettingsData::instance()->delayLoadingPlugins() ) { timer.restart(); splash->message( i18n( "Loading Plug-ins" ) ); loadPlugins(); qCInfo(startupTime) << "loading plug-ins: " << timer.elapsed() << "ms."; } splash->done(); show(); 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 ); } 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.") ); } 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("<p><b>Delete Your Temporary Demo Database</b></p>" "<p>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.</p>" ); 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") ); } } doQuit: 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 ); 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<QUrl> 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<QUrl> 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<MainWindow::AutoStackImages> 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<MainWindow::WelcomeDialog> 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<KToggleAction>( QString::fromLatin1("orderIncr"), this, SLOT(slotOrderIncr()) ); a->setText( i18n("Show &Oldest First") ) ; a->setActionGroup(grp); a->setChecked( !Settings::SettingsData::instance()->showNewestThumbnailFirst() ); a = actionCollection()->add<KToggleAction>( 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 = actionCollection()->addAction( QString::fromLatin1("statistics"), this, SLOT(slotStatistics()) ); a->setText( i18n("Statistics...") ); m_markUntagged = actionCollection()->addAction(QString::fromUtf8("markUntagged"), this, SLOT(slotMarkUntagged())); m_markUntagged->setText(i18n("Mark As Untagged")); // Settings KStandardAction::preferences( this, SLOT(slotOptions()), actionCollection() ); KStandardAction::keyBindings( this, SLOT(slotConfigureKeyBindings()), actionCollection() ); KStandardAction::configureToolbars( this, SLOT(slotConfigureToolbars()), actionCollection() ); a = actionCollection()->addAction( QString::fromLatin1("readdAllMessages"), this, SLOT(slotReenableMessages()) ); a->setText( i18n("Enable All Messages") ); m_viewMenu = actionCollection()->add<KActionMenu>( 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<KToggleAction>( 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<KToggleAction>( 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<KToggleAction>( 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<KToggleAction>( 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 ); 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>( 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("<p>Could not open given index.xml as provided directory does not exist.<br />%1</p>", 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("<p>Given index file does not exist, do you want to create following?" "<br />%1/index.xml</p>", 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( "<p>You have configured a tag for untagged images, but either the tag itself " "or its category does not exist in the database.</p>" "<p>Please review your untagged tag setting under " "<interface>Settings|Configure KPhotoAlbum...|Categories</interface></p>")); } 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<QUrl> 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( "<p>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.</p>" "<p><b>In other words, anyone with access to the index.xml file can easily " "circumvent this password.</b></p>"), 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 <KPasswordDialog> 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 ) { 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 ); m_browser->reload(); } void MainWindow::Window::changePassword() { bool OK = ( Settings::SettingsData::instance()->password().isEmpty() ); QPointer<KPasswordDialog> 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(), pluginInfo->comment() ); } #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<KEditToolBar> 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("<p>Really enable all message boxes where you previously " "checked the do-not-show-again check box?</p>" ) ); if ( ret == KMessageBox::Yes ) KMessageBox::enableAllMessages(); } void MainWindow::Window::setupPluginMenu() { QMenu* menu = findChild<QMenu*>( QString::fromLatin1("plugins") ); if ( !menu ) { KMessageBox::error( this, i18n("<p>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.</p><p>KPhotoAlbum will continue execution, but it is not entirely unlikely that it will crash later on due to the missing make install.</p>" ), 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<QAction*> importActions; QList<QAction*> exportActions; QList<QAction*> imageActions; QList<QAction*> toolsActions; QList<QAction*> 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<QAction*> 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 { qDebug() << "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<QAction*>& actions ) { QMenu* menu = findChild<QMenu*>( QString::fromLatin1(name) ); if ( menu ) menu->setEnabled(actions.count() != 0); } void MainWindow::Window::slotImagesChanged( const QList<QUrl>& urls ) { for( QList<QUrl>::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<InvalidDateFinder> 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("<p>You have not yet configured which tag to use for indicating untagged images." "</p>" "<p>Please follow these steps to do so:" "<ul><li>In the menu bar choose <b>Settings</b></li>" "<li>From there choose <b>Configure KPhotoAlbum</b></li>" "<li>Now choose the <b>Categories</b> icon</li>" "<li>Now configure section <b>Untagged Images</b></li></ul></p>"), i18n("Feature has not been configured") ); } } void MainWindow::Window::setupStatusBar() { m_statusBar = new MainWindow::StatusBar; setStatusBar( m_statusBar ); setLocked( Settings::SettingsData::instance()->locked(), true ); } 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("<p>Unable to find ffmpeg or MPlayer on the system.</p>" "<p>Without either of these, KPhotoAlbum will not be able to display video thumbnails and video lengths. " "Please install the ffmpeg or MPlayer package</p>"), 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("<p>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.</p>"), 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("<p>Did you know that there is an Android client for KPhotoAlbum?<br/>" "With the Android client you can view your images from your desktop.</p>" "<p><a href=\"https://www.youtube.com/results?search_query=kphotoalbum+on+android\">See youtube video</a> or " "<a href=\"https://play.google.com/store/apps/details?id=org.kde.kphotoalbum\">install from google play</a></p>" ); 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 - -#include "Window.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/Interface.cpp b/Plugins/Interface.cpp index fa8f1a63..45b1045a 100644 --- a/Plugins/Interface.cpp +++ b/Plugins/Interface.cpp @@ -1,219 +1,218 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "Interface.h" #include <QByteArray> #include <QImageReader> #include <QList> #include <KFileItem> #include <KIO/PreviewJob> #include <KIPI/ImageCollection> #include <KLocalizedString> #include <Browser/BrowserWidget.h> #include <Browser/TreeCategoryModel.h> #include <DB/CategoryCollection.h> #include <DB/ImageDB.h> #include <DB/ImageInfo.h> #include <ImageManager/ThumbnailCache.h> #include <MainWindow/Window.h> #include <Plugins/CategoryImageCollection.h> #include <Plugins/ImageCollection.h> #include <Plugins/ImageCollectionSelector.h> #include <Plugins/ImageInfo.h> #include "Utilities/Util.h" #include "UploadWidget.h" namespace KIPI { class UploadWidget; } Plugins::Interface::Interface(QObject *parent, QString name ) :KIPI::Interface( parent, name ) { connect( Browser::BrowserWidget::instance(), SIGNAL(pathChanged(Browser::BreadcrumbList)), this, SLOT(pathChanged(Browser::BreadcrumbList)) ); } KIPI::ImageCollection Plugins::Interface::currentAlbum() { return KIPI::ImageCollection( new Plugins::ImageCollection( Plugins::ImageCollection::CurrentAlbum ) ); } KIPI::ImageCollection Plugins::Interface::currentSelection() { if (!MainWindow::Window::theMainWindow()->selected().isEmpty()) return KIPI::ImageCollection( new Plugins::ImageCollection( Plugins::ImageCollection::CurrentSelection ) ); else return KIPI::ImageCollection(nullptr); } QList<KIPI::ImageCollection> Plugins::Interface::allAlbums() { QList<KIPI::ImageCollection> result; DB::ImageSearchInfo context = MainWindow::Window::theMainWindow()->currentContext(); QString category = MainWindow::Window::theMainWindow()->currentBrowseCategory(); if ( category.isNull() ) category = Settings::SettingsData::instance()->albumCategory(); QMap<QString,uint> categories = DB::ImageDB::instance()->classify( context, category, DB::Image ); for( QMap<QString,uint>::iterator it = categories.begin(); it != categories.end(); ++it ) { CategoryImageCollection* col = new CategoryImageCollection( context, category, it.key() ); result.append( KIPI::ImageCollection( col ) ); } return result; } KIPI::ImageInfo Plugins::Interface::info( const QUrl &url ) { return KIPI::ImageInfo(new Plugins::ImageInfo(this, url)); } void Plugins::Interface::refreshImages( const QList<QUrl>& urls ) { emit imagesChanged( urls ); } int Plugins::Interface::features() const { return KIPI::ImagesHasComments | KIPI::ImagesHasTime | KIPI::HostSupportsDateRanges | KIPI::HostAcceptNewImages | KIPI::ImagesHasTitlesWritable | KIPI::HostSupportsTags | KIPI::HostSupportsRating | KIPI::HostSupportsThumbnails; } QAbstractItemModel * Plugins::Interface::getTagTree() const { DB::ImageSearchInfo matchAll; DB::CategoryPtr rootCategory; // since this is currently used by the geolocation plugin only, try the (localized) "Places" category first: rootCategory = DB::ImageDB::instance()->categoryCollection()->categoryForName( i18n( "Places" )); // ... if that's not available, return a category that exists: if ( !rootCategory ) rootCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial( DB::Category::TokensCategory ); return new Browser::TreeCategoryModel( rootCategory , matchAll ); } bool Plugins::Interface::addImage( const QUrl &url, QString& errmsg ) { const QString dir = url.path(); const QString root = Settings::SettingsData::instance()->imageDirectory(); if ( !dir.startsWith( root ) ) { errmsg = i18n("<p>Image needs to be placed in a sub directory of your photo album, " "which is rooted at %1. Image path was %2</p>",root , dir ); return false; } DB::ImageInfoPtr info( new DB::ImageInfo( DB::FileName::fromAbsolutePath(dir) ) ); DB::ImageInfoList list; list.append( info ); DB::ImageDB::instance()->addImages( list ); return true; } void Plugins::Interface::delImage( const QUrl &url ) { DB::ImageInfoPtr info = DB::ImageDB::instance()->info( DB::FileName::fromAbsolutePath(url.path())); if ( info ) DB::ImageDB::instance()->deleteList(DB::FileNameList() << info->fileName() ); } void Plugins::Interface::slotSelectionChanged( bool b ) { emit selectionChanged( b ); } void Plugins::Interface::pathChanged( const Browser::BreadcrumbList& path ) { static Browser::BreadcrumbList _path; if ( _path != path ) { emit currentAlbumChanged( true ); _path = path; } } KIPI::ImageCollectionSelector* Plugins::Interface::imageCollectionSelector(QWidget *parent) { return new ImageCollectionSelector( parent, this ); } KIPI::UploadWidget* Plugins::Interface::uploadWidget(QWidget* parent) { return new Plugins::UploadWidget(parent); } void Plugins::Interface::thumbnail(const QUrl &url, int size) { DB::FileName file = DB::FileName::fromAbsolutePath( url.path() ); if (size <= Settings::SettingsData::instance()->thumbnailSize() && ImageManager::ThumbnailCache::instance()->contains(file)) { // look up in the cache QPixmap thumb = ImageManager::ThumbnailCache::instance()->lookup( file ); emit gotThumbnail( url, thumb); } else { // for bigger thumbnails, fall back to previewJob: KFileItem f { url }; f.setDelayedMimeTypes( true ); KFileItemList fl; fl.append(f); KIO::PreviewJob *job = KIO::filePreview( fl, QSize(size,size)); connect(job, &KIO::PreviewJob::gotPreview, this, &Interface::gotKDEPreview); connect(job, &KIO::PreviewJob::failed, this, &Interface::failedKDEPreview); } } void Plugins::Interface::thumbnails(const QList<QUrl> &list, int size) { for (const QUrl url : list) thumbnail( url, size ); } KIPI::FileReadWriteLock *Plugins::Interface::createReadWriteLock(const QUrl &) const { return nullptr; } KIPI::MetadataProcessor *Plugins::Interface::createMetadataProcessor() const { return nullptr; } void Plugins::Interface::gotKDEPreview(const KFileItem& item, const QPixmap& pix) { emit gotThumbnail(item.url(), pix); } void Plugins::Interface::failedKDEPreview(const KFileItem& item) { emit gotThumbnail(item.url(), QPixmap()); } -#include "Interface.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/SettingsData.cpp b/Settings/SettingsData.cpp index 7bf366a6..cbfea9c0 100644 --- a/Settings/SettingsData.cpp +++ b/Settings/SettingsData.cpp @@ -1,559 +1,558 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "SettingsData.moc" #include <stdlib.h> #include <QApplication> #include <QPixmapCache> #include <QStringList> #include <KConfig> #include <KConfigGroup> #include <KLocalizedString> #include <KMessageBox> #include <KSharedConfig> #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( 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 , unsigned 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<SettingsData*>(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 ) ///////////////////////// //// Face Management //// ///////////////////////// #ifdef HAVE_KFACE property_copy(faceDetectionAccuracy , setFaceDetectionAccuracy , int, FaceManagement, 80); property_copy(faceDetectionSensitivity, setFaceDetectionSensitivity, int, FaceManagement, 80); #endif /////////////////////// //// Miscellaneous //// /////////////////////// property_copy( delayLoadingPlugins, setDelayLoadingPlugins, bool, Plug-ins, true ) property_ref_( HTMLBaseDir, setHTMLBaseDir, QString, groupForDatabase( "HTML Settings" ), QString::fromLocal8Bit(qgetenv( "HOME" )) + STR( "/public_html" ) ) property_ref_( HTMLBaseURL, setHTMLBaseURL, QString, groupForDatabase( "HTML Settings" ), STR( "file://" ) + HTMLBaseDir() ) property_ref_( HTMLDestURL, setHTMLDestURL, QString, groupForDatabase( "HTML Settings" ), STR( "file://" ) + HTMLBaseDir() ) property_ref_( HTMLCopyright, setHTMLCopyright, QString, groupForDatabase( "HTML Settings" ), STR( "" ) ) property_ref_( HTMLDate, setHTMLDate, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTMLTheme, setHTMLTheme, int, groupForDatabase( "HTML Settings" ), -1 ) property_ref_( HTMLKimFile, setHTMLKimFile, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTMLInlineMovies, setHTMLInlineMovies, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTML5Video, setHTML5Video, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTML5VideoGenerate, setHTML5VideoGenerate, int, groupForDatabase( "HTML Settings" ), true ) property_ref_( HTMLThumbSize, setHTMLThumbSize, int, groupForDatabase( "HTML Settings" ), 128 ) property_ref_( HTMLNumOfCols, setHTMLNumOfCols, int, groupForDatabase( "HTML Settings" ), 5 ) property_ref_( HTMLSizes, setHTMLSizes, QString, groupForDatabase( "HTML Settings" ), STR("") ) property_ref_( HTMLIncludeSelections, setHTMLIncludeSelections, QString, groupForDatabase( "HTML Settings" ), STR("") ) property_ref_( password, setPassword, QString, groupForDatabase( "Privacy Settings" ), STR("") ) QDate SettingsData::fromDate() const { QString date = value( "Miscellaneous", "fromDate", STR("") ); return date.isEmpty() ? QDate( QDate::currentDate().year(), 1, 1 ) : QDate::fromString( date, Qt::ISODate ); } void SettingsData::setFromDate( const QDate& date) { if (date.isValid()) setValue( "Miscellaneous", "fromDate", date.toString( Qt::ISODate ) ); } QDate SettingsData::toDate() const { QString date = value( "Miscellaneous", "toDate", STR("") ); return date.isEmpty() ? QDate( QDate::currentDate().year()+1, 1, 1 ) : QDate::fromString( date, Qt::ISODate ); } void SettingsData::setToDate( const QDate& date) { if (date.isValid()) setValue( "Miscellaneous", "toDate", date.toString( Qt::ISODate ) ); } QString SettingsData::imageDirectory() const { return m_imageDirectory; } QString SettingsData::groupForDatabase( const char* setting ) const { return STR("%1 - %2").arg( STR(setting) ).arg( imageDirectory() ); } DB::ImageSearchInfo SettingsData::currentLock() const { return DB::ImageSearchInfo::loadLock(); } void SettingsData::setCurrentLock( const DB::ImageSearchInfo& info, bool exclude ) { info.saveLock(); setValue( groupForDatabase( "Privacy Settings" ), "exclude", exclude ); } bool SettingsData::lockExcludes() const { return value( groupForDatabase( "Privacy Settings" ), "exclude", false ); } getValueFunc_( bool,locked, groupForDatabase("Privacy Settings"),"locked",false ) void SettingsData::setLocked( bool lock, bool force ) { if ( lock == locked() && !force ) return; setValue( groupForDatabase( "Privacy Settings" ), "locked", lock ); emit locked( lock, lockExcludes() ); } void SettingsData::setWindowGeometry( WindowType win, const QRect& geometry ) { setValue( "Window Geometry", win, geometry ); } QRect SettingsData::windowGeometry( WindowType win ) const { return value( "Window Geometry", win, QRect(0,0,800,600) ); } bool Settings::SettingsData::hasUntaggedCategoryFeatureConfigured() const { return DB::ImageDB::instance()->categoryCollection()->categoryNames().contains( untaggedCategory() ) && DB::ImageDB::instance()->categoryCollection()->categoryForName( untaggedCategory())->items().contains( untaggedTag() ); } double Settings::SettingsData::getThumbnailAspectRatio() const { double ratio = 1.0; switch (Settings::SettingsData::instance()->thumbnailAspectRatio()) { case Settings::Aspect_16_9: ratio = 9.0 / 16; break; case Settings::Aspect_4_3: ratio = 3.0 / 4; break; case Settings::Aspect_3_2: ratio = 2.0 / 3; break; case Settings::Aspect_9_16: ratio = 16 / 9.0; break; case Settings::Aspect_3_4: ratio = 4 / 3.0; break; case Settings::Aspect_2_3: ratio = 3 / 2.0; break; case Settings::Aspect_1_1: ratio = 1.0; break; } return ratio; } QStringList Settings::SettingsData::EXIFCommentsToStrip() { return m_EXIFCommentsToStrip; } void Settings::SettingsData::setEXIFCommentsToStrip(QStringList EXIFCommentsToStrip) { m_EXIFCommentsToStrip = EXIFCommentsToStrip; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/SettingsDialog.cpp b/Settings/SettingsDialog.cpp index 88d75d05..6680fc31 100644 --- a/Settings/SettingsDialog.cpp +++ b/Settings/SettingsDialog.cpp @@ -1,184 +1,183 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "config-kpa-kipi.h" #include "SettingsDialog.h" #include <QDialogButtonBox> #include <QPushButton> #include <QVBoxLayout> #include <KLocalizedString> #include <KSharedConfig> #include "BirthdayPage.h" #include "CategoryPage.h" #include "DatabaseBackendPage.h" #include "ExifPage.h" #include "FileVersionDetectionPage.h" #include "GeneralPage.h" #include "PluginsPage.h" #include "TagGroupsPage.h" #include "ThumbnailsPage.h" #include "ViewerPage.h" #include <Utilities/ShowBusyCursor.h> struct Data { QString title; const char* icon; QWidget* widget; }; Settings::SettingsDialog::SettingsDialog( QWidget* parent) :KPageDialog( parent ) { m_generalPage = new Settings::GeneralPage(this); m_fileVersionDetectionPage = new Settings::FileVersionDetectionPage(this); m_thumbnailsPage = new Settings::ThumbnailsPage(this); m_categoryPage = new Settings::CategoryPage(this); m_tagGroupsPage = new Settings::TagGroupsPage(this); m_viewerPage = new Settings::ViewerPage(this); #ifdef HASKIPI m_pluginsPage = new Settings::PluginsPage(this); #endif m_exifPage = new Settings::ExifPage(this); m_birthdayPage = new Settings::BirthdayPage(this); m_databaseBackendPage = new Settings::DatabaseBackendPage(this); Data data[] = { { i18n("General"), "configure-shortcuts", m_generalPage }, { i18n("File Searching & Versions"), "system-search", m_fileVersionDetectionPage }, { i18n("Thumbnail View" ), "view-preview", m_thumbnailsPage }, { i18n("Categories"), "edit-group", m_categoryPage }, { i18n("Birthdays"), "view-calendar-birthday", m_birthdayPage }, { i18n("Tag Groups" ), "view-group", m_tagGroupsPage }, { i18n("Viewer" ), "document-preview", m_viewerPage }, #ifdef HASKIPI { i18n("Plugins" ), "plugins", m_pluginsPage }, #endif { i18n("EXIF/IPTC Information" ), "document-properties", m_exifPage }, { i18n("Database backend"), "document-save", m_databaseBackendPage }, { QString(), "", 0 } }; int i = 0; while ( data[i].widget != 0 ) { KPageWidgetItem* page = new KPageWidgetItem( data[i].widget, data[i].title ); page->setHeader( data[i].title ); page->setIcon( QIcon::fromTheme( QString::fromLatin1( data[i].icon ) ) ); addPage( page ); ++i; } setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply); button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(this, &QDialog::accepted, this, &SettingsDialog::slotMyOK); connect(button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &SettingsDialog::slotMyOK); connect(this, &QDialog::rejected, m_birthdayPage, &Settings::BirthdayPage::discardChanges); setWindowTitle( i18n( "Settings" ) ); connect(m_categoryPage, &Settings::CategoryPage::categoryChangesPending, m_tagGroupsPage, &Settings::TagGroupsPage::categoryChangesPending); connect(this, &SettingsDialog::currentPageChanged, m_tagGroupsPage, &Settings::TagGroupsPage::slotPageChange); connect(this, &SettingsDialog::currentPageChanged, m_birthdayPage, &Settings::BirthdayPage::pageChange); // slot is protected -> use old style connect: connect(this, SIGNAL(rejected()), m_categoryPage, SLOT(resetCategoryLabel())); } void Settings::SettingsDialog::show() { Settings::SettingsData* opt = Settings::SettingsData::instance(); m_generalPage->loadSettings( opt ); m_fileVersionDetectionPage->loadSettings( opt ); m_thumbnailsPage->loadSettings(opt); m_tagGroupsPage->loadSettings(); m_databaseBackendPage->loadSettings(opt); m_viewerPage->loadSettings(opt); #ifdef HASKIPI m_pluginsPage->loadSettings(opt); #endif m_categoryPage->loadSettings(opt); m_exifPage->loadSettings( opt ); m_categoryPage->enableDisable( false ); m_birthdayPage->reload(); m_categoryPage->resetCategoryNamesChanged(); QDialog::show(); } // QDialog has a slotOK which we do not want to override. void Settings::SettingsDialog::slotMyOK() { Utilities::ShowBusyCursor dummy; Settings::SettingsData* opt = Settings::SettingsData::instance(); m_categoryPage->resetInterface(); m_generalPage->saveSettings( opt ); m_fileVersionDetectionPage->saveSettings( opt ); m_thumbnailsPage->saveSettings(opt); m_birthdayPage->saveSettings(); m_tagGroupsPage->saveSettings(); m_categoryPage->saveSettings( opt, m_tagGroupsPage->memberMap() ); m_viewerPage->saveSettings( opt ); #ifdef HASKIPI m_pluginsPage->saveSettings( opt ); #endif m_exifPage->saveSettings(opt); m_databaseBackendPage->saveSettings(opt); emit changed(); KSharedConfig::openConfig()->sync(); } void Settings::SettingsDialog::showBackendPage() { setCurrentPage(m_backendPage); } void Settings::SettingsDialog::keyPressEvent(QKeyEvent*) { // This prevents the dialog to be closed if the ENTER key is pressed anywhere } -#include "SettingsDialog.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/ViewerSizeConfig.cpp b/Settings/ViewerSizeConfig.cpp index 097284b5..8c51e099 100644 --- a/Settings/ViewerSizeConfig.cpp +++ b/Settings/ViewerSizeConfig.cpp @@ -1,80 +1,79 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "ViewerSizeConfig.h" #include <qcheckbox.h> #include <qlayout.h> #include <qspinbox.h> #include <KLocalizedString> #include <qlabel.h> Settings::ViewerSizeConfig::ViewerSizeConfig( const QString& title, QWidget* parent ) :QGroupBox( title, parent ) { QVBoxLayout* topLayout = new QVBoxLayout(this); setLayout(topLayout); m_fullScreen = new QCheckBox( i18n("Launch in full screen" ), this ); topLayout->addWidget(m_fullScreen); QWidget* sizeBox = new QWidget( this ); topLayout->addWidget(sizeBox); QHBoxLayout* lay = new QHBoxLayout( sizeBox ); QLabel* label = new QLabel( i18n("Size:"), sizeBox ); lay->addWidget( label ); m_width = new QSpinBox; m_width->setRange( 100, 5000 ); m_width->setSingleStep( 50 ); lay->addWidget( m_width ); label = new QLabel( QString::fromLatin1("x"), sizeBox ); lay->addWidget( label ); m_height = new QSpinBox; m_height->setRange( 100, 5000 ); m_height->setSingleStep( 50 ); lay->addWidget( m_height ); lay->addStretch( 1 ); topLayout->addStretch(1); } void Settings::ViewerSizeConfig::setSize( const QSize& size ) { m_width->setValue( size.width() ); m_height->setValue( size.height() ); } QSize Settings::ViewerSizeConfig::size() { return QSize( m_width->value(), m_height->value() ); } void Settings::ViewerSizeConfig::setLaunchFullScreen( bool b ) { m_fullScreen->setChecked( b ); } bool Settings::ViewerSizeConfig::launchFullScreen() const { return m_fullScreen->isChecked(); } -#include "ViewerSizeConfig.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/GridResizeSlider.cpp b/ThumbnailView/GridResizeSlider.cpp index 18448e77..9786caac 100644 --- a/ThumbnailView/GridResizeSlider.cpp +++ b/ThumbnailView/GridResizeSlider.cpp @@ -1,198 +1,196 @@ -/* Copyright (C) 2015 Johannes Zarl <johannes@zarl.at> +/* Copyright (C) 2015-2018 Johannes Zarl-Zierl <johannes@zarl-zierl.at> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Qt includes #include <QDebug> #include <QTimer> // KDE includes #include <KLocalizedString> #include <KMessageBox> #include <KSharedConfig> // Local includes #include <ImageManager/ThumbnailBuilder.h> #include <MainWindow/Window.h> #include <Settings/SettingsData.h> #include "CellGeometry.h" #include "GridResizeSlider.h" #include "ThumbnailModel.h" #include "ThumbnailWidget.h" #ifdef DEBUG_ResizeSlider #define Debug qDebug #else #define Debug if (0) qDebug #endif ThumbnailView::GridResizeSlider::GridResizeSlider( ThumbnailFactory* factory ) : QSlider( Qt::Horizontal ), ThumbnailComponent( factory ) { Settings::SettingsData *settings = Settings::SettingsData::instance(); setMinimum( settings->minimumThumbnailSize() ); setMaximum( settings->thumbnailSize() ); setValue( settings->actualThumbnailSize() ); // timer for event-timeout: m_timer = new QTimer( this ); m_timer->setSingleShot(true); // we have no definitive leave event when using the mousewheel -> use a timeout connect(m_timer, &QTimer::timeout, this, &GridResizeSlider::leaveGridResizingMode); connect( settings, SIGNAL(actualThumbnailSizeChanged(int)), this , SLOT(setValue(int)) ); connect(settings, &Settings::SettingsData::thumbnailSizeChanged, this, &GridResizeSlider::setMaximum); connect(this, &GridResizeSlider::sliderPressed, this, &GridResizeSlider::enterGridResizingMode); connect(this, &GridResizeSlider::valueChanged, this, &GridResizeSlider::setCellSize); // disable drawing of thumbnails while resizing: connect( this, SIGNAL(isResizing(bool)), widget(), SLOT(setExternallyResizing(bool)) ); } ThumbnailView::GridResizeSlider::~GridResizeSlider() { delete m_timer; } void ThumbnailView::GridResizeSlider::mousePressEvent( QMouseEvent* event) { Debug() << "Mouse pressed"; enterGridResizingMode(); QSlider::mousePressEvent( event ); } void ThumbnailView::GridResizeSlider::mouseReleaseEvent( QMouseEvent* event) { Debug() << "Mouse released"; leaveGridResizingMode(); QSlider::mouseReleaseEvent( event ); } void ThumbnailView::GridResizeSlider::wheelEvent( QWheelEvent* event) { // set (or reset) the timer to leave resizing mode: m_timer->start(200); Debug() << "(Re)starting timer"; if (!m_resizing) { enterGridResizingMode(); } QSlider::wheelEvent( event ); } void ThumbnailView::GridResizeSlider::enterGridResizingMode() { if (m_resizing) return; //already resizing m_resizing = true; Debug() << "Entering grid resizing mode"; ImageManager::ThumbnailBuilder::instance()->cancelRequests(); emit isResizing( true ); } void ThumbnailView::GridResizeSlider::leaveGridResizingMode() { if (!m_resizing) return; //not resizing m_resizing = false; Debug() << "Leaving grid resizing mode"; model()->beginResetModel(); cellGeometryInfo()->flushCache(); model()->endResetModel(); model()->updateVisibleRowInfo(); emit isResizing( false ); } void ThumbnailView::GridResizeSlider::setCellSize(int size) { blockSignals(true); Settings::SettingsData::instance()->setActualThumbnailSize( size ); blockSignals(false); model()->beginResetModel(); cellGeometryInfo()->calculateCellSize(); model()->endResetModel(); } void ThumbnailView::GridResizeSlider::setMaximum(int size) { // QSlider::setMaximum() is not a slot, which is why we need this slot as workaround QSlider::setMaximum(size); } void ThumbnailView::GridResizeSlider::increaseThumbnailSize() { calculateNewThumbnailSize(-1); } void ThumbnailView::GridResizeSlider::decreaseThumbnailSize() { calculateNewThumbnailSize(1); } void ThumbnailView::GridResizeSlider::calculateNewThumbnailSize(int perRowDifference) { if (! Settings::SettingsData::instance()->incrementalThumbnails()) { int code = KMessageBox::questionYesNo( MainWindow::Window::theMainWindow(), i18n("Really resize the stored thumbnail size? It will result in all thumbnails being " "regenerated!"), i18n("Really resize the thumbnails?"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QLatin1String("resizeGrid") ); if (code == KMessageBox::Yes) { KSharedConfig::openConfig()->sync(); } else { return; } } // + 6 because 5 pixels are added in ThumbnailView::CellGeometry::iconGeometry // and one additional pixel is needed for the grid. So we need to add/remove 6 pixels. int thumbnailSize = Settings::SettingsData::instance()->actualThumbnailSize() + 6; int thumbnailSpace = Settings::SettingsData::instance()->thumbnailSpace(); int viewportWidth = widget()->viewport()->width(); int perRow = viewportWidth / (thumbnailSize + thumbnailSpace); if (perRow + perRowDifference <= 0) { return; } int newWidth = (viewportWidth / (perRow + perRowDifference) - thumbnailSpace) - 6; if (newWidth < Settings::SettingsData::instance()->minimumThumbnailSize()) { return; } Settings::SettingsData::instance()->setThumbnailSize(newWidth); Settings::SettingsData::instance()->setActualThumbnailSize(newWidth); model()->beginResetModel(); cellGeometryInfo()->flushCache(); model()->endResetModel(); model()->updateVisibleRowInfo(); } -#include "GridResizeSlider.moc" - // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/SelectionInteraction.cpp b/ThumbnailView/SelectionInteraction.cpp index ea106904..a2a7a380 100644 --- a/ThumbnailView/SelectionInteraction.cpp +++ b/ThumbnailView/SelectionInteraction.cpp @@ -1,85 +1,85 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "SelectionInteraction.h" #include <QApplication> #include <QDrag> #include <QMimeData> #include <QMouseEvent> #include <QUrl> #include <DB/FileNameList.h> #include <MainWindow/Window.h> #include "CellGeometry.h" #include "ThumbnailFactory.h" #include "ThumbnailModel.h" #include "ThumbnailWidget.h" ThumbnailView::SelectionInteraction::SelectionInteraction( ThumbnailFactory* factory ) : ThumbnailComponent( factory ), m_dragInProgress( false ) { } bool ThumbnailView::SelectionInteraction::mousePressEvent( QMouseEvent* event ) { m_mousePressPos = event->pos(); const DB::FileName fileName = widget()->mediaIdUnderCursor(); m_isMouseDragOperation = widget()->isSelected(fileName) && !event->modifiers(); return m_isMouseDragOperation; } bool ThumbnailView::SelectionInteraction::mouseMoveEvent( QMouseEvent* event ) { if ( m_isMouseDragOperation ) { if ( (m_mousePressPos - event->pos()).manhattanLength() > QApplication::startDragDistance() ) startDrag(); return true; } return false; } void ThumbnailView::SelectionInteraction::startDrag() { m_dragInProgress = true; QList<QUrl> urls; Q_FOREACH(const DB::FileName& fileName, widget()->selection( NoExpandCollapsedStacks )) { urls.append( QUrl::fromLocalFile(fileName.absolute()) ); } QDrag* drag = new QDrag( MainWindow::Window::theMainWindow() ); QMimeData* data = new QMimeData; data->setUrls(urls); drag->setMimeData( data ); drag->exec(Qt::ActionMask); widget()->m_mouseHandler = &(widget()->m_mouseTrackingHandler); m_dragInProgress = false; } bool ThumbnailView::SelectionInteraction::isDragging() const { return m_dragInProgress; } -#include "SelectionInteraction.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailToolTip.cpp b/ThumbnailView/ThumbnailToolTip.cpp index ac6e5c57..145e6edc 100644 --- a/ThumbnailView/ThumbnailToolTip.cpp +++ b/ThumbnailView/ThumbnailToolTip.cpp @@ -1,122 +1,121 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "ThumbnailToolTip.h" #include <QDesktopWidget> #include <QApplication> #include <QCursor> #include "DB/ImageDB.h" #include "DB/ImageInfo.h" #include "Settings/SettingsData.h" #include "ThumbnailWidget.h" #include "Utilities/Util.h" /** \class ThumbnailToolTip This class takes care of showing tooltips for the individual items in the thumbnail view. I tried implementing this with QToolTip::maybeTip() on the iconview, but it had the disadvantages that either the tooltip would not follow the mouse( and would therefore stand on top of the image), or it flickered. */ ThumbnailView::ThumbnailToolTip::ThumbnailToolTip( ThumbnailWidget* view ) : Utilities::ToolTip(view, Qt::FramelessWindowHint | Qt::Window | Qt::X11BypassWindowManagerHint | Qt::Tool ), m_view( view ), m_widthInverse( false ), m_heightInverse( false ) { } bool ThumbnailView::ThumbnailToolTip::eventFilter( QObject* o , QEvent* event ) { if ( o == m_view->viewport() && event->type() == QEvent::Leave ) hide(); else if ( event->type() == QEvent::MouseMove || event->type() == QEvent::Wheel) { // We need this to be done through a timer, so the thumbnail view gets the wheel even first, // otherwise the fileName reported by mediaIdUnderCursor is wrong. QTimer::singleShot(0,this, SLOT(requestToolTip())); } return false; } void ThumbnailView::ThumbnailToolTip::requestToolTip() { const DB::FileName fileName = m_view->mediaIdUnderCursor(); ToolTip::requestToolTip(fileName); } void ThumbnailView::ThumbnailToolTip::setActive( bool b ) { if ( b ) { requestToolTip(); m_view->viewport()->installEventFilter( this ); } else { m_view->viewport()->removeEventFilter( this ); hide(); } } void ThumbnailView::ThumbnailToolTip::placeWindow() { // First try to set the position. QPoint pos = QCursor::pos() + QPoint( 20, 20 ); if ( m_widthInverse ) pos.setX( pos.x() - 30 - width() ); if ( m_heightInverse ) pos.setY( pos.y() - 30 - height() ); QRect geom = qApp->desktop()->screenGeometry( QCursor::pos() ); // Now test whether the window moved outside the screen if ( m_widthInverse ) { if ( pos.x() < geom.x() ) { pos.setX( QCursor::pos().x() + 20 ); m_widthInverse = false; } } else { if ( pos.x() + width() > geom.right() ) { pos.setX( QCursor::pos().x() - width() ); m_widthInverse = true; } } if ( m_heightInverse ) { if ( pos.y() < geom.y() ) { pos.setY( QCursor::pos().y() + 10 ); m_heightInverse = false; } } else { if ( pos.y() + height() > geom.bottom() ) { pos.setY( QCursor::pos().y() - 10 - height() ); m_heightInverse = true; } } move( pos ); } -#include "ThumbnailToolTip.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailWidget.cpp b/ThumbnailView/ThumbnailWidget.cpp index 87c04479..9ee5628e 100644 --- a/ThumbnailView/ThumbnailWidget.cpp +++ b/ThumbnailView/ThumbnailWidget.cpp @@ -1,435 +1,435 @@ -/* Copyright (C) 2003-2011 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "ThumbnailWidget.h" #include <QScrollBar> #include <QTimer> #include "Delegate.h" #include "ThumbnailDND.h" #include "KeyboardEventHandler.h" #include "ThumbnailFactory.h" #include "ThumbnailModel.h" #include "CellGeometry.h" -#include "ThumbnailWidget.moc" #include <math.h> #include <KLocalizedString> #include <qcursor.h> #include <qfontmetrics.h> #include <qpainter.h> #include "Browser/BrowserWidget.h" #include "DB/ImageDB.h" #include "DB/ImageInfoPtr.h" #include "Settings/SettingsData.h" #include "Utilities/Util.h" #include "SelectionMaintainer.h" /** * \class ThumbnailView::ThumbnailWidget * This is the widget which shows the thumbnails. * * In previous versions this was implemented using a QIconView, but there * simply was too many problems, so after years of tears and pains I * rewrote it. */ ThumbnailView::ThumbnailWidget::ThumbnailWidget( ThumbnailFactory* factory) :QListView(), ThumbnailComponent( factory ), m_isSettingDate(false), m_gridResizeInteraction( factory ), m_wheelResizing( false ), m_externallyResizing( false ), m_selectionInteraction( factory ), m_mouseTrackingHandler( factory ), m_mouseHandler( &m_mouseTrackingHandler ), m_dndHandler( new ThumbnailDND( factory ) ), m_pressOnStackIndicator( false ), m_keyboardHandler( new KeyboardEventHandler( factory ) ), m_videoThumbnailCycler( new VideoThumbnailCycler(model()) ) { setModel( ThumbnailComponent::model() ); setResizeMode( QListView::Adjust ); setViewMode( QListView::IconMode ); setUniformItemSizes(true); setSelectionMode( QAbstractItemView::ExtendedSelection ); // It beats me why I need to set mouse tracking on both, but without it doesn't work. viewport()->setMouseTracking( true ); setMouseTracking( true ); connect( selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(scheduleDateChangeSignal()) ); viewport()->setAcceptDrops( true ); setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOn ); setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); connect(&m_mouseTrackingHandler, &MouseTrackingInteraction::fileIdUnderCursorChanged, this, &ThumbnailWidget::fileIdUnderCursorChanged); connect(m_keyboardHandler, &KeyboardEventHandler::showSelection, this, &ThumbnailWidget::showSelection); updatePalette(); setItemDelegate( new Delegate(factory, this) ); connect( selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(emitSelectionChangedSignal()) ); setDragEnabled(false); // We run our own dragging, so disable QListView's version. connect( verticalScrollBar(), SIGNAL(valueChanged(int)), model(), SLOT(updateVisibleRowInfo()) ); setupDateChangeTimer(); } bool ThumbnailView::ThumbnailWidget::isGridResizing() const { return m_mouseHandler->isResizingGrid() || m_wheelResizing || m_externallyResizing; } void ThumbnailView::ThumbnailWidget::keyPressEvent( QKeyEvent* event ) { if ( !m_keyboardHandler->keyPressEvent( event ) ) QListView::keyPressEvent( event ); } void ThumbnailView::ThumbnailWidget::keyReleaseEvent( QKeyEvent* event ) { const bool propagate = m_keyboardHandler->keyReleaseEvent( event ); if ( propagate ) QListView::keyReleaseEvent(event); } bool ThumbnailView::ThumbnailWidget::isMouseOverStackIndicator( const QPoint& point ) { // first check if image is stack, if not return. DB::ImageInfoPtr imageInfo = mediaIdUnderCursor().info(); if (!imageInfo) return false; if (!imageInfo->isStacked()) return false; const QModelIndex index = indexUnderCursor(); const QRect itemRect = visualRect( index ); const QPixmap pixmap = index.data( Qt::DecorationRole ).value<QPixmap>(); if ( pixmap.isNull() ) return false; const QRect pixmapRect = cellGeometryInfo()->iconGeometry( pixmap ).translated( itemRect.topLeft() ); const QRect blackOutRect = pixmapRect.adjusted( 0,0, -10, -10 ); return pixmapRect.contains(point) && !blackOutRect.contains( point ); } static bool isMouseResizeGesture( QMouseEvent* event ) { return (event->button() & Qt::MidButton) || ((event->modifiers() & Qt::ControlModifier) && (event->modifiers() & Qt::AltModifier)); } void ThumbnailView::ThumbnailWidget::mousePressEvent( QMouseEvent* event ) { if ( (!(event->modifiers() & ( Qt::ControlModifier | Qt::ShiftModifier ) )) && isMouseOverStackIndicator( event->pos() ) ) { model()->toggleStackExpansion(mediaIdUnderCursor()); m_pressOnStackIndicator = true; return; } if ( isMouseResizeGesture( event ) ) m_mouseHandler = &m_gridResizeInteraction; else m_mouseHandler = &m_selectionInteraction; if ( !m_mouseHandler->mousePressEvent( event ) ) QListView::mousePressEvent( event ); if (event->button() & Qt::RightButton) //get out of selection mode if this is a right click m_mouseHandler = &m_mouseTrackingHandler; } void ThumbnailView::ThumbnailWidget::mouseMoveEvent( QMouseEvent* event ) { if ( m_pressOnStackIndicator ) return; if ( !m_mouseHandler->mouseMoveEvent( event ) ) QListView::mouseMoveEvent( event ); } void ThumbnailView::ThumbnailWidget::mouseReleaseEvent( QMouseEvent* event ) { if ( m_pressOnStackIndicator ) { m_pressOnStackIndicator = false; return; } if ( !m_mouseHandler->mouseReleaseEvent( event ) ) QListView::mouseReleaseEvent( event ); m_mouseHandler = &m_mouseTrackingHandler; } void ThumbnailView::ThumbnailWidget::mouseDoubleClickEvent( QMouseEvent * event ) { if ( isMouseOverStackIndicator( event->pos() ) ) { model()->toggleStackExpansion(mediaIdUnderCursor()); m_pressOnStackIndicator = true; } else if ( !( event->modifiers() & Qt::ControlModifier ) ) { DB::FileName id = mediaIdUnderCursor(); if ( !id.isNull() ) emit showImage( id ); } } void ThumbnailView::ThumbnailWidget::wheelEvent( QWheelEvent* event ) { if ( event->modifiers() & Qt::ControlModifier ) { event->setAccepted(true); if ( !m_wheelResizing) m_gridResizeInteraction.enterGridResizingMode(); m_wheelResizing = true; model()->beginResetModel(); const int delta = -event->delta() / 20; static int _minimum_ = Settings::SettingsData::instance()->minimumThumbnailSize(); Settings::SettingsData::instance()->setActualThumbnailSize( qMax( _minimum_, Settings::SettingsData::instance()->actualThumbnailSize() + delta ) ); cellGeometryInfo()->calculateCellSize(); model()->endResetModel(); } else { int delta = event->delta() / 5; QWheelEvent newevent = QWheelEvent(event->pos(), delta, event->buttons(), nullptr); QListView::wheelEvent(&newevent); } } void ThumbnailView::ThumbnailWidget::emitDateChange() { if ( m_isSettingDate ) return; int row = currentIndex().row(); if (row == -1) return; DB::FileName fileName = model()->imageAt( row ); if ( fileName.isNull() ) return; static QDateTime lastDate; QDateTime date = fileName.info()->date().start(); if ( date != lastDate ) { lastDate = date; if ( date.date().year() != 1900 ) emit currentDateChanged( date ); } } /** * scroll to the date specified with the parameter date. * The boolean includeRanges tells whether we accept range matches or not. */ void ThumbnailView::ThumbnailWidget::gotoDate( const DB::ImageDate& date, bool includeRanges ) { m_isSettingDate = true; DB::FileName candidate = DB::ImageDB::instance() ->findFirstItemInRange(model()->imageList(ViewOrder), date, includeRanges); if ( !candidate.isNull() ) setCurrentItem( candidate ); m_isSettingDate = false; } void ThumbnailView::ThumbnailWidget::setExternallyResizing( bool state ) { m_externallyResizing = state; } void ThumbnailView::ThumbnailWidget::reload(SelectionUpdateMethod method ) { SelectionMaintainer maintainer( this, model()); ThumbnailComponent::model()->beginResetModel(); cellGeometryInfo()->flushCache(); updatePalette(); ThumbnailComponent::model()->endResetModel(); if ( method == ClearSelection ) maintainer.disable(); } DB::FileName ThumbnailView::ThumbnailWidget::mediaIdUnderCursor() const { const QModelIndex index = indexUnderCursor(); if ( index.isValid() ) return model()->imageAt(index.row()); else return DB::FileName(); } QModelIndex ThumbnailView::ThumbnailWidget::indexUnderCursor() const { return indexAt( mapFromGlobal( QCursor::pos() ) ); } void ThumbnailView::ThumbnailWidget::dragMoveEvent( QDragMoveEvent* event ) { m_dndHandler->contentsDragMoveEvent(event); } void ThumbnailView::ThumbnailWidget::dragLeaveEvent( QDragLeaveEvent* event ) { m_dndHandler->contentsDragLeaveEvent( event ); } void ThumbnailView::ThumbnailWidget::dropEvent( QDropEvent* event ) { m_dndHandler->contentsDropEvent( event ); } void ThumbnailView::ThumbnailWidget::dragEnterEvent( QDragEnterEvent * event ) { m_dndHandler->contentsDragEnterEvent( event ); } void ThumbnailView::ThumbnailWidget::setCurrentItem( const DB::FileName& fileName ) { if ( fileName.isNull() ) return; const int row = model()->indexOf(fileName); setCurrentIndex( QListView::model()->index( row, 0 ) ); } DB::FileName ThumbnailView::ThumbnailWidget::currentItem() const { if ( !currentIndex().isValid() ) return DB::FileName(); return model()->imageAt( currentIndex().row()); } void ThumbnailView::ThumbnailWidget::updatePalette() { QPalette pal = palette(); pal.setBrush( QPalette::Base, QColor(Settings::SettingsData::instance()->backgroundColor()) ); pal.setBrush( QPalette::Text, Utilities::contrastColor( QColor(Settings::SettingsData::instance()->backgroundColor() ) ) ); setPalette( pal ); } int ThumbnailView::ThumbnailWidget::cellWidth() const { return visualRect( QListView::model()->index(0,0) ).size().width(); } void ThumbnailView::ThumbnailWidget::emitSelectionChangedSignal() { emit selectionCountChanged( selection( ExpandCollapsedStacks ).size() ); } void ThumbnailView::ThumbnailWidget::scheduleDateChangeSignal() { m_dateChangedTimer->start(200); } /** * During profiling, I found that emitting the dateChanged signal was * rather expensive, so now I delay that signal, so it is only emitted 200 * msec after the scroll, which means it will not be emitted when the user * holds down, say the page down key for scrolling. */ void ThumbnailView::ThumbnailWidget::setupDateChangeTimer() { m_dateChangedTimer = new QTimer(this); m_dateChangedTimer->setSingleShot(true); connect(m_dateChangedTimer, &QTimer::timeout, this, &ThumbnailWidget::emitDateChange); } void ThumbnailView::ThumbnailWidget::showEvent( QShowEvent* event ) { model()->updateVisibleRowInfo(); QListView::showEvent( event ); } DB::FileNameList ThumbnailView::ThumbnailWidget::selection( ThumbnailView::SelectionMode mode ) const { DB::FileNameList res; Q_FOREACH(const QModelIndex& index, selectedIndexes()) { DB::FileName currFileName = model()->imageAt(index.row()); bool includeAllStacks = false; switch ( mode ) { case IncludeAllStacks: includeAllStacks = true; /* FALLTHROUGH */ case ExpandCollapsedStacks: { // if the selected image belongs to a collapsed thread, // imply that all images in the stack are selected: DB::ImageInfoPtr imageInfo = currFileName.info(); if ( imageInfo && imageInfo->isStacked() && ( includeAllStacks || ! model()->isItemInExpandedStack( imageInfo->stackId() ) ) ) { // add all images in the same stack res.append(DB::ImageDB::instance()->getStackFor(currFileName)); } else res.append(currFileName); } break; case NoExpandCollapsedStacks: res.append(currFileName); break; } } return res; } bool ThumbnailView::ThumbnailWidget::isSelected( const DB::FileName& fileName ) const { return selection( NoExpandCollapsedStacks ).indexOf(fileName) != -1; } /** This very specific method will make the item specified by id selected, if there only are one item selected. This is used from the Viewer when you start it without a selection, and are going forward or backward. */ void ThumbnailView::ThumbnailWidget::changeSingleSelection(const DB::FileName& fileName) { if ( selection( NoExpandCollapsedStacks ).size() == 1 ) { QItemSelectionModel* selection = selectionModel(); selection->select( model()->fileNameToIndex(fileName), QItemSelectionModel::ClearAndSelect ); setCurrentItem(fileName); } } void ThumbnailView::ThumbnailWidget::select(const DB::FileNameList& items ) { Q_FOREACH( const DB::FileName& fileName, items ) selectionModel()->select(model()->fileNameToIndex(fileName), QItemSelectionModel::Select); } bool ThumbnailView::ThumbnailWidget::isItemUnderCursorSelected() const { return widget()->selection(ExpandCollapsedStacks).contains(mediaIdUnderCursor()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/AbstractDisplay.cpp b/Viewer/AbstractDisplay.cpp index dafa46ef..fae9a66f 100644 --- a/Viewer/AbstractDisplay.cpp +++ b/Viewer/AbstractDisplay.cpp @@ -1,27 +1,28 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "AbstractDisplay.h" #include <Settings/SettingsData.h> #include <DB/ImageInfo.h> + Viewer::AbstractDisplay::AbstractDisplay( QWidget* parent ) :QWidget( parent ), m_info( nullptr ) { } -#include "AbstractDisplay.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/CategoryImageConfig.cpp b/Viewer/CategoryImageConfig.cpp index 7f04fd4f..34247810 100644 --- a/Viewer/CategoryImageConfig.cpp +++ b/Viewer/CategoryImageConfig.cpp @@ -1,197 +1,196 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "CategoryImageConfig.h" #include <QDialogButtonBox> #include <QGridLayout> #include <QLabel> #include <QLayout> #include <QList> #include <QPixmap> #include <QPushButton> #include <QVBoxLayout> #include <KComboBox> #include <KLocalizedString> #include <DB/CategoryCollection.h> #include <DB/ImageDB.h> #include <DB/ImageInfo.h> #include <DB/MemberMap.h> #include <Settings/SettingsData.h> #include <Utilities/Util.h> using Utilities::StringSet; Viewer::CategoryImageConfig* Viewer::CategoryImageConfig::s_instance = nullptr; Viewer::CategoryImageConfig::CategoryImageConfig() : m_image( QImage() ) { setWindowTitle( i18nc("@title:window","Configure Category Image") ); QWidget* top = new QWidget; QVBoxLayout* lay1 = new QVBoxLayout( top ); setLayout(lay1); QGridLayout* lay2 = new QGridLayout; lay1->addLayout( lay2 ); // Group QLabel* label = new QLabel( i18nc("@label:listbox As in 'select the tag category'","Category:" ), top ); lay2->addWidget( label, 0, 0 ); m_group = new KComboBox( top ); lay2->addWidget( m_group, 0, 1 ); connect(m_group, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &CategoryImageConfig::groupChanged); // Member label = new QLabel( i18nc("@label:listbox As in 'select a tag'", "Tag:" ), top ); lay2->addWidget( label, 1, 0 ); m_member = new KComboBox( top ); lay2->addWidget( m_member, 1, 1 ); connect(m_member, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &CategoryImageConfig::memberChanged); // Current Value QGridLayout* lay3 = new QGridLayout; lay1->addLayout( lay3 ); label = new QLabel( i18nc("@label The current category image","Current image:"), top ); lay3->addWidget( label, 0, 0 ); m_current = new QLabel( top ); m_current->setFixedSize( 128, 128 ); lay3->addWidget( m_current, 0, 1 ); // New Value m_imageLabel = new QLabel( i18nc("@label Preview of the new category imape", "New image:"), top ); lay3->addWidget( m_imageLabel, 1, 0 ); m_imageLabel = new QLabel( top ); m_imageLabel->setFixedSize( 128, 128 ); lay3->addWidget( m_imageLabel, 1, 1 ); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); QPushButton *user1Button = new QPushButton; user1Button->setText(i18nc("@action:button As in 'Set the category image'", "Set")); buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole); connect(user1Button, &QPushButton::clicked, this, &CategoryImageConfig::slotSet); connect(buttonBox, &QDialogButtonBox::accepted, this, &CategoryImageConfig::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &CategoryImageConfig::reject); lay1->addWidget(buttonBox); } void Viewer::CategoryImageConfig::groupChanged() { QString categoryName = currentGroup(); if (categoryName.isNull()) return; QString currentText = m_member->currentText(); m_member->clear(); StringSet directMembers = m_info->itemsOfCategory(categoryName); StringSet set = directMembers; QMap<QString,StringSet> map = DB::ImageDB::instance()->memberMap().inverseMap(categoryName); for( StringSet::const_iterator directMembersIt = directMembers.begin(); directMembersIt != directMembers.end(); ++directMembersIt ) { set += map[*directMembersIt]; } QStringList list = set.toList(); list.sort(); m_member->addItems( list ); int index = list.indexOf( currentText ); if ( index != -1 ) m_member->setCurrentIndex( index ); memberChanged(); } void Viewer::CategoryImageConfig::memberChanged() { QString categoryName = currentGroup(); if (categoryName.isNull()) return; QPixmap pix = DB::ImageDB::instance()->categoryCollection()->categoryForName( categoryName )-> categoryImage(categoryName, m_member->currentText(), 128, 128); m_current->setPixmap( pix ); } void Viewer::CategoryImageConfig::slotSet() { QString categoryName = currentGroup(); if (categoryName.isNull()) return; DB::ImageDB::instance()->categoryCollection()->categoryForName( categoryName )-> setCategoryImage(categoryName, m_member->currentText(), m_image); memberChanged(); } QString Viewer::CategoryImageConfig::currentGroup() { int index = m_group->currentIndex(); if (index == -1) return QString(); return m_categoryNames[index]; } void Viewer::CategoryImageConfig::setCurrentImage( const QImage& image, const DB::ImageInfoPtr& info ) { m_image = image; m_imageLabel->setPixmap( QPixmap::fromImage(image) ); m_info = info; groupChanged(); } Viewer::CategoryImageConfig* Viewer::CategoryImageConfig::instance() { if ( !s_instance ) s_instance = new CategoryImageConfig(); return s_instance; } void Viewer::CategoryImageConfig::show() { QString currentCategory = m_group->currentText(); m_group->clear(); m_categoryNames.clear(); QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); int index = 0; int currentIndex = -1; for ( QList<DB::CategoryPtr>::ConstIterator categoryIt = categories.constBegin(); categoryIt != categories.constEnd(); ++categoryIt ) { if ( !(*categoryIt)->isSpecialCategory() ) { m_group->addItem((*categoryIt)->name()); m_categoryNames.push_back((*categoryIt)->name()); if ((*categoryIt)->name() == currentCategory) currentIndex = index; ++index; } } if ( currentIndex != -1 ) m_group->setCurrentIndex( currentIndex ); groupChanged(); QDialog::show(); } -#include "CategoryImageConfig.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ImageDisplay.cpp b/Viewer/ImageDisplay.cpp index a24fb12f..eb846801 100644 --- a/Viewer/ImageDisplay.cpp +++ b/Viewer/ImageDisplay.cpp @@ -1,735 +1,734 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageDisplay.h" #include <qpainter.h> #include <QPaintEvent> #include <QResizeEvent> #include <QMouseEvent> #include <KMessageBox> #include <KLocalizedString> #include "Settings/SettingsData.h" #include "Viewer/ViewHandler.h" #include "ImageManager/AsyncLoader.h" #include <qcursor.h> #include <qapplication.h> #include <math.h> #include "DB/ImageDB.h" #include <qtimer.h> #include <QDebug> /** Area displaying the actual image in the viewer. The purpose of this class is to display the actual image in the viewer. This involves controlling zooming and drawing on the images. This class is quite complicated as it had to both be fast and memory efficient. The following are dead end tried: 1) Initially QPainter::setWindow was used for zooming the images, but this had the effect that if you zoom to 100x100 from a 2300x1700 image on a 800x600 display, then Qt would internally create a pixmap with the size (2300/100)*800, (1700/100)*600, which takes up 1.4Gb of memory! 2) I tried doing all scaling and cropping using QPixmap's as that would allow me to keep all transformations on the X Server site (making resizing fast - or I beleived so). Unfortunately it showed up that this was much slower than doing it using QImage, and the result was thus that the looking at a series of images was slow. The process is as follows: - The image loaded from disk is rotated and stored in _loadedImage. Initially this image is as large as the view, until the user starts zooming, at which time the image is reloaded to the size as it is on disk. - Then _loadedImage is cropped and scaled to _croppedAndScaledImg. This image is the size of the display. Resizing the window thus needs to redo step. - Finally in paintEvent _croppedAndScaledImg is drawn to the screen. The aboce might very likely be simplified. Back in the old days it needed to be that complex to allow drawing on images. To propagate the cache, we need to know which direction the images are viewed in, which is the job of the instance variable _forward. */ Viewer::ImageDisplay::ImageDisplay( QWidget* parent) :AbstractDisplay( parent ), m_reloadImageInProgress( false ), m_forward(true), m_curIndex(0),m_busy( false ), m_cursorHiding(true) { m_viewHandler = new ViewHandler( this ); setMouseTracking( true ); m_cursorTimer = new QTimer( this ); m_cursorTimer->setSingleShot(true); connect(m_cursorTimer, &QTimer::timeout, this, &ImageDisplay::hideCursor); showCursor(); } /** * If mouse cursor hiding is enabled, hide the cursor right now */ void Viewer::ImageDisplay::hideCursor() { if (m_cursorHiding) setCursor( Qt::BlankCursor ); } /** * If mouse cursor hiding is enabled, show normal cursor and start a timer that will hide it later */ void Viewer::ImageDisplay::showCursor() { if (m_cursorHiding) { unsetCursor(); m_cursorTimer->start( 1500 ); } } /** * Prevent hideCursor() and showCursor() from altering cursor state */ void Viewer::ImageDisplay::disableCursorHiding() { m_cursorHiding = false; } /** * Enable automatic mouse cursor hiding */ void Viewer::ImageDisplay::enableCursorHiding() { m_cursorHiding = true; } void Viewer::ImageDisplay::mousePressEvent( QMouseEvent* event ) { // disable cursor hiding till button release disableCursorHiding(); QMouseEvent e( event->type(), mapPos( event->pos() ), event->button(), event->buttons(), event->modifiers() ); double ratio = sizeRatio( QSize(m_zEnd.x()-m_zStart.x(), m_zEnd.y()-m_zStart.y()), size() ); bool block = m_viewHandler->mousePressEvent( &e, event->pos(), ratio ); if ( !block ) QWidget::mousePressEvent( event ); update(); } void Viewer::ImageDisplay::mouseMoveEvent( QMouseEvent* event ) { // just reset the timer showCursor(); QMouseEvent e( event->type(), mapPos( event->pos() ), event->button(), event->buttons(), event->modifiers() ); double ratio = sizeRatio( QSize(m_zEnd.x()-m_zStart.x(), m_zEnd.y()-m_zStart.y()), size() ); bool block = m_viewHandler->mouseMoveEvent( &e, event->pos(), ratio ); if ( !block ) QWidget::mouseMoveEvent( event ); update(); } void Viewer::ImageDisplay::mouseReleaseEvent( QMouseEvent* event ) { // enable cursor hiding and reset timer enableCursorHiding(); showCursor(); m_cache.remove( m_curIndex ); QMouseEvent e( event->type(), mapPos( event->pos() ), event->button(), event->buttons(), event->modifiers() ); double ratio = sizeRatio( QSize(m_zEnd.x()-m_zStart.x(), m_zEnd.y()-m_zStart.y()), size() ); bool block = m_viewHandler->mouseReleaseEvent( &e, event->pos(), ratio ); if ( !block ) { QWidget::mouseReleaseEvent( event ); } emit possibleChange(); update(); } bool Viewer::ImageDisplay::setImage( DB::ImageInfoPtr info, bool forward ) { m_info = info; m_loadedImage = QImage(); // Find the index of the current image m_curIndex = 0; Q_FOREACH( const DB::FileName &filename, m_imageList) { if ( filename == info->fileName() ) break; ++m_curIndex; } if ( m_cache.contains(m_curIndex) && m_cache[m_curIndex].angle == info->angle()) { const ViewPreloadInfo& found = m_cache[m_curIndex]; m_loadedImage = found.img; updateZoomPoints( Settings::SettingsData::instance()->viewerStandardSize(), found.img.size() ); cropAndScale(); info->setSize( found.size ); emit imageReady(); } else { requestImage( info, true ); busy(); } m_forward = forward; updatePreload(); return true; } void Viewer::ImageDisplay::resizeEvent( QResizeEvent* event ) { ImageManager::AsyncLoader::instance()->stop( this, ImageManager::StopOnlyNonPriorityLoads ); m_cache.clear(); if ( m_info ) { cropAndScale(); if ( event->size().width() > 1.5*this->m_loadedImage.size().width() || event->size().height() > 1.5*this->m_loadedImage.size().height() ) potentialyLoadFullSize(); // Only do if we scale much bigger. } updatePreload(); } void Viewer::ImageDisplay::paintEvent( QPaintEvent* ) { int x = ( width() - m_croppedAndScaledImg.width() ) / 2; int y = ( height() - m_croppedAndScaledImg.height() ) / 2; QPainter painter( this ); painter.fillRect( 0,0, width(), height(), Qt::black ); painter.drawImage( x,y, m_croppedAndScaledImg ); } QPoint Viewer::ImageDisplay::offset( int logicalWidth, int logicalHeight, int physicalWidth, int physicalHeight, double* ratio ) { double rat = sizeRatio( QSize( logicalWidth, logicalHeight ), QSize( physicalWidth, physicalHeight ) ); int ox = (int) (physicalWidth - logicalWidth*rat)/2; int oy = (int) (physicalHeight - logicalHeight*rat)/2; if ( ratio ) *ratio = rat; return QPoint(ox,oy); } void Viewer::ImageDisplay::zoom( QPoint p1, QPoint p2 ) { m_cache.remove( m_curIndex ); normalize( p1, p2 ); double ratio; QPoint off = offset( (p2-p1).x(), (p2-p1).y(), width(), height(), &ratio ); off = off / ratio; p1.setX( p1.x() - off.x() ); p1.setY( p1.y() - off.y() ); p2.setX( p2.x()+off.x() ); p2.setY( p2.y()+off.y() ); m_zStart = p1; m_zEnd = p2; potentialyLoadFullSize(); cropAndScale(); } QPoint Viewer::ImageDisplay::mapPos( QPoint p ) { QPoint off = offset( qAbs( m_zEnd.x()-m_zStart.x() ), qAbs( m_zEnd.y()-m_zStart.y() ), width(), height(), 0 ); p -= off; int x = (int) (m_zStart.x() + (m_zEnd.x()-m_zStart.x())*((double)p.x()/ (width()-2*off.x()))); int y = (int) (m_zStart.y() + (m_zEnd.y()-m_zStart.y())*((double)p.y()/ (height()-2*off.y()))); return QPoint( x, y ); } void Viewer::ImageDisplay::xformPainter( QPainter* p ) { QPoint off = offset( qAbs( m_zEnd.x()-m_zStart.x() ), qAbs( m_zEnd.y()-m_zStart.y() ), width(), height(), 0 ); double s = (width()-2*off.x())/qAbs( (double)m_zEnd.x()-m_zStart.x()); p->scale( s, s ); p->translate( -m_zStart.x(), -m_zStart.y() ); } void Viewer::ImageDisplay::zoomIn() { QPoint size = (m_zEnd-m_zStart); QPoint p1 = m_zStart + size*(0.2/2); QPoint p2 = m_zEnd - size*(0.2/2); zoom(p1, p2); } void Viewer::ImageDisplay::zoomOut() { QPoint size = (m_zEnd-m_zStart); //Bug 150971, Qt tries to render bigger and bigger images (10000x10000), hence running out of memory. if ( ( size.x() * size.y() > 25*1024*1024 ) ) return; QPoint p1 = m_zStart - size*(0.25/2); QPoint p2 = m_zEnd + size*(0.25/2); zoom(p1,p2); } void Viewer::ImageDisplay::zoomFull() { m_zStart = QPoint(0,0); m_zEnd = QPoint( m_loadedImage.width(), m_loadedImage.height() ); zoom( QPoint(0,0), QPoint( m_loadedImage.width(), m_loadedImage.height() ) ); } void Viewer::ImageDisplay::normalize( QPoint& p1, QPoint& p2 ) { int minx = qMin( p1.x(), p2.x() ); int miny = qMin( p1.y(), p2.y() ); int maxx = qMax( p1.x(), p2.x() ); int maxy = qMax( p1.y(), p2.y() ); p1 = QPoint( minx, miny ); p2 = QPoint( maxx, maxy ); } void Viewer::ImageDisplay::pan( const QPoint& point ) { m_zStart += point; m_zEnd += point; cropAndScale(); } void Viewer::ImageDisplay::cropAndScale() { if ( m_loadedImage.isNull() ) { return; } if ( m_zStart != QPoint(0,0) || m_zEnd != QPoint( m_loadedImage.width(), m_loadedImage.height() ) ) { m_croppedAndScaledImg = m_loadedImage.copy( m_zStart.x(), m_zStart.y(), m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y() ); } else m_croppedAndScaledImg = m_loadedImage; updateZoomCaption(); if ( !m_croppedAndScaledImg.isNull() ) // I don't know how this can happen, but it seems not to be dangerous. m_croppedAndScaledImg = m_croppedAndScaledImg.scaled( width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); update(); emit viewGeometryChanged(m_croppedAndScaledImg.size(), QRect(m_zStart, m_zEnd), sizeRatio(m_loadedImage.size(), m_info->size())); } void Viewer::ImageDisplay::filterNone() { cropAndScale(); update(); } bool Viewer::ImageDisplay::filterMono() { m_croppedAndScaledImg = m_croppedAndScaledImg.convertToFormat(m_croppedAndScaledImg.Format_Mono); update(); return true; } // I can't believe there isn't a standard conversion for this??? -- WH bool Viewer::ImageDisplay::filterBW() { if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error( this, i18n("Insufficient color depth for this filter")); return false; } for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int gray = qGray(pixel); int alpha = qAlpha(pixel); m_croppedAndScaledImg.setPixel(x, y, qRgba(gray, gray, gray, alpha)); } } update(); return true; } bool Viewer::ImageDisplay::filterContrastStretch() { int redMin, redMax, greenMin, greenMax, blueMin, blueMax; redMin = greenMin = blueMin = 255; redMax = greenMax = blueMax = 0; if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error( this, i18n("Insufficient color depth for this filter")); return false; } // Look for minimum and maximum intensities within each color channel for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int red = qRed(pixel); int green = qGreen(pixel); int blue = qBlue(pixel); redMin = redMin < red ? redMin : red; redMax = redMax > red ? redMax : red; greenMin = greenMin < green ? greenMin : green; greenMax = greenMax > green ? greenMax : green; blueMin = blueMin < blue ? blueMin : blue; blueMax = blueMax > blue ? blueMax : blue; } } // Calculate factor for stretching each color intensity throughout the // whole range float redFactor, greenFactor, blueFactor; redFactor = ((float)(255) / (float) (redMax - redMin)); greenFactor = ((float)(255) / (float) (greenMax - greenMin)); blueFactor = ((float)(255) / (float) (blueMax - blueMin)); // Perform the contrast stretching for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int red = qRed(pixel); int green = qGreen(pixel); int blue = qBlue(pixel); int alpha = qAlpha(pixel); red = (red - redMin) * redFactor; red = red < 255 ? red : 255; red = red > 0 ? red : 0; green = (green - greenMin) * greenFactor; green = green < 255 ? green : 255; green = green > 0 ? green : 0; blue = (blue - blueMin) * blueFactor; blue = blue < 255 ? blue : 255; blue = blue > 0 ? blue : 0; m_croppedAndScaledImg.setPixel(x, y, qRgba(red, green, blue, alpha)); } } update(); return true; } bool Viewer::ImageDisplay::filterHistogramEqualization() { int width, height; float R_histogram[256]; float G_histogram[256]; float B_histogram[256]; float d; if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error( this, i18n("Insufficient color depth for this filter")); return false; } memset(R_histogram, 0, sizeof(R_histogram)); memset(G_histogram, 0, sizeof(G_histogram)); memset(B_histogram, 0, sizeof(B_histogram)); width = m_croppedAndScaledImg.width(); height = m_croppedAndScaledImg.height(); d = 1.0 / width / height; // Populate histogram for each color channel for (int y = 0; y < height; ++y) { for (int x = 1; x < width; ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); R_histogram[qRed(pixel)] += d; G_histogram[qGreen(pixel)] += d; B_histogram[qBlue(pixel)] += d; } } // Transfer histogram table to cumulative distribution table float R_sum = 0.0; float G_sum = 0.0; float B_sum = 0.0; for (int i = 0; i < 256; ++i) { R_sum += R_histogram[i]; G_sum += G_histogram[i]; B_sum += B_histogram[i]; R_histogram[i] = R_sum * 255 + 0.5; G_histogram[i] = G_sum * 255 + 0.5; B_histogram[i] = B_sum * 255 + 0.5; } // Equalize the image for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); m_croppedAndScaledImg.setPixel( x, y, qRgba(R_histogram[qRed(pixel)], G_histogram[qGreen(pixel)], B_histogram[qBlue(pixel)], qAlpha(pixel)) ); } } update(); return true; } void Viewer::ImageDisplay::updateZoomCaption() { const QSize imgSize = m_loadedImage.size(); // similar to sizeRatio(), but we take the _highest_ factor. double ratio = ((double)imgSize.width())/(m_zEnd.x()-m_zStart.x()); if ( ratio * (m_zEnd.y()-m_zStart.y()) < imgSize.height() ) { ratio = ((double)imgSize.height())/(m_zEnd.y()-m_zStart.y()); } emit setCaptionInfo((ratio > 1.05) ? ki18n("[ zoom x%1 ]").subs(ratio, 0, 'f', 1).toString() : QString()); } QImage Viewer::ImageDisplay::currentViewAsThumbnail() const { if ( m_croppedAndScaledImg.isNull() ) return QImage(); else return m_croppedAndScaledImg.scaled( 512, 512, Qt::KeepAspectRatio, Qt::SmoothTransformation ); } bool Viewer::ImageDisplay::isImageZoomed( const Settings::StandardViewSize type, const QSize& imgSize ) { if (type == Settings::FullSize) return true; if ( type == Settings::NaturalSizeIfFits ) return !(imgSize.width() < width() && imgSize.height() < height() ); return false; } void Viewer::ImageDisplay::pixmapLoaded(ImageManager::ImageRequest* request, const QImage& image) { const DB::FileName fileName = request->databaseFileName(); const QSize imgSize = request->size(); const QSize fullSize = request->fullSize(); const int angle = request->angle(); const bool loadedOK = request->loadedOK(); if ( loadedOK && fileName == m_info->fileName() ) { if ( fullSize.isValid() && !m_info->size().isValid() ) m_info->setSize( fullSize ); if ( !m_reloadImageInProgress ) updateZoomPoints( Settings::SettingsData::instance()->viewerStandardSize(), image.size() ); else { // See documentation for zoomPixelForPixel for details. // We just loaded a likel much larger image, so the zoom points // need to be scaled. Notice _loadedImage is the size of the // old image. double ratio = sizeRatio( m_loadedImage.size(), m_info->size() ); m_zStart *= ratio; m_zEnd *= ratio; m_reloadImageInProgress = false; } m_loadedImage = image; cropAndScale(); emit imageReady(); } else { if ( imgSize != size() ) return; // Might be an old preload version, or a loaded version that never made it in time ViewPreloadInfo info( image, fullSize, angle ); m_cache.insert( indexOf(fileName), info ); updatePreload(); } unbusy(); emit possibleChange(); } void Viewer::ImageDisplay::setImageList( const DB::FileNameList& list ) { m_imageList = list; m_cache.clear(); } void Viewer::ImageDisplay::updatePreload() { // cacheSize: number of images at current window dimensions (at 4 byte per pixel) const int cacheSize = (int) ((long long) ( Settings::SettingsData::instance()->viewerCacheSize() * 1024LL * 1024LL ) / (width()*height()*4)); bool cacheFull = (m_cache.count() > cacheSize); int incr = ( m_forward ? 1 : -1 ); int nextOnesInCache = 0; // Iterate from the current image in the direction of the viewing for ( int i = m_curIndex+incr; cacheSize ; i += incr ) { if ( m_forward ? ( i >= (int) m_imageList.count() ) : (i < 0) ) break; DB::ImageInfoPtr info = DB::ImageDB::instance()->info(m_imageList[i]); if ( !info ) { qWarning("Info was null for index %d!", i); return; } if ( m_cache.contains(i) ) { nextOnesInCache++; if ( nextOnesInCache >= ceil(cacheSize/2.0) && cacheFull ) { // Ok enough images in cache return; } } else { requestImage( info ); if ( cacheFull ) { // The cache was full, we need to delete an item from the cache. // First try to find an item from the direction we came from for ( int j = ( m_forward ? 0 : m_imageList.count() -1 ); j != m_curIndex; j += ( m_forward ? 1 : -1 ) ) { if ( m_cache.contains(j) ) { m_cache.remove(j); return; } } // OK We found no item in the direction we came from (think of home/end keys) for ( int j = ( m_forward ? m_imageList.count() -1 : 0 ); j != m_curIndex; j += ( m_forward ? -1 : 1 ) ) { if ( m_cache.contains(j) ) { m_cache.remove(j); return; } } Q_ASSERT( false ); // We should never get here. } return; } } } int Viewer::ImageDisplay::indexOf( const DB::FileName& fileName ) { int i = 0; Q_FOREACH( const DB::FileName &name, m_imageList ) { if ( name == fileName ) break; ++i; } return i; } void Viewer::ImageDisplay::busy() { if ( !m_busy ) qApp->setOverrideCursor( Qt::WaitCursor ); m_busy = true; } void Viewer::ImageDisplay::unbusy() { if ( m_busy ) qApp->restoreOverrideCursor(); m_busy = false; } void Viewer::ImageDisplay::zoomPixelForPixel() { // This is rather tricky. // We want to zoom to a pixel level for the real image, which we might // or might not have loaded yet. // // First we ask for zoom points as they would look like had we had the // real image loaded now. (We need to ask for them, for the real image, // otherwise we would just zoom to the pixel level of the view size // image) updateZoomPoints( Settings::NaturalSize, m_info->size() ); // The points now, however might not match the current visible image - // as this image might be be only view size large. We therefore need // to scale the coordinates. double ratio = sizeRatio( m_loadedImage.size(), m_info->size() ); m_zStart /= ratio; m_zEnd /= ratio; cropAndScale(); potentialyLoadFullSize(); } void Viewer::ImageDisplay::updateZoomPoints( const Settings::StandardViewSize type, const QSize& imgSize ) { const int iw = imgSize.width(); const int ih = imgSize.height(); if ( isImageZoomed( type, imgSize ) ) { m_zStart=QPoint( 0, 0 ); m_zEnd=QPoint( iw, ih ); } else { m_zStart = QPoint( - ( width()-iw ) / 2, -(height()-ih)/2); m_zEnd = QPoint( iw + (width()-iw)/2, ih+(height()-ih)/2); } } void Viewer::ImageDisplay::potentialyLoadFullSize() { if ( m_info->size() != m_loadedImage.size() ) { ImageManager::ImageRequest* request = new ImageManager::ImageRequest( m_info->fileName(), QSize(-1,-1), m_info->angle(), this ); request->setPriority( ImageManager::Viewer ); ImageManager::AsyncLoader::instance()->load( request ); busy(); m_reloadImageInProgress = true; } } /** * return the ratio of the two sizes. That is newSize/baseSize. */ double Viewer::ImageDisplay::sizeRatio( const QSize& baseSize, const QSize& newSize ) const { double res = ((double)newSize.width())/baseSize.width(); if ( res * baseSize.height() > newSize.height() ) { res = ((double)newSize.height())/baseSize.height(); } return res; } void Viewer::ImageDisplay::requestImage( const DB::ImageInfoPtr& info, bool priority ) { Settings::StandardViewSize viewSize = Settings::SettingsData::instance()->viewerStandardSize(); QSize s = size(); if ( viewSize == Settings::NaturalSize ) s = QSize(-1,-1); ImageManager::ImageRequest* request = new ImageManager::ImageRequest( info->fileName(), s, info->angle(), this ); request->setUpScale( viewSize == Settings::FullSize ); request->setPriority( priority ? ImageManager::Viewer : ImageManager::ViewerPreload ); ImageManager::AsyncLoader::instance()->load( request ); } void Viewer::ImageDisplay::hideEvent(QHideEvent *) { m_viewHandler->hideEvent(); } -#include "ImageDisplay.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/InfoBox.cpp b/Viewer/InfoBox.cpp index acc40b08..864ef2f1 100644 --- a/Viewer/InfoBox.cpp +++ b/Viewer/InfoBox.cpp @@ -1,348 +1,347 @@ -/* Copyright (C) 2003-2014 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "InfoBox.h" // Qt includes #include <QApplication> #include <QBitmap> #include <QDebug> #include <QDesktopWidget> #include <QMouseEvent> #include <QScrollBar> #include <QToolButton> // KDE includes #include <KActionCollection> #include <KLocalizedString> #include <KRatingWidget> // Local includes #include <Browser/BrowserWidget.h> #include <DB/ImageDB.h> #include <DB/ImageInfo.h> #include <MainWindow/Window.h> #ifdef HAVE_KGEOMAP #include <Map/MapView.h> #endif #include "VisibleOptionsMenu.h" using namespace Settings; Viewer::InfoBox::InfoBox(Viewer::ViewerWidget* viewer) : QTextBrowser(viewer) ,m_viewer(viewer) ,m_hoveringOverLink(false) ,m_infoBoxResizer(this) ,m_menu(nullptr) #ifdef HAVE_KGEOMAP ,m_map(nullptr) #endif { setFrameStyle(Box | Plain); setLineWidth(1); setMidLineWidth(0); setAutoFillBackground(false); QPalette p = palette(); p.setColor(QPalette::Base, QColor(0, 0, 0, 170)); // r, g, b, A p.setColor(QPalette::Text, Qt::white); p.setColor(QPalette::Link, QColor(Qt::blue).light()); setPalette(p); m_jumpToContext = new QToolButton(this); m_jumpToContext->setIcon(QIcon::fromTheme(QString::fromUtf8("kphotoalbum"))); m_jumpToContext->setFixedSize(16, 16); connect(m_jumpToContext, &QToolButton::clicked, this, &InfoBox::jumpToContext); connect(this, SIGNAL(highlighted(QString)), SLOT(linkHovered(QString))); #ifdef HAVE_KGEOMAP m_showOnMap = new QToolButton(this); m_showOnMap->setIcon(QIcon::fromTheme(QString::fromUtf8("atmosphere"))); m_showOnMap->setFixedSize(16, 16); m_showOnMap->setCursor(Qt::ArrowCursor); m_showOnMap->setToolTip(i18n("Show the geographic position of this image on a map")); connect(m_showOnMap, &QToolButton::clicked, this, &InfoBox::launchMapView); m_showOnMap->hide(); connect(m_viewer, &ViewerWidget::soughtTo, this, &InfoBox::updateMapForCurrentImage); #endif KRatingWidget* rating = new KRatingWidget( nullptr ); // Unfortunately, the KRatingWidget now thinks that it has some absurdly big // dimensions. This call will persuade it to stay reasonably small. rating->adjustSize(); for (int i = 0; i <= 10; ++i) { rating->setRating( i ); // QWidget::grab() does not create an alpha channel // Therefore, we need to create a mask using heuristics (yes, this is slow, but we only do it once) QPixmap pixmap = rating->grab(); pixmap.setMask(pixmap.createHeuristicMask()); m_ratingPixmap.append(pixmap); } delete rating; } QVariant Viewer::InfoBox::loadResource(int type, const QUrl& name) { if (name.scheme() == QString::fromUtf8("kratingwidget")) { int rating = name.port(); Q_ASSERT(0 <= rating && rating <= 10); return m_ratingPixmap[rating]; } return QTextBrowser::loadResource(type, name); } void Viewer::InfoBox::setSource(const QUrl& source) { int index = source.path().toInt(); QPair<QString,QString> p = m_linkMap[index]; QString category = p.first; QString value = p.second; Browser::BrowserWidget::instance()->load(category, value); showBrowser(); } void Viewer::InfoBox::setInfo(const QString& text, const QMap<int, QPair<QString,QString>>& linkMap) { m_linkMap = linkMap; setText(text); hackLinkColorForQt44(); #ifdef HAVE_KGEOMAP if (m_viewer->currentInfo()->coordinates().hasCoordinates()) { m_showOnMap->show(); } else { m_showOnMap->hide(); } #endif setSize(); } void Viewer::InfoBox::setSize() { const int maxWidth = Settings::SettingsData::instance()->infoBoxWidth(); const int maxHeight = Settings::SettingsData::instance()->infoBoxHeight(); document()->setPageSize(QSize(maxWidth, maxHeight)); bool showVerticalBar = document()->size().height() > maxHeight; setVerticalScrollBarPolicy(showVerticalBar ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); const int realWidth = static_cast<int>(document()->idealWidth()) + (showVerticalBar ? verticalScrollBar()->width() + frameWidth() : 0) + m_jumpToContext->width() + 10; resize(realWidth, qMin((int) document()->size().height(), maxHeight)); } void Viewer::InfoBox::mousePressEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton) { possiblyStartResize(event->pos()); } QTextBrowser::mousePressEvent(event); } void Viewer::InfoBox::mouseReleaseEvent(QMouseEvent* event) { if (m_infoBoxResizer.isActive()) { Settings::SettingsData::instance()->setInfoBoxWidth(width()); Settings::SettingsData::instance()->setInfoBoxHeight(height()); } m_infoBoxResizer.deactivate(); QTextBrowser::mouseReleaseEvent(event); } void Viewer::InfoBox::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) { if (m_infoBoxResizer.isActive()) { m_infoBoxResizer.setPos(event->pos()); } else { m_viewer->infoBoxMove(); } // Do not tell QTextBrowser about the mouse movement, as this will just start a selection. } else { updateCursor(event->pos()); QTextBrowser::mouseMoveEvent(event); } } void Viewer::InfoBox::linkHovered(const QString& linkName) { if (linkName.isEmpty()) { emit noTagHovered(); } else { emit tagHovered(m_linkMap[linkName.toInt()]); } m_hoveringOverLink = ! linkName.isNull(); } void Viewer::InfoBox::jumpToContext() { Browser::BrowserWidget::instance()->addImageView(m_viewer->currentInfo()->fileName()); showBrowser(); } void Viewer::InfoBox::showBrowser() { QDesktopWidget* desktop = qApp->desktop(); if (desktop->screenNumber(Browser::BrowserWidget::instance()) == desktop->screenNumber(m_viewer)) { if (m_viewer->showingFullScreen()) { m_viewer->setShowFullScreen(false); } MainWindow::Window::theMainWindow()->raise(); } } /** * Update the cursor based on the cursors position in the info box */ void Viewer::InfoBox::updateCursor(const QPoint& pos) { const int border = 25; bool left = (pos.x() < border); bool right = pos.x() > width() - border; bool top = pos.y() < border; bool bottom = pos.y() > height() - border; Settings::Position windowPos = Settings::SettingsData::instance()->infoBoxPosition(); Qt::CursorShape shape = Qt::SizeAllCursor; if (m_hoveringOverLink) { shape = Qt::PointingHandCursor; } else if (atBlackoutPos(left, right, top, bottom, windowPos)) { shape = Qt::SizeAllCursor; } else if ((left && top) || (right && bottom)) { shape = Qt::SizeFDiagCursor; } else if ((left && bottom) || (right && top)) { shape = Qt::SizeBDiagCursor; } else if (top || bottom) { shape = Qt::SizeVerCursor; } else if (left || right) { shape = Qt::SizeHorCursor; } setCursor(shape); viewport()->setCursor(shape); } /** * Return true if we are at an edge of the image info box that is towards the edge of the viewer. * We can forexample not make the box taller at the bottom if the box is sitting at the bottom of the viewer. */ bool Viewer::InfoBox::atBlackoutPos(bool left, bool right, bool top, bool bottom, Settings::Position pos) const { return (left && (pos == Left || pos == TopLeft || pos == BottomLeft)) || (right && (pos == Right || pos == TopRight || pos == BottomRight)) || (top && (pos == Top || pos == TopLeft || pos == TopRight)) || (bottom && (pos == Bottom || pos == BottomLeft || pos == BottomRight)); } void Viewer::InfoBox::possiblyStartResize(const QPoint& pos) { const int border = 25; bool left = (pos.x() < border); bool right = pos.x() > width() - border; bool top = pos.y() < border; bool bottom = pos.y() > height() - border; if (left || right || top || bottom) { m_infoBoxResizer.setup(left, right, top, bottom); } } void Viewer::InfoBox::resizeEvent(QResizeEvent*) { QPoint pos = viewport()->rect().adjusted(0, 2, -m_jumpToContext->width() - 2, 0).topRight(); m_jumpToContext->move(pos); #ifdef HAVE_KGEOMAP pos.setY(pos.y() + 20); m_showOnMap->move(pos); #endif } void Viewer::InfoBox::hackLinkColorForQt44() { QTextCursor cursor(document()); Q_FOREVER { QTextCharFormat f = cursor.charFormat(); if (f.isAnchor()) { f.setForeground(QColor(Qt::blue).light()); QTextCursor c2 = cursor; c2.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); c2.setCharFormat(f); } if (cursor.atEnd()) { break; } cursor.movePosition(QTextCursor::NextCharacter); } } void Viewer::InfoBox::contextMenuEvent(QContextMenuEvent* event) { if (! m_menu) { m_menu = new VisibleOptionsMenu(m_viewer, new KActionCollection((QObject*) nullptr)); connect(m_menu, &VisibleOptionsMenu::visibleOptionsChanged, m_viewer, &ViewerWidget::updateInfoBox); } m_menu->exec(event->globalPos()); } #ifdef HAVE_KGEOMAP void Viewer::InfoBox::launchMapView() { if (! m_map) { m_map = new Map::MapView(m_viewer, Map::MapView::MapViewWindow); } m_map->addImage(m_viewer->currentInfo()); m_map->setShowThumbnails(false); m_map->zoomToMarkers(); m_map->show(); m_map->raise(); } void Viewer::InfoBox::updateMapForCurrentImage(DB::FileName) { if (! m_map) { return; } if (m_viewer->currentInfo()->coordinates().hasCoordinates()) { m_map->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); m_map->clear(); m_map->addImage(m_viewer->currentInfo()); m_map->setCenter(m_viewer->currentInfo()); } else { m_map->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } } #endif -#include "InfoBox.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/SpeedDisplay.cpp b/Viewer/SpeedDisplay.cpp index 58c6438d..5853ae48 100644 --- a/Viewer/SpeedDisplay.cpp +++ b/Viewer/SpeedDisplay.cpp @@ -1,92 +1,91 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "SpeedDisplay.h" #include <QLabel> #include <QLayout> #include <QTimeLine> #include <QTimer> #include <KLocalizedString> Viewer::SpeedDisplay::SpeedDisplay( QWidget* parent ) :QLabel( parent ) { m_timeLine = new QTimeLine( 1000, this); connect( m_timeLine, SIGNAL(frameChanged(int)), this, SLOT(setAlphaChannel(int)) ); m_timeLine->setFrameRange( 0, 170 ); m_timeLine->setDirection( QTimeLine::Backward ); m_timer = new QTimer( this ); m_timer->setSingleShot(true); connect(m_timer, &QTimer::timeout, m_timeLine, &QTimeLine::start); setAutoFillBackground(true); } void Viewer::SpeedDisplay::display( int i ) { setText( i18nc("OSD for slideshow, num of seconds per image","<p><center><font size=\"+4\">%1 s</font></center></p>", i/1000.0 ) ); go(); } void Viewer::SpeedDisplay::start( ) { setText( i18nc("OSD for slideshow","<p><center><font size=\"+4\">Starting Slideshow<br/>Ctrl++ makes the slideshow faster<br/>Ctrl + - makes the slideshow slower</font></center></p>")); go(); } void Viewer::SpeedDisplay::go() { resize( sizeHint() ); QWidget* p = static_cast<QWidget*>( parent() ); move( ( p->width() - width() )/2, ( p->height() - height() )/2 ); setAlphaChannel( 170, 255 ); m_timer->start( 1000 ); m_timeLine->stop(); show(); raise(); } void Viewer::SpeedDisplay::end() { setText( i18nc("OSD for slideshow","<p><center><font size=\"+4\">Ending Slideshow</font></center></p>") ); go(); } void Viewer::SpeedDisplay::setAlphaChannel(int background, int label) { QPalette p = palette(); p.setColor(QPalette::Background, QColor(0,0,0,background)); // r,g,b,A p.setColor(QPalette::WindowText, QColor(255,255,255,label) ); setPalette(p); } void Viewer::SpeedDisplay::setAlphaChannel(int alpha) { setAlphaChannel(alpha,alpha); if ( alpha == 0 ) hide(); } -#include "SpeedDisplay.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/TextDisplay.cpp b/Viewer/TextDisplay.cpp index adad0b99..def04907 100644 --- a/Viewer/TextDisplay.cpp +++ b/Viewer/TextDisplay.cpp @@ -1,57 +1,56 @@ -/* Copyright (C) 2007-2010 Jan Kundrat <jkt@gentoo.org> +/* Copyright (C) 2007-2018 Jan Kundrat <jkt@gentoo.org> 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 "TextDisplay.h" #include <qlabel.h> #include <qlayout.h> #include <QLabel> #include <QVBoxLayout> #include "ImageDisplay.h" #include "DB/ImageDB.h" /** * Display a text instead of actual image/video data. */ Viewer::TextDisplay::TextDisplay( QWidget* parent ) :AbstractDisplay( parent ) { QVBoxLayout *lay = new QVBoxLayout( this ); m_text = new QLabel( this ); lay->addWidget( m_text ); m_text->setAlignment( Qt::AlignCenter ); QPalette pal = m_text->palette(); pal.setColor( QPalette::Background, Qt::white ); m_text->setPalette( pal ); } bool Viewer::TextDisplay::setImage( DB::ImageInfoPtr info, bool forward ) { Q_UNUSED( info ); Q_UNUSED( forward ); return true; } void Viewer::TextDisplay::setText( const QString text ) { m_text->setText( text ); } -#include "TextDisplay.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/VideoDisplay.cpp b/Viewer/VideoDisplay.cpp index 643f593f..80ee704d 100644 --- a/Viewer/VideoDisplay.cpp +++ b/Viewer/VideoDisplay.cpp @@ -1,224 +1,223 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "VideoDisplay.h" #include <phonon/videowidget.h> #include <phonon/audiooutput.h> #include <qglobal.h> #include <DB/ImageInfoPtr.h> #include <DB/ImageInfo.h> #include <qlayout.h> #include <qtimer.h> #include <QResizeEvent> #include <MainWindow/FeatureDialog.h> #include <KLocalizedString> #include <kmessagebox.h> #include <ktoolbar.h> #include <kxmlguiclient.h> #include <kxmlguibuilder.h> #include <kxmlguifactory.h> #include <QAction> #include <ktoolinvocation.h> #include <phonon/seekslider.h> #include <phonon/mediaobject.h> Viewer::VideoDisplay::VideoDisplay( QWidget* parent ) :Viewer::AbstractDisplay( parent ), m_zoomType( FullZoom ), m_zoomFactor(1) { QPalette pal = palette(); pal.setColor( QPalette::Window, Qt::black ); setPalette( pal ); setAutoFillBackground( true ); m_mediaObject = nullptr; } void Viewer::VideoDisplay::setup() { m_mediaObject = new Phonon::MediaObject(this); Phonon::AudioOutput* audioDevice = new Phonon::AudioOutput( Phonon::VideoCategory, this ); Phonon::createPath( m_mediaObject, audioDevice ); m_videoWidget = new Phonon::VideoWidget(this); Phonon::createPath( m_mediaObject, m_videoWidget ); m_slider = new Phonon::SeekSlider(this); m_slider->setMediaObject( m_mediaObject ); m_slider->show(); m_mediaObject->setTickInterval(100); m_videoWidget->setFocus(); m_videoWidget->resize(1024,768 ); m_videoWidget->move(0,0); m_videoWidget->show(); connect(m_mediaObject, &Phonon::MediaObject::finished, this, &VideoDisplay::stopped); connect(m_mediaObject, &Phonon::MediaObject::stateChanged, this, &VideoDisplay::phononStateChanged); } bool Viewer::VideoDisplay::setImage( DB::ImageInfoPtr info, bool /*forward*/ ) { if ( !m_mediaObject ) setup(); m_info = info; m_mediaObject->setCurrentSource( QUrl::fromLocalFile( info->fileName().absolute() ) ); m_mediaObject->play(); return true; } void Viewer::VideoDisplay::zoomIn() { resize( 1.25 ); } void Viewer::VideoDisplay::zoomOut() { resize( 0.8 ); } void Viewer::VideoDisplay::zoomFull() { m_zoomType = FullZoom; setVideoWidgetSize(); } void Viewer::VideoDisplay::zoomPixelForPixel() { m_zoomType = PixelForPixelZoom; m_zoomFactor = 1; setVideoWidgetSize(); } void Viewer::VideoDisplay::resize( double factor ) { m_zoomType = FixedZoom; m_zoomFactor *= factor; setVideoWidgetSize(); } void Viewer::VideoDisplay::resizeEvent( QResizeEvent* event ) { AbstractDisplay::resizeEvent( event ); setVideoWidgetSize(); } Viewer::VideoDisplay::~VideoDisplay() { if ( m_mediaObject ) m_mediaObject->stop(); } void Viewer::VideoDisplay::stop() { if ( m_mediaObject ) m_mediaObject->stop(); } void Viewer::VideoDisplay::playPause() { if ( !m_mediaObject ) return; if ( m_mediaObject->state() != Phonon::PlayingState ) m_mediaObject->play(); else m_mediaObject->pause(); } QImage Viewer::VideoDisplay::screenShoot() { return QPixmap::grabWindow( m_videoWidget->winId()).toImage(); } void Viewer::VideoDisplay::restart() { if ( !m_mediaObject ) return; m_mediaObject->seek(0); m_mediaObject->play(); } void Viewer::VideoDisplay::seek() { if (!m_mediaObject ) return; QAction* action = static_cast<QAction*>(sender()); int value = action->data().value<int>(); m_mediaObject->seek( m_mediaObject->currentTime() + value ); } bool Viewer::VideoDisplay::isPaused() const { if (!m_mediaObject ) return false; return m_mediaObject->state() == Phonon::PausedState; } bool Viewer::VideoDisplay::isPlaying() const { if (!m_mediaObject ) return false; return m_mediaObject->state() == Phonon::PlayingState; } void Viewer::VideoDisplay::phononStateChanged(Phonon::State newState, Phonon::State /*oldState*/) { setVideoWidgetSize(); if ( newState == Phonon::ErrorState ) { KMessageBox::error( nullptr, m_mediaObject->errorString(), i18n("Error playing media") ); } } void Viewer::VideoDisplay::setVideoWidgetSize() { if ( !m_mediaObject ) return; QSize videoSize; if ( m_zoomType == FullZoom ) { videoSize = QSize( size().width(), size().height() - m_slider->height() ); if (m_videoWidget->sizeHint().width() > 0) { m_zoomFactor = videoSize.width() / m_videoWidget->sizeHint().width(); } } else { videoSize = m_videoWidget->sizeHint(); if ( m_zoomType == FixedZoom ) videoSize *= m_zoomFactor; } m_videoWidget->resize( videoSize ); QPoint pos = QPoint( width()/2, (height()-m_slider->sizeHint().height())/2 )-QPoint(videoSize.width()/2, videoSize.height()/2); m_videoWidget->move(pos); m_slider->move( 0, height() - m_slider->sizeHint().height() ); m_slider->resize( width(), m_slider->sizeHint().height() ); } -#include "VideoDisplay.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewerWidget.cpp b/Viewer/ViewerWidget.cpp index 0a759468..c593081b 100644 --- a/Viewer/ViewerWidget.cpp +++ b/Viewer/ViewerWidget.cpp @@ -1,1537 +1,1536 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ViewerWidget.h" #include <QAction> #include <QApplication> #include <QContextMenuEvent> #include <QDBusConnection> #include <QDBusMessage> #include <QDesktopWidget> #include <QEventLoop> #include <QFileDialog> #include <QFileInfo> #include <qglobal.h> #include <QKeyEvent> #include <QList> #include <QPushButton> #include <QResizeEvent> #include <QStackedWidget> #include <QTimeLine> #include <QTimer> #include <QWheelEvent> #include <KActionCollection> #include <KIconLoader> #include <KIO/CopyJob> #include <KLocalizedString> #include <DB/CategoryCollection.h> #include <DB/ImageDB.h> #include <Exif/InfoDialog.h> #include <ImageManager/ThumbnailCache.h> #include <MainWindow/CategoryImagePopup.h> #include <MainWindow/DeleteDialog.h> #include <MainWindow/DirtyIndicator.h> #include <MainWindow/ExternalPopup.h> #include <MainWindow/Window.h> #include <Utilities/Util.h> #include "CategoryImageConfig.h" #include "ImageDisplay.h" #include "InfoBox.h" #include "SpeedDisplay.h" #include "TaggedArea.h" #include "TextDisplay.h" #include "VideoDisplay.h" #include "VideoShooter.h" #include "VisibleOptionsMenu.h" Viewer::ViewerWidget* Viewer::ViewerWidget::s_latest = nullptr; Viewer::ViewerWidget* Viewer::ViewerWidget::latest() { return s_latest; } // Notice the parent is zero to allow other windows to come on top of it. Viewer::ViewerWidget::ViewerWidget( UsageType type, QMap<Qt::Key, QPair<QString,QString> > *macroStore ) :QStackedWidget( nullptr ) , m_current(0), m_popup(nullptr), m_showingFullScreen( false ), m_forward( true ) , m_isRunningSlideShow( false ), m_videoPlayerStoppedManually(false), m_type(type) , m_currentCategory(DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name()) , m_inputMacros(macroStore), m_myInputMacros(nullptr) { if ( type == ViewerWindow ) { setWindowFlags( Qt::Window ); setAttribute( Qt::WA_DeleteOnClose ); s_latest = this; } if (! m_inputMacros) { m_myInputMacros = m_inputMacros = new QMap<Qt::Key, QPair<QString,QString> >; } m_screenSaverCookie = -1; m_currentInputMode = InACategory; m_display = m_imageDisplay = new ImageDisplay( this ); addWidget( m_imageDisplay ); m_textDisplay = new TextDisplay( this ); addWidget( m_textDisplay ); createVideoViewer(); connect(m_imageDisplay, &ImageDisplay::possibleChange, this, &ViewerWidget::updateCategoryConfig); connect(m_imageDisplay, &ImageDisplay::imageReady, this, &ViewerWidget::updateInfoBox); connect(m_imageDisplay, &ImageDisplay::setCaptionInfo, this, &ViewerWidget::setCaptionWithDetail); connect(m_imageDisplay, &ImageDisplay::viewGeometryChanged, this, &ViewerWidget::remapAreas); // This must not be added to the layout, as it is standing on top of // the ImageDisplay m_infoBox = new InfoBox( this ); m_infoBox->hide(); setupContextMenu(); m_slideShowTimer = new QTimer( this ); m_slideShowTimer->setSingleShot( true ); m_slideShowPause = Settings::SettingsData::instance()->slideShowInterval() * 1000; connect(m_slideShowTimer, &QTimer::timeout, this, &ViewerWidget::slotSlideShowNextFromTimer); m_speedDisplay = new SpeedDisplay( this ); m_speedDisplay->hide(); setFocusPolicy( Qt::StrongFocus ); QTimer::singleShot( 2000, this, SLOT(test()) ); } void Viewer::ViewerWidget::setupContextMenu() { m_popup = new QMenu( this ); m_actions = new KActionCollection( this ); createSlideShowMenu(); createZoomMenu(); createRotateMenu(); createSkipMenu(); createShowContextMenu(); createWallPaperMenu(); createInvokeExternalMenu(); createVideoMenu(); createCategoryImageMenu(); createFilterMenu(); QAction * action = m_actions->addAction( QString::fromLatin1("viewer-edit-image-properties"), this, SLOT(editImage()) ); action->setText( i18nc("@action:inmenu","Annotate...") ); action->setShortcut( Qt::CTRL+Qt::Key_1 ); m_popup->addAction( action ); m_setStackHead = m_actions->addAction( QString::fromLatin1("viewer-set-stack-head"), this, SLOT(slotSetStackHead()) ); m_setStackHead->setText( i18nc("@action:inmenu","Set as First Image in Stack") ); m_setStackHead->setShortcut( Qt::CTRL+Qt::Key_4 ); m_popup->addAction( m_setStackHead ); m_showExifViewer = m_actions->addAction( QString::fromLatin1("viewer-show-exif-viewer"), this, SLOT(showExifViewer()) ); m_showExifViewer->setText( i18nc("@action:inmenu","Show EXIF Viewer") ); m_popup->addAction( m_showExifViewer ); m_copyTo = m_actions->addAction( QString::fromLatin1("viewer-copy-to"), this, SLOT(copyTo()) ); m_copyTo->setText( i18nc("@action:inmenu","Copy image to...") ); m_copyTo->setShortcut( Qt::Key_F7 ); m_popup->addAction( m_copyTo ); if ( m_type == ViewerWindow ) { action = m_actions->addAction( QString::fromLatin1("viewer-close"), this, SLOT(close()) ); action->setText( i18nc("@action:inmenu","Close") ); action->setShortcut( Qt::Key_Escape ); } m_popup->addAction( action ); m_actions->readSettings(); Q_FOREACH( QAction* action, m_actions->actions() ) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } } void Viewer::ViewerWidget::createShowContextMenu() { VisibleOptionsMenu* menu = new VisibleOptionsMenu( this, m_actions ); connect(menu, &VisibleOptionsMenu::visibleOptionsChanged, this, &ViewerWidget::updateInfoBox); m_popup->addMenu( menu ); } void Viewer::ViewerWidget::createWallPaperMenu() { // Setting wallpaper has still not yet been ported to KPA4 #ifndef DOES_STILL_NOT_WORK_IN_KPA4 m_wallpaperMenu = new QMenu( m_popup ); m_wallpaperMenu->setTitle( i18nc("@title:inmenu","Set as Wallpaper") ); QAction * action = m_actions->addAction( QString::fromLatin1("viewer-centered"), this, SLOT(slotSetWallpaperC()) ); action->setText( i18nc("@action:inmenu","Centered") ); m_wallpaperMenu->addAction(action); action = m_actions->addAction( QString::fromLatin1("viewer-tiled"), this, SLOT(slotSetWallpaperT()) ); action->setText( i18nc("@action:inmenu","Tiled") ); m_wallpaperMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-center-tiled"), this, SLOT(slotSetWallpaperCT()) ); action->setText( i18nc("@action:inmenu","Center Tiled") ); m_wallpaperMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-centered-maxspect"), this, SLOT(slotSetWallpaperCM()) ); action->setText( i18nc("@action:inmenu","Centered Maxpect") ); m_wallpaperMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-tiled-maxpect"), this, SLOT(slotSetWallpaperTM()) ); action->setText( i18nc("@action:inmenu","Tiled Maxpect") ); m_wallpaperMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-scaled"), this, SLOT(slotSetWallpaperS()) ); action->setText( i18nc("@action:inmenu","Scaled") ); m_wallpaperMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-centered-auto-fit"), this, SLOT(slotSetWallpaperCAF()) ); action->setText( i18nc("@action:inmenu","Centered Auto Fit") ); m_wallpaperMenu->addAction( action ); m_popup->addMenu( m_wallpaperMenu ); #endif // DOES_STILL_NOT_WORK_IN_KPA4 } void Viewer::ViewerWidget::inhibitScreenSaver( bool inhibit ) { QDBusMessage message; if (inhibit) { message = QDBusMessage::createMethodCall( QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("Inhibit") ); message << QString( QString::fromLatin1("KPhotoAlbum") ); message << QString( QString::fromLatin1("Giving a slideshow") ); QDBusMessage reply = QDBusConnection::sessionBus().call( message ); if ( reply.type() == QDBusMessage::ReplyMessage ) m_screenSaverCookie = reply.arguments().first().toInt(); } else { if ( m_screenSaverCookie != -1 ) { message = QDBusMessage::createMethodCall( QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("UnInhibit") ); message << (uint)m_screenSaverCookie; QDBusConnection::sessionBus().send( message ); m_screenSaverCookie = -1; } } } void Viewer::ViewerWidget::createInvokeExternalMenu() { m_externalPopup = new MainWindow::ExternalPopup( m_popup ); m_popup->addMenu( m_externalPopup ); connect(m_externalPopup, &MainWindow::ExternalPopup::aboutToShow, this, &ViewerWidget::populateExternalPopup); } void Viewer::ViewerWidget::createRotateMenu() { m_rotateMenu = new QMenu( m_popup ); m_rotateMenu->setTitle( i18nc("@title:inmenu","Rotate") ); QAction * action = m_actions->addAction( QString::fromLatin1("viewer-rotate90"), this, SLOT(rotate90()) ); action->setText( i18nc("@action:inmenu","Rotate clockwise") ); action->setShortcut( Qt::Key_9 ); m_rotateMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-rotate180"), this, SLOT(rotate180()) ); action->setText( i18nc("@action:inmenu","Flip Over") ); action->setShortcut( Qt::Key_8 ); m_rotateMenu->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-rotare270"), this, SLOT(rotate270()) ); // ^ this is a typo, isn't it?! action->setText( i18nc("@action:inmenu","Rotate counterclockwise") ); action->setShortcut( Qt::Key_7 ); m_rotateMenu->addAction( action ); m_popup->addMenu( m_rotateMenu ); } void Viewer::ViewerWidget::createSkipMenu() { QMenu *popup = new QMenu( m_popup ); popup->setTitle( i18nc("@title:inmenu As in 'skip 2 images'","Skip") ); QAction * action = m_actions->addAction( QString::fromLatin1("viewer-home"), this, SLOT(showFirst()) ); action->setText( i18nc("@action:inmenu Go to first image","First") ); action->setShortcut( Qt::Key_Home ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-end"), this, SLOT(showLast()) ); action->setText( i18nc("@action:inmenu Go to last image","Last") ); action->setShortcut( Qt::Key_End ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next"), this, SLOT(showNext()) ); action->setText( i18nc("@action:inmenu","Show Next") ); action->setShortcuts(QList<QKeySequence>() << Qt::Key_PageDown << Qt::Key_Space); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next-10"), this, SLOT(showNext10()) ); action->setText( i18nc("@action:inmenu","Skip 10 Forward") ); action->setShortcut( Qt::CTRL+Qt::Key_PageDown ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next-100"), this, SLOT(showNext100()) ); action->setText( i18nc("@action:inmenu","Skip 100 Forward") ); action->setShortcut( Qt::SHIFT+Qt::Key_PageDown ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-next-1000"), this, SLOT(showNext1000()) ); action->setText( i18nc("@action:inmenu","Skip 1000 Forward") ); action->setShortcut( Qt::CTRL+Qt::SHIFT+Qt::Key_PageDown ); popup->addAction( action ); m_forwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev"), this, SLOT(showPrev()) ); action->setText( i18nc("@action:inmenu","Show Previous") ); action->setShortcuts(QList<QKeySequence>() << Qt::Key_PageUp << Qt::Key_Backspace); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev-10"), this, SLOT(showPrev10()) ); action->setText( i18nc("@action:inmenu","Skip 10 Backward") ); action->setShortcut( Qt::CTRL+Qt::Key_PageUp ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev-100"), this, SLOT(showPrev100()) ); action->setText( i18nc("@action:inmenu","Skip 100 Backward") ); action->setShortcut( Qt::SHIFT+Qt::Key_PageUp ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-prev-1000"), this, SLOT(showPrev1000()) ); action->setText( i18nc("@action:inmenu","Skip 1000 Backward") ); action->setShortcut( Qt::CTRL+Qt::SHIFT+Qt::Key_PageUp ); popup->addAction( action ); m_backwardActions.append(action); action = m_actions->addAction( QString::fromLatin1("viewer-delete-current"), this, SLOT(deleteCurrent()) ); action->setText( i18nc("@action:inmenu","Delete Image") ); action->setShortcut( Qt::CTRL + Qt::Key_Delete ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-remove-current"), this, SLOT(removeCurrent()) ); action->setText( i18nc("@action:inmenu","Remove Image from Display List") ); action->setShortcut( Qt::Key_Delete ); popup->addAction( action ); m_popup->addMenu( popup ); } void Viewer::ViewerWidget::createZoomMenu() { QMenu *popup = new QMenu( m_popup ); popup->setTitle( i18nc("@action:inmenu","Zoom") ); // PENDING(blackie) Only for image display? QAction * action = m_actions->addAction( QString::fromLatin1("viewer-zoom-in"), this, SLOT(zoomIn()) ); action->setText( i18nc("@action:inmenu","Zoom In") ); action->setShortcut( Qt::Key_Plus ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-zoom-out"), this, SLOT(zoomOut()) ); action->setText( i18nc("@action:inmenu","Zoom Out") ); action->setShortcut( Qt::Key_Minus ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-zoom-full"), this, SLOT(zoomFull()) ); action->setText( i18nc("@action:inmenu","Full View") ); action->setShortcut( Qt::Key_Period ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-zoom-pixel"), this, SLOT(zoomPixelForPixel()) ); action->setText( i18nc("@action:inmenu","Pixel for Pixel View") ); action->setShortcut( Qt::Key_Equal ); popup->addAction( action ); action = m_actions->addAction( QString::fromLatin1("viewer-toggle-fullscreen"), this, SLOT(toggleFullScreen()) ); action->setText( i18nc("@action:inmenu","Toggle Full Screen") ); action->setShortcuts(QList<QKeySequence>() << Qt::Key_F11 << Qt::Key_Return); popup->addAction( action ); m_popup->addMenu( popup ); } void Viewer::ViewerWidget::createSlideShowMenu() { QMenu *popup = new QMenu( m_popup ); popup->setTitle( i18nc("@title:inmenu","Slideshow") ); m_startStopSlideShow = m_actions->addAction( QString::fromLatin1("viewer-start-stop-slideshow"), this, SLOT(slotStartStopSlideShow()) ); m_startStopSlideShow->setText( i18nc("@action:inmenu","Run Slideshow") ); m_startStopSlideShow->setShortcut( Qt::CTRL+Qt::Key_R ); popup->addAction( m_startStopSlideShow ); m_slideShowRunFaster = m_actions->addAction( QString::fromLatin1("viewer-run-faster"), this, SLOT(slotSlideShowFaster()) ); m_slideShowRunFaster->setText( i18nc("@action:inmenu","Run Faster") ); m_slideShowRunFaster->setShortcut( Qt::CTRL + Qt::Key_Plus ); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction( m_slideShowRunFaster ); m_slideShowRunSlower = m_actions->addAction( QString::fromLatin1("viewer-run-slower"), this, SLOT(slotSlideShowSlower()) ); m_slideShowRunSlower->setText( i18nc("@action:inmenu","Run Slower") ); m_slideShowRunSlower->setShortcut( Qt::CTRL+Qt::Key_Minus ); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction( m_slideShowRunSlower ); m_popup->addMenu( popup ); } void Viewer::ViewerWidget::load( const DB::FileNameList& list, int index ) { m_list = list; m_imageDisplay->setImageList( list ); m_current = index; load(); bool on = ( list.count() > 1 ); m_startStopSlideShow->setEnabled(on); m_slideShowRunFaster->setEnabled(on); m_slideShowRunSlower->setEnabled(on); } void Viewer::ViewerWidget::load() { const bool isReadable = QFileInfo( currentInfo()->fileName().absolute() ).isReadable(); const bool isVideo = isReadable && Utilities::isVideo( currentInfo()->fileName() ); if ( isReadable ) { if ( isVideo ) { m_display = m_videoDisplay; } else m_display = m_imageDisplay; } else { m_display = m_textDisplay; m_textDisplay->setText( i18n("File not available") ); updateInfoBox(); } setCurrentWidget( m_display ); m_infoBox->raise(); m_rotateMenu->setEnabled( !isVideo ); m_wallpaperMenu->setEnabled( !isVideo ); m_categoryImagePopup->setEnabled( !isVideo ); m_filterMenu->setEnabled( !isVideo ); m_showExifViewer->setEnabled( !isVideo ); if ( m_exifViewer ) m_exifViewer->setImage( currentInfo()->fileName() ); Q_FOREACH( QAction* videoAction, m_videoActions ) { videoAction->setVisible( isVideo ); } emit soughtTo( m_list[ m_current ]); bool ok = m_display->setImage( currentInfo(), m_forward ); if ( !ok ) { close( false ); return; } setCaptionWithDetail( QString() ); // PENDING(blackie) This needs to be improved, so that it shows the actions only if there are that many images to jump. for( QList<QAction *>::const_iterator it = m_forwardActions.constBegin(); it != m_forwardActions.constEnd(); ++it ) (*it)->setEnabled( m_current +1 < (int) m_list.count() ); for( QList<QAction *>::const_iterator it = m_backwardActions.constBegin(); it != m_backwardActions.constEnd(); ++it ) (*it)->setEnabled( m_current > 0 ); m_setStackHead->setEnabled( currentInfo()->isStacked() ); if ( isVideo ) updateCategoryConfig(); if ( m_isRunningSlideShow ) m_slideShowTimer->start( m_slideShowPause ); if ( m_display == m_textDisplay ) updateInfoBox(); // Add all tagged areas addTaggedAreas(); } void Viewer::ViewerWidget::setCaptionWithDetail( const QString& detail ) { setWindowTitle( QString::fromLatin1( "KPhotoAlbum - %1 %2" ) .arg( currentInfo()->fileName().absolute() ) .arg( detail ) ); } void Viewer::ViewerWidget::contextMenuEvent( QContextMenuEvent * e ) { if ( m_videoDisplay ) { if ( m_videoDisplay->isPaused() ) m_playPause->setText(i18nc("@action:inmenu Start video playback","Play")); else m_playPause->setText(i18nc("@action:inmenu Pause video playback","Pause")); m_stop->setEnabled( m_videoDisplay->isPlaying() ); } m_popup->exec( e->globalPos() ); e->setAccepted(true); } void Viewer::ViewerWidget::showNextN(int n) { filterNone(); if ( m_display == m_videoDisplay ) { m_videoPlayerStoppedManually = true; m_videoDisplay->stop(); } if ( m_current + n < (int) m_list.count() ) { m_current += n; if (m_current >= (int) m_list.count()) m_current = (int) m_list.count() - 1; m_forward = true; load(); } } void Viewer::ViewerWidget::showNext() { showNextN(1); } void Viewer::ViewerWidget::removeCurrent() { removeOrDeleteCurrent(OnlyRemoveFromViewer); } void Viewer::ViewerWidget::deleteCurrent() { removeOrDeleteCurrent( RemoveImageFromDatabase ); } void Viewer::ViewerWidget::removeOrDeleteCurrent( RemoveAction action ) { const DB::FileName fileName = m_list[m_current]; if ( action == RemoveImageFromDatabase ) m_removed.append(fileName); m_list.removeAll(fileName); if ( m_list.isEmpty() ) close(); if ( m_current == m_list.count() ) showPrev(); else showNextN(0); } void Viewer::ViewerWidget::showNext10() { showNextN(10); } void Viewer::ViewerWidget::showNext100() { showNextN(100); } void Viewer::ViewerWidget::showNext1000() { showNextN(1000); } void Viewer::ViewerWidget::showPrevN(int n) { if ( m_display == m_videoDisplay ) m_videoDisplay->stop(); if ( m_current > 0 ) { m_current -= n; if (m_current < 0) m_current = 0; m_forward = false; load(); } } void Viewer::ViewerWidget::showPrev() { showPrevN(1); } void Viewer::ViewerWidget::showPrev10() { showPrevN(10); } void Viewer::ViewerWidget::showPrev100() { showPrevN(100); } void Viewer::ViewerWidget::showPrev1000() { showPrevN(1000); } void Viewer::ViewerWidget::rotate90() { currentInfo()->rotate( 90 ); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[ m_current]); } void Viewer::ViewerWidget::rotate180() { currentInfo()->rotate( 180 ); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[ m_current]); } void Viewer::ViewerWidget::rotate270() { currentInfo()->rotate( 270 ); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[ m_current]); } void Viewer::ViewerWidget::showFirst() { showPrevN(m_list.count()); } void Viewer::ViewerWidget::showLast() { showNextN(m_list.count()); } void Viewer::ViewerWidget::slotSetWallpaperC() { setAsWallpaper(1); } void Viewer::ViewerWidget::slotSetWallpaperT() { setAsWallpaper(2); } void Viewer::ViewerWidget::slotSetWallpaperCT() { setAsWallpaper(3); } void Viewer::ViewerWidget::slotSetWallpaperCM() { setAsWallpaper(4); } void Viewer::ViewerWidget::slotSetWallpaperTM() { setAsWallpaper(5); } void Viewer::ViewerWidget::slotSetWallpaperS() { setAsWallpaper(6); } void Viewer::ViewerWidget::slotSetWallpaperCAF() { setAsWallpaper(7); } void Viewer::ViewerWidget::setAsWallpaper(int /*mode*/) { #ifdef DOES_STILL_NOT_WORK_IN_KPA4 if(mode>7 || mode<1) return; DCOPRef kdesktop("kdesktop","KBackgroundIface"); kdesktop.send("setWallpaper(QString,int)",currentInfo()->fileName(0),mode); #endif } bool Viewer::ViewerWidget::close( bool alsoDelete) { if ( !m_removed.isEmpty() ) { MainWindow::DeleteDialog dialog( this ); dialog.exec( m_removed ); } m_slideShowTimer->stop(); m_isRunningSlideShow = false; return QWidget::close(); if ( alsoDelete ) deleteLater(); } DB::ImageInfoPtr Viewer::ViewerWidget::currentInfo() const { return DB::ImageDB::instance()->info(m_list[ m_current]); // PENDING(blackie) can we postpone this lookup? } void Viewer::ViewerWidget::infoBoxMove() { QPoint p = mapFromGlobal( QCursor::pos() ); Settings::Position oldPos = Settings::SettingsData::instance()->infoBoxPosition(); Settings::Position pos = oldPos; int x = m_display->mapFromParent( p ).x(); int y = m_display->mapFromParent( p ).y(); int w = m_display->width(); int h = m_display->height(); if ( x < w/3 ) { if ( y < h/3 ) pos = Settings::TopLeft; else if ( y > h*2/3 ) pos = Settings::BottomLeft; else pos = Settings::Left; } else if ( x > w*2/3 ) { if ( y < h/3 ) pos = Settings::TopRight; else if ( y > h*2/3 ) pos = Settings::BottomRight; else pos = Settings::Right; } else { if ( y < h/3 ) pos = Settings::Top; else if ( y > h*2/3 ) pos = Settings::Bottom; } if ( pos != oldPos ) { Settings::SettingsData::instance()->setInfoBoxPosition( pos ); updateInfoBox(); } } void Viewer::ViewerWidget::moveInfoBox() { m_infoBox->setSize(); Settings::Position pos = Settings::SettingsData::instance()->infoBoxPosition(); int lx = m_display->pos().x(); int ly = m_display->pos().y(); int lw = m_display->width(); int lh = m_display->height(); int bw = m_infoBox->width(); int bh = m_infoBox->height(); int bx, by; // x-coordinate if ( pos == Settings::TopRight || pos == Settings::BottomRight || pos == Settings::Right ) bx = lx+lw-5-bw; else if ( pos == Settings::TopLeft || pos == Settings::BottomLeft || pos == Settings::Left ) bx = lx+5; else bx = lx+lw/2-bw/2; // Y-coordinate if ( pos == Settings::TopLeft || pos == Settings::TopRight || pos == Settings::Top ) by = ly+5; else if ( pos == Settings::BottomLeft || pos == Settings::BottomRight || pos == Settings::Bottom ) by = ly+lh-5-bh; else by = ly+lh/2-bh/2; m_infoBox->move(bx,by); } void Viewer::ViewerWidget::resizeEvent( QResizeEvent* e ) { moveInfoBox(); QWidget::resizeEvent( e ); } void Viewer::ViewerWidget::updateInfoBox() { QString tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name(); if ( currentInfo() || !m_currentInput.isEmpty() || (!m_currentCategory.isEmpty() && m_currentCategory != tokensCategory)) { QMap<int, QPair<QString,QString> > map; QString text = Utilities::createInfoText( currentInfo(), &map ); QString selecttext = QString::fromLatin1(""); if (m_currentCategory.isEmpty()) { selecttext = i18nc("Basically 'enter a category name'","<b>Setting Category: </b>") + m_currentInput; if (m_currentInputList.length() > 0) { selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}"); } } else if ( ( !m_currentInput.isEmpty() && m_currentCategory != tokensCategory)) { selecttext = i18nc("Basically 'enter a tag name'","<b>Assigning: </b>") + m_currentCategory + QString::fromLatin1("/") + m_currentInput; if (m_currentInputList.length() > 0) { selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}"); } } else if ( !m_currentInput.isEmpty() && m_currentCategory == tokensCategory) { m_currentInput = QString::fromLatin1(""); } if (!selecttext.isEmpty()) text = selecttext + QString::fromLatin1("<br />") + text; if ( Settings::SettingsData::instance()->showInfoBox() && !text.isNull() && ( m_type != InlineViewer ) ) { m_infoBox->setInfo( text, map ); m_infoBox->show(); } else m_infoBox->hide(); moveInfoBox(); } } Viewer::ViewerWidget::~ViewerWidget() { inhibitScreenSaver(false); if ( s_latest == this ) s_latest = nullptr; if ( m_myInputMacros ) delete m_myInputMacros; } void Viewer::ViewerWidget::toggleFullScreen() { setShowFullScreen( !m_showingFullScreen ); } void Viewer::ViewerWidget::slotStartStopSlideShow() { bool wasRunningSlideShow = m_isRunningSlideShow; m_isRunningSlideShow = !m_isRunningSlideShow && m_list.count() != 1; if ( wasRunningSlideShow ) { m_startStopSlideShow->setText( i18nc("@action:inmenu","Run Slideshow") ); m_slideShowTimer->stop(); if ( m_list.count() != 1 ) m_speedDisplay->end(); inhibitScreenSaver(false); } else { m_startStopSlideShow->setText( i18nc("@action:inmenu","Stop Slideshow") ); if ( currentInfo()->mediaType() != DB::Video ) m_slideShowTimer->start( m_slideShowPause ); m_speedDisplay->start(); inhibitScreenSaver(true); } } void Viewer::ViewerWidget::slotSlideShowNextFromTimer() { // Load the next images. QTime timer; timer.start(); if ( m_display == m_imageDisplay ) slotSlideShowNext(); // ensure that there is a few milliseconds pause, so that an end slideshow keypress // can get through immediately, we don't want it to queue up behind a bunch of timer events, // which loaded a number of new images before the slideshow stops int ms = qMax( 200, m_slideShowPause - timer.elapsed() ); m_slideShowTimer->start( ms ); } void Viewer::ViewerWidget::slotSlideShowNext() { m_forward = true; if ( m_current +1 < (int) m_list.count() ) m_current++; else m_current = 0; load(); } void Viewer::ViewerWidget::slotSlideShowFaster() { changeSlideShowInterval(-500); } void Viewer::ViewerWidget::slotSlideShowSlower() { changeSlideShowInterval(+500); } void Viewer::ViewerWidget::changeSlideShowInterval( int delta ) { if ( m_list.count() == 1 ) return; m_slideShowPause += delta; m_slideShowPause = qMax( m_slideShowPause, 500 ); m_speedDisplay->display( m_slideShowPause ); if (m_slideShowTimer->isActive() ) m_slideShowTimer->start( m_slideShowPause ); } void Viewer::ViewerWidget::editImage() { DB::ImageInfoList list; list.append( currentInfo() ); MainWindow::Window::configureImages( list, true ); } void Viewer::ViewerWidget::filterNone() { if ( m_display == m_imageDisplay ) { m_imageDisplay->filterNone(); m_filterMono->setChecked( false ); m_filterBW->setChecked( false ); m_filterContrastStretch->setChecked( false ); m_filterHistogramEqualization->setChecked( false ); } } void Viewer::ViewerWidget::filterSelected() { // The filters that drop bit depth below 32 should be the last ones // so that filters requiring more bit depth are processed first if ( m_display == m_imageDisplay ) { m_imageDisplay->filterNone(); if (m_filterBW->isChecked()) m_imageDisplay->filterBW(); if (m_filterContrastStretch->isChecked()) m_imageDisplay->filterContrastStretch(); if (m_filterHistogramEqualization->isChecked()) m_imageDisplay->filterHistogramEqualization(); if (m_filterMono->isChecked()) m_imageDisplay->filterMono(); } } void Viewer::ViewerWidget::filterBW() { if ( m_display == m_imageDisplay ) { if ( m_filterBW->isChecked() ) m_filterBW->setChecked( m_imageDisplay->filterBW()); else filterSelected(); } } void Viewer::ViewerWidget::filterContrastStretch() { if ( m_display == m_imageDisplay ) { if (m_filterContrastStretch->isChecked()) m_filterContrastStretch->setChecked( m_imageDisplay->filterContrastStretch() ); else filterSelected(); } } void Viewer::ViewerWidget::filterHistogramEqualization() { if ( m_display == m_imageDisplay ) { if ( m_filterHistogramEqualization->isChecked() ) m_filterHistogramEqualization->setChecked( m_imageDisplay->filterHistogramEqualization() ); else filterSelected(); } } void Viewer::ViewerWidget::filterMono() { if ( m_display == m_imageDisplay ) { if ( m_filterMono->isChecked() ) m_filterMono->setChecked( m_imageDisplay->filterMono() ); else filterSelected(); } } void Viewer::ViewerWidget::slotSetStackHead() { MainWindow::Window::theMainWindow()->setStackHead(m_list[ m_current ]); } bool Viewer::ViewerWidget::showingFullScreen() const { return m_showingFullScreen; } void Viewer::ViewerWidget::setShowFullScreen( bool on ) { if ( on ) { // To avoid that the image is first loaded in a small size and the reloaded when scaled up, we need to resize the window right away. // (this results in odd behaviour (the image // 'jumps' because fullscreen > fullwindow) and should be // reconsidered. Henner.) resize( qApp->desktop()->screenGeometry().size() ); setWindowState( windowState() | Qt::WindowFullScreen ); // set moveInfoBox(); } else { // We need to size the image when going out of full screen, in case we started directly in full screen // setWindowState( windowState() & ~Qt::WindowFullScreen ); // reset resize( Settings::SettingsData::instance()->viewerSize() ); } m_showingFullScreen = on; } void Viewer::ViewerWidget::updateCategoryConfig() { if ( !CategoryImageConfig::instance()->isVisible() ) return; CategoryImageConfig::instance()->setCurrentImage( m_imageDisplay->currentViewAsThumbnail(), currentInfo() ); } void Viewer::ViewerWidget::populateExternalPopup() { m_externalPopup->populate( currentInfo(), m_list ); } void Viewer::ViewerWidget::populateCategoryImagePopup() { m_categoryImagePopup->populate( m_imageDisplay->currentViewAsThumbnail(), m_list[m_current] ); } void Viewer::ViewerWidget::show( bool slideShow ) { QSize size; bool fullScreen; if ( slideShow ) { fullScreen = Settings::SettingsData::instance()->launchSlideShowFullScreen(); size = Settings::SettingsData::instance()->slideShowSize(); } else { fullScreen = Settings::SettingsData::instance()->launchViewerFullScreen(); size = Settings::SettingsData::instance()->viewerSize(); } if ( fullScreen ) setShowFullScreen( true ); else resize( size ); QWidget::show(); if ( slideShow != m_isRunningSlideShow) { // The info dialog will show up at the wrong place if we call this function directly // don't ask me why - 4 Sep. 2004 15:13 -- Jesper K. Pedersen QTimer::singleShot(0, this, SLOT(slotStartStopSlideShow()) ); } } KActionCollection* Viewer::ViewerWidget::actions() { return m_actions; } int Viewer::ViewerWidget::find_tag_in_list(const QStringList &list, QString &namefound) { int found = 0; m_currentInputList = QString::fromLatin1(""); for( QStringList::ConstIterator listIter = list.constBegin(); listIter != list.constEnd(); ++listIter ) { if (listIter->startsWith(m_currentInput, Qt::CaseInsensitive)) { found++; if (m_currentInputList.length() > 0) m_currentInputList = m_currentInputList + QString::fromLatin1(","); m_currentInputList =m_currentInputList + listIter->right(listIter->length() - m_currentInput.length()); if (found > 1 && m_currentInputList.length() > 20) { // already found more than we want to display // bail here for now // XXX: non-ideal? display more? certainly config 20 return found; } else { namefound = *listIter; } } } return found; } void Viewer::ViewerWidget::keyPressEvent( QKeyEvent* event ) { if (event->key() == Qt::Key_Backspace) { // remove stuff from the current input string m_currentInput.remove( m_currentInput.length()-1, 1 ); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); m_currentInputList = QString::fromLatin1(""); // } else if (event->modifier & (Qt::AltModifier | Qt::MetaModifier) && // event->key() == Qt::Key_Enter) { return; // we've handled it } else if (event->key() == Qt::Key_Comma) { // force set the "new" token if (!m_currentCategory.isEmpty()) { if (m_currentInput.left(1) == QString::fromLatin1("\"") || // allow a starting ' or " to signal a brand new category // this bypasses the auto-selection of matching characters m_currentInput.left(1) == QString::fromLatin1("\'")) { m_currentInput = m_currentInput.right(m_currentInput.length()-1); } if (m_currentInput.isEmpty()) return; currentInfo()->addCategoryInfo(m_currentCategory, m_currentInput); DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_currentCategory); category->addItem(m_currentInput); } m_currentInput = QString::fromLatin1(""); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); return; // we've handled it } else if ( event->modifiers() == 0 && event->key() >= Qt::Key_0 && event->key() <= Qt::Key_5 ) { bool ok; short rating = event->text().left(1).toShort(&ok, 10); if (ok) { currentInfo()->setRating(rating * 2); updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); } } else if (event->modifiers() == 0 || event->modifiers() == Qt::ShiftModifier) { // search the category for matches QString namefound; QString incomingKey = event->text().left(1); // start searching for a new category name if (incomingKey == QString::fromLatin1("/")) { if (m_currentInput.isEmpty() && m_currentCategory.isEmpty()) { if (m_currentInputMode == InACategory) { m_currentInputMode = AlwaysStartWithCategory; } else { m_currentInputMode = InACategory; } } else { // reset the category to search through m_currentInput = QString::fromLatin1(""); m_currentCategory = QString::fromLatin1(""); } // use an assigned key or map to a given key for future reference } else if (m_currentInput.isEmpty() && // can map to function keys event->key() >= Qt::Key_F1 && event->key() <= Qt::Key_F35) { // we have a request to assign a macro key or use one Qt::Key key = (Qt::Key) event->key(); if (m_inputMacros->contains(key)) { // Use the requested toggle if ( event->modifiers() == Qt::ShiftModifier ) { if ( currentInfo()->hasCategoryInfo( (*m_inputMacros)[key].first, (*m_inputMacros)[key].second ) ) { currentInfo()->removeCategoryInfo( (*m_inputMacros)[key].first, (*m_inputMacros)[key].second ); } } else { currentInfo()->addCategoryInfo( (*m_inputMacros)[key].first, (*m_inputMacros)[key].second ); } } else { (*m_inputMacros)[key] = qMakePair(m_lastCategory, m_lastFound); } updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); // handled it return; } else if (m_currentCategory.isEmpty()) { // still searching for a category to lock to m_currentInput += incomingKey; QStringList categorynames = DB::ImageDB::instance()->categoryCollection()->categoryTexts(); if (find_tag_in_list(categorynames, namefound) == 1) { // yay, we have exactly one! m_currentCategory = namefound; m_currentInput = QString::fromLatin1(""); m_currentInputList = QString::fromLatin1(""); } } else { m_currentInput += incomingKey; DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection() ->categoryForName(m_currentCategory); QStringList items = category->items(); if (find_tag_in_list(items, namefound) == 1) { // yay, we have exactly one! if ( currentInfo()->hasCategoryInfo( category->name(), namefound ) ) currentInfo()->removeCategoryInfo( category->name(), namefound ); else currentInfo()->addCategoryInfo( category->name(), namefound ); m_lastFound = namefound; m_lastCategory = m_currentCategory; m_currentInput = QString::fromLatin1(""); m_currentInputList = QString::fromLatin1(""); if (m_currentInputMode == AlwaysStartWithCategory) m_currentCategory = QString::fromLatin1(""); } } updateInfoBox(); MainWindow::DirtyIndicator::markDirty(); } QWidget::keyPressEvent( event ); return; } void Viewer::ViewerWidget::videoStopped() { if ( !m_videoPlayerStoppedManually && m_isRunningSlideShow ) slotSlideShowNext(); m_videoPlayerStoppedManually=false; } void Viewer::ViewerWidget::wheelEvent( QWheelEvent* event ) { if ( event->delta() < 0) { showNext(); } else { showPrev(); } } void Viewer::ViewerWidget::showExifViewer() { m_exifViewer = new Exif::InfoDialog( currentInfo()->fileName(), this ); m_exifViewer->show(); } void Viewer::ViewerWidget::zoomIn() { m_display->zoomIn(); } void Viewer::ViewerWidget::zoomOut() { m_display->zoomOut(); } void Viewer::ViewerWidget::zoomFull() { m_display->zoomFull(); } void Viewer::ViewerWidget::zoomPixelForPixel() { m_display->zoomPixelForPixel(); } void Viewer::ViewerWidget::makeThumbnailImage() { VideoShooter::go(currentInfo(), this); } struct SeekInfo { SeekInfo( const QString& title, const char* name, int value, const QKeySequence& key ) : title( title ), name(name), value(value), key(key) {} QString title; const char* name; int value; QKeySequence key; }; void Viewer::ViewerWidget::createVideoMenu() { QMenu* menu = new QMenu(m_popup); menu->setTitle(i18nc("@title:inmenu","Seek")); m_videoActions.append( m_popup->addMenu( menu ) ); QList<SeekInfo> list; list << SeekInfo( i18nc("@action:inmenu","10 minutes backward"), "seek-10-minute", -600000, QKeySequence(QString::fromLatin1("Ctrl+Left"))) << SeekInfo( i18nc("@action:inmenu","1 minute backward"), "seek-1-minute", -60000, QKeySequence(QString::fromLatin1( "Shift+Left"))) << SeekInfo( i18nc("@action:inmenu","10 seconds backward"), "seek-10-second", -10000, QKeySequence(QString::fromLatin1( "Left"))) << SeekInfo( i18nc("@action:inmenu","1 seconds backward"), "seek-1-second", -1000, QKeySequence(QString::fromLatin1( "Up"))) << SeekInfo( i18nc("@action:inmenu","100 milliseconds backward"), "seek-100-millisecond", -100, QKeySequence(QString::fromLatin1( "Shift+Up"))) << SeekInfo( i18nc("@action:inmenu","100 milliseconds forward"), "seek+100-millisecond", 100, QKeySequence(QString::fromLatin1( "Shift+Down"))) << SeekInfo( i18nc("@action:inmenu","1 seconds forward"), "seek+1-second", 1000, QKeySequence(QString::fromLatin1( "Down"))) << SeekInfo( i18nc("@action:inmenu","10 seconds forward"), "seek+10-second", 10000, QKeySequence(QString::fromLatin1( "Right"))) << SeekInfo( i18nc("@action:inmenu","1 minute forward"), "seek+1-minute", 60000, QKeySequence(QString::fromLatin1( "Shift+Right"))) << SeekInfo( i18nc("@action:inmenu","10 minutes forward"), "seek+10-minute", 600000, QKeySequence(QString::fromLatin1( "Ctrl+Right"))); int count=0; Q_FOREACH( const SeekInfo& info, list ) { if ( count++ == 5 ) { QAction* sep = new QAction( menu ); sep->setSeparator(true); menu->addAction(sep); } QAction * seek = m_actions->addAction( QString::fromLatin1(info.name), m_videoDisplay, SLOT(seek())); seek->setText(info.title); seek->setData(info.value); seek->setShortcut( info.key ); menu->addAction(seek); } QAction* sep = new QAction(m_popup); sep->setSeparator(true); m_popup->addAction( sep ); m_videoActions.append( sep ); m_stop = m_actions->addAction( QString::fromLatin1("viewer-video-stop"), m_videoDisplay, SLOT(stop()) ); m_stop->setText( i18nc("@action:inmenu Stop video playback","Stop") ); m_popup->addAction( m_stop ); m_videoActions.append(m_stop); m_playPause = m_actions->addAction( QString::fromLatin1("viewer-video-pause"), m_videoDisplay, SLOT(playPause()) ); // text set in contextMenuEvent() m_playPause->setShortcut( Qt::Key_P ); m_popup->addAction( m_playPause ); m_videoActions.append( m_playPause ); m_makeThumbnailImage = m_actions->addAction( QString::fromLatin1("make-thumbnail-image"), this, SLOT(makeThumbnailImage())); m_makeThumbnailImage->setShortcut(Qt::ControlModifier + Qt::Key_S); m_makeThumbnailImage->setText( i18nc("@action:inmenu","Use current frame in thumbnail view") ); m_popup->addAction(m_makeThumbnailImage); m_videoActions.append(m_makeThumbnailImage); QAction * restart = m_actions->addAction( QString::fromLatin1("viewer-video-restart"), m_videoDisplay, SLOT(restart()) ); restart->setText( i18nc("@action:inmenu Restart video playback.","Restart") ); m_popup->addAction( restart ); m_videoActions.append( restart ); } void Viewer::ViewerWidget::createCategoryImageMenu() { m_categoryImagePopup = new MainWindow::CategoryImagePopup( m_popup ); m_popup->addMenu( m_categoryImagePopup ); connect(m_categoryImagePopup, &MainWindow::CategoryImagePopup::aboutToShow, this, &ViewerWidget::populateCategoryImagePopup); } void Viewer::ViewerWidget::createFilterMenu() { m_filterMenu = new QMenu( m_popup ); m_filterMenu->setTitle( i18nc("@title:inmenu","Filters") ); m_filterNone = m_actions->addAction( QString::fromLatin1("filter-empty"), this, SLOT(filterNone()) ); m_filterNone->setText( i18nc("@action:inmenu","Remove All Filters") ); m_filterMenu->addAction( m_filterNone ); m_filterBW = m_actions->addAction( QString::fromLatin1("filter-bw"), this, SLOT(filterBW()) ); m_filterBW->setText( i18nc("@action:inmenu","Apply Grayscale Filter") ); m_filterBW->setCheckable( true ); m_filterMenu->addAction( m_filterBW ); m_filterContrastStretch = m_actions->addAction( QString::fromLatin1("filter-cs"), this, SLOT(filterContrastStretch()) ); m_filterContrastStretch->setText( i18nc("@action:inmenu","Apply Contrast Stretching Filter") ); m_filterContrastStretch->setCheckable( true ); m_filterMenu->addAction( m_filterContrastStretch ); m_filterHistogramEqualization = m_actions->addAction( QString::fromLatin1("filter-he"), this, SLOT(filterHistogramEqualization()) ); m_filterHistogramEqualization->setText( i18nc("@action:inmenu","Apply Histogram Equalization Filter") ); m_filterHistogramEqualization->setCheckable( true ); m_filterMenu->addAction( m_filterHistogramEqualization ); m_filterMono = m_actions->addAction( QString::fromLatin1("filter-mono"), this, SLOT(filterMono()) ); m_filterMono->setText( i18nc("@action:inmenu","Apply Monochrome Filter") ); m_filterMono->setCheckable( true ); m_filterMenu->addAction( m_filterMono ); m_popup->addMenu( m_filterMenu ); } void Viewer::ViewerWidget::test() { #ifdef TESTING QTimeLine* timeline = new QTimeLine; timeline->setStartFrame( _infoBox->y() ); timeline->setEndFrame( height() ); connect(timeline, &QTimeLine::frameChanged, this, &ViewerWidget::moveInfoBox); timeline->start(); #endif // TESTING } void Viewer::ViewerWidget::moveInfoBox( int y) { m_infoBox->move( m_infoBox->x(), y ); } void Viewer::ViewerWidget::createVideoViewer() { m_videoDisplay = new VideoDisplay( this ); addWidget( m_videoDisplay ); connect(m_videoDisplay, &VideoDisplay::stopped, this, &ViewerWidget::videoStopped); } void Viewer::ViewerWidget::stopPlayback() { m_videoDisplay->stop(); } void Viewer::ViewerWidget::invalidateThumbnail() const { ImageManager::ThumbnailCache::instance()->removeThumbnail( currentInfo()->fileName() ); } void Viewer::ViewerWidget::addTaggedAreas() { // Clean all areas we probably already have foreach (TaggedArea *area, findChildren<TaggedArea *>()) { area->deleteLater(); } QMap<QString, QMap<QString, QRect>> taggedAreas = currentInfo()->taggedAreas(); QMapIterator<QString, QMap<QString, QRect>> areasInCategory(taggedAreas); QString category; QString tag; while (areasInCategory.hasNext()) { areasInCategory.next(); category = areasInCategory.key(); QMapIterator<QString, QRect> areaData(areasInCategory.value()); while (areaData.hasNext()) { areaData.next(); tag = areaData.key(); // Add a new frame for the area TaggedArea *newArea = new TaggedArea(this); newArea->setTagInfo(category, category, tag); newArea->setActualGeometry(areaData.value()); newArea->show(); connect(m_infoBox, &InfoBox::tagHovered, newArea, &TaggedArea::checkShowArea); connect(m_infoBox, &InfoBox::noTagHovered, newArea, &TaggedArea::resetViewStyle); } } // Be sure to display the areas, as viewGeometryChanged is not always emitted on load QSize imageSize = currentInfo()->size(); QSize windowSize = this->size(); // On load, the image is never zoomed, so it's a bit easier ;-) double scaleWidth = double(imageSize.width()) / windowSize.width(); double scaleHeight = double(imageSize.height()) / windowSize.height(); int offsetTop = 0; int offsetLeft = 0; if (scaleWidth > scaleHeight) { offsetTop = (windowSize.height() - imageSize.height() / scaleWidth); } else { offsetLeft = (windowSize.width() - imageSize.width() / scaleHeight); } remapAreas( QSize(windowSize.width() - offsetLeft, windowSize.height() - offsetTop), QRect(QPoint(0, 0), QPoint(imageSize.width(), imageSize.height())), 1 ); } void Viewer::ViewerWidget::remapAreas(QSize viewSize, QRect zoomWindow, double sizeRatio) { QSize currentWindowSize = this->size(); int outerOffsetLeft = (currentWindowSize.width() - viewSize.width()) / 2; int outerOffsetTop = (currentWindowSize.height() - viewSize.height()) / 2; if (sizeRatio != 1) { zoomWindow = QRect( QPoint( double(zoomWindow.left()) * sizeRatio, double(zoomWindow.top()) * sizeRatio ), QPoint( double(zoomWindow.left() + zoomWindow.width()) * sizeRatio, double(zoomWindow.top() + zoomWindow.height()) * sizeRatio ) ); } double scaleHeight = double(viewSize.height()) / zoomWindow.height(); double scaleWidth = double(viewSize.width()) / zoomWindow.width(); int innerOffsetLeft = -zoomWindow.left() * scaleWidth; int innerOffsetTop = -zoomWindow.top() * scaleHeight; Q_FOREACH(TaggedArea *area, findChildren<TaggedArea *>()) { QRect actualGeometry = area->actualGeometry(); QRect screenGeometry; screenGeometry.setWidth(actualGeometry.width() * scaleWidth); screenGeometry.setHeight(actualGeometry.height() * scaleHeight); screenGeometry.moveTo( actualGeometry.left() * scaleWidth + outerOffsetLeft + innerOffsetLeft, actualGeometry.top() * scaleHeight + outerOffsetTop + innerOffsetTop ); area->setGeometry(screenGeometry); } } void Viewer::ViewerWidget::copyTo() { QUrl src = QUrl::fromLocalFile(currentInfo()->fileName().absolute()); if (m_lastCopyToTarget.isNull()) { // get directory of src file m_lastCopyToTarget = QFileInfo(src.path()).path(); } QFileDialog dialog( this ); dialog.setWindowTitle( i18nc("@title:window", "Copy image to...") ); // use directory of src as start-location: dialog.setDirectory(m_lastCopyToTarget); dialog.selectFile(src.fileName()); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setLabelText(QFileDialog::Accept, i18nc("@action:button", "Copy")); if (dialog.exec()) { QUrl dst = dialog.selectedUrls().first(); KIO::CopyJob *job = KIO::copy(src, dst); connect(job, &KIO::CopyJob::finished, job, &QObject::deleteLater); // get directory of dst file m_lastCopyToTarget = QFileInfo(dst.path()).path(); } } -#include "ViewerWidget.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/Database.cpp b/XMLDB/Database.cpp index 705bdd80..362ec8cf 100644 --- a/XMLDB/Database.cpp +++ b/XMLDB/Database.cpp @@ -1,710 +1,709 @@ -/* Copyright (C) 2003-2014 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Database.h" #include "Settings/SettingsData.h" #include <kmessagebox.h> #include <KLocalizedString> #include "Utilities/Util.h" #include "DB/GroupCounter.h" #include "Browser/BrowserWidget.h" #include "DB/ImageInfo.h" #include "DB/ImageInfoPtr.h" #include "DB/CategoryCollection.h" -#include "Database.moc" #include "XMLCategory.h" #include <QExplicitlySharedDataPointer> #include "XMLImageDateCollection.h" #include "FileReader.h" #include "FileWriter.h" #include "Exif/Database.h" #include <DB/FileName.h> using Utilities::StringSet; bool XMLDB::Database::s_anyImageWithEmptySize = false; XMLDB::Database::Database( const QString& configFile ): m_fileName(configFile) { Utilities::checkForBackupFile( configFile ); FileReader reader( this ); reader.read( configFile ); m_nextStackId = reader.nextStackId(); connect( categoryCollection(), SIGNAL(itemRemoved(DB::Category*,QString)), this, SLOT(deleteItem(DB::Category*,QString)) ); connect( categoryCollection(), SIGNAL(itemRenamed(DB::Category*,QString,QString)), this, SLOT(renameItem(DB::Category*,QString,QString)) ); connect( categoryCollection(), SIGNAL(itemRemoved(DB::Category*,QString)), &m_members, SLOT(deleteItem(DB::Category*,QString)) ); connect( categoryCollection(), SIGNAL(itemRenamed(DB::Category*,QString,QString)), &m_members, SLOT(renameItem(DB::Category*,QString,QString)) ); connect( categoryCollection(), SIGNAL(categoryRemoved(QString)), &m_members, SLOT(deleteCategory(QString))); } uint XMLDB::Database::totalCount() const { return m_images.count(); } /** * I was considering merging the two calls to this method (one for images, one for video), but then I * realized that all the work is really done after the check for whether the given * imageInfo is of the right type, and as a match can't be both, this really * would buy me nothing. */ QMap<QString,uint> XMLDB::Database::classify( const DB::ImageSearchInfo& info, const QString &category, DB::MediaType typemask ) { QMap<QString, uint> map; DB::GroupCounter counter( category ); Utilities::StringSet alreadyMatched = info.findAlreadyMatched( category ); DB::ImageSearchInfo noMatchInfo = info; QString currentMatchTxt = noMatchInfo.categoryMatchText( category ); if ( currentMatchTxt.isEmpty() ) noMatchInfo.setCategoryMatchText( category, DB::ImageDB::NONE() ); else noMatchInfo.setCategoryMatchText( category, QString::fromLatin1( "%1 & %2" ).arg(currentMatchTxt).arg(DB::ImageDB::NONE()) ); // Iterate through the whole database of images. for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it ) { bool match = ( (*it)->mediaType() & typemask ) && !(*it)->isLocked() && info.match( *it ) && rangeInclude( *it ); if ( match ) { // If the given image is currently matched. // Now iterate through all the categories the current image // contains, and increase them in the map mapping from category // to count. StringSet items = (*it)->itemsOfCategory(category); counter.count( items ); for( StringSet::const_iterator it2 = items.begin(); it2 != items.end(); ++it2 ) { if ( !alreadyMatched.contains(*it2) ) // We do not want to match "Jesper & Jesper" map[*it2]++; } // Find those with no other matches if ( noMatchInfo.match( *it ) ) map[DB::ImageDB::NONE()]++; } } QMap<QString,uint> groups = counter.result(); for( QMap<QString,uint>::iterator it= groups.begin(); it != groups.end(); ++it ) { map[it.key()] = it.value(); } return map; } void XMLDB::Database::renameCategory( const QString& oldName, const QString newName ) { for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { (*it)->renameCategory( oldName, newName ); } } void XMLDB::Database::addToBlockList(const DB::FileNameList& list) { Q_FOREACH(const DB::FileName& fileName, list) { m_blockList.insert(fileName); } deleteList( list ); } void XMLDB::Database::deleteList(const DB::FileNameList& list) { Q_FOREACH(const DB::FileName& fileName, list) { DB::ImageInfoPtr inf = fileName.info(); StackMap::iterator found = m_stackMap.find(inf->stackId()); if ( inf->isStacked() && found != m_stackMap.end() ) { const DB::FileNameList origCache = found.value(); DB::FileNameList newCache; Q_FOREACH(const DB::FileName& cacheName, origCache) { if (fileName != cacheName) newCache.append(cacheName); } if (newCache.size() <= 1) { // we're destroying a stack Q_FOREACH(const DB::FileName& cacheName, newCache) { DB::ImageInfoPtr cacheInf = cacheName.info(); cacheInf->setStackId(0); cacheInf->setStackOrder(0); } m_stackMap.remove( inf->stackId() ); } else { m_stackMap.insert(inf->stackId(), newCache); } } m_images.remove( inf ); } Exif::Database::instance()->remove( list ); emit totalChanged( m_images.count() ); emit imagesDeleted(list); emit dirty(); } void XMLDB::Database::renameItem( DB::Category* category, const QString& oldName, const QString& newName ) { for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { (*it)->renameItem( category->name(), oldName, newName ); } } void XMLDB::Database::deleteItem( DB::Category* category, const QString& value ) { for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { (*it)->removeCategoryInfo( category->name(), value ); } } void XMLDB::Database::lockDB( bool lock, bool exclude ) { DB::ImageSearchInfo info = Settings::SettingsData::instance()->currentLock(); for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); ++it ) { if ( lock ) { bool match = info.match( *it ); if ( !exclude ) match = !match; (*it)->setLocked( match ); } else (*it)->setLocked( false ); } } void XMLDB::Database::addImages( const DB::ImageInfoList& images ) { // FIXME: merge stack information DB::ImageInfoList newImages = images.sort(); if ( m_images.count() == 0 ) { // case 1: The existing imagelist is empty. m_images = newImages; } else if ( newImages.count() == 0 ) { // case 2: No images to merge in - that's easy ;-) return; } else if ( newImages.first()->date().start() > m_images.last()->date().start() ) { // case 2: The new list is later than the existsing m_images.appendList(newImages); } else if ( m_images.isSorted() ) { // case 3: The lists overlaps, and the existsing list is sorted m_images.mergeIn( newImages ); } else{ // case 4: The lists overlaps, and the existsing list is not sorted in the overlapping range. m_images.appendList( newImages ); } for( DB::ImageInfoListConstIterator imageIt = images.constBegin(); imageIt != images.constEnd(); ++imageIt ) { DB::ImageInfoPtr info = *imageIt; info->addCategoryInfo( i18n( "Media Type" ), info->mediaType() == DB::Image ? i18n( "Image" ) : i18n( "Video" ) ); } emit totalChanged( m_images.count() ); emit dirty(); } void XMLDB::Database::renameImage( DB::ImageInfoPtr info, const DB::FileName& newName ) { info->delaySavingChanges(false); info->setFileName(newName); } DB::ImageInfoPtr XMLDB::Database::info( const DB::FileName& fileName ) const { typedef QHash<QString, DB::ImageInfoPtr > Cache; static Cache fileMap; if ( fileName.isNull() ) return DB::ImageInfoPtr(); const QString name = fileName.absolute(); Cache::iterator lookup = fileMap.find(name); if ( lookup != fileMap.end() ) return *lookup; else { fileMap.clear(); for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it ) { fileMap.insert( (*it)->fileName().absolute(), *it ); } if ( fileMap.contains( name ) ) return fileMap[ name ]; } if (fileMap.contains( name )) return DB::ImageInfoPtr(new DB::ImageInfo); else return DB::ImageInfoPtr(); } bool XMLDB::Database::rangeInclude( DB::ImageInfoPtr info ) const { if (m_selectionRange.start().isNull() ) return true; DB::ImageDate::MatchType tp = info->date().isIncludedIn( m_selectionRange ); if ( m_includeFuzzyCounts ) return ( tp == DB::ImageDate::ExactMatch || tp == DB::ImageDate::RangeMatch ); else return ( tp == DB::ImageDate::ExactMatch ); } DB::MemberMap& XMLDB::Database::memberMap() { return m_members; } void XMLDB::Database::save( const QString& fileName, bool isAutoSave ) { FileWriter saver( this ); saver.save( fileName, isAutoSave ); } DB::MD5Map* XMLDB::Database::md5Map() { return &m_md5map; } bool XMLDB::Database::isBlocking( const DB::FileName& fileName ) { return m_blockList.contains( fileName ); } DB::FileNameList XMLDB::Database::images() { return m_images.files(); } DB::FileNameList XMLDB::Database::search( const DB::ImageSearchInfo& info, bool requireOnDisk) const { return searchPrivate( info, requireOnDisk, true ); } DB::FileNameList XMLDB::Database::searchPrivate( const DB::ImageSearchInfo& info, bool requireOnDisk, bool onlyItemsMatchingRange) const { // When searching for images counts for the datebar, we want matches outside the range too. // When searching for images for the thumbnail view, we only want matches inside the range. DB::FileNameList result; for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it ) { bool match = !(*it)->isLocked() && info.match( *it ) && ( !onlyItemsMatchingRange || rangeInclude( *it )); match &= !requireOnDisk || DB::ImageInfo::imageOnDisk( (*it)->fileName() ); if (match) result.append((*it)->fileName()); } return result; } void XMLDB::Database::sortAndMergeBackIn(const DB::FileNameList& fileNameList) { DB::ImageInfoList infoList; Q_FOREACH( const DB::FileName &fileName, fileNameList ) infoList.append(fileName.info()); m_images.sortAndMergeBackIn(infoList); } DB::CategoryCollection* XMLDB::Database::categoryCollection() { return &m_categoryCollection; } QExplicitlySharedDataPointer<DB::ImageDateCollection> XMLDB::Database::rangeCollection() { return QExplicitlySharedDataPointer<DB::ImageDateCollection>( new XMLImageDateCollection( searchPrivate( Browser::BrowserWidget::instance()->currentContext(), false, false))); } void XMLDB::Database::reorder( const DB::FileName& item, const DB::FileNameList& selection, bool after) { Q_ASSERT(!item.isNull()); DB::ImageInfoList list = takeImagesFromSelection(selection); insertList(item, list, after ); } // Remove all the images from the database that match the given selection and // return that sublist. // This returns the selected and erased images in the order in which they appear // in the image list itself. DB::ImageInfoList XMLDB::Database::takeImagesFromSelection(const DB::FileNameList& selection) { DB::ImageInfoList result; if (selection.isEmpty()) return result; // iterate over all images (expensive!!) TODO: improve? for( DB::ImageInfoListIterator it = m_images.begin(); it != m_images.end(); /**/ ) { const DB::FileName imagefile = (*it)->fileName(); DB::FileNameList::const_iterator si = selection.begin(); // for each image, iterate over selection, break on match for ( /**/; si != selection.end(); ++si ) { const DB::FileName file = *si; if ( imagefile == file ) { break; } } // if image is not in selection, simply advance to next, if not add to result and erase if (si == selection.end()) { ++it; } else { result << *it; it = m_images.erase(it); } // if all images from selection are in result (size of lists is equal) break. if (result.size() == selection.size()) break; } return result; } void XMLDB::Database::insertList( const DB::FileName& fileName, const DB::ImageInfoList& list, bool after) { DB::ImageInfoListIterator imageIt = m_images.begin(); for( ; imageIt != m_images.end(); ++imageIt ) { if ( (*imageIt)->fileName() == fileName ) { break; } } // since insert() inserts before iterator increment when inserting AFTER image if ( after ) imageIt++; for( DB::ImageInfoListConstIterator it = list.begin(); it != list.end(); ++it ) { // the call to insert() destroys the given iterator so use the new one after the call imageIt = m_images.insert( imageIt, *it ); // increment always to retain order of selected images imageIt++; } emit dirty(); } bool XMLDB::Database::stack(const DB::FileNameList& items) { unsigned int changed = 0; QSet<DB::StackID> stacks; QList<DB::ImageInfoPtr> images; unsigned int stackOrder = 1; Q_FOREACH(const DB::FileName& fileName, items) { DB::ImageInfoPtr imgInfo = fileName.info(); Q_ASSERT( imgInfo ); if ( imgInfo->isStacked() ) { stacks << imgInfo->stackId(); stackOrder = qMax( stackOrder, imgInfo->stackOrder() + 1 ); } else { images << imgInfo; } } if ( stacks.size() > 1 ) return false; // images already in different stacks -> can't stack DB::StackID stackId = ( stacks.size() == 1 ) ? *(stacks.begin() ) : m_nextStackId++; Q_FOREACH( DB::ImageInfoPtr info, images ) { info->setStackOrder( stackOrder ); info->setStackId( stackId ); m_stackMap[stackId].append(info->fileName()); ++changed; ++stackOrder; } if ( changed ) emit dirty(); return changed; } void XMLDB::Database::unstack(const DB::FileNameList& items) { Q_FOREACH(const DB::FileName& fileName, items) { DB::FileNameList allInStack = getStackFor(fileName); if (allInStack.size() <= 2) { // we're destroying stack here Q_FOREACH(const DB::FileName& stackFileName, allInStack) { DB::ImageInfoPtr imgInfo = stackFileName.info(); Q_ASSERT( imgInfo ); if ( imgInfo->isStacked() ) { m_stackMap.remove( imgInfo->stackId() ); imgInfo->setStackId( 0 ); imgInfo->setStackOrder( 0 ); } } } else { DB::ImageInfoPtr imgInfo = fileName.info(); Q_ASSERT( imgInfo ); if ( imgInfo->isStacked() ) { m_stackMap[imgInfo->stackId()].removeAll(fileName); imgInfo->setStackId( 0 ); imgInfo->setStackOrder( 0 ); } } } if (!items.isEmpty()) emit dirty(); } DB::FileNameList XMLDB::Database::getStackFor(const DB::FileName& referenceImg) const { DB::ImageInfoPtr imageInfo = info( referenceImg ); if ( !imageInfo || ! imageInfo->isStacked() ) return DB::FileNameList(); StackMap::iterator found = m_stackMap.find(imageInfo->stackId()); if ( found != m_stackMap.end() ) return found.value(); // it wasn't in the cache -> rebuild it m_stackMap.clear(); for( DB::ImageInfoListConstIterator it = m_images.constBegin(); it != m_images.constEnd(); ++it ) { if ( (*it)->isStacked() ) { DB::StackID stackid = (*it)->stackId(); m_stackMap[stackid].append((*it)->fileName()); } } found = m_stackMap.find(imageInfo->stackId()); if ( found != m_stackMap.end() ) return found.value(); else return DB::FileNameList(); } void XMLDB::Database::copyData(const DB::FileName &from, const DB::FileName &to) { (*info(to)).merge(*info(from)); } int XMLDB::Database::fileVersion() { // File format version, bump it up every time the format for the file changes. return 7; } // During profiling of loading, I found that a significant amount of time was spent in QDateTime::fromString. // Reviewing the code, I fount that it did a lot of extra checks we don't need (like checking if the string have // timezone information (which they won't in KPA), this function is a replacement that is faster than the original. QDateTime dateTimeFromString(const QString& str) { static QChar T = QChar::fromLatin1('T'); if ( str[10] == T) return QDateTime(QDate::fromString(str.left(10), Qt::ISODate),QTime::fromString(str.mid(11),Qt::ISODate)); else return QDateTime::fromString(str,Qt::ISODate); } DB::ImageInfoPtr XMLDB::Database::createImageInfo( const DB::FileName& fileName, ReaderPtr reader, Database* db, const QMap<QString,QString> *newToOldCategory ) { static QString _label_ = QString::fromUtf8("label"); static QString _description_ = QString::fromUtf8("description"); static QString _startDate_ = QString::fromUtf8("startDate"); static QString _endDate_ = QString::fromUtf8("endDate"); static QString _yearFrom_ = QString::fromUtf8("yearFrom"); static QString _monthFrom_ = QString::fromUtf8("monthFrom"); static QString _dayFrom_ = QString::fromUtf8("dayFrom"); static QString _hourFrom_ = QString::fromUtf8("hourFrom"); static QString _minuteFrom_ = QString::fromUtf8("minuteFrom"); static QString _secondFrom_ = QString::fromUtf8("secondFrom"); static QString _yearTo_ = QString::fromUtf8("yearTo"); static QString _monthTo_ = QString::fromUtf8("monthTo"); static QString _dayTo_ = QString::fromUtf8("dayTo"); static QString _angle_ = QString::fromUtf8("angle"); static QString _md5sum_ = QString::fromUtf8("md5sum"); static QString _width_ = QString::fromUtf8("width"); static QString _height_ = QString::fromUtf8("height"); static QString _rating_ = QString::fromUtf8("rating"); static QString _stackId_ = QString::fromUtf8("stackId"); static QString _stackOrder_ = QString::fromUtf8("stackOrder"); static QString _gpsPrec_ = QString::fromUtf8("gpsPrec"); static QString _gpsLon_ = QString::fromUtf8("gpsLon"); static QString _gpsLat_ = QString::fromUtf8("gpsLat"); static QString _gpsAlt_ = QString::fromUtf8("gpsAlt"); static QString _videoLength_ = QString::fromUtf8("videoLength"); static QString _options_ = QString::fromUtf8("options"); static QString _0_ = QString::fromUtf8("0"); static QString _minus1_ = QString::fromUtf8("-1"); static QString _MediaType_ = i18n("Media Type"); static QString _Image_ = i18n("Image"); static QString _Video_ = i18n("Video"); QString label = reader->attribute(_label_); QString description; if ( reader->hasAttribute(_description_) ) description = reader->attribute(_description_); DB::ImageDate date; if ( reader->hasAttribute(_startDate_) ) { QDateTime start; QDateTime end; QString str = reader->attribute( _startDate_ ); if ( !str.isEmpty() ) start = dateTimeFromString( str ); str = reader->attribute( _endDate_ ); if ( !str.isEmpty() ) end = dateTimeFromString(str); date = DB::ImageDate( start, end ); } else { int yearFrom = 0, monthFrom = 0, dayFrom = 0, yearTo = 0, monthTo = 0, dayTo = 0, hourFrom = -1, minuteFrom = -1, secondFrom = -1; yearFrom = reader->attribute( _yearFrom_, _0_ ).toInt(); monthFrom = reader->attribute( _monthFrom_, _0_ ).toInt(); dayFrom = reader->attribute( _dayFrom_, _0_ ).toInt(); hourFrom = reader->attribute( _hourFrom_, _minus1_ ).toInt(); minuteFrom = reader->attribute( _minuteFrom_, _minus1_ ).toInt(); secondFrom = reader->attribute( _secondFrom_, _minus1_ ).toInt(); yearTo = reader->attribute( _yearTo_, _0_ ).toInt(); monthTo = reader->attribute( _monthTo_, _0_ ).toInt(); dayTo = reader->attribute( _dayTo_, _0_ ).toInt(); date = DB::ImageDate( yearFrom, monthFrom, dayFrom, yearTo, monthTo, dayTo, hourFrom, minuteFrom, secondFrom ); } int angle = reader->attribute( _angle_, _0_).toInt(); DB::MD5 md5sum(reader->attribute( _md5sum_ )); s_anyImageWithEmptySize |= !reader->hasAttribute(_width_); int w = reader->attribute( _width_ , _minus1_ ).toInt(); int h = reader->attribute( _height_ , _minus1_ ).toInt(); QSize size = QSize( w,h ); DB::MediaType mediaType = Utilities::isVideo(fileName) ? DB::Video : DB::Image; short rating = reader->attribute( _rating_, _minus1_ ).toShort(); DB::StackID stackId = reader->attribute( _stackId_, _0_ ).toULong(); unsigned int stackOrder = reader->attribute( _stackOrder_, _0_ ).toULong(); DB::ImageInfo* info = new DB::ImageInfo( fileName, label, description, date, angle, md5sum, size, mediaType, rating, stackId, stackOrder ); if ( reader->hasAttribute(_videoLength_)) info->setVideoLength(reader->attribute(_videoLength_).toInt()); DB::ImageInfoPtr result(info); possibleLoadCompressedCategories( reader, result, db, newToOldCategory ); while( reader->readNextStartOrStopElement(_options_).isStartToken) { readOptions( result, reader, newToOldCategory ); } info->addCategoryInfo( _MediaType_, info->mediaType() == DB::Image ? _Image_ : _Video_ ); return result; } void XMLDB::Database::readOptions( DB::ImageInfoPtr info, ReaderPtr reader, const QMap<QString,QString> *newToOldCategory ) { static QString _name_ = QString::fromUtf8("name"); static QString _value_ = QString::fromUtf8("value"); static QString _option_ = QString::fromUtf8("option"); static QString _area_ = QString::fromUtf8("area"); while (reader->readNextStartOrStopElement(_option_).isStartToken) { QString name = FileReader::unescape( reader->attribute(_name_) ); // If the silent update to db version 6 has been done, use the updated category names. if (newToOldCategory) { name = newToOldCategory->key(name,name); } if ( !name.isNull() ) { // Read values while (reader->readNextStartOrStopElement(_value_).isStartToken) { QString value = reader->attribute(_value_); if (reader->hasAttribute(_area_)) { QStringList areaData = reader->attribute(_area_).split(QString::fromUtf8(" ")); int x = areaData[0].toInt(); int y = areaData[1].toInt(); int w = areaData[2].toInt(); int h = areaData[3].toInt(); QRect area = QRect(QPoint(x, y), QPoint(x + w - 1, y + h - 1)); if (! value.isNull()) { info->addCategoryInfo(name, value, area); } } else { if (! value.isNull()) { info->addCategoryInfo(name, value); } } reader->readEndElement(); } } } } void XMLDB::Database::possibleLoadCompressedCategories( ReaderPtr reader, DB::ImageInfoPtr info, Database* db, const QMap<QString,QString> *newToOldCategory ) { if ( db == nullptr ) return; Q_FOREACH( const DB::CategoryPtr categoryPtr, db->m_categoryCollection.categories() ) { QString categoryName = categoryPtr->name(); QString oldCategoryName; if ( newToOldCategory ) { // translate to old categoryName, defaulting to the original name if not found: oldCategoryName = newToOldCategory->value( categoryName, categoryName ); } else { oldCategoryName = categoryName; } QString str = reader->attribute( FileWriter::escape( oldCategoryName ) ); if ( !str.isEmpty() ) { QStringList list = str.split(QString::fromLatin1( "," ), QString::SkipEmptyParts ); Q_FOREACH( const QString &tagString, list ) { int id = tagString.toInt(); QString name = static_cast<const XMLCategory*>(categoryPtr.data())->nameForId(id); info->addCategoryInfo( categoryName, name ); } } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLCategory.cpp b/XMLDB/XMLCategory.cpp index 56e4bcb6..a7baf212 100644 --- a/XMLDB/XMLCategory.cpp +++ b/XMLDB/XMLCategory.cpp @@ -1,217 +1,216 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "XMLCategory.h" #include "DB/ImageDB.h" #include "DB/MemberMap.h" #include "Utilities/List.h" XMLDB::XMLCategory::XMLCategory( const QString& name, const QString& icon, ViewType type, int thumbnailSize, bool show, bool positionable ) : m_name( name ), m_icon( icon ), m_show( show ), m_type( type ), m_thumbnailSize( thumbnailSize ), m_positionable ( positionable ), m_categoryType(DB::Category::PlainCategory), m_shouldSave( true ) { } QString XMLDB::XMLCategory::name() const { return m_name; } void XMLDB::XMLCategory::setName( const QString& name ) { m_name = name; } void XMLDB::XMLCategory::setPositionable( bool positionable ) { if ( positionable != m_positionable ) { m_positionable = positionable; emit changed(); } } bool XMLDB::XMLCategory::positionable() const { return m_positionable; } QString XMLDB::XMLCategory::iconName() const { return m_icon; } void XMLDB::XMLCategory::setIconName( const QString& name ) { m_icon = name; emit changed(); } void XMLDB::XMLCategory::setViewType( ViewType type ) { m_type = type; emit changed(); } XMLDB::XMLCategory::ViewType XMLDB::XMLCategory::viewType() const { return m_type; } void XMLDB::XMLCategory::setDoShow( bool b ) { m_show = b; emit changed(); } bool XMLDB::XMLCategory::doShow() const { return m_show; } void XMLDB::XMLCategory::setType(DB::Category::CategoryType t) { m_categoryType = t; } DB::Category::CategoryType XMLDB::XMLCategory::type() const { return m_categoryType; } bool XMLDB::XMLCategory::isSpecialCategory() const { return m_categoryType != DB::Category::PlainCategory; } void XMLDB::XMLCategory::addOrReorderItems( const QStringList& items ) { m_items = Utilities::mergeListsUniqly(items, m_items); } void XMLDB::XMLCategory::setItems( const QStringList& items ) { m_items = items; } void XMLDB::XMLCategory::removeItem( const QString& item ) { m_items.removeAll( item ); m_nameMap.remove(idForName(item)); m_idMap.remove(item); emit itemRemoved( item ); } void XMLDB::XMLCategory::renameItem( const QString& oldValue, const QString& newValue ) { int id = idForName(oldValue); m_items.removeAll( oldValue ); m_nameMap.remove(id); m_idMap.remove(oldValue); addItem( newValue ); setIdMapping(newValue,id); emit itemRenamed( oldValue, newValue ); } void XMLDB::XMLCategory::addItem( const QString& item ) { // for the "SortLastUsed" functionality in ListSelect we remove the item and insert it again: if (m_items.contains( item )) m_items.removeAll(item); m_items.prepend(item); } QStringList XMLDB::XMLCategory::items() const { return m_items; } int XMLDB::XMLCategory::idForName( const QString& name ) const { return m_idMap[name]; } /** * @brief Make sure that the id/name mapping is a full mapping. */ void XMLDB::XMLCategory::initIdMap() { // find maximum id // obviously, this will leave gaps in numbering when tags are deleted // assuming that tags are seldomly removed this should not be a problem int i = 0; if (!m_nameMap.empty()) { i = m_nameMap.lastKey(); } Q_FOREACH( const QString &tag, m_items ) { if (!m_idMap.contains(tag)) setIdMapping(tag, ++i); } const QStringList groups = DB::ImageDB::instance()->memberMap().groups(m_name); Q_FOREACH( const QString &group, groups ) { if ( !m_idMap.contains( group ) ) setIdMapping(group, ++i); } } void XMLDB::XMLCategory::setIdMapping( const QString& name, int id ) { m_nameMap.insert( id, name ); m_idMap.insert( name, id); } QString XMLDB::XMLCategory::nameForId( int id ) const { return m_nameMap[id]; } void XMLDB::XMLCategory::setThumbnailSize( int size ) { m_thumbnailSize = size; emit changed(); } int XMLDB::XMLCategory::thumbnailSize() const { return m_thumbnailSize; } bool XMLDB::XMLCategory::shouldSave() { return m_shouldSave; } void XMLDB::XMLCategory::setShouldSave( bool b) { m_shouldSave = b; } void XMLDB::XMLCategory::setBirthDate(const QString &item, const QDate &birthDate) { m_birthDates.insert(item,birthDate); } QDate XMLDB::XMLCategory::birthDate(const QString &item) const { return m_birthDates[item]; } -#include "XMLCategory.moc" // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLCategoryCollection.cpp b/XMLDB/XMLCategoryCollection.cpp index 44c170f4..75481ac1 100644 --- a/XMLDB/XMLCategoryCollection.cpp +++ b/XMLDB/XMLCategoryCollection.cpp @@ -1,110 +1,110 @@ -/* Copyright (C) 2003-2010 Jesper K. Pedersen <blackie@kde.org> +/* Copyright (C) 2003-2018 Jesper K. Pedersen <blackie@kde.org> 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 "XMLCategoryCollection.h" #include <QList> #include <DB/ImageDB.h> #include "XMLCategory.h" DB::CategoryPtr XMLDB::XMLCategoryCollection::categoryForName( const QString& name ) const { for( QList<DB::CategoryPtr>::ConstIterator it = m_categories.begin(); it != m_categories.end(); ++it ) { if ((*it)->name() == name) return *it; } return DB::CategoryPtr(); } void XMLDB::XMLCategoryCollection::addCategory( DB::CategoryPtr category ) { m_categories.append( category ); if (category->isSpecialCategory()) { m_specialCategories[category->type()] = category; } connect( category.data(), SIGNAL(changed()), this, SIGNAL(categoryCollectionChanged()) ); connect( category.data(), SIGNAL(itemRemoved(QString)), this, SLOT(itemRemoved(QString)) ); connect( category.data(), SIGNAL(itemRenamed(QString,QString)), this, SLOT(itemRenamed(QString,QString)) ); emit categoryCollectionChanged(); } QStringList XMLDB::XMLCategoryCollection::categoryNames() const { QStringList res; for( QList<DB::CategoryPtr>::ConstIterator it = m_categories.begin(); it != m_categories.end(); ++it ) { res.append( (*it)->name() ); } return res; } QStringList XMLDB::XMLCategoryCollection::categoryTexts() const { QStringList res; for( QList<DB::CategoryPtr>::ConstIterator it = m_categories.begin(); it != m_categories.end(); ++it ) { res.append((*it)->name()); } return res; } void XMLDB::XMLCategoryCollection::removeCategory( const QString& name ) { for( QList<DB::CategoryPtr>::iterator it = m_categories.begin(); it != m_categories.end(); ++it ) { if ( (*it)->name() == name ) { m_categories.erase(it); emit categoryRemoved(name); emit categoryCollectionChanged(); return; } } Q_ASSERT_X( false, "removeCategory", "trying to remove non-existing category" ); } void XMLDB::XMLCategoryCollection::rename( const QString& oldName, const QString& newName ) { categoryForName(oldName)->setName(newName); DB::ImageDB::instance()->renameCategory( oldName, newName ); emit categoryCollectionChanged(); } QList<DB::CategoryPtr> XMLDB::XMLCategoryCollection::categories() const { return m_categories; } void XMLDB::XMLCategoryCollection::addCategory( const QString& text, const QString& icon, DB::Category::ViewType type, int thumbnailSize, bool show, bool positionable ) { addCategory( DB::CategoryPtr( new XMLCategory( text, icon, type, thumbnailSize, show, positionable ) ) ); } DB::CategoryPtr XMLDB::XMLCategoryCollection::categoryForSpecial(const DB::Category::CategoryType type) const { return m_specialCategories[type]; } void XMLDB::XMLCategoryCollection::initIdMap() { Q_FOREACH( DB::CategoryPtr categoryPtr, m_categories ) { static_cast<XMLCategory*>(categoryPtr.data())->initIdMap(); } } -#include "XMLCategoryCollection.moc" // vi:expandtab:tabstop=4 shiftwidth=4: