diff --git a/AnnotationDialog/CompletableLineEdit.cpp b/AnnotationDialog/CompletableLineEdit.cpp index 6b7076f0..5644cc85 100644 --- a/AnnotationDialog/CompletableLineEdit.cpp +++ b/AnnotationDialog/CompletableLineEdit.cpp @@ -1,252 +1,254 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "CompletableLineEdit.h" + #include "ListSelect.h" #include "ResizableFrame.h" + #include #include #include #include AnnotationDialog::CompletableLineEdit::CompletableLineEdit(ListSelect *parent) : KLineEdit(parent) , m_listSelect(parent) { } AnnotationDialog::CompletableLineEdit::CompletableLineEdit(AnnotationDialog::ListSelect *ls, QWidget *parent) : KLineEdit(parent) , m_listSelect(ls) { } void AnnotationDialog::CompletableLineEdit::setListView(QTreeWidget *listView) { m_listView = listView; } void AnnotationDialog::CompletableLineEdit::setMode(UsageMode mode) { m_mode = mode; } void AnnotationDialog::CompletableLineEdit::keyPressEvent(QKeyEvent *ev) { if (ev->key() == Qt::Key_Down || ev->key() == Qt::Key_Up) { selectPrevNextMatch(ev->key() == Qt::Key_Down); return; } if (m_mode == SearchMode && (ev->key() == Qt::Key_Return || ev->key() == Qt::Key_Enter)) { //Confirm autocomplete, deselect all text handleSpecialKeysInSearch(ev); m_listSelect->showOnlyItemsMatching(QString()); // Show all again after confirming autocomplete suggestion. return; } if (m_mode != SearchMode && isSpecialKey(ev)) return; // Don't insert the special character. if (ev->key() == Qt::Key_Space && ev->modifiers() & Qt::ControlModifier) { mergePreviousImageSelection(); return; } QString prevContent = text(); if (ev->text().isEmpty() || !ev->text()[0].isPrint()) { // If final Return is handled by the default implementation, // it can "leak" to other widgets. So we swallow it here: if (ev->key() == Qt::Key_Return || ev->key() == Qt::Key_Enter) emit KLineEdit::returnPressed(text()); else KLineEdit::keyPressEvent(ev); if (prevContent != text()) m_listSelect->showOnlyItemsMatching(text()); return; } // &,|, or ! should result in the current item being inserted if (m_mode == SearchMode && isSpecialKey(ev)) { handleSpecialKeysInSearch(ev); m_listSelect->showOnlyItemsMatching(QString()); // Show all again after a special caracter. return; } int cursorPos = cursorPosition(); int selStart = selectionStart(); KLineEdit::keyPressEvent(ev); // Find the text of the current item int itemStart = 0; QString input = text(); if (m_mode == SearchMode) { input = input.left(cursorPosition()); itemStart = input.lastIndexOf(QRegExp(QString::fromLatin1("[!&|]"))) + 1; if (itemStart > 0) { itemStart++; } input = input.mid(itemStart); } // Find the text in the listView QTreeWidgetItem *item = findItemInListView(input); if (!item && m_mode == SearchMode) { // revert setText(prevContent); setCursorPosition(cursorPos); item = findItemInListView(input); if (selStart >= 0) setSelection(selStart, prevContent.length()); // Reset previous selection. } if (item) { selectItemAndUpdateLineEdit(item, itemStart, input); m_listSelect->showOnlyItemsMatching(input); } else if (m_mode != SearchMode) m_listSelect->showOnlyItemsMatching(input); } /** * Search for the first item in the appearance order, which matches text. */ QTreeWidgetItem *AnnotationDialog::CompletableLineEdit::findItemInListView(const QString &text) { for (QTreeWidgetItemIterator itemIt(m_listView); *itemIt; ++itemIt) { // Hide the "untagged image" tag from the auto-completion if ((*itemIt)->isHidden()) { continue; } if (itemMatchesText(*itemIt, text)) { return *itemIt; } } return nullptr; } bool AnnotationDialog::CompletableLineEdit::itemMatchesText(QTreeWidgetItem *item, const QString &text) { return item->text(0).toLower().startsWith(text.toLower()); } bool AnnotationDialog::CompletableLineEdit::isSpecialKey(QKeyEvent *ev) { return (ev->text() == QString::fromLatin1("&") || ev->text() == QString::fromLatin1("|") || ev->text() == QString::fromLatin1("!") /* || ev->text() == "(" */ ); } void AnnotationDialog::CompletableLineEdit::handleSpecialKeysInSearch(QKeyEvent *ev) { int cursorPos = cursorPosition(); QString txt; int additionalLength; if (!isSpecialKey(ev)) { txt = text().left(cursorPos) + ev->text() + text().mid(cursorPos); additionalLength = 0; } else { txt = text() + QString::fromUtf8(" %1 ").arg(ev->text()); cursorPos += 2; additionalLength = 2; } setText(txt); if (!isSpecialKey(ev)) { //Special handling for ENTER to position the cursor correctly setText(text().left(text().size() - 1)); cursorPos--; } setCursorPosition(cursorPos + ev->text().length() + additionalLength); deselect(); // Select the item in the listView - not perfect but acceptable for now. int start = txt.lastIndexOf(QRegExp(QString::fromLatin1("[!&|]")), cursorPosition() - 2) + 1; if (start > 0) { start++; } QString input = txt.mid(start, cursorPosition() - start); if (!input.isEmpty()) { QTreeWidgetItem *item = findItemInListView(input); if (item) { item->setCheckState(0, Qt::Checked); } } } void AnnotationDialog::CompletableLineEdit::selectPrevNextMatch(bool next) { QTreeWidgetItem *item { nullptr }; // the current item is usually the selected one... QList selectedItems = m_listView->selectedItems(); if (!selectedItems.isEmpty()) item = selectedItems.at(0); // ...except when the selected one is filtered out: if (!item || item->isHidden()) { // in that case, we select the first item in the viewport item = m_listView->itemAt(0, 0); } if (!item) return; QTreeWidgetItem *baseItem = item; // only go to the next item, if there was a "previous" selected item: if (!selectedItems.isEmpty()) { if (next) item = m_listView->itemBelow(item); else item = m_listView->itemAbove(item); } // select current item if there is no next/prev item: if (!item) { item = baseItem; } // extract last component of line edit int itemStart = text().lastIndexOf(QRegExp(QString::fromLatin1("[!&|]"))) + 1; QString input = text().mid(itemStart); selectItemAndUpdateLineEdit(item, itemStart, text().left(selectionStart())); } void AnnotationDialog::CompletableLineEdit::selectItemAndUpdateLineEdit(QTreeWidgetItem *item, const int itemStart, const QString &inputText) { m_listView->setCurrentItem(item); m_listView->scrollToItem(item); QString txt = text().left(itemStart) + item->text(0) + text().mid(cursorPosition()); setText(txt); setSelection(itemStart + inputText.length(), item->text(0).length() - inputText.length()); } void AnnotationDialog::CompletableLineEdit::mergePreviousImageSelection() { } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/CompletableLineEdit.h b/AnnotationDialog/CompletableLineEdit.h index 91c467fe..bf12709e 100644 --- a/AnnotationDialog/CompletableLineEdit.h +++ b/AnnotationDialog/CompletableLineEdit.h @@ -1,64 +1,65 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef ANNOTATIONDIALOG_COMPLETABLELINEEDIT_H #define ANNOTATIONDIALOG_COMPLETABLELINEEDIT_H #include "enums.h" + #include class QKeyEvent; class QTreeWidget; class QTreeWidgetItem; namespace AnnotationDialog { class ListSelect; class ResizableFrame; class CompletableLineEdit : public KLineEdit { public: /** * @brief This is just a convenience constructor for the common use-case when the lineEdit is inside a ListSelect. * @param parent */ explicit CompletableLineEdit(ListSelect *parent); explicit CompletableLineEdit(ListSelect *ls, QWidget *parent); void setListView(QTreeWidget *); void setMode(UsageMode mode); void keyPressEvent(QKeyEvent *ev) override; protected: QTreeWidgetItem *findItemInListView(const QString &startWith); bool isSpecialKey(QKeyEvent *); void handleSpecialKeysInSearch(QKeyEvent *); bool itemMatchesText(QTreeWidgetItem *item, const QString &text); void selectPrevNextMatch(bool next); void selectItemAndUpdateLineEdit(QTreeWidgetItem *item, int itemStart, const QString &inputText); void mergePreviousImageSelection(); private: QTreeWidget *m_listView; UsageMode m_mode; ListSelect *m_listSelect; }; } #endif /* ANNOTATIONDIALOG_COMPLETABLELINEEDIT_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/DateEdit.cpp b/AnnotationDialog/DateEdit.cpp index 95b191c8..e15113b8 100644 --- a/AnnotationDialog/DateEdit.cpp +++ b/AnnotationDialog/DateEdit.cpp @@ -1,351 +1,350 @@ /* 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 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 = QLocale().dayName(i, QLocale::LongFormat).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/DateEdit.h b/AnnotationDialog/DateEdit.h index 0b766017..7e8cc51d 100644 --- a/AnnotationDialog/DateEdit.h +++ b/AnnotationDialog/DateEdit.h @@ -1,155 +1,156 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** * 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 */ #ifndef ANNOTATIONDIALOG_DATEEDIT_H #define ANNOTATIONDIALOG_DATEEDIT_H -#include "DB/ImageDate.h" +#include + #include #include #include #include class QEvent; class KDatePicker; namespace AnnotationDialog { class DateEdit : public QComboBox { Q_OBJECT public: explicit DateEdit(bool isStartEdit, QWidget *parent = nullptr); ~DateEdit() override; /** @return True if the date in the text edit is valid, * false otherwise. This will not modify the display of the date, * but only check for validity. */ bool inputIsValid() const; /** @return The date entered. This will not * modify the display of the date, but only return it. */ QDate date() const; /** Sets the date. * * @param date The new date to display. This date must be valid or * it will not be displayed. */ void setDate(const QDate &date); /** @return The default date used if no valid date has been set or entered. */ QDate defaultDate() const; /** Sets the default date to use if no valid date has been set or entered. * If no default date has been set, the current date is used as the default. * @param date The default date. */ void setDefaultDate(const QDate &date); /** @param handleInvalid If true the date edit accepts invalid dates * and displays them as the empty ("") string. It also returns an invalid date. * If false (default) invalid dates are not accepted and instead the date * of today will be returned. */ void setHandleInvalid(bool handleInvalid); /** @return True if the widget is accepts invalid dates, false otherwise. */ bool handlesInvalid() const; /** Sets whether the widget is read-only for the user. If read-only, the date * picker pop-up is inactive, and the displayed date cannot be edited. * @param readOnly True to set the widget read-only, false to set it read-write. */ void setReadOnly(bool readOnly); /** @return True if the widget is read-only, false if read-write. */ bool isReadOnly() const; /** Called when a new date has been entered, to validate its value. * @param newDate The new date which has been entered. * @return True to accept the new date, false to reject the new date. * If false is returned, the value reverts to what it was before the * new date was entered. */ virtual bool validate(const QDate &newDate); void showPopup() override; signals: /** This signal is emitted whenever the user modifies the date. This * may not get emitted until the user presses enter in the line edit or * focus leaves the widget (i.e. the user confirms their selection). */ void dateChanged(QDate); void dateChanged(const DB::ImageDate &); /** This signal is emitted whenever the user enters an invalid date. */ void invalidDateEntered(); protected slots: void dateSelected(QDate); void dateEntered(QDate); void lineEnterPressed(); void slotTextChanged(const QString &); void mousePressEvent(QMouseEvent *) override; private: void keyPressEvent(QKeyEvent *event) override; bool eventFilter(QObject *o, QEvent *e) override; bool readDate(QDate &result, QDate *end) const; /** Maps the text that the user can enter to the offset in days from * today. For example, the text 'tomorrow' is mapped to +1. */ QMap m_KeywordMap; bool m_TextChanged; bool m_HandleInvalid; KDatePicker *m_DatePicker; QFrame *m_DateFrame; QDate m_defaultValue; QDate m_value; bool m_ReadOnly; bool m_DiscardNextMousePress; bool m_IsStartEdit; }; } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/DescriptionEdit.cpp b/AnnotationDialog/DescriptionEdit.cpp index fd64176c..85e97d3e 100644 --- a/AnnotationDialog/DescriptionEdit.cpp +++ b/AnnotationDialog/DescriptionEdit.cpp @@ -1,40 +1,41 @@ /* 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); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/Dialog.cpp b/AnnotationDialog/Dialog.cpp index 50e4cf8a..39a48225 100644 --- a/AnnotationDialog/Dialog.cpp +++ b/AnnotationDialog/Dialog.cpp @@ -1,1731 +1,1730 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "DateEdit.h" #include "DescriptionEdit.h" #include "ImagePreviewWidget.h" #include "ListSelect.h" #include "Logging.h" #include "ResizableFrame.h" #include "ShortCutManager.h" #include "ShowSelectionOnlyManager.h" #include "enums.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include - #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_KGEOMAP #include #include #include #endif #include #include #include #include #include using Utilities::StringSet; /** * \class AnnotationDialog::Dialog * \brief QDialog subclass used for tagging images */ AnnotationDialog::Dialog::Dialog(QWidget *parent) : QDialog(parent) , m_ratingChanged(false) , m_conflictText(i18n("(You have differing descriptions on individual images, setting text here will override them all)")) { Utilities::ShowBusyCursor dummy; ShortCutManager shortCutManager; // The widget stack QWidget *mainWidget = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(mainWidget); setLayout(layout); layout->addWidget(mainWidget); m_stack = new QStackedWidget(mainWidget); layout->addWidget(m_stack); // The Viewer m_fullScreenPreview = new Viewer::ViewerWidget(Viewer::ViewerWidget::InlineViewer); m_stack->addWidget(m_fullScreenPreview); // The dock widget m_dockWindow = new QMainWindow; m_stack->addWidget(m_dockWindow); m_dockWindow->setDockNestingEnabled(true); // -------------------------------------------------- Dock widgets m_generalDock = createDock(i18n("Label and Dates"), QString::fromLatin1("Label and Dates"), Qt::TopDockWidgetArea, createDateWidget(shortCutManager)); m_previewDock = createDock(i18n("Image Preview"), QString::fromLatin1("Image Preview"), Qt::TopDockWidgetArea, createPreviewWidget()); m_description = new DescriptionEdit(this); m_description->setProperty("WantsFocus", true); m_description->setObjectName(i18n("Description")); m_description->setCheckSpellingEnabled(true); m_description->setTabChangesFocus(true); // this allows tabbing to the next item in the tab order. m_description->setWhatsThis(i18nc("@info:whatsthis", "A descriptive text of the image." "If Use Exif description is enabled under " "Settings|Configure KPhotoAlbum...|General, a description " "embedded in the image Exif information is imported to this field if available.")); m_descriptionDock = createDock(i18n("Description"), QString::fromLatin1("description"), Qt::LeftDockWidgetArea, m_description); shortCutManager.addDock(m_descriptionDock, m_description); connect(m_description, SIGNAL(pageUpDownPressed(QKeyEvent *)), this, SLOT(descriptionPageUpDownPressed(QKeyEvent *))); #ifdef HAVE_KGEOMAP // -------------------------------------------------- Map representation m_annotationMapContainer = new QWidget(this); QVBoxLayout *annotationMapContainerLayout = new QVBoxLayout(m_annotationMapContainer); m_annotationMap = new Map::MapView(this); annotationMapContainerLayout->addWidget(m_annotationMap); QHBoxLayout *mapLoadingProgressLayout = new QHBoxLayout(); annotationMapContainerLayout->addLayout(mapLoadingProgressLayout); m_mapLoadingProgress = new QProgressBar(this); mapLoadingProgressLayout->addWidget(m_mapLoadingProgress); m_mapLoadingProgress->hide(); m_cancelMapLoadingButton = new QPushButton(i18n("Cancel")); mapLoadingProgressLayout->addWidget(m_cancelMapLoadingButton); m_cancelMapLoadingButton->hide(); connect(m_cancelMapLoadingButton, SIGNAL(clicked()), this, SLOT(setCancelMapLoading())); m_annotationMapContainer->setObjectName(i18n("Map")); m_mapDock = createDock( i18n("Map"), QString::fromLatin1("map"), Qt::LeftDockWidgetArea, m_annotationMapContainer); shortCutManager.addDock(m_mapDock, m_annotationMapContainer); connect(m_mapDock, SIGNAL(visibilityChanged(bool)), this, SLOT(annotationMapVisibilityChanged(bool))); m_mapDock->setWhatsThis(i18nc("@info:whatsthis", "The map widget allows you to view the location of images if GPS coordinates are found in the Exif information.")); #endif // -------------------------------------------------- Categories QList categories = DB::ImageDB::instance()->categoryCollection()->categories(); // Let's first assume we don't have positionable categories m_positionableCategories = false; for (QList::ConstIterator categoryIt = categories.constBegin(); categoryIt != categories.constEnd(); ++categoryIt) { ListSelect *sel = createListSel(*categoryIt); // Create a QMap of all ListSelect instances, so that we can easily // check if a specific (positioned) tag is (still) selected later m_listSelectList[(*categoryIt)->name()] = sel; QDockWidget *dock = createDock((*categoryIt)->name(), (*categoryIt)->name(), Qt::BottomDockWidgetArea, sel); shortCutManager.addDock(dock, sel->lineEdit()); if ((*categoryIt)->isSpecialCategory()) dock->hide(); // Pass the positionable selection to the object sel->setPositionable((*categoryIt)->positionable()); if (sel->positionable()) { connect(sel, SIGNAL(positionableTagSelected(QString, QString)), this, SLOT(positionableTagSelected(QString, QString))); connect(sel, SIGNAL(positionableTagDeselected(QString, QString)), this, SLOT(positionableTagDeselected(QString, QString))); connect(sel, SIGNAL(positionableTagRenamed(QString, QString, QString)), this, SLOT(positionableTagRenamed(QString, QString, QString))); connect(m_preview->preview(), SIGNAL(proposedTagSelected(QString, QString)), sel, SLOT(ensureTagIsSelected(QString, QString))); // We have at least one positionable category m_positionableCategories = true; } } // -------------------------------------------------- The buttons. // don't use default buttons (Ok, Cancel): QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::NoButton); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); QHBoxLayout *lay1 = new QHBoxLayout; layout->addLayout(lay1); m_revertBut = new QPushButton(i18n("Revert This Item")); KAcceleratorManager::setNoAccel(m_revertBut); lay1->addWidget(m_revertBut); m_clearBut = new QPushButton(); KGuiItem::assign(m_clearBut, KGuiItem(i18n("Clear Form"), QApplication::isRightToLeft() ? QString::fromLatin1("clear_left") : QString::fromLatin1("locationbar_erase"))); KAcceleratorManager::setNoAccel(m_clearBut); lay1->addWidget(m_clearBut); QPushButton *optionsBut = new QPushButton(i18n("Options...")); KAcceleratorManager::setNoAccel(optionsBut); lay1->addWidget(optionsBut); lay1->addStretch(1); m_okBut = new QPushButton(i18n("&Done")); lay1->addWidget(m_okBut); m_continueLaterBut = new QPushButton(i18n("Continue &Later")); lay1->addWidget(m_continueLaterBut); QPushButton *cancelBut = new QPushButton(); KGuiItem::assign(cancelBut, KStandardGuiItem::cancel()); lay1->addWidget(cancelBut); // It is unfortunately not possible to ask KAcceleratorManager not to setup the OK and cancel keys. shortCutManager.addTaken(i18nc("@action:button", "&Search")); shortCutManager.addTaken(m_okBut->text()); shortCutManager.addTaken(m_continueLaterBut->text()); shortCutManager.addTaken(cancelBut->text()); connect(m_revertBut, SIGNAL(clicked()), this, SLOT(slotRevert())); connect(m_okBut, SIGNAL(clicked()), this, SLOT(doneTagging())); connect(m_continueLaterBut, SIGNAL(clicked()), this, SLOT(continueLater())); connect(cancelBut, SIGNAL(clicked()), this, SLOT(reject())); connect(m_clearBut, SIGNAL(clicked()), this, SLOT(slotClear())); connect(optionsBut, SIGNAL(clicked()), this, SLOT(slotOptions())); connect(m_preview, SIGNAL(imageRotated(int)), this, SLOT(rotate(int))); connect(m_preview, SIGNAL(indexChanged(int)), this, SLOT(slotIndexChanged(int))); connect(m_preview, SIGNAL(imageDeleted(DB::ImageInfo)), this, SLOT(slotDeleteImage())); connect(m_preview, SIGNAL(copyPrevClicked()), this, SLOT(slotCopyPrevious())); connect(m_preview, SIGNAL(areaVisibilityChanged(bool)), this, SLOT(slotShowAreas(bool))); connect(m_preview->preview(), SIGNAL(areaCreated(ResizableFrame *)), this, SLOT(slotNewArea(ResizableFrame *))); // Disable so no button accept return (which would break with the line edits) m_revertBut->setAutoDefault(false); m_okBut->setAutoDefault(false); m_continueLaterBut->setAutoDefault(false); cancelBut->setAutoDefault(false); m_clearBut->setAutoDefault(false); optionsBut->setAutoDefault(false); m_dockWindowCleanState = m_dockWindow->saveState(); loadWindowLayout(); m_current = -1; setGeometry(Settings::SettingsData::instance()->windowGeometry(Settings::AnnotationDialog)); setupActions(); shortCutManager.setupShortCuts(); // WARNING layout->addWidget(buttonBox) must be last item in layout layout->addWidget(buttonBox); } QDockWidget *AnnotationDialog::Dialog::createDock(const QString &title, const QString &name, Qt::DockWidgetArea location, QWidget *widget) { QDockWidget *dock = new QDockWidget(title); KAcceleratorManager::setNoAccel(dock); dock->setObjectName(name); dock->setAllowedAreas(Qt::AllDockWidgetAreas); dock->setWidget(widget); m_dockWindow->addDockWidget(location, dock); m_dockWidgets.append(dock); return dock; } QWidget *AnnotationDialog::Dialog::createDateWidget(ShortCutManager &shortCutManager) { QWidget *top = new QWidget; QVBoxLayout *lay2 = new QVBoxLayout(top); // Image Label QHBoxLayout *lay3 = new QHBoxLayout; lay2->addLayout(lay3); QLabel *label = new QLabel(i18n("Label: ")); lay3->addWidget(label); m_imageLabel = new KLineEdit; m_imageLabel->setProperty("WantsFocus", true); m_imageLabel->setObjectName(i18n("Label")); lay3->addWidget(m_imageLabel); shortCutManager.addLabel(label); label->setBuddy(m_imageLabel); // Date QHBoxLayout *lay4 = new QHBoxLayout; lay2->addLayout(lay4); label = new QLabel(i18n("Date: ")); lay4->addWidget(label); m_startDate = new ::AnnotationDialog::DateEdit(true); lay4->addWidget(m_startDate, 1); connect(m_startDate, SIGNAL(dateChanged(DB::ImageDate)), this, SLOT(slotStartDateChanged(DB::ImageDate))); shortCutManager.addLabel(label); label->setBuddy(m_startDate); m_endDateLabel = new QLabel(QString::fromLatin1("-")); lay4->addWidget(m_endDateLabel); m_endDate = new ::AnnotationDialog::DateEdit(false); lay4->addWidget(m_endDate, 1); // Time m_timeLabel = new QLabel(i18n("Time: ")); lay4->addWidget(m_timeLabel); m_time = new QTimeEdit; lay4->addWidget(m_time); m_isFuzzyDate = new QCheckBox(i18n("Use Fuzzy Date")); m_isFuzzyDate->setWhatsThis(i18nc("@info", "In KPhotoAlbum, images can either have an exact date and time" ", or a fuzzy date which happened any time during" " a specified time interval. Images produced by digital cameras" " do normally have an exact date." "If you don't know exactly when a photo was taken" " (e.g. if the photo comes from an analog camera), then you should set" " Use Fuzzy Date.")); m_isFuzzyDate->setToolTip(m_isFuzzyDate->whatsThis()); lay4->addWidget(m_isFuzzyDate); lay4->addStretch(1); connect(m_isFuzzyDate, SIGNAL(stateChanged(int)), this, SLOT(slotSetFuzzyDate())); QHBoxLayout *lay8 = new QHBoxLayout; lay2->addLayout(lay8); m_megapixelLabel = new QLabel(i18n("Minimum megapixels:")); lay8->addWidget(m_megapixelLabel); m_megapixel = new QSpinBox; m_megapixel->setRange(0, 99); m_megapixel->setSingleStep(1); m_megapixelLabel->setBuddy(m_megapixel); lay8->addWidget(m_megapixel); lay8->addStretch(1); m_max_megapixelLabel = new QLabel(i18n("Maximum megapixels:")); lay8->addWidget(m_max_megapixelLabel); m_max_megapixel = new QSpinBox; m_max_megapixel->setRange(0, 99); m_max_megapixel->setSingleStep(1); m_max_megapixelLabel->setBuddy(m_max_megapixel); lay8->addWidget(m_max_megapixel); lay8->addStretch(1); QHBoxLayout *lay9 = new QHBoxLayout; lay2->addLayout(lay9); label = new QLabel(i18n("Rating:")); lay9->addWidget(label); m_rating = new KRatingWidget; m_rating->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); lay9->addWidget(m_rating, 0, Qt::AlignCenter); connect(m_rating, SIGNAL(ratingChanged(uint)), this, SLOT(slotRatingChanged(uint))); m_ratingSearchLabel = new QLabel(i18n("Rating search mode:")); lay9->addWidget(m_ratingSearchLabel); m_ratingSearchMode = new KComboBox(lay9); m_ratingSearchMode->addItems(QStringList() << i18n("==") << i18n(">=") << i18n("<=") << i18n("!=")); m_ratingSearchLabel->setBuddy(m_ratingSearchMode); lay9->addWidget(m_ratingSearchMode); // File name search pattern QHBoxLayout *lay10 = new QHBoxLayout; lay2->addLayout(lay10); m_imageFilePatternLabel = new QLabel(i18n("File Name Pattern: ")); lay10->addWidget(m_imageFilePatternLabel); m_imageFilePattern = new KLineEdit; m_imageFilePattern->setObjectName(i18n("File Name Pattern")); lay10->addWidget(m_imageFilePattern); shortCutManager.addLabel(m_imageFilePatternLabel); m_imageFilePatternLabel->setBuddy(m_imageFilePattern); m_searchRAW = new QCheckBox(i18n("Search only for RAW files")); lay2->addWidget(m_searchRAW); lay9->addStretch(1); lay2->addStretch(1); return top; } QWidget *AnnotationDialog::Dialog::createPreviewWidget() { m_preview = new ImagePreviewWidget(); connect(m_preview, &ImagePreviewWidget::togglePreview, this, &Dialog::togglePreview); return m_preview; } void AnnotationDialog::Dialog::slotRevert() { if (m_setup == InputSingleImageConfigMode) load(); } void AnnotationDialog::Dialog::slotIndexChanged(int index) { if (m_setup != InputSingleImageConfigMode) return; if (m_current >= 0) writeToInfo(); m_current = index; load(); } void AnnotationDialog::Dialog::doneTagging() { saveAndClose(); if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()) { for (DB::ImageInfoListIterator it = m_origList.begin(); it != m_origList.end(); ++it) { (*it)->removeCategoryInfo(Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag()); } } } /* * Copy tags (only tags/categories, not description/label/...) from previous image to the currently showed one */ void AnnotationDialog::Dialog::slotCopyPrevious() { if (m_setup != InputSingleImageConfigMode) return; if (m_current < 1) return; // FIXME: it would be better to compute the "previous image" in a better way, but let's stick with this for now... DB::ImageInfo &old_info = m_editList[m_current - 1]; m_positionableTagCandidates.clear(); m_lastSelectedPositionableTag.first = QString(); m_lastSelectedPositionableTag.second = QString(); Q_FOREACH (ListSelect *ls, m_optionList) { ls->setSelection(old_info.itemsOfCategory(ls->category())); // Also set all positionable tag candidates if (ls->positionable()) { QString category = ls->category(); QSet selectedTags = old_info.itemsOfCategory(category); QSet positionedTagSet = positionedTags(category); // Add the tag to the positionable candiate list, if no area is already associated with it Q_FOREACH (const auto &tag, selectedTags) { if (!positionedTagSet.contains(tag)) { addTagToCandidateList(category, tag); } } // Check all areas for a linked tag in this category that is probably not selected anymore for (ResizableFrame *area : areas()) { QPair tagData = area->tagData(); if (tagData.first == category) { if (!selectedTags.contains(tagData.second)) { // The linked tag is not selected anymore, so remove it area->removeTagData(); } } } } } } void AnnotationDialog::Dialog::load() { // Remove all areas tidyAreas(); // No areas have been changed m_areasChanged = false; // Empty the positionable tag candidate list and the last selected positionable tag m_positionableTagCandidates.clear(); m_lastSelectedPositionableTag = QPair(); DB::ImageInfo &info = m_editList[m_current]; m_startDate->setDate(info.date().start().date()); if (info.date().hasValidTime()) { m_time->show(); m_time->setTime(info.date().start().time()); m_isFuzzyDate->setChecked(false); } else { m_time->hide(); m_isFuzzyDate->setChecked(true); } if (info.date().start().date() == info.date().end().date()) m_endDate->setDate(QDate()); else m_endDate->setDate(info.date().end().date()); m_imageLabel->setText(info.label()); m_description->setPlainText(info.description()); if (m_setup == InputSingleImageConfigMode) m_rating->setRating(qMax(static_cast(0), info.rating())); m_ratingChanged = false; // A category areas have been linked against could have been deleted // or un-marked as positionable in the meantime, so ... QMap categoryIsPositionable; QList positionableCategories; Q_FOREACH (ListSelect *ls, m_optionList) { ls->setSelection(info.itemsOfCategory(ls->category())); ls->rePopulate(); // Get all selected positionable tags and add them to the candidate list if (ls->positionable()) { QSet selectedTags = ls->itemsOn(); Q_FOREACH (const QString &tagName, selectedTags) { addTagToCandidateList(ls->category(), tagName); } } // ... create a list of all categories and their positionability ... categoryIsPositionable[ls->category()] = ls->positionable(); if (ls->positionable()) { positionableCategories << ls->category(); } } // Create all tagged areas QMap> taggedAreas = info.taggedAreas(); QMapIterator> areasInCategory(taggedAreas); while (areasInCategory.hasNext()) { areasInCategory.next(); QString category = areasInCategory.key(); // ... and check if the respective category is actually there yet and still positionable // (operator[] will insert an empty item if the category has been deleted // and is thus missing in the QMap, but the respective key won't be true) if (categoryIsPositionable[category]) { QMapIterator areaData(areasInCategory.value()); while (areaData.hasNext()) { areaData.next(); QString tag = areaData.key(); // Be sure that the corresponding tag is still checked. The category could have // been un-marked as positionable in the meantime and the tag could have been // deselected, without triggering positionableTagDeselected and the area thus // still remaining. If the category is then re-marked as positionable, the area would // show up without the tag being selected. if (m_listSelectList[category]->tagIsChecked(tag)) { m_preview->preview()->createTaggedArea(category, tag, areaData.value(), m_preview->showAreas()); } } } } if (m_setup == InputSingleImageConfigMode) { setWindowTitle(i18nc("@title:window image %1 of %2 images", "Annotations (%1/%2)", m_current + 1, m_origList.count())); m_preview->canCreateAreas( m_setup == InputSingleImageConfigMode && !info.isVideo() && m_positionableCategories); #ifdef HAVE_KGEOMAP updateMapForCurrentImage(); #endif } m_preview->updatePositionableCategories(positionableCategories); } void AnnotationDialog::Dialog::writeToInfo() { Q_FOREACH (ListSelect *ls, m_optionList) { ls->slotReturn(); } DB::ImageInfo &info = m_editList[m_current]; if (!info.size().isValid()) { // The actual image size has been fetched by ImagePreview, so we can add it to // the database silenty, so that it's saved if the database will be saved. info.setSize(m_preview->preview()->getActualImageSize()); } if (m_time->isHidden()) { if (m_endDate->date().isValid()) info.setDate(DB::ImageDate(QDateTime(m_startDate->date(), QTime(0, 0, 0)), QDateTime(m_endDate->date(), QTime(23, 59, 59)))); else info.setDate(DB::ImageDate(QDateTime(m_startDate->date(), QTime(0, 0, 0)), QDateTime(m_startDate->date(), QTime(23, 59, 59)))); } else info.setDate(DB::ImageDate(QDateTime(m_startDate->date(), m_time->time()))); // Generate a list of all tagged areas QMap> areas = taggedAreas(); 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(), areas[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(); } QMap> AnnotationDialog::Dialog::taggedAreas() const { QMap> taggedAreas; foreach (ResizableFrame *area, areas()) { QPair tagData = area->tagData(); if (!tagData.first.isEmpty()) { taggedAreas[tagData.first][tagData.second] = area->actualCoordinates(); } } return taggedAreas; } 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(QStandardPaths::locate(QStandardPaths::DataLocation, QString::fromLatin1("pics/search.jpg"))); m_ratingChanged = false; showHelpDialog(SearchMode); int ok = exec(); if (ok == QDialog::Accepted) { const QDate start = m_startDate->date(); const QDate end = m_endDate->date(); m_oldSearch = DB::ImageSearchInfo(DB::ImageDate(start, end), m_imageLabel->text(), m_description->toPlainText(), m_imageFilePattern->text()); Q_FOREACH (const ListSelect *ls, m_optionList) { m_oldSearch.setCategoryMatchText(ls->category(), ls->text()); } //FIXME: for the user to search for 0-rated images, he must first change the rating to anything > 0 //then change back to 0 . if (m_ratingChanged) m_oldSearch.setRating(m_rating->rating()); m_ratingChanged = false; m_oldSearch.setSearchMode(m_ratingSearchMode->currentIndex()); m_oldSearch.setMegaPixel(m_megapixel->value()); m_oldSearch.setMaxMegaPixel(m_max_megapixel->value()); m_oldSearch.setSearchRAW(m_searchRAW->isChecked()); #ifdef HAVE_KGEOMAP const KGeoMap::GeoCoordinates::Pair regionSelection = m_annotationMap->getRegionSelection(); m_oldSearch.setRegionSelection(regionSelection); #endif return m_oldSearch; } else return DB::ImageSearchInfo(); } void AnnotationDialog::Dialog::setup() { // Repopulate the listboxes in case data has changed // An group might for example have been renamed. Q_FOREACH (ListSelect *ls, m_optionList) { ls->populate(); } if (m_setup == SearchMode) { KGuiItem::assign(m_okBut, KGuiItem(i18nc("@action:button", "&Search"), QString::fromLatin1("find"))); m_continueLaterBut->hide(); m_revertBut->hide(); m_clearBut->show(); m_preview->setSearchMode(true); setWindowTitle(i18nc("@title:window title of the 'find images' window", "Search")); loadInfo(m_oldSearch); } else { m_okBut->setText(i18n("Done")); m_continueLaterBut->show(); m_revertBut->setEnabled(m_setup == InputSingleImageConfigMode); m_clearBut->hide(); m_revertBut->show(); m_preview->setSearchMode(false); m_preview->setToggleFullscreenPreviewEnabled(m_setup == InputSingleImageConfigMode); setWindowTitle(i18nc("@title:window", "Annotations")); } Q_FOREACH (ListSelect *ls, m_optionList) { ls->setMode(m_setup); } } void AnnotationDialog::Dialog::slotClear() { loadInfo(DB::ImageSearchInfo()); } void AnnotationDialog::Dialog::loadInfo(const DB::ImageSearchInfo &info) { m_startDate->setDate(info.date().start().date()); m_endDate->setDate(info.date().end().date()); Q_FOREACH (ListSelect *ls, m_optionList) { ls->setText(info.categoryMatchText(ls->category())); } m_imageLabel->setText(info.label()); m_description->setText(info.description()); } void AnnotationDialog::Dialog::slotOptions() { // create menu entries for dock windows QMenu *menu = new QMenu(this); QMenu *dockMenu = m_dockWindow->createPopupMenu(); menu->addMenu(dockMenu) ->setText(i18n("Configure Window Layout...")); QAction *saveCurrent = dockMenu->addAction(i18n("Save Current Window Setup")); QAction *reset = dockMenu->addAction(i18n("Reset layout")); // create SortType entries menu->addSeparator(); QActionGroup *sortTypes = new QActionGroup(menu); QAction *alphaTreeSort = new QAction( SmallIcon(QString::fromLatin1("view-list-tree")), i18n("Sort Alphabetically (Tree)"), sortTypes); QAction *alphaFlatSort = new QAction( SmallIcon(QString::fromLatin1("draw-text")), i18n("Sort Alphabetically (Flat)"), sortTypes); QAction *dateSort = new QAction( SmallIcon(QString::fromLatin1("x-office-calendar")), i18n("Sort by Date"), sortTypes); alphaTreeSort->setCheckable(true); alphaFlatSort->setCheckable(true); dateSort->setCheckable(true); alphaTreeSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaTree); alphaFlatSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortAlphaFlat); dateSort->setChecked(Settings::SettingsData::instance()->viewSortType() == Settings::SortLastUse); menu->addActions(sortTypes->actions()); connect(dateSort, SIGNAL(triggered()), m_optionList.at(0), SLOT(slotSortDate())); connect(alphaTreeSort, SIGNAL(triggered()), m_optionList.at(0), SLOT(slotSortAlphaTree())); connect(alphaFlatSort, SIGNAL(triggered()), m_optionList.at(0), SLOT(slotSortAlphaFlat())); // create MatchType entries menu->addSeparator(); QActionGroup *matchTypes = new QActionGroup(menu); QAction *matchFromBeginning = new QAction(i18n("Match Tags from the First Character"), matchTypes); QAction *matchFromWordStart = new QAction(i18n("Match Tags from Word Boundaries"), matchTypes); QAction *matchAnywhere = new QAction(i18n("Match Tags Anywhere"), matchTypes); matchFromBeginning->setCheckable(true); matchFromWordStart->setCheckable(true); matchAnywhere->setCheckable(true); // TODO add StatusTip text? // set current state: matchFromBeginning->setChecked(Settings::SettingsData::instance()->matchType() == AnnotationDialog::MatchFromBeginning); matchFromWordStart->setChecked(Settings::SettingsData::instance()->matchType() == AnnotationDialog::MatchFromWordStart); matchAnywhere->setChecked(Settings::SettingsData::instance()->matchType() == AnnotationDialog::MatchAnywhere); // add MatchType actions to menu: menu->addActions(matchTypes->actions()); // create toggle-show-selected entry# if (m_setup != SearchMode) { menu->addSeparator(); QAction *showSelectedOnly = new QAction( SmallIcon(QString::fromLatin1("view-filter")), i18n("Show Only Selected Ctrl+S"), menu); showSelectedOnly->setCheckable(true); showSelectedOnly->setChecked(ShowSelectionOnlyManager::instance().selectionIsLimited()); menu->addAction(showSelectedOnly); connect(showSelectedOnly, SIGNAL(triggered()), &ShowSelectionOnlyManager::instance(), SLOT(toggle())); } // execute menu & handle response: QAction *res = menu->exec(QCursor::pos()); if (res == saveCurrent) slotSaveWindowSetup(); else if (res == reset) slotResetLayout(); else if (res == matchFromBeginning) Settings::SettingsData::instance()->setMatchType(AnnotationDialog::MatchFromBeginning); else if (res == matchFromWordStart) Settings::SettingsData::instance()->setMatchType(AnnotationDialog::MatchFromWordStart); else if (res == matchAnywhere) Settings::SettingsData::instance()->setMatchType(AnnotationDialog::MatchAnywhere); } int AnnotationDialog::Dialog::exec() { m_stack->setCurrentWidget(m_dockWindow); showTornOfWindows(); this->setFocus(); // Set temporary focus before show() is called so that extra cursor is not shown on any "random" input widget show(); // We need to call show before we call setupFocus() otherwise the widget will not yet all have been moved in place. setupFocus(); const int ret = QDialog::exec(); hideTornOfWindows(); return ret; } void AnnotationDialog::Dialog::slotSaveWindowSetup() { const QByteArray data = m_dockWindow->saveState(); QFile file(QString::fromLatin1("%1/layout.dat").arg(Settings::SettingsData::instance()->imageDirectory())); if (!file.open(QIODevice::WriteOnly)) { KMessageBox::sorry(this, i18n("

Could not save the window layout.

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

Could not save the window layout.

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

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

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

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

" "

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

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

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

" "

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

"); } KMessageBox::information(this, txt, QString(), doNotShowKey, KMessageBox::AllowLink); } void AnnotationDialog::Dialog::resizeEvent(QResizeEvent *) { Settings::SettingsData::instance()->setWindowGeometry(Settings::AnnotationDialog, geometry()); } void AnnotationDialog::Dialog::moveEvent(QMoveEvent *) { Settings::SettingsData::instance()->setWindowGeometry(Settings::AnnotationDialog, geometry()); } void AnnotationDialog::Dialog::setupFocus() { QList list = findChildren(); QList orderedList; // Iterate through all widgets in our dialog. for (QObject *obj : list) { QWidget *current = static_cast(obj); if (!current->property("WantsFocus").isValid() || !current->isVisible()) continue; int cx = current->mapToGlobal(QPoint(0, 0)).x(); int cy = current->mapToGlobal(QPoint(0, 0)).y(); bool inserted = false; // Iterate through the ordered list of widgets, and insert the current one, so it is in the right position in the tab chain. for (QList::iterator orderedIt = orderedList.begin(); orderedIt != orderedList.end(); ++orderedIt) { const QWidget *w = *orderedIt; int wx = w->mapToGlobal(QPoint(0, 0)).x(); int wy = w->mapToGlobal(QPoint(0, 0)).y(); if (wy > cy || (wy == cy && wx >= cx)) { orderedList.insert(orderedIt, current); inserted = true; break; } } if (!inserted) orderedList.append(current); } // now setup tab order. QWidget *prev = nullptr; QWidget *first = nullptr; Q_FOREACH (QWidget *widget, orderedList) { if (prev) { setTabOrder(prev, widget); } else { first = widget; } prev = widget; } if (first) { setTabOrder(prev, first); } // Finally set focus on the first list select Q_FOREACH (QWidget *widget, orderedList) { if (widget->property("FocusCandidate").isValid() && widget->isVisible()) { widget->setFocus(); break; } } } void AnnotationDialog::Dialog::slotResetLayout() { m_dockWindow->restoreState(m_dockWindowCleanState); } void AnnotationDialog::Dialog::slotStartDateChanged(const DB::ImageDate &date) { if (date.start() == date.end()) m_endDate->setDate(QDate()); else m_endDate->setDate(date.end().date()); } void AnnotationDialog::Dialog::loadWindowLayout() { QString fileName = QString::fromLatin1("%1/layout.dat").arg(Settings::SettingsData::instance()->imageDirectory()); if (!QFileInfo(fileName).exists()) { // create default layout // label/date/rating in a visual block with description: m_dockWindow->splitDockWidget(m_generalDock, m_descriptionDock, Qt::Vertical); // more space for description: m_dockWindow->resizeDocks({ m_generalDock, m_descriptionDock }, { 60, 100 }, Qt::Vertical); // more space for preview: m_dockWindow->resizeDocks({ m_generalDock, m_descriptionDock, m_previewDock }, { 200, 200, 800 }, Qt::Horizontal); #ifdef HAVE_KGEOMAP // group the map with the preview m_dockWindow->tabifyDockWidget(m_previewDock, m_mapDock); // make sure the preview tab is active: m_previewDock->raise(); #endif return; } QFile file(fileName); file.open(QIODevice::ReadOnly); QByteArray data = file.readAll(); m_dockWindow->restoreState(data); } void AnnotationDialog::Dialog::setupActions() { m_actions = new KActionCollection(this); QAction *action = nullptr; action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-alphatree"), m_optionList.at(0), SLOT(slotSortAlphaTree())); action->setText(i18n("Sort Alphabetically (Tree)")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_F4); action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-alphaflat"), m_optionList.at(0), SLOT(slotSortAlphaFlat())); action->setText(i18n("Sort Alphabetically (Flat)")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-sort-MRU"), m_optionList.at(0), SLOT(slotSortDate())); action->setText(i18n("Sort Most Recently Used")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-sort"), m_optionList.at(0), SLOT(toggleSortType())); action->setText(i18n("Toggle Sorting")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_T); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-showing-selected-only"), &ShowSelectionOnlyManager::instance(), SLOT(toggle())); action->setText(i18n("Toggle Showing Selected Items Only")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_S); action = m_actions->addAction(QString::fromLatin1("annotationdialog-next-image"), m_preview, SLOT(slotNext())); action->setText(i18n("Annotate Next")); m_actions->setDefaultShortcut(action, Qt::Key_PageDown); action = m_actions->addAction(QString::fromLatin1("annotationdialog-prev-image"), m_preview, SLOT(slotPrev())); action->setText(i18n("Annotate Previous")); m_actions->setDefaultShortcut(action, Qt::Key_PageUp); action = m_actions->addAction(QString::fromLatin1("annotationdialog-OK-dialog"), this, SLOT(doneTagging())); action->setText(i18n("OK dialog")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Return); action = m_actions->addAction(QString::fromLatin1("annotationdialog-delete-image"), this, SLOT(slotDeleteImage())); action->setText(i18n("Delete")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete); action = m_actions->addAction(QString::fromLatin1("annotationdialog-copy-previous"), this, SLOT(slotCopyPrevious())); action->setText(i18n("Copy tags from previous image")); m_actions->setDefaultShortcut(action, Qt::ALT + Qt::Key_Insert); action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-left"), m_preview, SLOT(rotateLeft())); action->setText(i18n("Rotate counterclockwise")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-rotate-right"), m_preview, SLOT(rotateRight())); action->setText(i18n("Rotate clockwise")); action = m_actions->addAction(QString::fromLatin1("annotationdialog-toggle-viewer"), this, SLOT(togglePreview())); action->setText(i18n("Toggle fullscreen preview")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Space); foreach (QAction *action, m_actions->actions()) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } // the annotation dialog is created when it's first used; // therefore, its actions are registered well after the MainWindow sets up its actionCollection, // and it has to read the shortcuts here, after they are set up: m_actions->readSettings(); } KActionCollection *AnnotationDialog::Dialog::actions() { return m_actions; } void AnnotationDialog::Dialog::setUpCategoryListBoxForMultiImageSelection(ListSelect *listSel, const DB::ImageInfoList &images) { StringSet on, partialOn; std::tie(on, partialOn) = selectionForMultiSelect(listSel, images); listSel->setSelection(on, partialOn); } std::tuple AnnotationDialog::Dialog::selectionForMultiSelect(ListSelect *listSel, const DB::ImageInfoList &images) { const QString category = listSel->category(); const StringSet allItems = DB::ImageDB::instance()->categoryCollection()->categoryForName(category)->itemsInclCategories().toSet(); StringSet itemsNotSelectedOnAllImages; StringSet itemsOnSomeImages; for (DB::ImageInfoList::ConstIterator imageIt = images.begin(); imageIt != images.end(); ++imageIt) { const StringSet itemsOnThisImage = (*imageIt)->itemsOfCategory(category); itemsNotSelectedOnAllImages += (allItems - itemsOnThisImage); itemsOnSomeImages += itemsOnThisImage; } const StringSet itemsOnAllImages = allItems - itemsNotSelectedOnAllImages; const StringSet itemsPartiallyOn = itemsOnSomeImages - itemsOnAllImages; return std::make_tuple(itemsOnAllImages, itemsPartiallyOn); } void AnnotationDialog::Dialog::slotRatingChanged(unsigned int) { m_ratingChanged = true; } void AnnotationDialog::Dialog::continueLater() { saveAndClose(); } void AnnotationDialog::Dialog::saveAndClose() { tidyAreas(); m_fullScreenPreview->stopPlayback(); if (m_origList.isEmpty()) { // all images are deleted. QDialog::accept(); return; } // I need to check for the changes first, as the case for m_setup == InputSingleImageConfigMode, saves to the m_origList, // and we can thus not check for changes anymore const bool anyChanges = hasChanges(); if (m_setup == InputSingleImageConfigMode) { writeToInfo(); for (int i = 0; i < m_editList.count(); ++i) { *(m_origList[i]) = m_editList[i]; } } else if (m_setup == InputMultiImageConfigMode) { Q_FOREACH (ListSelect *ls, m_optionList) { ls->slotReturn(); } for (DB::ImageInfoListConstIterator it = m_origList.constBegin(); it != m_origList.constEnd(); ++it) { DB::ImageInfoPtr info = *it; 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()); } } 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 { DB::ImageInfo currentInfo = m_editList[m_current]; m_stack->setCurrentWidget(m_fullScreenPreview); m_fullScreenPreview->load(DB::FileNameList() << currentInfo.fileName()); // compute altered tags by removing existing tags from full set: const QMap> existingAreas = currentInfo.taggedAreas(); QMap> alteredAreas = taggedAreas(); for (auto catIt = existingAreas.constBegin(); catIt != existingAreas.constEnd(); ++catIt) { const QString &categoryName = catIt.key(); const QMap &tags = catIt.value(); for (auto tagIt = tags.cbegin(); tagIt != tags.constEnd(); ++tagIt) { const QString &tagName = tagIt.key(); const QRect &area = tagIt.value(); // remove unchanged areas if (area == alteredAreas[categoryName][tagName]) { alteredAreas[categoryName].remove(tagName); if (alteredAreas[categoryName].empty()) alteredAreas.remove(categoryName); } } } m_fullScreenPreview->addAdditionalTaggedAreas(alteredAreas); } } } void AnnotationDialog::Dialog::tidyAreas() { // Remove all areas marked on the preview image foreach (ResizableFrame *area, areas()) { area->markTidied(); area->deleteLater(); } } void AnnotationDialog::Dialog::slotNewArea(ResizableFrame *area) { area->setDialog(this); } void AnnotationDialog::Dialog::positionableTagSelected(QString category, QString tag) { // Be sure not to propose an already-associated tag QPair tagData = qMakePair(category, tag); foreach (ResizableFrame *area, areas()) { if (area->tagData() == tagData) { return; } } // Set the selected tag as the last selected positionable tag m_lastSelectedPositionableTag = tagData; // Add the tag to the positionable tag candidate list addTagToCandidateList(category, tag); } void AnnotationDialog::Dialog::positionableTagDeselected(QString category, QString tag) { // Remove the tag from the candidate list removeTagFromCandidateList(category, tag); // Search for areas linked against the tag on this image if (m_setup == InputSingleImageConfigMode) { QPair deselectedTag = QPair(category, tag); foreach (ResizableFrame *area, areas()) { if (area->tagData() == deselectedTag) { area->removeTagData(); m_areasChanged = true; // Only one area can be associated with the tag, so we can return here return; } } } // Removal of tagged areas in InputMultiImageConfigMode is done in DB::ImageInfo::removeCategoryInfo } void AnnotationDialog::Dialog::addTagToCandidateList(QString category, QString tag) { m_positionableTagCandidates << QPair(category, tag); } void AnnotationDialog::Dialog::removeTagFromCandidateList(QString category, QString tag) { // Is the deselected tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == tag) { m_lastSelectedPositionableTag = QPair(); } // Remove the tag from the candidate list m_positionableTagCandidates.removeAll(QPair(category, tag)); // When a positionable tag is entered via the AreaTagSelectDialog, it's added to this // list twice, so we use removeAll here to be sure to also wipe duplicate entries. } QPair AnnotationDialog::Dialog::lastSelectedPositionableTag() const { return m_lastSelectedPositionableTag; } QList> AnnotationDialog::Dialog::positionableTagCandidates() const { return m_positionableTagCandidates; } void AnnotationDialog::Dialog::slotShowAreas(bool showAreas) { foreach (ResizableFrame *area, areas()) { area->setVisible(showAreas); } } void AnnotationDialog::Dialog::positionableTagRenamed(QString category, QString oldTag, QString newTag) { // Is the renamed tag the last selected positionable tag? if (m_lastSelectedPositionableTag.first == category && m_lastSelectedPositionableTag.second == oldTag) { m_lastSelectedPositionableTag.second = newTag; } // Check the candidate list for the tag QPair oldTagData = QPair(category, oldTag); if (m_positionableTagCandidates.contains(oldTagData)) { // The tag is in the list, so update it m_positionableTagCandidates.removeAt(m_positionableTagCandidates.indexOf(oldTagData)); m_positionableTagCandidates << QPair(category, newTag); } // Check if an area on the current image contains the changed or proposed tag foreach (ResizableFrame *area, areas()) { if (area->tagData() == oldTagData) { area->setTagData(category, newTag); } } } void AnnotationDialog::Dialog::descriptionPageUpDownPressed(QKeyEvent *event) { if (event->key() == Qt::Key_PageUp) { m_actions->action(QString::fromLatin1("annotationdialog-prev-image"))->trigger(); } else if (event->key() == Qt::Key_PageDown) { m_actions->action(QString::fromLatin1("annotationdialog-next-image"))->trigger(); } } void AnnotationDialog::Dialog::checkProposedTagData( QPair tagData, ResizableFrame *areaToExclude) const { foreach (ResizableFrame *area, areas()) { if (area != areaToExclude && area->proposedTagData() == tagData && area->tagData().first.isEmpty()) { area->removeProposedTagData(); } } } void AnnotationDialog::Dialog::areaChanged() { m_areasChanged = true; } /** * @brief positionableTagValid checks whether a given tag can still be associated to an area. * This checks for empty and duplicate tags. * @return */ bool AnnotationDialog::Dialog::positionableTagAvailable(const QString &category, const QString &tag) const { if (category.isEmpty() || tag.isEmpty()) return false; // does any area already have that tag? foreach (const ResizableFrame *area, areas()) { const auto tagData = area->tagData(); if (tagData.first == category && tagData.second == tag) return false; } return true; } /** * @brief Generates a set of positionable tags currently used on the image * @param category * @return */ QSet AnnotationDialog::Dialog::positionedTags(const QString &category) const { QSet tags; foreach (const ResizableFrame *area, areas()) { const auto tagData = area->tagData(); if (tagData.first == category) tags += tagData.second; } return tags; } AnnotationDialog::ListSelect *AnnotationDialog::Dialog::listSelectForCategory(const QString &category) { return m_listSelectList.value(category, nullptr); } #ifdef HAVE_KGEOMAP void AnnotationDialog::Dialog::updateMapForCurrentImage() { if (m_setup != InputSingleImageConfigMode) { return; } if (m_editList[m_current].coordinates().hasCoordinates()) { m_annotationMap->setCenter(m_editList[m_current]); m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } } void AnnotationDialog::Dialog::annotationMapVisibilityChanged(bool visible) { // This populates the map if it's added when the dialog is already open if (visible) { // when the map dockwidget is already visible on show(), the call to // annotationMapVisibilityChanged is executed in the GUI thread. // This ensures that populateMap() doesn't block the GUI in this case: QTimer::singleShot(0, this, SLOT(populateMap())); } else { m_cancelMapLoading = true; } } void AnnotationDialog::Dialog::populateMap() { // populateMap is called every time the map widget gets visible if (m_mapIsPopulated) { return; } m_annotationMap->displayStatus(Map::MapView::MapStatus::Loading); m_cancelMapLoading = false; m_mapLoadingProgress->setMaximum(m_editList.count()); m_mapLoadingProgress->show(); m_cancelMapLoadingButton->show(); int processedImages = 0; int imagesWithCoordinates = 0; foreach (DB::ImageInfo info, m_editList) { processedImages++; m_mapLoadingProgress->setValue(processedImages); // keep things responsive by processing events manually: QApplication::processEvents(); if (info.coordinates().hasCoordinates()) { m_annotationMap->addImage(info); imagesWithCoordinates++; } // m_cancelMapLoading is set to true by clicking the "Cancel" button if (m_cancelMapLoading) { m_annotationMap->clear(); break; } } // at this point either we canceled loading or the map is populated: m_mapIsPopulated = !m_cancelMapLoading; mapLoadingFinished(imagesWithCoordinates > 0, imagesWithCoordinates == processedImages); } void AnnotationDialog::Dialog::setCancelMapLoading() { m_cancelMapLoading = true; } void AnnotationDialog::Dialog::mapLoadingFinished(bool mapHasImages, bool allImagesHaveCoordinates) { m_mapLoadingProgress->hide(); m_cancelMapLoadingButton->hide(); if (m_setup == InputSingleImageConfigMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasNoCoordinates); } else { if (m_setup == SearchMode) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SearchCoordinates); } else { if (mapHasImages) { if (!allImagesHaveCoordinates) { m_annotationMap->displayStatus(Map::MapView::MapStatus::SomeImagesHaveNoCoordinates); } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::ImageHasCoordinates); } } else { m_annotationMap->displayStatus(Map::MapView::MapStatus::NoImagesHaveNoCoordinates); } } } if (m_setup != SearchMode) { m_annotationMap->zoomToMarkers(); updateMapForCurrentImage(); } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/Dialog.h b/AnnotationDialog/Dialog.h index a3059f79..27a85f87 100644 --- a/AnnotationDialog/Dialog.h +++ b/AnnotationDialog/Dialog.h @@ -1,246 +1,246 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef ANNOTATIONDIALOG_DIALOG_H #define ANNOTATIONDIALOG_DIALOG_H #include "config-kpa-kgeomap.h" #include "ImagePreviewWidget.h" #include "ListSelect.h" #include "enums.h" -#include "DB/Category.h" -#include "DB/ImageInfoList.h" -#include "DB/ImageSearchInfo.h" -#include "Utilities/StringSet.h" +#include +#include +#include +#include #include #include #include #include class DockWidget; class KActionCollection; class KComboBox; class KLineEdit; class KRatingWidget; class KTextEdit; class QCloseEvent; class QDockWidget; class QMainWindow; class QMoveEvent; class QProgressBar; class QPushButton; class QResizeEvent; class QSplitter; class QStackedWidget; class QTimeEdit; namespace Viewer { class ViewerWidget; } namespace DB { class ImageInfo; } namespace Map { class MapView; } namespace AnnotationDialog { class ImagePreview; class DateEdit; class ShortCutManager; class ResizableFrame; class Dialog : public QDialog { Q_OBJECT public: explicit Dialog(QWidget *parent); ~Dialog() override; int configure(DB::ImageInfoList list, bool oneAtATime); DB::ImageSearchInfo search(DB::ImageSearchInfo *search = nullptr); KActionCollection *actions(); QPair lastSelectedPositionableTag() const; QList> positionableTagCandidates() const; void addTagToCandidateList(QString category, QString tag); void removeTagFromCandidateList(QString category, QString tag); void checkProposedTagData(QPair tagData, ResizableFrame *areaToExclude) const; void areaChanged(); bool positionableTagAvailable(const QString &category, const QString &tag) const; QSet positionedTags(const QString &category) const; /** * @return A list of all ResizableFrame objects on the current image */ QList areas() const; /** * @brief taggedAreas creates a map of all the currently tagged areas. * This is different from areas(), which also contains untagged areas. * This is different from \code m_editList[m_current].areas()\endcode, which * does not include newly added (or deleted) areas. * @return a map of currently tagged areas */ QMap> taggedAreas() const; ListSelect *listSelectForCategory(const QString &category); protected slots: void slotRevert(); void slotIndexChanged(int index); void doneTagging(); void continueLater(); void slotClear(); void slotOptions(); void slotSaveWindowSetup(); void slotDeleteOption(DB::Category *, const QString &); void slotRenameOption(DB::Category *, const QString &, const QString &); void reject() override; void rotate(int angle); void slotSetFuzzyDate(); void slotDeleteImage(); void slotResetLayout(); void slotStartDateChanged(const DB::ImageDate &); void slotCopyPrevious(); void slotShowAreas(bool showAreas); void slotRatingChanged(unsigned int); void togglePreview(); void descriptionPageUpDownPressed(QKeyEvent *event); void slotNewArea(ResizableFrame *area); void positionableTagSelected(QString category, QString tag); void positionableTagDeselected(QString category, QString tag); void positionableTagRenamed(QString category, QString oldTag, QString newTag); #ifdef HAVE_KGEOMAP void setCancelMapLoading(); void annotationMapVisibilityChanged(bool visible); void populateMap(); #endif signals: void imageRotated(const DB::FileName &id); protected: QDockWidget *createDock(const QString &title, const QString &name, Qt::DockWidgetArea location, QWidget *widget); QWidget *createDateWidget(ShortCutManager &shortCutManager); QWidget *createPreviewWidget(); ListSelect *createListSel(const DB::CategoryPtr &category); void load(); void writeToInfo(); void setup(); void loadInfo(const DB::ImageSearchInfo &); int exec() override; void closeEvent(QCloseEvent *) override; void showTornOfWindows(); void hideTornOfWindows(); bool hasChanges(); void showHelpDialog(UsageMode); void resizeEvent(QResizeEvent *) override; void moveEvent(QMoveEvent *) override; void setupFocus(); void closeDialog(); void loadWindowLayout(); void setupActions(); void setUpCategoryListBoxForMultiImageSelection(ListSelect *, const DB::ImageInfoList &images); std::tuple selectionForMultiSelect(ListSelect *, const DB::ImageInfoList &images); void saveAndClose(); void ShowHideSearch(bool show); private: QStackedWidget *m_stack; Viewer::ViewerWidget *m_fullScreenPreview; DB::ImageInfoList m_origList; QList m_editList; int m_current; UsageMode m_setup; QList m_optionList; DB::ImageSearchInfo m_oldSearch; int m_accept; QList m_dockWidgets; // "special" named dockWidgets (used to set default layout): QDockWidget *m_generalDock; QDockWidget *m_previewDock; QDockWidget *m_descriptionDock; // Widgets QMainWindow *m_dockWindow; KLineEdit *m_imageLabel; DateEdit *m_startDate; DateEdit *m_endDate; QLabel *m_endDateLabel; QLabel *m_imageFilePatternLabel; KLineEdit *m_imageFilePattern; ImagePreviewWidget *m_preview; QPushButton *m_revertBut; QPushButton *m_clearBut; QPushButton *m_okBut; QPushButton *m_continueLaterBut; KTextEdit *m_description; QTimeEdit *m_time; QLabel *m_timeLabel; QCheckBox *m_isFuzzyDate; KRatingWidget *m_rating; KComboBox *m_ratingSearchMode; QLabel *m_ratingSearchLabel; bool m_ratingChanged; QSpinBox *m_megapixel; QLabel *m_megapixelLabel; QSpinBox *m_max_megapixel; QLabel *m_max_megapixelLabel; QCheckBox *m_searchRAW; QString m_conflictText; QString m_firstDescription; KActionCollection *m_actions; /** Clean state of the dock window. * * Used in slotResetLayout(). */ QByteArray m_dockWindowCleanState; void tidyAreas(); QPair m_lastSelectedPositionableTag; QList> m_positionableTagCandidates; QMap m_listSelectList; bool m_positionableCategories; bool m_areasChanged; #ifdef HAVE_KGEOMAP QDockWidget *m_mapDock; QWidget *m_annotationMapContainer; Map::MapView *m_annotationMap; void updateMapForCurrentImage(); QProgressBar *m_mapLoadingProgress; QPushButton *m_cancelMapLoadingButton; void mapLoadingFinished(bool mapHasImages, bool allImagesHaveCoordinates); bool m_cancelMapLoading; bool m_mapIsPopulated; #endif }; } #endif /* ANNOTATIONDIALOG_DIALOG_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ImagePreview.cpp b/AnnotationDialog/ImagePreview.cpp index e23d0bf9..7895ff49 100644 --- a/AnnotationDialog/ImagePreview.cpp +++ b/AnnotationDialog/ImagePreview.cpp @@ -1,584 +1,582 @@ /* 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 "Logging.h" +#include "ResizableFrame.h" #include #include #include #include -#include "ResizableFrame.h" - +#include +#include #include #include #include #include - -#include -#include - #include 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) { qCDebug(AnnotationDialogLog) << "Resizing from" << ev->oldSize() << "to" << ev->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(); qCDebug(AnnotationDialogLog) << "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())) { qCDebug(AnnotationDialogLog) << "reload(): set preloader image"; setCurrentImage(m_preloader.getImage()); } else if (m_lastImage.has(m_info.fileName(), m_info.angle())) { qCDebug(AnnotationDialogLog) << "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()); } qCDebug(AnnotationDialogLog) << "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 { qCDebug(AnnotationDialogLog) << "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, QSize(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() { qCDebug(AnnotationDialogLog) << "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)); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ImagePreview.h b/AnnotationDialog/ImagePreview.h index 18c53b69..7808a57f 100644 --- a/AnnotationDialog/ImagePreview.h +++ b/AnnotationDialog/ImagePreview.h @@ -1,143 +1,143 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef IMAGEPREVIEW_H #define IMAGEPREVIEW_H -#include "DB/ImageInfo.h" -#include "ImageManager/ImageClientInterface.h" +#include +#include #include #include class QResizeEvent; class QRubberBand; namespace AnnotationDialog { class ResizableFrame; class ImagePreview : public QLabel, public ImageManager::ImageClientInterface { Q_OBJECT public: explicit ImagePreview(QWidget *parent); int heightForWidth(int width) const override; QSize sizeHint() const override; void rotate(int angle); void setImage(const DB::ImageInfo &info); void setImage(const QString &fileName); int angle() const; void anticipate(DB::ImageInfo &info1); void pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) override; QRect areaPreviewToActual(QRect area) const; QRect minMaxAreaPreview() const; void createTaggedArea(QString category, QString tag, QRect geometry, bool showArea); QSize getActualImageSize(); void acceptProposedTag(QPair tagData, ResizableFrame *area); QPixmap grabAreaImage(QRect area); public slots: void setAreaCreationEnabled(bool state); signals: void areaCreated(ResizableFrame *area); void proposedTagSelected(QString category, QString tag); protected: void resizeEvent(QResizeEvent *) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void reload(); void setCurrentImage(const QImage &image); QImage rotateAndScale(QImage, int width, int height, int angle) const; void updateScaleFactors(); QRect areaActualToPreview(QRect area) const; void processNewArea(); void remapAreas(); void rotateAreas(int angle); class PreviewImage { public: bool has(const DB::FileName &fileName, int angle) const; QImage &getImage(); void set(const DB::FileName &fileName, const QImage &image, int angle); void set(const PreviewImage &other); void setAngle(int angle); void reset(); protected: DB::FileName m_fileName; QImage m_image; int m_angle; }; struct PreloadInfo { PreloadInfo(); void set(const DB::FileName &fileName, int angle); DB::FileName m_fileName; int m_angle; }; class PreviewLoader : public ImageManager::ImageClientInterface, public PreviewImage { public: void preloadImage(const DB::FileName &fileName, int width, int height, int angle); void cancelPreload(); void pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) override; }; PreviewLoader m_preloader; protected slots: void resizeFinished(); private: DB::ImageInfo m_info; QString m_fileName; PreviewImage m_currentImage, m_lastImage; PreloadInfo m_anticipated; int m_angle; int m_minX; int m_maxX; int m_minY; int m_maxY; QPoint m_areaStart; QPoint m_areaEnd; QPoint m_currentPos; QRubberBand *m_selectionRect; double m_scaleWidth; double m_scaleHeight; double m_aspectRatio; QTimer *m_reloadTimer; void createNewArea(QRect geometry, QRect actualGeometry); QRect rotateArea(QRect originalAreaGeometry, int angle); bool m_areaCreationEnabled; QMap> m_imageSizes; QImage m_fullSizeImage; void fetchFullSizeImage(); bool fuzzyAreaExists(QList &existingAreas, QRect area); float distance(QPoint point1, QPoint point2); }; } #endif /* IMAGEPREVIEW_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ImagePreviewWidget.cpp b/AnnotationDialog/ImagePreviewWidget.cpp index d48bd724..0f25394d 100644 --- a/AnnotationDialog/ImagePreviewWidget.cpp +++ b/AnnotationDialog/ImagePreviewWidget.cpp @@ -1,360 +1,359 @@ /* 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 "ImagePreviewWidget.h" +#include +#include +#include + +#include #include #include #include #include #include #include #include -#include - -#include -#include -#include - using namespace AnnotationDialog; ImagePreviewWidget::ImagePreviewWidget() : QWidget() { QVBoxLayout *layout = new QVBoxLayout(this); m_preview = new ImagePreview(this); layout->addWidget(m_preview, 1); connect(this, SIGNAL(areaVisibilityChanged(bool)), m_preview, SLOT(setAreaCreationEnabled(bool))); m_controlWidget = new QWidget; layout->addWidget(m_controlWidget); QVBoxLayout *controlLayout = new QVBoxLayout(m_controlWidget); QHBoxLayout *controlButtonsLayout = new QHBoxLayout; controlLayout->addLayout(controlButtonsLayout); controlButtonsLayout->addStretch(1); m_prevBut = new QPushButton(this); m_prevBut->setIcon(QIcon::fromTheme(QString::fromLatin1("arrow-left"))); m_prevBut->setFixedWidth(40); controlButtonsLayout->addWidget(m_prevBut); m_prevBut->setToolTip(i18n("Annotate previous image")); m_nextBut = new QPushButton(this); m_nextBut->setIcon(QIcon::fromTheme(QString::fromLatin1("arrow-right"))); m_nextBut->setFixedWidth(40); controlButtonsLayout->addWidget(m_nextBut); m_nextBut->setToolTip(i18n("Annotate next image")); controlButtonsLayout->addStretch(1); m_toggleFullscreenPreview = new QPushButton; m_toggleFullscreenPreview->setIcon(QIcon::fromTheme(QString::fromUtf8("file-zoom-in"))); m_toggleFullscreenPreview->setFixedWidth(40); m_toggleFullscreenPreview->setToolTip(i18n("Toggle full-screen preview (CTRL+Space)")); controlButtonsLayout->addWidget(m_toggleFullscreenPreview); connect(m_toggleFullscreenPreview, &QPushButton::clicked, this, &ImagePreviewWidget::toggleFullscreenPreview); m_rotateLeft = new QPushButton(this); controlButtonsLayout->addWidget(m_rotateLeft); m_rotateLeft->setIcon(QIcon::fromTheme(QString::fromLatin1("object-rotate-left"))); m_rotateLeft->setFixedWidth(40); m_rotateLeft->setToolTip(i18n("Rotate counterclockwise")); m_rotateRight = new QPushButton(this); controlButtonsLayout->addWidget(m_rotateRight); m_rotateRight->setIcon(QIcon::fromTheme(QString::fromLatin1("object-rotate-right"))); m_rotateRight->setFixedWidth(40); m_rotateRight->setToolTip(i18n("Rotate clockwise")); m_copyPreviousBut = new QPushButton(this); controlButtonsLayout->addWidget(m_copyPreviousBut); m_copyPreviousBut->setIcon(QIcon::fromTheme(QString::fromLatin1("go-bottom"))); m_copyPreviousBut->setFixedWidth(40); m_copyPreviousBut->setToolTip(i18n("Copy tags from previously tagged image")); m_copyPreviousBut->setWhatsThis(i18nc("@info:whatsthis", "Set the same tags on this image than on the previous one. The image date, label, rating, and description are left unchanged.")); m_toggleAreasBut = new QPushButton(this); controlButtonsLayout->addWidget(m_toggleAreasBut); m_toggleAreasBut->setIcon(QIcon::fromTheme(QString::fromLatin1("document-preview"))); m_toggleAreasBut->setFixedWidth(40); m_toggleAreasBut->setCheckable(true); m_toggleAreasBut->setChecked(true); // tooltip text is set in updateTexts() controlButtonsLayout->addStretch(1); m_delBut = new QPushButton(this); m_delBut->setIcon(QIcon::fromTheme(QString::fromLatin1("edit-delete"))); controlButtonsLayout->addWidget(m_delBut); m_delBut->setToolTip(i18n("Delete image")); m_delBut->setAutoDefault(false); controlButtonsLayout->addStretch(1); connect(m_copyPreviousBut, SIGNAL(clicked()), this, SLOT(slotCopyPrevious())); connect(m_delBut, SIGNAL(clicked()), this, SLOT(slotDeleteImage())); connect(m_nextBut, SIGNAL(clicked()), this, SLOT(slotNext())); connect(m_prevBut, SIGNAL(clicked()), this, SLOT(slotPrev())); connect(m_rotateLeft, SIGNAL(clicked()), this, SLOT(rotateLeft())); connect(m_rotateRight, SIGNAL(clicked()), this, SLOT(rotateRight())); connect(m_toggleAreasBut, SIGNAL(clicked(bool)), this, SLOT(slotShowAreas(bool))); QHBoxLayout *defaultAreaCategoryLayout = new QHBoxLayout; controlLayout->addLayout(defaultAreaCategoryLayout); m_defaultAreaCategoryLabel = new QLabel(i18n("Category for new areas:")); m_defaultAreaCategoryLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); defaultAreaCategoryLayout->addWidget(m_defaultAreaCategoryLabel); m_defaultAreaCategory = new QComboBox(this); defaultAreaCategoryLayout->addWidget(m_defaultAreaCategory); m_current = -1; updateTexts(); } void ImagePreviewWidget::updatePositionableCategories(QList positionableCategories) { if (positionableCategories.size() <= 1) { m_defaultAreaCategoryLabel->hide(); m_defaultAreaCategory->hide(); } else { m_defaultAreaCategoryLabel->show(); m_defaultAreaCategory->show(); } m_defaultAreaCategory->clear(); for (const QString &categoryName : positionableCategories) { m_defaultAreaCategory->addItem(categoryName); } } QString ImagePreviewWidget::defaultPositionableCategory() const { return m_defaultAreaCategory->currentText(); } int ImagePreviewWidget::angle() const { return m_preview->angle(); } void ImagePreviewWidget::anticipate(DB::ImageInfo &info1) { m_preview->anticipate(info1); } void ImagePreviewWidget::configure(QList *imageList, bool singleEdit) { m_imageList = imageList; m_current = 0; setImage(m_imageList->at(m_current)); m_singleEdit = singleEdit; m_delBut->setEnabled(m_singleEdit); m_copyPreviousBut->setEnabled(m_singleEdit); m_rotateLeft->setEnabled(m_singleEdit); m_rotateRight->setEnabled(m_singleEdit); } void ImagePreviewWidget::slotPrev() { if ((m_current <= 0)) return; m_current--; if (m_current != 0) m_preview->anticipate((*m_imageList)[m_current - 1]); setImage(m_imageList->at(m_current)); emit indexChanged(m_current); } void ImagePreviewWidget::slotNext() { if ((m_current == -1) || (m_current == (int)m_imageList->count() - 1)) return; m_current++; if (m_current != (int)m_imageList->count() - 1) m_preview->anticipate((*m_imageList)[m_current + 1]); setImage(m_imageList->at(m_current)); emit indexChanged(m_current); } void ImagePreviewWidget::slotCopyPrevious() { emit copyPrevClicked(); } void ImagePreviewWidget::rotateLeft() { rotate(-90); } void ImagePreviewWidget::rotateRight() { rotate(90); } void ImagePreviewWidget::rotate(int angle) { if (!m_singleEdit) return; m_preview->rotate(angle); emit imageRotated(angle); } void ImagePreviewWidget::slotDeleteImage() { if (!m_singleEdit) return; MainWindow::DeleteDialog dialog(this); DB::ImageInfo info = m_imageList->at(m_current); const DB::FileNameList deleteList = DB::FileNameList() << info.fileName(); int ret = dialog.exec(deleteList); if (ret == QDialog::Rejected) //Delete Dialog rejected, do nothing return; emit imageDeleted(m_imageList->at(m_current)); if (!m_nextBut->isEnabled()) //No next image exists, select previous m_current--; if (m_imageList->count() == 0) return; //No images left setImage(m_imageList->at(m_current)); } void ImagePreviewWidget::setImage(const DB::ImageInfo &info) { m_nextBut->setEnabled(m_current != (int)m_imageList->count() - 1); m_prevBut->setEnabled(m_current != 0); m_copyPreviousBut->setEnabled(m_current != 0 && m_singleEdit); m_preview->setImage(info); emit imageChanged(info); } void ImagePreviewWidget::setImage(const int index) { m_current = index; setImage(m_imageList->at(m_current)); } void ImagePreviewWidget::setImage(const QString &fileName) { m_preview->setImage(fileName); m_current = -1; m_nextBut->setEnabled(false); m_prevBut->setEnabled(false); m_rotateLeft->setEnabled(false); m_rotateRight->setEnabled(false); m_delBut->setEnabled(false); m_copyPreviousBut->setEnabled(false); } ImagePreview *ImagePreviewWidget::preview() const { return m_preview; } void ImagePreviewWidget::slotShowAreas(bool show) { // slot can be triggered by something else than the button: m_toggleAreasBut->setChecked(show); emit areaVisibilityChanged(show); } bool ImagePreviewWidget::showAreas() const { return m_toggleAreasBut->isChecked(); } void ImagePreviewWidget::canCreateAreas(bool state) { if (m_toggleAreasBut->isEnabled() != state) { m_toggleAreasBut->setChecked(state); m_toggleAreasBut->setEnabled(state); emit areaVisibilityChanged(state); } m_preview->setAreaCreationEnabled(state); updateTexts(); } void ImagePreviewWidget::updateTexts() { if (m_toggleAreasBut->isEnabled()) { // positionable tags enabled m_toggleAreasBut->setToolTip(i18nc("@info:tooltip", "Hide or show areas on the image")); } else { if (m_singleEdit) { // positionable tags disabled m_toggleAreasBut->setToolTip(i18nc("@info:tooltip", "If you enable positionable tags for at least one category in " "Settings|Configure KPhotoAlbum...|Categories, you can " "associate specific image areas with tags.")); } else { m_toggleAreasBut->setToolTip(i18nc("@info:tooltip", "Areas on an image can only be shown in single-image annotation mode.")); } } } void ImagePreviewWidget::setFacedetectButEnabled(bool state) { if (state == false) { QApplication::setOverrideCursor(Qt::WaitCursor); } else { QApplication::restoreOverrideCursor(); } m_facedetectBut->setChecked(!state); m_facedetectBut->setEnabled(state); // Better disable the whole widget so that the user can't // change or delete the image during face detection. this->setEnabled(state); } void ImagePreviewWidget::setSearchMode(bool state) { m_controlWidget->setVisible(!state); } void ImagePreviewWidget::toggleFullscreenPreview() { emit togglePreview(); } void ImagePreviewWidget::setToggleFullscreenPreviewEnabled(bool state) { m_toggleFullscreenPreview->setEnabled(state); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ImagePreviewWidget.h b/AnnotationDialog/ImagePreviewWidget.h index 3fcaf9b3..975c4308 100644 --- a/AnnotationDialog/ImagePreviewWidget.h +++ b/AnnotationDialog/ImagePreviewWidget.h @@ -1,106 +1,107 @@ /* 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. */ #ifndef IMAGEPREVIEWWIDGET_H #define IMAGEPREVIEWWIDGET_H // Qt includes #include #include // Local includes -#include "DB/ImageInfo.h" #include "ImagePreview.h" +#include + class QCheckBox; class QPushButton; class QComboBox; namespace AnnotationDialog { class ImagePreviewWidget : public QWidget { Q_OBJECT public: ImagePreviewWidget(); void rotate(int angle); void setImage(const DB::ImageInfo &info); void setImage(const QString &fileName); void setImage(const int index); void configure(QList *imageList, bool singleEdit); int angle() const; void anticipate(DB::ImageInfo &info1); const QString &lastImage(); ImagePreview *preview() const; bool showAreas() const; void canCreateAreas(bool state); void setFacedetectButEnabled(bool state); void setSearchMode(bool state); void updatePositionableCategories(QList positionableCategories = QList()); QString defaultPositionableCategory() const; void setToggleFullscreenPreviewEnabled(bool state); public slots: void slotNext(); void slotPrev(); void slotCopyPrevious(); void slotDeleteImage(); void rotateLeft(); void rotateRight(); void slotShowAreas(bool show); signals: void imageDeleted(const DB::ImageInfo &deletedImage); void imageRotated(int angle); void imageChanged(const DB::ImageInfo &newImage); void indexChanged(int newIndex); void copyPrevClicked(); void areaVisibilityChanged(bool visible); void togglePreview(); private: // Functions /** * Update labels and tooltip texts when canCreateAreas() changes. */ void updateTexts(); void toggleFullscreenPreview(); private: // Variables ImagePreview *m_preview; QPushButton *m_prevBut; QPushButton *m_nextBut; QPushButton *m_toggleFullscreenPreview; QPushButton *m_rotateLeft; QPushButton *m_rotateRight; QPushButton *m_delBut; QPushButton *m_copyPreviousBut; QPushButton *m_facedetectBut; QPushButton *m_toggleAreasBut; QList *m_imageList; int m_current; bool m_singleEdit; QLabel *m_defaultAreaCategoryLabel; QComboBox *m_defaultAreaCategory; QWidget *m_controlWidget; }; } #endif /* IMAGEPREVIEWWIDGET_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ListSelect.cpp b/AnnotationDialog/ListSelect.cpp index e4b8a7b6..89f3dff6 100644 --- a/AnnotationDialog/ListSelect.cpp +++ b/AnnotationDialog/ListSelect.cpp @@ -1,863 +1,862 @@ /* 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 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::fromLocalFile(oldFile), QUrl::fromLocalFile(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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ListSelect.h b/AnnotationDialog/ListSelect.h index 63f4a84d..6fec6553 100644 --- a/AnnotationDialog/ListSelect.h +++ b/AnnotationDialog/ListSelect.h @@ -1,144 +1,146 @@ /* Copyright (C) 2003-2015 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef LISTSELECT_H #define LISTSELECT_H -#include "DB/CategoryPtr.h" -#include "Settings/SettingsData.h" #include "enums.h" + +#include +#include + #include #include class QTreeWidgetItem; class CategoryItem; class QToolButton; class QEvent; class QRadioButton; class QLabel; namespace DB { class ImageInfo; } namespace CategoryListView { class DragableTreeWidget; } namespace CategoryListView { class CheckDropItem; } namespace AnnotationDialog { using Utilities::StringSet; class CompletableLineEdit; class ListSelect : public QWidget { Q_OBJECT public: ListSelect(const DB::CategoryPtr &category, QWidget *parent); QString category() const; QString text() const; void setText(const QString &); void setSelection(const StringSet &on, const StringSet &partiallyOn = StringSet()); StringSet itemsOn() const; StringSet itemsOff() const; StringSet itemsUnchanged() const; bool isAND() const; void setMode(UsageMode); void populate(); void showOnlyItemsMatching(const QString &text); QWidget *lineEdit() const; void setPositionable(bool positionableState); bool positionable() const; bool tagIsChecked(QString tag) const; void connectLineEdit(CompletableLineEdit *le); void deselectTag(QString tag); public slots: void slotReturn(); void slotExternalReturn(const QString &text); void slotSortDate(); void slotSortAlphaTree(); void slotSortAlphaFlat(); void toggleSortType(); void updateListview(); void rePopulate(); void ensureTagIsSelected(QString category, QString tag); signals: /** * This signal is emitted whenever a positionable tag is (de)selected. */ void positionableTagSelected(const QString category, const QString tag); void positionableTagDeselected(const QString category, const QString tag); void positionableTagRenamed(const QString category, const QString oldTag, const QString newTag); protected slots: void itemSelected(QTreeWidgetItem *); void showContextMenu(const QPoint &); void setViewSortType(Settings::ViewSortType); void limitToSelection(); void showAllChildren(); void updateSelectionCount(); protected: void addItems(DB::CategoryItem *item, QTreeWidgetItem *parent); void populateAlphaTree(); void populateAlphaFlat(); void populateMRU(); void configureItem(CategoryListView::CheckDropItem *item); bool isInputMode() const; StringSet itemsOfState(Qt::CheckState state) const; void checkItem(const QString itemText, bool); void ensureAllInstancesAreStateChanged(QTreeWidgetItem *item); private: // Functions bool searchForUntaggedImagesTagNeeded(); void hideUntaggedImagesTag(); QTreeWidgetItem *getUntaggedImagesTag(); private: // Variables DB::CategoryPtr m_category; CompletableLineEdit *m_lineEdit; CategoryListView::DragableTreeWidget *m_treeWidget; QRadioButton *m_or; QRadioButton *m_and; UsageMode m_mode; QToolButton *m_alphaTreeSort; QToolButton *m_alphaFlatSort; QToolButton *m_dateSort; QToolButton *m_showSelectedOnly; QString m_baseTitle; bool m_positionable; }; } #endif /* LISTSELECT_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/AnnotationDialog/ShortCutManager.cpp b/AnnotationDialog/ShortCutManager.cpp index 8d37781c..be143bdd 100644 --- a/AnnotationDialog/ShortCutManager.cpp +++ b/AnnotationDialog/ShortCutManager.cpp @@ -1,80 +1,80 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ShortCutManager.h" +#include "ListSelect.h" + #include #include -#include "ListSelect.h" - /** * Register the dock widget for getting a shortcut. its buddy will get the * actual focus when the shortcut is execute. */ void AnnotationDialog::ShortCutManager::addDock(QDockWidget *dock, QWidget *buddy) { m_docks.append(qMakePair(dock, buddy)); } void AnnotationDialog::ShortCutManager::addLabel(QLabel *label) { m_labelWidgets.append(label); } void AnnotationDialog::ShortCutManager::setupShortCuts() { for (const DockPair &pair : m_docks) { QDockWidget *dock = pair.first; QWidget *widget = pair.second; QString title = dock->windowTitle(); for (int index = 0; index < title.length(); ++index) { const QChar ch = title[index].toLower(); if (!m_taken.contains(ch)) { m_taken.insert(ch); dock->setWindowTitle(title.left(index) + QString::fromLatin1("&") + title.mid(index)); new QShortcut(QString::fromLatin1("Alt+") + ch, widget, SLOT(setFocus())); break; } } } for (QLabel *label : m_labelWidgets) { const QString title = label->text(); for (int index = 0; index < title.length(); ++index) { const QChar ch = title[index].toLower(); if (!m_taken.contains(ch)) { m_taken.insert(ch); label->setText(title.left(index) + QString::fromLatin1("&") + title.mid(index)); break; } } } } /** * Search for & in the text, and if found register the character after the ampersand as a shortcut * This is needed as the OK and Cancel button will get a shortcut by KDE, * despite an attempt at telling it not too. */ void AnnotationDialog::ShortCutManager::addTaken(const QString &text) { const int index = text.indexOf(QChar::fromLatin1('&')); if (index != -1) m_taken.insert(text[index + 1].toLower()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundJobs/ExtractOneThumbnailJob.cpp b/BackgroundJobs/ExtractOneThumbnailJob.cpp index 49ed2ab2..4e9d56e9 100644 --- a/BackgroundJobs/ExtractOneThumbnailJob.cpp +++ b/BackgroundJobs/ExtractOneThumbnailJob.cpp @@ -1,102 +1,105 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ExtractOneThumbnailJob.h" -#include + +#include "HandleVideoThumbnailRequestJob.h" + #include #include +#include + #include #include #include #include -#include namespace BackgroundJobs { ExtractOneThumbnailJob::ExtractOneThumbnailJob(const DB::FileName &fileName, int index, BackgroundTaskManager::Priority priority) : JobInterface(priority) , m_fileName(fileName) , m_index(index) , m_wasCanceled(false) { Q_ASSERT(index >= 0 && index <= 9); } void ExtractOneThumbnailJob::execute() { if (m_wasCanceled || frameName().exists()) emit completed(); else { DB::ImageInfoPtr info = DB::ImageDB::instance()->info(m_fileName); const int length = info->videoLength(); ImageManager::ExtractOneVideoFrame::extract(m_fileName, length * m_index / 10.0, this, SLOT(frameLoaded(QImage))); } } QString ExtractOneThumbnailJob::title() const { return i18n("Extracting Thumbnail"); } QString ExtractOneThumbnailJob::details() const { return QString::fromLatin1("%1 #%2").arg(m_fileName.relative()).arg(m_index); } int ExtractOneThumbnailJob::index() const { return m_index; } void ExtractOneThumbnailJob::cancel() { m_wasCanceled = true; } void ExtractOneThumbnailJob::frameLoaded(const QImage &image) { if (!image.isNull()) { #if 0 QImage img = image; { QPainter painter(&img); QFont fnt; fnt.setPointSize(24); painter.setFont(fnt); painter.drawText(QPoint(100,100),QString::number(m_index)); } #endif Utilities::saveImage(frameName(), image, "JPEG"); } else { // Create empty file to avoid that we recheck at next start up. QFile file(frameName().absolute()); file.open(QFile::WriteOnly); file.close(); } emit completed(); } DB::FileName ExtractOneThumbnailJob::frameName() const { return BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(m_fileName, m_index); } } // namespace BackgroundJobs // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundJobs/HandleVideoThumbnailRequestJob.cpp b/BackgroundJobs/HandleVideoThumbnailRequestJob.cpp index ce96cb8f..95a5dad6 100644 --- a/BackgroundJobs/HandleVideoThumbnailRequestJob.cpp +++ b/BackgroundJobs/HandleVideoThumbnailRequestJob.cpp @@ -1,115 +1,114 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "HandleVideoThumbnailRequestJob.h" -#include -#include -#include -#include - -#include -#include - #include #include #include #include #include #include #include #include +#include +#include +#include +#include +#include +#include + namespace BackgroundJobs { HandleVideoThumbnailRequestJob::HandleVideoThumbnailRequestJob(ImageManager::ImageRequest *request, BackgroundTaskManager::Priority priority) : BackgroundTaskManager::JobInterface(priority) , m_request(request) { } QString HandleVideoThumbnailRequestJob::title() const { return i18n("Extract Video Thumbnail"); } QString HandleVideoThumbnailRequestJob::details() const { return m_request->databaseFileName().relative(); } void HandleVideoThumbnailRequestJob::execute() { QImage image(pathForRequest(m_request->fileSystemFileName()).absolute()); if (!image.isNull()) frameLoaded(image); else ImageManager::ExtractOneVideoFrame::extract(m_request->fileSystemFileName(), 0, this, SLOT(frameLoaded(QImage))); } void HandleVideoThumbnailRequestJob::frameLoaded(QImage image) { if (image.isNull()) image = brokenImage(); saveFullScaleFrame(m_request->databaseFileName(), image); sendResult(image); emit completed(); } void HandleVideoThumbnailRequestJob::saveFullScaleFrame(const DB::FileName &fileName, const QImage &image) { Utilities::saveImage(pathForRequest(fileName), image, "JPEG"); } DB::FileName HandleVideoThumbnailRequestJob::pathForRequest(const DB::FileName &fileName) { QCryptographicHash md5(QCryptographicHash::Md5); md5.addData(fileName.absolute().toUtf8()); return DB::FileName::fromRelativePath(QString::fromLatin1(".videoThumbnails/%2").arg(QString::fromUtf8(md5.result().toHex()))); } DB::FileName HandleVideoThumbnailRequestJob::frameName(const DB::FileName &videoName, int frameNumber) { return DB::FileName::fromRelativePath(pathForRequest(videoName).relative() + QLatin1String("-") + QString::number(frameNumber)); } void HandleVideoThumbnailRequestJob::removeFullScaleFrame(const DB::FileName &fileName) { QDir().remove(BackgroundJobs::HandleVideoThumbnailRequestJob::pathForRequest(fileName).absolute()); } void HandleVideoThumbnailRequestJob::sendResult(QImage image) { //if ( m_request->isRequestStillValid(m_request) ) { image = image.scaled(QSize(m_request->width(), m_request->height()), Qt::KeepAspectRatio, Qt::SmoothTransformation); if (m_request->isThumbnailRequest()) ImageManager::ThumbnailCache::instance()->insert(m_request->databaseFileName(), image); m_request->setLoadedOK(!image.isNull()); m_request->client()->pixmapLoaded(m_request, image); //} } QImage HandleVideoThumbnailRequestJob::brokenImage() const { return QIcon::fromTheme(QString::fromUtf8("applications-multimedia")).pixmap(ThumbnailView::CellGeometry::preferredIconSize()).toImage(); } } // namespace BackgroundJobs // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundJobs/HandleVideoThumbnailRequestJob.h b/BackgroundJobs/HandleVideoThumbnailRequestJob.h index e1e595b2..14595171 100644 --- a/BackgroundJobs/HandleVideoThumbnailRequestJob.h +++ b/BackgroundJobs/HandleVideoThumbnailRequestJob.h @@ -1,68 +1,68 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef BACKGROUNDJOBS_HANDLEVIDEOTHUMBNAILREQUESTJOB_H #define BACKGROUNDJOBS_HANDLEVIDEOTHUMBNAILREQUESTJOB_H -#include - #include +#include + namespace ImageManager { class ImageRequest; } namespace DB { class FileName; } class QImage; namespace BackgroundJobs { class HandleVideoThumbnailRequestJob : public BackgroundTaskManager::JobInterface { Q_OBJECT public: explicit HandleVideoThumbnailRequestJob(ImageManager::ImageRequest *request, BackgroundTaskManager::Priority priority); QString title() const override; QString details() const override; static void saveFullScaleFrame(const DB::FileName &fileName, const QImage &image); static DB::FileName pathForRequest(const DB::FileName &fileName); static DB::FileName frameName(const DB::FileName &videoName, int frameNumber); static void removeFullScaleFrame(const DB::FileName &fileName); protected: void execute() override; private slots: void frameLoaded(QImage); private: void sendResult(QImage image); QImage brokenImage() const; ImageManager::ImageRequest *m_request; }; } // namespace BackgroundJobs #endif // BACKGROUNDJOBS_HANDLEVIDEOTHUMBNAILREQUESTJOB_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundJobs/ReadVideoLengthJob.cpp b/BackgroundJobs/ReadVideoLengthJob.cpp index c74b9451..97df05dd 100644 --- a/BackgroundJobs/ReadVideoLengthJob.cpp +++ b/BackgroundJobs/ReadVideoLengthJob.cpp @@ -1,66 +1,68 @@ /* Copyright (C) 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 "ReadVideoLengthJob.h" -#include "ImageManager/VideoLengthExtractor.h" + #include #include -#include +#include #include +#include + BackgroundJobs::ReadVideoLengthJob::ReadVideoLengthJob(const DB::FileName &fileName, BackgroundTaskManager::Priority priority) : JobInterface(priority) , m_fileName(fileName) { } void BackgroundJobs::ReadVideoLengthJob::execute() { ImageManager::VideoLengthExtractor *extractor = new ImageManager::VideoLengthExtractor(this); extractor->extract(m_fileName); connect(extractor, SIGNAL(lengthFound(int)), this, SLOT(lengthFound(int))); connect(extractor, SIGNAL(unableToDetermineLength()), this, SLOT(unableToDetermineLength())); } QString BackgroundJobs::ReadVideoLengthJob::title() const { return i18n("Read Video Length"); } QString BackgroundJobs::ReadVideoLengthJob::details() const { return m_fileName.relative(); } void BackgroundJobs::ReadVideoLengthJob::lengthFound(int length) { DB::ImageInfoPtr info = DB::ImageDB::instance()->info(m_fileName); // Only mark dirty if it is required if (info->videoLength() != length) { info->setVideoLength(length); MainWindow::DirtyIndicator::markDirty(); } emit completed(); } void BackgroundJobs::ReadVideoLengthJob::unableToDetermineLength() { // PENDING(blackie) Should we mark these as trouble, so we don't try them over and over again? emit completed(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundJobs/SearchForVideosWithoutLengthInfo.cpp b/BackgroundJobs/SearchForVideosWithoutLengthInfo.cpp index 9e650848..7ca13652 100644 --- a/BackgroundJobs/SearchForVideosWithoutLengthInfo.cpp +++ b/BackgroundJobs/SearchForVideosWithoutLengthInfo.cpp @@ -1,65 +1,68 @@ /* Copyright (C) 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 "SearchForVideosWithoutLengthInfo.h" + #include "ReadVideoLengthJob.h" + #include #include #include #include + #include /** \class BackgroundJobs::SearchForVideosWithoutLengthInfo \brief Task for searching the database for videos without length information */ BackgroundJobs::SearchForVideosWithoutLengthInfo::SearchForVideosWithoutLengthInfo() : BackgroundTaskManager::JobInterface(BackgroundTaskManager::BackgroundVideoInfoRequest) { } void BackgroundJobs::SearchForVideosWithoutLengthInfo::execute() { const DB::FileNameList images = DB::ImageDB::instance()->images(); for (const DB::FileName &image : images) { const DB::ImageInfoPtr info = image.info(); if (!info->isVideo()) continue; // silently ignore videos not (currently) on disk: if (!info->fileName().exists()) continue; int length = info->videoLength(); if (length == -1) { BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::ReadVideoLengthJob(info->fileName(), BackgroundTaskManager::BackgroundVideoPreviewRequest)); } } emit completed(); } QString BackgroundJobs::SearchForVideosWithoutLengthInfo::title() const { return i18n("Search for videos without length information"); } QString BackgroundJobs::SearchForVideosWithoutLengthInfo::details() const { return QString(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundJobs/SearchForVideosWithoutVideoThumbnailsJob.cpp b/BackgroundJobs/SearchForVideosWithoutVideoThumbnailsJob.cpp index 4dd2cce3..f60b5145 100644 --- a/BackgroundJobs/SearchForVideosWithoutVideoThumbnailsJob.cpp +++ b/BackgroundJobs/SearchForVideosWithoutVideoThumbnailsJob.cpp @@ -1,76 +1,79 @@ /* Copyright (C) 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 "SearchForVideosWithoutVideoThumbnailsJob.h" + #include "ExtractOneThumbnailJob.h" +#include "HandleVideoThumbnailRequestJob.h" #include "ReadVideoLengthJob.h" -#include + #include #include #include #include + #include #include using namespace BackgroundJobs; void BackgroundJobs::SearchForVideosWithoutVideoThumbnailsJob::execute() { const DB::FileNameList images = DB::ImageDB::instance()->images(); for (const DB::FileName &image : images) { const DB::ImageInfoPtr info = image.info(); if (!info->isVideo()) continue; // silently ignore videos not (currently) on disk: if (!info->fileName().exists()) continue; const DB::FileName thumbnailName = BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(info->fileName(), 9); if (thumbnailName.exists()) continue; BackgroundJobs::ReadVideoLengthJob *readVideoLengthJob = new BackgroundJobs::ReadVideoLengthJob(info->fileName(), BackgroundTaskManager::BackgroundVideoPreviewRequest); for (int i = 0; i < 10; ++i) { ExtractOneThumbnailJob *extractJob = new ExtractOneThumbnailJob(info->fileName(), i, BackgroundTaskManager::BackgroundVideoPreviewRequest); extractJob->addDependency(readVideoLengthJob); } BackgroundTaskManager::JobManager::instance()->addJob(readVideoLengthJob); } emit completed(); } QString BackgroundJobs::SearchForVideosWithoutVideoThumbnailsJob::title() const { return i18n("Searching for videos without video thumbnails"); } QString BackgroundJobs::SearchForVideosWithoutVideoThumbnailsJob::details() const { return QString(); } SearchForVideosWithoutVideoThumbnailsJob::SearchForVideosWithoutVideoThumbnailsJob() : JobInterface(BackgroundTaskManager::BackgroundVideoPreviewRequest) { } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/JobInfo.cpp b/BackgroundTaskManager/JobInfo.cpp index 28a07007..296dfb83 100644 --- a/BackgroundTaskManager/JobInfo.cpp +++ b/BackgroundTaskManager/JobInfo.cpp @@ -1,86 +1,87 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "JobInfo.h" + #include namespace BackgroundTaskManager { int JobInfo::s_jobCounter = 0; JobInfo::JobInfo(BackgroundTaskManager::Priority priority) : state(NotStarted) , m_priority(priority) , m_elapsed(0) , m_jobIndex(++s_jobCounter) { } JobInfo::JobInfo(const JobInfo *other) { m_priority = other->m_priority; state = other->state; m_elapsed = other->m_elapsed; m_jobIndex = other->m_jobIndex; } JobInfo::~JobInfo() { } Priority JobInfo::priority() const { return m_priority; } void JobInfo::start() { m_timer.start(); state = Running; } void JobInfo::stop() { m_elapsed = m_timer.elapsed(); state = Completed; } QString JobInfo::elapsed() const { if (state == NotStarted) return i18n("Not Started"); qint64 time = m_timer.elapsed(); if (state == Completed) time = m_elapsed; const int secs = time / 1000; const int part = (time % 1000) / 100; return QString::fromLatin1("%1.%2").arg(secs).arg(part); } int JobInfo::jobIndex() const { return m_jobIndex; } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/JobInfo.h b/BackgroundTaskManager/JobInfo.h index b75e6fcb..21eeed3c 100644 --- a/BackgroundTaskManager/JobInfo.h +++ b/BackgroundTaskManager/JobInfo.h @@ -1,68 +1,69 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef JOBINFO_H #define JOBINFO_H #include "Priority.h" + #include #include #include namespace BackgroundTaskManager { class JobInfo : public QObject { Q_OBJECT public: explicit JobInfo(BackgroundTaskManager::Priority priority); explicit JobInfo(const JobInfo *other); ~JobInfo() override; virtual QString title() const = 0; virtual QString details() const = 0; BackgroundTaskManager::Priority priority() const; enum State { NotStarted, Running, Completed }; State state; QString elapsed() const; int jobIndex() const; protected slots: void start(); void stop(); signals: void changed() const; private: BackgroundTaskManager::Priority m_priority; QElapsedTimer m_timer; uint m_elapsed; int m_jobIndex; static int s_jobCounter; }; } #endif // JOBINFO_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/JobInterface.cpp b/BackgroundTaskManager/JobInterface.cpp index 49121ba3..8efbb6f2 100644 --- a/BackgroundTaskManager/JobInterface.cpp +++ b/BackgroundTaskManager/JobInterface.cpp @@ -1,63 +1,64 @@ /* 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 "Logging.h" /** \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) { qCDebug(BackgroundTaskManagerLog) << "Created Job #" << jobIndex(); connect(this, SIGNAL(completed()), this, SLOT(stop())); } BackgroundTaskManager::JobInterface::~JobInterface() { } void BackgroundTaskManager::JobInterface::start() { qCDebug(BackgroundTaskManagerLog, "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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/JobInterface.h b/BackgroundTaskManager/JobInterface.h index 7455961e..bb63f264 100644 --- a/BackgroundTaskManager/JobInterface.h +++ b/BackgroundTaskManager/JobInterface.h @@ -1,53 +1,54 @@ /* Copyright (C) 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef JOBINTERFACE_H #define JOBINTERFACE_H #include "JobInfo.h" + #include namespace BackgroundTaskManager { class JobInterface : public JobInfo { Q_OBJECT public: explicit JobInterface(BackgroundTaskManager::Priority); ~JobInterface() override; void start(); void addDependency(JobInterface *job); protected: virtual void execute() = 0; signals: void completed(); private slots: void dependedJobCompleted(); private: int m_dependencies; }; } #endif // JOBINTERFACE_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/JobManager.cpp b/BackgroundTaskManager/JobManager.cpp index 34e0e4f9..7ce1cc45 100644 --- a/BackgroundTaskManager/JobManager.cpp +++ b/BackgroundTaskManager/JobManager.cpp @@ -1,135 +1,138 @@ /* Copyright (C) 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 "JobManager.h" + #include "JobInfo.h" + #include + #include /** \class BackgroundTaskManager::JobManager \brief Engine for running background jobs This is the engine for running background jobs. Each job is a subclass of \ref BackgroundTaskManager::JobInterface. The jobs are added using \ref addJob. Currently the jobs are executed one after the other on the main thread, but down the road I imagine it will provide for running jobs on secondary threads. The jobs would need to indicate that that is a possibility. */ BackgroundTaskManager::JobManager *BackgroundTaskManager::JobManager::s_instance = nullptr; BackgroundTaskManager::JobManager::JobManager() : m_isPaused(false) { } bool BackgroundTaskManager::JobManager::shouldExecute() const { return m_queue.hasForegroundTasks() || !m_isPaused; } int BackgroundTaskManager::JobManager::maxJobCount() const { // See comment in ImageManager::AsyncLoader::init() // We will at least have one active background task at the time, as some of them // currently aren't that much for background stuff. The key example of this is generating video thumbnails. const int max = qMin(3, QThread::idealThreadCount()); int count = qMax(1, max - ImageManager::AsyncLoader::instance()->activeCount() - 1); return count; } void BackgroundTaskManager::JobManager::execute() { if (m_queue.isEmpty()) return; if (!shouldExecute()) return; while (m_active.count() < maxJobCount() && !m_queue.isEmpty()) { JobInterface *job = m_queue.dequeue(); connect(job, SIGNAL(completed()), this, SLOT(jobCompleted())); m_active.append(job); emit jobStarted(job); job->start(); } } void BackgroundTaskManager::JobManager::addJob(BackgroundTaskManager::JobInterface *job) { m_queue.enqueue(job, job->priority()); execute(); } BackgroundTaskManager::JobManager *BackgroundTaskManager::JobManager::instance() { if (!s_instance) s_instance = new JobManager; return s_instance; } int BackgroundTaskManager::JobManager::activeJobCount() const { return m_active.count(); } BackgroundTaskManager::JobInfo *BackgroundTaskManager::JobManager::activeJob(int index) const { if (index < m_active.count()) return m_active[index]; return nullptr; } int BackgroundTaskManager::JobManager::futureJobCount() const { return m_queue.count(); } BackgroundTaskManager::JobInfo *BackgroundTaskManager::JobManager::futureJob(int index) const { return m_queue.peek(index); } bool BackgroundTaskManager::JobManager::isPaused() const { return m_isPaused; } bool BackgroundTaskManager::JobManager::hasActiveJobs() const { return !m_active.isEmpty(); } void BackgroundTaskManager::JobManager::jobCompleted() { JobInterface *job = qobject_cast(sender()); Q_ASSERT(job); emit jobEnded(job); m_active.removeAll(job); job->deleteLater(); execute(); } void BackgroundTaskManager::JobManager::togglePaused() { m_isPaused = !m_isPaused; execute(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/JobModel.cpp b/BackgroundTaskManager/JobModel.cpp index 99952380..f0d89728 100644 --- a/BackgroundTaskManager/JobModel.cpp +++ b/BackgroundTaskManager/JobModel.cpp @@ -1,183 +1,185 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "JobModel.h" + #include "CompletedJobInfo.h" #include "JobInfo.h" #include "JobManager.h" + #include #include #include #include #include #include namespace BackgroundTaskManager { JobModel::JobModel(QObject *parent) : QAbstractTableModel(parent) , blinkStateOn(true) { connect(JobManager::instance(), SIGNAL(jobStarted(JobInterface *)), this, SLOT(jobStarted(JobInterface *))); connect(JobManager::instance(), SIGNAL(jobEnded(JobInterface *)), this, SLOT(jobEnded(JobInterface *))); // Make the current task blink QTimer *timer = new QTimer(this); timer->start(500); connect(timer, SIGNAL(timeout()), this, SLOT(heartbeat())); } JobModel::~JobModel() { qDeleteAll(m_previousJobs); } int JobModel::rowCount(const QModelIndex &index) const { if (index.isValid()) return 0; else return m_previousJobs.count() + JobManager::instance()->activeJobCount() + JobManager::instance()->futureJobCount(); } int JobModel::columnCount(const QModelIndex &) const { return 4; } QVariant JobModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); const int row = index.row(); const int col = index.column(); JobInfo *current = info(row); if (!current) return QVariant(); if (role == Qt::DisplayRole) { switch (col) { case IDCol: return current->jobIndex(); case TitleCol: return current->title(); case DetailsCol: return current->details(); case ElapsedCol: return current->elapsed(); default: return QVariant(); } } else if (role == Qt::DecorationRole && col == TitleCol) return statusImage(current->state); else if (role == Qt::TextAlignmentRole) return index.column() == IDCol ? Qt::AlignRight : Qt::AlignLeft; return QVariant(); } QVariant JobModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); switch (section) { case IDCol: return i18nc("@title:column Background job id", "ID"); case TitleCol: return i18nc("@title:column Background job title", "Title"); case DetailsCol: return i18nc("@title:column Additional information on background job", "Details"); case ElapsedCol: return i18nc("@title:column Elapsed time", "Elapsed"); default: return QVariant(); } } void JobModel::reset() { // FIXME: this is just a stand-in replacement for a call to the deprecated // QAbstractTableModel::reset(); // fix this by replacing the calls to reset() using: // beginInsertRows() // beginRemoveRows() // beginMoveRows() beginResetModel(); endResetModel(); } void JobModel::jobEnded(JobInterface *job) { m_previousJobs.append(new CompletedJobInfo(job)); reset(); } void JobModel::jobStarted(JobInterface *job) { connect(job, SIGNAL(changed()), this, SLOT(reset())); reset(); } void JobModel::heartbeat() { beginResetModel(); blinkStateOn = !blinkStateOn; // optional improvement: emit dataChanged for running jobs only endResetModel(); } JobInfo *JobModel::info(int row) const { if (row < m_previousJobs.count()) return m_previousJobs[row]; row -= m_previousJobs.count(); if (row < JobManager::instance()->activeJobCount()) return JobManager::instance()->activeJob(row); row -= JobManager::instance()->activeJobCount(); Q_ASSERT(row < JobManager::instance()->futureJobCount()); return JobManager::instance()->futureJob(row); } QPixmap JobModel::statusImage(JobInfo::State state) const { QColor color; if (state == JobInfo::Running) color = blinkStateOn ? Qt::green : Qt::gray; else if (state == JobInfo::Completed) color = Qt::red; else color = QColor(Qt::yellow).darker(); KLed led; led.setColor(color); QPalette pal = led.palette(); pal.setColor(QPalette::Window, Qt::white); led.setPalette(pal); return led.grab(); } } // namespace BackgroundTaskManager // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/JobModel.h b/BackgroundTaskManager/JobModel.h index 5730f61b..9d29d6e7 100644 --- a/BackgroundTaskManager/JobModel.h +++ b/BackgroundTaskManager/JobModel.h @@ -1,71 +1,72 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef BACKGROUNDTASKS_JOBMODEL_H #define BACKGROUNDTASKS_JOBMODEL_H #include "CompletedJobInfo.h" #include "JobInfo.h" #include "JobInterface.h" + #include namespace BackgroundTaskManager { class JobModel : public QAbstractTableModel { Q_OBJECT public: explicit JobModel(QObject *parent = nullptr); ~JobModel() override; int rowCount(const QModelIndex &) const override; int columnCount(const QModelIndex &) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; public slots: void reset(); private slots: void jobEnded(JobInterface *job); void jobStarted(JobInterface *job); /** * @brief heartbeat * Makes the running jobs blink. */ void heartbeat(); private: enum Column { IDCol = 0, TitleCol = 1, DetailsCol = 2, ElapsedCol = 3 }; bool blinkStateOn; JobInfo *info(int row) const; QPixmap statusImage(JobInfo::State state) const; QList m_previousJobs; }; } // namespace BackgroundTaskManager #endif // BACKGROUNDTASKS_JOBMODEL_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/JobViewer.cpp b/BackgroundTaskManager/JobViewer.cpp index 9619946a..07710c4c 100644 --- a/BackgroundTaskManager/JobViewer.cpp +++ b/BackgroundTaskManager/JobViewer.cpp @@ -1,82 +1,82 @@ /* Copyright 2012-2016 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ +#include "JobViewer.h" + +#include "JobManager.h" +#include "JobModel.h" + +#include #include #include #include #include -#include - -#include "JobManager.h" -#include "JobModel.h" -#include "JobViewer.h" - BackgroundTaskManager::JobViewer::JobViewer(QWidget *parent) : QDialog(parent) , m_model(nullptr) { setWindowTitle(i18nc("@title:window", "Background Job Viewer")); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); m_treeView = new QTreeView; mainLayout->addWidget(m_treeView); QDialogButtonBox *buttonBox = new QDialogButtonBox; m_pauseButton = buttonBox->addButton(i18n("Pause"), QDialogButtonBox::YesRole); buttonBox->addButton(QDialogButtonBox::Close); connect(m_pauseButton, SIGNAL(clicked()), this, SLOT(togglePause())); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::accept); mainLayout->addWidget(buttonBox); } void BackgroundTaskManager::JobViewer::setVisible(bool b) { if (b) { m_model = new JobModel(this); m_treeView->setModel(m_model); updatePauseButton(); } else { delete m_model; m_model = nullptr; } m_treeView->setColumnWidth(0, 50); m_treeView->setColumnWidth(1, 300); m_treeView->setColumnWidth(2, 300); m_treeView->setColumnWidth(3, 50); QDialog::setVisible(b); } void BackgroundTaskManager::JobViewer::togglePause() { JobManager::instance()->togglePaused(); updatePauseButton(); } void BackgroundTaskManager::JobViewer::updatePauseButton() { m_pauseButton->setText(JobManager::instance()->isPaused() ? i18n("Continue") : i18n("Pause")); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/PriorityQueue.cpp b/BackgroundTaskManager/PriorityQueue.cpp index e019daa0..8bf05980 100644 --- a/BackgroundTaskManager/PriorityQueue.cpp +++ b/BackgroundTaskManager/PriorityQueue.cpp @@ -1,81 +1,83 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "PriorityQueue.h" -#include "Utilities/AlgorithmHelper.h" + +#include + #include using namespace Utilities; namespace BackgroundTaskManager { PriorityQueue::PriorityQueue() { m_jobs.resize(SIZE_OF_PRIORITY_QUEUE); } bool PriorityQueue::isEmpty() const { return all_of(m_jobs, std::mem_fn(&QueueType::isEmpty)); } int PriorityQueue::count() const { return sum(m_jobs, std::mem_fn(&QueueType::length)); } void PriorityQueue::enqueue(JobInterface *job, Priority priority) { m_jobs[priority].enqueue(job); } JobInterface *PriorityQueue::dequeue() { for (QueueType &queue : m_jobs) { if (!queue.isEmpty()) return queue.dequeue(); } Q_ASSERT(false && "Queue was empty"); return nullptr; } JobInterface *PriorityQueue::peek(int index) const { int offset = 0; for (const QueueType &queue : m_jobs) { if (index - offset < queue.count()) return queue[index - offset]; else offset += queue.count(); } Q_ASSERT(false && "index beyond queue"); return nullptr; } bool PriorityQueue::hasForegroundTasks() const { for (int i = 0; i < BackgroundTask; ++i) { if (!m_jobs[i].isEmpty()) return true; } return false; } } // namespace BackgroundTaskManager // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/PriorityQueue.h b/BackgroundTaskManager/PriorityQueue.h index db0d1bda..b6c279d2 100644 --- a/BackgroundTaskManager/PriorityQueue.h +++ b/BackgroundTaskManager/PriorityQueue.h @@ -1,51 +1,52 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef BACKGROUNDTASKMANAGER_PRIORITYQUEUE_H #define BACKGROUNDTASKMANAGER_PRIORITYQUEUE_H #include "Priority.h" + #include #include namespace BackgroundTaskManager { class JobInterface; class PriorityQueue { public: PriorityQueue(); bool isEmpty() const; int count() const; void enqueue(JobInterface *job, Priority priority); JobInterface *dequeue(); JobInterface *peek(int index) const; bool hasForegroundTasks() const; private: typedef QQueue QueueType; QVector m_jobs; }; } // namespace BackgroundTaskManager #endif // BACKGROUNDTASKMANAGER_PRIORITYQUEUE_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/BackgroundTaskManager/StatusIndicator.cpp b/BackgroundTaskManager/StatusIndicator.cpp index 2b06862d..f810c6ff 100644 --- a/BackgroundTaskManager/StatusIndicator.cpp +++ b/BackgroundTaskManager/StatusIndicator.cpp @@ -1,104 +1,106 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "StatusIndicator.h" + #include "JobManager.h" #include "JobViewer.h" + #include #include #include #include #include namespace BackgroundTaskManager { StatusIndicator::StatusIndicator(QWidget *parent) : KLed(Qt::green, parent) , m_timer(new QTimer(this)) , m_jobViewer(nullptr) { connect(m_timer, SIGNAL(timeout()), this, SLOT(flicker())); setCursor(Qt::PointingHandCursor); connect(JobManager::instance(), SIGNAL(jobStarted(JobInterface *)), this, SLOT(maybeStartFlicker())); } bool StatusIndicator::event(QEvent *event) { if (event->type() == QEvent::ToolTip) { showToolTip(dynamic_cast(event)); return true; } return KLed::event(event); } void StatusIndicator::mouseReleaseEvent(QMouseEvent *) { if (!m_jobViewer) m_jobViewer = new JobViewer; m_jobViewer->setVisible(!m_jobViewer->isVisible()); } void StatusIndicator::flicker() { QColor newColor; if (!JobManager::instance()->hasActiveJobs()) { m_timer->stop(); newColor = Qt::gray; } else if (JobManager::instance()->isPaused() && !JobManager::instance()->hasActiveJobs()) newColor = QColor(Qt::yellow).lighter(); else newColor = (color() == Qt::gray ? currentColor() : Qt::gray); setColor(newColor); } void StatusIndicator::maybeStartFlicker() { if (!m_timer->isActive()) m_timer->start(500); } QColor StatusIndicator::currentColor() const { return JobManager::instance()->isPaused() ? Qt::yellow : Qt::green; } void StatusIndicator::showToolTip(QHelpEvent *event) { const int activeCount = JobManager::instance()->activeJobCount(); const int pendingCount = JobManager::instance()->futureJobCount(); const QString text = i18n("

Active jobs: %1
" "Pending jobs: %2" "



" "Color codes:" "
  • blinking green: Active background jobs
  • " "
  • gray: No active jobs
  • " "
  • solid yellow: Job queue is paused
  • " "
  • blinking yellow: Job queue is paused for background jobs, but is executing a foreground job " "(like extracting a thumbnail for a video file, which is currently shown in the thumbnail viewer)

", activeCount, pendingCount); QToolTip::showText(event->globalPos(), text); } } // namespace BackgroundTaskManager // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/AbstractCategoryModel.cpp b/Browser/AbstractCategoryModel.cpp index 9c9fbf08..1c206403 100644 --- a/Browser/AbstractCategoryModel.cpp +++ b/Browser/AbstractCategoryModel.cpp @@ -1,174 +1,177 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "AbstractCategoryModel.h" + #include "BrowserWidget.h" #include "enums.h" + #include #include + #include #include #include Browser::AbstractCategoryModel::AbstractCategoryModel(const DB::CategoryPtr &category, const DB::ImageSearchInfo &info) : m_category(category) , m_info(info) { m_images = DB::ImageDB::instance()->classify(info, m_category->name(), DB::Image); m_videos = DB::ImageDB::instance()->classify(info, m_category->name(), DB::Video); } bool Browser::AbstractCategoryModel::hasNoneEntry() const { int imageCount = m_images[DB::ImageDB::NONE()].count; int videoCount = m_videos[DB::ImageDB::NONE()].count; return (imageCount + videoCount != 0); } QString Browser::AbstractCategoryModel::text(const QString &name) const { if (name == DB::ImageDB::NONE()) { if (m_info.categoryMatchText(m_category->name()).length() == 0) return i18nc("As in No persons, no locations etc.", "None"); else return i18nc("As in no other persons, or no other locations. ", "No other"); } else { if (m_category->type() == DB::Category::FolderCategory) { QRegExp rx(QString::fromLatin1("(.*/)(.*)$")); QString value = name; value.replace(rx, QString::fromLatin1("\\2")); return value; } else { return name; } } } QPixmap Browser::AbstractCategoryModel::icon(const QString &name) const { const int size = m_category->thumbnailSize(); if (BrowserWidget::isResizing()) { QPixmap res(size, size * Settings::SettingsData::instance()->getThumbnailAspectRatio()); res.fill(Qt::white); return res; } if (m_category->viewType() == DB::Category::TreeView || m_category->viewType() == DB::Category::IconView) { if (DB::ImageDB::instance()->memberMap().isGroup(m_category->name(), name)) { return QIcon::fromTheme(QString::fromUtf8("folder-image")).pixmap(22); } else { return m_category->icon(); } } else { // The category images are screenshot from the size of the viewer (Which might very well be considered a bug) // This is the reason for asking for the thumbnail height being 3/4 of its width. return m_category->categoryImage(m_category->name(), name, size, size * Settings::SettingsData::instance()->getThumbnailAspectRatio()); } } QVariant Browser::AbstractCategoryModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); const QString name = indexToName(index); const int column = index.column(); if (role == Qt::DisplayRole) { switch (column) { case 0: return text(name); case 1: return i18ncp("@item:intable number of images with a specific tag.", "1 image", "%1 images", m_images[name].count); case 2: return i18ncp("@item:intable number of videos with a specific tag.", "1 video", "%1 videos", m_videos[name].count); case 3: { DB::ImageDate range = m_images[name].range; range.extendTo(m_videos[name].range); return DB::ImageDate(range.start()).toString(false); } case 4: { DB::ImageDate range = m_images[name].range; range.extendTo(m_videos[name].range); return DB::ImageDate(range.end()).toString(false); } } } else if (role == Qt::DecorationRole && column == 0) { return icon(name); } else if (role == Qt::ToolTipRole) return text(name); else if (role == ItemNameRole) return name; else if (role == ValueRole) { switch (column) { case 0: return name; // Notice we sort by **None** rather than None, which makes it show up at the top for less than searches. case 1: return m_images[name].count; case 2: return m_videos[name].count; case 3: { DB::ImageDate range = m_images[name].range; range.extendTo(m_videos[name].range); return range.start().toSecsSinceEpoch(); } case 4: { DB::ImageDate range = m_images[name].range; range.extendTo(m_videos[name].range); return range.end().toSecsSinceEpoch(); } } } return QVariant(); } Qt::ItemFlags Browser::AbstractCategoryModel::flags(const QModelIndex &index) const { return index.isValid() ? Qt::ItemIsSelectable | Qt::ItemIsEnabled : Qt::ItemFlags(); } QVariant Browser::AbstractCategoryModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Vertical || role != Qt::DisplayRole) return QVariant(); switch (section) { case 0: return m_category->name(); case 1: return i18n("Images"); case 2: return i18n("Videos"); case 3: return i18n("Start Date"); case 4: return i18n("End Date"); } return QVariant(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/AbstractCategoryModel.h b/Browser/AbstractCategoryModel.h index 2c51ec92..fb7a55c1 100644 --- a/Browser/AbstractCategoryModel.h +++ b/Browser/AbstractCategoryModel.h @@ -1,61 +1,62 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef ABSTRACTCATEGORYMODEL_H #define ABSTRACTCATEGORYMODEL_H #include #include #include + #include namespace Browser { /** * \brief Base class for Category models * * See \ref Browser for a detailed description of how this fits in with the rest of the classes in this module * * This class implements what is common for \ref FlatCategoryModel and \ref TreeCategoryModel. */ class AbstractCategoryModel : public QAbstractItemModel { public: Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; protected: AbstractCategoryModel(const DB::CategoryPtr &category, const DB::ImageSearchInfo &info); bool hasNoneEntry() const; QString text(const QString &name) const; QPixmap icon(const QString &name) const; virtual QString indexToName(const QModelIndex &) const = 0; DB::CategoryPtr m_category; DB::ImageSearchInfo m_info; QMap m_images; QMap m_videos; }; } #endif /* ABSTRACTCATEGORYMODEL_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/Breadcrumb.cpp b/Browser/Breadcrumb.cpp index 9f658766..0edaa182 100644 --- a/Browser/Breadcrumb.cpp +++ b/Browser/Breadcrumb.cpp @@ -1,73 +1,74 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Breadcrumb.h" + #include int Browser::Breadcrumb::s_count = 0; Browser::Breadcrumb::Breadcrumb(const QString &text, bool isBeginning) : m_index(++s_count) , m_isBeginning(isBeginning) , m_isView(false) , m_text(text) { } Browser::Breadcrumb Browser::Breadcrumb::empty() { return Breadcrumb(QString()); } Browser::Breadcrumb Browser::Breadcrumb::home() { return Breadcrumb(i18nc("As in 'all pictures'.", "All"), true); } QString Browser::Breadcrumb::text() const { return m_text; } bool Browser::Breadcrumb::isBeginning() const { return m_isBeginning; } bool Browser::Breadcrumb::operator==(const Breadcrumb &other) const { return other.m_index == m_index; } bool Browser::Breadcrumb::operator!=(const Breadcrumb &other) const { return !(other == *this); } Browser::Breadcrumb Browser::Breadcrumb::view() { Breadcrumb res(QString(), false); res.m_isView = true; return res; } bool Browser::Breadcrumb::isView() const { return m_isView; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/BreadcrumbList.cpp b/Browser/BreadcrumbList.cpp index 1cb4c25f..c7cfc84e 100644 --- a/Browser/BreadcrumbList.cpp +++ b/Browser/BreadcrumbList.cpp @@ -1,46 +1,47 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "BreadcrumbList.h" + #include Browser::BreadcrumbList Browser::BreadcrumbList::latest() const { BreadcrumbList result; for (int i = count() - 1; i >= 0; --i) { const Breadcrumb crumb = at(i); const QString txt = crumb.text(); if (!txt.isEmpty() || crumb.isView()) result.prepend(crumb); if (crumb.isBeginning()) break; } return result; } QString Browser::BreadcrumbList::toString() const { QStringList list; for (const Breadcrumb &item : latest()) list.append(item.text()); return list.join(QString::fromLatin1(" > ")); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/BreadcrumbList.h b/Browser/BreadcrumbList.h index 7af5aed3..bb86e5d5 100644 --- a/Browser/BreadcrumbList.h +++ b/Browser/BreadcrumbList.h @@ -1,43 +1,44 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef BREADCRUMBLIST_H #define BREADCRUMBLIST_H #include "Breadcrumb.h" + #include namespace Browser { /** * \brief A List of \ref Breadcrumb's * * See \ref Browser for a detailed description of how this fits in with the rest of the classes in this module */ class BreadcrumbList : public QList { public: BreadcrumbList latest() const; QString toString() const; }; } #endif /* BREADCRUMBLIST_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/BrowserPage.h b/Browser/BrowserPage.h index 82b4c8b5..f9422ac7 100644 --- a/Browser/BrowserPage.h +++ b/Browser/BrowserPage.h @@ -1,74 +1,75 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef BROWSERPAGE_H #define BROWSERPAGE_H #include "Breadcrumb.h" + #include #include class QModelIndex; namespace Browser { class BrowserWidget; enum Viewer { ShowBrowser, ShowImageViewer, ShowGeoPositionViewer }; /** * \brief Information about a single page in the browser * * See \ref Browser for a detailed description of how this fits in with the rest of the classes in this module * * This interface represent a single page in the browser (one that you can go * back/forward to using the back/forward buttons in the toolbar). */ class BrowserPage : public QObject { Q_OBJECT public: BrowserPage(const DB::ImageSearchInfo &info, BrowserWidget *browser); ~BrowserPage() override {} /** * Construct the page. Result of activation may be to call \ref BrowserWidget::addAction. */ virtual void activate() = 0; virtual void deactivate(); virtual BrowserPage *activateChild(const QModelIndex &); virtual Viewer viewer(); virtual DB::Category::ViewType viewType() const; virtual bool isSearchable() const; virtual bool isViewChangeable() const; virtual Breadcrumb breadcrumb() const; virtual bool showDuringMovement() const; DB::ImageSearchInfo searchInfo() const; BrowserWidget *browser() const; private: DB::ImageSearchInfo m_info; BrowserWidget *m_browser; }; } #endif /* BROWSERPAGE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/BrowserWidget.cpp b/Browser/BrowserWidget.cpp index f19caac2..60fe84ae 100644 --- a/Browser/BrowserWidget.cpp +++ b/Browser/BrowserWidget.cpp @@ -1,468 +1,469 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "CategoryPage.h" #include "ImageViewPage.h" #include "OverviewPage.h" +#include "TreeCategoryModel.h" #include "TreeFilter.h" #include "enums.h" + +#include #include #include +#include +#include +#include + +#include #include +#include #include #include #include -#include - -#include "DB/CategoryCollection.h" -#include "Settings/SettingsData.h" -#include "Utilities/FileUtil.h" -#include "Utilities/ShowBusyCursor.h" -#include -#include #include +#include #include -#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 isFilterable(currentAction()->viewer() == ShowImageViewer); 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 (QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) 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::clicked, 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::clicked, 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(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/BrowserWidget.h b/Browser/BrowserWidget.h index a440d9c8..67f635cb 100644 --- a/Browser/BrowserWidget.h +++ b/Browser/BrowserWidget.h @@ -1,129 +1,131 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef BROWSER_H #define BROWSER_H #include "BreadcrumbList.h" #include "CenteringIconView.h" -#include "Settings/SettingsData.h" + +#include + #include class CenteringIconView; class QSortFilterProxyModel; class QTreeView; class QListView; class QStackedWidget; namespace DB { class ImageSearchInfo; class FileName; } namespace Browser { class TreeFilter; class BrowserPage; /** * \brief The widget that makes up the Browser, and the interface to the other modules in KPhotoAlbum. * * See \ref Browser for a detailed description of how this fits in with the rest of the classes in this module */ class BrowserWidget : public QWidget { Q_OBJECT friend class ImageFolderAction; public: explicit BrowserWidget(QWidget *parent); void addSearch(DB::ImageSearchInfo &info); void addImageView(const DB::FileName &context); static BrowserWidget *instance(); void load(const QString &category, const QString &value); DB::ImageSearchInfo currentContext(); void setFocus(); QString currentCategory() const; void addAction(Browser::BrowserPage *); void setModel(QAbstractItemModel *); static bool isResizing() { return s_isResizing; } public slots: void back(); void forward(); void go(); void home(); void reload(); void slotSmallListView(); void slotLargeListView(); void slotSmallIconView(); void slotLargeIconView(); void slotLimitToMatch(const QString &); void slotInvokeSeleted(); void scrollKeyPressed(QKeyEvent *); void widenToBreadcrumb(const Browser::Breadcrumb &); signals: void canGoBack(bool); void canGoForward(bool); void showingOverview(); void pathChanged(const Browser::BreadcrumbList &); void isSearchable(bool); void isFilterable(bool); void isViewChangeable(bool); void currentViewTypeChanged(DB::Category::ViewType); void viewChanged(); void imageCount(uint); protected: bool eventFilter(QObject *, QEvent *) override; void activatePage(int pageIndex); private slots: void resetIconViewSearch(); void itemClicked(const QModelIndex &); void adjustTreeViewColumnSize(); void emitSignals(); private: void changeViewTypeForCurrentView(DB::Category::ViewType type); Browser::BrowserPage *currentAction() const; void switchToViewType(DB::Category::ViewType); void setBranchOpen(const QModelIndex &parent, bool open); Browser::BreadcrumbList createPath() const; void createWidgets(); void handleResizeEvent(QMouseEvent *); private: static BrowserWidget *s_instance; QList m_list; int m_current; QStackedWidget *m_stack; CenteringIconView *m_listView; QTreeView *m_treeView; QAbstractItemView *m_curView; TreeFilter *m_filterProxy; Browser::BreadcrumbList m_breadcrumbs; QPoint m_resizePressPos; static bool s_isResizing; }; } #endif /* BROWSER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/CategoryPage.cpp b/Browser/CategoryPage.cpp index bc4be538..c1fa05fe 100644 --- a/Browser/CategoryPage.cpp +++ b/Browser/CategoryPage.cpp @@ -1,75 +1,78 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "CategoryPage.h" + #include "BrowserWidget.h" #include "FlatCategoryModel.h" #include "ImageViewPage.h" #include "OverviewPage.h" #include "TreeCategoryModel.h" #include "enums.h" + #include + #include Browser::CategoryPage::CategoryPage(const DB::CategoryPtr &category, const DB::ImageSearchInfo &info, BrowserWidget *browser) : BrowserPage(info, browser) , m_category(category) , m_model(nullptr) { } void Browser::CategoryPage::activate() { delete m_model; if (m_category->viewType() == DB::Category::TreeView || m_category->viewType() == DB::Category::ThumbedTreeView) m_model = new TreeCategoryModel(m_category, searchInfo()); else m_model = new FlatCategoryModel(m_category, searchInfo()); browser()->setModel(m_model); } Browser::BrowserPage *Browser::CategoryPage::activateChild(const QModelIndex &index) { const QString name = m_model->data(index, ItemNameRole).value(); DB::ImageSearchInfo info = searchInfo(); info.addAnd(m_category->name(), name); if (DB::ImageDB::instance()->search(info).size() <= Settings::SettingsData::instance()->autoShowThumbnailView()) { browser()->addAction(new Browser::OverviewPage(Breadcrumb(name), info, browser())); return new ImageViewPage(info, browser()); } else return new Browser::OverviewPage(Breadcrumb(name), info, browser()); } DB::CategoryPtr Browser::CategoryPage::category() const { return m_category; } DB::Category::ViewType Browser::CategoryPage::viewType() const { return m_category->viewType(); } bool Browser::CategoryPage::isViewChangeable() const { return true; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/CategoryPage.h b/Browser/CategoryPage.h index 6d511c5b..8d790943 100644 --- a/Browser/CategoryPage.h +++ b/Browser/CategoryPage.h @@ -1,59 +1,60 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CATEGORYPAGE_H #define CATEGORYPAGE_H #include "BrowserPage.h" -#include "DB/CategoryPtr.h" + #include +#include #include class QAbstractItemModel; class FlatCategoryModel; class BrowserWidget; namespace Browser { /** * \brief The Browser page for categories. * * See \ref Browser for a detailed description of how this fits in with the rest of the classes in this module * */ class CategoryPage : public BrowserPage { public: CategoryPage(const DB::CategoryPtr &category, const DB::ImageSearchInfo &info, BrowserWidget *browser); void activate() override; BrowserPage *activateChild(const QModelIndex &) override; DB::Category::ViewType viewType() const override; bool isViewChangeable() const override; DB::CategoryPtr category() const; private: const DB::CategoryPtr m_category; QAbstractItemModel *m_model; }; } #endif /* CATEGORYPAGE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/CenteringIconView.cpp b/Browser/CenteringIconView.cpp index db86e4d7..f028505d 100644 --- a/Browser/CenteringIconView.cpp +++ b/Browser/CenteringIconView.cpp @@ -1,123 +1,125 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "CenteringIconView.h" -#include "Settings/SettingsData.h" -#include "Utilities/FileUtil.h" -#include + +#include #include +#include + +#include #include const int CELLWIDTH = 200; const int CELLHEIGHT = 150; Browser::CenteringIconView::CenteringIconView(QWidget *parent) : QListView(parent) , m_viewMode(NormalIconView) { QPalette pal = palette(); pal.setBrush(QPalette::Base, QApplication::palette().color(QPalette::Base)); setPalette(pal); setGridSize(QSize(CELLWIDTH, CELLHEIGHT)); viewport()->setAutoFillBackground(false); QListView::setViewMode(QListView::IconMode); } void Browser::CenteringIconView::setViewMode(ViewMode viewMode) { m_viewMode = viewMode; if (viewMode == CenterView) { setGridSize(QSize(200, 150)); setupMargins(); } else { setGridSize(QSize()); setViewportMargins(0, 0, 0, 0); } } void Browser::CenteringIconView::setupMargins() { if (m_viewMode == NormalIconView || !model() || !viewport()) return; // In this code I'll call resize, which calls resizeEvent, which calls // this code. So I need to break that loop, which I do here. static bool inAction = false; Utilities::BooleanGuard guard(inAction); if (!guard.canContinue()) return; const int count = model()->rowCount(); if (count == 0) return; const int columns = columnCount(count); const int rows = std::ceil(1.0 * count / columns); const int xMargin = (availableWidth() - columns * CELLWIDTH) / 2; const int yMargin = qMax(0, (int)(availableHeight() - rows * CELLHEIGHT) / 2); setViewportMargins(xMargin, yMargin, xMargin, yMargin); } void Browser::CenteringIconView::resizeEvent(QResizeEvent *event) { QListView::resizeEvent(event); setupMargins(); } void Browser::CenteringIconView::setModel(QAbstractItemModel *model) { QListView::setModel(model); setupMargins(); } void Browser::CenteringIconView::showEvent(QShowEvent *event) { setupMargins(); QListView::showEvent(event); } int Browser::CenteringIconView::columnCount(int elementCount) const { const int preferredCount = std::ceil(std::sqrt(elementCount)); const int maxVisibleColumnsPossible = availableWidth() / CELLWIDTH; const int maxVisibleRowsPossible = qMax(1, availableHeight() / CELLHEIGHT); const int colCountToMakeAllRowVisible = std::ceil(1.0 * elementCount / maxVisibleRowsPossible); int res = preferredCount; res = qMax(res, colCountToMakeAllRowVisible); // Should we go for more due to limited row count? res = qMin(res, maxVisibleColumnsPossible); // No more than maximal visible count res = qMax(res, 1); // at least 1 return res; } int Browser::CenteringIconView::availableWidth() const { // The 40 and 10 below are some magic numbers. I think the reason I // need them is that the viewport doesn't cover all of the list views area. return width() - 40; } int Browser::CenteringIconView::availableHeight() const { return height() - 10; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/FlatCategoryModel.cpp b/Browser/FlatCategoryModel.cpp index c66d4992..0d7ad9a6 100644 --- a/Browser/FlatCategoryModel.cpp +++ b/Browser/FlatCategoryModel.cpp @@ -1,72 +1,74 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "FlatCategoryModel.h" + #include + #include Browser::FlatCategoryModel::FlatCategoryModel(const DB::CategoryPtr &category, const DB::ImageSearchInfo &info) : AbstractCategoryModel(category, info) { if (hasNoneEntry()) m_items.append(DB::ImageDB::NONE()); QStringList items = m_category->itemsInclCategories(); items.sort(); Q_FOREACH (const QString &name, items) { const int imageCount = m_images.contains(name) ? m_images[name].count : 0; const int videoCount = m_videos.contains(name) ? m_videos[name].count : 0; if (imageCount + videoCount > 0) m_items.append(name); } } int Browser::FlatCategoryModel::rowCount(const QModelIndex &index) const { if (!index.isValid()) return m_items.count(); else return 0; } int Browser::FlatCategoryModel::columnCount(const QModelIndex &) const { return 1; } QModelIndex Browser::FlatCategoryModel::index(int row, int column, const QModelIndex &parent) const { if (row >= 0 && row < rowCount(parent) && column >= 0 && column < columnCount(parent)) return createIndex(row, column); else return QModelIndex(); } QModelIndex Browser::FlatCategoryModel::parent(const QModelIndex &) const { return QModelIndex(); } QString Browser::FlatCategoryModel::indexToName(const QModelIndex &index) const { return m_items[index.row()]; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/FlatCategoryModel.h b/Browser/FlatCategoryModel.h index a00e2df3..b4e87af2 100644 --- a/Browser/FlatCategoryModel.h +++ b/Browser/FlatCategoryModel.h @@ -1,58 +1,59 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef FLATCATEGORYMODEL_H #define FLATCATEGORYMODEL_H #include "AbstractCategoryModel.h" + #include #include namespace RemoteControl { class RemoteInterface; } namespace Browser { /** * \brief Implements a flat model for categories - ie. a model where all catergories, including subcategories, are shown. * * See \ref Browser for a detailed description of how this fits in with the rest of the classes in this module */ class FlatCategoryModel : public AbstractCategoryModel { public: FlatCategoryModel(const DB::CategoryPtr &category, const DB::ImageSearchInfo &info); int rowCount(const QModelIndex &index) const override; int columnCount(const QModelIndex &) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; QString indexToName(const QModelIndex &) const override; private: friend class RemoteControl::RemoteInterface; QStringList m_items; }; } #endif /* FLATCATEGORYMODEL_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/GeoPositionPage.cpp b/Browser/GeoPositionPage.cpp index ffc06b32..99fcc94a 100644 --- a/Browser/GeoPositionPage.cpp +++ b/Browser/GeoPositionPage.cpp @@ -1,86 +1,88 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "GeoPositionPage.h" + #include "BrowserWidget.h" -#include "DB/ImageDB.h" #include "ImageViewPage.h" -#include "MainWindow/Window.h" #include "OverviewPage.h" #include "enums.h" +#include +#include + #include Browser::GeoPositionPage::GeoPositionPage(const DB::ImageSearchInfo &info, BrowserWidget *browser) : BrowserPage(info, browser) { active = false; } void Browser::GeoPositionPage::activate() { if (!active) { MainWindow::Window::theMainWindow()->showPositionBrowser(); Browser::PositionBrowserWidget *positionBrowserWidget = MainWindow::Window::theMainWindow()->positionBrowserWidget(); positionBrowserWidget->showImages(searchInfo()); connect(positionBrowserWidget, &Browser::PositionBrowserWidget::signalNewRegionSelected, this, &GeoPositionPage::slotNewRegionSelected); active = true; } } void Browser::GeoPositionPage::deactivate() { if (active) { active = false; Browser::PositionBrowserWidget *positionBrowserWidget = MainWindow::Window::theMainWindow()->positionBrowserWidget(); positionBrowserWidget->clearImages(); disconnect(positionBrowserWidget, 0, this, 0); } } void Browser::GeoPositionPage::slotNewRegionSelected(KGeoMap::GeoCoordinates::Pair coordinates) { const QString name = i18n("Geo position"); DB::ImageSearchInfo info = searchInfo(); info.setRegionSelection(coordinates); browser()->addAction(new Browser::OverviewPage(Breadcrumb(name), info, browser())); if (DB::ImageDB::instance()->search(info).size() <= Settings::SettingsData::instance()->autoShowThumbnailView()) { browser()->addAction(new ImageViewPage(info, browser())); } } Browser::Viewer Browser::GeoPositionPage::viewer() { return ShowGeoPositionViewer; } bool Browser::GeoPositionPage::isSearchable() const { return false; } bool Browser::GeoPositionPage::showDuringMovement() const { return true; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/GeoPositionPage.h b/Browser/GeoPositionPage.h index b2570e2c..9ca30f24 100644 --- a/Browser/GeoPositionPage.h +++ b/Browser/GeoPositionPage.h @@ -1,61 +1,62 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef GEOPOSITIONPAGE_H #define GEOPOSITIONPAGE_H #include "BrowserPage.h" -#include "DB/CategoryPtr.h" + #include +#include #include class QAbstractItemModel; class FlatCategoryModel; class BrowserWidget; namespace Browser { /** * \brief The Browser page for categories. * * See \ref Browser for a detailed description of how this fits in with the rest of the classes in this module * */ class GeoPositionPage : public BrowserPage { Q_OBJECT public: GeoPositionPage(const DB::ImageSearchInfo &info, BrowserWidget *browser); Viewer viewer() override; void activate() override; void deactivate() override; bool isSearchable() const override; bool showDuringMovement() const override; public slots: void slotNewRegionSelected(KGeoMap::GeoCoordinates::Pair coordinates); private: bool active; }; } #endif /* GEOPOSITIONPAGE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/ImageViewPage.cpp b/Browser/ImageViewPage.cpp index 3cec300a..9c4007a4 100644 --- a/Browser/ImageViewPage.cpp +++ b/Browser/ImageViewPage.cpp @@ -1,64 +1,65 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageViewPage.h" -#include "ThumbnailView/ThumbnailFacade.h" + #include #include +#include Browser::ImageViewPage::ImageViewPage(const DB::ImageSearchInfo &info, BrowserWidget *browser) : BrowserPage(info, browser) { } void Browser::ImageViewPage::activate() { MainWindow::Window::theMainWindow()->showThumbNails(DB::ImageDB::instance()->search(searchInfo())); if (!m_context.isNull()) { // PENDING(blackie) this is the only place that uses the ThumbnailFacade as a singleton. Rewrite to make it communicate with it otherwise. ThumbnailView::ThumbnailFacade::instance()->setCurrentItem(m_context); } } Browser::Viewer Browser::ImageViewPage::viewer() { return Browser::ShowImageViewer; } bool Browser::ImageViewPage::isSearchable() const { return false; } Browser::ImageViewPage::ImageViewPage(const DB::FileName &context, BrowserWidget *browser) : BrowserPage(DB::ImageSearchInfo(), browser) , m_context(context) { } bool Browser::ImageViewPage::showDuringMovement() const { return true; } Browser::Breadcrumb Browser::ImageViewPage::breadcrumb() const { return Breadcrumb::view(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/ImageViewPage.h b/Browser/ImageViewPage.h index 1e789dc0..bfd1d933 100644 --- a/Browser/ImageViewPage.h +++ b/Browser/ImageViewPage.h @@ -1,52 +1,53 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef IMAGEVIEWPAGE_H #define IMAGEVIEWPAGE_H #include "BrowserPage.h" + #include #include namespace Browser { /** * \brief The page showing the actual images. * * See \ref Browser for a detailed description of how this fits in with the rest of the classes in this module */ class ImageViewPage : public BrowserPage { public: ImageViewPage(const DB::ImageSearchInfo &info, BrowserWidget *browser); ImageViewPage(const DB::FileName &context, BrowserWidget *browser); void activate() override; Viewer viewer() override; bool isSearchable() const override; bool showDuringMovement() const override; Breadcrumb breadcrumb() const override; private: DB::FileName m_context; }; } #endif /* IMAGEVIEWPAGE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/OverviewPage.cpp b/Browser/OverviewPage.cpp index df6c15a5..3026e7e4 100644 --- a/Browser/OverviewPage.cpp +++ b/Browser/OverviewPage.cpp @@ -1,342 +1,341 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "OverviewPage.h" #include "BrowserWidget.h" #include "CategoryPage.h" #include "ImageViewPage.h" #include "enums.h" #include #ifdef HAVE_KGEOMAP -#include +#include "GeoPositionPage.h" #endif #include #include #include #include +#include #include #include #include -#include - #include #include #include const int THUMBNAILSIZE = 70; AnnotationDialog::Dialog *Browser::OverviewPage::s_config = nullptr; Browser::OverviewPage::OverviewPage(const Breadcrumb &breadcrumb, const DB::ImageSearchInfo &info, BrowserWidget *browser) : BrowserPage(info, browser) , m_breadcrumb(breadcrumb) { //updateImageCount(); } int Browser::OverviewPage::rowCount(const QModelIndex &parent) const { if (parent != QModelIndex()) return 0; return categories().count() + #ifdef HAVE_KGEOMAP 1 + #endif 4; // Exiv search + Search info + Untagged Images + Show Image } QVariant Browser::OverviewPage::data(const QModelIndex &index, int role) const { if (role == ValueRole) return index.row(); const int row = index.row(); if (isCategoryIndex(row)) return categoryInfo(row, role); #ifdef HAVE_KGEOMAP else if (isGeoPositionIndex(row)) return geoPositionInfo(role); #endif else if (isExivIndex(row)) return exivInfo(role); else if (isSearchIndex(row)) return searchInfo(role); else if (isUntaggedImagesIndex(row)) return untaggedImagesInfo(role); else if (isImageIndex(row)) return imageInfo(role); return QVariant(); } bool Browser::OverviewPage::isCategoryIndex(int row) const { return row < categories().count() && row >= 0; } bool Browser::OverviewPage::isGeoPositionIndex(int row) const { #ifdef HAVE_KGEOMAP return row == categories().count(); #else Q_UNUSED(row); return false; #endif } bool Browser::OverviewPage::isExivIndex(int row) const { int exivRow = categories().count(); #ifdef HAVE_KGEOMAP exivRow++; #endif return row == exivRow; } bool Browser::OverviewPage::isSearchIndex(int row) const { return rowCount() - 3 == row; } bool Browser::OverviewPage::isUntaggedImagesIndex(int row) const { return rowCount() - 2 == row; } bool Browser::OverviewPage::isImageIndex(int row) const { return rowCount() - 1 == row; } QList Browser::OverviewPage::categories() const { return DB::ImageDB::instance()->categoryCollection()->categories(); } QVariant Browser::OverviewPage::categoryInfo(int row, int role) const { if (role == Qt::DisplayRole) return categories()[row]->name(); else if (role == Qt::DecorationRole) return categories()[row]->icon(THUMBNAILSIZE); return QVariant(); } QVariant Browser::OverviewPage::geoPositionInfo(int role) const { if (role == Qt::DisplayRole) return i18n("Geo Position"); else if (role == Qt::DecorationRole) { return QIcon::fromTheme(QString::fromLatin1("globe")).pixmap(THUMBNAILSIZE); } return QVariant(); } QVariant Browser::OverviewPage::exivInfo(int role) const { if (role == Qt::DisplayRole) return i18n("Exif Info"); else if (role == Qt::DecorationRole) { return QIcon::fromTheme(QString::fromLatin1("document-properties")).pixmap(THUMBNAILSIZE); } return QVariant(); } QVariant Browser::OverviewPage::searchInfo(int role) const { if (role == Qt::DisplayRole) return i18nc("@action Search button in the browser view.", "Search"); else if (role == Qt::DecorationRole) return QIcon::fromTheme(QString::fromLatin1("system-search")).pixmap(THUMBNAILSIZE); return QVariant(); } QVariant Browser::OverviewPage::untaggedImagesInfo(int role) const { if (role == Qt::DisplayRole) return i18n("Untagged Images"); else if (role == Qt::DecorationRole) return QIcon::fromTheme(QString::fromUtf8("archive-insert")).pixmap(THUMBNAILSIZE); return QVariant(); } QVariant Browser::OverviewPage::imageInfo(int role) const { if (role == Qt::DisplayRole) return i18n("Show Thumbnails"); else if (role == Qt::DecorationRole) { QIcon icon = QIcon::fromTheme(QString::fromUtf8("view-preview")); QPixmap pixmap = icon.pixmap(THUMBNAILSIZE); // workaround for QListView in Qt 5.5: // On Qt5.5 if the last item in the list view has no DecorationRole, then // the whole list view "collapses" to the size of text-only items, // cutting off the existing thumbnails. // This can be triggered by an incomplete icon theme. if (pixmap.isNull()) { pixmap = QPixmap(THUMBNAILSIZE, THUMBNAILSIZE); pixmap.fill(Qt::transparent); } return pixmap; } return QVariant(); } Browser::BrowserPage *Browser::OverviewPage::activateChild(const QModelIndex &index) { const int row = index.row(); if (isCategoryIndex(row)) return new Browser::CategoryPage(categories()[row], BrowserPage::searchInfo(), browser()); #ifdef HAVE_KGEOMAP else if (isGeoPositionIndex(row)) return new Browser::GeoPositionPage(BrowserPage::searchInfo(), browser()); #endif else if (isExivIndex(row)) return activateExivAction(); else if (isSearchIndex(row)) return activateSearchAction(); else if (isUntaggedImagesIndex(row)) { return activateUntaggedImagesAction(); } else if (isImageIndex(row)) return new ImageViewPage(BrowserPage::searchInfo(), browser()); return nullptr; } void Browser::OverviewPage::activate() { updateImageCount(); browser()->setModel(this); } Qt::ItemFlags Browser::OverviewPage::flags(const QModelIndex &index) const { if (isCategoryIndex(index.row()) && !m_rowHasSubcategories[index.row()]) return QAbstractListModel::flags(index) & ~Qt::ItemIsEnabled; else return QAbstractListModel::flags(index); } bool Browser::OverviewPage::isSearchable() const { return true; } Browser::BrowserPage *Browser::OverviewPage::activateExivAction() { QPointer dialog = new Exif::SearchDialog(browser()); { Utilities::ShowBusyCursor undoTheBusyWhileShowingTheDialog(Qt::ArrowCursor); if (dialog->exec() == QDialog::Rejected) { delete dialog; return nullptr; } // Dialog can be deleted by its parent in event loop while in exec() if (dialog.isNull()) return nullptr; } Exif::SearchInfo result = dialog->info(); DB::ImageSearchInfo info = BrowserPage::searchInfo(); info.addExifSearchInfo(dialog->info()); delete dialog; if (DB::ImageDB::instance()->count(info).total() == 0) { KMessageBox::information(browser(), i18n("Search did not match any images or videos."), i18n("Empty Search Result")); return nullptr; } return new OverviewPage(Breadcrumb(i18n("Exif Search")), info, browser()); } Browser::BrowserPage *Browser::OverviewPage::activateSearchAction() { if (!s_config) s_config = new AnnotationDialog::Dialog(browser()); Utilities::ShowBusyCursor undoTheBusyWhileShowingTheDialog(Qt::ArrowCursor); DB::ImageSearchInfo tmpInfo = BrowserPage::searchInfo(); DB::ImageSearchInfo info = s_config->search(&tmpInfo); // PENDING(blackie) why take the address? if (info.isNull()) return nullptr; if (DB::ImageDB::instance()->count(info).total() == 0) { KMessageBox::information(browser(), i18n("Search did not match any images or videos."), i18n("Empty Search Result")); return nullptr; } return new OverviewPage(Breadcrumb(i18nc("Breadcrumb denoting that we 'browsed' to a search result.", "search")), info, browser()); } Browser::Breadcrumb Browser::OverviewPage::breadcrumb() const { return m_breadcrumb; } bool Browser::OverviewPage::showDuringMovement() const { return true; } void Browser::OverviewPage::updateImageCount() { QElapsedTimer timer; timer.start(); int row = 0; for (const DB::CategoryPtr &category : categories()) { QMap items = DB::ImageDB::instance()->classify(BrowserPage::searchInfo(), category->name(), DB::anyMediaType, DB::ClassificationMode::PartialCount); m_rowHasSubcategories[row] = items.count() > 1; ++row; } qCDebug(TimingLog) << "Browser::Overview::updateImageCount(): " << timer.elapsed() << "ms."; } Browser::BrowserPage *Browser::OverviewPage::activateUntaggedImagesAction() { if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()) { DB::ImageSearchInfo info = BrowserPage::searchInfo(); info.addAnd(Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag()); return new ImageViewPage(info, browser()); } else { // Note: the same dialog text is used in MainWindow::Window::slotMarkUntagged(), // so if it is changed, be sure to also change it there! KMessageBox::information(browser(), i18n("

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

" "

Please follow these steps to do so:" "

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

"), i18n("Feature has not been configured")); return nullptr; } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/OverviewPage.h b/Browser/OverviewPage.h index 755fde5b..19c69858 100644 --- a/Browser/OverviewPage.h +++ b/Browser/OverviewPage.h @@ -1,100 +1,102 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef OVERVIEWPAGE_H #define OVERVIEWPAGE_H #include "Breadcrumb.h" #include "BrowserPage.h" + #include + #include namespace AnnotationDialog { class Dialog; } namespace DB { class ImageSearchInfo; class MediaCount; } namespace Browser { class BrowserWidget; /** * \brief The overview page in the browser (the one containing People, Places, Show Images etc) * * See \ref Browser for a detailed description of how this fits in with the rest of the classes in this module * * The OverviewPage implements two interfaces \ref BrowserPage (with * information about the page itself) and QAbstractListModel (the model * set on the view in the Browser). * * Combining both in the same class was done mostly for convenience, the * two interfaces was to a large extend referring to the same data. */ class OverviewPage : public QAbstractListModel, public BrowserPage { public: OverviewPage(const Breadcrumb &breadcrumb, const DB::ImageSearchInfo &info, Browser::BrowserWidget *); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; void activate() override; BrowserPage *activateChild(const QModelIndex &) override; Qt::ItemFlags flags(const QModelIndex &index) const override; bool isSearchable() const override; Breadcrumb breadcrumb() const override; bool showDuringMovement() const override; private: /** * @brief Count images/videos in each category. */ void updateImageCount(); QList categories() const; bool isCategoryIndex(int row) const; bool isGeoPositionIndex(int row) const; bool isExivIndex(int row) const; bool isSearchIndex(int row) const; bool isUntaggedImagesIndex(int row) const; bool isImageIndex(int row) const; QVariant categoryInfo(int row, int role) const; QVariant geoPositionInfo(int role) const; QVariant exivInfo(int role) const; QVariant searchInfo(int role) const; QVariant untaggedImagesInfo(int rolw) const; QVariant imageInfo(int role) const; BrowserPage *activateExivAction(); BrowserPage *activateSearchAction(); BrowserPage *activateUntaggedImagesAction(); private: QMap m_rowHasSubcategories; static AnnotationDialog::Dialog *s_config; Breadcrumb m_breadcrumb; }; } #endif /* OVERVIEWPAGE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/PositionBrowserWidget.cpp b/Browser/PositionBrowserWidget.cpp index 23909653..e7d16092 100644 --- a/Browser/PositionBrowserWidget.cpp +++ b/Browser/PositionBrowserWidget.cpp @@ -1,72 +1,71 @@ /* Copyright (C) 2016-2019 The KPhotoAlbum Development Team Copyright (C) 2016-2017 Matthias Füssel 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 "PositionBrowserWidget.h" +#include +#include + +#include #include #include #include #include #include -#include - -#include "DB/ImageDB.h" -#include "DB/ImageInfo.h" - Browser::PositionBrowserWidget::PositionBrowserWidget(QWidget *parent) : QWidget(parent) { m_mapView = new Map::MapView(this); m_mapView->displayStatus(Map::MapView::MapStatus::Loading); QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(m_mapView); connect(m_mapView, &Map::MapView::signalRegionSelectionChanged, this, &Browser::PositionBrowserWidget::slotRegionSelectionChanged); } Browser::PositionBrowserWidget::~PositionBrowserWidget() { } void Browser::PositionBrowserWidget::showImages(const DB::ImageSearchInfo &searchInfo) { m_mapView->displayStatus(Map::MapView::MapStatus::Loading); m_mapView->clear(); DB::FileNameList images = DB::ImageDB::instance()->search(searchInfo); for (DB::FileNameList::const_iterator imageIter = images.constBegin(); imageIter < images.constEnd(); ++imageIter) { DB::ImageInfoPtr image = imageIter->info(); if (image->coordinates().hasCoordinates()) { m_mapView->addImage(image); } } m_mapView->displayStatus(Map::MapView::MapStatus::SearchCoordinates); m_mapView->zoomToMarkers(); } void Browser::PositionBrowserWidget::clearImages() { m_mapView->clear(); } void Browser::PositionBrowserWidget::slotRegionSelectionChanged() { if (m_mapView->regionSelected()) { emit signalNewRegionSelected(m_mapView->getRegionSelection()); } } diff --git a/Browser/PositionBrowserWidget.h b/Browser/PositionBrowserWidget.h index e5a409a1..1af5973b 100644 --- a/Browser/PositionBrowserWidget.h +++ b/Browser/PositionBrowserWidget.h @@ -1,52 +1,54 @@ /* Copyright (C) 2016-2017 Matthias Füssel This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef POSITIONBROWSERWIDGET_H_ #define POSITIONBROWSERWIDGET_H_ -#include "DB/FileNameList.h" -#include "DB/ImageSearchInfo.h" -#include "Map/MapView.h" #include "qwidget.h" +#include +#include + +#include + namespace Browser { class PositionBrowserWidget : public QWidget { Q_OBJECT public: PositionBrowserWidget(QWidget *parent); ~PositionBrowserWidget() override; virtual void showImages(const DB::ImageSearchInfo &searchInfo); virtual void clearImages(); Q_SIGNALS: void signalNewRegionSelected(KGeoMap::GeoCoordinates::Pair coordinates); public slots: void slotRegionSelectionChanged(); private: Map::MapView *m_mapView; }; } #endif /* POSITIONBROWSERWIDGET_H_ */ diff --git a/Browser/TreeCategoryModel.cpp b/Browser/TreeCategoryModel.cpp index c3d2a521..bdf691ae 100644 --- a/Browser/TreeCategoryModel.cpp +++ b/Browser/TreeCategoryModel.cpp @@ -1,252 +1,253 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 // Local includes -#include "DB/Category.h" -#include "DB/CategoryItem.h" -#include "DB/ImageDB.h" -#include "MainWindow/DirtyIndicator.h" #include "TreeCategoryModel.h" +#include +#include +#include +#include + struct Browser::TreeCategoryModel::Data { Data(const QString &name) : name(name) , parent(nullptr) { } ~Data() { qDeleteAll(children); } void addChild(Data *child) { child->parent = this; children.append(child); } QString name; QList children; Data *parent; }; Browser::TreeCategoryModel::TreeCategoryModel(const DB::CategoryPtr &category, const DB::ImageSearchInfo &info) : AbstractCategoryModel(category, info) { m_data = new Data(QString()); createData(m_category->itemsCategories().data(), 0); if (hasNoneEntry()) { Data *data = new Data(DB::ImageDB::NONE()); data->parent = m_data; m_data->children.prepend(data); } m_memberMap = DB::ImageDB::instance()->memberMap(); } int Browser::TreeCategoryModel::rowCount(const QModelIndex &index) const { return indexToData(index)->children.count(); } int Browser::TreeCategoryModel::columnCount(const QModelIndex &) const { return 5; } QModelIndex Browser::TreeCategoryModel::index(int row, int column, const QModelIndex &parent) const { const Data *data = indexToData(parent); QList children = data->children; int size = children.count(); if (row >= size || row < 0 || column >= columnCount(parent) || column < 0) { // Invalid index return QModelIndex(); } else { return createIndex(row, column, children[row]); } } QModelIndex Browser::TreeCategoryModel::parent(const QModelIndex &index) const { Data *me = indexToData(index); if (me == m_data) { return QModelIndex(); } Data *parent = me->parent; if (parent == m_data) { return QModelIndex(); } Data *grandParent = parent->parent; return createIndex(grandParent->children.indexOf(parent), 0, parent); } Browser::TreeCategoryModel::~TreeCategoryModel() { delete m_data; } bool Browser::TreeCategoryModel::createData(DB::CategoryItem *parentCategoryItem, Data *parent) { const QString name = parentCategoryItem->mp_name; const int imageCount = m_images.contains(name) ? m_images[name].count : 0; const int videoCount = m_videos.contains(name) ? m_videos[name].count : 0; Data *myData = new Data(name); bool anyItems = imageCount != 0 || videoCount != 0; for (QList::ConstIterator subCategoryIt = parentCategoryItem->mp_subcategories.constBegin(); subCategoryIt != parentCategoryItem->mp_subcategories.constEnd(); ++subCategoryIt) { anyItems = createData(*subCategoryIt, myData) || anyItems; } if (parent) { if (anyItems) { parent->addChild(myData); } else { delete myData; } } else { m_data = myData; } return anyItems; } Browser::TreeCategoryModel::Data *Browser::TreeCategoryModel::indexToData(const QModelIndex &index) const { if (!index.isValid()) { return m_data; } else { return static_cast(index.internalPointer()); } } QString Browser::TreeCategoryModel::indexToName(const QModelIndex &index) const { const Browser::TreeCategoryModel::Data *data = indexToData(index); return data->name; } Qt::DropActions Browser::TreeCategoryModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } Qt::ItemFlags Browser::TreeCategoryModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); if (m_category->isSpecialCategory() || indexToName(index) == QString::fromUtf8("**NONE**")) { return defaultFlags; } if (indexToData(index)->parent != nullptr) { return defaultFlags | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; } else if (index.column() == -1) { return defaultFlags | Qt::ItemIsDropEnabled; } else { return defaultFlags | Qt::ItemIsDragEnabled; } } QStringList Browser::TreeCategoryModel::mimeTypes() const { return QStringList() << QString::fromUtf8("x-kphotoalbum/x-browser-tag-drag"); } QMimeData *Browser::TreeCategoryModel::mimeData(const QModelIndexList &indexes) const { QMimeData *mimeData = new QMimeData(); QByteArray encodedData; QDataStream stream(&encodedData, QIODevice::WriteOnly); // only use the first index, even if more than one are selected: stream << indexToName(indexes[0]); if (!indexes[0].parent().isValid()) { stream << QString(); } else { stream << indexToName(indexes[0].parent()); } mimeData->setData(QString::fromUtf8("x-kphotoalbum/x-browser-tag-drag"), encodedData); return mimeData; } Browser::TreeCategoryModel::tagData Browser::TreeCategoryModel::getDroppedTagData(QByteArray &encodedData) { QDataStream stream(&encodedData, QIODevice::ReadOnly); Browser::TreeCategoryModel::tagData droppedTagData; stream >> droppedTagData.tagName; stream >> droppedTagData.tagGroup; return droppedTagData; } bool Browser::TreeCategoryModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int, int, const QModelIndex &parent) { if (action == Qt::IgnoreAction) { return true; } const QString thisCategory = indexToName(parent); QByteArray encodedData = data->data(QString::fromUtf8("x-kphotoalbum/x-browser-tag-drag")); Browser::TreeCategoryModel::tagData droppedTagData = getDroppedTagData(encodedData); // the difference between a CopyAction and a MoveAction is that with the MoveAction, // we have to remove the tag from its current group first if (action == Qt::MoveAction) { // Remove the tag from its group and remove the group if it's empty now m_memberMap.removeMemberFromGroup(m_category->name(), droppedTagData.tagGroup, droppedTagData.tagName); if (m_memberMap.members(m_category->name(), droppedTagData.tagGroup, true) == QStringList()) { m_memberMap.deleteGroup(m_category->name(), droppedTagData.tagGroup); } } if (parent.isValid()) { // Check if the tag is dropped onto a copy of itself const DB::CategoryItemPtr categoryInfo = m_category->itemsCategories(); if (thisCategory == droppedTagData.tagName || categoryInfo->isDescendentOf(thisCategory, droppedTagData.tagName)) { return true; } // Add the tag to a group, create it if we don't have it yet if (!m_memberMap.groups(m_category->name()).contains(thisCategory)) { m_memberMap.addGroup(m_category->name(), thisCategory); DB::ImageDB::instance()->memberMap() = m_memberMap; } m_memberMap.addMemberToGroup(m_category->name(), thisCategory, droppedTagData.tagName); } DB::ImageDB::instance()->memberMap() = m_memberMap; MainWindow::DirtyIndicator::markDirty(); emit(dataChanged()); return true; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Browser/TreeCategoryModel.h b/Browser/TreeCategoryModel.h index 05989cd6..f73f4e11 100644 --- a/Browser/TreeCategoryModel.h +++ b/Browser/TreeCategoryModel.h @@ -1,107 +1,108 @@ /* Copyright (C) 2003-2015 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TREECATEGORYMODEL_H #define TREECATEGORYMODEL_H // Local includes #include "AbstractCategoryModel.h" -#include "DB/MemberMap.h" + +#include // Qt classes class QMimeData; namespace DB { class CategoryItem; } namespace Browser { /** * \brief A QAbstractItemModel subclass that represent the items of a given category as a tree * * See \ref Browser for a detailed description of how this fits in with the rest of the classes in this module * * This class implements the QAbstractItemModel interface, which is * actually what most of the methods is about. The constructor queries * the category information from the back end, and builds an internal * data structure representing the tree. It does build its own data structure for two reasons: * \li The \ref DB::CategoryItem's do not have an easy way to go from child * to parent, something that was needed by the \ref parent method. It was * considered too risky to add that to the \ref DB::CategoryItem * data structure at the time this was implemented. * \li By building its own data structure it can ensure that the data is * not changing behind the scene, something that might have happened if * this class was constructed, categories was added or removed, and the * class was asked information abouts its data. * * The drag and drop support is in some ways similar to the CategoryListView classes. * Any bugs there probably apply here as well and vice versa. */ class TreeCategoryModel : public AbstractCategoryModel { Q_OBJECT public: TreeCategoryModel(const DB::CategoryPtr &category, const DB::ImageSearchInfo &info); ~TreeCategoryModel() override; int rowCount(const QModelIndex &) const override; int columnCount(const QModelIndex &) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; QString indexToName(const QModelIndex &) const override; Qt::DropActions supportedDropActions() const override; Qt::ItemFlags flags(const QModelIndex &index) const override; QStringList mimeTypes() const override; QMimeData *mimeData(const QModelIndexList &indexes) const override; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; struct tagData { QString tagName; QString tagGroup; }; signals: void dataChanged(); private: // Functions struct Data; bool createData(DB::CategoryItem *parentCategoryItem, Data *parent); Data *indexToData(const QModelIndex &index) const; TreeCategoryModel::tagData getDroppedTagData(QByteArray &encodedData); private: // Variables Data *m_data; DB::MemberMap m_memberMap; }; } #endif // TREECATEGORYMODEL_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/CategoryListView/CheckDropItem.cpp b/CategoryListView/CheckDropItem.cpp index a7678aea..c694f0d1 100644 --- a/CategoryListView/CheckDropItem.cpp +++ b/CategoryListView/CheckDropItem.cpp @@ -1,167 +1,170 @@ /* Copyright (C) 2003-2015 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "CheckDropItem.h" -#include "DB/Category.h" -#include "DB/CategoryItem.h" + #include "DragItemInfo.h" #include "DragableTreeWidget.h" + +#include +#include #include #include + #include #include #include CategoryListView::CheckDropItem::CheckDropItem(DragableTreeWidget *parent, const QString &column1, const QString &column2) : QTreeWidgetItem(parent) , m_listView(parent) { setCheckState(0, Qt::Unchecked); setText(0, column1); setText(1, column2); } CategoryListView::CheckDropItem::CheckDropItem(DragableTreeWidget *listView, QTreeWidgetItem *parent, const QString &column1, const QString &column2) : QTreeWidgetItem(parent) , m_listView(listView) { setCheckState(0, Qt::Unchecked); setText(0, column1); setText(1, column2); } CategoryListView::DragItemInfoSet CategoryListView::CheckDropItem::extractData(const QMimeData *data) const { DragItemInfoSet items; QByteArray array = data->data(QString::fromUtf8("x-kphotoalbum/x-categorydrag")); QDataStream stream(array); stream >> items; return items; } bool CategoryListView::CheckDropItem::dataDropped(const QMimeData *data) { DragItemInfoSet items = extractData(data); const QString newParent = text(0); if (!verifyDropWasIntended(newParent, items)) return false; DB::MemberMap &memberMap = DB::ImageDB::instance()->memberMap(); memberMap.addGroup(m_listView->category()->name(), newParent); for (DragItemInfoSet::const_iterator itemIt = items.begin(); itemIt != items.end(); ++itemIt) { const QString oldParent = (*itemIt).parent(); const QString child = (*itemIt).child(); memberMap.addMemberToGroup(m_listView->category()->name(), newParent, child); memberMap.removeMemberFromGroup(m_listView->category()->name(), oldParent, child); } //DB::ImageDB::instance()->setMemberMap( memberMap ); m_listView->emitItemsChanged(); return true; } bool CategoryListView::CheckDropItem::isSelfDrop(const QMimeData *data) const { const QString thisCategory = text(0); const DragItemInfoSet children = extractData(data); const DB::CategoryItemPtr categoryInfo = m_listView->category()->itemsCategories(); for (DragItemInfoSet::const_iterator childIt = children.begin(); childIt != children.end(); ++childIt) { if (thisCategory == (*childIt).child() || categoryInfo->isDescendentOf(thisCategory, (*childIt).child())) return true; } return false; } void CategoryListView::CheckDropItem::setTristate(bool b) { if (b) setFlags(flags() | Qt::ItemIsTristate); else setFlags(flags() & ~Qt::ItemIsTristate); } bool CategoryListView::CheckDropItem::verifyDropWasIntended(const QString &parent, const DragItemInfoSet &items) { QStringList children; for (DragItemInfoSet::const_iterator itemIt = items.begin(); itemIt != items.end(); ++itemIt) { children.append((*itemIt).child()); } QString allChildren; if (children.size() == 1) { allChildren = children[0]; } else if (children.size() == 2) { allChildren = i18n("\"%1\" and \"%2\"", children[0], children[1]); } else { for (int i = 0; i < children.size() - 1; i++) { if (i == 0) { allChildren += i18n("\"%1\"", children[i]); } else { allChildren += i18n(", \"%1\"", children[i]); } } allChildren += i18n(" and \"%1\"", children[children.size() - 1]); } const QString msg = i18np( "

" "You have just dragged an item onto another. This will make the target item a tag group " "and define the dragged item as a member of this group. " "Tag groups may be used to denote facts such as 'Las Vegas is in the USA'. In that example " "you would drag Las Vegas onto USA. " "When you have set up tag groups, you can, for instance, see all images from the USA by " "simply selecting that item in the Browser." "

" "

" "Was it really your intention to make \"%3\" a tag group and add \"%2\" as a member?" "

", "

" "You have just dragged some items onto one other item. This will make the target item a " "tag group and define the dragged items as members of this group. " "Tag groups may be used to denote facts such as 'Las Vegas and New York are in the USA'. " "In that example you would drag Las Vegas and New York onto USA. " "When you have set up tag groups, you can, for instance, see all images from the USA by " "simply selecting that item in the Browser." "

" "

" "Was it really your intention to make \"%3\" a tag group and add %2 as members?" "

", children.size(), allChildren, parent); const int answer = KMessageBox::warningContinueCancel(nullptr, msg, i18n("Move Items"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString::fromLatin1("DoYouReallyWantToMessWithMemberGroups")); return answer == KMessageBox::Continue; } void CategoryListView::CheckDropItem::setDNDEnabled(const bool b) { if (b) setFlags(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | flags()); else setFlags(flags() & ~Qt::ItemIsDragEnabled & ~Qt::ItemIsDropEnabled); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/CategoryListView/CheckDropItem.h b/CategoryListView/CheckDropItem.h index 754974db..679e8a8c 100644 --- a/CategoryListView/CheckDropItem.h +++ b/CategoryListView/CheckDropItem.h @@ -1,56 +1,57 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CATEGORYLISTVIEW_CHECKDROPITEM_H #define CATEGORYLISTVIEW_CHECKDROPITEM_H #include "DragItemInfo.h" + #include #include class QDropEvent; namespace CategoryListView { class DragableTreeWidget; /* * Implementation detail: * The drag and drop support here is partly similar to Browser::TreeCategoryModel. * Any bugs there probably apply here as well and vice versa. */ class CheckDropItem : public QTreeWidgetItem { public: CheckDropItem(DragableTreeWidget *listview, const QString &column1, const QString &column2); CheckDropItem(DragableTreeWidget *listview, QTreeWidgetItem *parent, const QString &column1, const QString &column2); void setDNDEnabled(bool); bool dataDropped(const QMimeData *data); bool isSelfDrop(const QMimeData *data) const; void setTristate(bool b); protected: bool verifyDropWasIntended(const QString &parent, const DragItemInfoSet &children); DragItemInfoSet extractData(const QMimeData *data) const; private: DragableTreeWidget *m_listView; }; } #endif /* CATEGORYLISTVIEW_CHECKDROPITEM_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/CategoryListView/DragableTreeWidget.cpp b/CategoryListView/DragableTreeWidget.cpp index a4ae590d..b45a4871 100644 --- a/CategoryListView/DragableTreeWidget.cpp +++ b/CategoryListView/DragableTreeWidget.cpp @@ -1,93 +1,96 @@ /* 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 "CheckDropItem.h" -#include "DB/Category.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(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/CategoryListView/DragableTreeWidget.h b/CategoryListView/DragableTreeWidget.h index 28d38fbf..ba424218 100644 --- a/CategoryListView/DragableTreeWidget.h +++ b/CategoryListView/DragableTreeWidget.h @@ -1,50 +1,51 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CATEGORYLISTVIEW_DragableTreeWidget_H #define CATEGORYLISTVIEW_DragableTreeWidget_H -#include "DB/CategoryPtr.h" +#include + #include namespace CategoryListView { class DragableTreeWidget : public QTreeWidget { Q_OBJECT public: DragableTreeWidget(const DB::CategoryPtr &category, QWidget *parent); DB::CategoryPtr category() const; void emitItemsChanged(); protected: QMimeData *mimeData(const QList items) const override; QStringList mimeTypes() const override; bool dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action) override; void dragMoveEvent(QDragMoveEvent *event) override; signals: void itemsChanged(); private: const DB::CategoryPtr m_category; }; } #endif /* CATEGORYLISTVIEW_DragableTreeWidget_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/AndCategoryMatcher.cpp b/DB/AndCategoryMatcher.cpp index b88dfb48..a7941487 100644 --- a/DB/AndCategoryMatcher.cpp +++ b/DB/AndCategoryMatcher.cpp @@ -1,37 +1,38 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "AndCategoryMatcher.h" + #include "ImageInfo.h" #include "Logging.h" bool DB::AndCategoryMatcher::eval(ImageInfoPtr info, QMap &alreadyMatched) { Q_FOREACH (CategoryMatcher *subMatcher, mp_elements) { if (!subMatcher->eval(info, alreadyMatched)) return false; } return true; } void DB::AndCategoryMatcher::debug(int level) const { qCDebug(DBCategoryMatcherLog, "%sAND:", qPrintable(spaces(level))); ContainerCategoryMatcher::debug(level + 1); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/Category.cpp b/DB/Category.cpp index 28616e5d..aeb436bd 100644 --- a/DB/Category.cpp +++ b/DB/Category.cpp @@ -1,195 +1,198 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "CategoryItem.h" -#include "DB/ImageDB.h" -#include "DB/MemberMap.h" -#include +#include "ImageDB.h" +#include "MemberMap.h" +#include "UIDelegate.h" + +#include +#include + #include #include #include #include #include #include -#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, QSize(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) { DB::ImageDB::instance()->uiDelegate().error( QString::fromLatin1("Unable to create CategoryImages directory!"), 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) { DB::ImageDB::instance()->uiDelegate().error( QString::fromLatin1("Unable to save category image '%1'!").arg(fileName), 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; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/Category.h b/DB/Category.h index 9365d58b..6282ace1 100644 --- a/DB/Category.h +++ b/DB/Category.h @@ -1,114 +1,113 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CATEGORY_H #define CATEGORY_H #include "ImageDate.h" +#include #include #include #include #include -#include - class QImage; class QPixmap; namespace DB { class CategoryItem; struct CountWithRange { uint count = 0; ImageDate range {}; void add(const ImageDate &date) { count++; range.extendTo(date); } }; /** This class stores information about categories (People/Places/Events) */ class Category : public QObject, public QSharedData { Q_OBJECT public: enum ViewType { TreeView, ThumbedTreeView, IconView, ThumbedIconView }; enum CategoryType { PlainCategory, FolderCategory, MediaTypeCategory, TokensCategory }; virtual QString name() const = 0; virtual void setName(const QString &name) = 0; virtual void setPositionable(bool) = 0; virtual bool positionable() const = 0; virtual QString iconName() const = 0; virtual void setIconName(const QString &name) = 0; virtual QPixmap icon(int size = 22, KIconLoader::States state = KIconLoader::DefaultState) const; virtual void setViewType(ViewType type) = 0; virtual ViewType viewType() const = 0; virtual void setThumbnailSize(int) = 0; virtual int thumbnailSize() const = 0; virtual void setDoShow(bool b) = 0; virtual bool doShow() const = 0; virtual void setType(CategoryType t) = 0; virtual CategoryType type() const = 0; virtual bool isSpecialCategory() const = 0; virtual void addOrReorderItems(const QStringList &items) = 0; virtual void setItems(const QStringList &items) = 0; virtual void removeItem(const QString &item) = 0; virtual void renameItem(const QString &oldValue, const QString &newValue) = 0; virtual void addItem(const QString &item) = 0; virtual QStringList items() const = 0; virtual QStringList itemsInclCategories() const; QExplicitlySharedDataPointer itemsCategories() const; QPixmap categoryImage(const QString &category, QString, int width, int height) const; void setCategoryImage(const QString &category, QString, const QImage &image); QString fileForCategoryImage(const QString &category, QString member) const; virtual void setBirthDate(const QString &item, const QDate &birthDate) = 0; virtual QDate birthDate(const QString &item) const = 0; private: QString defaultIconName() const; signals: void changed(); void itemRenamed(const QString &oldName, const QString &newName); void itemRemoved(const QString &name); }; } #endif /* CATEGORY_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/CategoryCollection.h b/DB/CategoryCollection.h index c1ae7eec..6f907026 100644 --- a/DB/CategoryCollection.h +++ b/DB/CategoryCollection.h @@ -1,64 +1,65 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CATEGORYCOLLECTION_H #define CATEGORYCOLLECTION_H -#include "DB/Category.h" -#include "DB/CategoryPtr.h" +#include "Category.h" +#include "CategoryPtr.h" + #include namespace DB { /** \class CategoryCollection This class is the collection of categories. It is the basic anchor point to categories. */ class CategoryCollection : public QObject { Q_OBJECT public: virtual CategoryPtr categoryForName(const QString &name) const = 0; virtual QStringList categoryNames() const = 0; virtual QStringList categoryTexts() const = 0; virtual void removeCategory(const QString &name) = 0; virtual void rename(const QString &oldName, const QString &newName) = 0; virtual QList categories() const = 0; virtual void addCategory(const QString &text, const QString &icon, Category::ViewType type, int thumbnailSize, bool show, bool positionable = false) = 0; virtual CategoryPtr categoryForSpecial(const Category::CategoryType type) const = 0; signals: void categoryCollectionChanged(); void categoryRemoved(const QString &categoryName); void itemRenamed(DB::Category *category, const QString &oldName, const QString &newName); void itemRemoved(DB::Category *category, const QString &name); protected slots: void itemRenamed(const QString &oldName, const QString &newName); void itemRemoved(const QString &item); }; } #endif /* CATEGORYCOLLECTION_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/CategoryMatcher.h b/DB/CategoryMatcher.h index 22ba6557..b98ad7a6 100644 --- a/DB/CategoryMatcher.h +++ b/DB/CategoryMatcher.h @@ -1,65 +1,66 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CATEGORYMATCHER_H #define CATEGORYMATCHER_H -#include +#include "ImageInfoPtr.h" + #include #include namespace DB { class ImageInfo; using Utilities::StringSet; /** \brief Base class for components of the image searching frame work. The matcher component must implement \ref eval which tells if the given image is matched by this component. If the over all search contains a "No other" part (as in Jesper and no other people", then we need to collect items of the category items seen. This, however, is rather expensive, so this collection is only turned on in that case. */ class CategoryMatcher { public: CategoryMatcher(); virtual ~CategoryMatcher() {} virtual void debug(int level) const = 0; virtual bool eval(ImageInfoPtr, QMap &alreadyMatched) = 0; virtual void setShouldCreateMatchedSet(bool); protected: QString spaces(int level) const; bool m_shouldPrepareMatchedSet; }; } #endif /* CATEGORYMATCHER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ExactCategoryMatcher.cpp b/DB/ExactCategoryMatcher.cpp index 0de600b2..5cd17246 100644 --- a/DB/ExactCategoryMatcher.cpp +++ b/DB/ExactCategoryMatcher.cpp @@ -1,82 +1,83 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ExactCategoryMatcher.h" + #include "ImageInfo.h" #include "Logging.h" DB::ExactCategoryMatcher::ExactCategoryMatcher(const QString category) : m_category(category) , m_matcher(nullptr) { } DB::ExactCategoryMatcher::~ExactCategoryMatcher() { if (m_matcher) { delete m_matcher; m_matcher = nullptr; } } void DB::ExactCategoryMatcher::setMatcher(CategoryMatcher *subMatcher) { m_matcher = subMatcher; if (m_matcher) // always collect matched tags of _matcher: m_matcher->setShouldCreateMatchedSet(true); } bool DB::ExactCategoryMatcher::eval(ImageInfoPtr info, QMap &alreadyMatched) { // it makes no sense to put one ExactCategoryMatcher into another, so we ignore alreadyMatched. Q_UNUSED(alreadyMatched); if (!m_matcher) return false; QMap matchedTags; // first, do a regular match and collect all matched Tags. if (!m_matcher->eval(info, matchedTags)) return false; // if the match succeeded, check if it is exact: for (const QString &item : info->itemsOfCategory(m_category)) if (!matchedTags[m_category].contains(item)) return false; // tag was not contained in matcher return true; } void DB::ExactCategoryMatcher::debug(int level) const { qCDebug(DBCategoryMatcherLog, "%sEXACT:", qPrintable(spaces(level))); m_matcher->debug(level + 1); } void DB::ExactCategoryMatcher::setShouldCreateMatchedSet(bool) { // no-op: // shouldCreateMatchedSet is already set to true for _matcher; // setting this to false would disable the ExactCategoryMatcher, so it is ignored. // only ExactCategoryMatcher ever calls setShouldCreateMatchedSet. // ExactCategoryMatcher are never stacked, so this can't be called. Q_ASSERT(false); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FastDir.cpp b/DB/FastDir.cpp index 4cbe7355..2aaa5c9d 100644 --- a/DB/FastDir.cpp +++ b/DB/FastDir.cpp @@ -1,192 +1,193 @@ /* Copyright (C) 2010-2018 Jesper Pedersen and Robert Krawitz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "FastDir.h" + #include "Logging.h" #include #include #include extern "C" { #include #include #include /* * Ideally the order of entries returned by readdir() should be close * to optimal; intuitively, it should reflect the order in which inodes * are returned from getdents() or equivalent, which should be the order * in which they're stored on the disk. Experimentally, that isn't always * true. One test, involving 10839 files totaling 90 GB resulted in * readdir() returning files in random order, where "find" returned them * sorted (and the same version of find does *not* in general return files * in alphabetical order). * * By repeated measurement, loading the files in the order returned by * readdir took about 16:30, where loading them in alphanumeric sorted order * took about 15:00. Running a similar test outside of kpa (using the order * returned by readdir() vs. sorted to cat the files through dd and measuring * the time) yielded if anything an even greater discrepancy (17:35 vs. 14:10). * * This issue is filesystem dependent, but is known to affect the extN * filesystems commonly used on Linux that use a hashed tree structure to * store directories. See e. g. * http://home.ifi.uio.no/paalh/publications/files/ipccc09.pdf and its * accompanying presentation * http://www.linux-kongress.org/2009/slides/linux_disk_io_performance_havard_espeland.pdf * * We could do even better by sorting by block position, but that would * greatly increase complexity. */ #ifdef __linux__ #include #include #define HAVE_STATFS #define STATFS_FSTYPE_EXT2 EXT2_SUPER_MAGIC // Includes EXT3_SUPER_MAGIC, EXT4_SUPER_MAGIC #else #ifdef __FreeBSD__ #include #include #include #define HAVE_STATFS #define STATFS_FSTYPE_EXT2 FS_EXT2FS #endif // other platforms fall back to known-safe (but slower) implementation #endif // __linux__ } typedef QMap InodeMap; typedef QSet StringSet; DB::FastDir::FastDir(const QString &path) : m_path(path) { InodeMap tmpAnswer; DIR *dir; dirent *file; QByteArray bPath(QFile::encodeName(path)); dir = opendir(bPath.constData()); if (!dir) return; const bool doSortByInode = sortByInode(bPath); const bool doSortByName = sortByName(bPath); #if defined(QT_THREAD_SUPPORT) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(Q_OS_CYGWIN) // ZaJ (2016-03-23): while porting to Qt5/KF5, this code-path is disabled on my system // I don't want to touch this right now since I can't verify correctness in any way. // rlk 2018-05-20: readdir_r is deprecated as of glibc 2.24; see // http://man7.org/linux/man-pages/man3/readdir_r.3.html. // There are problems with MAXNAMLEN/NAME_MAX and friends, that // can differ from filesystem to filesystem. It's also expected // that POSIX will (if it hasn't already) deprecate readdir_r // and require readdir to be thread safe. union dirent_buf { struct KDE_struct_dirent mt_file; char b[sizeof(struct dirent) + MAXNAMLEN + 1]; } *u = new union dirent_buf; while (readdir_r(dir, &(u->mt_file), &file) == 0 && file) #else // FIXME: use 64bit versions of readdir and dirent? while ((file = readdir(dir))) #endif // QT_THREAD_SUPPORT && _POSIX_THREAD_SAFE_FUNCTIONS { if (doSortByInode) tmpAnswer.insert(file->d_ino, QFile::decodeName(file->d_name)); else m_sortedList.append(QFile::decodeName(file->d_name)); } #if defined(QT_THREAD_SUPPORT) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) && !defined(Q_OS_CYGWIN) delete u; #endif (void)closedir(dir); if (doSortByInode) { for (InodeMap::iterator it = tmpAnswer.begin(); it != tmpAnswer.end(); ++it) { m_sortedList << it.value(); } } else if (doSortByName) { m_sortedList.sort(); } } // No currently known filesystems where sort by name is optimal constexpr bool DB::sortByName(const QByteArray &) { return false; } bool DB::sortByInode(const QByteArray &path) { #ifdef HAVE_STATFS struct statfs buf; if (statfs(path.constData(), &buf) == -1) return -1; // Add other filesystems as appropriate switch (buf.f_type) { case STATFS_FSTYPE_EXT2: return true; default: return false; } #else // HAVE_STATFS Q_UNUSED(path); return false; #endif // HAVE_STATFS } const QStringList DB::FastDir::entryList() const { return m_sortedList; } QStringList DB::FastDir::sortFileList(const StringSet &files) const { QStringList answer; StringSet tmp(files); for (const QString &fileName : m_sortedList) { if (tmp.contains(fileName)) { answer << fileName; tmp.remove(fileName); } else if (tmp.contains(m_path + fileName)) { answer << m_path + fileName; tmp.remove(m_path + fileName); } } if (tmp.count() > 0) { qCDebug(FastDirLog) << "Files left over after sorting on " << m_path; for (const QString &fileName : tmp) { qCDebug(FastDirLog) << fileName; answer << fileName; } } return answer; } QStringList DB::FastDir::sortFileList(const QStringList &files) const { StringSet tmp; for (const QString &fileName : files) { tmp << fileName; } return sortFileList(tmp); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FastDir.h b/DB/FastDir.h index 2dbb0037..78b741aa 100644 --- a/DB/FastDir.h +++ b/DB/FastDir.h @@ -1,66 +1,67 @@ /* Copyright (C) 2010-2018 Jesper Pedersen and Robert Krawitz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef FASTDIR_H #define FASTDIR_H -#include +#include "FileNameList.h" + #include #include #include namespace DB { /** FastDir is used in place of QDir because QDir stat()s every file in the directory, even if we tell it not to restrict anything. When scanning for new images, we don't want to look at files we already have in our database, and we also don't want to look at files whose names indicate that we don't care about them. So what we do is simply read the names from the directory and let the higher layers decide what to do with them. On my sample database with ~20,000 images, this improves the time to rescan for images on a cold system from about 100 seconds to about 3 seconds. -- Robert Krawitz, rlk@alum.mit.edu 2007-07-22 */ typedef QSet StringSet; class FastDir { public: explicit FastDir(const QString &path); const QStringList entryList() const; QStringList sortFileList(const QStringList &files) const; QStringList sortFileList(const StringSet &files) const; private: FastDir(); const QString m_path; QStringList m_sortedList; }; bool sortByInode(const QByteArray &path); constexpr bool sortByName(const QByteArray &path); } #endif /* FASTDIR_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileInfo.cpp b/DB/FileInfo.cpp index ee31b6be..56eda125 100644 --- a/DB/FileInfo.cpp +++ b/DB/FileInfo.cpp @@ -1,135 +1,135 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "FileInfo.h" #include -#include #include #include #include #include +#include using namespace DB; FileInfo FileInfo::read(const DB::FileName &fileName, DB::ExifMode mode) { return FileInfo(fileName, mode); } DB::FileInfo::FileInfo(const DB::FileName &fileName, DB::ExifMode mode) : m_angle(0) , m_fileName(fileName) { parseEXIV2(fileName); if (updateDataFromFileTimeStamp(fileName, mode)) m_date = QFileInfo(fileName.absolute()).lastModified(); } Exiv2::ExifData &DB::FileInfo::getExifData() { return m_exifMap; } const DB::FileName &DB::FileInfo::getFileName() const { return m_fileName; } bool DB::FileInfo::updateDataFromFileTimeStamp(const DB::FileName &fileName, DB::ExifMode mode) { // If the date is valid from Exif reading, then we should not use the time stamp from the file. if (m_date.isValid()) return false; // If we are not setting date, then we should of course not set the date if ((mode & EXIFMODE_DATE) == 0) return false; // If we are we already have specifies that we want to sent the date (from the ReReadExif dialog), then we of course should. if ((mode & EXIFMODE_USE_IMAGE_DATE_IF_INVALID_EXIF_DATE) != 0) return true; // Always trust for videos (this is a way to say that we should not trust for scaned in images - which makes no sense for videos) if (Utilities::isVideo(fileName)) return true; // Finally use the info from the settings dialog return Settings::SettingsData::instance()->trustTimeStamps(); } void DB::FileInfo::parseEXIV2(const DB::FileName &fileName) { m_exifMap = Exif::Info::instance()->metadata(fileName).exif; // Date m_date = fetchEXIV2Date(m_exifMap, "Exif.Photo.DateTimeOriginal"); if (!m_date.isValid()) { m_date = fetchEXIV2Date(m_exifMap, "Exif.Photo.DateTimeDigitized"); if (!m_date.isValid()) m_date = fetchEXIV2Date(m_exifMap, "Exif.Image.DateTime"); } // Angle if (m_exifMap.findKey(Exiv2::ExifKey("Exif.Image.Orientation")) != m_exifMap.end()) { const Exiv2::Exifdatum &datum = m_exifMap["Exif.Image.Orientation"]; int orientation = 0; if (datum.count() > 0) orientation = datum.toLong(); m_angle = orientationToAngle(orientation); } // Description if (m_exifMap.findKey(Exiv2::ExifKey("Exif.Image.ImageDescription")) != m_exifMap.end()) { const Exiv2::Exifdatum &datum = m_exifMap["Exif.Image.ImageDescription"]; m_description = QString::fromLocal8Bit(datum.toString().c_str()).trimmed(); // some cameras seem to add control characters. Remove them: m_description.remove(QRegularExpression(QString::fromLatin1("\\p{Cc}"))); } } QDateTime FileInfo::fetchEXIV2Date(Exiv2::ExifData &map, const char *key) { try { if (map.findKey(Exiv2::ExifKey(key)) != map.end()) { const Exiv2::Exifdatum &datum = map[key]; return QDateTime::fromString(QString::fromLatin1(datum.toString().c_str()), Qt::ISODate); } } catch (...) { } return QDateTime(); } int DB::FileInfo::orientationToAngle(int orientation) { if (orientation == 1 || orientation == 2) return 0; else if (orientation == 3 || orientation == 4) return 180; else if (orientation == 5 || orientation == 8) return 270; else if (orientation == 6 || orientation == 7) return 90; return 0; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileInfo.h b/DB/FileInfo.h index c5e895f7..2548ed08 100644 --- a/DB/FileInfo.h +++ b/DB/FileInfo.h @@ -1,62 +1,63 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef FILEINFO_H #define FILEINFO_H -#include "Exif/Info.h" +#include "ExifMode.h" + +#include + #include #include -#include "ExifMode.h" - namespace DB { class FileName; class FileInfo { public: static FileInfo read(const DB::FileName &fileName, DB::ExifMode mode); QDateTime dateTime() { return m_date; } int angle() { return m_angle; } QString description() { return m_description; } Exiv2::ExifData &getExifData(); const DB::FileName &getFileName() const; protected: void parseEXIV2(const DB::FileName &fileName); QDateTime fetchEXIV2Date(Exiv2::ExifData &map, const char *key); int orientationToAngle(int orientation); private: FileInfo(const DB::FileName &fileName, DB::ExifMode mode); bool updateDataFromFileTimeStamp(const DB::FileName &fileName, DB::ExifMode mode); QDateTime m_date; int m_angle; QString m_description; Exiv2::ExifData m_exifMap; const DB::FileName &m_fileName; }; } #endif /* FILEINFO_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileName.cpp b/DB/FileName.cpp index e50fd779..563f3004 100644 --- a/DB/FileName.cpp +++ b/DB/FileName.cpp @@ -1,108 +1,109 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "FileName.h" + #include "ImageDB.h" +#include "ImageInfoList.h" -#include #include #include #include DB::FileName::FileName() : m_isNull(true) { } DB::FileName DB::FileName::fromAbsolutePath(const QString &fileName) { const QString imageRoot = Utilities::stripEndingForwardSlash(Settings::SettingsData::instance()->imageDirectory()) + QLatin1String("/"); if (!fileName.startsWith(imageRoot)) return FileName(); FileName res; res.m_isNull = false; res.m_absoluteFilePath = fileName; res.m_relativePath = fileName.mid(imageRoot.length()); return res; } DB::FileName DB::FileName::fromRelativePath(const QString &fileName) { Q_ASSERT(!fileName.startsWith(QChar::fromLatin1('/'))); FileName res; res.m_isNull = false; res.m_relativePath = fileName; res.m_absoluteFilePath = Utilities::stripEndingForwardSlash(Settings::SettingsData::instance()->imageDirectory()) + QLatin1String("/") + fileName; return res; } QString DB::FileName::absolute() const { Q_ASSERT(!isNull()); return m_absoluteFilePath; } QString DB::FileName::relative() const { Q_ASSERT(!m_isNull); return m_relativePath; } bool DB::FileName::isNull() const { return m_isNull; } bool DB::FileName::operator==(const DB::FileName &other) const { return m_isNull == other.m_isNull && m_relativePath == other.m_relativePath; } bool DB::FileName::operator!=(const DB::FileName &other) const { return !(*this == other); } bool DB::FileName::operator<(const DB::FileName &other) const { return relative() < other.relative(); } bool DB::FileName::exists() const { return QFile::exists(absolute()); } DB::ImageInfoPtr DB::FileName::info() const { return ImageDB::instance()->info(*this); } DB::FileName::operator QUrl() const { return QUrl::fromLocalFile(absolute()); } uint DB::qHash(const DB::FileName &fileName) { return qHash(fileName.relative()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileName.h b/DB/FileName.h index 2248c865..44c72469 100644 --- a/DB/FileName.h +++ b/DB/FileName.h @@ -1,67 +1,68 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef FILENAME_H #define FILENAME_H #include "ImageInfoPtr.h" + #include #include #include #include namespace DB { class FileName { public: FileName(); static FileName fromAbsolutePath(const QString &fileName); static FileName fromRelativePath(const QString &fileName); QString absolute() const; QString relative() const; bool isNull() const; bool operator==(const FileName &other) const; bool operator!=(const FileName &other) const; bool operator<(const FileName &other) const; bool exists() const; ImageInfoPtr info() const; /** * @brief Conversion to absolute local file url. */ explicit operator QUrl() const; private: // During previous profilation it showed that converting between absolute and relative took quite some time, // so to avoid that, I store both. QString m_relativePath; QString m_absoluteFilePath; bool m_isNull; }; uint qHash(const DB::FileName &fileName); typedef QSet FileNameSet; } Q_DECLARE_METATYPE(DB::FileName) #endif // FILENAME_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/FileNameList.h b/DB/FileNameList.h index 65b6b550..933b0413 100644 --- a/DB/FileNameList.h +++ b/DB/FileNameList.h @@ -1,49 +1,50 @@ /* Copyright 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef FILENAMELIST_H #define FILENAMELIST_H #include "FileName.h" #include "ImageInfo.h" + #include #include namespace DB { class FileNameList : public QList { public: FileNameList() {} FileNameList(const QList &); /** * @brief Create a FileNameList from a list of absolute filenames. * @param files */ explicit FileNameList(const QStringList &files); QStringList toStringList(DB::PathType) const; FileNameList &operator<<(const DB::FileName &); FileNameList reversed() const; }; } #endif // FILENAMELIST_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/GroupCounter.cpp b/DB/GroupCounter.cpp index 620e113d..53c0d0ff 100644 --- a/DB/GroupCounter.cpp +++ b/DB/GroupCounter.cpp @@ -1,112 +1,114 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "GroupCounter.h" -#include "DB/ImageDB.h" -#include "DB/MemberMap.h" -#include "Utilities/StringSet.h" + +#include "ImageDB.h" +#include "MemberMap.h" + +#include using namespace DB; /** * \class DB::GroupCounter * \brief Utility class to help counting matches for member groups. * * This class is used to count the member group matches when * categorizing. The class is instantiating with the category we currently * are counting items for. * * The class builds the inverse member map, that is a map pointing from items * to parent. * * As an example, imagine we have the following member map (stored in the * variable groupToMemberMap in the code): * \code * { USA |-> [Chicago, Santa Clara], * California |-> [Santa Clara, Los Angeles] } * \endcode * * The inverse map (stored in m_memberToGroup in the code ) will then look * like this: * \code * { Chicago |-> [USA], * Sanata Clara |-> [ USA, California ], * Los Angeless |-> [ California ] } * \endcode */ GroupCounter::GroupCounter(const QString &category) { const MemberMap map = DB::ImageDB::instance()->memberMap(); QMap groupToMemberMap = map.groupMap(category); m_memberToGroup.reserve(2729 /* A large prime */); m_groupCount.reserve(2729 /* A large prime */); // Populate the m_memberToGroup map for (QMap::Iterator groupToMemberIt = groupToMemberMap.begin(); groupToMemberIt != groupToMemberMap.end(); ++groupToMemberIt) { StringSet members = groupToMemberIt.value(); QString group = groupToMemberIt.key(); Q_FOREACH (const auto &member, members) { m_memberToGroup[member].append(group); } m_groupCount.insert(group, CountWithRange()); } } /** * categories is the selected categories for one image, members may be Las Vegas, Chicago, and Los Angeles if the * category in question is Places. * This function then increases m_groupCount with 1 for each of the groups the relavant items belongs to * Las Vegas might increase the m_groupCount[Nevada] by one. * The tricky part is to avoid increasing it by more than 1 per image, that is what the countedGroupDict is * used for. */ void GroupCounter::count(const StringSet &categories, const ImageDate &date) { static StringSet countedGroupDict; countedGroupDict.clear(); for (StringSet::const_iterator categoryIt = categories.begin(); categoryIt != categories.end(); ++categoryIt) { if (m_memberToGroup.contains(*categoryIt)) { const QStringList groups = m_memberToGroup[*categoryIt]; for (const QString &group : groups) { if (!countedGroupDict.contains(group)) { countedGroupDict.insert(group); m_groupCount[group].add(date); } } } // The item Nevada should itself go into the group Nevada. if (!countedGroupDict.contains(*categoryIt) && m_groupCount.contains(*categoryIt)) { countedGroupDict.insert(*categoryIt); m_groupCount[*categoryIt].add(date); } } } QMap GroupCounter::result() { QMap res; for (QHash::const_iterator it = m_groupCount.constBegin(); it != m_groupCount.constEnd(); ++it) { if (it.value().count != 0) res.insert(it.key(), it.value()); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/GroupCounter.h b/DB/GroupCounter.h index 8c658426..f996700c 100644 --- a/DB/GroupCounter.h +++ b/DB/GroupCounter.h @@ -1,45 +1,47 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef GROUPCOUNTER_H #define GROUPCOUNTER_H #include "Category.h" -#include "Settings/SettingsData.h" + +#include + #include namespace DB { using Utilities::StringSet; class GroupCounter { public: explicit GroupCounter(const QString &category); void count(const StringSet &, const ImageDate &date); QMap result(); private: QHash m_memberToGroup; QHash m_groupCount; }; } #endif /* GROUPCOUNTER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageDB.cpp b/DB/ImageDB.cpp index ee54c3f6..2ec8ffa2 100644 --- a/DB/ImageDB.cpp +++ b/DB/ImageDB.cpp @@ -1,198 +1,201 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "Browser/BrowserWidget.h" -#include "DB/CategoryCollection.h" + +#include "CategoryCollection.h" +#include "FileName.h" +#include "MediaCount.h" #include "NewImageFinder.h" #include "UIDelegate.h" -#include "XMLDB/Database.h" -#include -#include + +#include +#include + #include #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, UIDelegate &delegate) { if (s_instance) qFatal("ImageDB::setupXMLDB: Setup must be called only once."); s_instance = new XMLDB::Database(configFile, delegate); 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); } UIDelegate &DB::ImageDB::uiDelegate() const { return m_UI; } ImageDB::ImageDB(UIDelegate &delegate) : m_UI(delegate) { } 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. */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageDB.h b/DB/ImageDB.h index 397b3703..af1e053d 100644 --- a/DB/ImageDB.h +++ b/DB/ImageDB.h @@ -1,200 +1,200 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef IMAGEDB_H #define IMAGEDB_H -#include +#include "Category.h" +#include "FileNameList.h" +#include "ImageDateCollection.h" +#include "ImageInfoList.h" +#include "ImageInfoPtr.h" +#include "MediaCount.h" -#include -#include -#include -#include -#include -#include +#include class QProgressBar; namespace DB { class CategoryCollection; class Category; class MD5Map; class MemberMap; class ImageSearchInfo; class FileName; class UIDelegate; /** * @brief The ClassificationMode enum can be used to short-circuit classification in the classify() method. * This allows you to only check whether a given category has more than one sub-category (including the "No other" category). * In other words, you can use a partial count when all you want to know is whether further search refinement is possible * in a category. * @see ImageDB::classify() * @see Browser::OverviewPage::updateImageCount() */ enum class ClassificationMode { FullCount ///< @brief run a full classification. This is normally what you want. , PartialCount ///< @brief Count until at least 2 categories are found }; class ImageDB : public QObject { Q_OBJECT public: static ImageDB *instance(); static void setupXMLDB(const QString &configFile, UIDelegate &delegate); static void deleteInstance(); DB::FileNameSet imagesWithMD5Changed(); UIDelegate &uiDelegate() const; public slots: void setDateRange(const ImageDate &, bool includeFuzzyCounts); void clearDateRange(); virtual void slotRescan(); void slotRecalcCheckSums(const DB::FileNameList &selection); virtual MediaCount count(const ImageSearchInfo &info); virtual void slotReread(const DB::FileNameList &list, DB::ExifMode mode); protected: ImageDate m_selectionRange; bool m_includeFuzzyCounts; ImageInfoList m_clipboard; UIDelegate &m_UI; private: static void connectSlots(); static ImageDB *s_instance; protected: ImageDB(UIDelegate &delegate); public: static QString NONE(); DB::FileNameList currentScope(bool requireOnDisk) const; virtual DB::FileName findFirstItemInRange( const FileNameList &images, const ImageDate &range, bool includeRanges) const; public: // Methods that must be overridden virtual uint totalCount() const = 0; virtual DB::FileNameList search(const ImageSearchInfo &, bool requireOnDisk = false) const = 0; virtual void renameCategory(const QString &oldName, const QString newName) = 0; /** * @brief classify computes a histogram of tags within a category. * I.e. for each sub-category within a given category it counts all images matching the current context, and * computes the date range for those images. * * @param info ImageSearchInfo describing the current search context * @param category the category for which images should be classified * @param typemask images/videos/both * @param mode whether accurate counts are required or not * @return a mapping of sub-category (tags/tag-groups) to the number of images (and the associated date range) */ virtual QMap classify(const ImageSearchInfo &info, const QString &category, MediaType typemask, ClassificationMode mode = ClassificationMode::FullCount) = 0; virtual FileNameList images() = 0; /** * @brief addImages to the database. * The parameter \p doUpdate decides whether all bookkeeping should be done right away * (\c true; the "normal" use-case), or if it should be deferred until later(\c false). * If doUpdate is deferred, either commitDelayedImages() or clearDelayedImages() needs to be called afterwards. * @param images * @param doUpdate */ virtual void addImages(const ImageInfoList &images, bool doUpdate = true) = 0; virtual void commitDelayedImages() = 0; virtual void clearDelayedImages() = 0; /** @short Update file name stored in the DB */ virtual void renameImage(const ImageInfoPtr info, const DB::FileName &newName) = 0; virtual void addToBlockList(const DB::FileNameList &list) = 0; virtual bool isBlocking(const DB::FileName &fileName) = 0; virtual void deleteList(const DB::FileNameList &list) = 0; virtual ImageInfoPtr info(const DB::FileName &fileName) const = 0; virtual MemberMap &memberMap() = 0; virtual void save(const QString &fileName, bool isAutoSave) = 0; virtual MD5Map *md5Map() = 0; virtual void sortAndMergeBackIn(const DB::FileNameList &list) = 0; virtual CategoryCollection *categoryCollection() = 0; virtual QExplicitlySharedDataPointer rangeCollection() = 0; /** * Reorder the items in the database by placing all the items given in * cutList directly before or after the given item. * If the parameter "after" determines where to place it. */ virtual void reorder(const DB::FileName &item, const DB::FileNameList &cutList, bool after) = 0; /** @short Create a stack of images/videos/whatever * * If the specified images already belong to different stacks, then no * change happens and the function returns false. * * If some of them are in one stack and others aren't stacked at all, then * the unstacked will be added to the existing stack and we return true. * * If none of them are stacked, then a new stack is created and we return * true. * * All images which previously weren't in the stack are added in order they * are present in the provided list and after all items that are already in * the stack. The order of images which were already in the stack is not * changed. * */ virtual bool stack(const DB::FileNameList &items) = 0; /** @short Remove all images from whichever stacks they might be in * * We might destroy some stacks in the process if they become empty or just * containing one image. * * This function doesn't touch the order of images at all. * */ virtual void unstack(const DB::FileNameList &images) = 0; /** @short Return a list of images which are in the same stack as the one specified. * * Returns an empty list when the image is not stacked. * * They are returned sorted according to their stackOrder. * */ virtual DB::FileNameList getStackFor(const DB::FileName &referenceId) const = 0; virtual void copyData(const DB::FileName &from, const DB::FileName &to) = 0; protected slots: virtual void lockDB(bool lock, bool exclude) = 0; void markDirty(); signals: void totalChanged(uint); void dirty(); void imagesDeleted(const DB::FileNameList &); }; } #endif /* IMAGEDB_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageDate.cpp b/DB/ImageDate.cpp index c821ea7a..7a1383ba 100644 --- a/DB/ImageDate.cpp +++ b/DB/ImageDate.cpp @@ -1,399 +1,400 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageDate.h" + #include #include #include using namespace DB; ImageDate::ImageDate(const QDate &date) { m_start = QDateTime(date, QTime(0, 0, 0)); m_end = QDateTime(date, QTime(0, 0, 0)); } ImageDate::ImageDate(const QDateTime &date) { m_start = date; m_end = date; } bool ImageDate::operator<=(const ImageDate &other) const { // This operator is used by QMap when checking for equal elements, thus we need the second part too. return m_start < other.m_start || (m_start == other.m_start && m_end <= other.m_end); } ImageDate::ImageDate() { } bool ImageDate::isNull() const { return m_start.isNull(); } bool ImageDate::isFuzzy() const { return m_start != m_end; } static bool isFirstSecOfMonth(const QDateTime &date) { return date.date().day() == 1 && date.time().hour() == 0 && date.time().minute() == 0; } static bool isLastSecOfMonth(QDateTime date) { return isFirstSecOfMonth(date.addSecs(1)); } static bool isFirstSecOfDay(const QDateTime &time) { return time.time().hour() == 0 && time.time().minute() == 0 && time.time().second() == 0; } static bool isLastSecOfDay(const QDateTime &time) { return time.time().hour() == 23 && time.time().minute() == 59 && time.time().second() == 59; } QString ImageDate::toString(bool withTime) const { if (m_start.isNull()) return QString(); if (m_start == m_end) { if (withTime && !isFirstSecOfDay(m_start)) return m_start.toString(QString::fromLatin1("d. MMM yyyy hh:mm:ss")); else return m_start.toString(QString::fromLatin1("d. MMM yyyy")); } // start is different from end. if (isFirstSecOfMonth(m_start) && isLastSecOfMonth(m_end)) { if (m_start.date().month() == 1 && m_end.date().month() == 12) { if (m_start.date().year() == m_end.date().year()) { // 2005 return QString::number(m_start.date().year()); } else { // 2005-2006 return QString::fromLatin1("%1 - %2").arg(m_start.date().year()).arg(m_end.date().year()); } } else { // a whole month, but not a whole year. if (m_start.date().year() == m_end.date().year() && m_start.date().month() == m_end.date().month()) { // jan 2005 return QString::fromLatin1("%1 %2") .arg(QLocale().standaloneMonthName(m_start.date().month(), QLocale::ShortFormat)) .arg(m_start.date().year()); } else { // jan 2005 - feb 2006 return QString::fromLatin1("%1 %2 - %3 %4") .arg(QLocale().standaloneMonthName(m_start.date().month(), QLocale::ShortFormat)) .arg(m_start.date().year()) .arg(QLocale().standaloneMonthName(m_end.date().month(), QLocale::ShortFormat)) .arg(m_end.date().year()); } } } if (!withTime || (isFirstSecOfDay(m_start) && isLastSecOfDay(m_end))) { if (m_start.date() == m_end.date()) { // A whole day return m_start.toString(QString::fromLatin1("d. MMM yyyy")); } else { // A day range return QString::fromLatin1("%1 - %2") .arg(m_start.toString(QString::fromLatin1("d. MMM yyyy"))) .arg(m_end.toString(QString::fromLatin1("d. MMM yyyy"))); } } // Range smaller than one day. if (withTime && (!isFirstSecOfDay(m_start) || !isLastSecOfDay(m_end))) return QString::fromLatin1("%1 - %2") .arg(m_start.toString(QString::fromLatin1("d. MMM yyyy hh:mm"))) .arg(m_end.toString(QString::fromLatin1("d. MMM yyyy hh:mm"))); else return QString::fromLatin1("%1 - %2") .arg(m_start.toString(QString::fromLatin1("d. MMM yyyy"))) .arg(m_end.toString(QString::fromLatin1("d. MMM yyyy"))); } bool ImageDate::operator==(const ImageDate &other) const { return m_start == other.m_start && m_end == other.m_end; } bool ImageDate::operator!=(const ImageDate &other) const { return !(*this == other); } QString ImageDate::formatRegexp() { static QString str; if (str.isEmpty()) { str = QString::fromLatin1("^((\\d\\d?)([-. /]+|$))?(("); QStringList months = monthNames(); for (QStringList::ConstIterator monthIt = months.constBegin(); monthIt != months.constEnd(); ++monthIt) str += QString::fromLatin1("%1|").arg(*monthIt); str += QString::fromLatin1("\\d?\\d)([-. /]+|$))?(\\d\\d(\\d\\d)?)?$"); } return str; } QDateTime ImageDate::start() const { return m_start; } QDateTime ImageDate::end() const { return m_end; } bool ImageDate::operator<(const ImageDate &other) const { return start() < other.start() || (start() == other.start() && end() < other.end()); } ImageDate::ImageDate(const QDateTime &start, const QDateTime &end) { if (!start.isValid() || !end.isValid() || start <= end) { m_start = start; m_end = end; } else { m_start = end; m_end = start; } } ImageDate::ImageDate(const QDate &start, const QDate &end) { if (!start.isValid() || !end.isValid() || start <= end) { m_start = QDateTime(start, QTime(0, 0, 0)); m_end = QDateTime(end, QTime(23, 59, 59)); } else { m_start = QDateTime(end, QTime(0, 0, 0)); m_end = QDateTime(start, QTime(23, 59, 59)); } } static QDate addMonth(int year, int month) { if (month == 12) { year++; month = 1; } else month++; return QDate(year, month, 1); } ImageDate::ImageDate(int yearFrom, int monthFrom, int dayFrom, int yearTo, int monthTo, int dayTo, int hourFrom, int minuteFrom, int secondFrom) { if (yearFrom <= 0) { m_start = QDateTime(); m_end = QDateTime(); return; } if (monthFrom <= 0) { m_start = QDateTime(QDate(yearFrom, 1, 1)); m_end = QDateTime(QDate(yearFrom + 1, 1, 1)).addSecs(-1); } else if (dayFrom <= 0) { m_start = QDateTime(QDate(yearFrom, monthFrom, 1)); m_end = QDateTime(addMonth(yearFrom, monthFrom)).addSecs(-1); } else if (hourFrom < 0) { m_start = QDateTime(QDate(yearFrom, monthFrom, dayFrom)); m_end = QDateTime(QDate(yearFrom, monthFrom, dayFrom).addDays(1)).addSecs(-1); } else if (minuteFrom < 0) { m_start = QDateTime(QDate(yearFrom, monthFrom, dayFrom), QTime(hourFrom, 0, 0)); m_end = QDateTime(QDate(yearFrom, monthFrom, dayFrom), QTime(hourFrom, 23, 59)); } else if (secondFrom < 0) { m_start = QDateTime(QDate(yearFrom, monthFrom, dayFrom), QTime(hourFrom, minuteFrom, 0)); m_end = QDateTime(QDate(yearFrom, monthFrom, dayFrom), QTime(hourFrom, minuteFrom, 59)); } else { m_start = QDateTime(QDate(yearFrom, monthFrom, dayFrom), QTime(hourFrom, minuteFrom, secondFrom)); m_end = m_start; } if (yearTo > 0) { m_end = QDateTime(QDate(yearTo + 1, 1, 1)).addSecs(-1); if (monthTo > 0) { m_end = QDateTime(addMonth(yearTo, monthTo)).addSecs(-1); if (dayTo > 0) { if (dayFrom == dayTo && monthFrom == monthTo && yearFrom == yearTo) m_end = m_start; else m_end = QDateTime(QDate(yearTo, monthTo, dayTo).addDays(1)).addSecs(-1); } } // It should not be possible here for m_end < m_start. Q_ASSERT(m_start <= m_end); } } QDate ImageDate::parseDate(const QString &date, bool startDate) { int year = 0; int month = 0; int day = 0; QRegExp regexp(formatRegexp(), Qt::CaseInsensitive); if (regexp.exactMatch(date)) { QString dayStr = regexp.cap(2); QString monthStr = regexp.cap(5).toLower(); QString yearStr = regexp.cap(7); if (dayStr.length() != 0) day = dayStr.toInt(); if (yearStr.length() != 0) { year = yearStr.toInt(); if (year < 50) year += 2000; if (year < 100) year += 1900; } if (monthStr.length() != 0) { int index = monthNames().indexOf(monthStr); if (index != -1) month = (index % 12) + 1; else month = monthStr.toInt(); } if (year == 0) year = QDate::currentDate().year(); if (month == 0) { if (startDate) { month = 1; day = 1; } else { month = 12; day = 31; } } else if (day == 0) { if (startDate) day = 1; else day = QDate(year, month, 1).daysInMonth(); } return QDate(year, month, day); } else return QDate(); } bool ImageDate::hasValidTime() const { return m_start == m_end; } ImageDate::ImageDate(const QDate &start, QDate end, const QTime &time) { if (!end.isValid()) end = start; if (start == end && time.isValid()) { m_start = QDateTime(start, time); m_end = m_start; } else { if (start > end) { m_end = QDateTime(start, QTime(0, 0, 0)); m_start = QDateTime(end, QTime(23, 59, 59)); } else { m_start = QDateTime(start, QTime(0, 0, 0)); m_end = QDateTime(end, QTime(23, 59, 59)); } } } ImageDate::MatchType ImageDate::isIncludedIn(const ImageDate &searchRange) const { if (searchRange.start() <= start() && searchRange.end() >= end()) return ExactMatch; if (searchRange.start() <= end() && searchRange.end() >= start()) { return RangeMatch; } return DontMatch; } bool ImageDate::includes(const QDateTime &date) const { return ImageDate(date).isIncludedIn(*this) == ExactMatch; } void ImageDate::extendTo(const ImageDate &other) { if (other.isNull()) return; if (isNull()) { m_start = other.m_start; m_end = other.m_end; } else { if (other.m_start < m_start) m_start = other.m_start; if (other.m_end > m_end) m_end = other.m_end; } } QStringList DB::ImageDate::monthNames() { static QStringList res; if (res.isEmpty()) { for (int i = 1; i <= 12; ++i) { res << QLocale().standaloneMonthName(i, QLocale::ShortFormat); } for (int i = 1; i <= 12; ++i) { res << QLocale().standaloneMonthName(i, QLocale::LongFormat); } res << i18nc("Abbreviated month name", "jan") << i18nc("Abbreviated month name", "feb") << i18nc("Abbreviated month name", "mar") << i18nc("Abbreviated month name", "apr") << i18nc("Abbreviated month name", "may") << i18nc("Abbreviated month name", "jun") << i18nc("Abbreviated month name", "jul") << i18nc("Abbreviated month name", "aug") << i18nc("Abbreviated month name", "sep") << i18nc("Abbreviated month name", "oct") << i18nc("Abbreviated month name", "nov") << i18nc("Abbreviated month name", "dec"); res << QString::fromLatin1("jan") << QString::fromLatin1("feb") << QString::fromLatin1("mar") << QString::fromLatin1("apr") << QString::fromLatin1("may") << QString::fromLatin1("jun") << QString::fromLatin1("jul") << QString::fromLatin1("aug") << QString::fromLatin1("sep") << QString::fromLatin1("oct") << QString::fromLatin1("nov") << QString::fromLatin1("dec"); for (int i = 1; i <= 12; ++i) { res << QLocale().monthName(i, QLocale::ShortFormat); } for (int i = 1; i <= 12; ++i) { res << QLocale().monthName(i, QLocale::LongFormat); } for (QStringList::iterator it = res.begin(); it != res.end(); ++it) *it = it->toLower(); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageDateCollection.h b/DB/ImageDateCollection.h index 93dfb050..56e74dd2 100644 --- a/DB/ImageDateCollection.h +++ b/DB/ImageDateCollection.h @@ -1,54 +1,55 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef IMAGEDATECOLLECTION_H #define IMAGEDATECOLLECTION_H -#include "DB/ImageDate.h" +#include "ImageDate.h" + #include #include namespace DB { class ImageCount { public: ImageCount(int exact, int rangeMatch) : mp_exact(exact) , mp_rangeMatch(rangeMatch) { } ImageCount() {} int mp_exact; int mp_rangeMatch; }; class ImageDateCollection : public QSharedData { public: virtual ~ImageDateCollection() {} virtual ImageCount count(const ImageDate &range) = 0; virtual QDateTime lowerLimit() const = 0; virtual QDateTime upperLimit() const = 0; }; } #endif /* IMAGEDATECOLLECTION_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageInfo.cpp b/DB/ImageInfo.cpp index d57d8ccb..a69ea050 100644 --- a/DB/ImageInfo.cpp +++ b/DB/ImageInfo.cpp @@ -1,799 +1,799 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageInfo.h" +#include "CategoryCollection.h" #include "FileInfo.h" +#include "ImageDB.h" #include "Logging.h" +#include "MemberMap.h" -#include -#include -#include #include #include #include #include #include #include #include #include using namespace DB; ImageInfo::ImageInfo() : m_null(true) , m_rating(-1) , m_stackId(0) , m_stackOrder(0) , m_videoLength(-1) , m_isMatched(false) , m_matchGeneration(-1) , m_locked(false) , m_dirty(false) { } ImageInfo::ImageInfo(const DB::FileName &fileName, MediaType type, bool readExifInfo, bool storeExifInfo) : m_imageOnDisk(YesOnDisk) , m_null(false) , m_size(-1, -1) , m_type(type) , m_rating(-1) , m_stackId(0) , m_stackOrder(0) , m_videoLength(-1) , m_isMatched(false) , m_matchGeneration(-1) , m_locked(false) { QFileInfo fi(fileName.absolute()); m_label = fi.completeBaseName(); m_angle = 0; setFileName(fileName); // Read Exif information if (readExifInfo) { ExifMode mode = EXIFMODE_INIT; if (!storeExifInfo) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName, mode); } m_dirty = false; } ImageInfo::ImageInfo(const ImageInfo &other) { *this = other; } void ImageInfo::setIsMatched(bool isMatched) { m_isMatched = isMatched; } bool ImageInfo::isMatched() const { return m_isMatched; } void ImageInfo::setMatchGeneration(int matchGeneration) { m_matchGeneration = matchGeneration; } int ImageInfo::matchGeneration() const { return m_matchGeneration; } void ImageInfo::setLabel(const QString &desc) { if (desc != m_label) m_dirty = true; m_label = desc; } QString ImageInfo::label() const { return m_label; } void ImageInfo::setDescription(const QString &desc) { if (desc != m_description) m_dirty = true; m_description = desc.trimmed(); } QString ImageInfo::description() const { return m_description; } void ImageInfo::setCategoryInfo(const QString &key, const StringSet &value) { // Don't check if really changed, because it's too slow. m_dirty = true; m_categoryInfomation[key] = value; } bool ImageInfo::hasCategoryInfo(const QString &key, const QString &value) const { return m_categoryInfomation[key].contains(value); } bool DB::ImageInfo::hasCategoryInfo(const QString &key, const StringSet &values) const { return values.intersects(m_categoryInfomation[key]); } StringSet ImageInfo::itemsOfCategory(const QString &key) const { return m_categoryInfomation[key]; } void ImageInfo::renameItem(const QString &category, const QString &oldValue, const QString &newValue) { if (m_taggedAreas.contains(category)) { if (m_taggedAreas[category].contains(oldValue)) { m_taggedAreas[category][newValue] = m_taggedAreas[category][oldValue]; m_taggedAreas[category].remove(oldValue); } } StringSet &set = m_categoryInfomation[category]; StringSet::iterator it = set.find(oldValue); if (it != set.end()) { m_dirty = true; set.erase(it); set.insert(newValue); } } DB::FileName ImageInfo::fileName() const { return m_fileName; } void ImageInfo::setFileName(const DB::FileName &fileName) { if (fileName != m_fileName) m_dirty = true; m_fileName = fileName; m_imageOnDisk = Unchecked; DB::CategoryPtr folderCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::FolderCategory); if (folderCategory) { DB::MemberMap &map = DB::ImageDB::instance()->memberMap(); createFolderCategoryItem(folderCategory, map); //ImageDB::instance()->setMemberMap( map ); } } void ImageInfo::rotate(int degrees, RotationMode mode) { // ensure positive degrees: degrees += 360; degrees = degrees % 360; if (degrees == 0) return; m_dirty = true; m_angle = (m_angle + degrees) % 360; if (degrees == 90 || degrees == 270) { m_size.transpose(); } // the AnnotationDialog manages this by itself and sets RotateImageInfoOnly: if (mode == RotateImageInfoAndAreas) { for (auto &areasOfCategory : m_taggedAreas) { for (auto &area : areasOfCategory) { QRect rotatedArea; // parameter order for QRect::setCoords: // setCoords( left, top, right, bottom ) // keep in mind that _size is already transposed switch (degrees) { case 90: rotatedArea.setCoords( m_size.width() - area.bottom(), area.left(), m_size.width() - area.top(), area.right()); break; case 180: rotatedArea.setCoords( m_size.width() - area.right(), m_size.height() - area.bottom(), m_size.width() - area.left(), m_size.height() - area.top()); break; case 270: rotatedArea.setCoords( area.top(), m_size.height() - area.right(), area.bottom(), m_size.height() - area.left()); break; default: // degrees==0; "odd" values won't happen. rotatedArea = area; break; } // update _taggedAreas[category][tag]: area = rotatedArea; } } } } int ImageInfo::angle() const { return m_angle; } void ImageInfo::setAngle(int angle) { if (angle != m_angle) m_dirty = true; m_angle = angle; } short ImageInfo::rating() const { return m_rating; } void ImageInfo::setRating(short rating) { Q_ASSERT((rating >= 0 && rating <= 10) || rating == -1); if (rating > 10) rating = 10; if (rating < -1) rating = -1; if (m_rating != rating) m_dirty = true; m_rating = rating; } DB::StackID ImageInfo::stackId() const { return m_stackId; } void ImageInfo::setStackId(const DB::StackID stackId) { if (stackId != m_stackId) m_dirty = true; m_stackId = stackId; } unsigned int ImageInfo::stackOrder() const { return m_stackOrder; } void ImageInfo::setStackOrder(const unsigned int stackOrder) { if (stackOrder != m_stackOrder) m_dirty = true; m_stackOrder = stackOrder; } void ImageInfo::setVideoLength(int length) { if (m_videoLength != length) m_dirty = true; m_videoLength = length; } int ImageInfo::videoLength() const { return m_videoLength; } void ImageInfo::setDate(const ImageDate &date) { if (date != m_date) m_dirty = true; m_date = date; } ImageDate &ImageInfo::date() { return m_date; } ImageDate ImageInfo::date() const { return m_date; } bool ImageInfo::operator!=(const ImageInfo &other) const { return !(*this == other); } bool ImageInfo::operator==(const ImageInfo &other) const { bool changed = (m_fileName != other.m_fileName || m_label != other.m_label || (!m_description.isEmpty() && !other.m_description.isEmpty() && m_description != other.m_description) || // one might be isNull. m_date != other.m_date || m_angle != other.m_angle || m_rating != other.m_rating || (m_stackId != other.m_stackId || !((m_stackId == 0) ? true : (m_stackOrder == other.m_stackOrder)))); if (!changed) { QStringList keys = DB::ImageDB::instance()->categoryCollection()->categoryNames(); for (QStringList::ConstIterator it = keys.constBegin(); it != keys.constEnd(); ++it) changed |= m_categoryInfomation[*it] != other.m_categoryInfomation[*it]; } return !changed; } void ImageInfo::renameCategory(const QString &oldName, const QString &newName) { m_dirty = true; m_categoryInfomation[newName] = m_categoryInfomation[oldName]; m_categoryInfomation.remove(oldName); m_taggedAreas[newName] = m_taggedAreas[oldName]; m_taggedAreas.remove(oldName); } void ImageInfo::setMD5Sum(const MD5 &sum, bool storeEXIF) { if (sum != m_md5sum) { // if we make a QObject derived class out of imageinfo, we might invalidate thumbnails from here // file changed -> reload/invalidate metadata: ExifMode mode = EXIFMODE_ORIENTATION | EXIFMODE_DATABASE_UPDATE; // fuzzy dates are usually set for a reason if (!m_date.isFuzzy()) mode |= EXIFMODE_DATE; // FIXME (ZaJ): the "right" thing to do would be to update the description // - if it is currently empty (done.) // - if it has been set from the exif info and not been changed (TODO) if (m_description.isEmpty()) mode |= EXIFMODE_DESCRIPTION; if (!storeEXIF) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName(), mode); // FIXME (ZaJ): it *should* make sense to set the ImageDB::md5Map() from here, but I want // to make sure I fully understand everything first... // this could also be done as signal md5Changed(old,new) // image size is invalidated by the thumbnail builder, if needed m_dirty = true; } m_md5sum = sum; } void ImageInfo::setLocked(bool locked) { m_locked = locked; } bool ImageInfo::isLocked() const { return m_locked; } void ImageInfo::readExif(const DB::FileName &fullPath, DB::ExifMode mode) { DB::FileInfo exifInfo = DB::FileInfo::read(fullPath, mode); // Date if (updateDateInformation(mode)) { const ImageDate newDate(exifInfo.dateTime()); setDate(newDate); } // Orientation if ((mode & EXIFMODE_ORIENTATION) && Settings::SettingsData::instance()->useEXIFRotate()) { setAngle(exifInfo.angle()); } // Description if ((mode & EXIFMODE_DESCRIPTION) && Settings::SettingsData::instance()->useEXIFComments()) { bool doSetDescription = true; QString desc = exifInfo.description(); if (Settings::SettingsData::instance()->stripEXIFComments()) { for (const auto &ignoredComment : Settings::SettingsData::instance()->EXIFCommentsToStrip()) { if (desc == ignoredComment) { doSetDescription = false; break; } } } if (doSetDescription) { setDescription(desc); } } // Database update if (mode & EXIFMODE_DATABASE_UPDATE) { Exif::Database::instance()->add(exifInfo); #ifdef HAVE_KGEOMAP // GPS coords might have changed... m_coordsIsSet = false; #endif } } QStringList ImageInfo::availableCategories() const { return m_categoryInfomation.keys(); } QSize ImageInfo::size() const { return m_size; } void ImageInfo::setSize(const QSize &size) { if (size != m_size) m_dirty = true; m_size = size; } bool ImageInfo::imageOnDisk(const DB::FileName &fileName) { return fileName.exists(); } ImageInfo::ImageInfo(const DB::FileName &fileName, const QString &label, const QString &description, const ImageDate &date, int angle, const MD5 &md5sum, const QSize &size, MediaType type, short rating, unsigned int stackId, unsigned int stackOrder) { m_fileName = fileName; m_label = label; m_description = description; m_date = date; m_angle = angle; m_md5sum = md5sum; m_size = size; m_imageOnDisk = Unchecked; m_locked = false; m_null = false; m_type = type; m_dirty = true; if (rating > 10) rating = 10; if (rating < -1) rating = -1; m_rating = rating; m_stackId = stackId; m_stackOrder = stackOrder; m_videoLength = -1; } // Note: we need this operator because the base class QSharedData hides // its copy operator to make exclude the reference counting from being // copied. ImageInfo &ImageInfo::operator=(const ImageInfo &other) { m_fileName = other.m_fileName; m_label = other.m_label; m_description = other.m_description; m_date = other.m_date; m_categoryInfomation = other.m_categoryInfomation; m_taggedAreas = other.m_taggedAreas; m_angle = other.m_angle; m_imageOnDisk = other.m_imageOnDisk; m_md5sum = other.m_md5sum; m_null = other.m_null; m_size = other.m_size; m_type = other.m_type; m_rating = other.m_rating; m_stackId = other.m_stackId; m_stackOrder = other.m_stackOrder; m_videoLength = other.m_videoLength; m_isMatched = other.m_isMatched; m_matchGeneration = other.m_matchGeneration; #ifdef HAVE_KGEOMAP m_coordinates = other.m_coordinates; m_coordsIsSet = other.m_coordsIsSet; #endif m_locked = other.m_locked; m_dirty = other.m_dirty; return *this; } MediaType DB::ImageInfo::mediaType() const { return m_type; } bool ImageInfo::isVideo() const { return m_type == Video; } void DB::ImageInfo::createFolderCategoryItem(DB::CategoryPtr folderCategory, DB::MemberMap &memberMap) { QString folderName = Utilities::relativeFolderName(m_fileName.relative()); if (folderName.isEmpty()) return; if (!memberMap.contains(folderCategory->name(), folderName)) { QStringList directories = folderName.split(QString::fromLatin1("/")); QString curPath; for (QStringList::ConstIterator directoryIt = directories.constBegin(); directoryIt != directories.constEnd(); ++directoryIt) { if (curPath.isEmpty()) curPath = *directoryIt; else { QString oldPath = curPath; curPath = curPath + QString::fromLatin1("/") + *directoryIt; memberMap.addMemberToGroup(folderCategory->name(), oldPath, curPath); } } folderCategory->addItem(folderName); } m_categoryInfomation.insert(folderCategory->name(), StringSet() << folderName); } void DB::ImageInfo::copyExtraData(const DB::ImageInfo &from, bool copyAngle) { m_categoryInfomation = from.m_categoryInfomation; m_description = from.m_description; // Hmm... what should the date be? orig or modified? // _date = from._date; if (copyAngle) m_angle = from.m_angle; m_rating = from.m_rating; } void DB::ImageInfo::removeExtraData() { m_categoryInfomation.clear(); m_description.clear(); m_rating = -1; } void ImageInfo::merge(const ImageInfo &other) { // Merge date if (other.date() != m_date) { // a fuzzy date has been set by the user and therefore "wins" over an exact date. // two fuzzy dates can be merged // two exact dates should ideally be cross-checked with Exif information in the file. // Nevertheless, we merge them into a fuzzy date to avoid the complexity of checking the file. if (other.date().isFuzzy()) { if (m_date.isFuzzy()) m_date.extendTo(other.date()); else m_date = other.date(); } else if (!m_date.isFuzzy()) { m_date.extendTo(other.date()); } // else: keep m_date } // Merge description if (!other.description().isEmpty()) { if (m_description.isEmpty()) m_description = other.description(); else if (m_description != other.description()) m_description += QString::fromUtf8("\n-----------\n") + other.m_description; } // Clear untagged tag if only one of the images was untagged const QString untaggedCategory = Settings::SettingsData::instance()->untaggedCategory(); const QString untaggedTag = Settings::SettingsData::instance()->untaggedTag(); const bool isCompleted = !m_categoryInfomation[untaggedCategory].contains(untaggedTag) || !other.m_categoryInfomation[untaggedCategory].contains(untaggedTag); // Merge tags QSet keys = QSet::fromList(m_categoryInfomation.keys()); keys.unite(QSet::fromList(other.m_categoryInfomation.keys())); for (const QString &key : keys) { m_categoryInfomation[key].unite(other.m_categoryInfomation[key]); } // Clear untagged tag if only one of the images was untagged if (isCompleted) m_categoryInfomation[untaggedCategory].remove(untaggedTag); // merge stacks: if (isStacked() || other.isStacked()) { DB::FileNameList stackImages; if (!isStacked()) stackImages.append(fileName()); else stackImages.append(DB::ImageDB::instance()->getStackFor(fileName())); stackImages.append(DB::ImageDB::instance()->getStackFor(other.fileName())); DB::ImageDB::instance()->unstack(stackImages); if (!DB::ImageDB::instance()->stack(stackImages)) qCWarning(DBLog, "Could not merge stacks!"); } } void DB::ImageInfo::addCategoryInfo(const QString &category, const StringSet &values) { for (StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt) { if (!m_categoryInfomation[category].contains(*valueIt)) { m_dirty = true; m_categoryInfomation[category].insert(*valueIt); } } } void DB::ImageInfo::clearAllCategoryInfo() { m_categoryInfomation.clear(); m_taggedAreas.clear(); } void DB::ImageInfo::removeCategoryInfo(const QString &category, const StringSet &values) { for (StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt) { if (m_categoryInfomation[category].contains(*valueIt)) { m_dirty = true; m_categoryInfomation[category].remove(*valueIt); m_taggedAreas[category].remove(*valueIt); } } } void DB::ImageInfo::addCategoryInfo(const QString &category, const QString &value, const QRect &area) { if (!m_categoryInfomation[category].contains(value)) { m_dirty = true; m_categoryInfomation[category].insert(value); if (area.isValid()) { m_taggedAreas[category][value] = area; } } } void DB::ImageInfo::removeCategoryInfo(const QString &category, const QString &value) { if (m_categoryInfomation[category].contains(value)) { m_dirty = true; m_categoryInfomation[category].remove(value); m_taggedAreas[category].remove(value); } } void DB::ImageInfo::setPositionedTags(const QString &category, const QMap &positionedTags) { m_dirty = true; m_taggedAreas[category] = positionedTags; } bool DB::ImageInfo::updateDateInformation(int mode) const { if ((mode & EXIFMODE_DATE) == 0) return false; if ((mode & EXIFMODE_FORCE) != 0) return true; return true; } QMap> DB::ImageInfo::taggedAreas() const { return m_taggedAreas; } QRect DB::ImageInfo::areaForTag(QString category, QString tag) const { // QMap::value returns a default constructed value if the key is not found: return m_taggedAreas.value(category).value(tag); } #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates DB::ImageInfo::coordinates() const { if (m_coordsIsSet) { return m_coordinates; } static const int EXIF_GPS_VERSIONID = 0; static const int EXIF_GPS_LATREF = 1; static const int EXIF_GPS_LAT = 2; static const int EXIF_GPS_LONREF = 3; static const int EXIF_GPS_LON = 4; static const int EXIF_GPS_ALTREF = 5; static const int EXIF_GPS_ALT = 6; static const QString S = QString::fromUtf8("S"); static const QString W = QString::fromUtf8("W"); static QList fields; if (fields.isEmpty()) { // the order here matters! we use the named int constants afterwards to refer to them: fields.append(new Exif::IntExifElement("Exif.GPSInfo.GPSVersionID")); // actually a byte value fields.append(new Exif::StringExifElement("Exif.GPSInfo.GPSLatitudeRef")); fields.append(new Exif::RationalExifElement("Exif.GPSInfo.GPSLatitude")); fields.append(new Exif::StringExifElement("Exif.GPSInfo.GPSLongitudeRef")); fields.append(new Exif::RationalExifElement("Exif.GPSInfo.GPSLongitude")); fields.append(new Exif::IntExifElement("Exif.GPSInfo.GPSAltitudeRef")); // actually a byte value fields.append(new Exif::RationalExifElement("Exif.GPSInfo.GPSAltitude")); } // read field values from database: bool foundIt = Exif::Database::instance()->readFields(m_fileName, fields); // if the Database query result doesn't contain exif GPS info (-> upgraded exifdb from DBVersion < 2), it is null // if the result is int 0, then there's no exif gps information in the image // otherwise we can proceed to parse the information if (foundIt && fields[EXIF_GPS_VERSIONID]->value().isNull()) { // update exif DB and repeat the search: Exif::Database::instance()->remove(fileName()); Exif::Database::instance()->add(fileName()); Exif::Database::instance()->readFields(m_fileName, fields); Q_ASSERT(!fields[EXIF_GPS_VERSIONID]->value().isNull()); } KGeoMap::GeoCoordinates coords; // gps info set? // don't use the versionid field here, because some cameras use 0 as its value if (foundIt && fields[EXIF_GPS_LAT]->value().toInt() != -1.0 && fields[EXIF_GPS_LON]->value().toInt() != -1.0) { // lat/lon/alt reference determines sign of float: double latr = (fields[EXIF_GPS_LATREF]->value().toString() == S) ? -1.0 : 1.0; double lat = fields[EXIF_GPS_LAT]->value().toFloat(); double lonr = (fields[EXIF_GPS_LONREF]->value().toString() == W) ? -1.0 : 1.0; double lon = fields[EXIF_GPS_LON]->value().toFloat(); double altr = (fields[EXIF_GPS_ALTREF]->value().toInt() == 1) ? -1.0 : 1.0; double alt = fields[EXIF_GPS_ALT]->value().toFloat(); if (lat != -1.0 && lon != -1.0) { coords.setLatLon(latr * lat, lonr * lon); if (alt != 0.0f) { coords.setAlt(altr * alt); } } } m_coordinates = coords; m_coordsIsSet = true; return m_coordinates; } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageInfo.h b/DB/ImageInfo.h index 15edd299..37af49ab 100644 --- a/DB/ImageInfo.h +++ b/DB/ImageInfo.h @@ -1,245 +1,247 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef IMAGEINFO_H #define IMAGEINFO_H -#include "DB/CategoryPtr.h" +#include "config-kpa-kgeomap.h" + +#include "CategoryPtr.h" #include "ExifMode.h" #include "FileName.h" #include "ImageDate.h" #include "MD5.h" -#include "Utilities/StringSet.h" + +#include + #include #include #include #include #include - -#include "config-kpa-kgeomap.h" #ifdef HAVE_KGEOMAP #include #endif namespace Plugins { class ImageInfo; } namespace XMLDB { class Database; } namespace DB { enum PathType { RelativeToImageRoot, AbsolutePath }; enum RotationMode { RotateImageInfoAndAreas, RotateImageInfoOnly }; using Utilities::StringSet; class MemberMap; enum MediaType { Image = 0x01, Video = 0x02 }; const MediaType anyMediaType = MediaType(Image | Video); typedef unsigned int StackID; class ImageInfo : public QSharedData { public: ImageInfo(); explicit ImageInfo(const DB::FileName &fileName, MediaType type = Image, bool readExifInfo = true, bool storeExifInfo = true); ImageInfo(const DB::FileName &fileName, const QString &label, const QString &description, const ImageDate &date, int angle, const MD5 &md5sum, const QSize &size, MediaType type, short rating = -1, StackID stackId = 0, unsigned int stackOrder = 0); ImageInfo(const ImageInfo &other); FileName fileName() const; void setFileName(const DB::FileName &relativeFileName); void setLabel(const QString &); QString label() const; void setDescription(const QString &); QString description() const; void setDate(const ImageDate &); ImageDate date() const; ImageDate &date(); void readExif(const DB::FileName &fullPath, DB::ExifMode mode); void rotate(int degrees, RotationMode mode = RotateImageInfoAndAreas); int angle() const; void setAngle(int angle); short rating() const; void setRating(short rating); bool isStacked() const { return m_stackId != 0; } StackID stackId() const; unsigned int stackOrder() const; void setStackOrder(const unsigned int stackOrder); void setVideoLength(int seconds); int videoLength() const; void setCategoryInfo(const QString &key, const StringSet &value); void addCategoryInfo(const QString &category, const StringSet &values); /** * Enable a tag within a category for this image. * Optionally, the tag's position can be given (for positionable categories). * @param category the category name * @param value the tag name * @param area the image region that the tag applies to. */ void addCategoryInfo(const QString &category, const QString &value, const QRect &area = QRect()); void clearAllCategoryInfo(); void removeCategoryInfo(const QString &category, const StringSet &values); void removeCategoryInfo(const QString &category, const QString &value); /** * Set the tagged areas for the image. * It is assumed that the positioned tags have already been set to the ImageInfo * using one of the functions setCategoryInfo or addCategoryInfo. * * @param category the category name. * @param positionedTags a mapping of tag names to image areas. */ void setPositionedTags(const QString &category, const QMap &positionedTags); bool hasCategoryInfo(const QString &key, const QString &value) const; bool hasCategoryInfo(const QString &key, const StringSet &values) const; QStringList availableCategories() const; StringSet itemsOfCategory(const QString &category) const; void renameItem(const QString &key, const QString &oldValue, const QString &newValue); void renameCategory(const QString &oldName, const QString &newName); bool operator!=(const ImageInfo &other) const; bool operator==(const ImageInfo &other) const; ImageInfo &operator=(const ImageInfo &other); static bool imageOnDisk(const DB::FileName &fileName); const MD5 &MD5Sum() const { return m_md5sum; } void setMD5Sum(const MD5 &sum, bool storeEXIF = true); void setLocked(bool); bool isLocked() const; bool isNull() const { return m_null; } QSize size() const; void setSize(const QSize &size); MediaType mediaType() const; void setMediaType(MediaType type) { if (type != m_type) m_dirty = true; m_type = type; } bool isVideo() const; void createFolderCategoryItem(DB::CategoryPtr, DB::MemberMap &memberMap); void copyExtraData(const ImageInfo &from, bool copyAngle = true); void removeExtraData(); /** * Merge another ImageInfo into this one. * The other ImageInfo is not altered in any way or removed. */ void merge(const ImageInfo &other); QMap> taggedAreas() const; /** * Return the area associated with a tag. * @param category the category name * @param tag the tag name * @return the associated area, or QRect() if no association exists. */ QRect areaForTag(QString category, QString tag) const; void setIsMatched(bool isMatched); bool isMatched() const; void setMatchGeneration(int matchGeneration); int matchGeneration() const; #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates coordinates() const; #endif protected: void setIsNull(bool b) { m_null = b; } bool isDirty() const { return m_dirty; } void setIsDirty(bool b) { m_dirty = b; } bool updateDateInformation(int mode) const; void setStackId(const StackID stackId); friend class XMLDB::Database; private: DB::FileName m_fileName; QString m_label; QString m_description; ImageDate m_date; QMap m_categoryInfomation; QMap> m_taggedAreas; int m_angle; enum OnDisk { YesOnDisk, NoNotOnDisk, Unchecked }; mutable OnDisk m_imageOnDisk; MD5 m_md5sum; bool m_null; QSize m_size; MediaType m_type; short m_rating; StackID m_stackId; unsigned int m_stackOrder; int m_videoLength; bool m_isMatched; int m_matchGeneration; #ifdef HAVE_KGEOMAP mutable KGeoMap::GeoCoordinates m_coordinates; mutable bool m_coordsIsSet = false; #endif // Cache information bool m_locked; // Will be set to true after every change bool m_dirty; }; } #endif /* IMAGEINFO_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageInfoList.cpp b/DB/ImageInfoList.cpp index bf081d86..42855155 100644 --- a/DB/ImageInfoList.cpp +++ b/DB/ImageInfoList.cpp @@ -1,178 +1,177 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "ImageInfoList.h" + +#include "FileNameList.h" #include "ImageInfo.h" #include "Logging.h" -#include - +#include #include #include #include - -#include using namespace DB; class SortableImageInfo { public: SortableImageInfo(const QDateTime &datetime, const QString &string, const ImageInfoPtr &info) : m_dt(datetime) , m_st(string) , m_in(info) { } SortableImageInfo() = default; const QDateTime &DateTime(void) const { return m_dt; } const QString &String(void) const { return m_st; } const ImageInfoPtr &ImageInfo(void) const { return m_in; } bool operator==(const SortableImageInfo &other) const { return m_dt == other.m_dt && m_st == other.m_st; } bool operator!=(const SortableImageInfo &other) const { return m_dt != other.m_dt || m_st != other.m_st; } bool operator>(const SortableImageInfo &other) const { if (m_dt != other.m_dt) { return m_dt > other.m_dt; } else { return m_st > other.m_st; } } bool operator<(const SortableImageInfo &other) const { if (m_dt != other.m_dt) { return m_dt < other.m_dt; } else { return m_st < other.m_st; } } bool operator>=(const SortableImageInfo &other) const { return *this == other || *this > other; } bool operator<=(const SortableImageInfo &other) const { return *this == other || *this < other; } private: QDateTime m_dt; QString m_st; ImageInfoPtr m_in; }; ImageInfoList ImageInfoList::sort() const { QVector vec; for (ImageInfoListConstIterator it = constBegin(); it != constEnd(); ++it) { vec.append(SortableImageInfo((*it)->date().start(), (*it)->fileName().absolute(), *it)); } std::sort(vec.begin(), vec.end()); ImageInfoList res; for (QVector::ConstIterator mapIt = vec.constBegin(); mapIt != vec.constEnd(); ++mapIt) { res.append(mapIt->ImageInfo()); } return res; } void ImageInfoList::sortAndMergeBackIn(ImageInfoList &subListToSort) { ImageInfoList sorted = subListToSort.sort(); const int insertIndex = indexOf(subListToSort[0]); Q_ASSERT(insertIndex >= 0); // Delete the items we will merge in. for (ImageInfoListIterator it = sorted.begin(); it != sorted.end(); ++it) remove(*it); ImageInfoListIterator insertIt = begin() + insertIndex; // Now merge in the items for (ImageInfoListIterator it = sorted.begin(); it != sorted.end(); ++it) { insertIt = insert(insertIt, *it); ++insertIt; } } void ImageInfoList::appendList(ImageInfoList &list) { for (ImageInfoListConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { append(*it); } } void ImageInfoList::printItems() { for (ImageInfoListConstIterator it = constBegin(); it != constEnd(); ++it) { qCDebug(DBLog) << (*it)->fileName().absolute(); } } bool ImageInfoList::isSorted() { if (count() == 0) return true; QDateTime prev = first()->date().start(); QString prevFile = first()->fileName().absolute(); for (ImageInfoListConstIterator it = constBegin(); it != constEnd(); ++it) { QDateTime cur = (*it)->date().start(); QString curFile = (*it)->fileName().absolute(); if (prev > cur || (prev == cur && prevFile > curFile)) return false; prev = cur; prevFile = curFile; } return true; } void ImageInfoList::mergeIn(ImageInfoList other) { ImageInfoList tmp; for (ImageInfoListConstIterator it = constBegin(); it != constEnd(); ++it) { QDateTime thisDate = (*it)->date().start(); QString thisFileName = (*it)->fileName().absolute(); while (other.count() != 0) { QDateTime otherDate = other.first()->date().start(); QString otherFileName = other.first()->fileName().absolute(); if (otherDate < thisDate || (otherDate == thisDate && otherFileName < thisFileName)) { tmp.append(other[0]); other.pop_front(); } else break; } tmp.append(*it); } tmp.appendList(other); *this = tmp; } void ImageInfoList::remove(const ImageInfoPtr &info) { for (ImageInfoListIterator it = begin(); it != end(); ++it) { if ((*(*it)) == *info) { QList::erase(it); return; } } } DB::FileNameList ImageInfoList::files() const { DB::FileNameList res; for (const ImageInfoPtr &info : *this) res.append(info->fileName()); return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageInfoList.h b/DB/ImageInfoList.h index 3e806b12..ec7d37d1 100644 --- a/DB/ImageInfoList.h +++ b/DB/ImageInfoList.h @@ -1,48 +1,49 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef IMAGEINFOLIST_H #define IMAGEINFOLIST_H -#include "DB/ImageInfo.h" -#include "DB/ImageInfoPtr.h" +#include "ImageInfo.h" +#include "ImageInfoPtr.h" + #include namespace DB { class FileNameList; class ImageInfoList : public QList { public: void sortAndMergeBackIn(ImageInfoList &subListToSort); ImageInfoList sort() const; void appendList(ImageInfoList &other); void printItems(); bool isSorted(); void mergeIn(ImageInfoList list); void remove(const ImageInfoPtr &info); DB::FileNameList files() const; }; typedef QList::Iterator ImageInfoListIterator; typedef QList::ConstIterator ImageInfoListConstIterator; } #endif /* IMAGEINFOLIST_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageScout.cpp b/DB/ImageScout.cpp index 2029c975..162eca5f 100644 --- a/DB/ImageScout.cpp +++ b/DB/ImageScout.cpp @@ -1,271 +1,272 @@ /* Copyright (C) 2018-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageScout.h" + #include "Logging.h" #include #include #include #include #include extern "C" { #include #include } using namespace DB; namespace { constexpr int DEFAULT_SCOUT_BUFFER_SIZE = 1048576; // *sizeof(int) bytes // We might want this to be bytes rather than images. constexpr int DEFAULT_MAX_SEEKAHEAD_IMAGES = 10; constexpr int SEEKAHEAD_WAIT_MS = 10; // 10 milliseconds, and retry constexpr int TERMINATION_WAIT_MS = 10; // 10 milliseconds, and retry } // 1048576 with a single scout thread empirically yields best performance // on a Seagate 2TB 2.5" disk, sustaining throughput in the range of // 95-100 MB/sec with 100-110 IO/sec on large files. This is close to what // would be expected. A SATA SSD (Crucial MX300) is much less sensitive to // I/O size and scout thread, achieving about 340 MB/sec with high CPU // utilization. class DB::ImageScoutThread : public QThread { friend class DB::ImageScout; public: ImageScoutThread(ImageScoutQueue &, QMutex *, QAtomicInt &count, QAtomicInt &preloadCount, QAtomicInt &skippedCount); protected: void run() override; void setBufSize(int); int getBufSize(); void setMaxSeekAhead(int); int getMaxSeekAhead(); void setReadLimit(int); int getReadLimit(); private: void doRun(char *); ImageScoutQueue &m_queue; QMutex *m_mutex; QAtomicInt &m_loadedCount; QAtomicInt &m_preloadedCount; QAtomicInt &m_skippedCount; int m_scoutBufSize; int m_maxSeekAhead; int m_readLimit; bool m_isStarted; }; ImageScoutThread::ImageScoutThread(ImageScoutQueue &queue, QMutex *mutex, QAtomicInt &count, QAtomicInt &preloadedCount, QAtomicInt &skippedCount) : m_queue(queue) , m_mutex(mutex) , m_loadedCount(count) , m_preloadedCount(preloadedCount) , m_skippedCount(skippedCount) , m_scoutBufSize(DEFAULT_SCOUT_BUFFER_SIZE) , m_maxSeekAhead(DEFAULT_MAX_SEEKAHEAD_IMAGES) , m_readLimit(-1) , m_isStarted(false) { } void ImageScoutThread::doRun(char *tmpBuf) { while (!isInterruptionRequested()) { QMutexLocker locker(m_mutex); if (m_queue.isEmpty()) { return; } DB::FileName fileName = m_queue.dequeue(); locker.unlock(); // If we're behind the reader, move along m_preloadedCount++; if (m_loadedCount.load() >= m_preloadedCount.load()) { m_skippedCount++; continue; } else { // Don't get too far ahead of the loader, or we just waste memory // TODO: wait on something rather than polling while (m_preloadedCount.load() >= m_loadedCount.load() + m_maxSeekAhead && !isInterruptionRequested()) { QThread::msleep(SEEKAHEAD_WAIT_MS); } // qCDebug(DBImageScoutLog) << ">>>>>Scout: preload" << m_preloadedCount.load() << "load" << m_loadedCount.load() << fileName.relative(); } int inputFD = open(QFile::encodeName(fileName.absolute()).constData(), O_RDONLY); int bytesRead = 0; if (inputFD >= 0) { while (read(inputFD, tmpBuf, m_scoutBufSize) && (m_readLimit < 0 || ((bytesRead += m_scoutBufSize) < m_readLimit)) && !isInterruptionRequested()) { } (void)close(inputFD); } } } void ImageScoutThread::setBufSize(int bufSize) { if (!m_isStarted) m_scoutBufSize = bufSize; } int ImageScoutThread::getBufSize() { return m_scoutBufSize; } void ImageScoutThread::setMaxSeekAhead(int maxSeekAhead) { if (!m_isStarted) m_maxSeekAhead = maxSeekAhead; } int ImageScoutThread::getMaxSeekAhead() { return m_maxSeekAhead; } void ImageScoutThread::setReadLimit(int readLimit) { if (!m_isStarted) m_readLimit = readLimit; } int ImageScoutThread::getReadLimit() { return m_readLimit; } void ImageScoutThread::run() { m_isStarted = true; char *tmpBuf = new char[m_scoutBufSize]; doRun(tmpBuf); delete[] tmpBuf; } ImageScout::ImageScout(ImageScoutQueue &images, QAtomicInt &count, int threads) : m_preloadedCount(0) , m_skippedCount(0) , m_isStarted(false) , m_scoutBufSize(DEFAULT_SCOUT_BUFFER_SIZE) , m_maxSeekAhead(DEFAULT_MAX_SEEKAHEAD_IMAGES) , m_readLimit(-1) { if (threads > 0) { for (int i = 0; i < threads; i++) { ImageScoutThread *t = new ImageScoutThread(images, threads > 1 ? &m_mutex : nullptr, count, m_preloadedCount, m_skippedCount); m_scoutList.append(t); } } } ImageScout::~ImageScout() { if (m_scoutList.count() > 0) { for (QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it) { if (m_isStarted) { if (!(*it)->isFinished()) { (*it)->requestInterruption(); while (!(*it)->isFinished()) QThread::msleep(TERMINATION_WAIT_MS); } } delete (*it); } } qCDebug(DBImageScoutLog) << "Total files:" << m_preloadedCount << "skipped" << m_skippedCount; } void ImageScout::start() { // Yes, there's a race condition here between isStartd and setting // the buf size or seek ahead...but this isn't a hot code path! if (!m_isStarted && m_scoutList.count() > 0) { m_isStarted = true; for (QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it) { (*it)->start(); } } } void ImageScout::setBufSize(int bufSize) { if (!m_isStarted && bufSize > 0) { m_scoutBufSize = bufSize; for (QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it) { (*it)->setBufSize(m_scoutBufSize); } } } int ImageScout::getBufSize() { return m_scoutBufSize; } void ImageScout::setMaxSeekAhead(int maxSeekAhead) { if (!m_isStarted && maxSeekAhead > 0) { m_maxSeekAhead = maxSeekAhead; for (QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it) { (*it)->setMaxSeekAhead(m_maxSeekAhead); } } } int ImageScout::getMaxSeekAhead() { return m_maxSeekAhead; } void ImageScout::setReadLimit(int readLimit) { if (!m_isStarted && readLimit > 0) { m_readLimit = readLimit; for (QList::iterator it = m_scoutList.begin(); it != m_scoutList.end(); ++it) { (*it)->setReadLimit(m_readLimit); } } } int ImageScout::getReadLimit() { return m_readLimit; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageScout.h b/DB/ImageScout.h index effaf358..6e860bbb 100644 --- a/DB/ImageScout.h +++ b/DB/ImageScout.h @@ -1,75 +1,76 @@ /* Copyright (C) 2018 Robert Krawitz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef IMAGESCOUT_H #define IMAGESCOUT_H -#include "DB/FileName.h" +#include "FileName.h" + #include #include #include #include namespace DB { typedef QQueue ImageScoutQueue; class ImageScoutThread; /** * Scout thread for image loading: preload images from disk to have them in * RAM to mask I/O latency. */ class ImageScout { public: // count is an atomic variable containing the number of images // that have been loaded thus far. Used to prevent the scout from // getting too far ahead, if it's able to run faster than the // loader. Which usually it can't; with hard disks on any halfway // decent system, the loader can run faster than the disk. ImageScout(ImageScoutQueue &, QAtomicInt &count, int threads = 1); ~ImageScout(); // Specify scout buffer size. May not be called after starting the scout. void setBufSize(int); int getBufSize(); // Specify how far we're allowed to run ahead of the loader, in images. // May not be called after starting the scout. void setMaxSeekAhead(int); int getMaxSeekAhead(); // Specify how many bytes we read before moving on. // May not be called after starting the scout. void setReadLimit(int); int getReadLimit(); // Start the scout running void start(); private: QMutex m_mutex; QList m_scoutList; QAtomicInt m_preloadedCount; QAtomicInt m_skippedCount; bool m_isStarted; int m_scoutBufSize; int m_maxSeekAhead; int m_readLimit; }; } #endif /* IMAGESCOUT_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageSearchInfo.cpp b/DB/ImageSearchInfo.cpp index fd31f8be..ed0098a5 100644 --- a/DB/ImageSearchInfo.cpp +++ b/DB/ImageSearchInfo.cpp @@ -1,664 +1,664 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageSearchInfo.h" + #include "AndCategoryMatcher.h" #include "CategoryMatcher.h" #include "ContainerCategoryMatcher.h" #include "ExactCategoryMatcher.h" #include "ImageDB.h" #include "Logging.h" #include "NegationCategoryMatcher.h" #include "NoTagCategoryMatcher.h" #include "OrCategoryMatcher.h" #include "ValueCategoryMatcher.h" #include #include #include #include #include - #include #include using namespace DB; static QAtomicInt s_matchGeneration; static int nextGeneration() { return s_matchGeneration++; } ImageSearchInfo::ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description) : m_date(date) , m_label(label) , m_description(description) , m_rating(-1) , m_megapixel(0) , m_max_megapixel(0) , m_ratingSearchMode(0) , m_searchRAW(false) , m_isNull(false) , m_isCacheable(true) , m_compiled(false) , m_matchGeneration(nextGeneration()) { } ImageSearchInfo::ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description, const QString &fnPattern) : m_date(date) , m_label(label) , m_description(description) , m_fnPattern(fnPattern) , m_rating(-1) , m_megapixel(0) , m_max_megapixel(0) , m_ratingSearchMode(0) , m_searchRAW(false) , m_isNull(false) , m_isCacheable(true) , m_compiled(false) , m_matchGeneration(nextGeneration()) { } QString ImageSearchInfo::label() const { return m_label; } QRegExp ImageSearchInfo::fnPattern() const { return m_fnPattern; } QString ImageSearchInfo::description() const { return m_description; } void ImageSearchInfo::checkIfNull() { if (m_compiled || isNull()) return; if (m_date.isNull() && m_label.isEmpty() && m_description.isEmpty() && m_rating == -1 && m_megapixel == 0 && m_exifSearchInfo.isNull() && m_categoryMatchText.isEmpty() #ifdef HAVE_KGEOMAP && !m_regionSelection.first.hasCoordinates() && !m_regionSelection.second.hasCoordinates() #endif ) { m_isNull = true; } } ImageSearchInfo::ImageSearchInfo() : m_rating(-1) , m_megapixel(0) , m_max_megapixel(0) , m_ratingSearchMode(0) , m_searchRAW(false) , m_isNull(true) , m_isCacheable(true) , m_compiled(false) , m_matchGeneration(nextGeneration()) { } bool ImageSearchInfo::isNull() const { return m_isNull; } bool ImageSearchInfo::isCacheable() const { return m_isCacheable; } void ImageSearchInfo::setCacheable(bool cacheable) { m_isCacheable = cacheable; } bool ImageSearchInfo::match(ImageInfoPtr info) const { if (m_isNull) return true; if (m_isCacheable && info->matchGeneration() == m_matchGeneration) return info->isMatched(); bool ok = doMatch(info); if (m_isCacheable) { info->setMatchGeneration(m_matchGeneration); info->setIsMatched(ok); } return ok; } bool ImageSearchInfo::doMatch(ImageInfoPtr info) const { if (!m_compiled) compile(); // -------------------------------------------------- Rating //ok = ok && (_rating == -1 ) || ( _rating == info->rating() ); if (m_rating != -1) { switch (m_ratingSearchMode) { case 1: // Image rating at least selected if (m_rating > info->rating()) return false; break; case 2: // Image rating less than selected if (m_rating < info->rating()) return false; break; case 3: // Image rating not equal if (m_rating == info->rating()) return false; break; default: if (m_rating != info->rating()) return false; break; } } // -------------------------------------------------- Resolution if (m_megapixel && (m_megapixel * 1000000 > info->size().width() * info->size().height())) return false; if (m_max_megapixel && m_max_megapixel < m_megapixel && (m_max_megapixel * 1000000 < info->size().width() * info->size().height())) return false; // -------------------------------------------------- Date QDateTime actualStart = info->date().start(); QDateTime actualEnd = info->date().end(); if (m_date.start().isValid()) { if (actualEnd < m_date.start() || (m_date.end().isValid() && actualStart > m_date.end())) return false; } else if (m_date.end().isValid() && actualStart > m_date.end()) { return false; } // -------------------------------------------------- Label if (m_label.isEmpty() && info->label().indexOf(m_label) == -1) return false; // -------------------------------------------------- RAW if (m_searchRAW && !ImageManager::RAWImageDecoder::isRAW(info->fileName())) return false; #ifdef HAVE_KGEOMAP // Search for GPS Position if (m_usingRegionSelection) { if (!info->coordinates().hasCoordinates()) return false; float infoLat = info->coordinates().lat(); if (m_regionSelectionMinLat > infoLat || m_regionSelectionMaxLat < infoLat) return false; float infoLon = info->coordinates().lon(); if (m_regionSelectionMinLon > infoLon || m_regionSelectionMaxLon < infoLon) return false; } #endif // -------------------------------------------------- File name pattern if (!m_fnPattern.isEmpty() && m_fnPattern.indexIn(info->fileName().relative()) == -1) return false; // -------------------------------------------------- Options // alreadyMatched map is used to make it possible to search for // Jesper & None QMap alreadyMatched; for (CategoryMatcher *optionMatcher : m_categoryMatchers) { if (!optionMatcher->eval(info, alreadyMatched)) return false; } // -------------------------------------------------- Text if (!m_description.isEmpty()) { const QString &txt(info->description()); QStringList list = m_description.split(QChar::fromLatin1(' '), QString::SkipEmptyParts); Q_FOREACH (const QString &word, list) { if (txt.indexOf(word, 0, Qt::CaseInsensitive) == -1) return false; } } // -------------------------------------------------- EXIF if (!m_exifSearchInfo.matches(info->fileName())) return false; return true; } QString ImageSearchInfo::categoryMatchText(const QString &name) const { return m_categoryMatchText[name]; } void ImageSearchInfo::setCategoryMatchText(const QString &name, const QString &value) { if (value.isEmpty()) { m_categoryMatchText.remove(name); } else { m_categoryMatchText[name] = value; } m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::addAnd(const QString &category, const QString &value) { // Escape literal "&"s in value by doubling it QString escapedValue = value; escapedValue.replace(QString::fromUtf8("&"), QString::fromUtf8("&&")); QString val = categoryMatchText(category); if (!val.isEmpty()) val += QString::fromLatin1(" & ") + escapedValue; else val = escapedValue; setCategoryMatchText(category, val); m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setRating(short rating) { m_rating = rating; m_isNull = false; m_compiled = false; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setMegaPixel(short megapixel) { m_megapixel = megapixel; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setMaxMegaPixel(short max_megapixel) { m_max_megapixel = max_megapixel; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setSearchMode(int index) { m_ratingSearchMode = index; m_matchGeneration = nextGeneration(); } void ImageSearchInfo::setSearchRAW(bool searchRAW) { m_searchRAW = searchRAW; m_matchGeneration = nextGeneration(); } QString ImageSearchInfo::toString() const { QString res; bool first = true; for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { if (!it.value().isEmpty()) { if (first) first = false; else res += QString::fromLatin1(" / "); QString txt = it.value(); if (txt == ImageDB::NONE()) txt = i18nc("As in No persons, no locations etc. I do realize that translators may have problem with this, " "but I need some how to indicate the category, and users may create their own categories, so this is " "the best I can do - Jesper.", "No %1", it.key()); if (txt.contains(QString::fromLatin1("|"))) txt.replace(QString::fromLatin1("&"), QString::fromLatin1(" %1 ").arg(i18n("and"))); else txt.replace(QString::fromLatin1("&"), QString::fromLatin1(" / ")); txt.replace(QString::fromLatin1("|"), QString::fromLatin1(" %1 ").arg(i18n("or"))); txt.replace(QString::fromLatin1("!"), QString::fromLatin1(" %1 ").arg(i18n("not"))); txt.replace(ImageDB::NONE(), i18nc("As in no other persons, or no other locations. " "I do realize that translators may have problem with this, " "but I need some how to indicate the category, and users may create their own categories, so this is " "the best I can do - Jesper.", "No other %1", it.key())); res += txt.simplified(); } } return res; } void ImageSearchInfo::debug() { for (QMap::Iterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { qCDebug(DBCategoryMatcherLog) << it.key() << ", " << it.value(); } } // PENDING(blackie) move this into the Options class instead of having it here. void ImageSearchInfo::saveLock() const { KConfigGroup config = KSharedConfig::openConfig()->group(Settings::SettingsData::instance()->groupForDatabase("Privacy Settings")); config.writeEntry(QString::fromLatin1("label"), m_label); config.writeEntry(QString::fromLatin1("description"), m_description); config.writeEntry(QString::fromLatin1("categories"), m_categoryMatchText.keys()); for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { config.writeEntry(it.key(), it.value()); } config.sync(); } ImageSearchInfo ImageSearchInfo::loadLock() { KConfigGroup config = KSharedConfig::openConfig()->group(Settings::SettingsData::instance()->groupForDatabase("Privacy Settings")); ImageSearchInfo info; info.m_label = config.readEntry("label"); info.m_description = config.readEntry("description"); QStringList categories = config.readEntry(QString::fromLatin1("categories"), QStringList()); for (QStringList::ConstIterator it = categories.constBegin(); it != categories.constEnd(); ++it) { info.setCategoryMatchText(*it, config.readEntry(*it, QString())); } return info; } ImageSearchInfo::ImageSearchInfo(const ImageSearchInfo &other) { m_date = other.m_date; m_categoryMatchText = other.m_categoryMatchText; m_label = other.m_label; m_description = other.m_description; m_fnPattern = other.m_fnPattern; m_isNull = other.m_isNull; m_compiled = false; m_rating = other.m_rating; m_ratingSearchMode = other.m_ratingSearchMode; m_megapixel = other.m_megapixel; m_max_megapixel = other.m_max_megapixel; m_searchRAW = other.m_searchRAW; m_exifSearchInfo = other.m_exifSearchInfo; m_matchGeneration = other.m_matchGeneration; m_isCacheable = other.m_isCacheable; #ifdef HAVE_KGEOMAP m_regionSelection = other.m_regionSelection; #endif } void ImageSearchInfo::compile() const { m_exifSearchInfo.search(); #ifdef HAVE_KGEOMAP // Prepare Search for GPS Position m_usingRegionSelection = m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates(); if (m_usingRegionSelection) { using std::max; using std::min; m_regionSelectionMinLat = min(m_regionSelection.first.lat(), m_regionSelection.second.lat()); m_regionSelectionMaxLat = max(m_regionSelection.first.lat(), m_regionSelection.second.lat()); m_regionSelectionMinLon = min(m_regionSelection.first.lon(), m_regionSelection.second.lon()); m_regionSelectionMaxLon = max(m_regionSelection.first.lon(), m_regionSelection.second.lon()); } #endif deleteMatchers(); for (QMap::ConstIterator it = m_categoryMatchText.begin(); it != m_categoryMatchText.end(); ++it) { QString category = it.key(); QString matchText = it.value(); QStringList orParts = matchText.split(QString::fromLatin1("|"), QString::SkipEmptyParts); DB::ContainerCategoryMatcher *orMatcher = new DB::OrCategoryMatcher; Q_FOREACH (QString orPart, orParts) { // Split by " & ", not only by "&", so that the doubled "&"s won't be used as a split point QStringList andParts = orPart.split(QString::fromLatin1(" & "), QString::SkipEmptyParts); DB::ContainerCategoryMatcher *andMatcher; bool exactMatch = false; bool negate = false; andMatcher = new DB::AndCategoryMatcher; Q_FOREACH (QString str, andParts) { static QRegExp regexp(QString::fromLatin1("^\\s*!\\s*(.*)$")); if (regexp.exactMatch(str)) { // str is preceded with NOT negate = true; str = regexp.cap(1); } str = str.trimmed(); CategoryMatcher *valueMatcher; if (str == ImageDB::NONE()) { // mark AND-group as containing a "No other" condition exactMatch = true; continue; } else { valueMatcher = new DB::ValueCategoryMatcher(category, str); if (negate) valueMatcher = new DB::NegationCategoryMatcher(valueMatcher); } andMatcher->addElement(valueMatcher); } if (exactMatch) { DB::CategoryMatcher *exactMatcher = nullptr; // if andMatcher has exactMatch set, but no CategoryMatchers, then // matching "category / None" is what we want: if (andMatcher->mp_elements.count() == 0) { exactMatcher = new DB::NoTagCategoryMatcher(category); } else { ExactCategoryMatcher *noOtherMatcher = new ExactCategoryMatcher(category); if (andMatcher->mp_elements.count() == 1) noOtherMatcher->setMatcher(andMatcher->mp_elements[0]); else noOtherMatcher->setMatcher(andMatcher); exactMatcher = noOtherMatcher; } if (negate) exactMatcher = new DB::NegationCategoryMatcher(exactMatcher); orMatcher->addElement(exactMatcher); } else if (andMatcher->mp_elements.count() == 1) orMatcher->addElement(andMatcher->mp_elements[0]); else if (andMatcher->mp_elements.count() > 1) orMatcher->addElement(andMatcher); } CategoryMatcher *matcher = nullptr; if (orMatcher->mp_elements.count() == 1) matcher = orMatcher->mp_elements[0]; else if (orMatcher->mp_elements.count() > 1) matcher = orMatcher; if (matcher) { m_categoryMatchers.append(matcher); if (DBCategoryMatcherLog().isDebugEnabled()) { qCDebug(DBCategoryMatcherLog) << "Matching text '" << matchText << "' in category " << category << ":"; matcher->debug(0); qCDebug(DBCategoryMatcherLog) << "."; } } } m_compiled = true; } ImageSearchInfo::~ImageSearchInfo() { deleteMatchers(); } void ImageSearchInfo::debugMatcher() const { if (!m_compiled) compile(); qCDebug(DBCategoryMatcherLog, "And:"); for (CategoryMatcher *optionMatcher : m_categoryMatchers) { optionMatcher->debug(1); } } QList> ImageSearchInfo::query() const { if (!m_compiled) compile(); // Combine _optionMachers to one list of lists in Disjunctive // Normal Form and return it. QList::Iterator it = m_categoryMatchers.begin(); QList> result; if (it == m_categoryMatchers.end()) return result; result = convertMatcher(*it); ++it; for (; it != m_categoryMatchers.end(); ++it) { QList> current = convertMatcher(*it); QList> oldResult = result; result.clear(); for (QList resultIt : oldResult) { for (QList currentIt : current) { QList tmp; tmp += resultIt; tmp += currentIt; result.append(tmp); } } } return result; } Utilities::StringSet ImageSearchInfo::findAlreadyMatched(const QString &group) const { Utilities::StringSet result; QString str = categoryMatchText(group); if (str.contains(QString::fromLatin1("|"))) { return result; } QStringList list = str.split(QString::fromLatin1("&"), QString::SkipEmptyParts); Q_FOREACH (QString part, list) { QString nm = part.trimmed(); if (!nm.contains(QString::fromLatin1("!"))) result.insert(nm); } return result; } void ImageSearchInfo::deleteMatchers() const { qDeleteAll(m_categoryMatchers); m_categoryMatchers.clear(); } QList ImageSearchInfo::extractAndMatcher(CategoryMatcher *matcher) const { QList result; AndCategoryMatcher *andMatcher; SimpleCategoryMatcher *simpleMatcher; if ((andMatcher = dynamic_cast(matcher))) { for (CategoryMatcher *child : andMatcher->mp_elements) { SimpleCategoryMatcher *simpleMatcher = dynamic_cast(child); Q_ASSERT(simpleMatcher); result.append(simpleMatcher); } } else if ((simpleMatcher = dynamic_cast(matcher))) result.append(simpleMatcher); else Q_ASSERT(false); return result; } /** Convert matcher to Disjunctive Normal Form. * * @return OR-list of AND-lists. (e.g. OR(AND(a,b),AND(c,d))) */ QList> ImageSearchInfo::convertMatcher(CategoryMatcher *item) const { QList> result; OrCategoryMatcher *orMacther; if ((orMacther = dynamic_cast(item))) { for (CategoryMatcher *child : orMacther->mp_elements) { result.append(extractAndMatcher(child)); } } else result.append(extractAndMatcher(item)); return result; } short ImageSearchInfo::rating() const { return m_rating; } ImageDate ImageSearchInfo::date() const { return m_date; } void ImageSearchInfo::addExifSearchInfo(const Exif::SearchInfo info) { m_exifSearchInfo = info; m_isNull = false; } void DB::ImageSearchInfo::renameCategory(const QString &oldName, const QString &newName) { m_categoryMatchText[newName] = m_categoryMatchText[oldName]; m_categoryMatchText.remove(oldName); m_compiled = false; } #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates::Pair ImageSearchInfo::regionSelection() const { return m_regionSelection; } void ImageSearchInfo::setRegionSelection(const KGeoMap::GeoCoordinates::Pair &actRegionSelection) { m_regionSelection = actRegionSelection; m_compiled = false; if (m_regionSelection.first.hasCoordinates() && m_regionSelection.second.hasCoordinates()) { m_isNull = false; } } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/ImageSearchInfo.h b/DB/ImageSearchInfo.h index 9bdfdfe0..d8309cf4 100644 --- a/DB/ImageSearchInfo.h +++ b/DB/ImageSearchInfo.h @@ -1,146 +1,146 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef IMAGESEARCHINFO_H #define IMAGESEARCHINFO_H -#include +#include "ImageDate.h" +#include "ImageInfoPtr.h" + +#include #include #include - -#include -#include -#include +#include #ifdef HAVE_KGEOMAP #include #endif #include namespace DB { class SimpleCategoryMatcher; class ImageInfo; class CategoryMatcher; class ImageSearchInfo { public: ImageSearchInfo(); ~ImageSearchInfo(); ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description); ImageSearchInfo(const ImageDate &date, const QString &label, const QString &description, const QString &fnPattern); ImageSearchInfo(const ImageSearchInfo &other); ImageDate date() const; QString categoryMatchText(const QString &name) const; void setCategoryMatchText(const QString &name, const QString &value); void renameCategory(const QString &oldName, const QString &newName); QString label() const; QRegExp fnPattern() const; QString description() const; /** * @brief checkIfNull evaluates whether the filter is indeed empty and * sets isNull() to \c true if that is the case. * You only need to call this if you re-use an existing ImageSearchInfo * and set/reset search parameters. * @see ThumbnailView::toggleRatingFilter */ void checkIfNull(); bool isNull() const; bool match(ImageInfoPtr) const; QList> query() const; void addAnd(const QString &category, const QString &value); short rating() const; void setRating(short rating); QString toString() const; void setMegaPixel(short megapixel); void setMaxMegaPixel(short maxmegapixel); void setSearchRAW(bool m_searchRAW); void setSearchMode(int index); void saveLock() const; static ImageSearchInfo loadLock(); void debug(); void debugMatcher() const; Utilities::StringSet findAlreadyMatched(const QString &group) const; void addExifSearchInfo(const Exif::SearchInfo info); // By default, an ImageSearchInfo is cacheable, but only one search // is cached per image. For a search that's only going to be // performed once, don't try to cache the result. void setCacheable(bool cacheable); bool isCacheable() const; #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates::Pair regionSelection() const; void setRegionSelection(const KGeoMap::GeoCoordinates::Pair &actRegionSelection); #endif protected: void compile() const; void deleteMatchers() const; QList extractAndMatcher(CategoryMatcher *andMatcher) const; QList> convertMatcher(CategoryMatcher *) const; private: ImageDate m_date; QMap m_categoryMatchText; QString m_label; QString m_description; QRegExp m_fnPattern; short m_rating; short m_megapixel; short m_max_megapixel; int m_ratingSearchMode; bool m_searchRAW; bool m_isNull; bool m_isCacheable; mutable bool m_compiled; mutable QList m_categoryMatchers; Exif::SearchInfo m_exifSearchInfo; int m_matchGeneration; bool doMatch(ImageInfoPtr) const; #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates::Pair m_regionSelection; mutable bool m_usingRegionSelection = false; mutable float m_regionSelectionMinLat; mutable float m_regionSelectionMaxLat; mutable float m_regionSelectionMinLon; mutable float m_regionSelectionMaxLon; #endif // When adding new instance variable, please notice that this class as an explicit written copy constructor. }; } #endif /* IMAGESEARCHINFO_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/MD5Map.h b/DB/MD5Map.h index 8805966b..38487275 100644 --- a/DB/MD5Map.h +++ b/DB/MD5Map.h @@ -1,56 +1,57 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MD5MAP_H #define MD5MAP_H +#include "FileName.h" #include "MD5.h" -#include + #include #include namespace DB { typedef QHash MD5FileMap; typedef QHash FileMD5Map; /** This class may be overridden by a which wants to store md5 information directly in a database, rather than in a map in memory. **/ class MD5Map { public: virtual ~MD5Map() {} virtual void insert(const MD5 &md5sum, const DB::FileName &fileName); virtual DB::FileName lookup(const MD5 &md5sum) const; virtual MD5 lookupFile(const DB::FileName &fileName) const; virtual bool contains(const MD5 &md5sum) const; virtual bool containsFile(const DB::FileName &fileName) const; virtual void clear(); virtual DB::FileNameSet diff(const MD5Map &other) const; private: MD5FileMap m_map; FileMD5Map m_i_map; }; } #endif /* MD5MAP_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/MemberMap.cpp b/DB/MemberMap.cpp index 2863ce4a..3a44c325 100644 --- a/DB/MemberMap.cpp +++ b/DB/MemberMap.cpp @@ -1,362 +1,363 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "Category.h" #include "Logging.h" 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()); } bool MemberMap::contains(const QString &category, const QString &item) const { return m_flatMembers[category].contains(item); } void MemberMap::markDirty(const QString &category) { if (m_loading) regenerateFlatList(category); else emit dirty(); } void MemberMap::deleteGroup(const QString &category, const QString &name) { m_members[category].remove(name); m_dirty = true; markDirty(category); } /** 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; markDirty(category); } 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>::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; markDirty(category); 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; markDirty(category->name()); } 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; markDirty(category->name()); } MemberMap &MemberMap::operator=(const MemberMap &other) { if (this != &other) { m_members = other.memberMap(); m_dirty = true; } return *this; } void MemberMap::regenerateFlatList(const QString &category) { m_flatMembers[category].clear(); for (QMap::const_iterator i = m_members[category].constBegin(); i != m_members[category].constEnd(); i++) { for (StringSet::const_iterator j = i.value().constBegin(); j != i.value().constEnd(); j++) { m_flatMembers[category].insert(*j); } } } 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)) { qCWarning(DBLog, "Inserting item %s into group %s/%s would create a cycle. Ignoring...", qPrintable(item), qPrintable(category), qPrintable(group)); return; } if (item.isEmpty()) { qCWarning(DBLog, "Tried to insert null item into group %s/%s. Ignoring...", qPrintable(category), qPrintable(group)); return; } m_members[category][group].insert(item); m_flatMembers[category].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 we are loading, we do *not* want to regenerate the list! 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); // We shouldn't be doing this very often, so just regenerate // the flat list regenerateFlatList(category); emit dirty(); } } void MemberMap::addGroup(const QString &category, const QString &group) { if (!m_members[category].contains(group)) { m_members[category].insert(group, StringSet()); } markDirty(category); } 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); markDirty(category); } 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; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/MemberMap.h b/DB/MemberMap.h index a8d2032f..9df10c06 100644 --- a/DB/MemberMap.h +++ b/DB/MemberMap.h @@ -1,101 +1,102 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MEMBERMAP_H #define MEMBERMAP_H -#include "Utilities/StringSet.h" +#include + #include #include #include namespace DB { using Utilities::StringSet; class Category; class MemberMap : public QObject { Q_OBJECT public: MemberMap(); MemberMap(const MemberMap &); virtual MemberMap &operator=(const MemberMap &); // TODO: this should return a StringSet virtual QStringList groups(const QString &category) const; virtual void deleteGroup(const QString &category, const QString &name); // TODO: this should return a StringSet virtual QStringList members(const QString &category, const QString &memberGroup, bool closure) const; virtual void setMembers(const QString &category, const QString &memberGroup, const QStringList &members); virtual bool isEmpty() const; virtual bool isGroup(const QString &category, const QString &memberGroup) const; virtual QMap groupMap(const QString &category) const; virtual QMap inverseMap(const QString &category) const; virtual void renameGroup(const QString &category, const QString &oldName, const QString &newName); virtual void renameCategory(const QString &oldName, const QString &newName); virtual void addGroup(const QString &category, const QString &group); bool canAddMemberToGroup(const QString &category, const QString &group, const QString &item) const { // If there already is a path from item to group then adding the // item to group would create a cycle, which we don't want. return !hasPath(category, item, group); } virtual void addMemberToGroup(const QString &category, const QString &group, const QString &item); virtual void removeMemberFromGroup(const QString &category, const QString &group, const QString &item); virtual const QMap> &memberMap() const { return m_members; } virtual bool hasPath(const QString &category, const QString &from, const QString &to) const; virtual bool contains(const QString &category, const QString &item) const; protected: void calculate() const; QStringList calculateClosure(QMap &resultSoFar, const QString &category, const QString &group) const; public slots: virtual void deleteCategory(const QString &category); virtual void deleteItem(DB::Category *category, const QString &name); virtual void renameItem(DB::Category *category, const QString &oldName, const QString &newName); void setLoading(bool b); signals: void dirty(); private: void markDirty(const QString &category); void regenerateFlatList(const QString &category); // This is the primary data structure // { category |-> { group |-> [ member ] } } <- VDM syntax ;-) QMap> m_members; mutable QMap> m_flatMembers; // These are the data structures used to develop closures, they are only // needed to speed up the program *SIGNIFICANTLY* ;-) mutable bool m_dirty; mutable QMap> m_closureMembers; bool m_loading; }; } #endif /* MEMBERMAP_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/NegationCategoryMatcher.cpp b/DB/NegationCategoryMatcher.cpp index eedf072a..3d8417d6 100644 --- a/DB/NegationCategoryMatcher.cpp +++ b/DB/NegationCategoryMatcher.cpp @@ -1,48 +1,49 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "NegationCategoryMatcher.h" + #include "ImageInfo.h" #include "Logging.h" DB::NegationCategoryMatcher::NegationCategoryMatcher(CategoryMatcher *child) : m_child(child) { Q_ASSERT(m_child); } DB::NegationCategoryMatcher::~NegationCategoryMatcher() { delete m_child; } void DB::NegationCategoryMatcher::setShouldCreateMatchedSet(bool b) { m_child->setShouldCreateMatchedSet(b); } bool DB::NegationCategoryMatcher::eval(ImageInfoPtr info, QMap &alreadyMatched) { return !m_child->eval(info, alreadyMatched); } void DB::NegationCategoryMatcher::debug(int level) const { qCDebug(DBCategoryMatcherLog, "%sNOT:", qPrintable(spaces(level))); m_child->debug(level + 1); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/NewImageFinder.cpp b/DB/NewImageFinder.cpp index 7cf66aab..f857d928 100644 --- a/DB/NewImageFinder.cpp +++ b/DB/NewImageFinder.cpp @@ -1,745 +1,745 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "NewImageFinder.h" + #include "FastDir.h" +#include "ImageDB.h" #include "ImageScout.h" #include "Logging.h" +#include "MD5Map.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 - using namespace DB; /***************************************************************** * * NOTES ON PERFORMANCE * ===== == =========== * * - Robert Krawitz 2018-05-24 * * * GENERAL NOTES ON STORAGE I/O * ------- ----- -- ------- --- * * The two main gates to loading new images are: * * 1) I/O (how fast can we read images off mass storage) * * Different I/O devices have different characteristics in terms of * througput, media latency, and protocol latency. * * - Throughput is the raw speed at which data can be transferred, * limited by the physical and/or electronic characteristics of * the medium and the interface. Short of reducing the amount of * data that's transferred, or clever games with using the most * efficient part of the medium (the outer tracks only for HDD's, * a practice referred to as "short stroking" because it reduces * the distance the head has to seek, at the cost of wasting a * lot of capacity), there's nothing that can be done about this. * * - Media latency is the latency component due to characteristics * of the underlying storage medium. For spinning disks, this is * a function of rotational latency and sek latency. In some * cases, particularly with hard disks, it is possible to reduce * media latency by arranging to access the data in a way that * reduces seeking. See DB/FastDir.cpp for an example of this. * * While media latency can sometimes be hidden by overlapping * I/O, generally not possible to avoid it. Sometimes trying too * hard can actually increase media latency if it results in I/O * operations competing against each other requiring additional * seeks. * * Overlapping I/O with computation is another matter; that can * easily yield benefit, especially if it eliminates rotational * latency. * * - Protocol latency. This refers to things like SATA overhead, * network overhead (for images stored on a network), and so * forth. This can encompass multiple things, and often they can * be pipelined by means of multiple queued I/O operations. For * example, multiple commands can be issued to modern interfaces * (SATA, NVMe) and many network interfaces without waiting for * earlier operations to return. * * If protocol latency is high compared with media latency, * having multiple requests outstanding simultaneously can * yield significant benefits. * * iostat is a valuable tool for investigating throughput and * looking for possible optimizations. The IO/sec and data * read/written per second when compared against known media * characteristics (disk and SSD throughput, network bandwidth) * provides valuable information about whether we're getting close * to full performance from the I/O, and user and system CPU time * give us additional clues about whether we're I/O-bound or * CPU-bound. * * Historically in the computer field, operations that require * relatively simple processing on large volumes of data are I/O * bound. But with very fast I/O devices such as NVMe SSDs, some * of which reach 3 GB/sec, that's not always the case. * * 2) Image (mostly JPEG) loading. * * This is a function of image characteristics and image processing * libraries. Sometimes it's possible to apply parameters to * the underlying image loader to speed it up. This shows up as user * CPU time. Usually the only way to improve this performance * characteristic is to use more or faster CPU cores (sometimes GPUs * can assist here) or use better image loading routines (better * libraries). * * * DESCRIPTION OF KPHOTOALBUM IMAGE LOAD PROCESS * ----------- -- ----------- ----- ---- ------- * * KPhotoAlbum, when it loads an image, performs three processing steps: * * 1) Compute the MD5 checksum * * 2) Extract the Exif metadata * * 3) Generate a thumbnail * * Previous to this round of performance tuning, the first two steps * were performed in the first pass, and thumbnails were generated in * a separate pass. Assuming that the set of new images is large enough * that they cannot all fit in RAM buffers, this results in the I/O * being performed twice. The rewrite results in I/O being performed once. * * In addition, I have made many other changes: * * 1) Prior to the MD5 calculation step, a new thread, called a "scout * thread", reads the files into memory. While this memory is not * directly used in the later computations, it results in the images * being in RAM when they are later needed, making the I/O very fast * (copying data in memory rather than reading it from storage). * * This is a way to overlap I/O with computation. * * 2) The MD5 checksum uses its own I/O to read the data in in larger * chunks than the Qt MD5 routine does. The Qt routine reads it in * in 4KiB chunks; my experimentation has found that 256KiB chunks * are more efficient, even with a scout thread (it reduces the * number of system calls). * * 3) When searching for other images to stack with the image being * loaded, the new image loader no longer attempts to determine * whether other candidate filenames are present, nor does it * compute the MD5 checksum of any such files it does find. Rather, * it only checks for files that are already in KPhotoAlbum, either * previously or as a result of the current load. Merely checking * for the presence of another file is not cheap, and it's not * necessary; if an image will belong to a stack, we'll either know * it now or when other images that can be stacked are loaded. * * 4) The Exif metadata extraction is now done only once; previously * it was performed several times at different stages of the loading * process. * * 5) The thumbnail index is now written out incrementally rather than * the entire index (which can be many megabytes in a large image * database) being rewritten frequently. The index is fully rewritten * prior to exit. * * * BASELINE PERFORMANCE * -------- ----------- * * These measurements were all taken on a Lenovo ThinkPad P70 with 32 * GB of dual-channel DDR4-2400 DRAM, a Xeon E3-1505M CPU (4 cores/8 * total hyperthreads, 2.8-3.7 GHz Skylake; usually runs around * 3.1-3.2 GHz in practice), a Seagate ST2000LM015-2E8174 2TB HDD, and * a Crucial MX300 1TB SATA SSD. Published numbers and measurements I * took otherwise indicate that the HDD can handle about 105-110 * MB/sec with a maximum of 180 IO/sec (in a favorable case). The SSD * is rated to handle 530 MB/sec read, 510 MB/sec write, 92K random * reads/sec, and 83K random writes/sec. * * The image set I used for all measurements, except as noted, * consists of 10839 total files of which about 85% are 20 MP JPEG and * the remainder (with a few exceptions are 20 MP RAW files from a * Canon EOS 7D mkII camera. The total dataset is about 92 GB in * size. * * I baselined both drives by reading the same dataset by means of * * % ls | xargs cat | dd bs=1048576 of=/dev/null * * The HDD required between 850 and 870 seconds (14'10" to 14'30") to * perform this operation, yielding about 105-108 MB/sec. The SSD * achieved about 271 MB/sec, which is well under its rated throughput * (hdparm -Tt yields 355 MB/sec, which is likewise nowhere close to * its rated throughput). hdparm -Tt on the HDD yields about 120 * MB/sec, but throughput to an HDD depends upon which part of the * disk is being read. The outer tracks have a greater angular * density to achieve the same linear density (in other words, the * circumference of an outer track is longer than that of an inner * track, and the data is stored at a constant linear density). So * hdparm isn't very useful on an HDD except as a best case. * * Note also that hdparm does a single stream read from the device. * It does not take advantage of the ability to queue multiple * requests. * * * ANALYSIS OF KPHOTOALBUM LOAD PERFORMANCE * -------- -- ----------- ---- ----------- * * I analyzed the following cases, with images stored both on the * HDD and the SSD: * * 1) Images loaded (All, JPEG only, RAW only) * * B) Thumbnail creation (Including, Excluding) * * C) Scout threads (0, 1, 2, 3) * * The JPG image set constitutes 9293 images totaling about 55 GB. The * JPEG files are mostly 20 MP high quality files, in the range of * 6-10 MB. * The RAW image set constitutes 1544 images totaling about 37 GB. The * RAW files are 20 MP files, in the range of 25 MB. * The ALL set consists of 10839 or 10840 images totaling about 92 GB * (the above set plus 2 .MOV files and in some cases one additional * JPEG file). * * Times are elapsed times; CPU consumption is approximate user+system * CPU consumption. Numbers in parentheses are with thumbnail * building disabled. Note that in the cases with no scout threads on * the SSD the times were reproducibly shorter with thumbnail building * enabled (reasons are not determined at this time). * * Cases building RAW thumbnails generally consumed somewhat more * system CPU (in the range of 10-15%) than JPEG-only cases. This may * be due to custom I/O routines used for generating thumbnails with * JPEG files; RAW files used the I/O provided by libkdcraw, which * uses smaller I/O operations. * * Estimating CPU time for mixed workloads proved very problematic, * as there were significant changes over time. * * Elapsed Time * ------- ---- * * SSD HDD * * JPG - 0 scouts 4:03 (3:59) * JPG - 1 scout 2:46 (2:44) * JPG - 2 scouts 2:20 (2:07) * JPG - 3 scouts 2:21 (1:58) * * ALL - 0 scouts 6:32 (7:03) 16:01 * ALL - 1 scout 4:33 (4:33) 15:01 * ALL - 2 scouts 3:37 (3:28) 16:59 * ALL - 3 scouts 3:36 (3:15) * * RAW - 0 scouts 2:18 (2:46) * RAW - 1 scout 1:46 (1:46) * RAW - 2 scouts 1:17 (1:17) * RAW - 3 scouts 1:13 (1:13) * * User+System CPU * ----------- --- * * SSD HDD * * JPG - 0 scouts 40% (12%) * JPG - 1 scout 70% (20%) * JPG - 2 scouts 85% (15%) * JPG - 3 scouts 85% (15%) * * RAW - 0 scouts 15% (10%) * RAW - 1 scout 18% (12%) * RAW - 2 scouts 25% (15%) * RAW - 3 scouts 25% (15%) * * I also used kcachegrind to measure CPU consumption on smaller * subsets of images (with and without thumbnail creation). In terms * of user CPU consumption, thumbnail creation constitutes the large * majority of CPU cycles for processing JPEG files, followed by MD5 * computation, with Exif parsing lagging far behind. For RAW files, * MD5 computation consumes more cycles, likely in part due to the * larger size of RAW files but possibly also related to the smaller * filesize of embedded thumbnails (on the Canon 7D mkII, the embedded * thumbnail is full size but low quality). * * With thumbnail generation: * ---- --------- ----------- * * RAW JPEG * * Thumbnail generation 44% 82% * libjpeg processing 43% 82% * MD5 computation 51% 13% * Read Exif 1% 1.0% * * Without thumbnail generation: * ------- --------- ----------- * * RAW JPEG * * MD5 computation 92% 80% * Read Exif 4% 10% * * * CONCLUSIONS * ----------- * * For loading files from hard disk (likely the most common case), * there's no reason to consider any loading method other than using a * single scout thread and computing thumbnails concurrently. Even * with thumbnail computation, there is very little CPU utilization. * * Loading from SATA SSD benefits from two scout threads, and possibly * more. For minimal time to regain control, there is some benefit * seen from separating thumbnail generation from the rest of the * processing stages at the cost of more total elapsed time. This is * more evident with JPEG files than with RAW files in this test case. * RAW files typically have smaller thumbnail images which can be * extracted and processed more quickly than full-size JPEG files. On * a slower CPU, it may be desirable to return control to the user * even if the thumbnails are not built yet. * * Two other cases would be NVMe (or other very fast) SSDs and network * storage. Since we're seeing evidence of CPU saturation on SATA * SSDs, we would likely see this even more strongly with NVMe; with * large numbers of images it may be desirable to separate the * thumbnail building from the rest of the processing. It may also be * beneficial to use more scout threads. * * Network storage presents a different problem. It is likely to have * lower throughput -- and certainly much higher latency -- than even * HDD, unless the underlying storage medium is SSD and the data is * located on a very fast, low latency network. So there would be no * benefit to separating thumbnail processing. However, due to * protocol vs. media latency discussed above, it may well work to use * more scout threads. However, this may saturate the network and the * storage, to the detriment of other users, and there's probably no * general (or easily discoverable) optimum for this. * * It's my judgment that most images will be stored on HDDs for at * least the next few years, so tuning for that use case is probably * the best single choice to be made. * *****************************************************************/ namespace { // Number of scout threads for preloading images. More than one scout thread // yields about 10% less performance with higher IO/sec but lower I/O throughput, // most probably due to thrashing. constexpr int IMAGE_SCOUT_THREAD_COUNT = 1; bool canReadImage(const DB::FileName &fileName) { bool fastMode = !Settings::SettingsData::instance()->ignoreFileExtension(); QMimeDatabase::MatchMode mode = fastMode ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault; QMimeDatabase db; QMimeType mimeType = db.mimeTypeForFile(fileName.absolute(), mode); return QImageReader::supportedMimeTypes().contains(mimeType.name().toUtf8()) || ImageManager::ImageDecoder::mightDecode(fileName); } } bool NewImageFinder::findImages() { // Load the information from the XML file. DB::FileNameSet loadedFiles; QElapsedTimer timer; timer.start(); // TODO: maybe the databas interface should allow to query if it // knows about an image ? Here we've to iterate through all of them and it // might be more efficient do do this in the database without fetching the // whole info. for (const DB::FileName &fileName : DB::ImageDB::instance()->images()) { loadedFiles.insert(fileName); } m_pendingLoad.clear(); searchForNewFiles(loadedFiles, Settings::SettingsData::instance()->imageDirectory()); int filesToLoad = m_pendingLoad.count(); loadExtraFiles(); qCDebug(TimingLog) << "Loaded " << filesToLoad << " images in " << timer.elapsed() / 1000.0 << " seconds"; // Man this is not super optimal, but will be changed onces the image finder moves to become a background task. if (MainWindow::FeatureDialog::hasVideoThumbnailer()) { BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::SearchForVideosWithoutVideoThumbnailsJob); } // To avoid deciding if the new images are shown in a given thumbnail view or in a given search // we rather just go to home. return (!m_pendingLoad.isEmpty()); // returns if new images was found. } void NewImageFinder::searchForNewFiles(const DB::FileNameSet &loadedFiles, QString directory) { qApp->processEvents(QEventLoop::AllEvents); directory = Utilities::stripEndingForwardSlash(directory); const QString imageDir = Utilities::stripEndingForwardSlash(Settings::SettingsData::instance()->imageDirectory()); FastDir dir(directory); const QStringList dirList = dir.entryList(); ImageManager::RAWImageDecoder dec; QStringList excluded; excluded << Settings::SettingsData::instance()->excludeDirectories(); excluded = excluded.at(0).split(QString::fromLatin1(",")); bool skipSymlinks = Settings::SettingsData::instance()->skipSymlinks(); // Keep files within a directory more local by processing all files within the // directory, and then all subdirectories. QStringList subdirList; for (QStringList::const_iterator it = dirList.constBegin(); it != dirList.constEnd(); ++it) { const DB::FileName file = DB::FileName::fromAbsolutePath(directory + QString::fromLatin1("/") + *it); if ((*it) == QString::fromLatin1(".") || (*it) == QString::fromLatin1("..") || excluded.contains((*it)) || loadedFiles.contains(file) || dec._skipThisFile(loadedFiles, file) || (*it) == QString::fromLatin1("CategoryImages")) continue; QFileInfo fi(file.absolute()); if (!fi.isReadable()) continue; if (skipSymlinks && fi.isSymLink()) continue; if (fi.isFile()) { if (!DB::ImageDB::instance()->isBlocking(file)) { if (canReadImage(file)) m_pendingLoad.append(qMakePair(file, DB::Image)); else if (Utilities::isVideo(file)) m_pendingLoad.append(qMakePair(file, DB::Video)); } } else if (fi.isDir()) { subdirList.append(file.absolute()); } } for (QStringList::const_iterator it = subdirList.constBegin(); it != subdirList.constEnd(); ++it) searchForNewFiles(loadedFiles, *it); } void NewImageFinder::loadExtraFiles() { // FIXME: should be converted to a threadpool for SMP stuff and whatnot :] QProgressDialog dialog; QElapsedTimer timeSinceProgressUpdate; dialog.setLabelText(i18n("

Loading information from new files

" "

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

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

Calculating checksum for %1 file

", "

Calculating checksums for %1 files

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

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

")); dialog.setMaximum(list.size()); dialog.setMinimumDuration(1000); int count = 0; DB::FileNameList cantRead; bool dirty = false; for (const FileName &fileName : list) { if (count % 10 == 0) { dialog.setValue(count); // ensure to call setProgress(0) qApp->processEvents(QEventLoop::AllEvents); if (dialog.wasCanceled()) { if (wasCanceled) *wasCanceled = true; return dirty; } } MD5 md5 = MD5Sum(fileName); if (md5.isNull()) { cantRead << fileName; continue; } ImageInfoPtr info = ImageDB::instance()->info(fileName); if (info->MD5Sum() != md5) { info->setMD5Sum(md5); dirty = true; ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } md5Map->insert(md5, fileName); ++count; } if (wasCanceled) *wasCanceled = false; if (!cantRead.empty()) KMessageBox::informationList(nullptr, i18n("Following files could not be read:"), cantRead.toStringList(DB::RelativeToImageRoot)); return dirty; } void DB::NewImageFinder::markUnTagged(ImageInfoPtr info) { if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured()) { info->addCategoryInfo(Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag()); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/NoTagCategoryMatcher.cpp b/DB/NoTagCategoryMatcher.cpp index f43984e6..26041889 100644 --- a/DB/NoTagCategoryMatcher.cpp +++ b/DB/NoTagCategoryMatcher.cpp @@ -1,42 +1,43 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "NoTagCategoryMatcher.h" + #include "ImageInfo.h" #include "Logging.h" DB::NoTagCategoryMatcher::NoTagCategoryMatcher(const QString &category) : m_category(category) { } DB::NoTagCategoryMatcher::~NoTagCategoryMatcher() { } bool DB::NoTagCategoryMatcher::eval(ImageInfoPtr info, QMap &alreadyMatched) { Q_UNUSED(alreadyMatched); return info->itemsOfCategory(m_category).isEmpty(); } void DB::NoTagCategoryMatcher::debug(int level) const { qCDebug(DBCategoryMatcherLog) << qPrintable(spaces(level)) << "No Tags for category " << m_category; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/OptimizedFileList.cpp b/DB/OptimizedFileList.cpp index 4045421f..6401fa40 100644 --- a/DB/OptimizedFileList.cpp +++ b/DB/OptimizedFileList.cpp @@ -1,126 +1,127 @@ /* Copyright (C) 2018 Robert Krawitz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "OptimizedFileList.h" + #include "FastDir.h" #include "Logging.h" extern "C" { #include #include #include #include } #include #include #include DB::OptimizedFileList::OptimizedFileList(const QStringList &files) : m_fileList(files) , m_haveOptimizedFiles(false) { optimizeFiles(); } DB::OptimizedFileList::OptimizedFileList(const DB::FileNameList &files) : m_fileList(files.toStringList(DB::AbsolutePath)) , m_haveOptimizedFiles(false) { optimizeFiles(); } QString DB::OptimizedFileList::getDirName(const QString &path) { static const QString pathSep(QString::fromLatin1("/")); int lastChar = path.lastIndexOf(pathSep); if (lastChar <= 0) return QString::fromLatin1("./"); else return path.left(lastChar + 1); } void DB::OptimizedFileList::optimizeFiles() const { if (m_haveOptimizedFiles) return; DirMap dirMap; QStringList dirList; // Map files to directories for (const QString &fileName : m_fileList) { QString dir = getDirName(fileName); if (!dirMap.contains(dir)) { StringSet newDir; dirMap.insert(dir, newDir); dirList << dir; } dirMap[dir] << fileName; } struct stat statbuf; for (const QString &dirName : dirList) { const StringSet &files(dirMap[dirName]); FastDir dir(dirName); QStringList sortedList = dir.sortFileList(files); QString fsName(QString::fromLatin1("NULLFS")); if (stat(QByteArray(QFile::encodeName(dirName)).constData(), &statbuf) == 0) { QCryptographicHash md5calculator(QCryptographicHash::Md5); QByteArray md5Buffer((const char *)&(statbuf.st_dev), sizeof(statbuf.st_dev)); md5calculator.addData(md5Buffer); fsName = QString::fromLatin1(md5calculator.result().toHex()); } if (!m_fsMap.contains(fsName)) { QStringList newList; m_fsMap.insert(fsName, newList); } m_fsMap[fsName] += sortedList; } FSMap tmpFsMap(m_fsMap); while (tmpFsMap.size() > 1) { QStringList filesystemsToRemove; for (FSMap::iterator it = tmpFsMap.begin(); it != tmpFsMap.end(); ++it) { if (it.value().length() > 0) { m_optimizedList.append(it.value().takeFirst()); } else { filesystemsToRemove << it.key(); } } for (const QString &fs : filesystemsToRemove) { tmpFsMap.remove(fs); } } if (tmpFsMap.size() > 0) { QStringList &remainder(tmpFsMap.last()); m_optimizedList += remainder; } // for (QStringList::iterator it = m_optimizedList.begin(); it != m_optimizedList.end(); ++it) { // qDebug() << *it; // } m_haveOptimizedFiles = true; } QStringList DB::OptimizedFileList::optimizedFiles() const { return m_optimizedList; } DB::FileNameList DB::OptimizedFileList::optimizedDbFiles() const { return FileNameList(m_optimizedList); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/OptimizedFileList.h b/DB/OptimizedFileList.h index e3d269d4..09b80913 100644 --- a/DB/OptimizedFileList.h +++ b/DB/OptimizedFileList.h @@ -1,63 +1,64 @@ /* Copyright (C) 2018 Robert Krawitz This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef OPTIMIZEDFILELIST_H #define OPTIMIZEDFILELIST_H -#include +#include "FileNameList.h" + #include #include #include namespace DB { typedef QSet StringSet; typedef QMap DirMap; // Key is MD5 hash of the (opaque) contents of f_fsid typedef QMap FSMap; /** * Provide a list of files optimized by filesystem. * File names are interleaved across all filesystems * with files belonging to them. * * In other words, you can put in a list of files, and get * back a list that is optimized for read performance. */ class OptimizedFileList { public: explicit OptimizedFileList(const DB::FileNameList &files); explicit OptimizedFileList(const QStringList &files); QStringList optimizedFiles() const; DB::FileNameList optimizedDbFiles() const; private: OptimizedFileList(); void optimizeFiles() const; const QStringList m_fileList; mutable QStringList m_optimizedList; mutable bool m_haveOptimizedFiles; mutable FSMap m_fsMap; static QString getDirName(const QString &); }; } #endif /* OPTIMIZEDFILELIST_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/OrCategoryMatcher.cpp b/DB/OrCategoryMatcher.cpp index cdf25a77..6622fd7d 100644 --- a/DB/OrCategoryMatcher.cpp +++ b/DB/OrCategoryMatcher.cpp @@ -1,37 +1,38 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "OrCategoryMatcher.h" + #include "ImageInfo.h" #include "Logging.h" bool DB::OrCategoryMatcher::eval(ImageInfoPtr info, QMap &alreadyMatched) { Q_FOREACH (CategoryMatcher *subMatcher, mp_elements) { if (subMatcher->eval(info, alreadyMatched)) return true; } return false; } void DB::OrCategoryMatcher::debug(int level) const { qCDebug(DBCategoryMatcherLog, "%sOR:", qPrintable(spaces(level))); ContainerCategoryMatcher::debug(level + 1); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/UIDelegate.cpp b/DB/UIDelegate.cpp index 762cef7b..47600688 100644 --- a/DB/UIDelegate.cpp +++ b/DB/UIDelegate.cpp @@ -1,52 +1,53 @@ /* Copyright (C) 2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e. V. (or its successor approved by the membership of KDE e. V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "UIDelegate.h" + #include "Logging.h" #include DB::UserFeedback DB::UIDelegate::warningContinueCancel(const QString &logMessage, const QString &msg, const QString &title, const QString &dialogId) { qCWarning(DBLog) << logMessage; return askWarningContinueCancel(msg, title, dialogId); } DB::UserFeedback DB::UIDelegate::questionYesNo(const QString &logMessage, const QString &msg, const QString &title, const QString &dialogId) { qCInfo(DBLog) << logMessage; return askQuestionYesNo(msg, title, dialogId); } void DB::UIDelegate::information(const QString &logMessage, const QString &msg, const QString &title, const QString &dialogId) { qCInfo(DBLog) << logMessage; showInformation(msg, title, dialogId); } void DB::UIDelegate::sorry(const QString &logMessage, const QString &msg, const QString &title, const QString &dialogId) { qCWarning(DBLog) << logMessage; showSorry(msg, title, dialogId); } void DB::UIDelegate::error(const QString &logMessage, const QString &msg, const QString &title, const QString &dialogId) { qCCritical(DBLog) << logMessage; showError(msg, title, dialogId); } diff --git a/DB/ValueCategoryMatcher.cpp b/DB/ValueCategoryMatcher.cpp index e7bb2547..27f4f8f7 100644 --- a/DB/ValueCategoryMatcher.cpp +++ b/DB/ValueCategoryMatcher.cpp @@ -1,58 +1,59 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ValueCategoryMatcher.h" + #include "ImageDB.h" #include "Logging.h" #include "MemberMap.h" void DB::ValueCategoryMatcher::debug(int level) const { qCDebug(DBCategoryMatcherLog, "%s%s: %s", qPrintable(spaces(level)), qPrintable(m_category), qPrintable(m_option)); } DB::ValueCategoryMatcher::ValueCategoryMatcher(const QString &category, const QString &value) { // Unescape doubled "&"s and restore the original value QString unEscapedValue = value; unEscapedValue.replace(QString::fromUtf8("&&"), QString::fromUtf8("&")); m_category = category; m_option = unEscapedValue; const MemberMap &map = DB::ImageDB::instance()->memberMap(); const QStringList members = map.members(m_category, m_option, true); m_members = members.toSet(); } bool DB::ValueCategoryMatcher::eval(ImageInfoPtr info, QMap &alreadyMatched) { // Only add the tag _option to the alreadyMatched tags, // and omit the tags in _members if (m_shouldPrepareMatchedSet) alreadyMatched[m_category].insert(m_option); if (info->hasCategoryInfo(m_category, m_option)) { return true; } if (info->hasCategoryInfo(m_category, m_members)) return true; return false; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DateBar/DateBarWidget.cpp b/DateBar/DateBarWidget.cpp index 71ad9687..5780be9d 100644 --- a/DateBar/DateBarWidget.cpp +++ b/DateBar/DateBarWidget.cpp @@ -1,893 +1,892 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "MouseHandler.h" + +#include +#include +#include #include #include #include #include #include #include #include #include #include #include - -#include - -#include "MouseHandler.h" -#include "Settings/SettingsData.h" -#include +#include namespace { constexpr int BORDER_ABOVE_HISTOGRAM = 4; constexpr int BORDER_AROUND_WIDGET = 0; constexpr int BUTTON_WIDTH = 22; constexpr int ARROW_LENGTH = 20; constexpr int SCROLL_AMOUNT = 1; constexpr int SCROLL_ACCELERATION = 10; } /** * \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"))); m_zoomIn->setToolTip(i18n("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"))); m_zoomOut->setToolTip(i18n("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::fromTheme(QStringLiteral("edit-clear"))); connect(m_cancelSelection, SIGNAL(clicked()), this, SLOT(clearSelection())); m_cancelSelection->setEnabled(false); m_cancelSelection->setToolTip(i18nc("The button clears the selection of a date range in the date bar.", "Clear date selection")); placeAndSizeButtons(); m_focusItemDragHandler = new FocusItemDragHandler(this); m_barDragHandler = new BarDragHandler(this); m_selectionHandler = new SelectionHandler(this); setWhatsThis(xi18nc("@info", "The date bar" "" "Scroll using the arrow buttons, the scrollwheel, or the middle mouse button." "Zoom using the +/- buttons or Ctrl + scrollwheel." "Restrict the view to a date range selection: Click/drag below the timeline." "Jump to a date by clicking on the histogram bar." "")); setToolTip(whatsThis()); connect(Settings::SettingsData::instance(), &Settings::SettingsData::histogramScaleChanged, this, &DateBarWidget::redraw); } QSize DateBar::DateBarWidget::sizeHint() const { int height = qMax(dateAreaGeometry().bottom() + BORDER_AROUND_WIDGET, m_barHeight + BUTTON_WIDTH + 2 * BORDER_AROUND_WIDGET + 7); return QSize(800, height); } QSize DateBar::DateBarWidget::minimumSizeHint() const { int height = qMax(dateAreaGeometry().bottom() + BORDER_AROUND_WIDGET, m_barHeight + BUTTON_WIDTH + 2 * BORDER_AROUND_WIDGET + 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() + BUTTON_WIDTH + 2); drawTickMarks(p, rect); drawHistograms(p); drawFocusRectangle(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, bool redrawNow) { setViewHandlerForType(tp); if (redrawNow) 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; case TenMinuteView: m_currentHandler = &m_tenMinuteViewHandler; break; case MinuteView: m_currentHandler = &m_minuteViewHandler; 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(MinuteView, false); 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); // determine maximum image count within visible units int max = 0; for (int unit = 0; unit <= numberOfUnits(); unit++) { 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; } } int unit = 0; const bool linearScale = Settings::SettingsData::instance()->histogramUseLinearScale(); for (int x = rect.x(); x + m_barWidth < rect.right(); x += m_barWidth, unit += 1) { const DB::ImageCount count = m_dates->count(rangeForUnit(unit)); int exactPx = 0; int rangePx = 0; if (max != 0) { double exactScaled; double rangeScaled; if (linearScale) { exactScaled = (double)count.mp_exact / max; rangeScaled = (double)count.mp_rangeMatch / max; } else { exactScaled = sqrt(count.mp_exact) / sqrt(max); rangeScaled = sqrt(count.mp_rangeMatch) / sqrt(max); } // convert to pixels: exactPx = (int)((double)(rect.height() - 2) * exactScaled); if (m_includeFuzzyCounts) rangePx = (int)((double)(rect.height() - 2) * rangeScaled); } Qt::BrushStyle style = Qt::SolidPattern; if (!isUnitSelected(unit) && hasSelection()) style = Qt::Dense5Pattern; p.setBrush(QBrush(Qt::yellow, style)); p.drawRect(x + 1, rect.bottom() - rangePx, m_barWidth - 2, rangePx); p.setBrush(QBrush(Qt::green, style)); p.drawRect(x + 1, rect.bottom() - rangePx - exactPx, m_barWidth - 2, exactPx); // 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 < exactPx + rangePx - 2) { p.setPen(Qt::black); p.drawText(0, 0, QString::number(tot)); } p.restore(); } } p.restore(); } void DateBar::DateBarWidget::scrollLeft() { int scrollAmount = -SCROLL_AMOUNT; if (QGuiApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) scrollAmount *= SCROLL_ACCELERATION; scroll(scrollAmount); } void DateBar::DateBarWidget::scrollRight() { int scrollAmount = SCROLL_AMOUNT; if (QGuiApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) scrollAmount *= SCROLL_ACCELERATION; scroll(scrollAmount); } void DateBar::DateBarWidget::scroll(int units) { m_currentDate = dateForUnit(units, m_currentDate); redraw(); emit dateSelected(currentDateRange(), includeFuzzyCounts()); } void DateBar::DateBarWidget::drawFocusRectangle(QPainter &p) { QRect rect = barAreaGeometry(); p.save(); int x = rect.left() + m_currentUnit * m_barWidth; QRect inner(QPoint(x - 1, BORDER_ABOVE_HISTOGRAM), QPoint(x + m_barWidth, BORDER_ABOVE_HISTOGRAM + 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 == MinuteView) return; zoom(+1); } void DateBar::DateBarWidget::zoomOut() { if (m_tp == DecadeView) return; zoom(-1); } void DateBar::DateBarWidget::zoom(int steps) { ViewType tp = (ViewType)(m_tp + steps); setViewType(tp); emit canZoomIn(tp != MinuteView); 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) { showStatusBarTip(event->pos()); if (m_currentMouseHandler == nullptr) return; 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(BORDER_AROUND_WIDGET, BORDER_ABOVE_HISTOGRAM)); barArea.setRight(width() - BORDER_AROUND_WIDGET - 2 * BUTTON_WIDTH - 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 - ARROW_LENGTH - 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 - ARROW_LENGTH, midLine), QPoint(startUnitPos, midLine)); drawArrow(p, QPoint(endUnitPos + ARROW_LENGTH, 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 + ARROW_LENGTH + 3, rect.top(), fm.width(text), fm.height(), Qt::TextSingleLine, text); p.restore(); *leftEdge = startUnitPos - ARROW_LENGTH - 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(BUTTON_WIDTH, BUTTON_WIDTH); m_zoomOut->setFixedSize(BUTTON_WIDTH, BUTTON_WIDTH); m_rightArrow->setFixedSize(QSize(BUTTON_WIDTH, m_barHeight)); m_leftArrow->setFixedSize(QSize(BUTTON_WIDTH, m_barHeight)); m_rightArrow->move(size().width() - m_rightArrow->width() - BORDER_AROUND_WIDGET, BORDER_ABOVE_HISTOGRAM); m_leftArrow->move(m_rightArrow->pos().x() - m_leftArrow->width() - 2, BORDER_ABOVE_HISTOGRAM); int x = m_leftArrow->pos().x(); int y = height() - BUTTON_WIDTH; m_zoomOut->move(x, y); x = m_rightArrow->pos().x(); m_zoomIn->move(x, y); m_cancelSelection->setFixedSize(BUTTON_WIDTH, BUTTON_WIDTH); m_cancelSelection->move(0, y); } void DateBar::DateBarWidget::keyPressEvent(QKeyEvent *event) { int offset = 0; if (event->key() == Qt::Key_Plus) { if (m_tp != MinuteView) 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 { Q_ASSERT_X(x - barAreaGeometry().left() >= 0, "DateBarWidget::unitAtPos", "horizontal offset cannot be negative!"); Q_ASSERT_X(x - barAreaGeometry().left() <= m_barWidth, "DateBarWidget::unitAtPos", "horizontal offset larger than m_barWidth!"); 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; } int scrollAmount = e->delta() > 0 ? SCROLL_AMOUNT : -SCROLL_AMOUNT; if (e->modifiers() & Qt::ShiftModifier) scrollAmount *= SCROLL_ACCELERATION; scroll(scrollAmount); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DateBar/DateBarWidget.h b/DateBar/DateBarWidget.h index 07a2abcd..2c1f60ed 100644 --- a/DateBar/DateBarWidget.h +++ b/DateBar/DateBarWidget.h @@ -1,232 +1,232 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef DATEBAR_H #define DATEBAR_H +#include "ViewHandler.h" + #include #include #include #include -#include - class QMenu; class QKeyEvent; class QMouseEvent; class QFocusEvent; class QResizeEvent; class QPaintEvent; class QWheelEvent; class QContextMenuEvent; class QToolButton; namespace DB { class ImageDateCollection; class ImageDate; } namespace DateBar { class MouseHandler; class FocusItemDragHandler; class BarDragHandler; class SelectionHandler; /** * @brief The DateBarWidget class provides a histogram-like depiction of the image distribution over time. * If \ref includeFuzzyCounts() is \c true, * then both exact and fuzzy dates are taken into account and shown in different style (currently yellow and green). * If enough space is available, the number of images within each time period is printed inside each box. * * ## "unit" concept * A central concept in the widget design is that of a time \c unit. * Units are an integer offset into the number of available "boxes". * The number of units (or boxes) is calculated according to the available space (see \ref numberOfUnits()). * Each unit corresponds to a time period at the current resolution and offset. * * The time resulution is represented by the \c ViewHandler, and the offset is stored in \c m_currentDate. * */ class DateBarWidget : public QWidget { Q_OBJECT public: explicit DateBarWidget(QWidget *parent); enum ViewType { DecadeView, YearView, MonthView, WeekView, DayView, HourView, TenMinuteView, MinuteView }; /** * @brief includeFuzzyCounts * @return \c true if date ranges are shown, \c false otherwise. * @see setIncludeFuzzyCounts */ bool includeFuzzyCounts() const; public slots: void clearSelection(); void setViewType(ViewType tp, bool redrawNow = true); void setDate(const QDateTime &date); void setImageDateCollection(const QExplicitlySharedDataPointer &); void scrollLeft(); void scrollRight(); void scroll(int units); void zoomIn(); void zoomOut(); void setHistogramBarSize(const QSize &size); void setIncludeFuzzyCounts(bool); /** * @brief setShowResolutionIndicator * If set to \c true, an indicator is shown to indicate the current ViewType. * The indicator indicates the size of one unit / histogram box alongside a text * indicating the respective temporal resolution (e.g. "10 minutes"). */ void setShowResolutionIndicator(bool); /** * @brief setAutomaticRangeAdjustment * If set to \c true, the ViewType and range is adjusted automatically to best * match the current set of images. */ void setAutomaticRangeAdjustment(bool); signals: void canZoomIn(bool); void canZoomOut(bool); void dateSelected(const DB::ImageDate &, bool includeRanges); void toolTipInfo(const QString &); void dateRangeChange(const DB::ImageDate &); void dateRangeCleared(); public: // Overridden methods for internal purpose QSize sizeHint() const override; QSize minimumSizeHint() const override; protected: void paintEvent(QPaintEvent *event) override; void resizeEvent(QResizeEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void contextMenuEvent(QContextMenuEvent *) override; void keyPressEvent(QKeyEvent *event) override; void focusInEvent(QFocusEvent *) override; void focusOutEvent(QFocusEvent *) override; void wheelEvent(QWheelEvent *e) override; /** * @brief redraw the widget * This method creates a QPainter and then uses the draw* methods to draw the different parts of the widget. * \see drawTickMarks * \see drawHistograms * \see drawFocusRectangle * \see drawResolutionIndicator */ void redraw(); void drawTickMarks(QPainter &p, const QRect &textRect); void drawHistograms(QPainter &p); void drawFocusRectangle(QPainter &p); void drawResolutionIndicator(QPainter &p, int *leftEdge); /** * @brief zoom in or out by a number of steps. * One steps corresponds to one step in the ViewType. * @param steps positive steps to increase temporal resolution, negative to decrease. */ void zoom(int steps); QRect barAreaGeometry() const; QRect tickMarkGeometry() const; QRect dateAreaGeometry() const; int numberOfUnits() const; void drawArrow(QPainter &, const QPoint &start, const QPoint &end); void updateArrowState(); DB::ImageDate currentDateRange() const; void showStatusBarTip(const QPoint &pos); DB::ImageDate rangeAt(const QPoint &); DB::ImageDate rangeForUnit(int unit); void placeAndSizeButtons(); /** * @brief unitAtPos maps horizontal screen coordinates to units. * @param x a valid pixel offset in the histogram area * @return a unit index between 0 and numberOfUnits */ int unitAtPos(int x) const; QDateTime dateForUnit(int unit, const QDateTime &offset = QDateTime()) const; /** * @brief unitForDate return the unit index corresponding to the date/time. * @param date a valid QDateTime. * @return An integer greater or equal to 0 if \p date is in view, -1 otherwise. */ int unitForDate(const QDateTime &date) const; bool isUnitSelected(int unit) const; bool hasSelection() const; DB::ImageDate currentSelection() const; void emitDateSelected(); void emitRangeSelection(const DB::ImageDate &); private: void setViewHandlerForType(ViewType tp); QPixmap m_buffer; friend class DateBarTip; QExplicitlySharedDataPointer m_dates; DecadeViewHandler m_decadeViewHandler; YearViewHandler m_yearViewHandler; MonthViewHandler m_monthViewHandler; WeekViewHandler m_weekViewHandler; DayViewHandler m_dayViewHandler; HourViewHandler m_hourViewHandler; TenMinuteViewHandler m_tenMinuteViewHandler; MinuteViewHandler m_minuteViewHandler; ViewHandler *m_currentHandler; ViewType m_tp; MouseHandler *m_currentMouseHandler; FocusItemDragHandler *m_focusItemDragHandler; BarDragHandler *m_barDragHandler; SelectionHandler *m_selectionHandler; friend class Handler; friend class FocusItemDragHandler; friend class BarDragHandler; friend class SelectionHandler; QToolButton *m_rightArrow; QToolButton *m_leftArrow; QToolButton *m_zoomIn; QToolButton *m_zoomOut; QToolButton *m_cancelSelection; int m_currentUnit; QDateTime m_currentDate; int m_barWidth; int m_barHeight; bool m_includeFuzzyCounts; QMenu *m_contextMenu; bool m_showResolutionIndicator; bool m_doAutomaticRangeAdjustment; }; } #endif /* DATEBAR_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DateBar/MouseHandler.cpp b/DateBar/MouseHandler.cpp index 7ee4d841..d981099a 100644 --- a/DateBar/MouseHandler.cpp +++ b/DateBar/MouseHandler.cpp @@ -1,212 +1,215 @@ /* 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 "DateBarWidget.h" + #include + #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(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DateBar/MouseHandler.h b/DateBar/MouseHandler.h index 806cfe47..be87f0b8 100644 --- a/DateBar/MouseHandler.h +++ b/DateBar/MouseHandler.h @@ -1,95 +1,96 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef DATEBARMOUSEHANDLER_H #define DATEBARMOUSEHANDLER_H -#include "DB/ImageDate.h" +#include + #include #include namespace DB { class ImageDate; } class QTimer; namespace DateBar { class DateBarWidget; class MouseHandler : public QObject { Q_OBJECT public: explicit MouseHandler(DateBarWidget *dateBar); virtual void mousePressEvent(int x) = 0; virtual void mouseMoveEvent(int x) = 0; virtual void mouseReleaseEvent() {}; void startAutoScroll(); void endAutoScroll(); protected slots: void autoScroll(); protected: DateBarWidget *m_dateBar; private: QTimer *m_autoScrollTimer; }; class FocusItemDragHandler : public MouseHandler { public: explicit FocusItemDragHandler(DateBarWidget *dateBar); void mousePressEvent(int x) override; void mouseMoveEvent(int x) override; }; class BarDragHandler : public MouseHandler { public: explicit BarDragHandler(DateBarWidget *); void mousePressEvent(int x) override; void mouseMoveEvent(int x) override; private: int m_movementOffset; }; class SelectionHandler : public MouseHandler { public: explicit SelectionHandler(DateBarWidget *); void mousePressEvent(int x) override; void mouseMoveEvent(int x) override; void mouseReleaseEvent() override; QDateTime min() const; QDateTime max() const; DB::ImageDate dateRange() const; void clearSelection(); bool hasSelection() const; private: QDateTime m_start; QDateTime m_end; }; } #endif /* DATEBARMOUSEHANDLER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DateBar/ViewHandler.cpp b/DateBar/ViewHandler.cpp index ffe841bd..f756f7d4 100644 --- a/DateBar/ViewHandler.cpp +++ b/DateBar/ViewHandler.cpp @@ -1,365 +1,363 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ViewHandler.h" -#include - -#include - #include +#include +#include using namespace DateBar; /** * \class DateBar::ViewHandler * \brief Base class for classes handling logic regarding individual bars in the datebar. * * A major part of the date bar is figuring out which date interval a given bar represent, this class is taking care of this. */ /** * Indicate that the first unit in the bar represent the date given as parameter */ void ViewHandler::init(const QDateTime &startDate) { m_startDate = startDate; } /** * Returns whether this unit is a major unit (like January in year view, or Monday in week view) * This information is used to draw a longer line in the date bar for the given unit. */ bool DateBar::ViewHandler::isMajorUnit(int) { // included for documentation return true; } /** * Returns whether this is a mid unit (like 30 minute in hour view) * This information is used to draw a slightly longer line in the date bar for the given unit. */ bool ViewHandler::isMidUnit(int /*unit*/) { return false; } /** * Returns the text to be shown for the given unit. This method will only be call for major units. */ QString DateBar::ViewHandler::text(int) { // Included for documentation. return QString(); } /** * Returns the length of one unit (to be shown at the right of the date bar) */ QString DateBar::ViewHandler::unitText() const { // Included for documentation. return QString(); } /** * Return the date for the beginning of the unit given as the first argument. If the second optional argument is * given, then this is used as the date for the first unit, otherwise the date given to \ref init will be used as offset. */ QDateTime DateBar::ViewHandler::date(int, QDateTime) { // Included for documentation. return QDateTime(); } void DecadeViewHandler::init(const QDateTime &startDate) { QDateTime date = QDateTime(QDate(startDate.date().year(), 1, 1), QTime(0, 0, 0)); ViewHandler::init(date); } bool DecadeViewHandler::isMajorUnit(int unit) { return date(unit).date().year() % 10 == 0; } bool DecadeViewHandler::isMidUnit(int unit) { return date(unit).date().year() % 5 == 0; } QString DecadeViewHandler::text(int unit) { return QString::number(date(unit).date().year()); } QDateTime DecadeViewHandler::date(int unit, QDateTime reference) { if (reference.isNull()) reference = m_startDate; return reference.addMonths(12 * unit); } QString DecadeViewHandler::unitText() const { return i18n("1 Year"); } void YearViewHandler::init(const QDateTime &startDate) { QDateTime date = QDateTime(QDate(startDate.date().year(), startDate.date().month(), 1), QTime(0, 0, 0)); ViewHandler::init(date); } bool YearViewHandler::isMajorUnit(int unit) { return date(unit).date().month() == 1; } bool YearViewHandler::isMidUnit(int unit) { return date(unit).date().month() == 7; } QString YearViewHandler::text(int unit) { return QString::number(date(unit).date().year()); } QDateTime YearViewHandler::date(int unit, QDateTime reference) { if (reference.isNull()) reference = m_startDate; return reference.addMonths(unit); } QString YearViewHandler::unitText() const { return i18n("1 Month"); } void MonthViewHandler::init(const QDateTime &startDate) { QDate date = startDate.date().addDays(-startDate.date().dayOfWeek() + 1); // Wind to monday ViewHandler::init(QDateTime(date, QTime(0, 0, 0))); } bool MonthViewHandler::isMajorUnit(int unit) { return date(unit).date().day() <= 7; } QString MonthViewHandler::text(int unit) { static int lastunit = 99999; static int printedLast = false; if (unit < lastunit) printedLast = true; QString str; if (!printedLast) str = QLocale().toString(date(unit).date(), QLocale::ShortFormat); printedLast = !printedLast; lastunit = unit; return str; } QDateTime MonthViewHandler::date(int unit, QDateTime reference) { if (reference.isNull()) reference = m_startDate; return reference.addDays(7 * unit); } QString MonthViewHandler::unitText() const { return i18n("1 Week"); } void WeekViewHandler::init(const QDateTime &startDate) { ViewHandler::init(QDateTime(startDate.date(), QTime(0, 0, 0))); } bool WeekViewHandler::isMajorUnit(int unit) { return date(unit).date().dayOfWeek() == 1; } QString WeekViewHandler::text(int unit) { return QLocale().toString(date(unit).date(), QLocale::ShortFormat); } QDateTime WeekViewHandler::date(int unit, QDateTime reference) { if (reference.isNull()) reference = m_startDate; return reference.addDays(unit); } QString WeekViewHandler::unitText() const { return i18n("1 Day"); } void DayViewHandler::init(const QDateTime &startDate) { QDateTime date = startDate; if (date.time().hour() % 2) date = date.addSecs(60 * 60); ViewHandler::init(QDateTime(date.date(), QTime(date.time().hour(), 0, 0))); } bool DayViewHandler::isMajorUnit(int unit) { int h = date(unit).time().hour(); return h == 0 || h == 12; } bool DayViewHandler::isMidUnit(int unit) { int h = date(unit).time().hour(); return h == 6 || h == 18; } QString DayViewHandler::text(int unit) { if (date(unit).time().hour() == 0) return QLocale().toString(date(unit).date(), QLocale::ShortFormat); else return date(unit).toString(QString::fromLatin1("h:00")); } QDateTime DayViewHandler::date(int unit, QDateTime reference) { if (reference.isNull()) reference = m_startDate; return reference.addSecs(2 * 60 * 60 * unit); } QString DayViewHandler::unitText() const { return i18n("2 Hours"); } void HourViewHandler::init(const QDateTime &startDate) { ViewHandler::init(QDateTime(startDate.date(), QTime(startDate.time().hour(), 10 * (int)floor(startDate.time().minute() / 10.0), 0))); } bool HourViewHandler::isMajorUnit(int unit) { return date(unit).time().minute() == 0; } bool HourViewHandler::isMidUnit(int unit) { int min = date(unit).time().minute(); return min == 30; } QString HourViewHandler::text(int unit) { return date(unit).toString(QString::fromLatin1("h:00")); } QDateTime HourViewHandler::date(int unit, QDateTime reference) { if (reference.isNull()) reference = m_startDate; return reference.addSecs(60 * 10 * unit); } QString HourViewHandler::unitText() const { return i18n("10 Minutes"); } void TenMinuteViewHandler::init(const QDateTime &startDate) { ViewHandler::init(QDateTime(startDate.date(), QTime(startDate.time().hour(), 10 * (int)floor(startDate.time().minute() / 10.0), 0))); } bool TenMinuteViewHandler::isMajorUnit(int unit) { return (date(unit).time().minute() % 10) == 0; } bool TenMinuteViewHandler::isMidUnit(int unit) { int min = date(unit).time().minute(); return (min % 10) == 5; } QString TenMinuteViewHandler::text(int unit) { return date(unit).toString(QString::fromLatin1("h:mm")); } QDateTime TenMinuteViewHandler::date(int unit, QDateTime reference) { if (reference.isNull()) reference = m_startDate; return reference.addSecs(60 * unit); } QString TenMinuteViewHandler::unitText() const { return i18n("1 Minute"); } void MinuteViewHandler::init(const QDateTime &startDate) { ViewHandler::init(QDateTime(startDate.date(), QTime(startDate.time().hour(), startDate.time().minute(), 0))); } bool MinuteViewHandler::isMajorUnit(int unit) { return date(unit).time().second() == 0; } bool MinuteViewHandler::isMidUnit(int unit) { int sec = date(unit).time().second(); return sec == 30; } QString MinuteViewHandler::text(int unit) { return date(unit).toString(QString::fromLatin1("h:mm")); } QDateTime MinuteViewHandler::date(int unit, QDateTime reference) { if (reference.isNull()) reference = m_startDate; return reference.addSecs(10 * unit); } QString MinuteViewHandler::unitText() const { return i18n("10 Seconds"); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/Database.cpp b/Exif/Database.cpp index 0a86345d..94f608ed 100644 --- a/Exif/Database.cpp +++ b/Exif/Database.cpp @@ -1,653 +1,652 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Database.h" + +#include "DatabaseElement.h" #include "Logging.h" -#include "DB/ImageDB.h" -#include "Exif/DatabaseElement.h" -#include "MainWindow/Window.h" -#include "Settings/SettingsData.h" - -#include -#include +#include +#include +#include #include -#include - #include #include #include #include #include #include #include #include +#include +#include +#include using namespace Exif; namespace { // schema version; bump it up whenever the database schema changes constexpr int DB_VERSION = 3; const Database::ElementList elements(int since = 0) { static Database::ElementList elms; static int sinceDBVersion[DB_VERSION] {}; if (elms.count() == 0) { elms.append(new RationalExifElement("Exif.Photo.FocalLength")); elms.append(new RationalExifElement("Exif.Photo.ExposureTime")); elms.append(new RationalExifElement("Exif.Photo.ApertureValue")); elms.append(new RationalExifElement("Exif.Photo.FNumber")); //elms.append( new RationalExifElement( "Exif.Photo.FlashEnergy" ) ); elms.append(new IntExifElement("Exif.Photo.Flash")); elms.append(new IntExifElement("Exif.Photo.Contrast")); elms.append(new IntExifElement("Exif.Photo.Sharpness")); elms.append(new IntExifElement("Exif.Photo.Saturation")); elms.append(new IntExifElement("Exif.Image.Orientation")); elms.append(new IntExifElement("Exif.Photo.MeteringMode")); elms.append(new IntExifElement("Exif.Photo.ISOSpeedRatings")); elms.append(new IntExifElement("Exif.Photo.ExposureProgram")); elms.append(new StringExifElement("Exif.Image.Make")); elms.append(new StringExifElement("Exif.Image.Model")); // gps info has been added in database schema version 2: sinceDBVersion[1] = elms.size(); elms.append(new IntExifElement("Exif.GPSInfo.GPSVersionID")); // actually a byte value elms.append(new RationalExifElement("Exif.GPSInfo.GPSAltitude")); elms.append(new IntExifElement("Exif.GPSInfo.GPSAltitudeRef")); // actually a byte value elms.append(new StringExifElement("Exif.GPSInfo.GPSMeasureMode")); elms.append(new RationalExifElement("Exif.GPSInfo.GPSDOP")); elms.append(new RationalExifElement("Exif.GPSInfo.GPSImgDirection")); elms.append(new RationalExifElement("Exif.GPSInfo.GPSLatitude")); elms.append(new StringExifElement("Exif.GPSInfo.GPSLatitudeRef")); elms.append(new RationalExifElement("Exif.GPSInfo.GPSLongitude")); elms.append(new StringExifElement("Exif.GPSInfo.GPSLongitudeRef")); elms.append(new RationalExifElement("Exif.GPSInfo.GPSTimeStamp")); // lens info has been added in database schema version 3: sinceDBVersion[2] = elms.size(); elms.append(new LensExifElement()); } // query only for the newly added stuff: if (since > 0) return elms.mid(sinceDBVersion[since]); return elms; } } Exif::Database *Exif::Database::s_instance = nullptr; /** * @brief show and error message for the failed \p query and disable the Exif database. * The database is closed because at this point we can not trust the data inside. * @param query */ void Database::showErrorAndFail(QSqlQuery &query) const { const QString txt = i18n("

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

" "

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

" "
" "

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

" "

The error message obtained was:
%2

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

There was an error while opening the Exif search database.

" "

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

" "
" "

The error message obtained was:
%1

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

The Exif search database is corrupted and has no data.

" "

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

"); const QString logMsg = QString::fromUtf8("Database open but empty!"); showErrorAndFail(txt, logMsg); return; } const int version = DBFileVersion(); if (m_isFailed) return; if (version < DBVersion()) { // on the next update, we can just query the DB Version createMetadataTable(SchemaChanged); } // update schema if (version < DBVersion()) { QSqlQuery query(m_db); for (const DatabaseElement *e : elements(version)) { query.prepare(QString::fromLatin1("alter table exif add column %1") .arg(e->createString())); if (!query.exec()) showErrorAndFail(query); } } } void Exif::Database::createMetadataTable(DBSchemaChangeType change) { QSqlQuery query(m_db); query.prepare(QString::fromLatin1("create table if not exists settings (keyword TEXT PRIMARY KEY, value TEXT) without rowid")); if (!query.exec()) { showErrorAndFail(query); return; } query.prepare(QString::fromLatin1("insert or replace into settings (keyword, value) values('DBVersion','%1')").arg(Database::DBVersion())); if (!query.exec()) { showErrorAndFail(query); return; } if (change == SchemaAndDataChanged) { query.prepare(QString::fromLatin1("insert or replace into settings (keyword, value) values('GuaranteedDataVersion','%1')").arg(Database::DBVersion())); if (!query.exec()) showErrorAndFail(query); } } bool Exif::Database::add(const DB::FileName &fileName) { if (!isUsable()) return false; try { Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data()); Q_ASSERT(image.get() != nullptr); image->readMetadata(); Exiv2::ExifData &exifData = image->exifData(); return insert(fileName, exifData); } catch (...) { qCWarning(ExifLog, "Error while reading exif information from %s", qPrintable(fileName.absolute())); return false; } } bool Exif::Database::add(DB::FileInfo &fileInfo) { if (!isUsable()) return false; return insert(fileInfo.getFileName(), fileInfo.getExifData()); } bool Exif::Database::add(const DB::FileNameList &list) { if (!isUsable()) return false; QList map; Q_FOREACH (const DB::FileName &fileName, list) { try { Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fileName.absolute().toLocal8Bit().data()); Q_ASSERT(image.get() != nullptr); image->readMetadata(); map << DBExifInfo(fileName, image->exifData()); } catch (...) { qWarning("Error while reading exif information from %s", qPrintable(fileName.absolute())); } } insert(map); return true; } void Exif::Database::remove(const DB::FileName &fileName) { if (!isUsable()) return; QSqlQuery query(m_db); query.prepare(QString::fromLatin1("DELETE FROM exif WHERE fileName=?")); query.bindValue(0, fileName.absolute()); if (!query.exec()) showErrorAndFail(query); } void Exif::Database::remove(const DB::FileNameList &list) { if (!isUsable()) return; m_db.transaction(); QSqlQuery query(m_db); query.prepare(QString::fromLatin1("DELETE FROM exif WHERE fileName=?")); Q_FOREACH (const DB::FileName &fileName, list) { query.bindValue(0, fileName.absolute()); if (!query.exec()) { m_db.rollback(); showErrorAndFail(query); return; } } m_db.commit(); } QSqlQuery *Exif::Database::getInsertQuery() { if (!isUsable()) return nullptr; if (m_insertTransaction) return m_insertTransaction; if (m_queryString.isEmpty()) { QStringList formalList; Database::ElementList elms = elements(); for (const DatabaseElement *e : elms) { formalList.append(e->queryString()); } m_queryString = QString::fromLatin1("INSERT OR REPLACE into exif values (?, %1) ").arg(formalList.join(QString::fromLatin1(", "))); } QSqlQuery *query = new QSqlQuery(m_db); if (query) query->prepare(m_queryString); return query; } void Exif::Database::concludeInsertQuery(QSqlQuery *query) { if (m_insertTransaction) return; m_db.commit(); delete query; } bool Exif::Database::startInsertTransaction() { Q_ASSERT(m_insertTransaction == nullptr); m_insertTransaction = getInsertQuery(); m_db.transaction(); return (m_insertTransaction != nullptr); } bool Exif::Database::commitInsertTransaction() { if (m_insertTransaction) { m_db.commit(); delete m_insertTransaction; m_insertTransaction = nullptr; } else qCWarning(ExifLog, "Trying to commit transaction, but no transaction is active!"); return true; } bool Exif::Database::abortInsertTransaction() { if (m_insertTransaction) { m_db.rollback(); delete m_insertTransaction; m_insertTransaction = nullptr; } else qCWarning(ExifLog, "Trying to abort transaction, but no transaction is active!"); return true; } bool Exif::Database::insert(const DB::FileName &filename, Exiv2::ExifData data) { if (!isUsable()) return false; QSqlQuery *query = getInsertQuery(); query->bindValue(0, filename.absolute()); int i = 1; for (const DatabaseElement *e : elements()) { query->bindValue(i++, e->valueFromExif(data)); } bool status = query->exec(); if (!status) showErrorAndFail(*query); concludeInsertQuery(query); return status; } bool Exif::Database::insert(QList map) { if (!isUsable()) return false; QSqlQuery *query = getInsertQuery(); // not a const reference because DatabaseElement::valueFromExif uses operator[] on the exif datum Q_FOREACH (DBExifInfo elt, map) { query->bindValue(0, elt.first.absolute()); int i = 1; for (const DatabaseElement *e : elements()) { query->bindValue(i++, e->valueFromExif(elt.second)); } if (!query->exec()) { showErrorAndFail(*query); } } concludeInsertQuery(query); return true; } Exif::Database *Exif::Database::instance() { if (!s_instance) { qCInfo(ExifLog) << "initializing Exif database..."; s_instance = new Exif::Database(); s_instance->init(); } return s_instance; } void Exif::Database::deleteInstance() { delete s_instance; s_instance = nullptr; } bool Exif::Database::isAvailable() { #ifdef QT_NO_SQL return false; #else return QSqlDatabase::isDriverAvailable(QString::fromLatin1("QSQLITE")); #endif } int Exif::Database::DBFileVersion() const { // previous to KPA 4.6, there was no metadata table: if (!m_db.tables().contains(QString::fromLatin1("settings"))) return 1; QSqlQuery query(QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'DBVersion'"), m_db); if (!query.exec()) showErrorAndFail(query); if (query.first()) { return query.value(0).toInt(); } return 0; } int Exif::Database::DBFileVersionGuaranteed() const { // previous to KPA 4.6, there was no metadata table: if (!m_db.tables().contains(QString::fromLatin1("settings"))) return 0; QSqlQuery query(QString::fromLatin1("SELECT value FROM settings WHERE keyword = 'GuaranteedDataVersion'"), m_db); if (!query.exec()) showErrorAndFail(query); if (query.first()) { return query.value(0).toInt(); } return 0; } constexpr int Exif::Database::DBVersion() { return DB_VERSION; } bool Exif::Database::isUsable() const { return (isAvailable() && isOpen()); } QString Exif::Database::exifDBFile() { return ::Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("/exif-info.db"); } bool Exif::Database::readFields(const DB::FileName &fileName, ElementList &fields) const { if (!isUsable()) return false; bool foundIt = false; QStringList fieldList; for (const DatabaseElement *e : fields) { fieldList.append(e->columnName()); } QSqlQuery query(m_db); // the query returns a single value, so we don't need the overhead for random access: query.setForwardOnly(true); query.prepare(QString::fromLatin1("select %1 from exif where filename=?") .arg(fieldList.join(QString::fromLatin1(", ")))); query.bindValue(0, fileName.absolute()); if (!query.exec()) { showErrorAndFail(query); } if (query.next()) { // file in exif db -> write back results int i = 0; for (DatabaseElement *e : fields) { e->setValue(query.value(i++)); } foundIt = true; } return foundIt; } DB::FileNameSet Exif::Database::filesMatchingQuery(const QString &queryStr) const { if (!isUsable()) return DB::FileNameSet(); DB::FileNameSet result; QSqlQuery query(queryStr, m_db); if (!query.exec()) showErrorAndFail(query); else { if (m_doUTF8Conversion) while (query.next()) result.insert(DB::FileName::fromAbsolutePath(QString::fromUtf8(query.value(0).toByteArray()))); else while (query.next()) result.insert(DB::FileName::fromAbsolutePath(query.value(0).toString())); } return result; } QList> Exif::Database::cameras() const { QList> result; if (!isUsable()) return result; QSqlQuery query(QString::fromLatin1("SELECT DISTINCT Exif_Image_Make, Exif_Image_Model FROM exif"), m_db); if (!query.exec()) { showErrorAndFail(query); } else { while (query.next()) { QString make = query.value(0).toString(); QString model = query.value(1).toString(); if (!make.isEmpty() && !model.isEmpty()) result.append(qMakePair(make, model)); } } return result; } QList Exif::Database::lenses() const { QList result; if (!isUsable()) return result; QSqlQuery query(QString::fromLatin1("SELECT DISTINCT Exif_Photo_LensModel FROM exif"), m_db); if (!query.exec()) { showErrorAndFail(query); } else { while (query.next()) { QString lens = query.value(0).toString(); if (!lens.isEmpty()) result.append(lens); } } return result; } void Exif::Database::init() { if (!isAvailable()) return; m_isFailed = false; m_insertTransaction = nullptr; bool dbExists = QFile::exists(exifDBFile()); openDatabase(); if (!isOpen()) return; if (!dbExists) populateDatabase(); else updateDatabase(); } void Exif::Database::recreate() { // We create a backup of the current database in case // the user presse 'cancel' or there is any error. In that case // we want to go back to the original DB. const QString origBackup = exifDBFile() + QLatin1String(".bak"); m_db.close(); QDir().remove(origBackup); QDir().rename(exifDBFile(), origBackup); init(); const DB::FileNameList allImages = DB::ImageDB::instance()->images(); QProgressDialog dialog; dialog.setModal(true); dialog.setLabelText(i18n("Rereading Exif information from all images")); dialog.setMaximum(allImages.size()); // using a transaction here removes a *huge* overhead on the insert statements startInsertTransaction(); int i = 0; for (const DB::FileName &fileName : allImages) { const DB::ImageInfoPtr info = fileName.info(); dialog.setValue(i++); if (info->mediaType() == DB::Image) { add(fileName); } if (i % 10) qApp->processEvents(); if (dialog.wasCanceled()) break; } // PENDING(blackie) We should count the amount of files that did not succeeded and warn the user. if (dialog.wasCanceled()) { abortInsertTransaction(); m_db.close(); QDir().remove(exifDBFile()); QDir().rename(origBackup, exifDBFile()); init(); } else { commitInsertTransaction(); QDir().remove(origBackup); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/DatabaseElement.cpp b/Exif/DatabaseElement.cpp index c44c03da..d10e98f9 100644 --- a/Exif/DatabaseElement.cpp +++ b/Exif/DatabaseElement.cpp @@ -1,230 +1,231 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "DatabaseElement.h" + #include "Logging.h" #include #include static QString replaceDotWithUnderscore(const char *cstr) { QString str(QString::fromLatin1(cstr)); return str.replace(QString::fromLatin1("."), QString::fromLatin1("_")); } Exif::DatabaseElement::DatabaseElement() : m_value() { } QVariant Exif::DatabaseElement::value() const { return m_value; } void Exif::DatabaseElement::setValue(QVariant val) { m_value = val; } Exif::StringExifElement::StringExifElement(const char *tag) : m_tag(tag) { } QString Exif::StringExifElement::columnName() const { return replaceDotWithUnderscore(m_tag); } QString Exif::StringExifElement::createString() const { return QString::fromLatin1("%1 string").arg(replaceDotWithUnderscore(m_tag)); } QString Exif::StringExifElement::queryString() const { return QString::fromLatin1("?"); } QVariant Exif::StringExifElement::valueFromExif(Exiv2::ExifData &data) const { return QVariant { QLatin1String(data[m_tag].toString().c_str()) }; } Exif::IntExifElement::IntExifElement(const char *tag) : m_tag(tag) { } QString Exif::IntExifElement::columnName() const { return replaceDotWithUnderscore(m_tag); } QString Exif::IntExifElement::createString() const { return QString::fromLatin1("%1 int").arg(replaceDotWithUnderscore(m_tag)); } QString Exif::IntExifElement::queryString() const { return QString::fromLatin1("?"); } QVariant Exif::IntExifElement::valueFromExif(Exiv2::ExifData &data) const { if (data[m_tag].count() > 0) return QVariant { (int)data[m_tag].toLong() }; else return QVariant { (int)0 }; } Exif::RationalExifElement::RationalExifElement(const char *tag) : m_tag(tag) { } QString Exif::RationalExifElement::columnName() const { return replaceDotWithUnderscore(m_tag); } QString Exif::RationalExifElement::createString() const { return QString::fromLatin1("%1 float").arg(replaceDotWithUnderscore(m_tag)); } QString Exif::RationalExifElement::queryString() const { return QString::fromLatin1("?"); } QVariant Exif::RationalExifElement::valueFromExif(Exiv2::ExifData &data) const { double value; Exiv2::Exifdatum &tagDatum = data[m_tag]; switch (tagDatum.count()) { case 0: // empty value = -1.0; break; case 1: // "normal" rational value = 1.0 * tagDatum.toRational().first / tagDatum.toRational().second; break; case 3: // GPS lat/lon data: { value = 0.0; double divisor = 1.0; // degree / minute / second: for (int i = 0; i < 3; i++) { double nom = tagDatum.toRational(i).first; double denom = tagDatum.toRational(i).second; if (denom != 0) value += (nom / denom) / divisor; divisor *= 60.0; } } break; default: // FIXME: there are at least the following other rational types: // whitepoints -> 2 components // YCbCrCoefficients -> 3 components (Coefficients for transformation from RGB to YCbCr image data. ) // chromaticities -> 6 components qCWarning(ExifLog) << "Exif rational data with " << tagDatum.count() << " components is not handled, yet!"; return QVariant {}; } return QVariant { value }; } Exif::LensExifElement::LensExifElement() : m_tag("Exif.Photo.LensModel") { } QString Exif::LensExifElement::columnName() const { return replaceDotWithUnderscore(m_tag); } QString Exif::LensExifElement::createString() const { return QString::fromLatin1("%1 string").arg(replaceDotWithUnderscore(m_tag)); } QString Exif::LensExifElement::queryString() const { return QString::fromLatin1("?"); } QVariant Exif::LensExifElement::valueFromExif(Exiv2::ExifData &data) const { QString value; bool canonHack = false; for (Exiv2::ExifData::const_iterator it = data.begin(); it != data.end(); ++it) { const QString datum = QString::fromLatin1(it->key().c_str()); // Exif.Photo.LensModel [Ascii] // Exif.Canon.LensModel [Ascii] // Exif.OlympusEq.LensModel [Ascii] if (datum.endsWith(QString::fromLatin1(".LensModel"))) { qCDebug(ExifLog) << datum << ": " << it->toString().c_str(); canonHack = false; value = QString::fromUtf8(it->toString().c_str()); // we can break here since Exif.Photo.LensModel should be bound first break; } // Exif.NikonLd3.LensIDNumber [Byte] // on Nikon cameras, this seems to provide better results than .Lens and .LensType // (i.e. it includes the lens manufacturer). if (datum.endsWith(QString::fromLatin1(".LensIDNumber"))) { // ExifDatum::print() returns the interpreted value qCDebug(ExifLog) << datum << ": " << it->print(&data).c_str(); canonHack = false; value = QString::fromUtf8(it->print(&data).c_str()); continue; } // Exif.Nikon3.LensType [Byte] // Exif.OlympusEq.LensType [Byte] // Exif.Panasonic.LensType [Ascii] // Exif.Pentax.LensType [Byte] // Exif.Samsung2.LensType [Short] if (datum.endsWith(QString::fromLatin1(".LensType"))) { // ExifDatum::print() returns the interpreted value qCDebug(ExifLog) << datum << ": " << it->print(&data).c_str(); // make sure this cannot overwrite LensIDNumber if (value.isEmpty()) { canonHack = (datum == QString::fromLatin1("Exif.CanonCs.LensType")); value = QString::fromUtf8(it->print(&data).c_str()); } } } // some canon lenses have a dummy value as LensType: if (canonHack && value == QString::fromLatin1("(65535)")) { value = QString::fromLatin1("Canon generic"); const auto datum = data.findKey(Exiv2::ExifKey("Exif.CanonCs.Lens")); if (datum != data.end()) { value += QString::fromLatin1(" "); value += QString::fromUtf8(datum->print(&data).c_str()); } } qCDebug(ExifLog) << "final lens value " << value; return QVariant { value }; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/Grid.cpp b/Exif/Grid.cpp index 057d0ba6..f6a2d41c 100644 --- a/Exif/Grid.cpp +++ b/Exif/Grid.cpp @@ -1,209 +1,212 @@ /* Copyright (C) 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) 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 "Grid.h" + #include "Info.h" + +#include + #include #include #include #include #include #include #include -#include Exif::Grid::Grid(QWidget *parent) : QScrollArea(parent) { setFocusPolicy(Qt::WheelFocus); setWidgetResizable(true); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); viewport()->installEventFilter(this); setMinimumSize(800, 400); } class Background : public QWidget { protected: void paintEvent(QPaintEvent *event) override { QPainter painter(this); painter.fillRect(event->rect(), QColor(Qt::white)); }; }; void Exif::Grid::setupUI(const QString &charset) { delete this->widget(); m_labels.clear(); Background *widget = new Background; QGridLayout *layout = new QGridLayout(widget); layout->setSpacing(0); int row = 0; const QMap map = Exif::Info::instance()->infoForDialog(m_fileName, charset); const StringSet groups = exifGroups(map); for (const QString &group : groups) { layout->addWidget(headerLabel(group), row++, 0, 1, 4); // Items of group const QMap items = itemsForGroup(group, map); QStringList sorted = items.keys(); sorted.sort(); int elements = sorted.size(); int perCol = (elements + 1) / 2; int count = 0; for (const QString &key : sorted) { const int subrow = (count % perCol); const QColor color = (subrow & 1) ? Qt::white : QColor(226, 235, 250); QPair pair = infoLabelPair(exifNameNoGroup(key), items[key].join(QLatin1String(", ")), color); int col = (count / perCol) * 2; layout->addWidget(pair.first, row + subrow, col); layout->addWidget(pair.second, row + subrow, col + 1); count++; } row += perCol; } setWidget(widget); widget->show(); QTimer::singleShot(0, this, SLOT(updateWidgetSize())); } QLabel *Exif::Grid::headerLabel(const QString &title) { QLabel *label = new QLabel(title); QPalette pal; pal.setBrush(QPalette::Background, Qt::lightGray); label->setPalette(pal); label->setAutoFillBackground(true); label->setAlignment(Qt::AlignCenter); return label; } QPair Exif::Grid::infoLabelPair(const QString &title, const QString &value, const QColor &color) { QLabel *keyLabel = new QLabel(title); QLabel *valueLabel = new QLabel(value); QPalette pal; pal.setBrush(QPalette::Background, color); keyLabel->setPalette(pal); valueLabel->setPalette(pal); keyLabel->setAutoFillBackground(true); valueLabel->setAutoFillBackground(true); m_labels.append(qMakePair(keyLabel, valueLabel)); return qMakePair(keyLabel, valueLabel); } void Exif::Grid::updateWidgetSize() { widget()->setFixedSize(viewport()->width(), widget()->height()); } StringSet Exif::Grid::exifGroups(const QMap &exifInfo) { StringSet result; for (QMap::ConstIterator it = exifInfo.begin(); it != exifInfo.end(); ++it) { result.insert(groupName(it.key())); } return result; } QMap Exif::Grid::itemsForGroup(const QString &group, const QMap &exifInfo) { QMap result; for (QMap::ConstIterator it = exifInfo.begin(); it != exifInfo.end(); ++it) { if (groupName(it.key()) == group) result.insert(it.key(), it.value()); } return result; } QString Exif::Grid::groupName(const QString &exifName) { QStringList list = exifName.split(QString::fromLatin1(".")); list.pop_back(); return list.join(QString::fromLatin1(".")); } QString Exif::Grid::exifNameNoGroup(const QString &fullName) { return fullName.split(QString::fromLatin1(".")).last(); } void Exif::Grid::scroll(int dy) { verticalScrollBar()->setValue(verticalScrollBar()->value() + dy); } void Exif::Grid::updateSearchString(const QString &search) { for (QPair tuple : m_labels) { const bool matches = tuple.first->text().contains(search, Qt::CaseInsensitive) && search.length() != 0; QPalette pal = tuple.first->palette(); pal.setBrush(QPalette::Foreground, matches ? Qt::red : Qt::black); tuple.first->setPalette(pal); tuple.second->setPalette(pal); QFont fnt = tuple.first->font(); fnt.setBold(matches); tuple.first->setFont(fnt); tuple.second->setFont(fnt); } } void Exif::Grid::keyPressEvent(QKeyEvent *e) { switch (e->key()) { case Qt::Key_Down: scroll(20); return; case Qt::Key_Up: scroll(-20); return; case Qt::Key_PageDown: scroll(viewport()->height() - 20); return; case Qt::Key_PageUp: scroll(-(viewport()->height() - 20)); return; case Qt::Key_Escape: QScrollArea::keyPressEvent(e); // Propagate to close dialog. return; } } bool Exif::Grid::eventFilter(QObject *object, QEvent *event) { if (object == viewport() && event->type() == QEvent::Resize) { QResizeEvent *re = static_cast(event); widget()->setFixedSize(re->size().width(), widget()->height()); } return false; } void Exif::Grid::setFileName(const DB::FileName &fileName) { m_fileName = fileName; setupUI(Settings::SettingsData::instance()->iptcCharset()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/Grid.h b/Exif/Grid.h index b9fabd04..c2470579 100644 --- a/Exif/Grid.h +++ b/Exif/Grid.h @@ -1,69 +1,70 @@ /* Copyright (C) 2012 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef EXIF_GRID_H #define EXIF_GRID_H #include +#include + #include #include -#include class QLabel; using Utilities::StringSet; namespace Exif { class Grid : public QScrollArea { Q_OBJECT public: explicit Grid(QWidget *parent); void setFileName(const DB::FileName &fileName); public slots: void updateSearchString(const QString &); private: void keyPressEvent(QKeyEvent *) override; bool eventFilter(QObject *, QEvent *) override; StringSet exifGroups(const QMap &exifInfo); QMap itemsForGroup(const QString &group, const QMap &exifInfo); QString groupName(const QString &exifName); QString exifNameNoGroup(const QString &fullName); void scroll(int dy); QLabel *headerLabel(const QString &title); QPair infoLabelPair(const QString &title, const QString &value, const QColor &color); private slots: void setupUI(const QString &charset); void updateWidgetSize(); private: QList> m_labels; int m_maxKeyWidth; DB::FileName m_fileName; }; } // namespace Exif #endif // EXIF_GRID_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/Info.cpp b/Exif/Info.cpp index e2066860..16cd7547 100644 --- a/Exif/Info.cpp +++ b/Exif/Info.cpp @@ -1,249 +1,249 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Info.h" + #include "Logging.h" -#include "DB/ImageDB.h" -#include "DB/ImageInfo.h" -#include "Settings/SettingsData.h" -#include "Utilities/StringSet.h" +#include +#include +#include +#include #include #include #include - #include #include #include using namespace Exif; namespace { QString cStringWithEncoding(const char *c_str, const QString &charset) { QTextCodec *codec = QTextCodec::codecForName(charset.toLatin1()); if (!codec) codec = QTextCodec::codecForLocale(); return codec->toUnicode(c_str); } } // namespace Info *Info::s_instance = nullptr; QMap Info::info(const DB::FileName &fileName, StringSet wantedKeys, bool returnFullExifName, const QString &charset) { QMap result; try { Metadata data = metadata(exifInfoFile(fileName)); for (Exiv2::ExifData::const_iterator i = data.exif.begin(); i != data.exif.end(); ++i) { QString key = QString::fromLocal8Bit(i->key().c_str()); m_keys.insert(key); if (wantedKeys.contains(key)) { QString text = key; if (!returnFullExifName) text = key.split(QLatin1String(".")).last(); std::ostringstream stream; stream << *i; QString str(cStringWithEncoding(stream.str().c_str(), charset)); result[text] += str; } } for (Exiv2::IptcData::const_iterator i = data.iptc.begin(); i != data.iptc.end(); ++i) { QString key = QString::fromLatin1(i->key().c_str()); m_keys.insert(key); if (wantedKeys.contains(key)) { QString text = key; if (!returnFullExifName) text = key.split(QString::fromLatin1(".")).last(); std::ostringstream stream; stream << *i; QString str(cStringWithEncoding(stream.str().c_str(), charset)); result[text] += str; } } } catch (...) { } return result; } Info *Info::instance() { if (!s_instance) s_instance = new Info; return s_instance; } StringSet Info::availableKeys() { return m_keys; } QMap Info::infoForViewer(const DB::FileName &fileName, const QString &charset) { return info(fileName, ::Settings::SettingsData::instance()->exifForViewer(), false, charset); } QMap Info::infoForDialog(const DB::FileName &fileName, const QString &charset) { return info(fileName, ::Settings::SettingsData::instance()->exifForDialog(), true, charset); } StringSet Info::standardKeys() { static StringSet res; if (!res.empty()) return res; QList tags; std::ostringstream s; #if (EXIV2_TEST_VERSION(0, 21, 0)) const Exiv2::GroupInfo *gi = Exiv2::ExifTags::groupList(); while (gi->tagList_ != 0) { Exiv2::TagListFct tl = gi->tagList_; const Exiv2::TagInfo *ti = tl(); while (ti->tag_ != 0xFFFF) { tags << ti; ++ti; } ++gi; } for (QList::iterator it = tags.begin(); it != tags.end(); ++it) { while ((*it)->tag_ != 0xffff) { res.insert(QString::fromLatin1(Exiv2::ExifKey(**it).key().c_str())); ++(*it); } } #else tags << Exiv2::ExifTags::ifdTagList() << Exiv2::ExifTags::exifTagList() << Exiv2::ExifTags::iopTagList() << Exiv2::ExifTags::gpsTagList(); for (QList::iterator it = tags.begin(); it != tags.end(); ++it) { while ((*it)->tag_ != 0xffff) { res.insert(QLatin1String(Exiv2::ExifKey((*it)->tag_, Exiv2::ExifTags::ifdItem((*it)->ifdId_)).key().c_str())); ++(*it); } } // Now the ugly part -- exiv2 doesn't have any way to get a list of // MakerNote tags in a reasonable form, so we have to parse it from strings for (Exiv2::IfdId kind = Exiv2::canonIfdId; kind < Exiv2::lastIfdId; kind = static_cast(kind + 1)) { #if EXIV2_TEST_VERSION(0, 17, 0) Exiv2::ExifTags::taglist(s, kind); #else Exiv2::ExifTags::makerTaglist(s, kind); #endif } #endif // IPTC tags use yet another format... Exiv2::IptcDataSets::dataSetList(s); QStringList lines = QString(QLatin1String(s.str().c_str())).split(QChar::fromLatin1('\n')); for (QStringList::const_iterator it = lines.constBegin(); it != lines.constEnd(); ++it) { if (it->isEmpty()) continue; QStringList fields = it->split(QChar::fromLatin1('\t')); if (fields.size() == 7) { QString id = fields[4]; if (id.endsWith(QChar::fromLatin1(','))) id.chop(1); res.insert(id); } else { fields = it->split(QLatin1String(", ")); if (fields.size() >= 11) { res.insert(fields[8]); } else { qCWarning(ExifLog) << "Unparsable output from exiv2 library: " << *it; continue; } } } return res; } Info::Info() { m_keys = standardKeys(); } void Exif::Info::writeInfoToFile(const DB::FileName &srcName, const QString &destName) { // Load Exif from source image Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(QFile::encodeName(srcName.absolute()).data()); image->readMetadata(); Exiv2::ExifData data = image->exifData(); // Modify Exif information from database. DB::ImageInfoPtr info = DB::ImageDB::instance()->info(srcName); data["Exif.Image.ImageDescription"] = info->description().toLocal8Bit().data(); image = Exiv2::ImageFactory::open(QFile::encodeName(destName).data()); image->setExifData(data); image->writeMetadata(); } /** * Some Canon cameras stores Exif info in files ending in .thm, so we need to use those files for fetching Exif info * if they exists. */ DB::FileName Exif::Info::exifInfoFile(const DB::FileName &fileName) { QString dirName = QFileInfo(fileName.relative()).path(); QString baseName = QFileInfo(fileName.relative()).baseName(); DB::FileName name = DB::FileName::fromRelativePath(dirName + QString::fromLatin1("/") + baseName + QString::fromLatin1(".thm")); if (name.exists()) return name; name = DB::FileName::fromRelativePath(dirName + QString::fromLatin1("/") + baseName + QString::fromLatin1(".THM")); if (name.exists()) return name; return fileName; } Exif::Metadata Exif::Info::metadata(const DB::FileName &fileName) { try { Exif::Metadata result; Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(QFile::encodeName(fileName.absolute()).data()); Q_ASSERT(image.get() != 0); image->readMetadata(); result.exif = image->exifData(); result.iptc = image->iptcData(); result.comment = image->comment(); return result; } catch (...) { } return Exif::Metadata(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/Info.h b/Exif/Info.h index afaee25d..99d2f5cb 100644 --- a/Exif/Info.h +++ b/Exif/Info.h @@ -1,67 +1,68 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef EXIF_INFO_H #define EXIF_INFO_H -#include "Utilities/StringSet.h" +#include + #include #include #include #include namespace DB { class FileName; } namespace Exif { using Utilities::StringSet; struct Metadata { Exiv2::ExifData exif; Exiv2::IptcData iptc; std::string comment; }; class Info { public: Info(); static Info *instance(); QMap info(const DB::FileName &fileName, StringSet wantedKeys, bool returnFullExifName, const QString &charset); QMap infoForViewer(const DB::FileName &fileName, const QString &charset); QMap infoForDialog(const DB::FileName &fileName, const QString &charset); StringSet availableKeys(); StringSet standardKeys(); void writeInfoToFile(const DB::FileName &srcName, const QString &destName); Metadata metadata(const DB::FileName &fileName); protected: DB::FileName exifInfoFile(const DB::FileName &fileName); private: static Info *s_instance; StringSet m_keys; }; } #endif /* EXIF_INFO_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/InfoDialog.cpp b/Exif/InfoDialog.cpp index 8a6bc21d..ee56d6e7 100644 --- a/Exif/InfoDialog.cpp +++ b/Exif/InfoDialog.cpp @@ -1,126 +1,127 @@ /* 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 "InfoDialog.h" + +#include "Grid.h" +#include "Info.h" + +#include +#include +#include +#include + +#include #include #include #include #include #include #include #include -#include - -#include "DB/ImageDB.h" -#include "Exif/Info.h" -#include "Exif/InfoDialog.h" -#include "Grid.h" -#include "ImageManager/AsyncLoader.h" -#include "ImageManager/ImageRequest.h" -#include "Settings/SettingsData.h" - using Utilities::StringSet; Exif::InfoDialog::InfoDialog(const DB::FileName &fileName, QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Exif Information")); setAttribute(Qt::WA_DeleteOnClose); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); buttonBox->button(QDialogButtonBox::Close)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QWidget *top = new QWidget(this); QVBoxLayout *vlay = new QVBoxLayout(top); setLayout(vlay); vlay->addWidget(top); // -------------------------------------------------- File name and pixmap QHBoxLayout *hlay = new QHBoxLayout; vlay->addLayout(hlay); m_fileNameLabel = new QLabel(top); QFont fnt = font(); fnt.setPointSize((int)(fnt.pointSize() * 1.2)); fnt.setWeight(QFont::Bold); m_fileNameLabel->setFont(fnt); m_fileNameLabel->setAlignment(Qt::AlignCenter); hlay->addWidget(m_fileNameLabel, 1); m_pix = new QLabel(top); hlay->addWidget(m_pix); // -------------------------------------------------- Exif Grid m_grid = new Exif::Grid(top); vlay->addWidget(m_grid); // -------------------------------------------------- Current Search hlay = new QHBoxLayout; vlay->addLayout(hlay); QLabel *searchLabel = new QLabel(i18n("Exif label search: "), top); hlay->addWidget(searchLabel); m_searchBox = new QLineEdit(top); hlay->addWidget(m_searchBox); hlay->addStretch(1); QLabel *iptcLabel = new QLabel(i18n("IPTC character set:"), top); m_iptcCharset = new QComboBox(top); QStringList charsets; QList charsetsBA = QTextCodec::availableCodecs(); for (QList::const_iterator it = charsetsBA.constBegin(); it != charsetsBA.constEnd(); ++it) charsets << QLatin1String(*it); m_iptcCharset->insertItems(0, charsets); m_iptcCharset->setCurrentIndex(qMax(0, QTextCodec::availableCodecs().indexOf(Settings::SettingsData::instance()->iptcCharset().toLatin1()))); hlay->addWidget(iptcLabel); hlay->addWidget(m_iptcCharset); connect(m_searchBox, SIGNAL(textChanged(QString)), m_grid, SLOT(updateSearchString(QString))); connect(m_iptcCharset, SIGNAL(activated(QString)), m_grid, SLOT(setupUI(QString))); setImage(fileName); vlay->addWidget(buttonBox); } QSize Exif::InfoDialog::sizeHint() const { return QSize(800, 400); } void Exif::InfoDialog::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) { if (request->loadedOK()) m_pix->setPixmap(QPixmap::fromImage(image)); } void Exif::InfoDialog::setImage(const DB::FileName &fileName) { m_fileNameLabel->setText(fileName.relative()); m_grid->setFileName(fileName); ImageManager::ImageRequest *request = new ImageManager::ImageRequest(fileName, QSize(128, 128), fileName.info()->angle(), this); request->setPriority(ImageManager::Viewer); ImageManager::AsyncLoader::instance()->load(request); } void Exif::InfoDialog::enterEvent(QEvent *) { m_grid->setFocus(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/InfoDialog.h b/Exif/InfoDialog.h index 8df282d2..2c1f81b9 100644 --- a/Exif/InfoDialog.h +++ b/Exif/InfoDialog.h @@ -1,70 +1,70 @@ /* Copyright (C) 2003-2016 Jesper K. Pedersen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef INFODIALOG_H #define INFODIALOG_H -#include +#include +#include -#include "DB/FileName.h" -#include "ImageManager/ImageClientInterface.h" +#include class QComboBox; class QLineEdit; class QLabel; class QKeyEvent; class QResizeEvent; namespace DB { class Id; } namespace Exif { class Grid; class InfoDialog : public QDialog, public ImageManager::ImageClientInterface { Q_OBJECT public: InfoDialog(const DB::FileName &fileName, QWidget *parent); void setImage(const DB::FileName &fileName); QSize sizeHint() const override; void enterEvent(QEvent *) override; // ImageManager::ImageClient interface. void pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) override; private: QLineEdit *m_searchBox; QLabel *m_pix; QComboBox *m_iptcCharset; Grid *m_grid; QLabel *m_fileNameLabel; }; } #endif /* INFODIALOG_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/RangeWidget.cpp b/Exif/RangeWidget.cpp index 1dba529a..915ff47c 100644 --- a/Exif/RangeWidget.cpp +++ b/Exif/RangeWidget.cpp @@ -1,100 +1,101 @@ /* 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; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/RangeWidget.h b/Exif/RangeWidget.h index 362b2775..f6cd7285 100644 --- a/Exif/RangeWidget.h +++ b/Exif/RangeWidget.h @@ -1,70 +1,71 @@ /* Copyright (C) 2003-2010 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. */ #ifndef RANGEWIDGET_H #define RANGEWIDGET_H -#include "Exif/SearchInfo.h" +#include "SearchInfo.h" + #include <QList> #include <qobject.h> class QGridLayout; class QComboBox; namespace Exif { class RangeWidget : public QObject { Q_OBJECT public: class Value { public: Value() {} Value(double value, const QString &text) : value(value) , text(text) { } double value; QString text; }; typedef QList<Value> ValueList; RangeWidget(const QString &text, const QString &searchTag, const ValueList &list, QGridLayout *layout, int row); Exif::SearchInfo::Range range() const; protected slots: void slotUpdateTo(int index); protected: QString tagToLabel(const QString &tag); private: QString m_searchTag; QComboBox *m_from; QComboBox *m_to; ValueList m_list; }; } #endif /* RANGEWIDGET_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/ReReadDialog.cpp b/Exif/ReReadDialog.cpp index f6d67d97..305d2058 100644 --- a/Exif/ReReadDialog.cpp +++ b/Exif/ReReadDialog.cpp @@ -1,145 +1,145 @@ /* 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 "Database.h" + +#include <DB/ImageDB.h> +#include <Settings/SettingsData.h> + +#include <KConfigGroup> +#include <KLocalizedString> +#include <KMessageBox> +#include <KSharedConfig> #include <QCheckBox> #include <QDialogButtonBox> #include <QGroupBox> #include <QLabel> #include <QListWidget> #include <QPushButton> #include <QVBoxLayout> -#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(i18nc("@title:window", "Read Exif Info from Files")); QWidget *top = new QWidget; QVBoxLayout *lay1 = new QVBoxLayout(top); setLayout(lay1); lay1->addWidget(top); m_exifDB = new QCheckBox(i18n("Update Exif search database"), top); 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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/ReReadDialog.h b/Exif/ReReadDialog.h index bc925d08..47146cf5 100644 --- a/Exif/ReReadDialog.h +++ b/Exif/ReReadDialog.h @@ -1,60 +1,60 @@ /* Copyright (C) 2003-2010 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. */ #ifndef REREADDIALOG_H #define REREADDIALOG_H -#include <QDialog> - #include <DB/FileNameList.h> +#include <QDialog> + class QCheckBox; class QLabel; class QListWidget; namespace Exif { class ReReadDialog : public QDialog { Q_OBJECT public: explicit ReReadDialog(QWidget *parent); // prevent hiding of base class method: using QDialog::exec; int exec(const DB::FileNameList &); protected slots: void readInfo(); void warnAboutDates(bool); private: DB::FileNameList m_list; QCheckBox *m_exifDB; QCheckBox *m_date; QCheckBox *m_orientation; QCheckBox *m_description; QCheckBox *m_force_date; QListWidget *m_fileList; }; } #endif /* REREADDIALOG_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/SearchDialog.cpp b/Exif/SearchDialog.cpp index 8692df42..85bd36ee 100644 --- a/Exif/SearchDialog.cpp +++ b/Exif/SearchDialog.cpp @@ -1,429 +1,431 @@ /* 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 "Exif/Database.h" + +#include "Database.h" + #include <KConfigGroup> #include <KLocalizedString> #include <QDialogButtonBox> #include <QGridLayout> #include <QGroupBox> #include <QHBoxLayout> #include <QPushButton> #include <QScrollArea> #include <QVBoxLayout> #include <qcheckbox.h> #include <qlabel.h> #include <qlayout.h> #include <qspinbox.h> using namespace Exif; Exif::SearchDialog::SearchDialog(QWidget *parent) : KPageDialog(parent) { setWindowTitle(i18nc("@title:window", "Exif Search")); setFaceType(Tabbed); QWidget *settings = new QWidget; KPageWidgetItem *page = new KPageWidgetItem(settings, i18n("Settings")); addPage(page); QVBoxLayout *vlay = new QVBoxLayout(settings); // Iso, Exposure, Aperture, FNumber QHBoxLayout *hlay = new QHBoxLayout; vlay->addLayout(hlay); QGridLayout *gridLayout = new QGridLayout; gridLayout->setSpacing(6); hlay->addLayout(gridLayout); hlay->addStretch(1); makeISO(gridLayout); makeExposureTime(gridLayout); hlay->addSpacing(30); gridLayout = new QGridLayout; gridLayout->setSpacing(6); hlay->addLayout(gridLayout); hlay->addStretch(1); m_apertureValue = makeApertureOrFNumber(i18n("Aperture Value"), QString::fromLatin1("Exif_Photo_ApertureValue"), gridLayout, 0); m_fNumber = makeApertureOrFNumber(i18n("F Number"), QString::fromLatin1("Exif_Photo_FNumber"), gridLayout, 1); hlay->addSpacing(30); // Focal length QHBoxLayout *focalLayout = new QHBoxLayout; focalLayout->setSpacing(6); hlay->addLayout(focalLayout); hlay->addStretch(1); QLabel *label = new QLabel(i18n("Focal Length")); focalLayout->addWidget(label); m_fromFocalLength = new QSpinBox; focalLayout->addWidget(m_fromFocalLength); m_fromFocalLength->setRange(0, 10000); m_fromFocalLength->setSingleStep(10); label = new QLabel(i18nc("As in 'A range from x to y'", "to")); focalLayout->addWidget(label); m_toFocalLength = new QSpinBox; focalLayout->addWidget(m_toFocalLength); m_toFocalLength->setRange(0, 10000); m_toFocalLength->setSingleStep(10); m_toFocalLength->setValue(10000); QString suffix = i18nc("This is millimeter for focal length, like 35mm", "mm"); m_fromFocalLength->setSuffix(suffix); m_toFocalLength->setSuffix(suffix); connect(m_fromFocalLength, SIGNAL(valueChanged(int)), this, SLOT(fromFocalLengthChanged(int))); connect(m_toFocalLength, SIGNAL(valueChanged(int)), this, SLOT(toFocalLengthChanged(int))); // exposure program and Metring mode hlay = new QHBoxLayout; vlay->addLayout(hlay); hlay->addWidget(makeExposureProgram(settings)); hlay->addWidget(makeMeteringMode(settings)); vlay->addStretch(1); // ------------------------------------------------------------ Camera page = new KPageWidgetItem(makeCamera(), i18n("Camera")); addPage(page); // ------------------------------------------------------------ Lens page = new KPageWidgetItem(makeLens(), i18n("Lens")); addPage(page); // ------------------------------------------------------------ Misc QWidget *misc = new QWidget; addPage(new KPageWidgetItem(misc, i18n("Miscellaneous"))); vlay = new QVBoxLayout(misc); vlay->addWidget(makeOrientation(misc), 1); hlay = new QHBoxLayout; vlay->addLayout(hlay); hlay->addWidget(makeContrast(misc)); hlay->addWidget(makeSharpness(misc)); hlay->addWidget(makeSaturation(misc)); vlay->addStretch(1); } void Exif::SearchDialog::makeISO(QGridLayout *layout) { Exif::RangeWidget::ValueList list; list << Exif::RangeWidget::Value(100, QString::fromLatin1("100")) << Exif::RangeWidget::Value(200, QString::fromLatin1("200")) << Exif::RangeWidget::Value(400, QString::fromLatin1("400")) << Exif::RangeWidget::Value(800, QString::fromLatin1("800")) << Exif::RangeWidget::Value(1600, QString::fromLatin1("1600")) << Exif::RangeWidget::Value(3200, QString::fromLatin1("3200")) << Exif::RangeWidget::Value(6400, QString::fromLatin1("6400")) << Exif::RangeWidget::Value(12800, QString::fromLatin1("12800")) << Exif::RangeWidget::Value(25600, QString::fromLatin1("25600")) << Exif::RangeWidget::Value(51200, QString::fromLatin1("51200")); m_iso = new RangeWidget(i18n("Iso setting"), QString::fromLatin1("Exif_Photo_ISOSpeedRatings"), list, layout, 0); } void Exif::SearchDialog::makeExposureTime(QGridLayout *layout) { QString secs = i18nc("Example 1.6 secs (as in seconds)", "secs."); Exif::RangeWidget::ValueList list; list << Exif::RangeWidget::Value(1.0 / 4000, QString::fromLatin1("1/4000")) << Exif::RangeWidget::Value(1.0 / 3200, QString::fromLatin1("1/3200")) << Exif::RangeWidget::Value(1.0 / 2500, QString::fromLatin1("1/2500")) << Exif::RangeWidget::Value(1.0 / 2000, QString::fromLatin1("1/2000")) << Exif::RangeWidget::Value(1.0 / 1600, QString::fromLatin1("1/1600")) << Exif::RangeWidget::Value(1.0 / 1250, QString::fromLatin1("1/1250")) << Exif::RangeWidget::Value(1.0 / 1000, QString::fromLatin1("1/1000")) << Exif::RangeWidget::Value(1.0 / 800, QString::fromLatin1("1/800")) << Exif::RangeWidget::Value(1.0 / 640, QString::fromLatin1("1/640")) << Exif::RangeWidget::Value(1.0 / 500, QString::fromLatin1("1/500")) << Exif::RangeWidget::Value(1.0 / 400, QString::fromLatin1("1/400")) << Exif::RangeWidget::Value(1.0 / 320, QString::fromLatin1("1/320")) << Exif::RangeWidget::Value(1.0 / 250, QString::fromLatin1("1/250")) << Exif::RangeWidget::Value(1.0 / 200, QString::fromLatin1("1/200")) << Exif::RangeWidget::Value(1.0 / 160, QString::fromLatin1("1/160")) << Exif::RangeWidget::Value(1.0 / 125, QString::fromLatin1("1/125")) << Exif::RangeWidget::Value(1.0 / 100, QString::fromLatin1("1/100")) << Exif::RangeWidget::Value(1.0 / 80, QString::fromLatin1("1/80")) << Exif::RangeWidget::Value(1.0 / 60, QString::fromLatin1("1/60")) << Exif::RangeWidget::Value(1.0 / 50, QString::fromLatin1("1/50")) << Exif::RangeWidget::Value(1.0 / 40, QString::fromLatin1("1/40")) << Exif::RangeWidget::Value(1.0 / 30, QString::fromLatin1("1/30")) << Exif::RangeWidget::Value(1.0 / 25, QString::fromLatin1("1/25")) << Exif::RangeWidget::Value(1.0 / 20, QString::fromLatin1("1/20")) << Exif::RangeWidget::Value(1.0 / 15, QString::fromLatin1("1/15")) << Exif::RangeWidget::Value(1.0 / 13, QString::fromLatin1("1/13")) << Exif::RangeWidget::Value(1.0 / 10, QString::fromLatin1("1/10")) << Exif::RangeWidget::Value(1.0 / 8, QString::fromLatin1("1/8")) << Exif::RangeWidget::Value(1.0 / 6, QString::fromLatin1("1/6")) << Exif::RangeWidget::Value(1.0 / 5, QString::fromLatin1("1/5")) << Exif::RangeWidget::Value(1.0 / 4, QString::fromLatin1("1/4")) << Exif::RangeWidget::Value(0.3, QString::fromLatin1("0.3 %1").arg(secs)) << Exif::RangeWidget::Value(0.4, QString::fromLatin1("0.4 %1").arg(secs)) << Exif::RangeWidget::Value(0.5, QString::fromLatin1("0.5 %1").arg(secs)) << Exif::RangeWidget::Value(0.6, QString::fromLatin1("0.6 %1").arg(secs)) << Exif::RangeWidget::Value(0.8, QString::fromLatin1("0.8 %1").arg(secs)) << Exif::RangeWidget::Value(1, i18n("1 second")) << Exif::RangeWidget::Value(1.3, QString::fromLatin1("1.3 %1").arg(secs)) << Exif::RangeWidget::Value(1.6, QString::fromLatin1("1.6 %1").arg(secs)) << Exif::RangeWidget::Value(2, QString::fromLatin1("2 %1").arg(secs)) << Exif::RangeWidget::Value(2.5, QString::fromLatin1("2.5 %1").arg(secs)) << Exif::RangeWidget::Value(3.2, QString::fromLatin1("3.2 %1").arg(secs)) << Exif::RangeWidget::Value(4, QString::fromLatin1("4 %1").arg(secs)) << Exif::RangeWidget::Value(5, QString::fromLatin1("5 %1").arg(secs)) << Exif::RangeWidget::Value(6, QString::fromLatin1("6 %1").arg(secs)) << Exif::RangeWidget::Value(8, QString::fromLatin1("8 %1").arg(secs)) << Exif::RangeWidget::Value(10, QString::fromLatin1("10 %1").arg(secs)) << Exif::RangeWidget::Value(13, QString::fromLatin1("13 %1").arg(secs)) << Exif::RangeWidget::Value(15, QString::fromLatin1("15 %1").arg(secs)) << Exif::RangeWidget::Value(20, QString::fromLatin1("20 %1").arg(secs)) << Exif::RangeWidget::Value(25, QString::fromLatin1("25 %1").arg(secs)) << Exif::RangeWidget::Value(30, QString::fromLatin1("30 %1").arg(secs)); m_exposureTime = new RangeWidget(i18n("Exposure time"), QString::fromLatin1("Exif_Photo_ExposureTime"), list, layout, 1); } RangeWidget *Exif::SearchDialog::makeApertureOrFNumber(const QString &text, const QString &key, QGridLayout *layout, int row) { Exif::RangeWidget::ValueList list; list << Exif::RangeWidget::Value(1.4, QString::fromLatin1("1.4")) << Exif::RangeWidget::Value(1.8, QString::fromLatin1("1.8")) << Exif::RangeWidget::Value(2.0, QString::fromLatin1("2.0")) << Exif::RangeWidget::Value(2.2, QString::fromLatin1("2.2")) << Exif::RangeWidget::Value(2.5, QString::fromLatin1("2.5")) << Exif::RangeWidget::Value(2.8, QString::fromLatin1("2.8")) << Exif::RangeWidget::Value(3.2, QString::fromLatin1("3.2")) << Exif::RangeWidget::Value(3.5, QString::fromLatin1("3.5")) << Exif::RangeWidget::Value(4.0, QString::fromLatin1("4.0")) << Exif::RangeWidget::Value(4.5, QString::fromLatin1("4.5")) << Exif::RangeWidget::Value(5.0, QString::fromLatin1("5.0")) << Exif::RangeWidget::Value(5.6, QString::fromLatin1("5.6")) << Exif::RangeWidget::Value(6.3, QString::fromLatin1("6.3")) << Exif::RangeWidget::Value(7.1, QString::fromLatin1("7.1")) << Exif::RangeWidget::Value(8.0, QString::fromLatin1("8.0")) << Exif::RangeWidget::Value(9.0, QString::fromLatin1("9.0")) << Exif::RangeWidget::Value(10, QString::fromLatin1("10")) << Exif::RangeWidget::Value(11, QString::fromLatin1("11")) << Exif::RangeWidget::Value(13, QString::fromLatin1("13")) << Exif::RangeWidget::Value(14, QString::fromLatin1("14")) << Exif::RangeWidget::Value(16, QString::fromLatin1("16")) << Exif::RangeWidget::Value(18, QString::fromLatin1("18")) << Exif::RangeWidget::Value(20, QString::fromLatin1("20")) << Exif::RangeWidget::Value(22, QString::fromLatin1("22")) << Exif::RangeWidget::Value(25, QString::fromLatin1("25")) << Exif::RangeWidget::Value(29, QString::fromLatin1("29")) << Exif::RangeWidget::Value(32, QString::fromLatin1("32")) << Exif::RangeWidget::Value(36, QString::fromLatin1("36")) << Exif::RangeWidget::Value(40, QString::fromLatin1("40")) << Exif::RangeWidget::Value(45, QString::fromLatin1("45")); return new RangeWidget(text, key, list, layout, row); } #define addSetting(settings, text, num) \ { \ QCheckBox *cb = new QCheckBox(i18n(text), box); \ settings.append(Setting<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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/SearchDialog.h b/Exif/SearchDialog.h index f6ec058e..7d234943 100644 --- a/Exif/SearchDialog.h +++ b/Exif/SearchDialog.h @@ -1,77 +1,78 @@ /* Copyright (C) 2003-2010 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. */ #ifndef EXIFSEARCHDIALOG_H #define EXIFSEARCHDIALOG_H -#include "Exif/RangeWidget.h" -#include "Exif/SearchDialogSettings.h" -#include "Exif/SearchInfo.h" +#include "RangeWidget.h" +#include "SearchDialogSettings.h" +#include "SearchInfo.h" + #include <KPageDialog> class QSpinBox; class QGridLayout; namespace Exif { class SearchDialog : public KPageDialog { Q_OBJECT public: explicit SearchDialog(QWidget *parent); Exif::SearchInfo info(); protected: void makeISO(QGridLayout *layout); QWidget *makeExposureProgram(QWidget *parent); QWidget *makeOrientation(QWidget *parent); QWidget *makeMeteringMode(QWidget *parent); QWidget *makeContrast(QWidget *parent); QWidget *makeSharpness(QWidget *parent); QWidget *makeSaturation(QWidget *parent); void makeExposureTime(QGridLayout *layout); RangeWidget *makeApertureOrFNumber(const QString &text, const QString &key, QGridLayout *layout, int row); QWidget *makeCamera(); QWidget *makeLens(); protected slots: void fromFocalLengthChanged(int); void toFocalLengthChanged(int); private: Exif::RangeWidget *m_iso; Exif::RangeWidget *m_exposureTime; Exif::RangeWidget *m_apertureValue; Exif::RangeWidget *m_fNumber; Settings<int> m_exposureProgram; Settings<int> m_orientation; Settings<int> m_meteringMode; Settings<int> m_contrast; Settings<int> m_sharpness; Settings<int> m_saturation; Settings<Database::Camera> m_cameras; Settings<Database::Lens> m_lenses; QSpinBox *m_fromFocalLength; QSpinBox *m_toFocalLength; }; } #endif /* EXIFSEARCHDIALOG_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/SearchInfo.cpp b/Exif/SearchInfo.cpp index 9d04c67e..cc21bf75 100644 --- a/Exif/SearchInfo.cpp +++ b/Exif/SearchInfo.cpp @@ -1,205 +1,207 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "SearchInfo.h" -#include <KLocalizedString> -#include "Exif/Database.h" +#include "Database.h" + #include <DB/FileName.h> +#include <KLocalizedString> + /** * \class Exif::SearchInfo * This class represents a search for Exif information. It is similar in functionality for category searches which is in the * class \ref DB::ImageSearchInfo. * * The search is build, from \ref Exif::SearchDialog, using the functions addRangeKey(), addSearchKey(), and addCamara(). * The search is stored in an instance of \ref DB::ImageSearchInfo, and may later be executed using search(). * Once a search has been executed, the application may ask if a given image is in the search result using matches() */ void Exif::SearchInfo::addSearchKey(const QString &key, const IntList &values) { m_intKeys.append(qMakePair(key, values)); } QStringList Exif::SearchInfo::buildIntKeyQuery() const { QStringList andArgs; for (IntKeyList::ConstIterator intIt = m_intKeys.begin(); intIt != m_intKeys.end(); ++intIt) { QStringList orArgs; QString key = (*intIt).first; IntList values = (*intIt).second; Q_FOREACH (int value, values) { orArgs << QString::fromLatin1("(%1 == %2)").arg(key).arg(value); } if (orArgs.count() != 0) andArgs << QString::fromLatin1("(%1)").arg(orArgs.join(QString::fromLatin1(" or "))); } return andArgs; } void Exif::SearchInfo::addRangeKey(const Range &range) { m_rangeKeys.append(range); } Exif::SearchInfo::Range::Range(const QString &key) : isLowerMin(false) , isLowerMax(false) , isUpperMin(false) , isUpperMax(false) , key(key) { } QString Exif::SearchInfo::buildQuery() const { QStringList subQueries; subQueries += buildIntKeyQuery(); subQueries += buildRangeQuery(); QString cameraQuery = buildCameraSearchQuery(); if (!cameraQuery.isEmpty()) subQueries.append(cameraQuery); QString lensQuery = buildLensSearchQuery(); if (!lensQuery.isEmpty()) subQueries.append(lensQuery); if (subQueries.empty()) return QString(); else return QString::fromLatin1("SELECT filename from exif WHERE %1") .arg(subQueries.join(QString::fromLatin1(" and "))); } QStringList Exif::SearchInfo::buildRangeQuery() const { QStringList result; for (QList<Range>::ConstIterator it = m_rangeKeys.begin(); it != m_rangeKeys.end(); ++it) { QString str = sqlForOneRangeItem(*it); if (!str.isEmpty()) result.append(str); } return result; } QString Exif::SearchInfo::sqlForOneRangeItem(const Range &range) const { // Notice I multiplied factors on each value to ensure that we do not fail due to rounding errors for say 1/3 if (range.isLowerMin) { // Min to Min means < x if (range.isUpperMin) return QString::fromLatin1("%1 < %2 and %3 > 0").arg(range.key).arg(range.min * 1.01).arg(range.key); // Min to Max means all images if (range.isUpperMax) return QString(); // Min to y means <= y return QString::fromLatin1("%1 <= %2 and %3 > 0").arg(range.key).arg(range.max * 1.01).arg(range.key); } // MAX to MAX means >= y if (range.isLowerMax) return QString::fromLatin1("%1 > %2").arg(range.key).arg(range.max * 0.99); // x to Max means >= x if (range.isUpperMax) return QString::fromLatin1("%1 >= %2").arg(range.key).arg(range.min * 0.99); // x to y means >=x and <=y return QString::fromLatin1("(%1 <= %2 and %3 <= %4)") .arg(range.min * 0.99) .arg(range.key) .arg(range.key) .arg(range.max * 1.01); } void Exif::SearchInfo::search() const { QString queryStr = buildQuery(); m_emptyQuery = queryStr.isEmpty(); // ensure to do SQL queries as little as possible. static QString lastQuery; if (queryStr == lastQuery) return; lastQuery = queryStr; m_matches.clear(); if (m_emptyQuery) return; m_matches = Exif::Database::instance()->filesMatchingQuery(queryStr); } bool Exif::SearchInfo::matches(const DB::FileName &fileName) const { if (m_emptyQuery) return true; return m_matches.contains(fileName); } bool Exif::SearchInfo::isNull() const { return buildQuery().isEmpty(); } void Exif::SearchInfo::addCamera(const CameraList &list) { m_cameras = list; } void Exif::SearchInfo::addLens(const LensList &list) { m_lenses = list; } QString Exif::SearchInfo::buildCameraSearchQuery() const { QStringList subResults; for (CameraList::ConstIterator cameraIt = m_cameras.begin(); cameraIt != m_cameras.end(); ++cameraIt) { subResults.append(QString::fromUtf8("(Exif_Image_Make='%1' and Exif_Image_Model='%2')") .arg((*cameraIt).first) .arg((*cameraIt).second)); } if (subResults.count() != 0) return QString::fromUtf8("(%1)").arg(subResults.join(QString::fromLatin1(" or "))); else return QString(); } QString Exif::SearchInfo::buildLensSearchQuery() const { QStringList subResults; for (LensList::ConstIterator lensIt = m_lenses.begin(); lensIt != m_lenses.end(); ++lensIt) { if (*lensIt == i18nc("As in No persons, no locations etc.", "None")) // compare to null (=entry from old db schema) and empty string (=entry w/o exif lens info) subResults.append(QString::fromUtf8("(nullif(Exif_Photo_LensModel,'') is null)")); else subResults.append(QString::fromUtf8("(Exif_Photo_LensModel='%1')") .arg(*lensIt)); } if (subResults.count() != 0) return QString::fromUtf8("(%1)").arg(subResults.join(QString::fromLatin1(" or "))); else return QString(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/SearchInfo.h b/Exif/SearchInfo.h index 8ab0a05b..a1d5e413 100644 --- a/Exif/SearchInfo.h +++ b/Exif/SearchInfo.h @@ -1,81 +1,83 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef EXIFSEARCHINFO_H #define EXIFSEARCHINFO_H -#include "Exif/Database.h" +#include "Database.h" + #include <DB/FileName.h> + #include <QList> #include <QPair> #include <QStringList> namespace Exif { class SearchInfo { public: typedef Database::CameraList CameraList; typedef Database::Camera Camera; typedef Database::LensList LensList; typedef Database::Lens Lens; typedef QList<int> IntList; class Range { public: Range() {} explicit Range(const QString &key); bool isLowerMin, isLowerMax, isUpperMin, isUpperMax; double min, max; QString key; }; void addSearchKey(const QString &key, const IntList &values); void addRangeKey(const Range &range); void addCamera(const CameraList &list); void addLens(const LensList &list); void search() const; bool matches(const DB::FileName &fileName) const; bool isNull() const; protected: QString buildQuery() const; QStringList buildIntKeyQuery() const; QStringList buildRangeQuery() const; QString buildCameraSearchQuery() const; QString buildLensSearchQuery() const; QString sqlForOneRangeItem(const Range &) const; private: typedef QList<QPair<QString, IntList>> IntKeyList; IntKeyList m_intKeys; QList<Range> m_rangeKeys; CameraList m_cameras; LensList m_lenses; mutable DB::FileNameSet m_matches; mutable bool m_emptyQuery; }; } #endif /* EXIFSEARCHINFO_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/TreeView.cpp b/Exif/TreeView.cpp index 19c359c1..9261ce7f 100644 --- a/Exif/TreeView.cpp +++ b/Exif/TreeView.cpp @@ -1,101 +1,104 @@ /* 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 "Exif/Info.h" -#include "Utilities/StringSet.h" +#include "TreeView.h" + +#include "Info.h" + +#include <Utilities/StringSet.h> + #include <qmap.h> #include <qstringlist.h> 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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Exif/TreeView.h b/Exif/TreeView.h index e9d0601b..37f19e83 100644 --- a/Exif/TreeView.h +++ b/Exif/TreeView.h @@ -1,47 +1,48 @@ /* Copyright (C) 2003-2019 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. */ #ifndef EXIFTREEVIEW_H #define EXIFTREEVIEW_H -#include "Utilities/StringSet.h" +#include <Utilities/StringSet.h> + #include <QTreeWidget> namespace Exif { using Utilities::StringSet; class TreeView : public QTreeWidget { Q_OBJECT public: TreeView(const QString &title, QWidget *parent); StringSet selected(); void setSelectedExif(const StringSet &selected); void reload(); protected slots: void toggleChildren(QTreeWidgetItem *); }; } #endif /* EXIFTREEVIEW_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/HTMLGenerator/Generator.cpp b/HTMLGenerator/Generator.cpp index b3f00e6b..07cfeed9 100644 --- a/HTMLGenerator/Generator.cpp +++ b/HTMLGenerator/Generator.cpp @@ -1,797 +1,795 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "Logging.h" -#include <sys/types.h> -#include <sys/wait.h> - -#include <QApplication> -#include <QDir> -#include <QDomDocument> -#include <QFile> -#include <QList> -#include <QMimeDatabase> -#include <QStandardPaths> - -#include <KConfig> -#include <KConfigGroup> -#include <KIO/CopyJob> -#include <KLocalizedString> -#include <KMessageBox> -#include <KRun> +#include "ImageSizeCheckBox.h" +#include "Logging.h" +#include "Setup.h" #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 <MainWindow/Window.h> #include <Utilities/FileUtil.h> #include <Utilities/VideoUtil.h> -#include "ImageSizeCheckBox.h" -#include "MainWindow/Window.h" -#include "Setup.h" +#include <KConfig> +#include <KConfigGroup> +#include <KIO/CopyJob> +#include <KLocalizedString> +#include <KMessageBox> +#include <KRun> +#include <QApplication> +#include <QDir> +#include <QDomDocument> +#include <QFile> +#include <QList> +#include <QMimeDatabase> +#include <QStandardPaths> +#include <sys/types.h> +#include <sys/wait.h> namespace { QString readFile(const QString &fileName) { if (fileName.isEmpty()) { KMessageBox::error(nullptr, i18n("<p>No file name given!</p>")); return QString(); } QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { //KMessageBox::error( nullptr, i18n("Could not open file %1").arg( fileName ) ); return QString(); } QTextStream stream(&file); QString content = stream.readAll(); file.close(); return content; } } //namespace 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() { qCDebug(HTMLGeneratorLog) << "Generating gallery" << m_setup.title() << "containing" << m_setup.imageList().size() << "entries in" << m_setup.baseDir(); // Generate .kim file if (m_setup.generateKimFile()) { qCDebug(HTMLGeneratorLog) << "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) { qCDebug(HTMLGeneratorLog) << ".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(); qCDebug(HTMLGeneratorLog) << "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 qCDebug(HTMLGeneratorLog) << "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; qCDebug(HTMLGeneratorLog) << "Linking image file..."; bool ok = linkIndexFile(); if (!ok) return; qCDebug(HTMLGeneratorLog) << "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) qCWarning(HTMLGeneratorLog) << 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::copyOrOverwrite(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(); qCDebug(HTMLGeneratorLog) << "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 = 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 ¤t, const DB::FileName &next) { QString themeDir, themeAuthor, themeName; getThemeInfo(&themeDir, &themeName, &themeAuthor); QString content = 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::copyOrOverwrite(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::copyOrOverwrite(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; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/HTMLGenerator/Generator.h b/HTMLGenerator/Generator.h index 51de6cca..04b2495c 100644 --- a/HTMLGenerator/Generator.h +++ b/HTMLGenerator/Generator.h @@ -1,95 +1,96 @@ /* Copyright (C) 2003-2010 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. */ #ifndef HTMLGENERATOR_GENERATOR_H #define HTMLGENERATOR_GENERATOR_H #include "Setup.h" + #include <DB/CategoryPtr.h> #include <ImageManager/ImageClientInterface.h> #include <Utilities/UniqFilenameMapper.h> #include <QEventLoop> #include <QPointer> #include <QProgressDialog> #include <QString> #include <QTemporaryDir> namespace DB { class Id; } namespace HTMLGenerator { class Generator : public QProgressDialog, private ImageManager::ImageClientInterface { Q_OBJECT public: Generator(const Setup &setup, QWidget *parent); ~Generator() override; void generate(); protected slots: void slotCancelGenerate(); void showBrowser(); protected: bool generateIndexPage(int width, int height); bool generateContentPage(int width, int height, const DB::FileName &prevInfo, const DB::FileName ¤t, const DB::FileName &nextInfo); bool linkIndexFile(); QString populateDescription(QList<DB::CategoryPtr> categories, const DB::ImageInfoPtr info); public: QString namePage(int width, int height, const DB::FileName &fileName); QString nameImage(const DB::FileName &fileName, int size); QString createImage(const DB::FileName &id, int size); QString createVideo(const DB::FileName &fileName); QString kimFileName(bool relative); bool writeToFile(const QString &fileName, const QString &str); QString translateToHTML(const QString &); int calculateSteps(); void getThemeInfo(QString *baseDir, QString *name, QString *author); void pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) override; int maxImageSize(); void minImageSize(int &width, int &height); private: Setup m_setup; int m_waitCounter; int m_total; QTemporaryDir m_tempDirHandle; QDir m_tempDir; Utilities::UniqFilenameMapper m_filenameMapper; QSet<QPair<DB::FileName, int>> m_generatedFiles; DB::FileNameSet m_copiedVideos; bool m_hasEnteredLoop; QPointer<QEventLoop> m_eventLoop; QString m_avconv; }; } #endif /* HTMLGENERATOR_GENERATOR_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/HTMLGenerator/HTMLDialog.cpp b/HTMLGenerator/HTMLDialog.cpp index fe45266d..0790377d 100644 --- a/HTMLGenerator/HTMLDialog.cpp +++ b/HTMLGenerator/HTMLDialog.cpp @@ -1,635 +1,634 @@ /* 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 "Generator.h" +#include "ImageSizeCheckBox.h" #include "Logging.h" +#include <DB/CategoryCollection.h> +#include <DB/ImageDB.h> +#include <MainWindow/Window.h> +#include <Settings/SettingsData.h> + +#include <KConfig> +#include <KConfigGroup> +#include <KFileItem> +#include <KIO/DeleteJob> +#include <KIO/StatJob> +#include <KJob> +#include <KJobWidgets> +#include <KLocalizedString> +#include <KMessageBox> +#include <KTextEdit> #include <QCheckBox> #include <QComboBox> #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; HTMLDialog::HTMLDialog(QWidget *parent) : KPageDialog(parent) , m_list() { setWindowTitle(i18nc("@title:window", "HTML Export")); QWidget *mainWidget = new QWidget(this); this->layout()->addWidget(mainWidget); createContentPage(); createLayoutPage(); createDestinationPage(); // destUrl is only relevant for .kim file creation: connect(m_generateKimFile, &QCheckBox::toggled, m_destURL, &QLineEdit::setEnabled); // automatically fill in output directory: connect(m_title, &QLineEdit::editingFinished, this, &HTMLDialog::slotSuggestOutputDir); QDialogButtonBox *buttonBox = this->buttonBox(); connect(buttonBox, &QDialogButtonBox::accepted, this, &HTMLDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &HTMLDialog::reject); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); okButton->setEnabled(m_themes.size() > 0); connect(okButton, &QPushButton::clicked, this, &HTMLDialog::slotOk); this->layout()->addWidget(buttonBox); } void HTMLDialog::createContentPage() { QWidget *contentPage = new QWidget; KPageWidgetItem *page = new KPageWidgetItem(contentPage, i18n("Content")); page->setHeader(i18n("Content")); page->setIcon(QIcon::fromTheme(QString::fromLatin1("document-properties"))); addPage(page); QVBoxLayout *lay1 = new QVBoxLayout(contentPage); QGridLayout *lay2 = new QGridLayout; lay1->addLayout(lay2); QLabel *label = new QLabel(i18n("Page title:"), contentPage); lay2->addWidget(label, 0, 0); m_title = new QLineEdit(contentPage); label->setBuddy(m_title); lay2->addWidget(m_title, 0, 1); // Copyright label = new QLabel(i18n("Copyright:"), contentPage); label->setAlignment(Qt::AlignTop); lay2->addWidget(label, 1, 0); m_copyright = new QLineEdit(contentPage); m_copyright->setText(Settings::SettingsData::instance()->HTMLCopyright()); label->setBuddy(m_copyright); lay2->addWidget(m_copyright, 1, 1); // Description label = new QLabel(i18n("Description:"), contentPage); label->setAlignment(Qt::AlignTop); lay2->addWidget(label, 2, 0); m_description = new KTextEdit(contentPage); label->setBuddy(m_description); lay2->addWidget(m_description, 2, 1); m_generateKimFile = new QCheckBox(i18n("Create .kim export file"), contentPage); m_generateKimFile->setChecked(Settings::SettingsData::instance()->HTMLKimFile()); lay1->addWidget(m_generateKimFile); m_inlineMovies = new QCheckBox(i18nc("Inline as a verb, i.e. 'please show movies right on the page, not as links'", "Inline Movies in pages"), contentPage); m_inlineMovies->setChecked(Settings::SettingsData::instance()->HTMLInlineMovies()); lay1->addWidget(m_inlineMovies); m_html5Video = new QCheckBox(i18nc("Tag as in HTML-tag, not as in image tag", "Use HTML5 video tag"), contentPage); m_html5Video->setChecked(Settings::SettingsData::instance()->HTML5Video()); lay1->addWidget(m_html5Video); QString avconv = QStandardPaths::findExecutable(QString::fromUtf8("avconv")); const QString ffmpeg2theora = QStandardPaths::findExecutable(QString::fromUtf8("ffmpeg2theora")); QStandardPaths::findExecutable(QString::fromUtf8("avconv")); if (avconv.isNull()) avconv = QStandardPaths::findExecutable(QString::fromUtf8("ffmpeg")); QString txt = i18n("<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; qCDebug(HTMLGeneratorLog) << "Theme directories:" << dirs; for (QStringList::Iterator it = dirs.begin(); it != dirs.end(); ++it) { QDir dir(*it); qCDebug(HTMLGeneratorLog) << "Searching themes in:" << dir; QStringList themes = dir.entryList(QDir::Dirs | QDir::Readable); for (QStringList::Iterator it = themes.begin(); it != themes.end(); ++it) { qCDebug(HTMLGeneratorLog) << " *" << *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()) { qCWarning(HTMLGeneratorLog) << "HTMLDialog called without images for export"; return false; } m_list = list; return QDialog::exec(); } Setup HTMLGenerator::HTMLDialog::setup() const { Setup setup; setup.setTitle(m_title->text()); setup.setBaseDir(m_baseDir->text()); if (m_openInBrowser->isEnabled()) { setup.setBaseURL(m_baseDir->text()); } setup.setDestURL(m_destURL->text()); setup.setOutputDir(m_outputDir->text()); setup.setThumbSize(m_thumbSize->value()); setup.setCopyright(m_copyright->text()); setup.setDate(m_date->isChecked()); setup.setDescription(m_description->toPlainText()); setup.setNumOfCols(m_numOfCols->value()); setup.setGenerateKimFile(m_generateKimFile->isChecked()); setup.setThemePath(m_themes[m_themeBox->currentIndex()]); for (QMap<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; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/HTMLGenerator/HTMLDialog.h b/HTMLGenerator/HTMLDialog.h index 8eedf73e..3ecf740b 100644 --- a/HTMLGenerator/HTMLDialog.h +++ b/HTMLGenerator/HTMLDialog.h @@ -1,98 +1,98 @@ /* Copyright (C) 2003-2010 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. */ #ifndef HTMLGENERATOR_HTMLDIALOG_H #define HTMLGENERATOR_HTMLDIALOG_H +#include <DB/FileNameList.h> + #include <KComboBox> #include <KPageDialog> -#include <DB/FileNameList.h> - class QCheckBox; class QComboBox; class QLabel; class QLineEdit; class QSpinBox; class QTextEdit; namespace HTMLGenerator { class ImageSizeCheckBox; class Generator; class Setup; class HTMLDialog : public KPageDialog { Q_OBJECT public: explicit HTMLDialog(QWidget *parent); // prevent hiding of base class method: using KPageDialog::exec; int exec(const DB::FileNameList &list); protected slots: void slotOk(); void selectDir(); void displayThemeDescription(int); void slotUpdateOutputLabel(); void slotSuggestOutputDir(); protected: bool checkVars(); Setup setup() const; QList<ImageSizeCheckBox *> activeResolutions() const; QString activeSizes() const; QString includeSelections() const; void populateThemesCombo(); void createContentPage(); void createLayoutPage(); void createDestinationPage(); private: QLineEdit *m_title; QLineEdit *m_baseDir; QLineEdit *m_baseURL; QLineEdit *m_destURL; QLineEdit *m_outputDir; QLabel *m_outputLabel; QCheckBox *m_openInBrowser; QLineEdit *m_copyright; QCheckBox *m_date; QSpinBox *m_thumbSize; QTextEdit *m_description; QSpinBox *m_numOfCols; QCheckBox *m_generateKimFile; QCheckBox *m_inlineMovies; QCheckBox *m_html5Video; QCheckBox *m_html5VideoGenerate; QMap<int, QString> m_themes; KComboBox *m_themeBox; QLabel *m_themeInfo; QStringList m_themeAuthors; QStringList m_themeDescriptions; QMap<QString, QCheckBox *> m_whatToIncludeMap; QList<ImageSizeCheckBox *> m_sizeCheckBoxes; DB::FileNameList m_list; }; } #endif /* HTMLGENERATOR_HTMLDIALOG_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/HTMLGenerator/Setup.h b/HTMLGenerator/Setup.h index 408d867c..42884f69 100644 --- a/HTMLGenerator/Setup.h +++ b/HTMLGenerator/Setup.h @@ -1,115 +1,115 @@ /* Copyright (C) 2003-2010 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. */ #ifndef HTMLGENERATOR_SETUP_H #define HTMLGENERATOR_SETUP_H +#include <DB/FileNameList.h> + #include <QList> #include <QMap> #include <QString> -#include <DB/FileNameList.h> - namespace HTMLGenerator { class ImageSizeCheckBox; class Setup { public: Setup(); void setTitle(const QString &title); QString title() const; void setBaseDir(const QString &baseDir); QString baseDir() const; void setBaseURL(const QString &baseURL); QString baseURL() const; void setDestURL(const QString &destURL); QString destURL() const; void setOutputDir(const QString &outputDir); QString outputDir() const; void setThumbSize(int thumbSize); int thumbSize() const; void setCopyright(const QString ©right); QString copyright() const; void setDate(bool date); bool date() const; void setDescription(const QString &description); QString description() const; void setNumOfCols(int numOfCols); int numOfCols() const; void setGenerateKimFile(bool generateKimFile); bool generateKimFile() const; void setThemePath(const QString &theme); QString themePath() const; void setIncludeCategory(const QString &category, bool include); bool includeCategory(const QString &category) const; void setResolutions(const QList<ImageSizeCheckBox *> &sizes); const QList<HTMLGenerator::ImageSizeCheckBox *> &activeResolutions() const; void setImageList(const DB::FileNameList &files); DB::FileNameList imageList() const; void setInlineMovies(bool inlineMovie); bool inlineMovies() const; void setHtml5Video(bool html5Video); bool html5Video() const; void setHtml5VideoGenerate(bool html5VideoGenerate); bool html5VideoGenerate() const; private: QString m_title; QString m_baseDir; QString m_baseURL; QString m_destURL; QString m_outputDir; int m_thumbSize; QString m_copyright; bool m_date; QString m_description; int m_numOfCols; bool m_generateKimFile; QString m_theme; QMap<QString, bool> m_includeCategory; QList<ImageSizeCheckBox *> m_resolutions; DB::FileNameList m_images; bool m_inlineMovies; bool m_html5Video; bool m_html5VideoGenerate; }; } #endif /* HTMLGENERATOR_SETUP_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/AsyncLoader.cpp b/ImageManager/AsyncLoader.cpp index ee585dba..e16cf90a 100644 --- a/ImageManager/AsyncLoader.cpp +++ b/ImageManager/AsyncLoader.cpp @@ -1,245 +1,245 @@ /* Copyright (C) 2003-2010 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 "AsyncLoader.h" -#include <QIcon> -#include <QPixmapCache> +#include "CancelEvent.h" +#include "ImageClientInterface.h" +#include "ImageEvent.h" +#include "ImageLoaderThread.h" +#include "ThumbnailBuilder.h" +#include "ThumbnailCache.h" #include <BackgroundJobs/HandleVideoThumbnailRequestJob.h> #include <BackgroundTaskManager/JobManager.h> -#include <ImageManager/ImageClientInterface.h> #include <MainWindow/FeatureDialog.h> #include <Utilities/VideoUtil.h> -#include "CancelEvent.h" -#include "ImageEvent.h" -#include "ImageLoaderThread.h" -#include "ThumbnailBuilder.h" -#include "ThumbnailCache.h" +#include <QIcon> +#include <QPixmapCache> ImageManager::AsyncLoader *ImageManager::AsyncLoader::s_instance = nullptr; // -- Manager -- ImageManager::AsyncLoader *ImageManager::AsyncLoader::instance() { if (!s_instance) { s_instance = new AsyncLoader; s_instance->init(); } return s_instance; } // We need this as a separate method as the s_instance variable will otherwise not be initialized // corrected before the thread starts. void ImageManager::AsyncLoader::init() { // Use up to three cores for thumbnail generation. No more than three as that // likely will make it less efficient due to three cores hitting the harddisk at the same time. // This might limit the throughput on SSD systems, but we likely have a few years before people // put all of their pictures on SSDs. // rlk 20180515: with improvements to the thumbnail generation code, I've conducted // experiments demonstrating benefit even at 2x the number of hyperthreads, even on // an HDD. However, we need to reserve a thread for the UI or it gets very sluggish // We need one more core in the computer for the GUI thread, but we won't dedicate it to GUI, // as that'd mean that a dual-core box would only have one core decoding images, which would be // suboptimal. // In case of only one core in the computer, use one core for thumbnail generation // TODO(isilmendil): It seems that many people have their images on NFS-mounts. // Should we somehow detect this and allocate less threads there? // rlk 20180515: IMO no; if anything, we need more threads to hide // the latency of NFS. const int cores = qMax(1, qMin(16, QThread::idealThreadCount() - 1)); m_exitRequested = false; for (int i = 0; i < cores; ++i) { ImageLoaderThread *imageLoader = new ImageLoaderThread(); // The thread is set to the lowest priority to ensure that it doesn't starve the GUI thread. m_threadList << imageLoader; imageLoader->start(QThread::IdlePriority); } } bool ImageManager::AsyncLoader::load(ImageRequest *request) { if (m_exitRequested) return false; // rlk 2018-05-15: Skip this check here. Even if the check // succeeds at this point, it may fail later, and if we're suddenly // processing a lot of requests (e. g. a thumbnail build), // this may be very I/O-intensive since it actually has to // read the inode. // silently ignore images not (currently) on disk: // if ( ! request->fileSystemFileName().exists() ) // return false; if (Utilities::isVideo(request->fileSystemFileName())) { if (!loadVideo(request)) return false; } else { loadImage(request); } return true; } bool ImageManager::AsyncLoader::loadVideo(ImageRequest *request) { if (m_exitRequested) return false; if (!MainWindow::FeatureDialog::hasVideoThumbnailer()) return false; BackgroundTaskManager::Priority priority = (request->priority() > ThumbnailInvisible) ? BackgroundTaskManager::ForegroundThumbnailRequest : BackgroundTaskManager::BackgroundVideoThumbnailRequest; BackgroundTaskManager::JobManager::instance()->addJob( new BackgroundJobs::HandleVideoThumbnailRequestJob(request, priority)); return true; } void ImageManager::AsyncLoader::loadImage(ImageRequest *request) { QMutexLocker dummy(&m_lock); if (m_exitRequested) return; QSet<ImageRequest *>::const_iterator req = m_currentLoading.find(request); if (req != m_currentLoading.end() && m_loadList.isRequestStillValid(request)) { // The last part of the test above is needed to not fail on a race condition from AnnotationDialog::ImagePreview, where the preview // at startup request the same image numerous time (likely from resize event). Q_ASSERT(*req != request); delete request; return; // We are currently loading it, calm down and wait please ;-) } // Try harder to find a pending request. Unfortunately, we can't simply use // m_currentLoading.contains() because that will compare pointers // when we want to compare values. for (req = m_currentLoading.begin(); req != m_currentLoading.end(); req++) { ImageRequest *r = *req; if (*request == *r) { delete request; return; // We are currently loading it, calm down and wait please ;-) } } // if request is "fresh" (not yet pending): if (m_loadList.addRequest(request)) m_sleepers.wakeOne(); } void ImageManager::AsyncLoader::stop(ImageClientInterface *client, StopAction action) { // remove from pending map. QMutexLocker requestLocker(&m_lock); m_loadList.cancelRequests(client, action); // PENDING(blackie) Reintroduce this // VideoManager::instance().stop( client, action ); // Was implemented as m_pending.cancelRequests( client, action ); // Where m_pending is the RequestQueue } int ImageManager::AsyncLoader::activeCount() const { QMutexLocker dummy(&m_lock); return m_currentLoading.count(); } bool ImageManager::AsyncLoader::isExiting() const { return m_exitRequested; } ImageManager::ImageRequest *ImageManager::AsyncLoader::next() { QMutexLocker dummy(&m_lock); ImageRequest *request = nullptr; while (!(request = m_loadList.popNext())) m_sleepers.wait(&m_lock); m_currentLoading.insert(request); return request; } void ImageManager::AsyncLoader::requestExit() { m_exitRequested = true; ImageManager::ThumbnailBuilder::instance()->cancelRequests(); m_sleepers.wakeAll(); // TODO(jzarl): check if we can just connect the finished() signal of the threads to deleteLater() // and exit this function without waiting for (QList<ImageLoaderThread *>::iterator it = m_threadList.begin(); it != m_threadList.end(); ++it) { while (!(*it)->isFinished()) { QThread::msleep(10); } delete (*it); } } void ImageManager::AsyncLoader::customEvent(QEvent *ev) { if (ev->type() == ImageEventID) { ImageEvent *iev = dynamic_cast<ImageEvent *>(ev); if (!iev) { Q_ASSERT(iev); return; } ImageRequest *request = iev->loadInfo(); QMutexLocker requestLocker(&m_lock); const bool requestStillNeeded = m_loadList.isRequestStillValid(request); m_loadList.removeRequest(request); m_currentLoading.remove(request); requestLocker.unlock(); QImage image = iev->image(); if (!request->loadedOK()) { if (m_brokenImage.size() != request->size()) { // we can ignore the krazy warning here because we have a valid fallback QIcon brokenFileIcon = QIcon::fromTheme(QLatin1String("file-broken")); // krazy:exclude=iconnames if (brokenFileIcon.isNull()) { brokenFileIcon = QIcon::fromTheme(QLatin1String("image-x-generic")); } m_brokenImage = brokenFileIcon.pixmap(request->size()).toImage(); } image = m_brokenImage; } if (request->isThumbnailRequest()) ImageManager::ThumbnailCache::instance()->insert(request->databaseFileName(), image); if (requestStillNeeded && request->client()) { request->client()->pixmapLoaded(request, image); } delete request; } else if (ev->type() == CANCELEVENTID) { CancelEvent *cancelEvent = dynamic_cast<CancelEvent *>(ev); cancelEvent->request()->client()->requestCanceled(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/AsyncLoader.h b/ImageManager/AsyncLoader.h index b9631f73..0c6cbaf6 100644 --- a/ImageManager/AsyncLoader.h +++ b/ImageManager/AsyncLoader.h @@ -1,87 +1,88 @@ /* Copyright (C) 2003-2010 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. */ #ifndef IMAGEMANAGER_ASYNCLOADER_H #define IMAGEMANAGER_ASYNCLOADER_H +#include "RequestQueue.h" +#include "enums.h" + +#include <MainWindow/Window.h> + #include <QImage> #include <QList> #include <QMutex> #include <QWaitCondition> -#include "MainWindow/Window.h" -#include "RequestQueue.h" -#include "enums.h" - class QEvent; namespace ImageManager { class ImageRequest; class ImageClientInterface; class ImageLoaderThread; // This class needs to inherit QObject to be capable of receiving events. class AsyncLoader : public QObject { Q_OBJECT public: static AsyncLoader *instance(); // Request to load an image. The Manager takes over the ownership of // the request (and may delete it anytime). bool load(ImageRequest *request); // Stop loading all images requested by the given client. void stop(ImageClientInterface *, StopAction action = StopAll); int activeCount() const; bool isExiting() const; protected: void customEvent(QEvent *ev) override; bool loadVideo(ImageRequest *); void loadImage(ImageRequest *); private: friend class ImageLoaderThread; // may call 'next()' friend class MainWindow::Window; // may call 'requestExit()' void init(); ImageRequest *next(); void requestExit(); static AsyncLoader *s_instance; RequestQueue m_loadList; QWaitCondition m_sleepers; // m_lock protects m_loadList and m_currentLoading mutable QMutex m_lock; QSet<ImageRequest *> m_currentLoading; QImage m_brokenImage; QList<ImageLoaderThread *> m_threadList; bool m_exitRequested; int m_exitRequestsProcessed; }; } #endif /* IMAGEMANAGER_ASYNCLOADER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/CancelEvent.cpp b/ImageManager/CancelEvent.cpp index 4125cd2f..ebee5dfe 100644 --- a/ImageManager/CancelEvent.cpp +++ b/ImageManager/CancelEvent.cpp @@ -1,36 +1,37 @@ /* Copyright (C) 2003-2011 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 "CancelEvent.h" + #include "ImageRequest.h" ImageManager::CancelEvent::CancelEvent(ImageRequest *request) : QEvent(static_cast<QEvent::Type>(CANCELEVENTID)) , m_request(request) { } ImageManager::CancelEvent::~CancelEvent() { delete m_request; } ImageManager::ImageRequest *ImageManager::CancelEvent::request() const { return m_request; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ExtractOneVideoFrame.cpp b/ImageManager/ExtractOneVideoFrame.cpp index 0b093f37..546da20d 100644 --- a/ImageManager/ExtractOneVideoFrame.cpp +++ b/ImageManager/ExtractOneVideoFrame.cpp @@ -1,173 +1,172 @@ /* Copyright 2012-2019 The KPhotoAlbum Development Team 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 "ExtractOneVideoFrame.h" + #include "Logging.h" #include <DB/CategoryCollection.h> #include <DB/ImageDB.h> #include <MainWindow/DirtyIndicator.h> #include <MainWindow/FeatureDialog.h> #include <MainWindow/TokenEditor.h> #include <MainWindow/Window.h> #include <Utilities/Process.h> #include <Utilities/StringSet.h> #include <KLocalizedString> #include <KMessageBox> - #include <QDir> - #include <cstdlib> namespace ImageManager { QString ExtractOneVideoFrame::s_tokenForShortVideos; #define STR(x) QString::fromUtf8(x) void ExtractOneVideoFrame::extract(const DB::FileName &fileName, double offset, QObject *receiver, const char *slot) { if (MainWindow::FeatureDialog::hasVideoThumbnailer()) new ExtractOneVideoFrame(fileName, offset, receiver, slot); } ExtractOneVideoFrame::ExtractOneVideoFrame(const DB::FileName &fileName, double offset, QObject *receiver, const char *slot) { m_fileName = fileName; m_process = new Utilities::Process(this); setupWorkingDirectory(); m_process->setWorkingDirectory(m_workingDirectory); connect(m_process, SIGNAL(finished(int)), this, SLOT(frameFetched())); connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(handleError(QProcess::ProcessError))); connect(this, SIGNAL(result(QImage)), receiver, slot); Q_ASSERT(MainWindow::FeatureDialog::hasVideoThumbnailer()); QStringList arguments; // analyzeduration is for videos where the videostream starts later than the sound arguments << STR("-ss") << QString::number(offset, 'f', 4) << STR("-analyzeduration") << STR("200M") << STR("-i") << fileName.absolute() << STR("-vf") << STR("thumbnail") << STR("-vframes") << STR("20") << m_workingDirectory + STR("/000000%02d.png"); qCDebug(ImageManagerLog, "%s %s", qPrintable(MainWindow::FeatureDialog::ffmpegBinary()), qPrintable(arguments.join(QString::fromLatin1(" ")))); m_process->start(MainWindow::FeatureDialog::ffmpegBinary(), arguments); } void ExtractOneVideoFrame::frameFetched() { if (!QFile::exists(m_workingDirectory + STR("/00000020.png"))) markShortVideo(m_fileName); QString name; for (int i = 20; i > 0; --i) { name = m_workingDirectory + STR("/000000%1.png").arg(i, 2, 10, QChar::fromLatin1('0')); if (QFile::exists(name)) { qCDebug(ImageManagerLog) << "Using video frame " << i; break; } } QImage image(name); emit result(image); deleteWorkingDirectory(); deleteLater(); } void ExtractOneVideoFrame::handleError(QProcess::ProcessError error) { QString message; switch (error) { case QProcess::FailedToStart: message = i18n("Failed to start"); break; case QProcess::Crashed: message = i18n("Crashed"); break; case QProcess::Timedout: message = i18n("Timedout"); break; case QProcess::ReadError: message = i18n("Read error"); break; case QProcess::WriteError: message = i18n("Write error"); break; case QProcess::UnknownError: message = i18n("Unknown error"); break; } KMessageBox::information(MainWindow::Window::theMainWindow(), i18n("<p>Error when extracting video thumbnails.<br/>Error was: %1</p>", message), QString(), QLatin1String("errorWhenRunningQProcessFromExtractOneVideoFrame")); emit result(QImage()); deleteLater(); } void ExtractOneVideoFrame::setupWorkingDirectory() { const QString tmpPath = STR("%1/KPA-XXXXXX").arg(QDir::tempPath()); m_workingDirectory = QString::fromUtf8(mkdtemp(tmpPath.toUtf8().data())); } void ExtractOneVideoFrame::deleteWorkingDirectory() { QDir dir(m_workingDirectory); QStringList files = dir.entryList(QDir::Files); for (const QString &file : files) dir.remove(file); dir.rmdir(m_workingDirectory); } void ExtractOneVideoFrame::markShortVideo(const DB::FileName &fileName) { if (s_tokenForShortVideos.isNull()) { Utilities::StringSet usedTokens = MainWindow::TokenEditor::tokensInUse().toSet(); for (int ch = 'A'; ch <= 'Z'; ++ch) { QString token = QChar::fromLatin1((char)ch); if (!usedTokens.contains(token)) { s_tokenForShortVideos = token; break; } } if (s_tokenForShortVideos.isNull()) { // Hmmm, no free token. OK lets just skip setting tokens. return; } KMessageBox::information(MainWindow::Window::theMainWindow(), i18n("Unable to extract video thumbnails from some files. " "Either the file is damaged in some way, or the video is ultra short. " "For your convenience, the token '%1' " "has been set on those videos.\n\n" "(You might need to wait till the video extraction led in your status bar has stopped blinking, " "to see all affected videos.)", s_tokenForShortVideos)); } DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); info->addCategoryInfo(tokensCategory->name(), s_tokenForShortVideos); MainWindow::DirtyIndicator::markDirty(); } } // namespace ImageManager // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ExtractOneVideoFrame.h b/ImageManager/ExtractOneVideoFrame.h index adc9ae87..c35cf9c1 100644 --- a/ImageManager/ExtractOneVideoFrame.h +++ b/ImageManager/ExtractOneVideoFrame.h @@ -1,70 +1,70 @@ /* Copyright 2012 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/>. */ #ifndef IMAGEMANAGER_EXTRACTONEVIDEOFRAME_H #define IMAGEMANAGER_EXTRACTONEVIDEOFRAME_H +#include <DB/FileName.h> + #include <QObject> #include <QProcess> -#include <DB/FileName.h> - class QImage; namespace Utilities { class Process; } namespace ImageManager { /** \brief Extract a thumbnail given a filename and offset. \see \ref videothumbnails */ class ExtractOneVideoFrame : public QObject { Q_OBJECT public: static void extract(const DB::FileName &filename, double offset, QObject *receiver, const char *slot); private slots: void frameFetched(); void handleError(QProcess::ProcessError); signals: void result(const QImage &); private: ExtractOneVideoFrame(const DB::FileName &filename, double offset, QObject *receiver, const char *slot); void setupWorkingDirectory(); void deleteWorkingDirectory(); void markShortVideo(const DB::FileName &fileName); QString m_workingDirectory; Utilities::Process *m_process; DB::FileName m_fileName; static QString s_tokenForShortVideos; }; } // namespace ImageManager #endif // IMAGEMANAGER_EXTRACTONEVIDEOFRAME_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ImageClientInterface.cpp b/ImageManager/ImageClientInterface.cpp index 0e02d903..42a713c7 100644 --- a/ImageManager/ImageClientInterface.cpp +++ b/ImageManager/ImageClientInterface.cpp @@ -1,26 +1,27 @@ /* Copyright (C) 2003-2010 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 "ImageClientInterface.h" + #include "AsyncLoader.h" ImageManager::ImageClientInterface::~ImageClientInterface() { AsyncLoader::instance()->stop(this); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ImageDecoder.cpp b/ImageManager/ImageDecoder.cpp index 0fba0873..6649aadf 100644 --- a/ImageManager/ImageDecoder.cpp +++ b/ImageManager/ImageDecoder.cpp @@ -1,54 +1,55 @@ /* Copyright (C) 2003-2010 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 "ImageDecoder.h" + #include <DB/FileName.h> QList<ImageManager::ImageDecoder *> *ImageManager::ImageDecoder::decoders() { static QList<ImageDecoder *> s_decoders; return &s_decoders; } ImageManager::ImageDecoder::ImageDecoder() { decoders()->append(this); } ImageManager::ImageDecoder::~ImageDecoder() { decoders()->removeOne(this); } bool ImageManager::ImageDecoder::decode(QImage *img, const DB::FileName &imageFile, QSize *fullSize, int dim) { for (ImageDecoder *decoder : *decoders()) { if (decoder->_decode(img, imageFile, fullSize, dim)) return true; } return false; } bool ImageManager::ImageDecoder::mightDecode(const DB::FileName &imageFile) { for (ImageDecoder *decoder : *decoders()) { if (decoder->_mightDecode(imageFile)) return true; } return false; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ImageLoaderThread.cpp b/ImageManager/ImageLoaderThread.cpp index e0b00d38..4bc59338 100644 --- a/ImageManager/ImageLoaderThread.cpp +++ b/ImageManager/ImageLoaderThread.cpp @@ -1,157 +1,159 @@ /* Copyright (C) 2003-2010 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 "ImageLoaderThread.h" -#include "ThumbnailCache.h" #include "AsyncLoader.h" #include "ImageDecoder.h" #include "RawImageDecoder.h" -#include "Utilities/FastJpeg.h" -#include "Utilities/ImageUtil.h" +#include "ThumbnailCache.h" + +#include <Utilities/FastJpeg.h> +#include <Utilities/ImageUtil.h> #include <qapplication.h> #include <qfileinfo.h> extern "C" { #include <limits.h> #include <setjmp.h> #include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> } #include "ImageEvent.h" + #include <kcodecs.h> #include <qmatrix.h> namespace ImageManager { // Create a global instance. Its constructor will itself register it. RAWImageDecoder rawdecoder; } ImageManager::ImageLoaderThread::ImageLoaderThread(size_t bufsize) : m_imageLoadBuffer(new char[bufsize]) , m_bufSize(bufsize) { } ImageManager::ImageLoaderThread::~ImageLoaderThread() { delete[] m_imageLoadBuffer; } void ImageManager::ImageLoaderThread::run() { while (true) { ImageRequest *request = AsyncLoader::instance()->next(); Q_ASSERT(request); if (request->isExitRequest()) { return; } bool ok; QImage img = loadImage(request, ok); if (ok) { img = scaleAndRotate(request, img); } request->setLoadedOK(ok); ImageEvent *iew = new ImageEvent(request, img); QApplication::postEvent(AsyncLoader::instance(), iew); } } QImage ImageManager::ImageLoaderThread::loadImage(ImageRequest *request, bool &ok) { int dim = calcLoadSize(request); QSize fullSize; ok = false; if (!request->fileSystemFileName().exists()) return QImage(); QImage img; if (Utilities::isJPEG(request->fileSystemFileName())) { ok = Utilities::loadJPEG(&img, request->fileSystemFileName(), &fullSize, dim, m_imageLoadBuffer, m_bufSize); if (ok == true) request->setFullSize(fullSize); } else { // At first, we have to give our RAW decoders a try. If we allowed // QImage's load() method, it'd for example load a tiny thumbnail from // NEF files, which is not what we want. ok = ImageDecoder::decode(&img, request->fileSystemFileName(), &fullSize, dim); if (ok) request->setFullSize(img.size()); } if (!ok) { // Now we can try QImage's stuff as a fallback... ok = img.load(request->fileSystemFileName().absolute()); if (ok) request->setFullSize(img.size()); } return img; } int ImageManager::ImageLoaderThread::calcLoadSize(ImageRequest *request) { return qMax(request->width(), request->height()); } QImage ImageManager::ImageLoaderThread::scaleAndRotate(ImageRequest *request, QImage img) { if (request->angle() != 0) { QMatrix matrix; matrix.rotate(request->angle()); img = img.transformed(matrix); int angle = (request->angle() + 360) % 360; Q_ASSERT(angle >= 0 && angle <= 360); if (angle == 90 || angle == 270) request->setFullSize(QSize(request->fullSize().height(), request->fullSize().width())); } // If we are looking for a scaled version, then scale if (shouldImageBeScale(img, request)) img = Utilities::scaleImage(img, request->size(), Qt::KeepAspectRatio); return img; } bool ImageManager::ImageLoaderThread::shouldImageBeScale(const QImage &img, ImageRequest *request) { // No size specified, meaning we want it full size. if (request->width() == -1) return false; if (img.width() < request->width() && img.height() < request->height()) { // The image is smaller than the requets. return request->doUpScale(); } return true; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ImageRequest.h b/ImageManager/ImageRequest.h index 9022d0cd..29871ee7 100644 --- a/ImageManager/ImageRequest.h +++ b/ImageManager/ImageRequest.h @@ -1,111 +1,113 @@ /* Copyright (C) 2003-2010 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. */ #ifndef IMAGEREQUEST_H #define IMAGEREQUEST_H #include "enums.h" + #include <DB/FileName.h> + #include <QHash> #include <qsize.h> #include <qstring.h> // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING // // This class is shared among the image loader thead and the GUI tread, if // you don't know the implication of this stay out of this class! // // WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING namespace ImageManager { class ImageClientInterface; class ImageRequest { public: ImageRequest(const DB::FileName &fileName, const QSize &size, int angle, ImageClientInterface *client); virtual ~ImageRequest() {} ImageRequest(bool requestExit); bool isNull() const; /** This is the filename that the media is known by in the database. See \ref fileSystemFileName for details **/ DB::FileName databaseFileName() const; /** This is the file name that needs to be loaded using the image loader. In case of a video file where we are loading the snapshot from a prerendered image, this file name may be different than the one returned from dabataseFileName. In that example, databaseFileName() returns the path to the video file, while fileSystemFileName returns the path to the prerendered image. **/ virtual DB::FileName fileSystemFileName() const; int width() const; int height() const; QSize size() const; int angle() const; ImageClientInterface *client() const; QSize fullSize() const; void setFullSize(const QSize &); void setLoadedOK(bool ok); bool loadedOK() const; void setPriority(const Priority prio); Priority priority() const; bool operator<(const ImageRequest &other) const; bool operator==(const ImageRequest &other) const; virtual bool stillNeeded() const; bool doUpScale() const; void setUpScale(bool b); void setIsThumbnailRequest(bool); bool isThumbnailRequest() const; bool isExitRequest() const; private: bool m_null; DB::FileName m_fileName; int m_width; int m_height; ImageClientInterface *m_client; int m_angle; QSize m_fullSize; Priority m_priority; bool m_loadedOK; bool m_dontUpScale; bool m_isThumbnailRequest; bool m_isExitRequest; }; inline uint qHash(const ImageRequest &ir) { return DB::qHash(ir.databaseFileName()) ^ ::qHash(ir.width()) ^ ::qHash(ir.angle()); } } #endif /* IMAGEREQUEST_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/PreloadRequest.cpp b/ImageManager/PreloadRequest.cpp index f5839c8c..024c7a8f 100644 --- a/ImageManager/PreloadRequest.cpp +++ b/ImageManager/PreloadRequest.cpp @@ -1,31 +1,32 @@ /* Copyright (C) 2003-2011 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 "PreloadRequest.h" + #include "ThumbnailCache.h" ImageManager::PreloadRequest::PreloadRequest(const DB::FileName &fileName, const QSize &size, int angle, ImageClientInterface *client) : ImageRequest(fileName, size, angle, client) { } bool ImageManager::PreloadRequest::stillNeeded() const { return !ThumbnailCache::instance()->contains(databaseFileName()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/RawImageDecoder.cpp b/ImageManager/RawImageDecoder.cpp index 6ba3f651..15165400 100644 --- a/ImageManager/RawImageDecoder.cpp +++ b/ImageManager/RawImageDecoder.cpp @@ -1,268 +1,269 @@ /* Copyright (C) 2003-2010 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 "RawImageDecoder.h" + #include "Logging.h" -#include <config-kpa-kdcraw.h> -#include "Settings/SettingsData.h" #include <DB/FileName.h> +#include <Settings/SettingsData.h> #include <Utilities/FastJpeg.h> #include <QFile> #include <QImage> +#include <config-kpa-kdcraw.h> #ifdef HAVE_KDCRAW #include <KDCRAW/KDcraw> #include <KDCRAW/RawFiles> #include <libkdcraw_version.h> #endif namespace ImageManager { bool RAWImageDecoder::_decode(QImage *img, const DB::FileName &imageFile, QSize *fullSize, int dim) { /* width and height seem to be only hints, ignore */ Q_UNUSED(dim); #ifdef HAVE_KDCRAW QByteArray previewData; if (!KDcrawIface::KDcraw::loadEmbeddedPreview(previewData, imageFile.absolute())) return false; // Faster than allowing loadRawPreview to do the decode itself if (!Utilities::loadJPEG(img, previewData, fullSize, dim)) return false; // FIXME: The preview data for Canon's image is always returned in its non-rotated form by libkdcraw, ie. KPA should do the rotation. // FIXME: This will happen later on. if (Settings::SettingsData::instance()->useRawThumbnail() && ((dim > 0 && img->width() >= dim && img->height() >= dim) || (img->width() >= Settings::SettingsData::instance()->useRawThumbnailSize().width() && img->height() >= Settings::SettingsData::instance()->useRawThumbnailSize().height()))) return true; KDcrawIface::DcrawInfoContainer metadata; if (!KDcrawIface::KDcraw::rawFileIdentify(metadata, imageFile.absolute())) return false; if ((img->width() < metadata.imageSize.width() * 0.8) || (img->height() < metadata.imageSize.height() * 0.8)) { // let's try to get a better resolution KDcrawIface::KDcraw decoder; KDcrawIface::RawDecodingSettings rawDecodingSettings; if (rawDecodingSettings.sixteenBitsImage) { qCWarning(ImageManagerLog) << "16 bits per color channel is not supported yet"; return false; } else { QByteArray imageData; /* 3 bytes for each pixel, */ int width, height, rgbmax; if (!decoder.decodeRAWImage(imageFile.absolute(), rawDecodingSettings, imageData, width, height, rgbmax)) return false; // Now the funny part, how to turn this fugly QByteArray into an QImage. Yay! *img = QImage(width, height, QImage::Format_RGB32); if (img->isNull()) return false; uchar *data = img->bits(); for (int i = 0; i < imageData.size(); i += 3, data += 4) { data[0] = imageData[i + 2]; // blue data[1] = imageData[i + 1]; // green data[2] = imageData[i]; // red data[3] = 0xff; // alpha } } } if (fullSize) *fullSize = img->size(); return true; #else /* HAVE_KDCRAW */ Q_UNUSED(img); Q_UNUSED(imageFile); Q_UNUSED(fullSize); return false; #endif /* HAVE_KDCRAW */ } void RAWImageDecoder::_initializeExtensionLists(QStringList &rawExtensions, QStringList &standardExtensions, QStringList &ignoredExtensions) { static QStringList _rawExtensions, _standardExtensions, _ignoredExtensions; static bool extensionListsInitialized = false; if (!extensionListsInitialized) { #ifdef HAVE_KDCRAW _rawExtensions = QString::fromLatin1(raw_file_extentions).split(QChar::fromLatin1(' '), QString::SkipEmptyParts); #endif /* HAVE_KDCRAW */ for (QStringList::iterator it = _rawExtensions.begin(); it != _rawExtensions.end(); ++it) (*it).remove(QString::fromUtf8("*.")); _standardExtensions << QString::fromLatin1("jpg") << QString::fromLatin1("JPG") << QString::fromLatin1("jpeg") << QString::fromLatin1("JPEG") << QString::fromLatin1("tif") << QString::fromLatin1("TIF") << QString::fromLatin1("tiff") << QString::fromLatin1("TIFF") << QString::fromLatin1("png") << QString::fromLatin1("PNG"); _ignoredExtensions << QString::fromLatin1("thm") // Thumbnails << QString::fromLatin1("THM") << QString::fromLatin1("thumb") // thumbnail files // from dcraw << QString::fromLatin1("ctg") // Catalog files << QString::fromLatin1("gz") // Compressed files << QString::fromLatin1("Z") << QString::fromLatin1("bz2") << QString::fromLatin1("zip") << QString::fromLatin1("xml") << QString::fromLatin1("XML") << QString::fromLatin1("html") << QString::fromLatin1("HTML") << QString::fromLatin1("htm") << QString::fromLatin1("HTM") << QString::fromLatin1("pp3") // RawTherapee Sidecar files << QString::fromLatin1("PP3") << QString::fromLatin1("xmp") // Other sidecars << QString::fromLatin1("XMP") << QString::fromLatin1("pto") // Hugin sidecars << QString::fromLatin1("PTO"); QChar dot(QChar::fromLatin1('.')); for (QStringList::iterator it = _rawExtensions.begin(); it != _rawExtensions.end(); ++it) if (!(*it).startsWith(dot)) *it = dot + *it; for (QStringList::iterator it = _standardExtensions.begin(); it != _standardExtensions.end(); ++it) if (!(*it).startsWith(dot)) *it = dot + *it; for (QStringList::iterator it = _ignoredExtensions.begin(); it != _ignoredExtensions.end(); ++it) if (!(*it).startsWith(dot)) *it = dot + *it; extensionListsInitialized = true; } rawExtensions = _rawExtensions; standardExtensions = _standardExtensions; ignoredExtensions = _ignoredExtensions; } bool RAWImageDecoder::_fileExistsWithExtensions(const DB::FileName &fileName, const QStringList &extensionList) const { QString baseFileName = fileName.absolute(); int extStart = baseFileName.lastIndexOf(QChar::fromLatin1('.')); // We're interested in xxx.yyy, not .yyy if (extStart <= 1) return false; baseFileName.remove(extStart, baseFileName.length() - extStart); for (QStringList::ConstIterator it = extensionList.begin(); it != extensionList.end(); ++it) { if (QFile::exists(baseFileName + *it)) return true; } return false; } bool RAWImageDecoder::_fileIsKnownWithExtensions(const DB::FileNameSet &files, const DB::FileName &fileName, const QStringList &extensionList) const { QString baseFileName = fileName.absolute(); int extStart = baseFileName.lastIndexOf(QChar::fromLatin1('.')); if (extStart <= 1) return false; baseFileName.remove(extStart, baseFileName.length() - extStart); for (QStringList::ConstIterator it = extensionList.begin(); it != extensionList.end(); ++it) { if (files.contains(DB::FileName::fromAbsolutePath(baseFileName + *it))) return true; } return false; } bool RAWImageDecoder::_fileEndsWithExtensions(const DB::FileName &fileName, const QStringList &extensionList) { for (QStringList::ConstIterator it = extensionList.begin(); it != extensionList.end(); ++it) { if (fileName.relative().endsWith(*it, Qt::CaseInsensitive)) return true; } return false; } bool RAWImageDecoder::_mightDecode(const DB::FileName &imageFile) { QStringList _rawExtensions, _standardExtensions, _ignoredExtensions; _initializeExtensionLists(_rawExtensions, _standardExtensions, _ignoredExtensions); if (Settings::SettingsData::instance()->skipRawIfOtherMatches() && _fileExistsWithExtensions(imageFile, _standardExtensions)) return false; if (_fileEndsWithExtensions(imageFile, _rawExtensions)) return true; return false; } bool RAWImageDecoder::isRAW(const DB::FileName &imageFile) { QStringList _rawExtensions, _standardExtensions, _ignoredExtensions; _initializeExtensionLists(_rawExtensions, _standardExtensions, _ignoredExtensions); return _fileEndsWithExtensions(imageFile, _rawExtensions); } QStringList RAWImageDecoder::rawExtensions() { QStringList _rawExtensions, _standardExtensions, _ignoredExtensions; _initializeExtensionLists(_rawExtensions, _standardExtensions, _ignoredExtensions); return _rawExtensions; } bool RAWImageDecoder::_skipThisFile(const DB::FileNameSet &loadedFiles, const DB::FileName &imageFile) const { QStringList _rawExtensions, _standardExtensions, _ignoredExtensions; _initializeExtensionLists(_rawExtensions, _standardExtensions, _ignoredExtensions); // We're not interested in thumbnail and other files. if (_fileEndsWithExtensions(imageFile, _ignoredExtensions)) return true; // If we *are* interested in raw files even when other equivalent // non-raw files are available, then we're interested in this file. if (!(Settings::SettingsData::instance()->skipRawIfOtherMatches())) return false; // If the file ends with something other than a known raw extension, // we're interested in it. if (!_fileEndsWithExtensions(imageFile, _rawExtensions)) return false; // At this point, the file ends with a known raw extension, and we're // not interested in raw files when other non-raw files are available. // So search for an existing file with one of the standard // extensions. // // This may not be the best way to do this, but it's using the // same algorithm as _mightDecode above. // -- Robert Krawitz rlk@alum.mit.edu 2007-07-22 return _fileIsKnownWithExtensions(loadedFiles, imageFile, _standardExtensions); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/RawImageDecoder.h b/ImageManager/RawImageDecoder.h index 135eaab4..e86e1583 100644 --- a/ImageManager/RawImageDecoder.h +++ b/ImageManager/RawImageDecoder.h @@ -1,46 +1,48 @@ /* Copyright (C) 2003-2010 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. */ #ifndef RAWIMAGEDECODER_H #define RAWIMAGEDECODER_H #include "ImageDecoder.h" + #include <DB/FileName.h> + #include <QStringList> namespace ImageManager { class RAWImageDecoder : public ImageDecoder { public: bool _decode(QImage *img, const DB::FileName &imageFile, QSize *fullSize, int dim = -1) override; bool _mightDecode(const DB::FileName &imageFile) override; virtual bool _skipThisFile(const DB::FileNameSet &loadedFiles, const DB::FileName &imageFile) const; static bool isRAW(const DB::FileName &imageFile); static QStringList rawExtensions(); private: bool _fileExistsWithExtensions(const DB::FileName &fileName, const QStringList &extensionList) const; static bool _fileEndsWithExtensions(const DB::FileName &fileName, const QStringList &extensionList); bool _fileIsKnownWithExtensions(const DB::FileNameSet &files, const DB::FileName &fileName, const QStringList &extensionList) const; static void _initializeExtensionLists(QStringList &rawExtensions, QStringList &standardExtensions, QStringList &ignoredExtensions); }; } #endif /* RAWIMAGEDECODER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/RequestQueue.cpp b/ImageManager/RequestQueue.cpp index 5634e2b1..6e3a9ff7 100644 --- a/ImageManager/RequestQueue.cpp +++ b/ImageManager/RequestQueue.cpp @@ -1,116 +1,118 @@ /* Copyright (C) 2003-2010 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 "RequestQueue.h" + #include "AsyncLoader.h" #include "CancelEvent.h" #include "ImageClientInterface.h" #include "ImageRequest.h" + #include <QApplication> bool ImageManager::RequestQueue::addRequest(ImageRequest *request) { const ImageRequestReference ref(request); if (m_uniquePending.contains(ref)) { // We have this very same request already in the queue. Ignore this one. delete request; return false; } m_queues[request->priority()].enqueue(request); m_uniquePending.insert(ref); if (request->client()) m_activeRequests.insert(request); return true; } ImageManager::ImageRequest *ImageManager::RequestQueue::popNext() { QueueType::iterator it = m_queues.end(); // m_queues is initialized to non-zero size do { --it; while (!it->empty()) { ImageRequest *request = it->dequeue(); if (!request->stillNeeded()) { removeRequest(request); request->setLoadedOK(false); CancelEvent *event = new CancelEvent(request); QApplication::postEvent(AsyncLoader::instance(), event); } else { const ImageRequestReference ref(request); m_uniquePending.remove(ref); return request; } } if (AsyncLoader::instance()->isExiting()) return new ImageRequest(true); } while (it != m_queues.begin()); return nullptr; } void ImageManager::RequestQueue::cancelRequests(ImageClientInterface *client, StopAction action) { // remove from active map for (QSet<ImageRequest *>::const_iterator it = m_activeRequests.begin(); it != m_activeRequests.end();) { ImageRequest *request = *it; ++it; // We need to increase it before removing the element. if (client == request->client() && (action == StopAll || (request->priority() < ThumbnailVisible))) { m_activeRequests.remove(request); // active requests are not deleted - they might already have been // popNext()ed and are being processed. They will be deleted // in Manger::customEvent(). } } for (QueueType::iterator qit = m_queues.begin(); qit != m_queues.end(); ++qit) { for (QQueue<ImageRequest *>::iterator it = qit->begin(); it != qit->end(); /* no increment here */) { ImageRequest *request = *it; if (request->client() == client && (action == StopAll || request->priority() < ThumbnailVisible)) { it = qit->erase(it); const ImageRequestReference ref(request); m_uniquePending.remove(ref); delete request; } else { ++it; } } } } bool ImageManager::RequestQueue::isRequestStillValid(ImageRequest *request) { return m_activeRequests.contains(request); } void ImageManager::RequestQueue::removeRequest(ImageRequest *request) { const ImageRequestReference ref(request); m_activeRequests.remove(request); m_uniquePending.remove(ref); } ImageManager::RequestQueue::RequestQueue() { for (int i = 0; i < LastPriority; ++i) m_queues.append(QQueue<ImageRequest *>()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/RequestQueue.h b/ImageManager/RequestQueue.h index f291b30f..4eac994f 100644 --- a/ImageManager/RequestQueue.h +++ b/ImageManager/RequestQueue.h @@ -1,111 +1,112 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef REQUESTQUEUE_H #define REQUESTQUEUE_H -#include "ImageManager/ImageRequest.h" +#include "ImageRequest.h" #include "enums.h" + #include <QQueue> #include <QSet> namespace ImageManager { class ImageClientInterface; // RequestQueue for ImageRequests. Non-synchronized, locking has to be // provided by the user. class RequestQueue { public: RequestQueue(); // Add a new request to the input queue in the right priority level. // @return 'true', if this is not a request already pending. bool addRequest(ImageRequest *request); // Return the next needed ImageRequest from the queue or nullptr if there // is none. The ownership is returned back to the caller so it has to // delete it. ImageRequest *popNext(); // Remove all pending requests from the given client. void cancelRequests(ImageClientInterface *client, StopAction action); bool isRequestStillValid(ImageRequest *request); void removeRequest(ImageRequest *); private: // A Reference to a ImageRequest with value semantic. // This only stores the pointer to an ImageRequest object but behaves // regarding the less-than and equals-operator like the object. // This allows to store ImageRequests with value-semantic in a Set. class ImageRequestReference { public: ImageRequestReference() : m_ptr(nullptr) { } explicit ImageRequestReference(const ImageRequest *ptr) : m_ptr(ptr) { } bool operator<(const ImageRequestReference &other) const { return *m_ptr < *other.m_ptr; } bool operator==(const ImageRequestReference &other) const { return *m_ptr == *other.m_ptr; } operator const ImageRequest &() const { return *m_ptr; } private: const ImageRequest *m_ptr; }; typedef QList<QQueue<ImageRequest *>> QueueType; /** @short Priotized list of queues (= 1 priority queue) of image requests * that are waiting for processing */ QueueType m_queues; /** * Set of unique requests currently pending; used to discard the exact * same requests. * TODO(hzeller): seems, that the unique-pending requests tried to be * handled in different places in kpa but sometimes in a snakeoil * way (it compares pointers instead of the content -> clean up that). */ QSet<ImageRequestReference> m_uniquePending; // All active requests that have a client QSet<ImageRequest *> m_activeRequests; }; } #endif /* REQUESTQUEUE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ThumbnailBuilder.cpp b/ImageManager/ThumbnailBuilder.cpp index 1ae52650..51cf9261 100644 --- a/ImageManager/ThumbnailBuilder.cpp +++ b/ImageManager/ThumbnailBuilder.cpp @@ -1,219 +1,220 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "AsyncLoader.h" #include "Logging.h" #include "PreloadRequest.h" #include "ThumbnailCache.h" #include <DB/ImageDB.h> #include <DB/ImageInfoPtr.h> #include <DB/OptimizedFileList.h> #include <MainWindow/StatusBar.h> #include <ThumbnailView/CellGeometry.h> #include <KLocalizedString> #include <QLoggingCategory> #include <QMessageBox> #include <QTimer> ImageManager::ThumbnailBuilder *ImageManager::ThumbnailBuilder::s_instance = nullptr; ImageManager::ThumbnailBuilder::ThumbnailBuilder(MainWindow::StatusBar *statusBar, QObject *parent) : QObject(parent) , m_statusBar(statusBar) , m_count(0) , m_isBuilding(false) , m_loadedCount(0) , m_preloadQueue(nullptr) , m_scout(nullptr) { connect(m_statusBar, &MainWindow::StatusBar::cancelRequest, this, &ThumbnailBuilder::cancelRequests); s_instance = this; // Make sure that this is created early, in the main thread, so it // can receive signals. ThumbnailCache::instance(); 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::terminateScout() { if (m_scout) { delete m_scout; m_scout = nullptr; } if (m_preloadQueue) { delete m_preloadQueue; m_preloadQueue = nullptr; } } 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_loadedCount++; m_statusBar->setProgress(++m_count); if (m_count >= m_expectedThumbnails) { terminateScout(); } } void ImageManager::ThumbnailBuilder::buildAll(ThumbnailBuildStart when) { QMessageBox msgBox; msgBox.setText(i18n("Building all thumbnails may take a long time.")); msgBox.setInformativeText(i18n("Do you want to rebuild all of your thumbnails?")); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); int ret = msgBox.exec(); if (ret == QMessageBox::Yes) { ImageManager::ThumbnailCache::instance()->flush(); scheduleThumbnailBuild(DB::ImageDB::instance()->images(), when); } } ImageManager::ThumbnailBuilder *ImageManager::ThumbnailBuilder::instance() { Q_ASSERT(s_instance); return s_instance; } ImageManager::ThumbnailBuilder::~ThumbnailBuilder() { terminateScout(); } 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(); DB::OptimizedFileList files(list); m_thumbnailsToBuild = files.optimizedDbFiles(); m_startBuildTimer->start(when == StartNow ? 0 : 5000); } void ImageManager::ThumbnailBuilder::buildOneThumbnail(const DB::ImageInfoPtr &info) { ImageManager::ImageRequest *request = new ImageManager::PreloadRequest(info->fileName(), ThumbnailView::CellGeometry::preferredIconSize(), info->angle(), this); request->setIsThumbnailRequest(true); request->setPriority(ImageManager::BuildThumbnails); ImageManager::AsyncLoader::instance()->load(request); } void ImageManager::ThumbnailBuilder::doThumbnailBuild() { m_isBuilding = true; int numberOfThumbnailsToBuild = 0; terminateScout(); m_count = 0; m_loadedCount = 0; m_preloadQueue = new DB::ImageScoutQueue; for (const DB::FileName &fileName : m_thumbnailsToBuild) { m_preloadQueue->enqueue(fileName); } qCDebug(ImageManagerLog) << "thumbnail builder starting scout"; m_scout = new DB::ImageScout(*m_preloadQueue, m_loadedCount, 1); m_scout->setMaxSeekAhead(10); m_scout->setReadLimit(10 * 1048576); m_scout->start(); m_statusBar->startProgress(i18n("Building thumbnails"), qMax(m_thumbnailsToBuild.size() - 1, 1)); // We'll update this later. Meanwhile, we want to make sure that the scout // isn't prematurely terminated because the expected number of thumbnails // is less than (i. e. zero) the number of thumbnails actually built. m_expectedThumbnails = m_thumbnailsToBuild.size(); for (const DB::FileName &fileName : m_thumbnailsToBuild) { DB::ImageInfoPtr info = fileName.info(); if (ImageManager::AsyncLoader::instance()->isExiting()) { cancelRequests(); break; } if (info->isNull()) { m_loadedCount++; 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; } m_expectedThumbnails = numberOfThumbnailsToBuild; if (numberOfThumbnailsToBuild == 0) { m_statusBar->setProgressBarVisible(false); terminateScout(); } } void ImageManager::ThumbnailBuilder::save() { ImageManager::ThumbnailCache::instance()->save(); } void ImageManager::ThumbnailBuilder::requestCanceled() { m_statusBar->setProgress(++m_count); m_loadedCount++; if (m_count >= m_expectedThumbnails) { terminateScout(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ThumbnailBuilder.h b/ImageManager/ThumbnailBuilder.h index 481ae583..f3814fca 100644 --- a/ImageManager/ThumbnailBuilder.h +++ b/ImageManager/ThumbnailBuilder.h @@ -1,83 +1,85 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef THUMBNAILBUILDER_H #define THUMBNAILBUILDER_H -#include "DB/ImageInfoPtr.h" -#include "ImageManager/ImageClientInterface.h" +#include "ImageClientInterface.h" #include "enums.h" + #include <DB/FileNameList.h> +#include <DB/ImageInfoPtr.h> #include <DB/ImageScout.h> + #include <QAtomicInt> #include <QImage> namespace MainWindow { class StatusBar; } namespace MainWindow { class Window; } class QTimer; namespace ImageManager { class ThumbnailBuilder : public QObject, public ImageManager::ImageClientInterface { Q_OBJECT public: static ThumbnailBuilder *instance(); ~ThumbnailBuilder() override; void pixmapLoaded(ImageRequest *request, const QImage &image) override; void requestCanceled() override; public slots: void buildAll(ThumbnailBuildStart when = ImageManager::StartDelayed); void buildMissing(); void cancelRequests(); void scheduleThumbnailBuild(const DB::FileNameList &list, ThumbnailBuildStart when); void buildOneThumbnail(const DB::ImageInfoPtr &fileName); void doThumbnailBuild(); void save(); private: friend class MainWindow::Window; static ThumbnailBuilder *s_instance; ThumbnailBuilder(MainWindow::StatusBar *statusBar, QObject *parent); void terminateScout(); MainWindow::StatusBar *m_statusBar; int m_count; int m_expectedThumbnails; bool m_isBuilding; QAtomicInt m_loadedCount; DB::ImageScoutQueue *m_preloadQueue; DB::ImageScout *m_scout; QTimer *m_startBuildTimer; DB::FileNameList m_thumbnailsToBuild; }; } #endif /* THUMBNAILBUILDER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ThumbnailCache.cpp b/ImageManager/ThumbnailCache.cpp index 7349b6a9..cdedeff4 100644 --- a/ImageManager/ThumbnailCache.cpp +++ b/ImageManager/ThumbnailCache.cpp @@ -1,479 +1,480 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "ThumbnailCache.h" + #include "Logging.h" #include <DB/FastDir.h> #include <DB/ImageDB.h> #include <MainWindow/Logging.h> #include <Settings/SettingsData.h> #include <QBuffer> #include <QCache> #include <QDir> #include <QElapsedTimer> #include <QFile> #include <QMutexLocker> #include <QPixmap> #include <QTemporaryFile> #include <QTimer> namespace { // We split the thumbnails into chunks to avoid a huge file changing over and over again, with a bad hit for backups constexpr int MAX_FILE_SIZE = 32 * 1024 * 1024; constexpr int THUMBNAIL_FILE_VERSION = 4; // We map some thumbnail files into memory and manage them in a least-recently-used fashion constexpr size_t LRU_SIZE = 2; constexpr int THUMBNAIL_CACHE_SAVE_INTERNAL_MS = (5 * 1000); } namespace ImageManager { /** * The ThumbnailMapping wraps the memory-mapped data of a QFile. * Upon initialization with a file name, the corresponding file is opened * and its contents mapped into memory (as a QByteArray). * * Deleting the ThumbnailMapping unmaps the memory and closes the file. */ class ThumbnailMapping { public: ThumbnailMapping(const QString &filename) : file(filename) , map(nullptr) { if (!file.open(QIODevice::ReadOnly)) qCWarning(ImageManagerLog, "Failed to open thumbnail file"); uchar *data = file.map(0, file.size()); if (!data || QFile::NoError != file.error()) { qCWarning(ImageManagerLog, "Failed to map thumbnail file"); } else { map = QByteArray::fromRawData(reinterpret_cast<const char *>(data), file.size()); } } bool isValid() { return !map.isEmpty(); } // we need to keep the file around to keep the data mapped: QFile file; QByteArray map; }; } ImageManager::ThumbnailCache *ImageManager::ThumbnailCache::s_instance = nullptr; ImageManager::ThumbnailCache::ThumbnailCache() : m_currentFile(0) , m_currentOffset(0) , m_timer(new QTimer) , m_needsFullSave(true) , m_isDirty(false) , m_memcache(new QCache<int, ThumbnailMapping>(LRU_SIZE)) , m_currentWriter(nullptr) { const QString dir = thumbnailPath(QString()); if (!QFile::exists(dir)) QDir().mkpath(dir); load(); connect(this, &ImageManager::ThumbnailCache::doSave, this, &ImageManager::ThumbnailCache::saveImpl); connect(m_timer, &QTimer::timeout, this, &ImageManager::ThumbnailCache::saveImpl); m_timer->setInterval(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); m_timer->setSingleShot(true); m_timer->start(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); } ImageManager::ThumbnailCache::~ThumbnailCache() { m_needsFullSave = true; saveInternal(); delete m_memcache; delete m_timer; if (m_currentWriter) delete m_currentWriter; } void ImageManager::ThumbnailCache::insert(const DB::FileName &name, const QImage &image) { QMutexLocker thumbnailLocker(&m_thumbnailWriterLock); if (!m_currentWriter) { m_currentWriter = new QFile(fileNameForIndex(m_currentFile)); if (!m_currentWriter->open(QIODevice::ReadWrite)) { qCWarning(ImageManagerLog, "Failed to open thumbnail file for inserting"); return; } } if (!m_currentWriter->seek(m_currentOffset)) { qCWarning(ImageManagerLog, "Failed to seek in thumbnail file"); return; } QMutexLocker dataLocker(&m_dataLock); // purge in-memory cache for the current file: m_memcache->remove(m_currentFile); QByteArray data; QBuffer buffer(&data); bool OK = buffer.open(QIODevice::WriteOnly); Q_ASSERT(OK); Q_UNUSED(OK); OK = image.save(&buffer, "JPG"); Q_ASSERT(OK); const int size = data.size(); if (!(m_currentWriter->write(data.data(), size) == size && m_currentWriter->flush())) { qCWarning(ImageManagerLog, "Failed to write image data to thumbnail file"); return; } if (m_currentOffset + size > MAX_FILE_SIZE) { delete m_currentWriter; m_currentWriter = nullptr; } thumbnailLocker.unlock(); if (m_hash.contains(name)) { CacheFileInfo info = m_hash[name]; if (info.fileIndex == m_currentFile && info.offset == m_currentOffset && info.size == size) { qCDebug(ImageManagerLog) << "Found duplicate thumbnail " << name.relative() << "but no change in information"; dataLocker.unlock(); return; } else { // File has moved; incremental save does no good. qCDebug(ImageManagerLog) << "Found duplicate thumbnail " << name.relative() << " at new location, need full save! "; m_saveLock.lock(); m_needsFullSave = true; m_saveLock.unlock(); } } m_hash.insert(name, CacheFileInfo(m_currentFile, m_currentOffset, size)); m_isDirty = true; m_unsavedHash.insert(name, CacheFileInfo(m_currentFile, m_currentOffset, size)); // Update offset m_currentOffset += size; if (m_currentOffset > MAX_FILE_SIZE) { m_currentFile++; m_currentOffset = 0; } int unsaved = m_unsavedHash.count(); dataLocker.unlock(); // Thumbnail building is a lot faster now. Even on an HDD this corresponds to less // than 1 minute of work. // // We need to call the internal version that does not interact with the timer. // We can't simply signal from here because if we're in the middle of loading new // images the signal won't get invoked until we return to the main application loop. if (unsaved >= 100) { saveInternal(); } } QString ImageManager::ThumbnailCache::fileNameForIndex(int index, const QString dir) const { return thumbnailPath(QString::fromLatin1("thumb-") + QString::number(index), dir); } QPixmap ImageManager::ThumbnailCache::lookup(const DB::FileName &name) const { m_dataLock.lock(); CacheFileInfo info = m_hash[name]; m_dataLock.unlock(); ThumbnailMapping *t = m_memcache->object(info.fileIndex); if (!t || !t->isValid()) { t = new ThumbnailMapping(fileNameForIndex(info.fileIndex)); if (!t->isValid()) { qCWarning(ImageManagerLog, "Failed to map thumbnail file"); return QPixmap(); } m_memcache->insert(info.fileIndex, t); } QByteArray array(t->map.mid(info.offset, info.size)); QBuffer buffer(&array); buffer.open(QIODevice::ReadOnly); QImage image; image.load(&buffer, "JPG"); // Notice the above image is sharing the bits with the file, so I can't just return it as it then will be invalid when the file goes out of scope. // PENDING(blackie) Is that still true? return QPixmap::fromImage(image); } QByteArray ImageManager::ThumbnailCache::lookupRawData(const DB::FileName &name) const { m_dataLock.lock(); CacheFileInfo info = m_hash[name]; m_dataLock.unlock(); ThumbnailMapping *t = m_memcache->object(info.fileIndex); if (!t || !t->isValid()) { t = new ThumbnailMapping(fileNameForIndex(info.fileIndex)); if (!t->isValid()) { qCWarning(ImageManagerLog, "Failed to map thumbnail file"); return QByteArray(); } m_memcache->insert(info.fileIndex, t); } QByteArray array(t->map.mid(info.offset, info.size)); return array; } void ImageManager::ThumbnailCache::saveFull() const { // First ensure that any dirty thumbnails are written to disk m_thumbnailWriterLock.lock(); if (m_currentWriter) { delete m_currentWriter; m_currentWriter = nullptr; } m_thumbnailWriterLock.unlock(); QMutexLocker dataLocker(&m_dataLock); if (!m_isDirty) { return; } QTemporaryFile file; if (!file.open()) { qCWarning(ImageManagerLog, "Failed to create temporary file"); return; } QHash<DB::FileName, CacheFileInfo> tempHash = m_hash; m_unsavedHash.clear(); m_needsFullSave = false; // Clear the dirty flag early so that we can allow further work to proceed. // If the save fails, we'll set the dirty flag again. m_isDirty = false; dataLocker.unlock(); QDataStream stream(&file); stream << THUMBNAIL_FILE_VERSION << m_currentFile << m_currentOffset << m_hash.count(); for (auto it = tempHash.constBegin(); it != tempHash.constEnd(); ++it) { const CacheFileInfo &cacheInfo = it.value(); stream << it.key().relative() << cacheInfo.fileIndex << cacheInfo.offset << cacheInfo.size; } file.close(); const QString realFileName = thumbnailPath(QString::fromLatin1("thumbnailindex")); QFile::remove(realFileName); if (!file.copy(realFileName)) { qCWarning(ImageManagerLog, "Failed to copy the temporary file %s to %s", qPrintable(file.fileName()), qPrintable(realFileName)); dataLocker.relock(); m_isDirty = true; m_needsFullSave = true; } else { QFile realFile(realFileName); realFile.open(QIODevice::ReadOnly); realFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::WriteGroup | QFile::ReadOther); realFile.close(); } } // Incremental save does *not* clear the dirty flag. We always want to do a full // save eventually. void ImageManager::ThumbnailCache::saveIncremental() const { m_thumbnailWriterLock.lock(); if (m_currentWriter) { delete m_currentWriter; m_currentWriter = nullptr; } m_thumbnailWriterLock.unlock(); QMutexLocker dataLocker(&m_dataLock); if (m_unsavedHash.count() == 0) { return; } QHash<DB::FileName, CacheFileInfo> tempUnsavedHash = m_unsavedHash; m_unsavedHash.clear(); m_isDirty = true; const QString realFileName = thumbnailPath(QString::fromLatin1("thumbnailindex")); QFile file(realFileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) { qCWarning(ImageManagerLog, "Failed to open thumbnail cache for appending"); m_needsFullSave = true; return; } QDataStream stream(&file); for (auto it = tempUnsavedHash.constBegin(); it != tempUnsavedHash.constEnd(); ++it) { const CacheFileInfo &cacheInfo = it.value(); stream << it.key().relative() << cacheInfo.fileIndex << cacheInfo.offset << cacheInfo.size; } file.close(); } void ImageManager::ThumbnailCache::saveInternal() const { m_saveLock.lock(); const QString realFileName = thumbnailPath(QString::fromLatin1("thumbnailindex")); // If something has asked for a full save, do it! if (m_needsFullSave || !QFile(realFileName).exists()) { saveFull(); } else { saveIncremental(); } m_saveLock.unlock(); } void ImageManager::ThumbnailCache::saveImpl() const { m_timer->stop(); saveInternal(); m_timer->setInterval(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); m_timer->setSingleShot(true); m_timer->start(THUMBNAIL_CACHE_SAVE_INTERNAL_MS); } void ImageManager::ThumbnailCache::save() const { m_saveLock.lock(); m_needsFullSave = true; m_saveLock.unlock(); emit doSave(); } void ImageManager::ThumbnailCache::load() { QFile file(thumbnailPath(QString::fromLatin1("thumbnailindex"))); if (!file.exists()) return; QElapsedTimer timer; timer.start(); file.open(QIODevice::ReadOnly); QDataStream stream(&file); int version; stream >> version; if (version != THUMBNAIL_FILE_VERSION) return; //Discard cache // We can't allow anything to modify the structure while we're doing this. QMutexLocker dataLocker(&m_dataLock); int count = 0; stream >> m_currentFile >> m_currentOffset >> count; while (!stream.atEnd()) { QString name; int fileIndex; int offset; int size; stream >> name >> fileIndex >> offset >> size; m_hash.insert(DB::FileName::fromRelativePath(name), CacheFileInfo(fileIndex, offset, size)); if (fileIndex > m_currentFile) { m_currentFile = fileIndex; m_currentOffset = offset + size; } else if (fileIndex == m_currentFile && offset + size > m_currentOffset) { m_currentOffset = offset + size; } if (m_currentOffset > MAX_FILE_SIZE) { m_currentFile++; m_currentOffset = 0; } count++; } qCDebug(TimingLog) << "Loaded thumbnails in " << timer.elapsed() / 1000.0 << " seconds"; } bool ImageManager::ThumbnailCache::contains(const DB::FileName &name) const { QMutexLocker dataLocker(&m_dataLock); bool answer = m_hash.contains(name); return answer; } QString ImageManager::ThumbnailCache::thumbnailPath(const QString &file, const QString dir) const { QString base = QDir(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath(dir); return base + file; } ImageManager::ThumbnailCache *ImageManager::ThumbnailCache::instance() { if (!s_instance) { s_instance = new ThumbnailCache; } return s_instance; } void ImageManager::ThumbnailCache::deleteInstance() { delete s_instance; s_instance = nullptr; } void ImageManager::ThumbnailCache::flush() { QMutexLocker dataLocker(&m_dataLock); for (int i = 0; i <= m_currentFile; ++i) QFile::remove(fileNameForIndex(i)); m_currentFile = 0; m_currentOffset = 0; m_isDirty = true; m_hash.clear(); m_unsavedHash.clear(); m_memcache->clear(); dataLocker.unlock(); save(); } void ImageManager::ThumbnailCache::removeThumbnail(const DB::FileName &fileName) { QMutexLocker dataLocker(&m_dataLock); m_isDirty = true; m_hash.remove(fileName); dataLocker.unlock(); save(); } void ImageManager::ThumbnailCache::removeThumbnails(const DB::FileNameList &files) { QMutexLocker dataLocker(&m_dataLock); m_isDirty = true; Q_FOREACH (const DB::FileName &fileName, files) { m_hash.remove(fileName); } dataLocker.unlock(); save(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/ThumbnailCache.h b/ImageManager/ThumbnailCache.h index 32f52ae0..924a8953 100644 --- a/ImageManager/ThumbnailCache.h +++ b/ImageManager/ThumbnailCache.h @@ -1,93 +1,95 @@ /* Copyright (C) 2003-2010 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. */ #ifndef THUMBNAILCACHE_H #define THUMBNAILCACHE_H #include "CacheFileInfo.h" + #include <DB/FileNameList.h> + #include <QFile> #include <QHash> #include <QImage> #include <QMutex> template <class Key, class T> class QCache; namespace ImageManager { class ThumbnailMapping; class ThumbnailCache : public QObject { Q_OBJECT public: static ThumbnailCache *instance(); static void deleteInstance(); ThumbnailCache(); void insert(const DB::FileName &name, const QImage &image); QPixmap lookup(const DB::FileName &name) const; QByteArray lookupRawData(const DB::FileName &name) const; bool contains(const DB::FileName &name) const; void load(); void removeThumbnail(const DB::FileName &); void removeThumbnails(const DB::FileNameList &); public slots: void save() const; void flush(); signals: void doSave() const; private: ~ThumbnailCache() override; QString fileNameForIndex(int index, const QString dir = QString::fromLatin1(".thumbnails/")) const; QString thumbnailPath(const QString &fileName, const QString dir = QString::fromLatin1(".thumbnails/")) const; static ThumbnailCache *s_instance; QHash<DB::FileName, CacheFileInfo> m_hash; mutable QHash<DB::FileName, CacheFileInfo> m_unsavedHash; /* Protects accesses to the data (hash and unsaved hash) */ mutable QMutex m_dataLock; /* Prevents multiple saves from happening simultaneously */ mutable QMutex m_saveLock; /* Protects writing thumbnails to disk */ mutable QMutex m_thumbnailWriterLock; int m_currentFile; int m_currentOffset; mutable QTimer *m_timer; mutable bool m_needsFullSave; mutable bool m_isDirty; void saveFull() const; void saveIncremental() const; void saveInternal() const; void saveImpl() const; /** * Holds an in-memory cache of thumbnail files. */ mutable QCache<int, ThumbnailMapping> *m_memcache; mutable QFile *m_currentWriter; }; } #endif /* THUMBNAILCACHE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/VideoLengthExtractor.cpp b/ImageManager/VideoLengthExtractor.cpp index 8d9e6f96..193af572 100644 --- a/ImageManager/VideoLengthExtractor.cpp +++ b/ImageManager/VideoLengthExtractor.cpp @@ -1,100 +1,101 @@ /* Copyright 2012-2019 The KPhotoAlbum Development Team 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 "VideoLengthExtractor.h" + #include "Logging.h" #include <MainWindow/FeatureDialog.h> #include <Utilities/Process.h> #include <QDir> #define STR(x) QString::fromUtf8(x) ImageManager::VideoLengthExtractor::VideoLengthExtractor(QObject *parent) : QObject(parent) , m_process(nullptr) { } void ImageManager::VideoLengthExtractor::extract(const DB::FileName &fileName) { m_fileName = fileName; if (m_process) { disconnect(m_process, SIGNAL(finished(int)), this, SLOT(processEnded())); m_process->kill(); delete m_process; m_process = nullptr; } if (!MainWindow::FeatureDialog::hasVideoThumbnailer()) { emit unableToDetermineLength(); return; } m_process = new Utilities::Process(this); m_process->setWorkingDirectory(QDir::tempPath()); connect(m_process, SIGNAL(finished(int)), this, SLOT(processEnded())); Q_ASSERT(MainWindow::FeatureDialog::hasVideoProber()); QStringList arguments; // Just look at the length of the container. Some videos have streams without duration entry arguments << STR("-v") << STR("0") << STR("-show_entries") << STR("format=duration") << STR("-of") << STR("default=noprint_wrappers=1:nokey=1") << fileName.absolute(); qCDebug(ImageManagerLog, "%s %s", qPrintable(MainWindow::FeatureDialog::ffprobeBinary()), qPrintable(arguments.join(QString::fromLatin1(" ")))); m_process->start(MainWindow::FeatureDialog::ffprobeBinary(), arguments); } void ImageManager::VideoLengthExtractor::processEnded() { if (!m_process->stdErr().isEmpty()) qCDebug(ImageManagerLog) << m_process->stdErr(); const QStringList list = m_process->stdOut().split(QChar::fromLatin1('\n')); // ffprobe -v 0 just prints one line, except if panicking if (list.count() < 1) { qCWarning(ImageManagerLog) << "Unable to parse video length from ffprobe output!" << "Output was:\n" << m_process->stdOut(); emit unableToDetermineLength(); return; } const QString lenStr = list[0].trimmed(); bool ok = false; const double length = lenStr.toDouble(&ok); if (!ok) { qCWarning(ImageManagerLog) << STR("Unable to convert string \"%1\"to double (for file %2)").arg(lenStr).arg(m_fileName.absolute()); emit unableToDetermineLength(); return; } if (length == 0) { qCWarning(ImageManagerLog) << "video length returned was 0 for file " << m_fileName.absolute(); emit unableToDetermineLength(); return; } emit lengthFound(int(length)); m_process->deleteLater(); m_process = nullptr; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/VideoLengthExtractor.h b/ImageManager/VideoLengthExtractor.h index de38f5ad..994a25ce 100644 --- a/ImageManager/VideoLengthExtractor.h +++ b/ImageManager/VideoLengthExtractor.h @@ -1,60 +1,61 @@ /* Copyright 2012 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/>. */ #ifndef VIDEOLENGTHEXTRACTOR_H #define VIDEOLENGTHEXTRACTOR_H #include <DB/FileName.h> + #include <QObject> namespace Utilities { class Process; } namespace ImageManager { /** \brief \todo \see \ref videothumbnails */ class VideoLengthExtractor : public QObject { Q_OBJECT public: explicit VideoLengthExtractor(QObject *parent = nullptr); void extract(const DB::FileName &fileName); signals: void lengthFound(int length); void unableToDetermineLength(); private slots: void processEnded(); private: Utilities::Process *m_process; DB::FileName m_fileName; }; } #endif // VIDEOLENGTHEXTRACTOR_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/VideoThumbnails.cpp b/ImageManager/VideoThumbnails.cpp index fbef7eff..528c9c5d 100644 --- a/ImageManager/VideoThumbnails.cpp +++ b/ImageManager/VideoThumbnails.cpp @@ -1,113 +1,115 @@ /* Copyright (C) 2012-2019 The KPhotoAlbum Development Team 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 "VideoThumbnails.h" + #include "VideoLengthExtractor.h" + #include <BackgroundJobs/ExtractOneThumbnailJob.h> #include <BackgroundJobs/HandleVideoThumbnailRequestJob.h> #include <BackgroundJobs/ReadVideoLengthJob.h> #include <BackgroundTaskManager/JobManager.h> #include <MainWindow/FeatureDialog.h> ImageManager::VideoThumbnails::VideoThumbnails(QObject *parent) : QObject(parent) , m_pendingRequest(false) , m_index(0) { m_cache.resize(10); m_activeRequests.reserve(10); } void ImageManager::VideoThumbnails::setVideoFile(const DB::FileName &fileName) { m_index = 0; m_videoFile = fileName; if (loadFramesFromCache(fileName)) return; // no video thumbnails without ffmpeg: if (!MainWindow::FeatureDialog::hasVideoThumbnailer()) return; cancelPreviousJobs(); m_pendingRequest = false; for (int i = 0; i < 10; ++i) m_cache[i] = QImage(); BackgroundJobs::ReadVideoLengthJob *lengthJob = new BackgroundJobs::ReadVideoLengthJob(fileName, BackgroundTaskManager::ForegroundCycleRequest); for (int i = 0; i < 10; ++i) { BackgroundJobs::ExtractOneThumbnailJob *extractJob = new BackgroundJobs::ExtractOneThumbnailJob(fileName, i, BackgroundTaskManager::ForegroundCycleRequest); extractJob->addDependency(lengthJob); connect(extractJob, &BackgroundJobs::ExtractOneThumbnailJob::completed, this, &VideoThumbnails::gotFrame); m_activeRequests.append(QPointer<BackgroundJobs::ExtractOneThumbnailJob>(extractJob)); } BackgroundTaskManager::JobManager::instance()->addJob(lengthJob); } void ImageManager::VideoThumbnails::requestNext() { for (int i = 0; i < 10; ++i) { m_index = (m_index + 1) % 10; if (!m_cache[m_index].isNull()) { emit frameLoaded(m_cache[m_index]); m_pendingRequest = false; return; } } m_pendingRequest = true; } void ImageManager::VideoThumbnails::gotFrame() { const BackgroundJobs::ExtractOneThumbnailJob *job = qobject_cast<BackgroundJobs::ExtractOneThumbnailJob *>(sender()); const int index = job->index(); const DB::FileName thumbnailFile = BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(m_videoFile, index); m_cache[index] = QImage(thumbnailFile.absolute()); if (m_pendingRequest) { m_index = index; emit frameLoaded(m_cache[index]); } } bool ImageManager::VideoThumbnails::loadFramesFromCache(const DB::FileName &fileName) { for (int i = 0; i < 10; ++i) { const DB::FileName thumbnailFile = BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(fileName, i); if (!thumbnailFile.exists()) return false; QImage image(thumbnailFile.absolute()); if (image.isNull()) return false; m_cache[i] = image; } return true; } void ImageManager::VideoThumbnails::cancelPreviousJobs() { for (const QPointer<BackgroundJobs::ExtractOneThumbnailJob> &job : m_activeRequests) { if (!job.isNull()) job->cancel(); } m_activeRequests.clear(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImageManager/VideoThumbnails.h b/ImageManager/VideoThumbnails.h index a2e52742..809ef991 100644 --- a/ImageManager/VideoThumbnails.h +++ b/ImageManager/VideoThumbnails.h @@ -1,71 +1,72 @@ /* Copyright (C) 2012 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. */ #ifndef IMAGEMANAGER_VIDEOTHUMBNAILS_H #define IMAGEMANAGER_VIDEOTHUMBNAILS_H #include <DB/FileName.h> + #include <QImage> #include <QObject> #include <QPointer> namespace BackgroundJobs { class ExtractOneThumbnailJob; } namespace ImageManager { class VideoThumbnailsExtractor; class VideoLengthExtractor; /** \brief Helper class for extracting videos for thumbnail cycling \see \ref videothumbnails */ class VideoThumbnails : public QObject { Q_OBJECT public: explicit VideoThumbnails(QObject *parent = nullptr); void setVideoFile(const DB::FileName &fileName); public slots: void requestNext(); signals: void frameLoaded(const QImage &); private slots: void gotFrame(); private: bool loadFramesFromCache(const DB::FileName &fileName); void cancelPreviousJobs(); DB::FileName m_videoFile; QVector<QImage> m_cache; bool m_pendingRequest; QVector<QPointer<BackgroundJobs::ExtractOneThumbnailJob>> m_activeRequests; int m_index; }; } #endif // IMAGEMANAGER_VIDEOTHUMBNAILS_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/Export.cpp b/ImportExport/Export.cpp index 1cf53bb2..86f0a20d 100644 --- a/ImportExport/Export.cpp +++ b/ImportExport/Export.cpp @@ -1,414 +1,413 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "XMLHandler.h" + +#include <DB/FileNameList.h> +#include <DB/ImageInfo.h> +#include <ImageManager/AsyncLoader.h> +#include <ImageManager/RawImageDecoder.h> +#include <Utilities/FileNameUtil.h> +#include <Utilities/FileUtil.h> +#include <Utilities/VideoUtil.h> + +#include <KConfigGroup> +#include <KHelpClient> +#include <KLocalizedString> +#include <KMessageBox> +#include <KZip> #include <QApplication> #include <QBuffer> #include <QCheckBox> +#include <QDialogButtonBox> #include <QFileDialog> #include <QFileInfo> #include <QGroupBox> #include <QLayout> #include <QProgressDialog> +#include <QPushButton> #include <QRadioButton> #include <QSpinBox> #include <QVBoxLayout> -#include <KHelpClient> -#include <KLocalizedString> -#include <KMessageBox> -#include <KZip> - -#include <DB/FileNameList.h> -#include <DB/ImageInfo.h> -#include <ImageManager/AsyncLoader.h> -#include <ImageManager/RawImageDecoder.h> -#include <KConfigGroup> -#include <QDialogButtonBox> -#include <QPushButton> -#include <Utilities/FileNameUtil.h> -#include <Utilities/FileUtil.h> -#include <Utilities/VideoUtil.h> - -#include "XMLHandler.h" - using namespace ImportExport; namespace { bool isRAW(const DB::FileName &fileName) { return ImageManager::RAWImageDecoder::isRAW(fileName); } } //namespace 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(i18nc("@title:window", "Export Configuration / Copy Images")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QWidget *top = new QWidget; mainLayout->addWidget(top); QVBoxLayout *lay1 = new QVBoxLayout(top); // Include images QGroupBox *grp = new QGroupBox(i18n("How to Handle Images")); lay1->addWidget(grp); QVBoxLayout *boxLay = new QVBoxLayout(grp); m_include = new QRadioButton(i18n("Include in .kim file"), grp); m_manually = new QRadioButton(i18n("Do not copy files, only generate .kim file"), grp); m_auto = new QRadioButton(i18n("Automatically copy next to .kim file"), grp); m_link = new QRadioButton(i18n("Hard link next to .kim file"), grp); m_symlink = new QRadioButton(i18n("Symbolic link next to .kim file"), grp); m_auto->setChecked(true); boxLay->addWidget(m_include); boxLay->addWidget(m_manually); boxLay->addWidget(m_auto); boxLay->addWidget(m_link); boxLay->addWidget(m_symlink); // Compress mp_compress = new QCheckBox(i18n("Compress export file"), top); lay1->addWidget(mp_compress); // Generate thumbnails mp_generateThumbnails = new QCheckBox(i18n("Generate thumbnails"), top); mp_generateThumbnails->setChecked(false); lay1->addWidget(mp_generateThumbnails); // Enforece max size QHBoxLayout *hlay = new QHBoxLayout; lay1->addLayout(hlay); mp_enforeMaxSize = new QCheckBox(i18n("Limit maximum image dimensions to: ")); hlay->addWidget(mp_enforeMaxSize); mp_maxSize = new QSpinBox; mp_maxSize->setRange(100, 4000); hlay->addWidget(mp_maxSize); mp_maxSize->setValue(800); connect(mp_enforeMaxSize, &QCheckBox::toggled, mp_maxSize, &QSpinBox::setEnabled); mp_maxSize->setEnabled(false); QString txt = i18n("<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_zip; 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(MainWindow::Window::theMainWindow()); 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) || 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::copyOrOverwrite(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) || 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")); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/Export.h b/ImportExport/Export.h index 36ce6d29..d5c36ff0 100644 --- a/ImportExport/Export.h +++ b/ImportExport/Export.h @@ -1,115 +1,115 @@ /* Copyright (C) 2003-2010 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. */ #ifndef IMPORTEXPORT_H #define IMPORTEXPORT_H +#include <ImageManager/ImageClientInterface.h> +#include <Utilities/UniqFilenameMapper.h> + #include <QDialog> #include <QEventLoop> #include <QPointer> -#include <ImageManager/ImageClientInterface.h> -#include <Utilities/UniqFilenameMapper.h> - class QRadioButton; class QSpinBox; class QCheckBox; class KZip; class QProgressDialog; namespace DB { class FileNameList; } namespace ImportExport { enum ImageFileLocation { Inline, ManualCopy, AutoCopy, Link, Symlink }; class Export : public ImageManager::ImageClientInterface { public: static void imageExport(const DB::FileNameList &list); Export(const DB::FileNameList &list, const QString &zipFile, bool compress, int maxSize, ImageFileLocation, const QString &baseUrl, bool generateThumbnails, bool *ok); ~Export() override; static void showUsageDialog(); // ImageManager::ImageClient callback. void pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) override; protected: void generateThumbnails(const DB::FileNameList &list); void copyImages(const DB::FileNameList &list); private: bool m_internalOk; // used in case m_ok is null bool *m_ok; int m_filesRemaining; int m_steps; QProgressDialog *m_progressDialog; KZip *m_zip; int m_maxSize; QString m_subdir; bool m_loopEntered; ImageFileLocation m_location; Utilities::UniqFilenameMapper m_filenameMapper; bool m_copyingFiles; QString m_destdir; const QPointer<QEventLoop> m_eventLoop; }; class ExportConfig : public QDialog { Q_OBJECT public: ExportConfig(); QCheckBox *mp_compress; QCheckBox *mp_generateThumbnails; QCheckBox *mp_enforeMaxSize; QSpinBox *mp_maxSize; ImageFileLocation imageFileLocation() const; private: QRadioButton *m_include; QRadioButton *m_manually; QRadioButton *m_link; QRadioButton *m_symlink; QRadioButton *m_auto; private slots: void showHelp(); }; } #endif /* IMPORTEXPORT_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImageRow.cpp b/ImportExport/ImageRow.cpp index 66179cd8..09de73b0 100644 --- a/ImportExport/ImageRow.cpp +++ b/ImportExport/ImageRow.cpp @@ -1,80 +1,79 @@ /* Copyright (C) 2003-2010 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 "ImageRow.h" #include "ImportDialog.h" #include "KimFileReader.h" #include "Logging.h" -#include "MainWindow/Window.h" #include "MiniViewer.h" +#include <MainWindow/Window.h> + #include <KIO/StoredTransferJob> #include <KJobUiDelegate> #include <KJobWidgets> - #include <QCheckBox> #include <QImage> - #include <memory> using namespace ImportExport; ImageRow::ImageRow(DB::ImageInfoPtr info, ImportDialog *import, KimFileReader *kimFileReader, QWidget *parent) : QObject(parent) , m_info(info) , m_import(import) , m_kimFileReader(kimFileReader) { m_checkbox = new QCheckBox(QString(), parent); m_checkbox->setChecked(true); } void ImageRow::showImage() { if (m_import->m_externalSource) { QUrl src1 = m_import->m_kimFile; QUrl src2 = m_import->m_baseUrl; for (int i = 0; i < 2; ++i) { // First try next to the .kim file, then the external URL QUrl src = src1; if (i == 1) src = src2; src = src.adjusted(QUrl::RemoveFilename); src.setPath(src.path() + m_info->fileName().relative()); QString tmpFile; std::unique_ptr<KIO::StoredTransferJob> downloadJob { KIO::storedGet(src) }; KJobWidgets::setWindow(downloadJob.get(), MainWindow::Window::theMainWindow()); if (downloadJob->exec()) { QImage img; if (img.loadFromData(downloadJob->data())) { MiniViewer::show(img, m_info, static_cast<QWidget *>(parent())); break; } else { qCWarning(ImportExportLog) << "Could not load image data for" << src.toDisplayString(); } } } } else { QImage img = QImage::fromData(m_kimFileReader->loadImage(m_info->fileName().relative())); MiniViewer::show(img, m_info, static_cast<QWidget *>(parent())); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImageRow.h b/ImportExport/ImageRow.h index f8bbe091..73baa95f 100644 --- a/ImportExport/ImageRow.h +++ b/ImportExport/ImageRow.h @@ -1,51 +1,52 @@ /* Copyright (C) 2003-2010 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. */ #ifndef IMAGEROW_H #define IMAGEROW_H -#include "DB/ImageInfoPtr.h" +#include <DB/ImageInfoPtr.h> + #include <QObject> class QCheckBox; namespace ImportExport { class ImportDialog; class KimFileReader; /** * This class represent a single row on the ImageDialog's "select widgets to import" page. */ class ImageRow : public QObject { Q_OBJECT public: ImageRow(DB::ImageInfoPtr info, ImportDialog *import, KimFileReader *kimFileReader, QWidget *parent); QCheckBox *m_checkbox; DB::ImageInfoPtr m_info; ImportDialog *m_import; KimFileReader *m_kimFileReader; public slots: void showImage(); }; } #endif /* IMAGEROW_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/Import.cpp b/ImportExport/Import.cpp index 61c64fc5..444480bc 100644 --- a/ImportExport/Import.cpp +++ b/ImportExport/Import.cpp @@ -1,122 +1,121 @@ /* Copyright (C) 2003-2010 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 "Import.h" -#include <QFileDialog> -#include <QTemporaryFile> +#include "ImportDialog.h" +#include "ImportHandler.h" +#include "KimFileReader.h" + +#include <MainWindow/Window.h> #include <KIO/Job> #include <KIO/JobUiDelegate> #include <KLocalizedString> #include <KMessageBox> - -#include <MainWindow/Window.h> - -#include "ImportDialog.h" -#include "ImportHandler.h" -#include "KimFileReader.h" +#include <QFileDialog> +#include <QTemporaryFile> using namespace ImportExport; void Import::imageImport() { QUrl url = QFileDialog::getOpenFileUrl( nullptr, /*parent*/ i18n("KPhotoAlbum Export Files"), /*caption*/ QUrl(), /* directory */ i18n("KPhotoAlbum import files") + QString::fromLatin1("(*.kim)") /*filter*/ ); if (url.isEmpty()) return; imageImport(url); // This instance will delete itself when done. } void Import::imageImport(const QUrl &url) { Import *import = new Import; import->m_kimFileUrl = url; if (!url.isLocalFile()) import->downloadUrl(url); else import->exec(url.path()); // This instance will delete itself when done. } ImportExport::Import::Import() : m_tmp(nullptr) { } void ImportExport::Import::downloadUrl(const QUrl &url) { m_tmp = new QTemporaryFile; m_tmp->setFileTemplate(QString::fromLatin1("XXXXXX.kim")); if (!m_tmp->open()) { KMessageBox::error(MainWindow::Window::theMainWindow(), i18n("Unable to create temporary file")); delete this; return; } KIO::TransferJob *job = KIO::get(url); connect(job, &KIO::TransferJob::result, this, &Import::downloadKimJobCompleted); connect(job, &KIO::TransferJob::data, this, &Import::data); } void ImportExport::Import::downloadKimJobCompleted(KJob *job) { if (job->error()) { job->uiDelegate()->showErrorMessage(); delete this; } else { QString path = m_tmp->fileName(); m_tmp->close(); exec(path); } } void ImportExport::Import::exec(const QString &fileName) { ImportDialog dialog(MainWindow::Window::theMainWindow()); KimFileReader kimFileReader; if (!kimFileReader.open(fileName)) { delete this; return; } bool ok = dialog.exec(&kimFileReader, m_kimFileUrl); if (ok) { ImportHandler handler; handler.exec(dialog.settings(), &kimFileReader); } delete this; } void ImportExport::Import::data(KIO::Job *, const QByteArray &data) { m_tmp->write(data); } ImportExport::Import::~Import() { delete m_tmp; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportDialog.cpp b/ImportExport/ImportDialog.cpp index aee4f1ad..bafdcc30 100644 --- a/ImportExport/ImportDialog.cpp +++ b/ImportExport/ImportDialog.cpp @@ -1,395 +1,394 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "ImageRow.h" +#include "ImportMatcher.h" +#include "KimFileReader.h" +#include "MD5CheckPage.h" + +#include <DB/ImageInfo.h> +#include <Settings/SettingsData.h> +#include <XMLDB/Database.h> + +#include <KHelpClient> +#include <KLocalizedString> +#include <KMessageBox> #include <QCheckBox> #include <QComboBox> #include <QDir> #include <QFileDialog> #include <QGridLayout> #include <QHBoxLayout> #include <QLabel> #include <QLineEdit> #include <QPixmap> #include <QPushButton> #include <QScrollArea> -#include <KHelpClient> -#include <KLocalizedString> -#include <KMessageBox> - -#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(DB::ImageDB::instance()->uiDelegate(), m_kimFile.toDisplayString())); 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")); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportDialog.h b/ImportExport/ImportDialog.h index 52dec0a5..6045e961 100644 --- a/ImportExport/ImportDialog.h +++ b/ImportExport/ImportDialog.h @@ -1,104 +1,103 @@ /* Copyright (C) 2003-2010 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. */ #ifndef IMPORT_H #define IMPORT_H -#include <QUrl> - -#include <KAssistantDialog> - #include "ImportMatcher.h" #include "ImportSettings.h" +#include <KAssistantDialog> +#include <QUrl> + class QTemporaryFile; class QLineEdit; namespace DB { class ImageInfo; } namespace ImportExport { class ImportMatcher; class ImageRow; class KimFileReader; class MD5CheckPage; /** * This is the wizard that configures the import process */ class ImportDialog : public KAssistantDialog { Q_OBJECT public: explicit ImportDialog(QWidget *parent); // prevent hiding of base class method: using KAssistantDialog::exec; bool exec(KimFileReader *kimFileReader, const QUrl &kimFilePath); ImportSettings settings(); protected: friend class ImageRow; void setupPages(); bool readFile(const QByteArray &data); void createIntroduction(); void createImagesPage(); void createDestination(); void createCategoryPages(); ImportMatcher *createCategoryPage(const QString &myCategory, const QString &otherCategory); void selectImage(bool on); DB::ImageInfoList selectedImages() const; void possiblyAddMD5CheckPage(); protected slots: void slotEditDestination(); void updateNextButtonState(); void next() override; void slotSelectAll(); void slotSelectNone(); void slotHelp(); signals: void failedToCopy(QStringList files); private: DB::ImageInfoList m_images; QLineEdit *m_destinationEdit; KPageWidgetItem *m_destinationPage; KPageWidgetItem *m_categoryMatcherPage; KPageWidgetItem *m_dummy; ImportMatcher *m_categoryMatcher; ImportMatchers m_matchers; QList<ImageRow *> m_imagesSelect; QTemporaryFile *m_tmp; bool m_externalSource; QUrl m_kimFile; bool m_hasFilled; QUrl m_baseUrl; KimFileReader *m_kimFileReader; MD5CheckPage *m_md5CheckPage; }; } #endif /* IMPORT_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportHandler.cpp b/ImportExport/ImportHandler.cpp index b6434731..91beb323 100644 --- a/ImportExport/ImportHandler.cpp +++ b/ImportExport/ImportHandler.cpp @@ -1,349 +1,349 @@ /* Copyright (C) 2003-2010 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 "ImportHandler.h" + +#include "ImportSettings.h" +#include "KimFileReader.h" #include "Logging.h" +#include <Browser/BrowserWidget.h> +#include <DB/Category.h> +#include <DB/CategoryCollection.h> +#include <DB/ImageDB.h> +#include <DB/MD5.h> +#include <DB/MD5Map.h> +#include <MainWindow/Window.h> +#include <Utilities/UniqFilenameMapper.h> + #include <KConfigGroup> #include <KIO/StatJob> #include <KJobUiDelegate> #include <KJobWidgets> #include <KLocalizedString> #include <QApplication> #include <QFile> #include <QProgressDialog> +#include <kio/job.h> #include <kmessagebox.h> - -#include "Browser/BrowserWidget.h" -#include "DB/Category.h" -#include "DB/CategoryCollection.h" -#include "DB/ImageDB.h" -#include "DB/MD5.h" -#include "DB/MD5Map.h" -#include "ImportSettings.h" -#include "KimFileReader.h" -#include "MainWindow/Window.h" -#include "Utilities/UniqFilenameMapper.h" -#include "kio/job.h" - #include <memory> using namespace ImportExport; ImportExport::ImportHandler::ImportHandler() : m_fileMapper(nullptr) , m_finishedPressed(false) , m_progress(0) , m_reportUnreadableFiles(true) , m_eventLoop(new QEventLoop) { } ImportHandler::~ImportHandler() { delete m_fileMapper; delete m_eventLoop; } bool ImportExport::ImportHandler::exec(const ImportSettings &settings, KimFileReader *kimFileReader) { m_settings = settings; m_kimFileReader = kimFileReader; m_finishedPressed = true; delete m_fileMapper; m_fileMapper = new Utilities::UniqFilenameMapper(m_settings.destination()); bool ok; // copy images if (m_settings.externalSource()) { copyFromExternal(); // If none of the images were to be copied, then we flushed the loop before we got started, in that case, don't start the loop. qCDebug(ImportExportLog) << "Copying" << m_pendingCopies.count() << "files from external source..."; if (m_pendingCopies.count() > 0) ok = m_eventLoop->exec(); else ok = false; } else { ok = copyFilesFromZipFile(); if (ok) updateDB(); } if (m_progress) delete m_progress; return ok; } void ImportExport::ImportHandler::copyFromExternal() { m_pendingCopies = m_settings.selectedImages(); m_totalCopied = 0; m_progress = new QProgressDialog(MainWindow::Window::theMainWindow()); m_progress->setWindowTitle(i18nc("@title:window", "Copying Images")); m_progress->setMinimum(0); m_progress->setMaximum(2 * m_pendingCopies.count()); m_progress->show(); connect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages); copyNextFromExternal(); } void ImportExport::ImportHandler::copyNextFromExternal() { DB::ImageInfoPtr info = m_pendingCopies[0]; if (isImageAlreadyInDB(info)) { qCDebug(ImportExportLog) << info->fileName().relative() << "is already in database."; aCopyJobCompleted(0); return; } const DB::FileName fileName = info->fileName(); bool succeeded = false; QStringList tried; // First search for images next to the .kim file // Second search for images base on the image root as specified in the .kim file QList<QUrl> searchUrls { m_settings.kimFile().adjusted(QUrl::RemoveFilename), m_settings.baseURL().adjusted(QUrl::RemoveFilename) }; Q_FOREACH (const QUrl &url, searchUrls) { QUrl src(url); src.setPath(src.path() + fileName.relative()); std::unique_ptr<KIO::StatJob> statJob { KIO::stat(src, KIO::StatJob::SourceSide, 0 /* just query for existence */) }; KJobWidgets::setWindow(statJob.get(), MainWindow::Window::theMainWindow()); if (statJob->exec()) { QUrl dest = QUrl::fromLocalFile(m_fileMapper->uniqNameFor(fileName)); m_job = KIO::file_copy(src, dest, -1, KIO::HideProgressInfo); connect(m_job, &KIO::FileCopyJob::result, this, &ImportHandler::aCopyJobCompleted); succeeded = true; qCDebug(ImportExportLog) << "Copying" << src << "to" << dest; break; } else tried << src.toDisplayString(); } if (!succeeded) aCopyFailed(tried); } bool ImportExport::ImportHandler::copyFilesFromZipFile() { DB::ImageInfoList images = m_settings.selectedImages(); m_totalCopied = 0; m_progress = new QProgressDialog(MainWindow::Window::theMainWindow()); m_progress->setWindowTitle(i18nc("@title:window", "Copying Images")); m_progress->setMinimum(0); m_progress->setMaximum(2 * m_pendingCopies.count()); m_progress->show(); for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) { if (!isImageAlreadyInDB(*it)) { const DB::FileName fileName = (*it)->fileName(); QByteArray data = m_kimFileReader->loadImage(fileName.relative()); if (data.isNull()) return false; QString newName = m_fileMapper->uniqNameFor(fileName); QFile out(newName); if (!out.open(QIODevice::WriteOnly)) { KMessageBox::error(MainWindow::Window::theMainWindow(), i18n("Error when writing image %1", newName)); return false; } out.write(data.constData(), data.size()); out.close(); } qApp->processEvents(); m_progress->setValue(++m_totalCopied); if (m_progress->wasCanceled()) { return false; } } return true; } void ImportExport::ImportHandler::updateDB() { disconnect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages); m_progress->setLabelText(i18n("Updating Database")); int len = Settings::SettingsData::instance()->imageDirectory().length(); // image directory is always a prefix of destination if (len == m_settings.destination().length()) len = 0; else qCDebug(ImportExportLog) << "Re-rooting of ImageInfos from " << Settings::SettingsData::instance()->imageDirectory() << " to " << m_settings.destination(); // Run though all images DB::ImageInfoList images = m_settings.selectedImages(); for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) { DB::ImageInfoPtr info = *it; if (len != 0) { // exchange prefix: QString name = m_settings.destination() + info->fileName().absolute().mid(len); qCDebug(ImportExportLog) << info->fileName().absolute() << " -> " << name; info->setFileName(DB::FileName::fromAbsolutePath(name)); } if (isImageAlreadyInDB(info)) { qCDebug(ImportExportLog) << "Updating ImageInfo for " << info->fileName().absolute(); updateInfo(matchingInfoFromDB(info), info); } else { qCDebug(ImportExportLog) << "Adding ImageInfo for " << info->fileName().absolute(); addNewRecord(info); } m_progress->setValue(++m_totalCopied); if (m_progress->wasCanceled()) break; } Browser::BrowserWidget::instance()->home(); } void ImportExport::ImportHandler::stopCopyingImages() { m_job->kill(); } void ImportExport::ImportHandler::aCopyFailed(QStringList files) { int result = m_reportUnreadableFiles ? KMessageBox::warningYesNoCancelList(m_progress, i18n("Cannot copy from any of the following locations:"), files, QString(), KStandardGuiItem::cont(), KGuiItem(i18n("Continue without Asking"))) : KMessageBox::Yes; switch (result) { case KMessageBox::Cancel: // This might be late -- if we managed to copy some files, we will // just throw away any changes to the DB, but some new image files // might be in the image directory... m_eventLoop->exit(false); m_pendingCopies.pop_front(); break; case KMessageBox::No: m_reportUnreadableFiles = false; // fall through default: aCopyJobCompleted(0); } } void ImportExport::ImportHandler::aCopyJobCompleted(KJob *job) { qCDebug(ImportExportLog) << "CopyJob" << job << "completed."; m_pendingCopies.pop_front(); if (job && job->error()) { job->uiDelegate()->showErrorMessage(); m_eventLoop->exit(false); } else if (m_pendingCopies.count() == 0) { updateDB(); m_eventLoop->exit(true); } else if (m_progress->wasCanceled()) { m_eventLoop->exit(false); } else { m_progress->setValue(++m_totalCopied); copyNextFromExternal(); } } bool ImportExport::ImportHandler::isImageAlreadyInDB(const DB::ImageInfoPtr &info) { return DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum()); } DB::ImageInfoPtr ImportExport::ImportHandler::matchingInfoFromDB(const DB::ImageInfoPtr &info) { const DB::FileName name = DB::ImageDB::instance()->md5Map()->lookup(info->MD5Sum()); return DB::ImageDB::instance()->info(name); } /** * Merge the ImageInfo data from the kim file into the existing ImageInfo. */ void ImportExport::ImportHandler::updateInfo(DB::ImageInfoPtr dbInfo, DB::ImageInfoPtr newInfo) { if (dbInfo->label() != newInfo->label() && m_settings.importAction(QString::fromLatin1("*Label*")) == ImportSettings::Replace) dbInfo->setLabel(newInfo->label()); if (dbInfo->description().simplified() != newInfo->description().simplified()) { if (m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Replace) dbInfo->setDescription(newInfo->description()); else if (m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Merge) dbInfo->setDescription(dbInfo->description() + QString::fromLatin1("<br/><br/>") + newInfo->description()); } if (dbInfo->angle() != newInfo->angle() && m_settings.importAction(QString::fromLatin1("*Orientation*")) == ImportSettings::Replace) dbInfo->setAngle(newInfo->angle()); if (dbInfo->date() != newInfo->date() && m_settings.importAction(QString::fromLatin1("*Date*")) == ImportSettings::Replace) dbInfo->setDate(newInfo->date()); updateCategories(newInfo, dbInfo, false); } void ImportExport::ImportHandler::addNewRecord(DB::ImageInfoPtr info) { const DB::FileName importName = info->fileName(); DB::ImageInfoPtr updateInfo(new DB::ImageInfo(importName, info->mediaType(), false /*don't read exif */)); updateInfo->setLabel(info->label()); updateInfo->setDescription(info->description()); updateInfo->setDate(info->date()); updateInfo->setAngle(info->angle()); updateInfo->setMD5Sum(DB::MD5Sum(updateInfo->fileName())); DB::ImageInfoList list; list.append(updateInfo); DB::ImageDB::instance()->addImages(list); updateCategories(info, updateInfo, true); } void ImportExport::ImportHandler::updateCategories(DB::ImageInfoPtr XMLInfo, DB::ImageInfoPtr DBInfo, bool forceReplace) { // Run though the categories const QList<CategoryMatchSetting> matches = m_settings.categoryMatchSetting(); for (const CategoryMatchSetting &match : matches) { QString XMLCategoryName = match.XMLCategoryName(); QString DBCategoryName = match.DBCategoryName(); ImportSettings::ImportAction action = m_settings.importAction(DBCategoryName); const Utilities::StringSet items = XMLInfo->itemsOfCategory(XMLCategoryName); DB::CategoryPtr DBCategoryPtr = DB::ImageDB::instance()->categoryCollection()->categoryForName(DBCategoryName); if (!forceReplace && action == ImportSettings::Replace) DBInfo->setCategoryInfo(DBCategoryName, Utilities::StringSet()); if (action == ImportSettings::Merge || action == ImportSettings::Replace || forceReplace) { for (const QString &item : items) { if (match.XMLtoDB().contains(item)) { DBInfo->addCategoryInfo(DBCategoryName, match.XMLtoDB()[item]); DBCategoryPtr->addItem(match.XMLtoDB()[item]); } } } } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportHandler.h b/ImportExport/ImportHandler.h index 14a8c6b2..09bad101 100644 --- a/ImportExport/ImportHandler.h +++ b/ImportExport/ImportHandler.h @@ -1,88 +1,90 @@ /* Copyright (C) 2003-2010 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. */ #ifndef IMPORTHANDLER_H #define IMPORTHANDLER_H -#include "DB/ImageInfoPtr.h" #include "ImportSettings.h" + +#include <DB/ImageInfoPtr.h> + #include <QEventLoop> #include <QPointer> namespace KIO { class FileCopyJob; } class KJob; namespace Utilities { class UniqFilenameMapper; } class QProgressDialog; namespace ImportExport { class KimFileReader; /** * This class contains the business logic for the import process */ class ImportHandler : public QObject { Q_OBJECT public: ImportHandler(); ~ImportHandler() override; bool exec(const ImportSettings &settings, KimFileReader *kimFileReader); private: void copyFromExternal(); void copyNextFromExternal(); bool copyFilesFromZipFile(); void updateDB(); private slots: void stopCopyingImages(); void aCopyFailed(QStringList files); void aCopyJobCompleted(KJob *); private: bool isImageAlreadyInDB(const DB::ImageInfoPtr &info); DB::ImageInfoPtr matchingInfoFromDB(const DB::ImageInfoPtr &info); void updateInfo(DB::ImageInfoPtr dbInfo, DB::ImageInfoPtr newInfo); void addNewRecord(DB::ImageInfoPtr newInfo); void updateCategories(DB::ImageInfoPtr XMLInfo, DB::ImageInfoPtr DBInfo, bool forceReplace); private: Utilities::UniqFilenameMapper *m_fileMapper; bool m_finishedPressed; DB::ImageInfoList m_pendingCopies; QProgressDialog *m_progress; int m_totalCopied; KIO::FileCopyJob *m_job; bool m_reportUnreadableFiles; QPointer<QEventLoop> m_eventLoop; ImportSettings m_settings; KimFileReader *m_kimFileReader; }; } #endif /* IMPORTHANDLER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportMatcher.cpp b/ImportExport/ImportMatcher.cpp index 2db3ddb8..15c73960 100644 --- a/ImportExport/ImportMatcher.cpp +++ b/ImportExport/ImportMatcher.cpp @@ -1,129 +1,131 @@ /* 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 "ImportSettings.h" + #include <KLocalizedString> #include <QGridLayout> #include <QVBoxLayout> #include <qcheckbox.h> #include <qcombobox.h> #include <qlabel.h> 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; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/ImportSettings.h b/ImportExport/ImportSettings.h index 62a6b8b1..2432037b 100644 --- a/ImportExport/ImportSettings.h +++ b/ImportExport/ImportSettings.h @@ -1,93 +1,94 @@ /* Copyright (C) 2003-2010 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. */ #ifndef IMPORTSETTINGS_H #define IMPORTSETTINGS_H -#include "DB/ImageInfoList.h" +#include <DB/ImageInfoList.h> + #include <QUrl> namespace ImportExport { class CategoryMatchSetting { public: CategoryMatchSetting(const QString &DBCategoryName, const QString &XMLFileCategoryName) : m_XMLCategoryName(XMLFileCategoryName) , m_DBCategoryName(DBCategoryName) { } void add(const QString &DBFileNameItem, const QString &XMLFileNameItem); QString XMLCategoryName() const; QString DBCategoryName() const; const QMap<QString, QString> &XMLtoDB() const; private: QString m_XMLCategoryName; QString m_DBCategoryName; QMap<QString, QString> m_XMLtoDB; }; /** * The class contains all the data that is transported between the * ImportDialog, and the ImportHandler. The purpose of this class is to * decouple the above two. */ class ImportSettings { public: enum ImportAction { Replace = 1, Keep = 2, Merge = 3 }; void setSelectedImages(const DB::ImageInfoList &); DB::ImageInfoList selectedImages() const; void setDestination(const QString &); QString destination() const; void setExternalSource(bool b); bool externalSource() const; void setKimFile(const QUrl &kimFile); QUrl kimFile() const; void setBaseURL(const QUrl &url); QUrl baseURL() const; void setImportActions(const QMap<QString, ImportAction> &actions); ImportAction importAction(const QString &item); void addCategoryMatchSetting(const CategoryMatchSetting &); QList<CategoryMatchSetting> categoryMatchSetting() const; private: DB::ImageInfoList m_selectedImages; QString m_destination; bool m_externalSource; QUrl m_kimFile; QUrl m_baseURL; QMap<QString, ImportAction> m_actions; QList<CategoryMatchSetting> m_categoryMatchSettings; }; } #endif /* IMPORTSETTINGS_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/KimFileReader.cpp b/ImportExport/KimFileReader.cpp index 4ddad676..55f9af17 100644 --- a/ImportExport/KimFileReader.cpp +++ b/ImportExport/KimFileReader.cpp @@ -1,123 +1,125 @@ /* Copyright (C) 2003-2010 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 "KimFileReader.h" -#include <KLocalizedString> -#include <QFileInfo> + #include <Utilities/FileNameUtil.h> #include <Utilities/VideoUtil.h> + +#include <KLocalizedString> +#include <QFileInfo> #include <kmessagebox.h> #include <kzip.h> ImportExport::KimFileReader::KimFileReader() : m_zip(nullptr) { } bool ImportExport::KimFileReader::open(const QString &fileName) { m_fileName = fileName; m_zip = new KZip(fileName); if (!m_zip->open(QIODevice::ReadOnly)) { KMessageBox::error(nullptr, i18n("Unable to open '%1' for reading.", fileName), i18n("Error Importing Data")); delete m_zip; m_zip = nullptr; return false; } m_dir = m_zip->directory(); if (m_dir == nullptr) { KMessageBox::error(nullptr, i18n("Error reading directory contents of file %1; it is likely that the file is broken.", fileName)); delete m_zip; m_zip = nullptr; return false; } return true; } QByteArray ImportExport::KimFileReader::indexXML() { const KArchiveEntry *indexxml = m_dir->entry(QString::fromLatin1("index.xml")); if (indexxml == nullptr || !indexxml->isFile()) { KMessageBox::error(nullptr, i18n("Error reading index.xml file from %1; it is likely that the file is broken.", m_fileName)); return QByteArray(); } const KArchiveFile *file = static_cast<const KArchiveFile *>(indexxml); return file->data(); } ImportExport::KimFileReader::~KimFileReader() { delete m_zip; } QPixmap ImportExport::KimFileReader::loadThumbnail(QString fileName) { const KArchiveEntry *thumbnails = m_dir->entry(QString::fromLatin1("Thumbnails")); if (!thumbnails) return QPixmap(); if (!thumbnails->isDirectory()) { KMessageBox::error(nullptr, i18n("Thumbnail item in export file was not a directory, this indicates that the file is broken.")); return QPixmap(); } const KArchiveDirectory *thumbnailDir = static_cast<const KArchiveDirectory *>(thumbnails); const QString ext = Utilities::isVideo(DB::FileName::fromRelativePath(fileName)) ? QString::fromLatin1("jpg") : QFileInfo(fileName).completeSuffix(); fileName = QString::fromLatin1("%1.%2").arg(Utilities::stripEndingForwardSlash(QFileInfo(fileName).baseName())).arg(ext); const KArchiveEntry *fileEntry = thumbnailDir->entry(fileName); if (fileEntry == nullptr || !fileEntry->isFile()) { KMessageBox::error(nullptr, i18n("No thumbnail existed in export file for %1", fileName)); return QPixmap(); } const KArchiveFile *file = static_cast<const KArchiveFile *>(fileEntry); QByteArray data = file->data(); QPixmap pixmap; pixmap.loadFromData(data); return pixmap; } QByteArray ImportExport::KimFileReader::loadImage(const QString &fileName) { const KArchiveEntry *images = m_dir->entry(QString::fromLatin1("Images")); if (!images) { KMessageBox::error(nullptr, i18n("export file did not contain a Images subdirectory, this indicates that the file is broken")); return QByteArray(); } if (!images->isDirectory()) { KMessageBox::error(nullptr, i18n("Images item in export file was not a directory, this indicates that the file is broken")); return QByteArray(); } const KArchiveDirectory *imagesDir = static_cast<const KArchiveDirectory *>(images); const KArchiveEntry *fileEntry = imagesDir->entry(fileName); if (fileEntry == nullptr || !fileEntry->isFile()) { KMessageBox::error(nullptr, i18n("No image existed in export file for %1", fileName)); return QByteArray(); } const KArchiveFile *file = static_cast<const KArchiveFile *>(fileEntry); QByteArray data = file->data(); return data; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/MD5CheckPage.cpp b/ImportExport/MD5CheckPage.cpp index cffb1b40..c1c7600f 100644 --- a/ImportExport/MD5CheckPage.cpp +++ b/ImportExport/MD5CheckPage.cpp @@ -1,197 +1,199 @@ /* Copyright (C) 2003-2010 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 "MD5CheckPage.h" -#include "DB/ImageDB.h" -#include "DB/MD5Map.h" + +#include <DB/ImageDB.h> +#include <DB/MD5Map.h> + #include <KLocalizedString> #include <QButtonGroup> #include <QFrame> #include <QLabel> #include <QRadioButton> #include <QVBoxLayout> ImportExport::ClashInfo::ClashInfo(const QStringList &categories) : label(false) , description(false) , orientation(false) , date(false) { for (const QString &category : categories) this->categories[category] = false; } bool ImportExport::MD5CheckPage::pageNeeded(const ImportSettings &settings) { if (countOfMD5Matches(settings) != 0 && clashes(settings).anyClashes()) return true; return false; } ImportExport::MD5CheckPage::MD5CheckPage(const ImportSettings &settings) { QVBoxLayout *vlay = new QVBoxLayout(this); const QString txt = i18np("One image from the import file, has the same MD5 sum as an image in the Database, how should that be resolved?", "%1 images from the import file, have the same MD5 sum as images in the Database, how should that be resolved?", countOfMD5Matches(settings)); QLabel *label = new QLabel(txt); label->setWordWrap(true); vlay->addWidget(label); QGridLayout *grid = new QGridLayout; grid->setHorizontalSpacing(0); vlay->addLayout(grid); int row = -1; // Titles label = new QLabel(i18n("Use data from\nImport File")); grid->addWidget(label, ++row, 1); label = new QLabel(i18n("Use data from\nDatabase")); grid->addWidget(label, row, 2); label = new QLabel(i18n("Merge data")); grid->addWidget(label, row, 3); ClashInfo clashes = this->clashes(settings); createRow(grid, row, QString::fromLatin1("*Label*"), i18n("Label"), clashes.label, false); createRow(grid, row, QString::fromLatin1("*Description*"), i18n("Description"), clashes.description, true); createRow(grid, row, QString::fromLatin1("*Orientation*"), i18n("Orientation"), clashes.orientation, false); createRow(grid, row, QString::fromLatin1("*Date*"), i18n("Date and Time"), clashes.date, false); for (QMap<QString, bool>::const_iterator it = clashes.categories.constBegin(); it != clashes.categories.constEnd(); ++it) { createRow(grid, row, it.key(), it.key(), *it, true); } vlay->addStretch(1); } /** * Return the number of images in the import set which has the same MD5 sum as those from the DB. */ int ImportExport::MD5CheckPage::countOfMD5Matches(const ImportSettings &settings) { int count = 0; DB::ImageInfoList list = settings.selectedImages(); for (DB::ImageInfoPtr info : list) { if (DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum())) ++count; } return count; } ImportExport::ClashInfo ImportExport::MD5CheckPage::clashes(const ImportSettings &settings) { QStringList myCategories; Q_FOREACH (const CategoryMatchSetting &matcher, settings.categoryMatchSetting()) { myCategories.append(matcher.DBCategoryName()); } ClashInfo res(myCategories); DB::ImageInfoList list = settings.selectedImages(); Q_FOREACH (DB::ImageInfoPtr info, list) { if (!DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum())) continue; const DB::FileName name = DB::ImageDB::instance()->md5Map()->lookup(info->MD5Sum()); DB::ImageInfoPtr other = DB::ImageDB::instance()->info(name); if (info->label() != other->label()) res.label = true; if (info->description() != other->description()) res.description = true; if (info->angle() != other->angle()) res.orientation = true; if (info->date() != other->date()) res.date = true; Q_FOREACH (const CategoryMatchSetting &matcher, settings.categoryMatchSetting()) { const QString XMLFileCategory = matcher.XMLCategoryName(); const QString DBCategory = matcher.DBCategoryName(); if (mapCategoriesToDB(matcher, info->itemsOfCategory(XMLFileCategory)) != other->itemsOfCategory(DBCategory)) res.categories[DBCategory] = true; } } return res; } bool ImportExport::ClashInfo::anyClashes() { if (label || description || orientation || date) return true; for (QMap<QString, bool>::ConstIterator categoryIt = categories.constBegin(); categoryIt != categories.constEnd(); ++categoryIt) { if (categoryIt.value()) return true; } return false; } void ImportExport::MD5CheckPage::createRow(QGridLayout *layout, int &row, const QString &name, const QString &title, bool anyClashes, bool allowMerge) { if (row % 3 == 0) { QFrame *line = new QFrame; line->setFrameShape(QFrame::HLine); layout->addWidget(line, ++row, 0, 1, 4); } QLabel *label = new QLabel(title); label->setEnabled(anyClashes); layout->addWidget(label, ++row, 0); QButtonGroup *group = new QButtonGroup(this); m_groups[name] = group; for (int i = 1; i < 4; ++i) { if (i == 3 && !allowMerge) continue; QRadioButton *rb = new QRadioButton; layout->addWidget(rb, row, i); group->addButton(rb, i); rb->setEnabled(anyClashes); if (i == 1) rb->setChecked(true); } } Utilities::StringSet ImportExport::MD5CheckPage::mapCategoriesToDB(const CategoryMatchSetting &matcher, const Utilities::StringSet &items) { Utilities::StringSet res; Q_FOREACH (const QString &item, items) { if (matcher.XMLtoDB().contains(item)) res.insert(matcher.XMLtoDB()[item]); } return res; } QMap<QString, ImportExport::ImportSettings::ImportAction> ImportExport::MD5CheckPage::settings() { QMap<QString, ImportSettings::ImportAction> res; for (QMap<QString, QButtonGroup *>::iterator it = m_groups.begin(); it != m_groups.end(); ++it) { res.insert(it.key(), static_cast<ImportSettings::ImportAction>(it.value()->checkedId())); } return res; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/MD5CheckPage.h b/ImportExport/MD5CheckPage.h index 26529931..e10efe62 100644 --- a/ImportExport/MD5CheckPage.h +++ b/ImportExport/MD5CheckPage.h @@ -1,63 +1,65 @@ /* Copyright (C) 2003-2010 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. */ #ifndef MD5CHECKPAGE_H #define MD5CHECKPAGE_H #include "ImportSettings.h" -#include "Utilities/StringSet.h" + +#include <Utilities/StringSet.h> + #include <QGridLayout> #include <QWidget> class QButtonGroup; namespace ImportExport { class ClashInfo { public: explicit ClashInfo(const QStringList &categories); bool anyClashes(); bool label; bool description; bool orientation; bool date; QMap<QString, bool> categories; }; class MD5CheckPage : public QWidget { public: explicit MD5CheckPage(const ImportSettings &settings); static bool pageNeeded(const ImportSettings &settings); QMap<QString, ImportSettings::ImportAction> settings(); private: void createRow(QGridLayout *layout, int &row, const QString &name, const QString &title, bool anyClashes, bool allowMerge); static int countOfMD5Matches(const ImportSettings &settings); static ClashInfo clashes(const ImportSettings &settings); static Utilities::StringSet mapCategoriesToDB(const CategoryMatchSetting &matcher, const Utilities::StringSet &items); private: QMap<QString, QButtonGroup *> m_groups; }; } #endif /* MD5CHECKPAGE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/MiniViewer.cpp b/ImportExport/MiniViewer.cpp index 0198a20d..5ced63ec 100644 --- a/ImportExport/MiniViewer.cpp +++ b/ImportExport/MiniViewer.cpp @@ -1,74 +1,76 @@ /* 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 "DB/ImageInfo.h" + +#include <DB/ImageInfo.h> + #include <KLocalizedString> #include <QDialogButtonBox> #include <QPushButton> #include <qimage.h> #include <qlabel.h> #include <qlayout.h> #include <qmatrix.h> 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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/MiniViewer.h b/ImportExport/MiniViewer.h index 1312a8a3..1b66b87e 100644 --- a/ImportExport/MiniViewer.h +++ b/ImportExport/MiniViewer.h @@ -1,58 +1,59 @@ /* Copyright (C) 2003-2010 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. */ #ifndef MINIVIEWER_H #define MINIVIEWER_H -#include "DB/ImageInfoPtr.h" +#include <DB/ImageInfoPtr.h> + #include <qdialog.h> #include <qimage.h> class QCloseEvent; class QLabel; namespace DB { class ImageInfo; } namespace ImportExport { class MiniViewer : public QDialog { Q_OBJECT public: static void show(QImage img, DB::ImageInfoPtr info, QWidget *parent = nullptr); void closeEvent(QCloseEvent *event) override; protected slots: void slotClose(); private: explicit MiniViewer(QWidget *parent = nullptr); static MiniViewer *s_instance; QLabel *m_pixmap; }; } #endif /* MINIVIEWER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ImportExport/XMLHandler.h b/ImportExport/XMLHandler.h index 9163132b..3bda521c 100644 --- a/ImportExport/XMLHandler.h +++ b/ImportExport/XMLHandler.h @@ -1,55 +1,55 @@ /* Copyright (C) 2003-2010 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. */ #ifndef XMLHANDLER_H #define XMLHANDLER_H -#include <QDomDocument> -#include <QDomElement> -#include <QString> +#include "Export.h" // ImageFileLocation #include <DB/FileNameList.h> #include <DB/ImageInfoPtr.h> -#include "Export.h" // ImageFileLocation +#include <QDomDocument> +#include <QDomElement> +#include <QString> namespace Utilities { class UniqFilenameMapper; } namespace ImportExport { class XMLHandler { public: QByteArray createIndexXML( const DB::FileNameList &images, const QString &baseUrl, ImageFileLocation location, Utilities::UniqFilenameMapper *nameMap); protected: QDomElement save(QDomDocument doc, const DB::ImageInfoPtr &info); void writeCategories(QDomDocument doc, QDomElement elm, const DB::ImageInfoPtr &info); }; } #endif /* XMLHANDLER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/AutoStackImages.cpp b/MainWindow/AutoStackImages.cpp index 2d5a3525..02cb7624 100644 --- a/MainWindow/AutoStackImages.cpp +++ b/MainWindow/AutoStackImages.cpp @@ -1,326 +1,326 @@ /* Copyright (C) 2010-2019 The KPhotoAlbum Development Team 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 "Window.h" + +#include <DB/FileInfo.h> +#include <DB/ImageDB.h> +#include <DB/ImageDate.h> +#include <DB/ImageInfo.h> +#include <DB/MD5Map.h> +#include <Settings/SettingsData.h> +#include <Utilities/FileUtil.h> +#include <Utilities/ShowBusyCursor.h> + +#include <KLocalizedString> #include <QApplication> #include <QCheckBox> #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/ImageDB.h> -#include <DB/ImageDate.h> -#include <DB/ImageInfo.h> -#include <DB/MD5Map.h> -#include <MainWindow/Window.h> -#include <Settings/SettingsData.h> -#include <Utilities/FileUtil.h> -#include <Utilities/ShowBusyCursor.h> - using namespace MainWindow; AutoStackImages::AutoStackImages(QWidget *parent, const DB::FileNameList &list) : QDialog(parent) , m_list(list) { setWindowTitle(i18nc("@title:window", "Automatically Stack Images")); QWidget *top = new QWidget; QVBoxLayout *lay1 = new QVBoxLayout(top); setLayout(lay1); QWidget *containerMd5 = new QWidget(this); lay1->addWidget(containerMd5); QHBoxLayout *hlayMd5 = new QHBoxLayout(containerMd5); m_matchingMD5 = new QCheckBox(i18n("Stack images with identical MD5 sum")); m_matchingMD5->setChecked(false); hlayMd5->addWidget(m_matchingMD5); QWidget *containerFile = new QWidget(this); lay1->addWidget(containerFile); QHBoxLayout *hlayFile = new QHBoxLayout(containerFile); m_matchingFile = new QCheckBox(i18n("Stack images based on file version detection")); m_matchingFile->setChecked(true); hlayFile->addWidget(m_matchingFile); m_origTop = new QCheckBox(i18n("Original to top")); m_origTop->setChecked(false); hlayFile->addWidget(m_origTop); QWidget *containerContinuous = new QWidget(this); lay1->addWidget(containerContinuous); QHBoxLayout *hlayContinuous = new QHBoxLayout(containerContinuous); //FIXME: This is hard to translate because of the split sentence. It is better //to use a single sentence here like "Stack images that are (were?) shot //within this time:" and use the spin method setSuffix() to set the "seconds". //Also: Would minutes not be a more sane time unit here? (schwarzer) m_continuousShooting = new QCheckBox(i18nc("The whole sentence should read: *Stack images that are shot within x seconds of each other*. So images that are shot in one burst are automatically stacked together. (This sentence is before the x.)", "Stack images that are shot within")); m_continuousShooting->setChecked(false); hlayContinuous->addWidget(m_continuousShooting); m_continuousThreshold = new QSpinBox; m_continuousThreshold->setRange(1, 999); m_continuousThreshold->setSingleStep(1); m_continuousThreshold->setValue(2); hlayContinuous->addWidget(m_continuousThreshold); QLabel *sec = new QLabel(i18nc("The whole sentence should read: *Stack images that are shot within x seconds of each other*. (This being the text after x.)", "seconds"), containerContinuous); hlayContinuous->addWidget(sec); QGroupBox *grpOptions = new QGroupBox(i18n("AutoStacking Options")); QVBoxLayout *grpLayOptions = new QVBoxLayout(grpOptions); lay1->addWidget(grpOptions); m_autostackDefault = new QRadioButton(i18n("Include matching image to appropriate stack (if one exists)")); m_autostackDefault->setChecked(true); grpLayOptions->addWidget(m_autostackDefault); m_autostackUnstack = new QRadioButton(i18n("Unstack images from their current stack and create new one for the matches")); m_autostackUnstack->setChecked(false); grpLayOptions->addWidget(m_autostackUnstack); m_autostackSkip = new QRadioButton(i18n("Skip images that are already in a stack")); m_autostackSkip->setChecked(false); grpLayOptions->addWidget(m_autostackSkip); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &AutoStackImages::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &AutoStackImages::reject); lay1->addWidget(buttonBox); } /* * This function searches for images with matching MD5 sums * Matches are automatically stacked */ void AutoStackImages::matchingMD5(DB::FileNameList &toBeShown) { QMap<DB::MD5, DB::FileNameList> tostack; DB::FileNameList showIfStacked; // Stacking all images that have the same MD5 sum // First make a map of MD5 sums with corresponding images Q_FOREACH (const DB::FileName &fileName, m_list) { DB::MD5 sum = fileName.info()->MD5Sum(); if (DB::ImageDB::instance()->md5Map()->contains(sum)) { if (tostack[sum].isEmpty()) tostack.insert(sum, DB::FileNameList() << fileName); else tostack[sum].append(fileName); } } // Then add images to stack (depending on configuration options) for (QMap<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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/BreadcrumbViewer.cpp b/MainWindow/BreadcrumbViewer.cpp index 9879885b..63a773d4 100644 --- a/MainWindow/BreadcrumbViewer.cpp +++ b/MainWindow/BreadcrumbViewer.cpp @@ -1,90 +1,91 @@ /* Copyright (C) 2003-2010 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 "BreadcrumbViewer.h" + #include <QTextDocument> void MainWindow::BreadcrumbViewer::setBreadcrumbs(const Browser::BreadcrumbList &list) { m_activeCrumbs = list.latest(); updateText(); } void MainWindow::BreadcrumbViewer::linkClicked(const QString &link) { emit widenToBreadcrumb(m_activeCrumbs[link.toInt()]); } MainWindow::BreadcrumbViewer::BreadcrumbViewer() { connect(this, &BreadcrumbViewer::linkActivated, this, &BreadcrumbViewer::linkClicked); } /** * Format the text with hyperlinks. The tricky part is to handle the situation where all the text doesn't fit in. * The by far best solution would be to compress at a letter level, but this code is really only used in the rare * situation where the user chooses a very long path, as his window usually is somewhat wide. */ void MainWindow::BreadcrumbViewer::updateText() { QStringList htmlList; for (int i = 0; i < m_activeCrumbs.count() - 1; ++i) htmlList.append(QString::fromLatin1("<a href=\"%1\">%2</a>").arg(i).arg(m_activeCrumbs[i].text())); if (!m_activeCrumbs[m_activeCrumbs.count() - 1].isView()) htmlList.append(m_activeCrumbs[m_activeCrumbs.count() - 1].text()); QTextDocument doc; doc.setDefaultFont(font()); QString res = htmlList.last(); const QString ellipses = QChar(0x2026) + QString::fromLatin1(" > "); for (int i = htmlList.count() - 2; i >= 0; --i) { // If we can't fit it in, then add ellipses const QString tmp = htmlList[i] + QString::fromLatin1(" > ") + res; doc.setHtml(tmp); if (doc.size().width() > width()) { res = ellipses + res; break; } // now check that we can fit in ellipses if this was the last token const QString tmp2 = ellipses + tmp; doc.setHtml(tmp2); if (doc.size().width() > width() && i != 0) { // Nope, so better stop here res = ellipses + res; break; } res = tmp; } setText(res); } void MainWindow::BreadcrumbViewer::resizeEvent(QResizeEvent *event) { QLabel::resizeEvent(event); updateText(); } QSize MainWindow::BreadcrumbViewer::minimumSizeHint() const { return QSize(100, QLabel::minimumSizeHint().height()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/BreadcrumbViewer.h b/MainWindow/BreadcrumbViewer.h index c97526f5..3e2c2478 100644 --- a/MainWindow/BreadcrumbViewer.h +++ b/MainWindow/BreadcrumbViewer.h @@ -1,55 +1,56 @@ /* Copyright (C) 2003-2010 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. */ #ifndef BREADCRUMBVIEWER_H #define BREADCRUMBVIEWER_H #include <Browser/BreadcrumbList.h> + #include <QLabel> namespace MainWindow { class BreadcrumbViewer : public QLabel { Q_OBJECT public: BreadcrumbViewer(); QSize minimumSizeHint() const override; public slots: void setBreadcrumbs(const Browser::BreadcrumbList &list); signals: void widenToBreadcrumb(const Browser::Breadcrumb &); protected: void resizeEvent(QResizeEvent *event) override; private slots: void linkClicked(const QString &link); private: void updateText(); private: Browser::BreadcrumbList m_activeCrumbs; }; } #endif /* BREADCRUMBVIEWER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/CategoryImagePopup.cpp b/MainWindow/CategoryImagePopup.cpp index 119a8f26..73199d05 100644 --- a/MainWindow/CategoryImagePopup.cpp +++ b/MainWindow/CategoryImagePopup.cpp @@ -1,84 +1,87 @@ /* 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 "DB/CategoryCollection.h" -#include "Viewer/CategoryImageConfig.h" + #include "Window.h" -#include <KLocalizedString> + +#include <DB/CategoryCollection.h> #include <Utilities/StringSet.h> +#include <Viewer/CategoryImageConfig.h> + +#include <KLocalizedString> #include <qstringlist.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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/CategoryImagePopup.h b/MainWindow/CategoryImagePopup.h index 6775b91f..6331c362 100644 --- a/MainWindow/CategoryImagePopup.h +++ b/MainWindow/CategoryImagePopup.h @@ -1,48 +1,49 @@ /* Copyright (C) 2003-2010 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. */ #ifndef CATEGORYIMAGEPOPUP_H #define CATEGORYIMAGEPOPUP_H -#include "DB/ImageDB.h" +#include <DB/ImageDB.h> + #include <QImage> #include <QMenu> namespace MainWindow { class CategoryImagePopup : public QMenu { Q_OBJECT public: explicit CategoryImagePopup(QWidget *parent); void populate(const QImage &image, const DB::FileName &imageName); protected slots: void slotExecuteService(QAction *); void makeCategoryImage(); private: QImage m_image; DB::ImageInfoPtr m_imageInfo; }; } #endif /* CATEGORYIMAGEPOPUP_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DeleteDialog.cpp b/MainWindow/DeleteDialog.cpp index 1af552ac..03cff093 100644 --- a/MainWindow/DeleteDialog.cpp +++ b/MainWindow/DeleteDialog.cpp @@ -1,108 +1,109 @@ /* 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 "Utilities/DeleteFiles.h" +#include <Utilities/DeleteFiles.h> + #include <KLocalizedString> #include <QDialogButtonBox> #include <QPushButton> #include <QVBoxLayout> #include <qcheckbox.h> #include <qlabel.h> #include <qlayout.h> using namespace MainWindow; DeleteDialog::DeleteDialog(QWidget *parent) : QDialog(parent) , m_list() { setWindowTitle(i18nc("@title:window", "Removing Items")); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); QWidget *top = new QWidget; QVBoxLayout *lay1 = new QVBoxLayout(top); mainLayout->addWidget(top); m_label = new QLabel; lay1->addWidget(m_label); m_useTrash = new QRadioButton; lay1->addWidget(m_useTrash); m_deleteFile = new QRadioButton; lay1->addWidget(m_deleteFile); m_deleteFromDb = new QRadioButton; lay1->addWidget(m_deleteFromDb); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &DeleteDialog::deleteImages); connect(buttonBox, &QDialogButtonBox::rejected, this, &DeleteDialog::reject); mainLayout->addWidget(buttonBox); } int DeleteDialog::exec(const DB::FileNameList &list) { if (!list.size()) return 0; bool someFileExists = false; Q_FOREACH (const DB::FileName &file, list) { if (file.exists()) { someFileExists = true; break; } } const QString msg1 = i18np("Removing 1 item", "Removing %1 items", list.size()); const QString msg2 = i18np("Selected item will be removed from the database.<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(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DeleteDialog.h b/MainWindow/DeleteDialog.h index 34ee7be2..f6b193e9 100644 --- a/MainWindow/DeleteDialog.h +++ b/MainWindow/DeleteDialog.h @@ -1,61 +1,62 @@ /* Copyright (C) 2003-2010 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. */ #ifndef DELETEDIALOG_H #define DELETEDIALOG_H #include <DB/FileNameList.h> + #include <QDialog> #include <QLabel> #include <kjob.h> #include <qradiobutton.h> class QLabel; class QCheckBox; class KJob; namespace KIO { class Job; } namespace MainWindow { class DeleteDialog : public QDialog { Q_OBJECT public: explicit DeleteDialog(QWidget *parent); // prevent hiding of base class method: using QDialog::exec; int exec(const DB::FileNameList &list); protected slots: void deleteImages(); private: DB::FileNameList m_list; QLabel *m_label; QRadioButton *m_deleteFile; QRadioButton *m_useTrash; QRadioButton *m_deleteFromDb; }; } #endif /* DELETEDIALOG_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DirtyIndicator.cpp b/MainWindow/DirtyIndicator.cpp index 2c8a70cd..b6bb850f 100644 --- a/MainWindow/DirtyIndicator.cpp +++ b/MainWindow/DirtyIndicator.cpp @@ -1,94 +1,95 @@ /* 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 <QLabel> #include <QPixmap> #include <kiconloader.h> static MainWindow::DirtyIndicator *s_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); s_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 (s_instance) { s_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; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DuplicateMerger/DuplicateMatch.cpp b/MainWindow/DuplicateMerger/DuplicateMatch.cpp index 67a1098a..edc7a4c0 100644 --- a/MainWindow/DuplicateMerger/DuplicateMatch.cpp +++ b/MainWindow/DuplicateMerger/DuplicateMatch.cpp @@ -1,164 +1,163 @@ /* 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 "MergeToolTip.h" + +#include <DB/ImageDB.h> +#include <DB/ImageInfo.h> +#include <DB/ImageInfoPtr.h> +#include <ImageManager/AsyncLoader.h> +#include <Utilities/DeleteFiles.h> + +#include <KLocalizedString> #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 // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DuplicateMerger/DuplicateMatch.h b/MainWindow/DuplicateMerger/DuplicateMatch.h index baf5a933..2f39a6fc 100644 --- a/MainWindow/DuplicateMerger/DuplicateMatch.h +++ b/MainWindow/DuplicateMerger/DuplicateMatch.h @@ -1,63 +1,63 @@ /* Copyright 2012 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/>. */ #ifndef MAINWINDOW_DUPLICATEMATCH_H #define MAINWINDOW_DUPLICATEMATCH_H +#include <DB/FileNameList.h> +#include <ImageManager/ImageClientInterface.h> +#include <Utilities/DeleteFiles.h> + #include <QList> #include <QWidget> -#include "DB/FileNameList.h" -#include "ImageManager/ImageClientInterface.h" -#include "Utilities/DeleteFiles.h" - class QLabel; class QCheckBox; class QRadioButton; namespace MainWindow { class MergeToolTip; class DuplicateMatch : public QWidget, ImageManager::ImageClientInterface { Q_OBJECT public: explicit DuplicateMatch(const DB::FileNameList &files); void pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) override; void setSelected(bool); bool selected() const; void execute(Utilities::DeleteMethod); bool eventFilter(QObject *, QEvent *) override; signals: void selectionChanged(); private: QLabel *m_image; QCheckBox *m_merge; QList<QRadioButton *> m_buttons; }; } // namespace MainWindow #endif // MAINWINDOW_DUPLICATEMATCH_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DuplicateMerger/DuplicateMerger.cpp b/MainWindow/DuplicateMerger/DuplicateMerger.cpp index a6ee9dd8..1f3a2723 100644 --- a/MainWindow/DuplicateMerger/DuplicateMerger.cpp +++ b/MainWindow/DuplicateMerger/DuplicateMerger.cpp @@ -1,206 +1,207 @@ /* 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 "DuplicateMerger.h" + +#include "DuplicateMatch.h" +#include "MergeToolTip.h" + +#include <DB/FileName.h> +#include <DB/FileNameList.h> +#include <DB/ImageDB.h> +#include <DB/ImageInfo.h> +#include <DB/MD5.h> +#include <Utilities/DeleteFiles.h> +#include <Utilities/ShowBusyCursor.h> + +#include <KLocalizedString> #include <QDebug> #include <QDialogButtonBox> #include <QLabel> #include <QPushButton> #include <QRadioButton> #include <QScrollArea> #include <QVBoxLayout> -#include <KLocalizedString> - -#include "DB/FileName.h" -#include "DB/FileNameList.h" -#include "DB/ImageDB.h" -#include "DB/ImageInfo.h" -#include "DB/MD5.h" -#include "DuplicateMatch.h" -#include "DuplicateMerger.h" -#include "MergeToolTip.h" -#include "Utilities/DeleteFiles.h" -#include "Utilities/ShowBusyCursor.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 // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DuplicateMerger/DuplicateMerger.h b/MainWindow/DuplicateMerger/DuplicateMerger.h index 53a526dd..3b10cf0d 100644 --- a/MainWindow/DuplicateMerger/DuplicateMerger.h +++ b/MainWindow/DuplicateMerger/DuplicateMerger.h @@ -1,79 +1,79 @@ /* Copyright 2012-2016 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/>. */ #ifndef MAINWINDOW_DUPLICATEMERGER_H #define MAINWINDOW_DUPLICATEMERGER_H +#include <DB/FileNameList.h> +#include <DB/MD5.h> + #include <QDialog> #include <QMap> #include <QWidget> -#include <DB/FileNameList.h> -#include <DB/MD5.h> - class QVBoxLayout; class QRadioButton; class QLabel; class QPushButton; namespace MainWindow { class DuplicateMatch; class DuplicateMerger : public QDialog { Q_OBJECT public: explicit DuplicateMerger(QWidget *parent = nullptr); ~DuplicateMerger() override; private slots: void selectAll(); void selectNone(); void go(); void updateSelectionCount(); private: void findDuplicates(); void addRow(const DB::MD5 &); void selectAll(bool b); void tellThatNoDuplicatesWereFound(); QMap<DB::MD5, DB::FileNameList> m_matches; QWidget *m_container; QVBoxLayout *m_scrollLayout; QList<DuplicateMatch *> m_selectors; QRadioButton *m_trash; QRadioButton *m_deleteFromDisk; QLabel *m_selectionCount; QPushButton *m_selectAllButton; QPushButton *m_selectNoneButton; QPushButton *m_okButton; QPushButton *m_cancelButton; }; } // namespace MainWindow #endif // MAINWINDOW_DUPLICATEMERGER_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/DuplicateMerger/MergeToolTip.h b/MainWindow/DuplicateMerger/MergeToolTip.h index ebd45f04..53a141e8 100644 --- a/MainWindow/DuplicateMerger/MergeToolTip.h +++ b/MainWindow/DuplicateMerger/MergeToolTip.h @@ -1,47 +1,47 @@ /* Copyright 2012 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/>. */ #ifndef MAINWINDOW_MERGETOOLTIP_H #define MAINWINDOW_MERGETOOLTIP_H -#include "Utilities/ToolTip.h" +#include <Utilities/ToolTip.h> namespace MainWindow { class MergeToolTip : public Utilities::ToolTip { Q_OBJECT public: static MergeToolTip *instance(); static void destroy(); protected: void placeWindow() override; private: static MergeToolTip *s_instance; explicit MergeToolTip(QWidget *parent = nullptr); }; } // namespace MainWindow #endif // MAINWINDOW_MERGETOOLTIP_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/ExternalPopup.cpp b/MainWindow/ExternalPopup.cpp index cc7ff7b8..61ba320d 100644 --- a/MainWindow/ExternalPopup.cpp +++ b/MainWindow/ExternalPopup.cpp @@ -1,204 +1,203 @@ /* 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 "Logging.h" #include "RunDialog.h" #include "Window.h" #include <DB/FileNameList.h> #include <DB/ImageInfo.h> #include <Settings/SettingsData.h> +#include <KFileItem> +#include <KLocalizedString> +#include <KMimeTypeTrader> +#include <KRun> +#include <KService> +#include <KShell> #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> - 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 { qCWarning(MainWindowLog, "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; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/ExternalPopup.h b/MainWindow/ExternalPopup.h index f5cc9d7b..895df981 100644 --- a/MainWindow/ExternalPopup.h +++ b/MainWindow/ExternalPopup.h @@ -1,67 +1,68 @@ /* Copyright (C) 2003-2010 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. */ #ifndef EXTERNALPOPUP_H #define EXTERNALPOPUP_H -#include "DB/ImageInfoList.h" #include <DB/FileNameList.h> +#include <DB/ImageInfoList.h> +#include <Utilities/StringSet.h> + #include <QMenu> #include <QPixmap> -#include <Utilities/StringSet.h> #include <qpair.h> namespace DB { class ImageInfo; } namespace MainWindow { using Utilities::StringSet; typedef QSet<QPair<QString, QString>> OfferType; class ExternalPopup : public QMenu { Q_OBJECT public: explicit ExternalPopup(QWidget *parent); void populate(DB::ImageInfoPtr current, const DB::FileNameList &list); protected slots: void slotExecuteService(QAction *); protected: QString mimeType(const DB::FileName &file); StringSet mimeTypes(const DB::FileNameList &files); OfferType appInfos(const DB::FileNameList &files); private: DB::FileNameList m_list; DB::ImageInfoPtr m_currentInfo; QMap<QString, StringSet> m_appToMimeTypeMap; }; } bool operator<(const QPair<QString, QPixmap> &a, const QPair<QString, QPixmap> &b); #endif /* EXTERNALPOPUP_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/FeatureDialog.cpp b/MainWindow/FeatureDialog.cpp index 1bb389c6..a25dd073 100644 --- a/MainWindow/FeatureDialog.cpp +++ b/MainWindow/FeatureDialog.cpp @@ -1,221 +1,220 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "FeatureDialog.h" -#include <config-kpa-kgeomap.h> -#include <config-kpa-kipi.h> +#include <Exif/Database.h> + +#include <KLocalizedString> #include <QDialogButtonBox> #include <QLayout> #include <QList> #include <QProcess> #include <QPushButton> #include <QStandardPaths> #include <QTextBrowser> #include <QVBoxLayout> - -#include <KLocalizedString> +#include <config-kpa-kgeomap.h> +#include <config-kpa-kipi.h> #include <phonon/backendcapabilities.h> -#include "Exif/Database.h" - using namespace MainWindow; FeatureDialog::FeatureDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "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> to extract thumbnails from videos. These thumbnails are used to preview " "videos in the thumbnail viewer.</p>"); text += i18n("<h1><a name=\"videoInfo\">Video metadata support</a></h1>" "<p>KPhotoAlbum can use <tt>ffprobe</tt> to extract length information from videos." "</p>" "<p>Correct length information is 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::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(); } bool FeatureDialog::hasVideoProber() { return !ffprobeBinary().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()); features << Data(i18n("Video thumbnail support"), QString::fromLatin1("#videoPreview"), hasVideoThumbnailer()); features << Data(i18n("Video metadata support"), QString::fromLatin1("#videoInfo"), hasVideoProber()); QString result = QString::fromLatin1("<p><table>"); const QString red = QString::fromLatin1("<font color=\"red\">%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); } result += QString::fromLatin1("</table></p>"); return result; } QStringList MainWindow::FeatureDialog::supportedVideoMimeTypes() { return Phonon::BackendCapabilities::availableMimeTypes(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/FeatureDialog.h b/MainWindow/FeatureDialog.h index aabcbde3..0233168f 100644 --- a/MainWindow/FeatureDialog.h +++ b/MainWindow/FeatureDialog.h @@ -1,63 +1,62 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef FEATUREDIALOG_H #define FEATUREDIALOG_H #include <QDialog> - #include <QTextBrowser> namespace MainWindow { class FeatureDialog : public QDialog { Q_OBJECT public: explicit FeatureDialog(QWidget *parent); QSize sizeHint() const override; static bool hasAllFeaturesAvailable(); static QString featureString(); static QStringList supportedVideoMimeTypes(); static QString ffmpegBinary(); static QString ffprobeBinary(); /** * @brief hasVideoThumbnailer * @return true, if a program capable of creating video thumbnails is found, false otherwise */ static bool hasVideoThumbnailer(); /** * @brief hasVideoProber * @return true, if a program capable of extracting video metadata is found, false otherwise */ static bool hasVideoProber(); protected: static bool hasKIPISupport(); static bool hasEXIV2Support(); static bool hasEXIV2DBSupport(); static bool hasGeoMapSupport(); }; } #endif /* FEATUREDIALOG_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/ImageCounter.cpp b/MainWindow/ImageCounter.cpp index ffe1b726..c45c2a8e 100644 --- a/MainWindow/ImageCounter.cpp +++ b/MainWindow/ImageCounter.cpp @@ -1,53 +1,54 @@ /* 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)); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/InvalidDateFinder.cpp b/MainWindow/InvalidDateFinder.cpp index 8064ccd0..3a3f172f 100644 --- a/MainWindow/InvalidDateFinder.cpp +++ b/MainWindow/InvalidDateFinder.cpp @@ -1,146 +1,149 @@ /* 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 "DB/FileInfo.h" -#include "DB/ImageDB.h" -#include "DB/ImageDate.h" -#include "DB/ImageInfo.h" -#include "MainWindow/Window.h" -#include "Utilities/ShowBusyCursor.h" + +#include "Window.h" + +#include <DB/FileInfo.h> +#include <DB/ImageDB.h> +#include <DB/ImageDate.h> +#include <DB/ImageInfo.h> +#include <Utilities/ShowBusyCursor.h> + #include <KLocalizedString> #include <KTextEdit> #include <QDialogButtonBox> #include <QGroupBox> #include <QProgressDialog> #include <QPushButton> #include <QVBoxLayout> #include <qapplication.h> #include <qeventloop.h> #include <qlayout.h> #include <qradiobutton.h> using namespace MainWindow; InvalidDateFinder::InvalidDateFinder(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Search for Images and Videos with Missing Dates")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QWidget *mainWidget = new QWidget(this); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); mainLayout->addWidget(mainWidget); QGroupBox *grp = new QGroupBox(i18n("Which Images and Videos to Display")); QVBoxLayout *grpLay = new QVBoxLayout(grp); mainLayout->addWidget(grp); m_dateNotTime = new QRadioButton(i18n("Search for images and videos with a valid date but an invalid time stamp")); m_missingDate = new QRadioButton(i18n("Search for images and videos missing date and time")); m_partialDate = new QRadioButton(i18n("Search for images and videos with only partial dates (like 1971 vs. 11/7-1971)")); m_dateNotTime->setChecked(true); grpLay->addWidget(m_dateNotTime); grpLay->addWidget(m_missingDate); grpLay->addWidget(m_partialDate); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &InvalidDateFinder::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &InvalidDateFinder::reject); mainLayout->addWidget(buttonBox); } void InvalidDateFinder::accept() { QDialog::accept(); Utilities::ShowBusyCursor dummy; // create the info dialog QDialog *info = new QDialog; QVBoxLayout *mainLayout = new QVBoxLayout; info->setLayout(mainLayout); info->setWindowTitle(i18nc("@title:window", "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(i18nc("@title:window", "Reading File Properties")); dialog.setMaximum(list.size()); dialog.setValue(0); int progress = 0; Q_FOREACH (const DB::FileName &fileName, list) { dialog.setValue(++progress); qApp->processEvents(QEventLoop::AllEvents); if (dialog.wasCanceled()) break; if (fileName.info()->isNull()) continue; DB::ImageDate date = fileName.info()->date(); bool show = false; if (m_dateNotTime->isChecked()) { DB::FileInfo fi = DB::FileInfo::read(fileName, DB::EXIFMODE_DATE); if (fi.dateTime().date() == date.start().date()) show = (fi.dateTime().time() != date.start().time()); if (show) { edit->append(QString::fromLatin1("%1:<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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Options.cpp b/MainWindow/Options.cpp index 3268efca..9e9464b6 100644 --- a/MainWindow/Options.cpp +++ b/MainWindow/Options.cpp @@ -1,143 +1,143 @@ /* Copyright (C) 2016 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) 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 "Options.h" + #include "Logging.h" +#include <KLocalizedString> #include <QCommandLineOption> #include <QCommandLineParser> -#include <KLocalizedString> - MainWindow::Options *MainWindow::Options::s_instance = nullptr; namespace MainWindow { class Options::OptionsPrivate { public: QCommandLineParser parser; // legacy option: "-c <imageDirectory>" QCommandLineOption configFile { QLatin1String("c"), i18n("Use <databaseFile> instead of the default. Deprecated - use '--db <databaseFile>' instead."), i18n("databaseFile") }; QCommandLineOption dbFile { QLatin1String("db"), i18n("Use <databaseFile> instead of the default."), i18n("databaseFile") }; QCommandLineOption demoOption { QLatin1String("demo"), i18n("Starts KPhotoAlbum with a prebuilt set of demo images.") }; QCommandLineOption importFile { QLatin1String("import"), i18n("Import file."), i18n("file.kim") }; // QCommandLineParser doesn't support optional values. // therefore, we need two separate options: QCommandLineOption listen { QLatin1String("listen"), i18n("Listen for network connections.") }; QCommandLineOption listenAddress { QLatin1String("listen-address"), i18n("Listen for network connections on address <interface_address>."), i18n("interface_address") }; QCommandLineOption searchOnStartup { QLatin1String("search"), i18n("Search for new images on startup.") }; }; } MainWindow::Options *MainWindow::Options::the() { if (!s_instance) s_instance = new Options(); return s_instance; } QCommandLineParser *MainWindow::Options::parser() const { return &(d->parser); } QUrl MainWindow::Options::dbFile() const { QUrl db; if (d->parser.isSet(d->dbFile)) { db = QUrl::fromLocalFile(d->parser.value(d->dbFile)); } else if (d->parser.isSet(d->configFile)) { // support for legacy option db = QUrl::fromLocalFile(d->parser.value(d->configFile)); } return db; } bool MainWindow::Options::demoMode() const { return d->parser.isSet(d->demoOption); } QUrl MainWindow::Options::importFile() const { if (d->parser.isSet(d->importFile)) return QUrl::fromLocalFile(d->parser.value(d->importFile)); return QUrl(); } QHostAddress MainWindow::Options::listen() const { QHostAddress address; QString value = d->parser.value(d->listenAddress); if (d->parser.isSet(d->listen) || !value.isEmpty()) { if (value.isEmpty()) address = QHostAddress::Any; else address = QHostAddress(value); } if (address.isMulticast() || address == QHostAddress::Broadcast) { qCWarning(MainWindowLog) << "Won't bind to address" << address; address = QHostAddress::Null; } return address; } bool MainWindow::Options::searchForImagesOnStart() const { return d->parser.isSet(d->searchOnStartup); } MainWindow::Options::Options() : d(new OptionsPrivate) { d->parser.addVersionOption(); d->parser.addHelpOption(); d->configFile.setFlags(QCommandLineOption::HiddenFromHelp); d->parser.addOptions( QList<QCommandLineOption>() << d->configFile << d->dbFile << d->demoOption << d->importFile << d->listen << d->listenAddress << d->searchOnStartup); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/RunDialog.cpp b/MainWindow/RunDialog.cpp index 001a8fa3..b29d0d15 100644 --- a/MainWindow/RunDialog.cpp +++ b/MainWindow/RunDialog.cpp @@ -1,107 +1,107 @@ /* Copyright (C) 2009-2010 Wes Hardaker <kpa@capturedonearth.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 "RunDialog.h" -#include <MainWindow/Window.h> -#include <KLocalizedString> -#include <krun.h> -#include <kshell.h> +#include "Window.h" +#include <KLocalizedString> #include <QDialog> #include <QDialogButtonBox> #include <QLabel> #include <QPushButton> #include <QVBoxLayout> #include <QWidget> +#include <krun.h> +#include <kshell.h> MainWindow::RunDialog::RunDialog(QWidget *parent) : QDialog(parent) { QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); // xgettext: no-c-format QString txt = i18n("<p>Enter your command to run below:</p>" "<p><i>%all will be replaced with a file list</i></p>"); QLabel *label = new QLabel(txt); mainLayout->addWidget(label); m_cmd = new QLineEdit(); mainLayout->addWidget(m_cmd); m_cmd->setMinimumWidth(400); // xgettext: no-c-format txt = i18n("<p>Enter the command you want to run on your image file(s). " "KPhotoAlbum will run your command and replace any '%all' tokens " "with a list of your files. For example, if you entered:</p>" "<ul><li>cp %all /tmp</li></ul>" "<p>Then the files you selected would be copied to the /tmp " "directory</p>" "<p>You can also use %each to have a command be run once per " "file.</p>"); m_cmd->setWhatsThis(txt); label->setWhatsThis(txt); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); mainLayout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(this, &QDialog::accepted, this, &RunDialog::slotMarkGo); } void MainWindow::RunDialog::setImageList(const DB::FileNameList &fileList) { m_fileList = fileList; } void MainWindow::RunDialog::slotMarkGo() { QString cmdString = m_cmd->text(); // xgettext: no-c-format QRegExp replaceall = QRegExp(i18nc("As in 'Execute a command and replace any occurrence of %all with the filenames of all selected files'", "%all")); // xgettext: no-c-format QRegExp replaceeach = QRegExp(i18nc("As in 'Execute a command for each selected file in turn and replace any occurrence of %each with the filename ", "%each")); // Replace the %all argument first QStringList fileList; Q_FOREACH (const DB::FileName &fileName, m_fileList) fileList.append(fileName.absolute()); cmdString.replace(replaceall, KShell::joinArgs(fileList)); if (cmdString.contains(replaceeach)) { // cmdString should be run multiple times, once per "each" QString cmdOnce; Q_FOREACH (const DB::FileName &filename, m_fileList) { cmdOnce = cmdString; cmdOnce.replace(replaceeach, filename.absolute()); KRun::runCommand(cmdOnce, MainWindow::Window::theMainWindow()); } } else { KRun::runCommand(cmdString, MainWindow::Window::theMainWindow()); } } void MainWindow::RunDialog::show() { QDialog::show(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/RunDialog.h b/MainWindow/RunDialog.h index 19e593f6..8e9649c3 100644 --- a/MainWindow/RunDialog.h +++ b/MainWindow/RunDialog.h @@ -1,51 +1,52 @@ /* Copyright (C) 2003-2006 Jesper K. Pedersen <blackie@kde.org> Copyright (C) 2009-2010 Wes Hardaker <kpa@capturedonearth.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. */ #ifndef RUNDIALOG_H #define RUNDIALOG_H #include <DB/FileNameList.h> + #include <QDialog> #include <QLineEdit> namespace MainWindow { class RunDialog : public QDialog { Q_OBJECT public: explicit RunDialog(QWidget *parent); void setImageList(const DB::FileNameList &fileList); void show(); protected slots: void slotMarkGo(); private: bool *m_ok; QLineEdit *m_cmd; DB::FileNameList m_fileList; }; } #endif /* RUNDIALOG_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/SearchBar.cpp b/MainWindow/SearchBar.cpp index 1e2ef313..c333927f 100644 --- a/MainWindow/SearchBar.cpp +++ b/MainWindow/SearchBar.cpp @@ -1,82 +1,83 @@ /* 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 <KLocalizedString> #include <QEvent> #include <QKeyEvent> #include <QLineEdit> #include <kactioncollection.h> #include <kmainwindow.h> #include <qapplication.h> #include <qlabel.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(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/SplashScreen.cpp b/MainWindow/SplashScreen.cpp index a171edaa..17f00ee0 100644 --- a/MainWindow/SplashScreen.cpp +++ b/MainWindow/SplashScreen.cpp @@ -1,73 +1,72 @@ /* 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 <KAboutData> +#include <KLocalizedString> #include <QPainter> #include <QRegExp> #include <QStandardPaths> -#include <KAboutData> -#include <KLocalizedString> - MainWindow::SplashScreen *MainWindow::SplashScreen::s_instance = nullptr; MainWindow::SplashScreen::SplashScreen() : QSplashScreen(QStandardPaths::locate(QStandardPaths::DataLocation, 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(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/StatisticsDialog.cpp b/MainWindow/StatisticsDialog.cpp index 71e1acaf..498a56ba 100644 --- a/MainWindow/StatisticsDialog.cpp +++ b/MainWindow/StatisticsDialog.cpp @@ -1,226 +1,225 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "StatisticsDialog.h" -#include "DB/Category.h" -#include "DB/CategoryCollection.h" -#include "DB/ImageDB.h" -#include "DB/ImageSearchInfo.h" -#include "Utilities/ShowBusyCursor.h" +#include <DB/Category.h> +#include <DB/CategoryCollection.h> +#include <DB/ImageDB.h> +#include <DB/ImageSearchInfo.h> +#include <Utilities/ShowBusyCursor.h> #include <KComboBox> #include <KLocalizedString> - #include <QDialogButtonBox> #include <QGroupBox> #include <QHeaderView> #include <QLabel> #include <QPushButton> #include <QTreeWidget> #include <QVBoxLayout> using namespace MainWindow; StatisticsDialog::StatisticsDialog(QWidget *parent) : QDialog(parent) { QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); QString txt = i18n("<h1>Description</h1>" "<table>" "<tr><td># of Items</td><td>This is the number of different items in the category</td></tr>" "<tr><td>Tags Total</td><td>This is a count of how many tags was made,<br/>i.e. a simple counting though all the images</td></tr>" "<tr><td>Tags Per Picture</td><td>This tells you how many tags are on each picture on average</td></tr>" "</table><br/><br/>" "Do not get too attached to this dialog, it has the problem that it counts categories AND subcategories,<br/>" "so if an image has been taken in Las Vegas, Nevada, USA, then 3 tags are counted for that image,<br/>" "while it should only be one.<br/>" "I am not really sure if it is worth fixing that bug (as it is pretty hard to fix),<br/>" "so maybe the dialog will simply go away again"); QLabel *label = new QLabel(txt); layout->addWidget(label); layout->addWidget(createAnnotatedGroupBox()); label = new QLabel(i18n("<h1>Statistics</h1>")); layout->addWidget(label); m_treeWidget = new QTreeWidget; layout->addWidget(m_treeWidget); QStringList labels; labels << i18n("Category") << i18n("# of Items") << i18n("Tags Totals") << i18n("Tags Per Picture") << QString(); m_treeWidget->setHeaderLabels(labels); 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); } void StatisticsDialog::show() { populate(); QDialog::show(); } QSize MainWindow::StatisticsDialog::sizeHint() const { return QSize(800, 800); } QTreeWidgetItem *MainWindow::StatisticsDialog::addRow(const QString &title, int noOfTags, int tagCount, int imageCount, QTreeWidgetItem *parent) { QStringList list; list << title << QString::number(noOfTags) << QString::number(tagCount) << QString::number((double)tagCount / imageCount, 'F', 2); QTreeWidgetItem *item = new QTreeWidgetItem(parent, list); for (int col = 1; col < 4; ++col) item->setTextAlignment(col, Qt::AlignRight); return item; } void MainWindow::StatisticsDialog::highlightTotalRow(QTreeWidgetItem *item) { for (int col = 0; col < 5; ++col) { QFont font = item->data(col, Qt::FontRole).value<QFont>(); font.setWeight(QFont::Bold); item->setData(col, Qt::FontRole, font); } } QGroupBox *MainWindow::StatisticsDialog::createAnnotatedGroupBox() { QGroupBox *box = new QGroupBox(i18n("Tag indication completed annotation")); m_boxLayout = new QGridLayout(box); m_boxLayout->setColumnStretch(2, 1); int row = -1; QLabel *label = new QLabel(i18n("If you use a specific tag to indicate that an image has been tagged, then specify it here.")); label->setWordWrap(true); m_boxLayout->addWidget(label, ++row, 0, 1, 3); label = new QLabel(i18n("Category:")); m_boxLayout->addWidget(label, ++row, 0); m_category = new KComboBox; m_boxLayout->addWidget(m_category, row, 1); m_tagLabel = new QLabel(i18n("Tag:")); m_boxLayout->addWidget(m_tagLabel, ++row, 0); m_tag = new KComboBox; m_tag->setSizeAdjustPolicy(KComboBox::AdjustToContents); m_boxLayout->addWidget(m_tag, row, 1); m_category->addItem(i18nc("@item:inlistbox meaning 'no category'", "None")); QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH (const DB::CategoryPtr &category, categories) { if (category->type() == DB::Category::MediaTypeCategory || category->type() == DB::Category::FolderCategory) { continue; } m_category->addItem(category->name(), category->name()); } connect(m_category, static_cast<void (KComboBox::*)(int)>(&KComboBox::activated), this, &StatisticsDialog::categoryChanged); connect(m_tag, static_cast<void (KComboBox::*)(int)>(&KComboBox::activated), this, &StatisticsDialog::populate); m_tagLabel->setEnabled(false); m_tag->setEnabled(false); return box; } void MainWindow::StatisticsDialog::categoryChanged(int index) { const bool enabled = (index != 0); m_tagLabel->setEnabled(enabled); m_tag->setEnabled(enabled); m_tag->clear(); if (enabled) { const QString name = m_category->itemData(index).value<QString>(); DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(name); m_tag->addItems(category->items()); } } void MainWindow::StatisticsDialog::populate() { Utilities::ShowBusyCursor dummy; m_treeWidget->clear(); const int imageCount = DB::ImageDB::instance()->totalCount(); QTreeWidgetItem *top = new QTreeWidgetItem(m_treeWidget, QStringList() << i18nc("As in 'all images'", "All") << QString::number(imageCount)); top->setTextAlignment(1, Qt::AlignRight); populateSubTree(DB::ImageSearchInfo(), imageCount, top); if (m_category->currentIndex() != 0) { const QString category = m_category->itemData(m_category->currentIndex()).value<QString>(); const QString tag = m_tag->currentText(); DB::ImageSearchInfo info; info.setCategoryMatchText(category, tag); const int imageCount = DB::ImageDB::instance()->count(info).total(); QTreeWidgetItem *item = new QTreeWidgetItem(m_treeWidget, QStringList() << QString::fromLatin1("%1: %2").arg(category).arg(tag) << QString::number(imageCount)); item->setTextAlignment(1, Qt::AlignRight); populateSubTree(info, imageCount, item); } m_treeWidget->header()->resizeSections(QHeaderView::ResizeToContents); } void MainWindow::StatisticsDialog::populateSubTree(const DB::ImageSearchInfo &info, int imageCount, QTreeWidgetItem *top) { top->setExpanded(true); QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); int tagsTotal = 0; int grantTotal = 0; Q_FOREACH (const DB::CategoryPtr &category, categories) { if (category->type() == DB::Category::MediaTypeCategory || category->type() == DB::Category::FolderCategory) { continue; } const QMap<QString, DB::CountWithRange> tags = DB::ImageDB::instance()->classify(info, category->name(), DB::anyMediaType); int total = 0; for (auto tagIt = tags.constBegin(); tagIt != tags.constEnd(); ++tagIt) { // Don't count the NONE tag, and the OK tag if (tagIt.key() != DB::ImageDB::NONE() && (category->name() != m_category->currentText() || tagIt.key() != m_tag->currentText())) total += tagIt.value().count; } addRow(category->name(), tags.count() - 1, total, imageCount, top); tagsTotal += tags.count() - 1; grantTotal += total; } QTreeWidgetItem *totalRow = addRow(i18n("Total"), tagsTotal, grantTotal, imageCount, top); highlightTotalRow(totalRow); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/StatusBar.cpp b/MainWindow/StatusBar.cpp index 68842e52..1133b285 100644 --- a/MainWindow/StatusBar.cpp +++ b/MainWindow/StatusBar.cpp @@ -1,188 +1,187 @@ /* Copyright (C) 2003-2010 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 "StatusBar.h" +#include "DirtyIndicator.h" +#include "ImageCounter.h" + +#include <BackgroundTaskManager/StatusIndicator.h> +#include <DB/ImageDB.h> +#include <Settings/SettingsData.h> +#include <ThumbnailView/ThumbnailFacade.h> + +#include <KIconLoader> +#include <KLocalizedString> #include <QApplication> #include <QHBoxLayout> #include <QIcon> #include <QLabel> #include <QProgressBar> #include <QSlider> #include <QTimer> #include <QToolButton> #include <QVBoxLayout> - -#include <KIconLoader> -#include <KLocalizedString> - -#include <BackgroundTaskManager/StatusIndicator.h> -#include <DB/ImageDB.h> #include <RemoteControl/ConnectionIndicator.h> -#include <Settings/SettingsData.h> -#include <ThumbnailView/ThumbnailFacade.h> - -#include "DirtyIndicator.h" -#include "ImageCounter.h" MainWindow::StatusBar::StatusBar() : QStatusBar() { QPalette pal = palette(); pal.setBrush(QPalette::Base, QApplication::palette().color(QPalette::Background)); pal.setBrush(QPalette::Background, QApplication::palette().color(QPalette::Background)); setPalette(pal); setupGUI(); m_pendingShowTimer = new QTimer(this); m_pendingShowTimer->setSingleShot(true); connect(m_pendingShowTimer, &QTimer::timeout, this, &StatusBar::showStatusBar); } void MainWindow::StatusBar::setupGUI() { setContentsMargins(7, 2, 7, 2); QWidget *indicators = new QWidget(this); QHBoxLayout *indicatorsHBoxLayout = new QHBoxLayout(indicators); indicatorsHBoxLayout->setMargin(0); indicatorsHBoxLayout->setSpacing(10); mp_dirtyIndicator = new DirtyIndicator(indicators); indicatorsHBoxLayout->addWidget(mp_dirtyIndicator); connect(DB::ImageDB::instance(), SIGNAL(dirty()), mp_dirtyIndicator, SLOT(markDirtySlot())); auto *remoteIndicator = new RemoteControl::ConnectionIndicator(indicators); indicatorsHBoxLayout->addWidget(remoteIndicator); auto *jobIndicator = new BackgroundTaskManager::StatusIndicator(indicators); indicatorsHBoxLayout->addWidget(jobIndicator); m_progressBar = new QProgressBar(this); m_progressBar->setMinimumWidth(400); addPermanentWidget(m_progressBar, 0); m_cancel = new QToolButton(this); m_cancel->setIcon(QIcon::fromTheme(QString::fromLatin1("dialog-close"))); m_cancel->setShortcut(Qt::Key_Escape); addPermanentWidget(m_cancel, 0); connect(m_cancel, &QToolButton::clicked, this, &StatusBar::cancelRequest); connect(m_cancel, &QToolButton::clicked, this, &StatusBar::hideStatusBar); m_lockedIndicator = new QLabel(indicators); indicatorsHBoxLayout->addWidget(m_lockedIndicator); addPermanentWidget(indicators, 0); mp_partial = new ImageCounter(this); addPermanentWidget(mp_partial, 0); mp_selected = new ImageCounter(this); addPermanentWidget(mp_selected, 0); ImageCounter *total = new ImageCounter(this); addPermanentWidget(total, 0); total->setTotal(DB::ImageDB::instance()->totalCount()); connect(DB::ImageDB::instance(), SIGNAL(totalChanged(uint)), total, SLOT(setTotal(uint))); mp_pathIndicator = new BreadcrumbViewer; addWidget(mp_pathIndicator, 1); setProgressBarVisible(false); m_thumbnailSizeSlider = ThumbnailView::ThumbnailFacade::instance()->createResizeSlider(); addPermanentWidget(m_thumbnailSizeSlider, 0); // prevent stretching: m_thumbnailSizeSlider->setMaximumSize(m_thumbnailSizeSlider->size()); m_thumbnailSizeSlider->setMinimumSize(m_thumbnailSizeSlider->size()); m_thumbnailSizeSlider->hide(); m_thumbnailSettings = new QToolButton; m_thumbnailSettings->setIcon(QIcon::fromTheme(QString::fromUtf8("settings-configure"))); m_thumbnailSettings->setToolTip(i18n("Thumbnail settings...")); addPermanentWidget(m_thumbnailSettings, 0); m_thumbnailSettings->hide(); connect(m_thumbnailSettings, &QToolButton::clicked, this, &StatusBar::thumbnailSettingsRequested); } void MainWindow::StatusBar::setLocked(bool locked) { static QPixmap *lockedPix = new QPixmap(SmallIcon(QString::fromLatin1("object-locked"))); m_lockedIndicator->setFixedWidth(lockedPix->width()); if (locked) m_lockedIndicator->setPixmap(*lockedPix); else m_lockedIndicator->setPixmap(QPixmap()); } void MainWindow::StatusBar::startProgress(const QString &text, int total) { m_progressBar->setFormat(text + QString::fromLatin1(": %p%")); m_progressBar->setMaximum(total); m_progressBar->setValue(0); m_pendingShowTimer->start(1000); // To avoid flicker we will only show the statusbar after 1 second. } void MainWindow::StatusBar::setProgress(int progress) { if (progress == m_progressBar->maximum()) hideStatusBar(); // If progress comes in to fast, then the UI will freeze from all time spent on updating the progressbar. static QTime time; if (time.isNull() || time.elapsed() > 200) { m_progressBar->setValue(progress); time.restart(); } } void MainWindow::StatusBar::setProgressBarVisible(bool show) { m_progressBar->setVisible(show); m_cancel->setVisible(show); } void MainWindow::StatusBar::showThumbnailSlider() { m_thumbnailSizeSlider->setVisible(true); m_thumbnailSettings->show(); } void MainWindow::StatusBar::hideThumbnailSlider() { m_thumbnailSizeSlider->setVisible(false); m_thumbnailSettings->hide(); } void MainWindow::StatusBar::enterEvent(QEvent *) { // make sure that breadcrumbs are not obscured by messages clearMessage(); } void MainWindow::StatusBar::hideStatusBar() { setProgressBarVisible(false); m_pendingShowTimer->stop(); } void MainWindow::StatusBar::showStatusBar() { setProgressBarVisible(true); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/StatusBar.h b/MainWindow/StatusBar.h index 0d58f307..f29c2460 100644 --- a/MainWindow/StatusBar.h +++ b/MainWindow/StatusBar.h @@ -1,78 +1,79 @@ /* Copyright (C) 2003-2010 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. */ #ifndef STATUSBAR_H #define STATUSBAR_H #include "BreadcrumbViewer.h" + #include <QStatusBar> class QToolButton; class QTimer; class QProgressBar; class QLabel; class QSlider; namespace MainWindow { class ImageCounter; class DirtyIndicator; class StatusBar : public QStatusBar { Q_OBJECT public: StatusBar(); DirtyIndicator *mp_dirtyIndicator; ImageCounter *mp_partial; ImageCounter *mp_selected; BreadcrumbViewer *mp_pathIndicator; void setLocked(bool locked); void startProgress(const QString &text, int total); void setProgress(int progress); void setProgressBarVisible(bool); void showThumbnailSlider(); void hideThumbnailSlider(); signals: void cancelRequest(); void thumbnailSettingsRequested(); protected: void enterEvent(QEvent *event) override; private slots: void hideStatusBar(); void showStatusBar(); private: void setupGUI(); void setPendingShow(); QLabel *m_lockedIndicator; QProgressBar *m_progressBar; QToolButton *m_cancel; QTimer *m_pendingShowTimer; QSlider *m_thumbnailSizeSlider; QToolButton *m_thumbnailSettings; }; } #endif /* STATUSBAR_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/TokenEditor.cpp b/MainWindow/TokenEditor.cpp index 30dbf839..a6648792 100644 --- a/MainWindow/TokenEditor.cpp +++ b/MainWindow/TokenEditor.cpp @@ -1,136 +1,138 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "DB/Category.h" -#include "DB/CategoryCollection.h" -#include "DB/ImageDB.h" -#include "DB/ImageSearchInfo.h" -#include "Settings/SettingsData.h" + +#include <DB/Category.h> +#include <DB/CategoryCollection.h> +#include <DB/ImageDB.h> +#include <DB/ImageSearchInfo.h> +#include <Settings/SettingsData.h> + #include <KLocalizedString> #include <QDialogButtonBox> #include <QGridLayout> #include <QHBoxLayout> #include <QPushButton> #include <QVBoxLayout> #include <qcheckbox.h> #include <qlabel.h> #include <qlayout.h> using namespace MainWindow; TokenEditor::TokenEditor(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Remove Tokens")); QVBoxLayout *dialogLayout = new QVBoxLayout(this); QWidget *mainContents = new QWidget; QVBoxLayout *vlay = new QVBoxLayout(mainContents); QLabel *label = new QLabel(i18n("Select tokens to remove from all images and videos:")); vlay->addWidget(label); QGridLayout *grid = new QGridLayout; vlay->addLayout(grid); int index = 0; for (int ch = 'A'; ch <= 'Z'; ch++, index++) { QChar token = QChar::fromLatin1((char)ch); QCheckBox *box = new QCheckBox(token); grid->addWidget(box, index / 5, index % 5); m_checkBoxes.append(box); } QHBoxLayout *hlay = new QHBoxLayout; vlay->addLayout(hlay); hlay->addStretch(1); QPushButton *selectAll = new QPushButton(i18n("Select All")); QPushButton *selectNone = new QPushButton(i18n("Select None")); hlay->addWidget(selectAll); hlay->addWidget(selectNone); connect(selectAll, &QPushButton::clicked, this, &TokenEditor::selectAll); connect(selectNone, &QPushButton::clicked, this, &TokenEditor::selectNone); dialogLayout->addWidget(mainContents); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttonBox->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &TokenEditor::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &TokenEditor::reject); dialogLayout->addWidget(buttonBox); } void TokenEditor::show() { QStringList tokens = tokensInUse(); Q_FOREACH (QCheckBox *box, m_checkBoxes) { box->setChecked(false); QString txt = box->text().remove(QString::fromLatin1("&")); box->setEnabled(tokens.contains(txt)); } QDialog::show(); } void TokenEditor::selectAll() { Q_FOREACH (QCheckBox *box, m_checkBoxes) { box->setChecked(true); } } void TokenEditor::selectNone() { Q_FOREACH (QCheckBox *box, m_checkBoxes) { box->setChecked(false); } } /** I would love to use Settings::optionValue, but that method does not forget about an item once it has seen it, which is really what it should do anyway, otherwise it would be way to expensive in use. */ QStringList TokenEditor::tokensInUse() { QStringList res; DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); QMap<QString, DB::CountWithRange> map = DB::ImageDB::instance()->classify(DB::ImageSearchInfo(), tokensCategory->name(), DB::anyMediaType); for (auto it = map.constBegin(); it != map.constEnd(); ++it) { if (it.value().count > 0) res.append(it.key()); } return res; } void TokenEditor::accept() { DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); Q_FOREACH (const QCheckBox *box, m_checkBoxes) { if (box->isChecked() && box->isEnabled()) { QString txt = box->text().remove(QString::fromLatin1("&")); tokensCategory->removeItem(txt); } } QDialog::accept(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/UpdateVideoThumbnail.cpp b/MainWindow/UpdateVideoThumbnail.cpp index 6843501c..df9bb93c 100644 --- a/MainWindow/UpdateVideoThumbnail.cpp +++ b/MainWindow/UpdateVideoThumbnail.cpp @@ -1,87 +1,89 @@ /* Copyright (C) 2012-2019 The KPhotoAlbum Development Team 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 "UpdateVideoThumbnail.h" + #include "Window.h" + #include <BackgroundJobs/HandleVideoThumbnailRequestJob.h> #include <ImageManager/ThumbnailCache.h> #include <ThumbnailView/CellGeometry.h> #include <Utilities/FileUtil.h> #include <Utilities/VideoUtil.h> namespace MainWindow { void UpdateVideoThumbnail::useNext(const DB::FileNameList &list) { update(list, +1); } void UpdateVideoThumbnail::usePrevious(const DB::FileNameList &list) { update(list, -1); } void UpdateVideoThumbnail::update(const DB::FileNameList &list, int direction) { Q_FOREACH (const DB::FileName &fileName, list) { if (Utilities::isVideo(fileName)) update(fileName, direction); } } void UpdateVideoThumbnail::update(const DB::FileName &fileName, int direction) { const DB::FileName baseImageName = BackgroundJobs::HandleVideoThumbnailRequestJob::pathForRequest(fileName); QImage baseImage(baseImageName.absolute()); int frame = 0; for (; frame < 10; ++frame) { const DB::FileName frameFile = BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(fileName, frame); QImage frameImage(frameFile.absolute()); if (frameImage.isNull()) continue; if (baseImage == frameImage) { break; } } const DB::FileName newImageName = nextExistingImage(fileName, frame, direction); Utilities::copyOrOverwrite(newImageName.absolute(), baseImageName.absolute()); QImage image = QImage(newImageName.absolute()).scaled(ThumbnailView::CellGeometry::preferredIconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation); ImageManager::ThumbnailCache::instance()->insert(fileName, image); MainWindow::Window::theMainWindow()->reloadThumbnails(); } DB::FileName UpdateVideoThumbnail::nextExistingImage(const DB::FileName &fileName, int frame, int direction) { for (int i = 1; i < 10; ++i) { const int nextIndex = (frame + 10 + direction * i) % 10; const DB::FileName file = BackgroundJobs::HandleVideoThumbnailRequestJob::frameName(fileName, nextIndex); if (file.exists()) return file; } Q_ASSERT(false && "We should always find at least the current frame"); return DB::FileName(); } } // namespace MainWindow // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/WelcomeDialog.cpp b/MainWindow/WelcomeDialog.cpp index 7338aba8..00c50c28 100644 --- a/MainWindow/WelcomeDialog.cpp +++ b/MainWindow/WelcomeDialog.cpp @@ -1,204 +1,204 @@ /* 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 <KConfigGroup> #include <KLocalizedString> #include <KMessageBox> #include <KSharedConfig> #include <KShell> - #include <QDialogButtonBox> #include <QFileDialog> #include <QHBoxLayout> #include <QLabel> #include <QLayout> #include <QLineEdit> #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(QStandardPaths::locate(QStandardPaths::DataLocation, 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 built with support for all its features. The following is a list " "indicating what you may be missing:<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")); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Window.cpp b/MainWindow/Window.cpp index f9c6d399..1b2f0201 100644 --- a/MainWindow/Window.cpp +++ b/MainWindow/Window.cpp @@ -1,1997 +1,1997 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "Window.h" -#include <config-kpa-kipi.h> +#include <config-kpa-kipi.h> #include <stdexcept> #ifdef HAVE_STDLIB_H #include <stdlib.h> #endif +#include <KActionCollection> +#include <KActionMenu> +#include <KConfigGroup> +#include <KEditToolBar> +#include <KIconLoader> +#include <KLocalizedString> +#include <KMessageBox> +#include <KPasswordDialog> +#include <KProcess> +#include <KSharedConfig> +#include <KShortcutsDialog> +#include <KStandardAction> +#include <KToggleAction> #include <QApplication> #include <QClipboard> #include <QCloseEvent> #include <QContextMenuEvent> #include <QCursor> #include <QDesktopServices> #include <QDir> #include <QElapsedTimer> #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 <KConfigGroup> -#include <KEditToolBar> -#include <KIconLoader> -#include <KLocalizedString> -#include <KMessageBox> -#include <KPasswordDialog> -#include <KProcess> -#include <KSharedConfig> -#include <KShortcutsDialog> -#include <KStandardAction> -#include <KToggleAction> #include <kio_version.h> // for #if KIO_VERSION... #include <ktip.h> #ifdef HASKIPI #include <KIPI/Plugin> #include <KIPI/PluginLoader> #endif #include <AnnotationDialog/Dialog.h> #include <BackgroundJobs/SearchForVideosWithoutLengthInfo.h> #include <BackgroundJobs/SearchForVideosWithoutVideoThumbnailsJob.h> #include <BackgroundTaskManager/JobManager.h> #include <Browser/BrowserWidget.h> #include <DB/CategoryCollection.h> #include <DB/ImageDB.h> #include <DB/ImageDateCollection.h> #include <DB/ImageInfo.h> #include <DB/MD5.h> #include <DB/MD5Map.h> #include <DB/UIDelegate.h> #include <DateBar/DateBarWidget.h> #include <Exif/Database.h> #include <Exif/Info.h> #include <Exif/InfoDialog.h> #include <Exif/ReReadDialog.h> #include <HTMLGenerator/HTMLDialog.h> #include <ImageManager/AsyncLoader.h> #include <ImageManager/ThumbnailBuilder.h> #include <ImageManager/ThumbnailCache.h> #include <ImportExport/Export.h> #include <ImportExport/Import.h> #ifdef HASKIPI #include <Plugins/Interface.h> #endif #ifdef KF5Purpose_FOUND #include <Plugins/PurposeMenu.h> #endif -#include <RemoteControl/RemoteInterface.h> -#include <Settings/SettingsData.h> -#include <Settings/SettingsDialog.h> -#include <ThumbnailView/FilterWidget.h> -#include <ThumbnailView/ThumbnailFacade.h> -#include <ThumbnailView/enums.h> -#include <Utilities/DemoUtil.h> -#include <Utilities/FileNameUtil.h> -#include <Utilities/List.h> -#include <Utilities/ShowBusyCursor.h> -#include <Utilities/VideoUtil.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" +#include <Settings/SettingsData.h> +#include <Settings/SettingsDialog.h> +#include <ThumbnailView/FilterWidget.h> +#include <ThumbnailView/ThumbnailFacade.h> +#include <ThumbnailView/enums.h> +#include <Utilities/DemoUtil.h> +#include <Utilities/FileNameUtil.h> +#include <Utilities/List.h> +#include <Utilities/ShowBusyCursor.h> +#include <Utilities/VideoUtil.h> +#include <Viewer/ViewerWidget.h> + +#include <RemoteControl/RemoteInterface.h> + using namespace DB; MainWindow::Window *MainWindow::Window::s_instance = nullptr; MainWindow::Window::Window(QWidget *parent) : KXmlGuiWindow(parent) , m_annotationDialog(nullptr) , m_deleteDialog(nullptr) , m_htmlDialog(nullptr) , m_tokenEditor(nullptr) { #ifdef HAVE_KGEOMAP m_positionBrowser = 0; #endif qCDebug(MainWindowLog) << "Using icon theme: " << QIcon::themeName(); qCDebug(MainWindowLog) << "Icon search paths: " << QIcon::themeSearchPaths(); QElapsedTimer timer; timer.start(); SplashScreen::instance()->message(i18n("Loading Database")); s_instance = this; bool gotConfigFile = load(); if (!gotConfigFile) throw 0; qCInfo(TimingLog) << "MainWindow: Loading Database: " << timer.restart() << "ms."; SplashScreen::instance()->message(i18n("Loading Main Window")); QWidget *top = new QWidget(this); QVBoxLayout *lay = new QVBoxLayout(top); lay->setSpacing(2); lay->setContentsMargins(2, 2, 2, 2); setCentralWidget(top); m_stack = new QStackedWidget(top); lay->addWidget(m_stack, 1); m_dateBar = new DateBar::DateBarWidget(top); lay->addWidget(m_dateBar); m_dateBarLine = new QFrame(top); m_dateBarLine->setFrameStyle(QFrame::HLine | QFrame::Plain); m_dateBarLine->setLineWidth(0); m_dateBarLine->setMidLineWidth(0); QPalette pal = m_dateBarLine->palette(); pal.setColor(QPalette::WindowText, QColor("#c4c1bd")); m_dateBarLine->setPalette(pal); lay->addWidget(m_dateBarLine); setHistogramVisibilty(Settings::SettingsData::instance()->showHistogram()); m_browser = new Browser::BrowserWidget(m_stack); m_thumbnailView = new ThumbnailView::ThumbnailFacade(); m_stack->addWidget(m_browser); m_stack->addWidget(m_thumbnailView->gui()); m_stack->setCurrentWidget(m_browser); m_settingsDialog = nullptr; qCInfo(TimingLog) << "MainWindow: Loading MainWindow: " << timer.restart() << "ms."; setupMenuBar(); qCInfo(TimingLog) << "MainWindow: setupMenuBar: " << timer.restart() << "ms."; createSearchBar(); qCInfo(TimingLog) << "MainWindow: createSearchBar: " << timer.restart() << "ms."; setupStatusBar(); qCInfo(TimingLog) << "MainWindow: setupStatusBar: " << timer.restart() << "ms."; // Misc m_autoSaveTimer = new QTimer(this); connect(m_autoSaveTimer, &QTimer::timeout, this, &Window::slotAutoSave); startAutoSaveTimer(); connect(m_browser, &Browser::BrowserWidget::showingOverview, this, &Window::showBrowser); connect(m_browser, SIGNAL(pathChanged(Browser::BreadcrumbList)), m_statusBar->mp_pathIndicator, SLOT(setBreadcrumbs(Browser::BreadcrumbList))); connect(m_statusBar->mp_pathIndicator, SIGNAL(widenToBreadcrumb(Browser::Breadcrumb)), m_browser, SLOT(widenToBreadcrumb(Browser::Breadcrumb))); connect(m_browser, SIGNAL(pathChanged(Browser::BreadcrumbList)), this, SLOT(updateDateBar(Browser::BreadcrumbList))); connect(m_dateBar, &DateBar::DateBarWidget::dateSelected, m_thumbnailView, &ThumbnailView::ThumbnailFacade::gotoDate); connect(m_dateBar, &DateBar::DateBarWidget::toolTipInfo, this, &Window::showDateBarTip); connect(Settings::SettingsData::instance(), SIGNAL(histogramSizeChanged(QSize)), m_dateBar, SLOT(setHistogramBarSize(QSize))); connect(Settings::SettingsData::instance(), SIGNAL(actualThumbnailSizeChanged(int)), this, SLOT(slotThumbnailSizeChanged())); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeChange, this, &Window::setDateRange); connect(m_dateBar, &DateBar::DateBarWidget::dateRangeCleared, this, &Window::clearDateRange); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::currentDateChanged, m_dateBar, &DateBar::DateBarWidget::setDate); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::showImage, this, &Window::showImage); connect(m_thumbnailView, SIGNAL(showSelection()), this, SLOT(slotView())); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::fileIdUnderCursorChanged, this, &Window::slotSetFileName); connect(DB::ImageDB::instance(), SIGNAL(totalChanged(uint)), this, SLOT(updateDateBar())); connect(DB::ImageDB::instance()->categoryCollection(), SIGNAL(categoryCollectionChanged()), this, SLOT(slotOptionGroupChanged())); connect(m_browser, SIGNAL(imageCount(uint)), m_statusBar->mp_partial, SLOT(showBrowserMatches(uint))); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, this, &Window::updateContextMenuFromSelectionSize); checkIfVideoThumbnailerIsInstalled(); executeStartupActions(); qCInfo(TimingLog) << "MainWindow: executeStartupActions " << timer.restart() << "ms."; QTimer::singleShot(0, this, SLOT(delayedInit())); updateContextMenuFromSelectionSize(0); // Automatically save toolbar settings setAutoSaveSettings(); qCInfo(TimingLog) << "MainWindow: misc setup time: " << timer.restart() << "ms."; } MainWindow::Window::~Window() { DB::ImageDB::deleteInstance(); ImageManager::ThumbnailCache::deleteInstance(); Exif::Database::deleteInstance(); } void MainWindow::Window::delayedInit() { QElapsedTimer timer; timer.start(); SplashScreen *splash = SplashScreen::instance(); setupPluginMenu(); qCInfo(TimingLog) << "MainWindow: setupPluginMenu: " << timer.restart() << "ms."; if (Settings::SettingsData::instance()->searchForImagesOnStart() || Options::the()->searchForImagesOnStart()) { splash->message(i18n("Searching for New Files")); qApp->processEvents(); DB::ImageDB::instance()->slotRescan(); qCInfo(TimingLog) << "MainWindow: Search for New Files: " << timer.restart() << "ms."; } if (!Settings::SettingsData::instance()->delayLoadingPlugins()) { splash->message(i18n("Loading Plug-ins")); loadKipiPlugins(); qCInfo(TimingLog) << "MainWindow: Loading Plug-ins: " << timer.restart() << "ms."; } splash->done(); show(); updateDateBar(); qCInfo(TimingLog) << "MainWindow: MainWindow.show():" << timer.restart() << "ms."; QUrl importUrl = Options::the()->importFile(); if (importUrl.isValid()) { // I need to do this in delayed init to get the import window on top of the normal window ImportExport::Import::imageImport(importUrl); qCInfo(TimingLog) << "MainWindow: imageImport:" << timer.restart() << "ms."; } else { // I need to postpone this otherwise the tip dialog will not get focus on start up KTipDialog::showTip(this); } Exif::Database::instance(); // Load the database qCInfo(TimingLog) << "MainWindow: Loading Exif DB:" << timer.restart() << "ms."; if (!Options::the()->listen().isNull()) RemoteControl::RemoteInterface::instance().listen(Options::the()->listen()); else if (Settings::SettingsData::instance()->listenForAndroidDevicesOnStartup()) RemoteControl::RemoteInterface::instance().listen(); } 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")); } } // Flush any remaining thumbnails ImageManager::ThumbnailCache::instance()->save(); doQuit: ImageManager::AsyncLoader::instance()->requestExit(); qApp->quit(); return true; } void MainWindow::Window::slotOptions() { if (!m_settingsDialog) { m_settingsDialog = new Settings::SettingsDialog(this); connect(m_settingsDialog, SIGNAL(changed()), this, SLOT(reloadThumbnails())); connect(m_settingsDialog, &Settings::SettingsDialog::changed, this, &Window::startAutoSaveTimer); connect(m_settingsDialog, &Settings::SettingsDialog::changed, m_browser, &Browser::BrowserWidget::reload); } m_settingsDialog->show(); } void MainWindow::Window::slotCreateImageStack() { const DB::FileNameList list = selected(); if (list.size() < 2) { // it doesn't make sense to make a stack from one image, does it? return; } bool ok = DB::ImageDB::instance()->stack(list); if (!ok) { if (KMessageBox::questionYesNo(this, i18n("Some of the selected images already belong to a stack. " "Do you want to remove them from their stacks and create a " "completely new one?"), i18n("Stacking Error")) == KMessageBox::Yes) { DB::ImageDB::instance()->unstack(list); if (!DB::ImageDB::instance()->stack(list)) { KMessageBox::sorry(this, i18n("Unknown error, stack creation failed."), i18n("Stacking Error")); return; } } else { return; } } DirtyIndicator::markDirty(); // The current item might have just became invisible m_thumbnailView->setCurrentItem(list.at(0)); m_thumbnailView->updateDisplayModel(); } /** @short Make the selected image the head of a stack * * The whole point of image stacking is to group images together and then select * one of them as the "most important". This function is (maybe just a * temporary) way of promoting a selected image to the "head" of a stack it * belongs to. In future, it might get replaced by a Ligtroom-like interface. * */ void MainWindow::Window::slotSetStackHead() { const DB::FileNameList list = selected(); if (list.size() != 1) { // this should be checked by enabling/disabling of QActions return; } setStackHead(*list.begin()); } void MainWindow::Window::setStackHead(const DB::FileName &image) { if (!image.info()->isStacked()) return; unsigned int oldOrder = image.info()->stackOrder(); DB::FileNameList others = DB::ImageDB::instance()->getStackFor(image); Q_FOREACH (const DB::FileName ¤t, others) { if (current == image) { current.info()->setStackOrder(1); } else if (current.info()->stackOrder() < oldOrder) { current.info()->setStackOrder(current.info()->stackOrder() + 1); } } DirtyIndicator::markDirty(); m_thumbnailView->updateDisplayModel(); } void MainWindow::Window::slotUnStackImages() { const DB::FileNameList &list = selected(); if (list.isEmpty()) return; DB::ImageDB::instance()->unstack(list); DirtyIndicator::markDirty(); m_thumbnailView->updateDisplayModel(); } void MainWindow::Window::slotConfigureAllImages() { configureImages(false); } void MainWindow::Window::slotConfigureImagesOneAtATime() { configureImages(true); } void MainWindow::Window::configureImages(bool oneAtATime) { const DB::FileNameList &list = selected(); if (list.isEmpty()) { KMessageBox::sorry(this, i18n("No item is selected."), i18n("No Selection")); } else { DB::ImageInfoList images; Q_FOREACH (const DB::FileName &fileName, list) { images.append(fileName.info()); } configureImages(images, oneAtATime); } } void MainWindow::Window::configureImages(const DB::ImageInfoList &list, bool oneAtATime) { s_instance->configImages(list, oneAtATime); } void MainWindow::Window::configImages(const DB::ImageInfoList &list, bool oneAtATime) { createAnnotationDialog(); if (m_annotationDialog->configure(list, oneAtATime) == QDialog::Rejected) return; reloadThumbnails(ThumbnailView::MaintainSelection); } void MainWindow::Window::slotSearch() { createAnnotationDialog(); DB::ImageSearchInfo searchInfo = m_annotationDialog->search(); if (!searchInfo.isNull()) m_browser->addSearch(searchInfo); } void MainWindow::Window::createAnnotationDialog() { Utilities::ShowBusyCursor dummy; if (!m_annotationDialog.isNull()) return; m_annotationDialog = new AnnotationDialog::Dialog(nullptr); connect(m_annotationDialog.data(), &AnnotationDialog::Dialog::imageRotated, this, &Window::slotImageRotated); } void MainWindow::Window::slotSave() { Utilities::ShowBusyCursor dummy; m_statusBar->showMessage(i18n("Saving..."), 5000); DB::ImageDB::instance()->save(Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("index.xml"), false); ImageManager::ThumbnailCache::instance()->save(); m_statusBar->mp_dirtyIndicator->saved(); QDir().remove(Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1(".#index.xml")); m_statusBar->showMessage(i18n("Saving... Done"), 5000); } void MainWindow::Window::slotDeleteSelected() { if (!m_deleteDialog) m_deleteDialog = new DeleteDialog(this); if (m_deleteDialog->exec(selected()) != QDialog::Accepted) return; DirtyIndicator::markDirty(); } void MainWindow::Window::slotCopySelectedURLs() { QList<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 = 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()); m_clearSelection = KStandardAction::deselect(m_thumbnailView, SLOT(clearSelection()), actionCollection()); m_clearSelection->setEnabled(false); KStandardAction::find(this, SLOT(slotSearch()), actionCollection()); m_deleteSelected = actionCollection()->addAction(QString::fromLatin1("deleteSelected")); m_deleteSelected->setText(i18nc("Delete selected images", "Delete Selected")); m_deleteSelected->setIcon(QIcon::fromTheme(QString::fromLatin1("edit-delete"))); actionCollection()->setDefaultShortcut(m_deleteSelected, Qt::Key_Delete); connect(m_deleteSelected, &QAction::triggered, this, &Window::slotDeleteSelected); a = actionCollection()->addAction(QString::fromLatin1("removeTokens"), this, SLOT(slotRemoveTokens())); a->setText(i18n("Remove Tokens...")); a = actionCollection()->addAction(QString::fromLatin1("showListOfFiles"), this, SLOT(slotShowListOfFiles())); a->setText(i18n("Open List of Files...")); m_configOneAtATime = actionCollection()->addAction(QString::fromLatin1("oneProp"), this, SLOT(slotConfigureImagesOneAtATime())); m_configOneAtATime->setText(i18n("Annotate Individual Items")); actionCollection()->setDefaultShortcut(m_configOneAtATime, Qt::CTRL + Qt::Key_1); m_configAllSimultaniously = actionCollection()->addAction(QString::fromLatin1("allProp"), this, SLOT(slotConfigureAllImages())); m_configAllSimultaniously->setText(i18n("Annotate Multiple Items at a Time")); actionCollection()->setDefaultShortcut(m_configAllSimultaniously, Qt::CTRL + Qt::Key_2); m_createImageStack = actionCollection()->addAction(QString::fromLatin1("createImageStack"), this, SLOT(slotCreateImageStack())); m_createImageStack->setText(i18n("Merge Images into a Stack")); actionCollection()->setDefaultShortcut(m_createImageStack, Qt::CTRL + Qt::Key_3); m_unStackImages = actionCollection()->addAction(QString::fromLatin1("unStackImages"), this, SLOT(slotUnStackImages())); m_unStackImages->setText(i18n("Remove Images from Stack")); m_setStackHead = actionCollection()->addAction(QString::fromLatin1("setStackHead"), this, SLOT(slotSetStackHead())); m_setStackHead->setText(i18n("Set as First Image in Stack")); actionCollection()->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4); m_rotLeft = actionCollection()->addAction(QString::fromLatin1("rotateLeft"), this, SLOT(slotRotateSelectedLeft())); m_rotLeft->setText(i18n("Rotate counterclockwise")); actionCollection()->setDefaultShortcut(m_rotLeft, Qt::Key_7); m_rotRight = actionCollection()->addAction(QString::fromLatin1("rotateRight"), this, SLOT(slotRotateSelectedRight())); m_rotRight->setText(i18n("Rotate clockwise")); actionCollection()->setDefaultShortcut(m_rotRight, Qt::Key_9); // The Images menu m_view = actionCollection()->addAction(QString::fromLatin1("viewImages"), this, SLOT(slotView())); m_view->setText(i18n("View")); actionCollection()->setDefaultShortcut(m_view, Qt::CTRL + Qt::Key_I); m_viewInNewWindow = actionCollection()->addAction(QString::fromLatin1("viewImagesNewWindow"), this, SLOT(slotViewNewWindow())); m_viewInNewWindow->setText(i18n("View (In New Window)")); m_runSlideShow = actionCollection()->addAction(QString::fromLatin1("runSlideShow"), this, SLOT(slotRunSlideShow())); m_runSlideShow->setText(i18n("Run Slide Show")); m_runSlideShow->setIcon(QIcon::fromTheme(QString::fromLatin1("view-presentation"))); actionCollection()->setDefaultShortcut(m_runSlideShow, Qt::CTRL + Qt::Key_R); m_runRandomSlideShow = actionCollection()->addAction(QString::fromLatin1("runRandomizedSlideShow"), this, SLOT(slotRunRandomizedSlideShow())); m_runRandomSlideShow->setText(i18n("Run Randomized Slide Show")); a = actionCollection()->addAction(QString::fromLatin1("collapseAllStacks"), m_thumbnailView, SLOT(collapseAllStacks())); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::collapseAllStacksEnabled, a, &QAction::setEnabled); a->setEnabled(false); a->setText(i18n("Collapse all stacks")); a = actionCollection()->addAction(QString::fromLatin1("expandAllStacks"), m_thumbnailView, SLOT(expandAllStacks())); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::expandAllStacksEnabled, a, &QAction::setEnabled); a->setEnabled(false); a->setText(i18n("Expand all stacks")); QActionGroup *grp = new QActionGroup(this); a = actionCollection()->add<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->setIcon(QIcon::fromTheme(QString::fromLatin1("document-import"))); a->setText(i18n("Rescan for Images and Videos")); QAction *recreateExif = actionCollection()->addAction(QString::fromLatin1("recreateExifDB"), this, SLOT(slotRecreateExifDB())); recreateExif->setText(i18n("Recreate Exif Search Database")); QAction *rereadExif = actionCollection()->addAction(QString::fromLatin1("reReadExifInfo"), this, SLOT(slotReReadExifInfo())); rereadExif->setText(i18n("Read Exif Info from Files...")); m_sortAllByDateAndTime = actionCollection()->addAction(QString::fromLatin1("sortAllImages"), this, SLOT(slotSortAllByDateAndTime())); m_sortAllByDateAndTime->setText(i18n("Sort All by Date && Time")); m_sortAllByDateAndTime->setEnabled(true); m_AutoStackImages = actionCollection()->addAction(QString::fromLatin1("autoStack"), this, SLOT(slotAutoStackImages())); m_AutoStackImages->setText(i18n("Automatically Stack Selected Images...")); a = actionCollection()->addAction(QString::fromLatin1("buildThumbs"), this, SLOT(slotBuildThumbnails())); a->setText(i18n("Build Thumbnails")); a = 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); ImageManager::ThumbnailCache::instance()->save(); m_statusBar->showMessage(i18n("Auto saving.... Done"), 5000); m_statusBar->mp_dirtyIndicator->autoSaved(); } } void MainWindow::Window::showThumbNails() { m_statusBar->showThumbnailSlider(); reloadThumbnails(ThumbnailView::ClearSelection); m_stack->setCurrentWidget(m_thumbnailView->gui()); m_thumbnailView->gui()->setFocus(); updateStates(true); } void MainWindow::Window::showBrowser() { m_statusBar->clearMessage(); m_statusBar->hideThumbnailSlider(); m_stack->setCurrentWidget(m_browser); m_browser->setFocus(); updateContextMenuFromSelectionSize(0); updateStates(false); } void MainWindow::Window::slotOptionGroupChanged() { // FIXME: What if annotation dialog is open? (if that's possible) delete m_annotationDialog; m_annotationDialog = nullptr; DirtyIndicator::markDirty(); } void MainWindow::Window::showTipOfDay() { KTipDialog::showTip(this, QString(), true); } void MainWindow::Window::runDemo() { KProcess *process = new KProcess; *process << qApp->applicationFilePath() << 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, *this); // 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; if (info) selectedFile = QUrl::fromLocalFile(info->fileName().absolute()); QList<QUrl> allSelectedFiles; for (const QString &selectedPath : selected().toStringList(DB::AbsolutePath)) { allSelectedFiles << QUrl::fromLocalFile(selectedPath); } // "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, bool recount) { m_statusBar->setLocked(locked); Settings::SettingsData::instance()->setLocked(locked, force); m_lock->setEnabled(!locked); m_unlock->setEnabled(locked); m_setDefaultPos->setEnabled(!locked); m_setDefaultNeg->setEnabled(!locked); if (recount) m_browser->reload(); } void MainWindow::Window::changePassword() { bool OK = (Settings::SettingsData::instance()->password().isEmpty()); QPointer<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 loadKipiPlugins(); Q_FOREACH (const KIPI::PluginLoader::Info *pluginInfo, m_pluginLoader->pluginList()) { KIPI::Plugin *plugin = pluginInfo->plugin(); if (plugin) dialog->addCollection(plugin->actionCollection(), i18nc("Add 'Plugin' prefix so that KIPI plugins are obvious in KShortcutsDialog…", "Plugin: %1", pluginInfo->name())); } #endif createAnnotationDialog(); dialog->addCollection(m_annotationDialog->actions(), i18n("Annotation Dialog")); dialog->configure(); delete dialog; delete viewer; } void MainWindow::Window::slotSetFileName(const DB::FileName &fileName) { ImageInfoPtr info; if (fileName.isNull()) m_statusBar->clearMessage(); else { info = fileName.info(); if (info != ImageInfoPtr(nullptr)) m_statusBar->showMessage(fileName.absolute(), 4000); } } void MainWindow::Window::updateContextMenuFromSelectionSize(int selectionSize) { m_configAllSimultaniously->setEnabled(selectionSize > 1); m_configOneAtATime->setEnabled(selectionSize >= 1); m_createImageStack->setEnabled(selectionSize > 1); m_unStackImages->setEnabled(selectionSize >= 1); m_setStackHead->setEnabled(selectionSize == 1); // FIXME: do we want to check if it's stacked here? m_sortByDateAndTime->setEnabled(selectionSize > 1); m_recreateThumbnails->setEnabled(selectionSize >= 1); m_rotLeft->setEnabled(selectionSize >= 1); m_rotRight->setEnabled(selectionSize >= 1); m_AutoStackImages->setEnabled(selectionSize > 1); m_markUntagged->setEnabled(selectionSize >= 1); m_statusBar->mp_selected->setSelectionCount(selectionSize); m_clearSelection->setEnabled(selectionSize > 0); } void MainWindow::Window::rotateSelected(int angle) { const DB::FileNameList list = selected(); if (list.isEmpty()) { KMessageBox::sorry(this, i18n("No item is selected."), i18n("No Selection")); } else { Q_FOREACH (const DB::FileName &fileName, list) { fileName.info()->rotate(angle); ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); } m_statusBar->mp_dirtyIndicator->markDirty(); } } void MainWindow::Window::slotRotateSelectedLeft() { rotateSelected(-90); reloadThumbnails(); } void MainWindow::Window::slotRotateSelectedRight() { rotateSelected(90); reloadThumbnails(); } void MainWindow::Window::reloadThumbnails(ThumbnailView::SelectionUpdateMethod method) { m_thumbnailView->reload(method); updateContextMenuFromSelectionSize(m_thumbnailView->selection().size()); } void MainWindow::Window::slotUpdateViewMenu(DB::Category::ViewType type) { if (type == DB::Category::TreeView) m_smallListView->setChecked(true); else if (type == DB::Category::ThumbedTreeView) m_largeListView->setChecked(true); else if (type == DB::Category::ThumbedIconView) m_largeIconView->setChecked(true); } void MainWindow::Window::slotShowNotOnDisk() { DB::FileNameList notOnDisk; Q_FOREACH (const DB::FileName &fileName, DB::ImageDB::instance()->images()) { if (!fileName.exists()) notOnDisk.append(fileName); } showThumbNails(notOnDisk); } void MainWindow::Window::slotShowImagesWithChangedMD5Sum() { #ifdef DOES_STILL_NOT_WORK_IN_KPA4 Utilities::ShowBusyCursor dummy; StringSet changed = DB::ImageDB::instance()->imagesWithMD5Changed(); showThumbNails(changed.toList()); #else // DOES_STILL_NOT_WORK_IN_KPA4 qFatal("Code commented out in MainWindow::Window::slotShowImagesWithChangedMD5Sum"); #endif // DOES_STILL_NOT_WORK_IN_KPA4 } void MainWindow::Window::updateStates(bool thumbNailView) { m_selectAll->setEnabled(thumbNailView); m_deleteSelected->setEnabled(thumbNailView); m_limitToMarked->setEnabled(thumbNailView); m_jumpToContext->setEnabled(thumbNailView); } void MainWindow::Window::slotRunSlideShow() { slotView(true, true); } void MainWindow::Window::slotRunRandomizedSlideShow() { slotView(true, true, true); } MainWindow::Window *MainWindow::Window::theMainWindow() { Q_ASSERT(s_instance); return s_instance; } void MainWindow::Window::slotConfigureToolbars() { QPointer<KEditToolBar> dlg = new KEditToolBar(guiFactory()); connect(dlg, SIGNAL(newToolbarConfig()), SLOT(slotNewToolbarConfig())); dlg->exec(); delete dlg; } void MainWindow::Window::slotNewToolbarConfig() { createGUI(); createSearchBar(); } void MainWindow::Window::slotImport() { ImportExport::Import::imageImport(); } void MainWindow::Window::slotExport() { ImportExport::Export::imageExport(selectedOnDisk()); } void MainWindow::Window::slotReenableMessages() { int ret = KMessageBox::questionYesNo(this, i18n("<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_hasLoadedKipiPlugins = true; return; // This is no good, but lets try and continue. } #ifdef KF5Purpose_FOUND Plugins::PurposeMenu *purposeMenu = new Plugins::PurposeMenu(menu); connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, purposeMenu, &Plugins::PurposeMenu::slotSelectionChanged); connect(purposeMenu, &Plugins::PurposeMenu::imageShared, [this](QUrl shareLocation) { QString message; if (shareLocation.isValid()) { message = i18n("Successfully shared image(s). Copying location to clipboard..."); QGuiApplication::clipboard()->setText(shareLocation.toString()); } else { message = i18n("Successfully shared image(s)."); } m_statusBar->showMessage(message); }); connect(purposeMenu, &Plugins::PurposeMenu::imageSharingFailed, [this](QString errorMessage) { QString message = i18n("Image sharing failed with message: %1", errorMessage); m_statusBar->showMessage(message); }); #endif #ifdef HASKIPI connect(menu, &QMenu::aboutToShow, this, &Window::loadKipiPlugins); m_hasLoadedKipiPlugins = false; #else #ifndef KF5Purpose_FOUND menu->setEnabled(false); #endif m_hasLoadedKipiPlugins = true; #endif } void MainWindow::Window::loadKipiPlugins() { #ifdef HASKIPI Utilities::ShowBusyCursor dummy; if (m_hasLoadedKipiPlugins) 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_hasLoadedKipiPlugins = 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 { qCWarning(MainWindowLog) << "Unknown category\n"; } } KConfigGroup group = KSharedConfig::openConfig()->group(QString::fromLatin1("Shortcuts")); plugin->actionCollection()->importGlobalShortcuts(&group); } setPluginMenuState("importplugin", importActions); setPluginMenuState("exportplugin", exportActions); setPluginMenuState("imagesplugins", imageActions); setPluginMenuState("batch_plugins", batchActions); setPluginMenuState("tool_plugins", toolsActions); // For this to work I need to pass false as second arg for createGUI plugActionList(QString::fromLatin1("import_actions"), importActions); plugActionList(QString::fromLatin1("export_actions"), exportActions); plugActionList(QString::fromLatin1("image_actions"), imageActions); plugActionList(QString::fromLatin1("tool_actions"), toolsActions); plugActionList(QString::fromLatin1("batch_actions"), batchActions); #endif } void MainWindow::Window::setPluginMenuState(const char *name, const QList<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 = 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() { QDesktopServices::openUrl(QUrl( QStringLiteral("http://www.kphotoalbum.org/documentation/videos/"))); } 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, false); connect(m_statusBar, &StatusBar::thumbnailSettingsRequested, [this]() { this->slotOptions(); m_settingsDialog->activatePage(Settings::SettingsPage::ThumbnailsPage); }); } 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::createSearchBar() { // Set up the search tool bar SearchBar *bar = new SearchBar(this); bar->setLineEditEnabled(false); bar->setObjectName(QString::fromUtf8("searchBar")); connect(bar, &SearchBar::textChanged, m_browser, &Browser::BrowserWidget::slotLimitToMatch); connect(bar, &SearchBar::returnPressed, m_browser, &Browser::BrowserWidget::slotInvokeSeleted); connect(bar, &SearchBar::keyPressed, m_browser, &Browser::BrowserWidget::scrollKeyPressed); connect(m_browser, &Browser::BrowserWidget::viewChanged, bar, &SearchBar::reset); connect(m_browser, &Browser::BrowserWidget::isSearchable, bar, &SearchBar::setLineEditEnabled); ThumbnailView::FilterWidget *filter = m_thumbnailView->createFilterWidget(this); filter->setObjectName(QString::fromUtf8("filterBar")); connect(m_browser, &Browser::BrowserWidget::viewChanged, ThumbnailView::ThumbnailFacade::instance(), &ThumbnailView::ThumbnailFacade::clearFilter); connect(m_browser, &Browser::BrowserWidget::isFilterable, filter, &ThumbnailView::FilterWidget::setEnabled); } 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::checkIfVideoThumbnailerIsInstalled() { if (Options::the()->demoMode()) return; if (!FeatureDialog::hasVideoThumbnailer()) { KMessageBox::information(this, i18n("<p>Unable to find ffmpeg on the system.</p>" "<p>Without it, KPhotoAlbum will not be able to display video thumbnails and video lengths. " "Please install the ffmpeg package</p>"), i18n("Video thumbnails are not available"), QString::fromLatin1("VideoThumbnailerNotInstalled")); } } bool MainWindow::Window::anyVideosSelected() const { Q_FOREACH (const DB::FileName &fileName, selected()) { if (Utilities::isVideo(fileName)) return true; } return false; } 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 UserFeedback MainWindow::Window::askWarningContinueCancel(const QString &msg, const QString &title, const QString &dialogId) { auto answer = KMessageBox::warningContinueCancel(this, msg, title, KStandardGuiItem::cont(), KStandardGuiItem::cancel(), dialogId); return (answer == KMessageBox::Continue) ? UserFeedback::Confirm : UserFeedback::Deny; } UserFeedback MainWindow::Window::askQuestionYesNo(const QString &msg, const QString &title, const QString &dialogId) { auto answer = KMessageBox::questionYesNo(this, msg, title, KStandardGuiItem::yes(), KStandardGuiItem::no(), dialogId); return (answer == KMessageBox::Yes) ? UserFeedback::Confirm : UserFeedback::Deny; } void MainWindow::Window::showInformation(const QString &msg, const QString &title, const QString &dialogId) { KMessageBox::information(this, msg, title, dialogId); } void MainWindow::Window::showSorry(const QString &msg, const QString &title, const QString &) { KMessageBox::sorry(this, msg, title); } void MainWindow::Window::showError(const QString &msg, const QString &title, const QString &) { KMessageBox::error(this, msg, title); } bool MainWindow::Window::isDialogDisabled(const QString &dialogId) { // Note(jzarl): there are different methods for different kinds of dialogs. // However, all these methods share exactly the same code in KMessageBox. // If that ever changes, we can still update our implementation - until then I won't just copy a stupid API... return !KMessageBox::shouldBeShownContinue(dialogId); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/MainWindow/Window.h b/MainWindow/Window.h index dc073651..d628ad4b 100644 --- a/MainWindow/Window.h +++ b/MainWindow/Window.h @@ -1,310 +1,308 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MAINWINDOW_WINDOW_H #define MAINWINDOW_WINDOW_H -#include <config-kpa-kipi.h> - -#include <QList> -#include <QPointer> -#include <QUrl> - -#include <KXmlGuiWindow> - #include <DB/Category.h> #include <DB/FileNameList.h> #include <DB/ImageSearchInfo.h> #include <DB/UIDelegate.h> #include <ThumbnailView/enums.h> + +#include <KXmlGuiWindow> +#include <QList> +#include <QPointer> +#include <QUrl> +#include <config-kpa-kipi.h> #ifdef HAVE_KGEOMAP #include <Browser/PositionBrowserWidget.h> #endif class QAction; class QCloseEvent; class QContextMenuEvent; class QFrame; class QLabel; class QMoveEvent; class QResizeEvent; class QStackedWidget; class QTimer; class KActionMenu; class KTipDialog; class KToggleAction; #ifdef HASKIPI namespace KIPI { class PluginLoader; } #endif namespace AnnotationDialog { class Dialog; } namespace Browser { class BrowserWidget; class BreadcrumbList; } namespace DateBar { class DateBarWidget; } namespace DB { class ImageInfoList; } namespace HTMLGenerator { class HTMLDialog; } namespace Plugins { class Interface; } namespace Settings { class SettingsDialog; } namespace ThumbnailView { class ThumbnailFacade; } class BreadcrumbViewer; namespace MainWindow { class DeleteDialog; class StatusBar; class TokenEditor; class Window : public KXmlGuiWindow, public DB::UIDelegate { Q_OBJECT public: explicit Window(QWidget *parent); ~Window() override; static void configureImages(const DB::ImageInfoList &list, bool oneAtATime); static Window *theMainWindow(); DB::FileNameList selected(ThumbnailView::SelectionMode mode = ThumbnailView::ExpandCollapsedStacks) const; DB::ImageSearchInfo currentContext(); QString currentBrowseCategory() const; void setStackHead(const DB::FileName &image); void setHistogramVisibilty(bool visible) const; bool dbIsDirty() const; #ifdef HAVE_KGEOMAP void showPositionBrowser(); Browser::PositionBrowserWidget *positionBrowserWidget(); #endif // implement UI delegate interface // Note(jzarl): we just could create a UIDelegate class that takes a QWidget, // implementing the same messageParent approach that we took before. // For now, I don't see anything wrong with directly implementing the interface instead. // I may change my mind later and I'm ready to convinced of the errors of my way, though... DB::UserFeedback askWarningContinueCancel(const QString &msg, const QString &title, const QString &dialogId) override; DB::UserFeedback askQuestionYesNo(const QString &msg, const QString &title, const QString &dialogId) override; void showInformation(const QString &msg, const QString &title, const QString &dialogId) override; void showSorry(const QString &msg, const QString &title, const QString &) override; void showError(const QString &msg, const QString &title, const QString &) override; bool isDialogDisabled(const QString &dialogId) override; public slots: void showThumbNails(const DB::FileNameList &items); void loadKipiPlugins(); void reloadThumbnails(ThumbnailView::SelectionUpdateMethod method = ThumbnailView::MaintainSelection); void runDemo(); void slotImageRotated(const DB::FileName &fileName); void slotSave(); protected slots: void showThumbNails(); bool slotExit(); void slotOptions(); void slotConfigureAllImages(); void slotConfigureImagesOneAtATime(); void slotCreateImageStack(); void slotUnStackImages(); void slotSetStackHead(); void slotCopySelectedURLs(); void slotPasteInformation(); void slotDeleteSelected(); void slotReReadExifInfo(); void slotAutoStackImages(); void slotSearch(); void slotView(bool reuse = true, bool slideShow = false, bool random = false); void slotViewNewWindow(); void slotSortByDateAndTime(); void slotSortAllByDateAndTime(); void slotLimitToSelected(); void slotExportToHTML(); void slotAutoSave(); void showBrowser(); void slotOptionGroupChanged(); void showTipOfDay(); void lockToDefaultScope(); void setDefaultScopePositive(); void setDefaultScopeNegative(); void unlockFromDefaultScope(); void changePassword(); void slotConfigureKeyBindings(); void slotSetFileName(const DB::FileName &); void updateContextMenuFromSelectionSize(int selectionSize); void slotUpdateViewMenu(DB::Category::ViewType); void slotShowNotOnDisk(); void slotBuildThumbnails(); void slotBuildThumbnailsIfWanted(); void slotRunSlideShow(); void slotRunRandomizedSlideShow(); void slotConfigureToolbars(); void slotNewToolbarConfig(); void slotImport(); void slotExport(); void delayedInit(); void slotReenableMessages(); void slotImagesChanged(const QList<QUrl> &); void slotSelectionChanged(int count); void plug(); void slotRemoveTokens(); void slotShowListOfFiles(); void updateDateBar(const Browser::BreadcrumbList &); void updateDateBar(); void slotShowImagesWithInvalidDate(); void slotShowImagesWithChangedMD5Sum(); void showDateBarTip(const QString &); void slotJumpToContext(); void setDateRange(const DB::ImageDate &); void clearDateRange(); void startAutoSaveTimer(); void slotRecalcCheckSums(); void slotShowExifInfo(); void showFeatures(); void showImage(const DB::FileName &fileName); void slotOrderIncr(); void slotOrderDecr(); void slotRotateSelectedLeft(); void slotRotateSelectedRight(); void rotateSelected(int angle); void showVideos(); void slotStatistics(); void slotRecreateExifDB(); void useNextVideoThumbnail(); void usePreviousVideoThumbnail(); void mergeDuplicates(); void slotThumbnailSizeChanged(); void slotMarkUntagged(); protected: void configureImages(bool oneAtATime); QString welcome(); void closeEvent(QCloseEvent *e) override; void resizeEvent(QResizeEvent *) override; void moveEvent(QMoveEvent *) override; void setupMenuBar(); void createAnnotationDialog(); bool load(); void contextMenuEvent(QContextMenuEvent *e) override; void setLocked(bool b, bool force, bool recount = true); void configImages(const DB::ImageInfoList &list, bool oneAtATime); void updateStates(bool thumbNailView); DB::FileNameList selectedOnDisk(); void setupPluginMenu(); void launchViewer(const DB::FileNameList &mediaList, bool reuse, bool slideShow, bool random); void setupStatusBar(); void setPluginMenuState(const char *name, const QList<QAction *> &actions); void createSearchBar(); void executeStartupActions(); void checkIfVideoThumbnailerIsInstalled(); bool anyVideosSelected() const; #ifdef HAVE_KGEOMAP Browser::PositionBrowserWidget *createPositionBrowser(); #endif private: static Window *s_instance; ThumbnailView::ThumbnailFacade *m_thumbnailView; Settings::SettingsDialog *m_settingsDialog; QPointer<AnnotationDialog::Dialog> m_annotationDialog; QStackedWidget *m_stack; QTimer *m_autoSaveTimer; Browser::BrowserWidget *m_browser; DeleteDialog *m_deleteDialog; QAction *m_lock; QAction *m_unlock; QAction *m_setDefaultPos; QAction *m_setDefaultNeg; QAction *m_jumpToContext; HTMLGenerator::HTMLDialog *m_htmlDialog; QAction *m_configOneAtATime; QAction *m_configAllSimultaniously; QAction *m_createImageStack; QAction *m_unStackImages; QAction *m_setStackHead; QAction *m_view; QAction *m_rotLeft; QAction *m_rotRight; QAction *m_sortByDateAndTime; QAction *m_sortAllByDateAndTime; QAction *m_AutoStackImages; QAction *m_viewInNewWindow; KActionMenu *m_viewMenu; KToggleAction *m_smallListView; KToggleAction *m_largeListView; KToggleAction *m_largeIconView; QAction *m_generateHtml; QAction *m_copy; QAction *m_paste; QAction *m_deleteSelected; QAction *m_limitToMarked; QAction *m_selectAll; QAction *m_clearSelection; QAction *m_runSlideShow; QAction *m_runRandomSlideShow; Plugins::Interface *m_pluginInterface; QAction *m_showExifDialog; #ifdef HASKIPI KIPI::PluginLoader *m_pluginLoader; #endif QAction *m_recreateThumbnails; QAction *m_useNextVideoThumbnail; QAction *m_usePreviousVideoThumbnail; QAction *m_markUntagged; TokenEditor *m_tokenEditor; DateBar::DateBarWidget *m_dateBar; QFrame *m_dateBarLine; bool m_hasLoadedKipiPlugins; QMap<Qt::Key, QPair<QString, QString>> m_viewerInputMacros; MainWindow::StatusBar *m_statusBar; QString m_lastTarget; #ifdef HAVE_KGEOMAP Browser::PositionBrowserWidget *m_positionBrowser; #endif }; } #endif /* MAINWINDOW_WINDOW_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Map/MapMarkerModelHelper.cpp b/Map/MapMarkerModelHelper.cpp index db24dcf9..4696083d 100644 --- a/Map/MapMarkerModelHelper.cpp +++ b/Map/MapMarkerModelHelper.cpp @@ -1,130 +1,131 @@ /* Copyright (C) 2014 Johannes Zarl <johannes@zarl.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. */ #include "MapMarkerModelHelper.h" + #include "Logging.h" // Qt includes #include <QItemSelectionModel> #include <QStandardItem> #include <QStandardItemModel> // Local includes #include <ImageManager/ThumbnailCache.h> const int FileNameRole = Qt::UserRole + 1; Map::MapMarkerModelHelper::MapMarkerModelHelper() : m_itemModel(0) , m_itemSelectionModel(0) { m_itemModel = new QStandardItemModel(1, 1); m_itemSelectionModel = new QItemSelectionModel(m_itemModel); connect(m_itemModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(slotDataChanged(QModelIndex, QModelIndex))); } Map::MapMarkerModelHelper::~MapMarkerModelHelper() { delete m_itemSelectionModel; delete m_itemModel; } void Map::MapMarkerModelHelper::clearItems() { m_itemModel->clear(); } void Map::MapMarkerModelHelper::addImage(const DB::ImageInfo &image) { qCDebug(MapLog) << "Adding marker for image " << image.label(); QStandardItem *const newItem = new QStandardItem(image.label()); newItem->setToolTip(image.label()); newItem->setData(QVariant::fromValue(image.fileName()), FileNameRole); m_itemModel->appendRow(newItem); } void Map::MapMarkerModelHelper::addImage(const DB::ImageInfoPtr image) { addImage(*image); } void Map::MapMarkerModelHelper::slotDataChanged(const QModelIndex &, const QModelIndex &) { emit(signalModelChangedDrastically()); } bool Map::MapMarkerModelHelper::itemCoordinates(const QModelIndex &index, KGeoMap::GeoCoordinates *const coordinates) const { if (!index.data(FileNameRole).canConvert<DB::FileName>()) { return false; } if (coordinates) { const DB::FileName filename = index.data(FileNameRole).value<DB::FileName>(); *coordinates = filename.info()->coordinates(); } return true; } QAbstractItemModel *Map::MapMarkerModelHelper::model() const { return m_itemModel; } QItemSelectionModel *Map::MapMarkerModelHelper::selectionModel() const { return m_itemSelectionModel; } KGeoMap::ModelHelper::Flags Map::MapMarkerModelHelper::modelFlags() const { return FlagVisible; } KGeoMap::ModelHelper::Flags Map::MapMarkerModelHelper::itemFlags(const QModelIndex &index) const { if (!index.data(FileNameRole).canConvert<DB::FileName>()) { return FlagNull; } return FlagVisible; } // FIXME: for some reason, itemIcon is never called -> no thumbnails bool Map::MapMarkerModelHelper::itemIcon(const QModelIndex &index, QPoint *const offset, QSize *const, QPixmap *const pixmap, QUrl *const) const { if (!index.data(FileNameRole).canConvert<DB::FileName>()) { return false; } const DB::FileName filename = index.data(FileNameRole).value<DB::FileName>(); *pixmap = ImageManager::ThumbnailCache::instance()->lookup(filename); *offset = QPoint(pixmap->width() / 2, pixmap->height() / 2); qCDebug(MapLog) << "Map icon for " << filename.relative() << (pixmap->isNull() ? " missing." : " found."); return !pixmap->isNull(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Map/MapView.cpp b/Map/MapView.cpp index 7e92d43a..b0ac2e07 100644 --- a/Map/MapView.cpp +++ b/Map/MapView.cpp @@ -1,238 +1,239 @@ /* Copyright (C) 2014-2015 Tobias Leupold <tobias.leupold@web.de> 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 "MapView.h" + #include "Logging.h" // Qt includes #include <QLabel> #include <QLoggingCategory> #include <QPixmap> #include <QPushButton> #include <QVBoxLayout> // KDE includes #include <KConfigGroup> #include <KIconLoader> #include <KLocalizedString> #include <KMessageBox> #include <KSharedConfig> // Libkgeomap includes #include <KGeoMap/MapWidget> // Local includes #include "MapMarkerModelHelper.h" #include "SearchMarkerTiler.h" Map::MapView::MapView(QWidget *parent, UsageType type) : QWidget(parent) { if (type == MapViewWindow) { setWindowFlags(Qt::Window); setAttribute(Qt::WA_DeleteOnClose); } QVBoxLayout *layout = new QVBoxLayout(this); m_statusLabel = new QLabel; m_statusLabel->setAlignment(Qt::AlignCenter); m_statusLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); m_statusLabel->hide(); layout->addWidget(m_statusLabel); m_mapWidget = new KGeoMap::MapWidget(this); layout->addWidget(m_mapWidget); QWidget *controlWidget = m_mapWidget->getControlWidget(); controlWidget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); layout->addWidget(controlWidget); m_mapWidget->setActive(true); QPushButton *saveButton = new QPushButton; saveButton->setIcon(QPixmap(SmallIcon(QString::fromUtf8("media-floppy")))); saveButton->setToolTip(i18n("Save the current map settings")); m_mapWidget->addWidgetToControlWidget(saveButton); connect(saveButton, &QPushButton::clicked, this, &MapView::saveSettings); m_setLastCenterButton = new QPushButton; m_setLastCenterButton->setIcon(QPixmap(SmallIcon(QString::fromUtf8("go-first")))); m_setLastCenterButton->setToolTip(i18n("Go to last map position")); m_mapWidget->addWidgetToControlWidget(m_setLastCenterButton); connect(m_setLastCenterButton, &QPushButton::clicked, this, &MapView::setLastCenter); // We first try set the default backend "marble" or the first one available ... const QString defaultBackend = QString::fromUtf8("marble"); auto backends = m_mapWidget->availableBackends(); if (backends.contains(defaultBackend)) { m_mapWidget->setBackend(defaultBackend); } else { qCDebug(MapLog) << "AnnotationMap: using backend " << backends[0]; m_mapWidget->setBackend(backends[0]); } // ... then we try to set the (probably) saved settings KConfigGroup configGroup = KSharedConfig::openConfig()->group(QString::fromUtf8("MapView")); m_mapWidget->readSettingsFromGroup(&configGroup); // Add the item model for the coordinates display m_modelHelper = new MapMarkerModelHelper(); m_itemMarkerTiler = new SearchMarkerTiler(m_modelHelper, this); m_mapWidget->setGroupedModel(m_itemMarkerTiler); connect(m_mapWidget, &KGeoMap::MapWidget::signalRegionSelectionChanged, this, &MapView::signalRegionSelectionChanged); } Map::MapView::~MapView() { delete m_modelHelper; delete m_itemMarkerTiler; } void Map::MapView::clear() { m_modelHelper->clearItems(); } void Map::MapView::addImage(const DB::ImageInfo &image) { m_modelHelper->addImage(image); } void Map::MapView::addImage(const DB::ImageInfoPtr image) { m_modelHelper->addImage(image); } void Map::MapView::zoomToMarkers() { if (m_modelHelper->model()->rowCount() > 0) { m_mapWidget->adjustBoundariesToGroupedMarkers(); } m_lastCenter = m_mapWidget->getCenter(); } void Map::MapView::setCenter(const DB::ImageInfo &image) { m_lastCenter = image.coordinates(); m_mapWidget->setCenter(m_lastCenter); } void Map::MapView::setCenter(const DB::ImageInfoPtr image) { m_lastCenter = image->coordinates(); m_mapWidget->setCenter(m_lastCenter); } void Map::MapView::saveSettings() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup configGroup = config->group(QString::fromUtf8("MapView")); m_mapWidget->saveSettingsToGroup(&configGroup); config->sync(); KMessageBox::information(this, i18n("Settings saved"), i18n("Map view")); } void Map::MapView::setShowThumbnails(bool state) { m_mapWidget->setShowThumbnails(state); } void Map::MapView::displayStatus(MapStatus status) { switch (status) { case MapStatus::Loading: m_statusLabel->setText(i18n("<i>Loading coordinates from the images ...</i>")); m_statusLabel->show(); m_mapWidget->hide(); m_mapWidget->clearRegionSelection(); m_setLastCenterButton->setEnabled(false); break; case MapStatus::ImageHasCoordinates: m_statusLabel->hide(); m_mapWidget->setAvailableMouseModes(KGeoMap::MouseModePan); m_mapWidget->setVisibleMouseModes(0); m_mapWidget->setMouseMode(KGeoMap::MouseModePan); m_mapWidget->clearRegionSelection(); m_mapWidget->show(); m_setLastCenterButton->show(); m_setLastCenterButton->setEnabled(true); break; case MapStatus::ImageHasNoCoordinates: m_statusLabel->setText(i18n("<i>This image does not contain geographic coordinates.</i>")); m_statusLabel->show(); m_mapWidget->hide(); m_setLastCenterButton->show(); m_setLastCenterButton->setEnabled(false); break; case MapStatus::SomeImagesHaveNoCoordinates: m_statusLabel->setText(i18n("<i>Some of the selected images do not contain geographic " "coordinates.</i>")); m_statusLabel->show(); m_mapWidget->setAvailableMouseModes(KGeoMap::MouseModePan); m_mapWidget->setVisibleMouseModes(0); m_mapWidget->setMouseMode(KGeoMap::MouseModePan); m_mapWidget->clearRegionSelection(); m_mapWidget->show(); m_setLastCenterButton->show(); m_setLastCenterButton->setEnabled(true); break; case MapStatus::SearchCoordinates: m_statusLabel->setText(i18n("<i>Search for geographic coordinates.</i>")); m_statusLabel->show(); m_mapWidget->setAvailableMouseModes(KGeoMap::MouseModePan | KGeoMap::MouseModeRegionSelectionFromIcon | KGeoMap::MouseModeRegionSelection); m_mapWidget->setVisibleMouseModes(KGeoMap::MouseModePan | KGeoMap::MouseModeRegionSelectionFromIcon | KGeoMap::MouseModeRegionSelection); m_mapWidget->setMouseMode(KGeoMap::MouseModeRegionSelectionFromIcon); m_mapWidget->show(); m_mapWidget->setCenter(KGeoMap::GeoCoordinates()); m_setLastCenterButton->hide(); break; case MapStatus::NoImagesHaveNoCoordinates: m_statusLabel->setText(i18n("<i>None of the selected images contain geographic " "coordinates.</i>")); m_statusLabel->show(); m_mapWidget->hide(); m_setLastCenterButton->show(); m_setLastCenterButton->setEnabled(false); break; } emit displayStatusChanged(status); } void Map::MapView::setLastCenter() { m_mapWidget->setCenter(m_lastCenter); } KGeoMap::GeoCoordinates::Pair Map::MapView::getRegionSelection() const { return m_mapWidget->getRegionSelection(); } bool Map::MapView::regionSelected() const { return m_mapWidget->getRegionSelection().first.hasCoordinates() && m_mapWidget->getRegionSelection().second.hasCoordinates(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Map/SearchMarkerTiler.cpp b/Map/SearchMarkerTiler.cpp index 44639e5e..960f2a03 100644 --- a/Map/SearchMarkerTiler.cpp +++ b/Map/SearchMarkerTiler.cpp @@ -1,44 +1,44 @@ /* Copyright (C) 2015 Johannes Zarl-Zierl <johannes@zarl.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. */ #include "SearchMarkerTiler.h" // libkgeomap includes // Local includes -#include <Map/MapView.h> +#include "MapView.h" Map::SearchMarkerTiler::SearchMarkerTiler(KGeoMap::ModelHelper *const modelHelper, QObject *const parent) : ItemMarkerTiler(modelHelper, parent) { m_mapView = dynamic_cast<MapView *>(parent); } Map::SearchMarkerTiler::~SearchMarkerTiler() { } KGeoMap::GroupState Map::SearchMarkerTiler::getGlobalGroupState() { if (m_mapView->regionSelected()) { return KGeoMap::ItemMarkerTiler::getGlobalGroupState() | KGeoMap::RegionSelectedAll; } else { return KGeoMap::ItemMarkerTiler::getGlobalGroupState(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/CategoryImageCollection.cpp b/Plugins/CategoryImageCollection.cpp index e7634522..4f172fa2 100644 --- a/Plugins/CategoryImageCollection.cpp +++ b/Plugins/CategoryImageCollection.cpp @@ -1,46 +1,48 @@ /* Copyright (C) 2003-2010 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 "CategoryImageCollection.h" -#include "DB/ImageDB.h" + +#include <DB/ImageDB.h> + #include <KLocalizedString> Plugins::CategoryImageCollection::CategoryImageCollection(const DB::ImageSearchInfo &context, const QString &category, const QString &value) : Plugins::ImageCollection(CategoryImageCollection::SubClass) , m_context(context) , m_category(category) , m_value(value) { } QString Plugins::CategoryImageCollection::name() { if (m_value == QString::fromLatin1("**NONE**")) return i18nc("The 'name' of an unnamed image collection.", "None"); else return m_value; } QList<QUrl> Plugins::CategoryImageCollection::images() { DB::ImageSearchInfo context(m_context); context.addAnd(m_category, m_value); QStringList list = DB::ImageDB::instance()->search(context, true).toStringList(DB::AbsolutePath); return stringListToUrlList(list); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/CategoryImageCollection.h b/Plugins/CategoryImageCollection.h index 85115db6..9d5fb995 100644 --- a/Plugins/CategoryImageCollection.h +++ b/Plugins/CategoryImageCollection.h @@ -1,46 +1,48 @@ /* Copyright (C) 2003-2010 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. */ #ifndef CATEGORYIMAGECOLLECTION_H #define CATEGORYIMAGECOLLECTION_H -#include "DB/ImageSearchInfo.h" -#include "Plugins/ImageCollection.h" +#include "ImageCollection.h" + +#include <DB/ImageSearchInfo.h> + #include <config-kpa-kipi.h> namespace Plugins { class CategoryImageCollection : public Plugins::ImageCollection { public: CategoryImageCollection(const DB::ImageSearchInfo &context, const QString &category, const QString &value); QString name() override; QList<QUrl> images() override; private: DB::ImageSearchInfo m_context; const QString m_category; const QString m_value; }; } #endif /* CATEGORYIMAGECOLLECTION_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/ImageCollection.cpp b/Plugins/ImageCollection.cpp index e794dd38..5998dac3 100644 --- a/Plugins/ImageCollection.cpp +++ b/Plugins/ImageCollection.cpp @@ -1,154 +1,154 @@ /* Copyright (C) 2003-2010 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 "ImageCollection.h" + #include "Logging.h" #include <DB/ImageDB.h> #include <DB/ImageInfo.h> #include <DB/ImageInfoList.h> #include <MainWindow/Window.h> #include <Settings/SettingsData.h> -#include <QFileInfo> - #include <KLocalizedString> +#include <QFileInfo> Plugins::ImageCollection::ImageCollection(Type tp) : m_type(tp) { } QString Plugins::ImageCollection::name() { QString res; switch (m_type) { case CurrentAlbum: res = MainWindow::Window::theMainWindow()->currentContext().toString(); break; case CurrentSelection: res = MainWindow::Window::theMainWindow()->currentContext().toString(); if (res.isEmpty()) { res = i18nc("As in 'an unknown set of images, created from the selection'.", "Unknown (Selection)"); } else { res += i18nc("As in 'A selection of [a generated context description]'", " (Selection)"); } break; case SubClass: qCWarning(PluginsLog, "Subclass of ImageCollection should overwrite ImageCollection::name()"); res = i18nc("A set of images with no description.", "Unknown"); break; } if (res.isEmpty()) { // at least html export plugin needs a non-empty name: res = i18nc("The 'name' of an unnamed image collection.", "None"); } return res; } QList<QUrl> Plugins::ImageCollection::images() { switch (m_type) { case CurrentAlbum: return stringListToUrlList(DB::ImageDB::instance()->currentScope(false).toStringList(DB::AbsolutePath)); case CurrentSelection: return stringListToUrlList(MainWindow::Window::theMainWindow()->selected(ThumbnailView::NoExpandCollapsedStacks).toStringList(DB::AbsolutePath)); case SubClass: qFatal("The subclass should implement images()"); return QList<QUrl>(); } return QList<QUrl>(); } QList<QUrl> Plugins::ImageCollection::imageListToUrlList(const DB::ImageInfoList &imageList) { QList<QUrl> urlList; for (DB::ImageInfoListConstIterator it = imageList.constBegin(); it != imageList.constEnd(); ++it) { QUrl url; url.setPath((*it)->fileName().absolute()); urlList.append(url); } return urlList; } QList<QUrl> Plugins::ImageCollection::stringListToUrlList(const QStringList &list) { QList<QUrl> urlList; for (QStringList::ConstIterator it = list.begin(); it != list.end(); ++it) { QUrl url; url.setPath(*it); urlList.append(url); } return urlList; } QUrl Plugins::ImageCollection::url() { return commonRoot(); } QUrl Plugins::ImageCollection::commonRoot() { QString imgRoot = Settings::SettingsData::instance()->imageDirectory(); const QList<QUrl> imgs = images(); if (imgs.count() == 0) return QUrl::fromLocalFile(imgRoot); QStringList res = QFileInfo(imgs[0].path()).absolutePath().split(QLatin1String("/")); for (QList<QUrl>::ConstIterator it = imgs.begin(); it != imgs.end(); ++it) { QStringList newRes; QStringList path = QFileInfo((*it).path()).absolutePath().split(QLatin1String("/")); int i = 0; for (; i < qMin(path.size(), res.size()); ++i) { if (path[i] == res[i]) newRes.append(res[i]); else break; } res = newRes; } QString result = res.join(QString::fromLatin1("/")); if (result.left(imgRoot.length()) != imgRoot) { result = imgRoot; } return QUrl::fromLocalFile(result); } QUrl Plugins::ImageCollection::uploadUrl() { return commonRoot(); } QUrl Plugins::ImageCollection::uploadRootUrl() { QUrl url = QUrl::fromLocalFile(Settings::SettingsData::instance()->imageDirectory()); return url; } QString Plugins::ImageCollection::uploadRootName() { return i18nc("'Name' of the image directory", "Image/Video root directory"); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/ImageCollection.h b/Plugins/ImageCollection.h index 59df8367..e140d39f 100644 --- a/Plugins/ImageCollection.h +++ b/Plugins/ImageCollection.h @@ -1,64 +1,63 @@ /* Copyright (C) 2003-2010 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. */ #ifndef MYIMAGECOLLECTION_H #define MYIMAGECOLLECTION_H -#include <config-kpa-kipi.h> +#include <DB/ImageInfoList.h> #include <KIPI/ImageCollectionShared> - -#include <DB/ImageInfoList.h> +#include <config-kpa-kipi.h> namespace Plugins { class ImageCollection : public KIPI::ImageCollectionShared { public: enum Type { CurrentAlbum, CurrentSelection, SubClass }; explicit ImageCollection(Type tp); QString name() override; QList<QUrl> images() override; // FIXME: url() should not called unless isDirectory() is true // therefore, we should also to implement isDirectory QUrl url() override; QUrl uploadUrl() override; QUrl uploadRootUrl() override; QString uploadRootName() override; // isDirectory protected: QList<QUrl> imageListToUrlList(const DB::ImageInfoList &list); QList<QUrl> stringListToUrlList(const QStringList &list); QUrl commonRoot(); private: Type m_type; }; } #endif /* MYIMAGECOLLECTION_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/ImageCollectionSelector.h b/Plugins/ImageCollectionSelector.h index 341fbb20..d843959c 100644 --- a/Plugins/ImageCollectionSelector.h +++ b/Plugins/ImageCollectionSelector.h @@ -1,59 +1,60 @@ /* Copyright (C) 2003-2010 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. */ #ifndef MYIMAGECOLLECTIONSELECTOR_H #define MYIMAGECOLLECTIONSELECTOR_H -#include "Plugins/ImageCollection.h" -#include "Plugins/Interface.h" +#include "ImageCollection.h" +#include "Interface.h" + #include <config-kpa-kipi.h> namespace Plugins { /** This class should provide a widget for selecting one ore more image collection for plugins that want the user to * select images. * * Since selecting images is all kphotoalbum is about ;-), this implementation just passes the images that are (or * would be) currently visible in thumbnail view - if some of them are selected, only selected ones, otherwise all. * * The widget shown is currently empty. * * Possible improvements: * * show some description of the currently selected images instead of just nothing * * give the user the possibility to group the selected images into image collections by some category: this would be * useful as i.e. html export plugin uses the names of image collections as headlines and groups the images visually by * image collection. */ class ImageCollectionSelector : public KIPI::ImageCollectionSelector { public: ImageCollectionSelector(QWidget *parent, Interface *interface); QList<KIPI::ImageCollection> selectedImageCollections() const override; protected: // just fake a selectionChanged event when first shown to make export plugin happy: void showEvent(QShowEvent *event) override; private: Interface *m_interface; bool m_firstTimeVisible; }; } #endif /* MYIMAGECOLLECTIONSELECTOR_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/ImageInfo.cpp b/Plugins/ImageInfo.cpp index 02609e7f..dd42bf35 100644 --- a/Plugins/ImageInfo.cpp +++ b/Plugins/ImageInfo.cpp @@ -1,366 +1,367 @@ /* Copyright (C) 2003-2010 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 "ImageInfo.h" -#include "Logging.h" -#include <QFileInfo> -#include <QList> +#include "Logging.h" #include <DB/Category.h> #include <DB/CategoryCollection.h> #include <DB/CategoryPtr.h> #include <DB/ImageDB.h> #include <DB/ImageInfo.h> #include <DB/MemberMap.h> #include <MainWindow/DirtyIndicator.h> +#include <QFileInfo> +#include <QList> + #define KEXIV_ORIENTATION_UNSPECIFIED 0 #define KEXIV_ORIENTATION_NORMAL 1 #define KEXIV_ORIENTATION_HFLIP 2 #define KEXIV_ORIENTATION_ROT_180 3 #define KEXIV_ORIENTATION_VFLIP 4 #define KEXIV_ORIENTATION_ROT_90_HFLIP 5 #define KEXIV_ORIENTATION_ROT_90 6 #define KEXIV_ORIENTATION_ROT_90_VFLIP 7 #define KEXIV_ORIENTATION_ROT_270 8 /** * Convert a rotation in degrees to a KExiv2::ImageOrientation value. */ static int deg2KexivOrientation(int deg) { deg = (deg + 360) % 360; ; switch (deg) { case 0: return KEXIV_ORIENTATION_NORMAL; case 90: return KEXIV_ORIENTATION_ROT_90; case 180: return KEXIV_ORIENTATION_ROT_180; case 270: return KEXIV_ORIENTATION_ROT_270; default: qCWarning(PluginsLog) << "Rotation of " << deg << "degrees can't be mapped to KExiv2::ImageOrientation value."; return KEXIV_ORIENTATION_UNSPECIFIED; } } /** * Convert a KExiv2::ImageOrientation value into a degrees angle. */ static int kexivOrientation2deg(int orient) { switch (orient) { case KEXIV_ORIENTATION_NORMAL: return 0; case KEXIV_ORIENTATION_ROT_90: return 90; case KEXIV_ORIENTATION_ROT_180: return 280; case KEXIV_ORIENTATION_ROT_270: return 270; default: qCWarning(PluginsLog) << "KExiv2::ImageOrientation value " << orient << " not a pure rotation. Discarding orientation info."; return 0; } } Plugins::ImageInfo::ImageInfo(KIPI::Interface *interface, const QUrl &url) : KIPI::ImageInfoShared(interface, url) { m_info = DB::ImageDB::instance()->info(DB::FileName::fromAbsolutePath(_url.path())); } QMap<QString, QVariant> Plugins::ImageInfo::attributes() { if (m_info == nullptr) { // This can happen if we're trying to access an image that // has been deleted on-disc, but not yet the database return QMap<QString, QVariant>(); } Q_ASSERT(m_info); QMap<QString, QVariant> res; res.insert(QString::fromLatin1("name"), QFileInfo(m_info->fileName().absolute()).baseName()); res.insert(QString::fromLatin1("comment"), m_info->description()); res.insert(QLatin1String("date"), m_info->date().start()); res.insert(QLatin1String("dateto"), m_info->date().end()); res.insert(QLatin1String("isexactdate"), m_info->date().start() == m_info->date().end()); res.insert(QString::fromLatin1("orientation"), deg2KexivOrientation(m_info->angle())); res.insert(QString::fromLatin1("angle"), deg2KexivOrientation(m_info->angle())); // for compatibility with older versions. Now called orientation. res.insert(QString::fromLatin1("title"), m_info->label()); res.insert(QString::fromLatin1("rating"), m_info->rating()); // not supported: //res.insert(QString::fromLatin1("colorlabel"), xxx ); //res.insert(QString::fromLatin1("picklabel"), xxx ); #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates position = m_info->coordinates(); if (position.hasCoordinates()) { res.insert(QString::fromLatin1("longitude"), QVariant(position.lon())); res.insert(QString::fromLatin1("latitude"), QVariant(position.lat())); if (position.hasAltitude()) res.insert(QString::fromLatin1("altitude"), QVariant(position.alt())); } #endif // Flickr plug-in expects the item tags, so we better give them. QString text; QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); QStringList tags; QStringList tagspath; const QLatin1String sep("/"); Q_FOREACH (const DB::CategoryPtr category, categories) { QString categoryName = category->name(); if (category->isSpecialCategory()) continue; // I don't know why any categories except the above should be excluded //if ( category->doShow() ) { Utilities::StringSet items = m_info->itemsOfCategory(categoryName); Q_FOREACH (const QString &tag, items) { tags.append(tag); // digikam compatible tag path: // note: this produces a semi-flattened hierarchy. // instead of "Places/France/Paris" this will yield "Places/Paris" tagspath.append(categoryName + sep + tag); } //} } res.insert(QString::fromLatin1("tagspath"), tagspath); res.insert(QString::fromLatin1("keywords"), tags); res.insert(QString::fromLatin1("tags"), tags); // for compatibility with older versions. Now called keywords. // TODO: implement this: //res.insert(QString::fromLatin1( "filesize" ), xxx ); // not supported: //res.insert(QString::fromLatin1( "creators" ), xxx ); //res.insert(QString::fromLatin1( "credit" ), xxx ); //res.insert(QString::fromLatin1( "rights" ), xxx ); //res.insert(QString::fromLatin1( "source" ), xxx ); return res; } void Plugins::ImageInfo::clearAttributes() { if (m_info) { // official behaviour is to delete all officially supported attributes: QStringList attr; attr.append(QString::fromLatin1("comment")); attr.append(QString::fromLatin1("date")); attr.append(QString::fromLatin1("title")); attr.append(QString::fromLatin1("orientation")); attr.append(QString::fromLatin1("tagspath")); attr.append(QString::fromLatin1("rating")); attr.append(QString::fromLatin1("colorlabel")); attr.append(QString::fromLatin1("picklabel")); attr.append(QString::fromLatin1("gpslocation")); attr.append(QString::fromLatin1("copyrights")); delAttributes(attr); } } void Plugins::ImageInfo::addAttributes(const QMap<QString, QVariant> &amap) { if (m_info && !amap.empty()) { QMap<QString, QVariant> map = amap; if (map.contains(QLatin1String("name"))) { // plugin renamed the item // TODO: implement this qCWarning(PluginsLog, "File renaming by kipi-plugin not supported."); //map.remove(QLatin1String("name")); } if (map.contains(QLatin1String("comment"))) { // is it save to do that? digikam seems to allow multiple comments on a single image // if a plugin assumes that it is adding a comment, not setting it, things might go badly... m_info->setDescription(map[QLatin1String("comment")].toString()); map.remove(QLatin1String("comment")); } // note: this probably won't work as expected because according to the spec, // "isexactdate" is supposed to be readonly and therefore never set here: if (map.contains(QLatin1String("isexactdate")) && map.contains(QLatin1String("date"))) { m_info->setDate(DB::ImageDate(map[QLatin1String("date")].toDateTime())); map.remove(QLatin1String("date")); } else if (map.contains(QLatin1String("date")) && map.contains(QLatin1String("dateto"))) { m_info->setDate(DB::ImageDate(map[QLatin1String("date")].toDateTime(), map[QLatin1String("dateto")].toDateTime())); map.remove(QLatin1String("date")); map.remove(QLatin1String("dateto")); } else if (map.contains(QLatin1String("date"))) { m_info->setDate(DB::ImageDate(map[QLatin1String("date")].toDateTime())); map.remove(QLatin1String("date")); } if (map.contains(QLatin1String("angle"))) { qCWarning(PluginsLog, "Kipi-plugin uses deprecated attribute \"angle\"."); m_info->setAngle(kexivOrientation2deg(map[QLatin1String("angle")].toInt())); map.remove(QLatin1String("angle")); } if (map.contains(QLatin1String("orientation"))) { m_info->setAngle(kexivOrientation2deg(map[QLatin1String("orientation")].toInt())); map.remove(QLatin1String("orientation")); } if (map.contains(QLatin1String("title"))) { m_info->setLabel(map[QLatin1String("title")].toString()); map.remove(QLatin1String("title")); } if (map.contains(QLatin1String("rating"))) { m_info->setRating(map[QLatin1String("rating")].toInt()); map.remove(QLatin1String("rating")); } if (map.contains(QLatin1String("tagspath"))) { const QStringList tagspaths = map[QLatin1String("tagspath")].toStringList(); const DB::CategoryCollection *categories = DB::ImageDB::instance()->categoryCollection(); DB::MemberMap &memberMap = DB::ImageDB::instance()->memberMap(); Q_FOREACH (const QString &path, tagspaths) { qCDebug(PluginsLog) << "Adding tags: " << path; QStringList tagpath = path.split(QLatin1String("/"), QString::SkipEmptyParts); // Note: maybe tagspaths with only one component or with unknown first component // should be added to the "keywords"/"Events" category? if (tagpath.size() < 2) { qCWarning(PluginsLog) << "Ignoring incompatible tag: " << path; continue; } // first component is the category, const QString categoryName = tagpath.takeFirst(); DB::CategoryPtr cat = categories->categoryForName(categoryName); if (cat) { QString previousTag; // last component is the tag: // others define hierarchy: Q_FOREACH (const QString ¤tTag, tagpath) { if (!cat->items().contains(currentTag)) { qCDebug(PluginsLog) << "Adding tag " << currentTag << " to category " << categoryName; // before we can use a tag, we have to add it cat->addItem(currentTag); } if (!previousTag.isNull()) { if (!memberMap.isGroup(categoryName, previousTag)) { // create a group for the parent tag, so we can add a sub-category memberMap.addGroup(categoryName, previousTag); } if (memberMap.canAddMemberToGroup(categoryName, previousTag, currentTag)) { // make currentTag a member of the previousTag group memberMap.addMemberToGroup(categoryName, previousTag, currentTag); } else { qCWarning(PluginsLog) << "Cannot make " << currentTag << " a subcategory of " << categoryName << "/" << previousTag << "!"; } } previousTag = currentTag; } qCDebug(PluginsLog) << "Adding tag " << previousTag << " in category " << categoryName << " to image " << m_info->label(); // previousTag must be a valid category (see addItem() above...) m_info->addCategoryInfo(categoryName, previousTag); } else { qCWarning(PluginsLog) << "Unknown category: " << categoryName; } } map.remove(QLatin1String("tagspath")); } // remove read-only keywords: map.remove(QLatin1String("filesize")); map.remove(QLatin1String("isexactdate")); map.remove(QLatin1String("keywords")); map.remove(QLatin1String("tags")); map.remove(QLatin1String("altitude")); map.remove(QLatin1String("longitude")); map.remove(QLatin1String("latitude")); // colorlabel // picklabel // creators // credit // rights // source MainWindow::DirtyIndicator::markDirty(); if (!map.isEmpty()) { qCWarning(PluginsLog) << "The following attributes are not (yet) supported by the KPhotoAlbum KIPI interface:" << map; } } } void Plugins::ImageInfo::delAttributes(const QStringList &attrs) { if (m_info && !attrs.empty()) { QStringList delAttrs = attrs; if (delAttrs.contains(QLatin1String("comment"))) { m_info->setDescription(QString()); delAttrs.removeAll(QLatin1String("comment")); } // not supported: date if (delAttrs.contains(QLatin1String("orientation")) || delAttrs.contains(QLatin1String("angle"))) { m_info->setAngle(0); delAttrs.removeAll(QLatin1String("orientation")); delAttrs.removeAll(QLatin1String("angle")); } if (delAttrs.contains(QLatin1String("rating"))) { m_info->setRating(-1); delAttrs.removeAll(QLatin1String("rating")); } if (delAttrs.contains(QLatin1String("title"))) { m_info->setLabel(QString()); delAttrs.removeAll(QLatin1String("title")); } // TODO: // (colorlabel) // (picklabel) // copyrights // not supported: gpslocation if (delAttrs.contains(QLatin1String("tags")) || delAttrs.contains(QLatin1String("tagspath"))) { m_info->clearAllCategoryInfo(); delAttrs.removeAll(QLatin1String("tags")); delAttrs.removeAll(QLatin1String("tagspath")); } MainWindow::DirtyIndicator::markDirty(); if (!delAttrs.isEmpty()) { qCWarning(PluginsLog) << "The following attributes are not (yet) supported by the KPhotoAlbum KIPI interface:" << delAttrs; } } } void Plugins::ImageInfo::cloneData(ImageInfoShared *const other) { ImageInfoShared::cloneData(other); if (m_info) { Plugins::ImageInfo *inf = static_cast<Plugins::ImageInfo *>(other); m_info->setDate(inf->m_info->date()); MainWindow::DirtyIndicator::markDirty(); } } bool Plugins::ImageInfo::isPositionAttribute(const QString &key) { return (key == QString::fromLatin1("longitude") || key == QString::fromLatin1("latitude") || key == QString::fromLatin1("altitude") || key == QString::fromLatin1("positionPrecision")); } bool Plugins::ImageInfo::isCategoryAttribute(const QString &key) { return (key != QString::fromLatin1("tags") && !isPositionAttribute(key)); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/ImageInfo.h b/Plugins/ImageInfo.h index cfe687cd..d2831b2c 100644 --- a/Plugins/ImageInfo.h +++ b/Plugins/ImageInfo.h @@ -1,59 +1,58 @@ /* Copyright (C) 2003-2010 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. */ #ifndef MYIMAGEINFO_H #define MYIMAGEINFO_H -#include <config-kpa-kipi.h> +#include <DB/ImageInfoPtr.h> #include <KIPI/ImageInfoShared> - -#include <DB/ImageInfoPtr.h> +#include <config-kpa-kipi.h> namespace DB { class ImageInfo; } namespace Plugins { class ImageInfo : public KIPI::ImageInfoShared { public: ImageInfo(KIPI::Interface *interface, const QUrl &url); QMap<QString, QVariant> attributes() override; void clearAttributes() override; void addAttributes(const QMap<QString, QVariant> &) override; void delAttributes(const QStringList &) override; void cloneData(ImageInfoShared *const other) override; private: DB::ImageInfoPtr m_info; bool isPositionAttribute(const QString &key); bool isCategoryAttribute(const QString &key); }; } #endif /* MYIMAGEINFO_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/Interface.cpp b/Plugins/Interface.cpp index c4bb5eaa..7694b8c1 100644 --- a/Plugins/Interface.cpp +++ b/Plugins/Interface.cpp @@ -1,213 +1,212 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "CategoryImageCollection.h" +#include "ImageCollection.h" +#include "ImageCollectionSelector.h" +#include "ImageInfo.h" +#include "UploadWidget.h" -#include "Utilities/FileUtil.h" #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/FileUtil.h> -#include "UploadWidget.h" +#include <KFileItem> +#include <KIO/PreviewJob> +#include <KIPI/ImageCollection> +#include <KLocalizedString> +#include <QByteArray> +#include <QImageReader> +#include <QList> 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, DB::CountWithRange> categories = DB::ImageDB::instance()->classify(context, category, DB::Image); for (auto it = categories.constBegin(); it != categories.constEnd(); ++it) { auto *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()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/Interface.h b/Plugins/Interface.h index 47fc0da0..bf68e310 100644 --- a/Plugins/Interface.h +++ b/Plugins/Interface.h @@ -1,92 +1,90 @@ /* Copyright (C) 2003-2010 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. */ #ifndef KPHOTOALBUM_PLUGININTERFACE_H #define KPHOTOALBUM_PLUGININTERFACE_H -#include <config-kpa-kipi.h> - -#include <QList> -#include <QUrl> -#include <QVariant> - #include <KIPI/ImageCollection> #include <KIPI/ImageCollectionSelector> #include <KIPI/ImageInfo> #include <KIPI/Interface> +#include <QList> +#include <QUrl> +#include <QVariant> +#include <config-kpa-kipi.h> class QPixmap; class KFileItem; namespace Browser { class BreadcrumbList; } namespace Plugins { class Interface : public KIPI::Interface { Q_OBJECT public: explicit Interface(QObject *parent, QString name = QString()); KIPI::ImageCollection currentAlbum() override; KIPI::ImageCollection currentSelection() override; QList<KIPI::ImageCollection> allAlbums() override; KIPI::ImageInfo info(const QUrl &) override; bool addImage(const QUrl &, QString &errmsg) override; void delImage(const QUrl &) override; void refreshImages(const QList<QUrl> &urls) override; void thumbnail(const QUrl &url, int size) override; void thumbnails(const QList<QUrl> &list, int size) override; KIPI::ImageCollectionSelector *imageCollectionSelector(QWidget *parent) override; KIPI::UploadWidget *uploadWidget(QWidget *parent) override; QAbstractItemModel *getTagTree() const override; // these two methods are only here because of a libkipi api error // either remove them when they are no longer pure virtual in KIPI::Interface, // or implement them and update features() accordingly: // FIXME: this can be safely removed if/when libkipi 5.1.0 is no longer supported KIPI::FileReadWriteLock *createReadWriteLock(const QUrl &) const override; KIPI::MetadataProcessor *createMetadataProcessor() const override; int features() const override; public slots: void slotSelectionChanged(bool); void pathChanged(const Browser::BreadcrumbList &path); private slots: void gotKDEPreview(const KFileItem &item, const QPixmap &pix); void failedKDEPreview(const KFileItem &item); signals: void imagesChanged(const QList<QUrl> &); }; } #endif /* PLUGININTERFACE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/PurposeMenu.cpp b/Plugins/PurposeMenu.cpp index a937707f..41bed572 100644 --- a/Plugins/PurposeMenu.cpp +++ b/Plugins/PurposeMenu.cpp @@ -1,95 +1,96 @@ /* Copyright (C) 2019 The KPhotoAlbum Development Team * * 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 "PurposeMenu.h" + #include "Logging.h" #include <MainWindow/Window.h> #include <KLocalizedString> #include <Purpose/AlternativesModel> #include <PurposeWidgets/Menu> #include <QJsonArray> #include <QJsonObject> #include <QMenu> Plugins::PurposeMenu::PurposeMenu(QMenu *parent) : QObject(parent) , m_parentMenu(parent) , m_purposeMenu(new Purpose::Menu(parent)) , m_menuUpdateNeeded(true) { loadPurposeMenu(); } void Plugins::PurposeMenu::slotSelectionChanged() { m_menuUpdateNeeded = true; m_purposeMenu->clear(); qCDebug(PluginsLog) << "Purpose menu items invalidated..."; } void Plugins::PurposeMenu::loadPurposeMenu() { // attach the menu QAction *purposeMenu = m_parentMenu->addMenu(m_purposeMenu); purposeMenu->setText(i18n("Share")); purposeMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-share"))); // set up the callback signal connect(m_purposeMenu, &Purpose::Menu::finished, this, [this](const QJsonObject &output, int error, const QString &message) { if (error) { qCDebug(PluginsLog) << "Failed to share image:" << message; emit imageSharingFailed(message); } else { // Note: most plugins don't seem to actually return anything in the url field... const QUrl returnUrl = QUrl(output[QStringLiteral("url")].toString(), QUrl::ParsingMode::StrictMode); qCDebug(PluginsLog) << "Image shared successfully."; qCDebug(PluginsLog) << "Raw json data: " << output; emit imageShared(returnUrl); } }); // update available options based on the latest picture connect(m_purposeMenu, &QMenu::aboutToShow, this, &PurposeMenu::loadPurposeItems); qCDebug(PluginsLog) << "Purpose menu loaded..."; } void Plugins::PurposeMenu::loadPurposeItems() { if (!m_menuUpdateNeeded) { return; } m_menuUpdateNeeded = false; const DB::FileNameList images = MainWindow::Window::theMainWindow()->selected(ThumbnailView::NoExpandCollapsedStacks); QJsonArray urls; for (const auto &image : images) { urls.append(QUrl(image).toString()); } // "image/jpeg" is certainly not always true, but the interface does not allow a mimeType list // and the plugins likely won't care... m_purposeMenu->model()->setInputData(QJsonObject { { QStringLiteral("mimeType"), QStringLiteral("image/jpeg") }, { QStringLiteral("urls"), urls } }); m_purposeMenu->model()->setPluginType(QStringLiteral("Export")); m_purposeMenu->reload(); qCDebug(PluginsLog) << "Purpose menu items loaded..."; } diff --git a/Plugins/PurposeMenu.h b/Plugins/PurposeMenu.h index 8d72beec..924aead2 100644 --- a/Plugins/PurposeMenu.h +++ b/Plugins/PurposeMenu.h @@ -1,76 +1,75 @@ /* Copyright (C) 2019 The KPhotoAlbum Development Team * * 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/>. */ #ifndef KPHOTOALBUM_PURPOSEMENU_H #define KPHOTOALBUM_PURPOSEMENU_H -#include <config-kpa-kipi.h> - #include <QObject> #include <QString> #include <QUrl> +#include <config-kpa-kipi.h> class QMenu; namespace Purpose { class Menu; } namespace Plugins { class PurposeMenu : public QObject { Q_OBJECT public: explicit PurposeMenu(QMenu *parent); public slots: void slotSelectionChanged(); signals: /** * @brief imageShared is emitted when an image was shared successfully. * The url contains the optional location of the shared data * (e.g. for plugins that upload to a remote location). */ void imageShared(QUrl); void imageSharingFailed(QString message); private: QMenu *m_parentMenu; Purpose::Menu *m_purposeMenu; bool m_menuUpdateNeeded; ///< Keeps track of changed image selection /** * @brief Load the Purpose::Menu, add it to the parent menu, and set up connections. */ void loadPurposeMenu(); /** * @brief Load Purpose menu items into the Purpose::Menu. * This is dependent on the current set of images. */ void loadPurposeItems(); }; } #endif /* PURPOSEMENU_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/UploadImageCollection.cpp b/Plugins/UploadImageCollection.cpp index cecc65d7..236b056e 100644 --- a/Plugins/UploadImageCollection.cpp +++ b/Plugins/UploadImageCollection.cpp @@ -1,60 +1,61 @@ /* Copyright 2012 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 "UploadImageCollection.h" + #include <Settings/SettingsData.h> #include <KLocalizedString> namespace Plugins { UploadImageCollection::UploadImageCollection(const QString &path) : m_path(path) { } QList<QUrl> UploadImageCollection::images() { return QList<QUrl>(); } QString UploadImageCollection::name() { return QString(); } QUrl UploadImageCollection::uploadUrl() { return QUrl::fromLocalFile(m_path); } QUrl UploadImageCollection::uploadRootUrl() { QUrl url = QUrl::fromLocalFile(Settings::SettingsData::instance()->imageDirectory()); return url; } QString UploadImageCollection::uploadRootName() { return i18nc("'Name' of the image directory", "Image/Video root directory"); } } // namespace Plugins // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Plugins/UploadWidget.cpp b/Plugins/UploadWidget.cpp index 11aeef40..57a13f40 100644 --- a/Plugins/UploadWidget.cpp +++ b/Plugins/UploadWidget.cpp @@ -1,61 +1,61 @@ /* Copyright 2012 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 "UploadWidget.h" -#include <QFileSystemModel> -#include <QHBoxLayout> -#include <QTreeView> +#include "ImageCollection.h" +#include "UploadImageCollection.h" #include <Settings/SettingsData.h> -#include "ImageCollection.h" -#include "UploadImageCollection.h" +#include <QFileSystemModel> +#include <QHBoxLayout> +#include <QTreeView> namespace Plugins { UploadWidget::UploadWidget(QWidget *parent) : KIPI::UploadWidget(parent) { QTreeView *listView = new QTreeView(this); QHBoxLayout *layout = new QHBoxLayout(this); layout->addWidget(listView); m_model = new QFileSystemModel(this); m_model->setFilter(QDir::Dirs | QDir::NoDotDot); listView->setModel(m_model); m_path = Settings::SettingsData::instance()->imageDirectory(); const QModelIndex index = m_model->setRootPath(m_path); listView->setRootIndex(index); connect(listView, &QTreeView::activated, this, &UploadWidget::newIndexSelected); } KIPI::ImageCollection UploadWidget::selectedImageCollection() const { return KIPI::ImageCollection(new Plugins::UploadImageCollection(m_path)); } void UploadWidget::newIndexSelected(const QModelIndex &index) { m_path = m_model->filePath(index); } } // namespace Plugins // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/RemoteControl/ConnectionIndicator.cpp b/RemoteControl/ConnectionIndicator.cpp index 286ed863..4d447d5a 100644 --- a/RemoteControl/ConnectionIndicator.cpp +++ b/RemoteControl/ConnectionIndicator.cpp @@ -1,159 +1,161 @@ /* Copyright (C) 2014-2019 The KPhotoAlbum Development Team 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 "ConnectionIndicator.h" + #include "RemoteInterface.h" + #include <MainWindow/Options.h> #include <Settings/SettingsData.h> #include <KLocalizedString> #include <QDialog> #include <QHBoxLayout> #include <QHostAddress> #include <QIcon> #include <QLabel> #include <QLineEdit> #include <QTimer> #include <QValidator> namespace RemoteControl { ConnectionIndicator::ConnectionIndicator(QWidget *parent) : QLabel(parent) , m_state(Off) { setToolTip(i18n("This icon indicates if KPhotoAlbum is connected to an android device.\n" "Click on the icon to toggle listening for clients in the local area network.\n" "If the local area network doesn't allow broadcast packages between the android client " "and KPhotoAlbum, then right click on the icon and specify the android device's address.\n" "The android client can be downloaded from google play.")); connect(&RemoteInterface::instance(), SIGNAL(connected()), this, SLOT(on())); connect(&RemoteInterface::instance(), SIGNAL(disConnected()), this, SLOT(wait())); connect(&RemoteInterface::instance(), SIGNAL(listening()), this, SLOT(wait())); connect(&RemoteInterface::instance(), SIGNAL(stoppedListening()), this, SLOT(off())); m_timer = new QTimer(this); connect(m_timer, SIGNAL(timeout()), this, SLOT(waitingAnimation())); off(); } void ConnectionIndicator::mouseReleaseEvent(QMouseEvent *) { if (m_state == Off) { QHostAddress bindTo = MainWindow::Options::the()->listen(); if (bindTo.isNull()) bindTo = QHostAddress::Any; RemoteInterface::instance().listen(bindTo); wait(); } else { RemoteInterface::instance().stopListening(); m_state = Off; m_timer->stop(); off(); } } namespace { class IPValidator : public QValidator { protected: State validate(QString &input, int &) const override { for (int pos = 0; pos < 15; pos += 4) { bool ok1; int i = input.mid(pos, 1).toInt(&ok1); bool ok2; int j = input.mid(pos + 1, 1).toInt(&ok2); bool ok3; int k = input.mid(pos + 2, 1).toInt(&ok3); if ((ok1 && i > 2) || (ok1 && ok2 && i == 2 && j > 5) || (ok1 && ok2 && ok3 && i * 100 + j * 10 + k > 255)) return Invalid; } return Acceptable; } }; } //namespace void ConnectionIndicator::contextMenuEvent(QContextMenuEvent *) { QDialog dialog; QLabel label(i18n("Android device address: "), &dialog); QLineEdit edit(&dialog); edit.setInputMask(QString::fromUtf8("000.000.000.000;_")); edit.setText(Settings::SettingsData::instance()->recentAndroidAddress()); IPValidator validator; edit.setValidator(&validator); QHBoxLayout layout(&dialog); layout.addWidget(&label); layout.addWidget(&edit); connect(&edit, SIGNAL(returnPressed()), &dialog, SLOT(accept())); int code = dialog.exec(); if (code == QDialog::Accepted) { RemoteInterface::instance().connectTo(QHostAddress(edit.text())); wait(); Settings::SettingsData::instance()->setRecentAndroidAddress(edit.text()); } } void ConnectionIndicator::on() { m_state = On; m_timer->stop(); QIcon icon { QIcon::fromTheme(QString::fromUtf8("network-wireless")) }; setPixmap(icon.pixmap(32, 32)); } void ConnectionIndicator::off() { m_timer->stop(); m_state = Off; QIcon icon { QIcon::fromTheme(QString::fromUtf8("network-disconnect")) }; setPixmap(icon.pixmap(32, 32)); } void ConnectionIndicator::wait() { m_timer->start(300); m_state = Connecting; } void ConnectionIndicator::waitingAnimation() { static int index = 0; static QList<QPixmap> icons; if (icons.isEmpty()) { icons.append(QIcon::fromTheme(QString::fromUtf8("network-wireless-disconnected")).pixmap(32, 32)); icons.append(QIcon::fromTheme(QString::fromUtf8("network-wireless-connected-25")).pixmap(32, 32)); icons.append(QIcon::fromTheme(QString::fromUtf8("network-wireless-connected-50")).pixmap(32, 32)); icons.append(QIcon::fromTheme(QString::fromUtf8("network-wireless-connected-75")).pixmap(32, 32)); icons.append(QIcon::fromTheme(QString::fromUtf8("network-wireless")).pixmap(32, 32)); } index = (index + 1) % icons.count(); setPixmap(icons[index]); } } // namespace RemoteControl diff --git a/RemoteControl/ImageNameStore.cpp b/RemoteControl/ImageNameStore.cpp index 593af8a8..d5143db2 100644 --- a/RemoteControl/ImageNameStore.cpp +++ b/RemoteControl/ImageNameStore.cpp @@ -1,69 +1,70 @@ /* Copyright (C) 2014 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 "ImageNameStore.h" -#include "DB/ImageDB.h" + +#include <DB/ImageDB.h> namespace RemoteControl { ImageNameStore::ImageNameStore() { // To avoid delays when the user shows all images the first time, lets pull all images now. for (const DB::FileName &fileName : DB::ImageDB::instance()->images()) { m_lastId++; m_idToNameMap.insert(m_lastId, fileName); m_nameToIdMap.insert(fileName, m_lastId); } } DB::FileName ImageNameStore::operator[](int id) { return m_idToNameMap[id]; } int ImageNameStore::operator[](const DB::FileName &fileName) { auto iterator = m_nameToIdMap.find(fileName); if (iterator == m_nameToIdMap.end()) { m_lastId++; m_nameToIdMap.insert(fileName, m_lastId); m_idToNameMap.insert(m_lastId, fileName); return m_lastId; } return *iterator; } int ImageNameStore::idForCategory(const QString &category, const QString &item) { auto key = qMakePair(category, item); auto it = m_categoryToIdMap.find(key); if (it == m_categoryToIdMap.end()) { m_lastId++; m_categoryToIdMap.insert(key, m_lastId); m_idToCategoryMap.insert(m_lastId, key); return m_lastId; } else return *it; } QPair<QString, QString> ImageNameStore::categoryForId(int id) { return m_idToCategoryMap[id]; } } // namespace RemoteControl diff --git a/RemoteControl/ImageNameStore.h b/RemoteControl/ImageNameStore.h index 66980b83..df0d78ae 100644 --- a/RemoteControl/ImageNameStore.h +++ b/RemoteControl/ImageNameStore.h @@ -1,48 +1,49 @@ /* Copyright (C) 2014 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. */ #ifndef REMOTECONTROL_IMAGENAMESTORE_H #define REMOTECONTROL_IMAGENAMESTORE_H -#include "DB/FileName.h" +#include <DB/FileName.h> + #include <QHash> #include <QPair> namespace RemoteControl { class ImageNameStore { public: ImageNameStore(); DB::FileName operator[](int id); int operator[](const DB::FileName &fileName); int idForCategory(const QString &category, const QString &item); QPair<QString, QString> categoryForId(int id); private: QHash<int, DB::FileName> m_idToNameMap; QHash<DB::FileName, int> m_nameToIdMap; QHash<QPair<QString, QString>, int> m_categoryToIdMap; QHash<int, QPair<QString, QString>> m_idToCategoryMap; int m_lastId = 0; }; } // namespace RemoteControl #endif // REMOTECONTROL_IMAGENAMESTORE_H diff --git a/RemoteControl/RemoteCommand.cpp b/RemoteControl/RemoteCommand.cpp index 2368d2a7..0523ce9f 100644 --- a/RemoteControl/RemoteCommand.cpp +++ b/RemoteControl/RemoteCommand.cpp @@ -1,267 +1,268 @@ /* Copyright (C) 2014 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 "RemoteCommand.h" #include "Serializer.h" + #include <QDebug> #include <QElapsedTimer> #include <QMap> #include <QPainter> #include <functional> #include <memory> using namespace RemoteControl; #define ENUMSTREAM(TYPE) \ QDataStream &operator<<(QDataStream &stream, TYPE type) \ { \ stream << (qint32)type; \ return stream; \ } \ \ QDataStream &operator>>(QDataStream &stream, TYPE &type) \ { \ stream >> (qint32 &)type; \ return stream; \ } ENUMSTREAM(ViewType) ENUMSTREAM(SearchType) ENUMSTREAM(ToggleTokenRequest::State) RemoteCommand::RemoteCommand(CommandType type) : m_type(type) { } RemoteCommand::~RemoteCommand() { qDeleteAll(m_serializers); } void RemoteCommand::encode(QDataStream &stream) const { for (SerializerInterface *serializer : m_serializers) serializer->encode(stream); } void RemoteCommand::decode(QDataStream &stream) { for (SerializerInterface *serializer : m_serializers) serializer->decode(stream); } CommandType RemoteCommand::commandType() const { return m_type; } void RemoteCommand::addSerializer(SerializerInterface *serializer) { m_serializers.append(serializer); } using CommandFacory = std::function<std::unique_ptr<RemoteCommand>()>; #define ADDFACTORY(COMMAND) \ factories.insert(CommandType::COMMAND, \ []() { return std::unique_ptr<RemoteCommand>(new COMMAND); }) std::unique_ptr<RemoteCommand> RemoteCommand::create(CommandType id) { static QMap<CommandType, CommandFacory> factories; if (factories.isEmpty()) { ADDFACTORY(ThumbnailResult); ADDFACTORY(CategoryListResult); ADDFACTORY(SearchRequest); ADDFACTORY(SearchResult); ADDFACTORY(ThumbnailRequest); ADDFACTORY(ThumbnailCancelRequest); ADDFACTORY(TimeCommand); ADDFACTORY(ImageDetailsRequest); ADDFACTORY(ImageDetailsResult); ADDFACTORY(CategoryItemsResult); ADDFACTORY(StaticImageRequest); ADDFACTORY(StaticImageResult); ADDFACTORY(ToggleTokenRequest); } Q_ASSERT(factories.contains(id)); return factories[id](); } ThumbnailResult::ThumbnailResult(ImageId _imageId, const QString &_label, const QImage &_image, ViewType _type) : RemoteCommand(CommandType::ThumbnailResult) , imageId(_imageId) , label(_label) , image(_image) , type(_type) { addSerializer(new Serializer<ImageId>(imageId)); addSerializer(new Serializer<QString>(label)); addSerializer(new Serializer<QImage>(image)); addSerializer(new Serializer<ViewType>(type)); } QDataStream &operator<<(QDataStream &stream, const Category &category) { stream << category.name << category.enabled << (int)category.viewType; fastStreamImage(stream, category.icon, BackgroundType::Transparent); return stream; } QDataStream &operator>>(QDataStream &stream, Category &category) { int tmp; stream >> category.name >> category.enabled >> tmp; category.viewType = static_cast<RemoteControl::CategoryViewType>(tmp); category.icon.load(stream.device(), "JPEG"); return stream; } CategoryListResult::CategoryListResult() : RemoteCommand(CommandType::CategoryListResult) { addSerializer(new Serializer<QList<Category>>(categories)); } SearchRequest::SearchRequest(SearchType _type, const SearchInfo &_searchInfo, int _size) : RemoteCommand(CommandType::SearchRequest) , type(_type) , searchInfo(_searchInfo) , size(_size) { addSerializer(new Serializer<SearchType>(type)); addSerializer(new Serializer<SearchInfo>(searchInfo)); addSerializer(new Serializer<int>(size)); } SearchResult::SearchResult(SearchType _type, const QList<int> &_result) : RemoteCommand(CommandType::SearchResult) , type(_type) , result(_result) { addSerializer(new Serializer<SearchType>(type)); addSerializer(new Serializer<QList<int>>(result)); } ThumbnailRequest::ThumbnailRequest(ImageId _imageId, const QSize &_size, ViewType _type) : RemoteCommand(CommandType::ThumbnailRequest) , imageId(_imageId) , size(_size) , type(_type) { addSerializer(new Serializer<ImageId>(imageId)); addSerializer(new Serializer<QSize>(size)); addSerializer(new Serializer<ViewType>(type)); } RemoteControl::ThumbnailCancelRequest::ThumbnailCancelRequest(ImageId _imageId, ViewType _type) : RemoteCommand(CommandType::ThumbnailCancelRequest) , imageId(_imageId) , type(_type) { addSerializer(new Serializer<ImageId>(imageId)); addSerializer(new Serializer<ViewType>(type)); } TimeCommand::TimeCommand() : RemoteCommand(CommandType::TimeCommand) { } static void printElapsed() { static QElapsedTimer timer; qDebug() << "Time since last dump: " << timer.elapsed(); timer.restart(); } void TimeCommand::encode(QDataStream &) const { printElapsed(); } void TimeCommand::decode(QDataStream &) { printElapsed(); } ImageDetailsRequest::ImageDetailsRequest(ImageId _imageId) : RemoteCommand(CommandType::ImageDetailsRequest) , imageId(_imageId) { addSerializer(new Serializer<ImageId>(imageId)); } ImageDetailsResult::ImageDetailsResult() : RemoteCommand(CommandType::ImageDetailsResult) { addSerializer(new Serializer<QString>(fileName)); addSerializer(new Serializer<QString>(date)); addSerializer(new Serializer<QString>(description)); addSerializer(new Serializer<QMap<QString, CategoryItemDetailsList>>(categories)); } //// WHAT WHAT WHAT QDataStream &operator<<(QDataStream &stream, const CategoryItemDetails &item) { stream << item.name << item.age; return stream; } QDataStream &operator>>(QDataStream &stream, CategoryItemDetails &item) { stream >> item.name >> item.age; return stream; } CategoryItemsResult::CategoryItemsResult(const QStringList &_items) : RemoteCommand(CommandType::CategoryItemsResult) , items(_items) { addSerializer(new Serializer<QStringList>(items)); } StaticImageRequest::StaticImageRequest(int _size) : RemoteCommand(CommandType::StaticImageRequest) , size(_size) { addSerializer(new Serializer<int>(size)); } StaticImageResult::StaticImageResult(const QImage &_homeIcon, const QImage &_kphotoalbumIcon, const QImage &_discoverIcon) : RemoteCommand(CommandType::StaticImageResult) , homeIcon(_homeIcon) , kphotoalbumIcon(_kphotoalbumIcon) , discoverIcon(_discoverIcon) { addSerializer(new Serializer<QImage>(homeIcon, BackgroundType::Transparent)); addSerializer(new Serializer<QImage>(kphotoalbumIcon, BackgroundType::Transparent)); addSerializer(new Serializer<QImage>(discoverIcon, BackgroundType::Transparent)); } ToggleTokenRequest::ToggleTokenRequest(ImageId _imageId, const QString &_token, State _state) : RemoteCommand(CommandType::ToggleTokenRequest) , imageId(_imageId) , token(_token) , state(_state) { addSerializer(new Serializer<ImageId>(imageId)); addSerializer(new Serializer<QString>(token)); addSerializer(new Serializer<State>(state)); } diff --git a/RemoteControl/RemoteCommand.h b/RemoteControl/RemoteCommand.h index 393b42c2..e8c64063 100644 --- a/RemoteControl/RemoteCommand.h +++ b/RemoteControl/RemoteCommand.h @@ -1,204 +1,204 @@ /* Copyright (C) 2014 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. */ #ifndef REMOTECOMMAND_H #define REMOTECOMMAND_H #include "SearchInfo.h" - #include "Types.h" + #include <QBuffer> #include <QDataStream> #include <QImage> #include <QMap> #include <QPainter> #include <QPair> #include <QString> #include <QStringList> #include <memory> namespace RemoteControl { class SerializerInterface; const int VERSION = 7; enum class CommandType { ThumbnailResult, CategoryListResult, SearchRequest, SearchResult, ThumbnailRequest, ThumbnailCancelRequest, TimeCommand, ImageDetailsRequest, ImageDetailsResult, CategoryItemsResult, StaticImageRequest, StaticImageResult, ToggleTokenRequest }; class RemoteCommand { public: RemoteCommand(CommandType type); virtual ~RemoteCommand(); virtual void encode(QDataStream &) const; virtual void decode(QDataStream &); CommandType commandType() const; void addSerializer(SerializerInterface *serializer); static std::unique_ptr<RemoteCommand> create(CommandType commandType); private: QList<SerializerInterface *> m_serializers; CommandType m_type; }; class ThumbnailResult : public RemoteCommand { public: ThumbnailResult(ImageId imageId = {}, const QString &label = {}, const QImage &image = QImage(), ViewType type = {}); ImageId imageId; QString label; QImage image; ViewType type; }; struct Category { QString name; QImage icon; bool enabled; CategoryViewType viewType; }; class CategoryListResult : public RemoteCommand { public: CategoryListResult(); QList<Category> categories; }; class SearchRequest : public RemoteCommand { public: SearchRequest(SearchType type = {}, const SearchInfo &searchInfo = {}, int size = {}); SearchType type; SearchInfo searchInfo; int size; // Only used for SearchType::Categories }; class SearchResult : public RemoteCommand { public: SearchResult(SearchType type = {}, const QList<int> &result = {}); SearchType type; QList<int> result; }; class ThumbnailRequest : public RemoteCommand { public: ThumbnailRequest(ImageId imageId = {}, const QSize &size = {}, ViewType type = {}); ImageId imageId; QSize size; ViewType type; }; class ThumbnailCancelRequest : public RemoteCommand { public: ThumbnailCancelRequest(ImageId imageId = {}, ViewType type = {}); ImageId imageId; ViewType type; }; class TimeCommand : public RemoteCommand { public: TimeCommand(); void encode(QDataStream &stream) const override; void decode(QDataStream &stream) override; }; class ImageDetailsRequest : public RemoteCommand { public: ImageDetailsRequest(ImageId imageId = {}); ImageId imageId; }; struct CategoryItemDetails { CategoryItemDetails(const QString &name = {}, const QString &age = {}) : name(name) , age(age) { } QString name; QString age; }; using CategoryItemDetailsList = QList<CategoryItemDetails>; class ImageDetailsResult : public RemoteCommand { public: ImageDetailsResult(); QString fileName; QString date; QString description; QMap<QString, CategoryItemDetailsList> categories; }; class CategoryItemsResult : public RemoteCommand { public: CategoryItemsResult(const QStringList &items = {}); QStringList items; }; class StaticImageRequest : public RemoteCommand { public: StaticImageRequest(int size = {}); int size; }; class StaticImageResult : public RemoteCommand { public: StaticImageResult(const QImage &homeIcon = {}, const QImage &kphotoalbumIcon = {}, const QImage &discoverIcon = {}); QImage homeIcon; QImage kphotoalbumIcon; QImage discoverIcon; }; class ToggleTokenRequest : public RemoteCommand { public: enum State { On, Off }; ToggleTokenRequest(ImageId imageId = {}, const QString &token = {}, State state = {}); ImageId imageId; QString token; State state; }; } #endif // REMOTECOMMAND_H diff --git a/RemoteControl/RemoteConnection.cpp b/RemoteControl/RemoteConnection.cpp index 5eb86c7a..0a2e8a28 100644 --- a/RemoteControl/RemoteConnection.cpp +++ b/RemoteControl/RemoteConnection.cpp @@ -1,115 +1,116 @@ /* Copyright (C) 2014 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 "RemoteConnection.h" + #include "RemoteCommand.h" #include <QApplication> #include <QBuffer> #include <QTcpSocket> #include <QThread> #include <QTime> #if 0 #define protocolDebug qDebug #else #define protocolDebug \ if (false) \ qDebug #endif using namespace RemoteControl; RemoteConnection::RemoteConnection(QObject *parent) : QObject(parent) { } void RemoteConnection::sendCommand(const RemoteCommand &command) { protocolDebug() << qPrintable(QTime::currentTime().toString(QString::fromUtf8("hh:mm:ss.zzz"))) << ": Sending " << QString::number((int)command.commandType()); Q_ASSERT(QThread::currentThread() == qApp->thread()); if (!isConnected()) return; // Stream into a buffer so we can send length of buffer over // this is to ensure the remote side gets all data before it // starts to demarshal the data. QBuffer buffer; buffer.open(QIODevice::WriteOnly); QDataStream stream(&buffer); // stream a placeholder for the length stream << (qint32)0; // Steam the id and the data stream << (qint32)command.commandType(); command.encode(stream); // Wind back and stream the length stream.device()->seek(0); stream << (qint32)buffer.size(); // Send the data. socket()->write(buffer.data()); socket()->flush(); } void RemoteConnection::dataReceived() { QTcpSocket *socket = this->socket(); if (!socket) return; QDataStream stream(socket); while (socket->bytesAvailable()) { if (m_state == WaitingForLength) { if (socket->bytesAvailable() < (qint64)sizeof(qint32)) return; stream >> m_length; m_length -= sizeof(qint32); m_state = WaitingForData; } if (m_state == WaitingForData) { if (socket->bytesAvailable() < m_length) return; m_state = WaitingForLength; QByteArray data = socket->read(m_length); Q_ASSERT(data.length() == m_length); QBuffer buffer(&data); buffer.open(QIODevice::ReadOnly); QDataStream stream(&buffer); qint32 id; stream >> id; std::unique_ptr<RemoteCommand> command = RemoteCommand::create(static_cast<CommandType>(id)); command->decode(stream); protocolDebug() << qPrintable(QTime::currentTime().toString(QString::fromUtf8("hh:mm:ss.zzz"))) << ": Received " << id; emit gotCommand(*command); } } } diff --git a/RemoteControl/RemoteImageRequest.h b/RemoteControl/RemoteImageRequest.h index 45ac92d6..90f35aaa 100644 --- a/RemoteControl/RemoteImageRequest.h +++ b/RemoteControl/RemoteImageRequest.h @@ -1,43 +1,44 @@ /* Copyright (C) 2014 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. */ #ifndef REMOTECONTROL_REMOTEIMAGEREQUEST_H #define REMOTECONTROL_REMOTEIMAGEREQUEST_H -#include "ImageManager/ImageRequest.h" #include "RemoteInterface.h" #include "Types.h" +#include <ImageManager/ImageRequest.h> + namespace RemoteControl { class RemoteImageRequest : public ImageManager::ImageRequest { public: RemoteImageRequest(const DB::FileName &fileName, const QSize &size, int angle, ViewType type, RemoteInterface *client); bool stillNeeded() const override; ViewType type() const; private: RemoteInterface *m_interface; ViewType m_type; }; } // namespace RemoteControl #endif // REMOTECONTROL_REMOTEIMAGEREQUEST_H diff --git a/RemoteControl/RemoteInterface.cpp b/RemoteControl/RemoteInterface.cpp index 2080ad6d..36f213f8 100644 --- a/RemoteControl/RemoteInterface.cpp +++ b/RemoteControl/RemoteInterface.cpp @@ -1,270 +1,268 @@ /* Copyright (C) 2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "RemoteInterface.h" -#include <algorithm> -#include <tuple> +#include "RemoteCommand.h" +#include "RemoteImageRequest.h" +#include "Server.h" +#include "Types.h" + +#include <Browser/FlatCategoryModel.h> +#include <DB/Category.h> +#include <DB/CategoryCollection.h> +#include <DB/CategoryPtr.h> +#include <DB/ImageDB.h> +#include <DB/ImageInfo.h> +#include <DB/ImageInfoPtr.h> +#include <DB/ImageSearchInfo.h> +#include <ImageManager/AsyncLoader.h> +#include <MainWindow/DirtyIndicator.h> +#include <Utilities/DescriptionUtil.h> +#include <KLocalizedString> #include <QBuffer> #include <QDataStream> #include <QImage> #include <QPainter> #include <QTcpSocket> - -#include <KLocalizedString> +#include <algorithm> #include <kiconloader.h> - -#include "Browser/FlatCategoryModel.h" -#include "DB/Category.h" -#include "DB/CategoryCollection.h" -#include "DB/CategoryPtr.h" -#include "DB/ImageDB.h" -#include "DB/ImageInfo.h" -#include "DB/ImageInfoPtr.h" -#include "DB/ImageSearchInfo.h" -#include "ImageManager/AsyncLoader.h" -#include "MainWindow/DirtyIndicator.h" -#include "Utilities/DescriptionUtil.h" - -#include "RemoteCommand.h" -#include "RemoteImageRequest.h" -#include "Server.h" -#include "Types.h" +#include <tuple> using namespace RemoteControl; RemoteInterface &RemoteInterface::instance() { static RemoteInterface instance; return instance; } RemoteInterface::RemoteInterface(QObject *parent) : QObject(parent) , m_connection(new Server(this)) { connect(m_connection, SIGNAL(gotCommand(RemoteCommand)), this, SLOT(handleCommand(RemoteCommand))); connect(m_connection, SIGNAL(connected()), this, SIGNAL(connected())); connect(m_connection, SIGNAL(disConnected()), this, SIGNAL(disConnected())); connect(m_connection, SIGNAL(stoppedListening()), this, SIGNAL(stoppedListening())); } DB::ImageSearchInfo RemoteInterface::convert(const SearchInfo &searchInfo) const { DB::ImageSearchInfo dbSearchInfo; QString category; QString value; for (auto item : searchInfo.values()) { std::tie(category, value) = item; dbSearchInfo.addAnd(category, value); } return dbSearchInfo; } void RemoteInterface::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) { m_connection->sendCommand(ThumbnailResult(m_imageNameStore[request->databaseFileName()], QString(), image, static_cast<RemoteImageRequest *>(request)->type())); } bool RemoteInterface::requestStillNeeded(const DB::FileName &fileName) { return m_activeReuqest.contains(fileName); } void RemoteInterface::listen(QHostAddress address) { m_connection->listen(address); emit listening(); } void RemoteInterface::stopListening() { m_connection->stopListening(); } void RemoteInterface::connectTo(const QHostAddress &address) { m_connection->connectToTcpServer(address); } void RemoteInterface::handleCommand(const RemoteCommand &command) { if (command.commandType() == CommandType::SearchRequest) { const SearchRequest &searchCommand = static_cast<const SearchRequest &>(command); if (searchCommand.type == SearchType::Categories) sendCategoryNames(searchCommand); else if (searchCommand.type == SearchType::CategoryItems) sendCategoryValues(searchCommand); else sendImageSearchResult(searchCommand.searchInfo); } else if (command.commandType() == CommandType::ThumbnailRequest) requestThumbnail(static_cast<const ThumbnailRequest &>(command)); else if (command.commandType() == CommandType::ThumbnailCancelRequest) cancelRequest(static_cast<const ThumbnailCancelRequest &>(command)); else if (command.commandType() == CommandType::ImageDetailsRequest) sendImageDetails(static_cast<const ImageDetailsRequest &>(command)); else if (command.commandType() == CommandType::StaticImageRequest) sendHomePageImages(static_cast<const StaticImageRequest &>(command)); else if (command.commandType() == CommandType::ToggleTokenRequest) setToken(static_cast<const ToggleTokenRequest &>(command)); } void RemoteInterface::sendCategoryNames(const SearchRequest &search) { const DB::ImageSearchInfo dbSearchInfo = convert(search.searchInfo); CategoryListResult command; for (const DB::CategoryPtr &category : DB::ImageDB::instance()->categoryCollection()->categories()) { if (category->type() == DB::Category::MediaTypeCategory) continue; QMap<QString, DB::CountWithRange> images = DB::ImageDB::instance()->classify(dbSearchInfo, category->name(), DB::Image); QMap<QString, DB::CountWithRange> videos = DB::ImageDB::instance()->classify(dbSearchInfo, category->name(), DB::Video); const bool enabled = (images.count() /*+ videos.count()*/ > 1); CategoryViewType type = (category->viewType() == DB::Category::IconView || category->viewType() == DB::Category::ThumbedIconView) ? Types::CategoryIconView : Types::CategoryListView; const QImage icon = category->icon(search.size, enabled ? KIconLoader::DefaultState : KIconLoader::DisabledState).toImage(); command.categories.append({ category->name(), icon, enabled, type }); } m_connection->sendCommand(command); } void RemoteInterface::sendCategoryValues(const SearchRequest &search) { const DB::ImageSearchInfo dbSearchInfo = convert(search.searchInfo); const QString categoryName = search.searchInfo.currentCategory(); const DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(search.searchInfo.currentCategory()); Browser::FlatCategoryModel model(category, dbSearchInfo); if (category->viewType() == DB::Category::IconView || category->viewType() == DB::Category::ThumbedIconView) { QList<int> result; std::transform(model.m_items.begin(), model.m_items.end(), std::back_inserter(result), [this, categoryName](const QString itemName) { return m_imageNameStore.idForCategory(categoryName, itemName); }); m_connection->sendCommand(SearchResult(SearchType::CategoryItems, result)); } else { m_connection->sendCommand(CategoryItemsResult(model.m_items)); } } void RemoteInterface::sendImageSearchResult(const SearchInfo &search) { const DB::FileNameList files = DB::ImageDB::instance()->search(convert(search), true /* Require on disk */); DB::FileNameList stacksRemoved; QList<int> result; std::remove_copy_if(files.begin(), files.end(), std::back_inserter(stacksRemoved), [](const DB::FileName &file) { // Only include unstacked images, and the top of stacked images. // And also exclude videos return DB::ImageDB::instance()->info(file)->stackOrder() > 1 || DB::ImageDB::instance()->info(file)->isVideo(); }); std::transform(stacksRemoved.begin(), stacksRemoved.end(), std::back_inserter(result), [this](const DB::FileName &fileName) { return m_imageNameStore[fileName]; }); m_connection->sendCommand(SearchResult(SearchType::Images, result)); } void RemoteInterface::requestThumbnail(const ThumbnailRequest &command) { if (command.type == ViewType::CategoryItems) { auto tuple = m_imageNameStore.categoryForId(command.imageId); QString categoryName = tuple.first; QString itemName = tuple.second; const DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(categoryName); QImage image = category->categoryImage(categoryName, itemName, command.size.width(), command.size.height()).toImage(); m_connection->sendCommand(ThumbnailResult(command.imageId, itemName, image, ViewType::CategoryItems)); } else { const DB::FileName fileName = m_imageNameStore[command.imageId]; const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); const int angle = info->angle(); m_activeReuqest.insert(fileName); QSize size = command.size; if (!size.isValid()) { // Request for full screen image. size = info->size(); } RemoteImageRequest *request = new RemoteImageRequest(fileName, size, angle, command.type, this); ImageManager::AsyncLoader::instance()->load(request); } } void RemoteInterface::cancelRequest(const ThumbnailCancelRequest &command) { m_activeReuqest.remove(m_imageNameStore[command.imageId]); } void RemoteInterface::sendImageDetails(const ImageDetailsRequest &command) { const DB::FileName fileName = m_imageNameStore[command.imageId]; const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); ImageDetailsResult result; result.fileName = fileName.relative(); result.date = info->date().toString(); result.description = info->description(); result.categories.clear(); for (const QString &categoryName : info->availableCategories()) { DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(categoryName); CategoryItemDetailsList list; for (const QString &item : info->itemsOfCategory(categoryName)) { const QString age = Utilities::formatAge(category, item, info); list.append(CategoryItemDetails(item, age)); } result.categories[categoryName] = list; } m_connection->sendCommand(result); } void RemoteInterface::sendHomePageImages(const StaticImageRequest &command) { const int size = command.size; QPixmap homeIcon = KIconLoader::global()->loadIcon(QString::fromUtf8("go-home"), KIconLoader::Desktop, size); QPixmap kphotoalbumIcon = KIconLoader::global()->loadIcon(QString::fromUtf8("kphotoalbum"), KIconLoader::Desktop, size); QPixmap discoverIcon = KIconLoader::global()->loadIcon(QString::fromUtf8("edit-find"), KIconLoader::Desktop, size); m_connection->sendCommand(StaticImageResult(homeIcon.toImage(), kphotoalbumIcon.toImage(), discoverIcon.toImage())); } void RemoteInterface::setToken(const ToggleTokenRequest &command) { const DB::FileName fileName = m_imageNameStore[command.imageId]; DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); if (command.state == ToggleTokenRequest::On) info->addCategoryInfo(tokensCategory->name(), command.token); else info->removeCategoryInfo(tokensCategory->name(), command.token); MainWindow::DirtyIndicator::markDirty(); } diff --git a/RemoteControl/RemoteInterface.h b/RemoteControl/RemoteInterface.h index a3c86f26..43a56ddb 100644 --- a/RemoteControl/RemoteInterface.h +++ b/RemoteControl/RemoteInterface.h @@ -1,74 +1,76 @@ /* Copyright (C) 2014 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. */ #ifndef REMOTEINTERFACE_H #define REMOTEINTERFACE_H -#include "DB/ImageSearchInfo.h" -#include "ImageManager/ImageClientInterface.h" #include "ImageNameStore.h" #include "RemoteCommand.h" + +#include <DB/ImageSearchInfo.h> +#include <ImageManager/ImageClientInterface.h> + #include <QHostAddress> #include <QObject> class QHostAddress; namespace RemoteControl { class Server; class RemoteInterface : public QObject, public ImageManager::ImageClientInterface { Q_OBJECT public: static RemoteInterface &instance(); void pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) override; bool requestStillNeeded(const DB::FileName &fileName); void listen(QHostAddress address = QHostAddress::Any); void stopListening(); void connectTo(const QHostAddress &address); private slots: void handleCommand(const RemoteCommand &); signals: void connected(); void disConnected(); void listening(); void stoppedListening(); private: explicit RemoteInterface(QObject *parent = 0); void sendCategoryNames(const SearchRequest &searchInfo); void sendCategoryValues(const SearchRequest &search); void sendImageSearchResult(const SearchInfo &search); void requestThumbnail(const ThumbnailRequest &command); void cancelRequest(const ThumbnailCancelRequest &command); void sendImageDetails(const ImageDetailsRequest &command); void sendHomePageImages(const StaticImageRequest &command); void setToken(const ToggleTokenRequest &command); DB::ImageSearchInfo convert(const RemoteControl::SearchInfo &) const; Server *m_connection; QSet<DB::FileName> m_activeReuqest; ImageNameStore m_imageNameStore; }; } #endif // REMOTEINTERFACE_H diff --git a/RemoteControl/SearchInfo.h b/RemoteControl/SearchInfo.h index 997657d0..690cd915 100644 --- a/RemoteControl/SearchInfo.h +++ b/RemoteControl/SearchInfo.h @@ -1,54 +1,53 @@ /* Copyright (C) 2014 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. */ #ifndef REMOTECONTROL_SEARCHINFO_H #define REMOTECONTROL_SEARCHINFO_H #include <QDataStream> #include <QStack> #include <QString> - #include <tuple> namespace RemoteControl { class SearchInfo { public: void addCategory(const QString &category); void addValue(const QString &value); void pop(); void clear(); QString currentCategory() const; QList<std::tuple<QString, QString>> values() const; friend QDataStream &operator<<(QDataStream &stream, const SearchInfo &searchInfo); friend QDataStream &operator>>(QDataStream &stream, SearchInfo &searchInfo); private: QStack<QString> m_categories; QStack<QString> m_values; }; QDataStream &operator<<(QDataStream &stream, const SearchInfo &searchInfo); QDataStream &operator>>(QDataStream &stream, SearchInfo &searchInfo); } // namespace RemoteControl #endif // REMOTECONTROL_SEARCHINFO_H diff --git a/RemoteControl/Server.cpp b/RemoteControl/Server.cpp index 49ee16d2..8eb62a42 100644 --- a/RemoteControl/Server.cpp +++ b/RemoteControl/Server.cpp @@ -1,112 +1,113 @@ /* Copyright (C) 2014 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 "Server.h" #include "RemoteCommand.h" + #include <KLocalizedString> #include <QMessageBox> #include <QTcpSocket> #include <QUdpSocket> using namespace RemoteControl; Server::Server(QObject *parent) : RemoteConnection(parent) { } bool Server::isConnected() const { return m_isConnected; } void Server::listen(QHostAddress address) { if (!m_socket) { m_socket = new QUdpSocket(this); bool ok = m_socket->bind(address, UDPPORT); if (!ok) { QMessageBox::critical(0, i18n("Unable to bind to socket"), i18n("Unable to listen for remote Android connections. " "This is likely because you have another KPhotoAlbum application running.")); } connect(m_socket, SIGNAL(readyRead()), this, SLOT(readIncommingUDP())); } } void Server::stopListening() { delete m_socket; m_socket = nullptr; delete m_tcpSocket; m_tcpSocket = nullptr; emit stoppedListening(); } QTcpSocket *Server::socket() { return m_tcpSocket; } void Server::readIncommingUDP() { Q_ASSERT(m_socket->hasPendingDatagrams()); char data[1000]; QHostAddress address; qint64 len = m_socket->readDatagram(data, 1000, &address); QString string = QString::fromUtf8(data).left(len); QStringList list = string.split(QChar::fromLatin1(' ')); if (list[0] != QString::fromUtf8("KPhotoAlbum")) { return; } if (list[1] != QString::number(RemoteControl::VERSION)) { QMessageBox::critical(0, i18n("Invalid Version"), i18n("Version mismatch between Remote Client and KPhotoAlbum on the desktop.\n" "Desktop protocol version: %1\n" "Remote Control protocol version: %2", RemoteControl::VERSION, list[1])); stopListening(); return; } connectToTcpServer(address); } void Server::connectToTcpServer(const QHostAddress &address) { m_tcpSocket = new QTcpSocket; connect(m_tcpSocket, SIGNAL(connected()), this, SLOT(gotConnected())); connect(m_tcpSocket, SIGNAL(readyRead()), this, SLOT(dataReceived())); m_tcpSocket->connectToHost(address, TCPPORT); connect(m_tcpSocket, SIGNAL(disconnected()), this, SLOT(lostConnection())); } void Server::gotConnected() { m_isConnected = true; emit connected(); } void Server::lostConnection() { m_isConnected = false; emit disConnected(); } diff --git a/Settings/BirthdayPage.cpp b/Settings/BirthdayPage.cpp index 35ea41a1..30fc37f8 100644 --- a/Settings/BirthdayPage.cpp +++ b/Settings/BirthdayPage.cpp @@ -1,335 +1,336 @@ /* Copyright (C) 2014-2015 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. */ // Qt includes #include <QCalendarWidget> #include <QComboBox> #include <QHBoxLayout> #include <QHeaderView> #include <QLabel> #include <QLineEdit> #include <QLocale> #include <QPushButton> #include <QShortcut> #include <QTableWidget> #include <QVBoxLayout> // KDE includes #include <KLocalizedString> #include <KPageWidgetModel> // Local includes #include "BirthdayPage.h" -#include "DB/Category.h" -#include "DB/CategoryCollection.h" -#include "DB/ImageDB.h" #include "DateTableWidgetItem.h" -#include "MainWindow/DirtyIndicator.h" + +#include <DB/Category.h> +#include <DB/CategoryCollection.h> +#include <DB/ImageDB.h> +#include <MainWindow/DirtyIndicator.h> Settings::BirthdayPage::BirthdayPage(QWidget *parent) : QWidget(parent) { QVBoxLayout *mainLayout = new QVBoxLayout(this); QHBoxLayout *dataLayout = new QHBoxLayout; mainLayout->addLayout(dataLayout); QVBoxLayout *itemsLayout = new QVBoxLayout; dataLayout->addLayout(itemsLayout); QHBoxLayout *itemsHeaderLayout = new QHBoxLayout; itemsLayout->addLayout(itemsHeaderLayout); QLabel *categoryText = new QLabel(i18n("Category:")); itemsHeaderLayout->addWidget(categoryText); m_categoryBox = new QComboBox; itemsHeaderLayout->addWidget(m_categoryBox); connect(m_categoryBox, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &BirthdayPage::changeCategory); m_filter = new QLineEdit; m_filter->setPlaceholderText(i18n("Filter (Alt+f)")); itemsHeaderLayout->addWidget(m_filter); connect(m_filter, &QLineEdit::textChanged, this, &BirthdayPage::resetCategory); new QShortcut(Qt::AltModifier + Qt::Key_F, m_filter, SLOT(setFocus())); if (QLocale().dateFormat(QLocale::ShortFormat).contains(QString::fromUtf8("yyyy"))) { m_dateFormats << QLocale().dateFormat(QLocale::ShortFormat); } else { m_dateFormats << QLocale().dateFormat(QLocale::ShortFormat).replace(QString::fromUtf8("yy"), QString::fromUtf8("yyyy")); } m_dateFormats << QLocale().dateFormat(QLocale::ShortFormat) << QLocale().dateFormat(QLocale::LongFormat); m_dataView = new QTableWidget; m_dataView->setColumnCount(2); m_dataView->verticalHeader()->hide(); m_dataView->setShowGrid(false); itemsLayout->addWidget(m_dataView); connect(m_dataView, &QTableWidget::cellClicked, this, &BirthdayPage::editDate); QVBoxLayout *calendarLayout = new QVBoxLayout; dataLayout->addLayout(calendarLayout); calendarLayout->addStretch(); m_birthdayOfLabel = new QLabel; calendarLayout->addWidget(m_birthdayOfLabel); m_dateInput = new QLineEdit; calendarLayout->addWidget(m_dateInput); connect(m_dateInput, &QLineEdit::textEdited, this, &BirthdayPage::checkDateInput); connect(m_dateInput, &QLineEdit::editingFinished, this, &BirthdayPage::checkDate); m_calendar = new QCalendarWidget; calendarLayout->addWidget(m_calendar); connect(m_calendar, &QCalendarWidget::clicked, this, &BirthdayPage::setDate); m_unsetButton = new QPushButton(i18n("Remove birthday")); calendarLayout->addWidget(m_unsetButton); connect(m_unsetButton, &QPushButton::clicked, this, &BirthdayPage::removeDate); calendarLayout->addStretch(); QLabel *info = new QLabel(i18n("Set the date of birth for items (say people) here, " "and then see their age when viewing the images.")); mainLayout->addWidget(info); m_noDateString = QString::fromUtf8("---"); m_boldFont.setBold(true); disableCalendar(); } void Settings::BirthdayPage::pageChange(KPageWidgetItem *page) { if (page->widget() == this) { m_lastItem = nullptr; reload(); } } void Settings::BirthdayPage::reload() { m_dateInput->setText(QString()); m_calendar->setSelectedDate(QDate::currentDate()); disableCalendar(); m_categoryBox->blockSignals(true); m_categoryBox->clear(); int defaultIndex = 0; int index = 0; for (const DB::CategoryPtr &category : DB::ImageDB::instance()->categoryCollection()->categories()) { if (category->isSpecialCategory()) { continue; } m_categoryBox->addItem(category->name()); if (category->name() == i18n("People")) { defaultIndex = index; } ++index; } m_categoryBox->setCurrentIndex(defaultIndex); changeCategory(defaultIndex); m_categoryBox->blockSignals(false); } void Settings::BirthdayPage::resetCategory() { changeCategory(m_categoryBox->currentIndex()); } void Settings::BirthdayPage::changeCategory(int index) { m_lastItem = nullptr; m_dataView->clear(); m_dataView->setSortingEnabled(false); m_dataView->setHorizontalHeaderLabels(QStringList() << i18n("Name") << i18n("Birthday")); const QString categoryName = m_categoryBox->itemText(index); const DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(categoryName); QStringList items = category->items(); m_dataView->setRowCount(items.count()); int row = 0; for (const QString &text : items) { if (!m_filter->text().isEmpty() && text.indexOf(m_filter->text(), 0, Qt::CaseInsensitive) == -1) { m_dataView->setRowCount(m_dataView->rowCount() - 1); continue; } QTableWidgetItem *nameItem = new QTableWidgetItem(text); nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable & ~Qt::ItemIsSelectable); m_dataView->setItem(row, 0, nameItem); QDate dateForItem; if (m_changedData.contains(categoryName)) { if (m_changedData[categoryName].contains(text)) { dateForItem = m_changedData[categoryName][text]; } else { dateForItem = category->birthDate(text); } } else { dateForItem = category->birthDate(text); } DateTableWidgetItem *dateItem = new DateTableWidgetItem(textForDate(dateForItem)); dateItem->setData(Qt::UserRole, dateForItem); dateItem->setFlags(dateItem->flags() & ~Qt::ItemIsEditable & ~Qt::ItemIsSelectable); m_dataView->setItem(row, 1, dateItem); row++; } m_dataView->setSortingEnabled(true); m_dataView->sortItems(0); disableCalendar(); } QString Settings::BirthdayPage::textForDate(const QDate &date) const { if (date.isNull()) { return m_noDateString; } else { return QLocale().toString(date, m_dateFormats.at(0)); } } void Settings::BirthdayPage::editDate(int row, int) { m_dateInput->setEnabled(true); m_calendar->setEnabled(true); m_unsetButton->setEnabled(m_dataView->item(row, 1)->text() != m_noDateString); if (m_lastItem != nullptr) { m_lastItem->setFont(m_font); m_dataView->item(m_lastItem->row(), 1)->setFont(m_font); } m_dataView->item(row, 0)->setFont(m_boldFont); m_dataView->item(row, 1)->setFont(m_boldFont); m_birthdayOfLabel->setText(i18n("Birthday of %1:", m_dataView->item(row, 0)->text())); QString dateString = m_dataView->item(row, 1)->text(); if (dateString != m_noDateString) { m_dateInput->setText(dateString); m_calendar->setSelectedDate(m_dataView->item(row, 1)->data(Qt::UserRole).toDate()); } else { m_dateInput->setText(QString()); m_dateInput->setPlaceholderText(i18n("Enter a date...")); m_calendar->setSelectedDate(QDate::currentDate()); } m_lastItem = m_dataView->item(row, 0); } QDate Settings::BirthdayPage::parseDate(QString date) { QDate parsedDate = QDate(); for (const QString &format : m_dateFormats) { parsedDate = QDate::fromString(date, format); if (parsedDate.isValid()) { return parsedDate; } } return parsedDate; } void Settings::BirthdayPage::checkDateInput(QString date) { QDate parsedDate = parseDate(date); if (parsedDate.isValid()) { m_calendar->setSelectedDate(parsedDate); m_dateInput->setStyleSheet(QString()); } else { m_dateInput->setStyleSheet(QString::fromUtf8("color:red;")); } } void Settings::BirthdayPage::checkDate() { QDate parsedDate = parseDate(m_dateInput->text()); if (parsedDate.isValid()) { setDate(parsedDate); } } void Settings::BirthdayPage::setDate(const QDate &date) { const QString currentCategory = m_categoryBox->currentText(); if (!m_changedData.contains(currentCategory)) { m_changedData[currentCategory] = QMap<QString, QDate>(); } const QString currentItem = m_dataView->item(m_dataView->currentRow(), 0)->text(); m_changedData[currentCategory][currentItem] = date; m_dataView->item(m_dataView->currentRow(), 1)->setText(textForDate(date)); m_dataView->item(m_dataView->currentRow(), 1)->setData(Qt::UserRole, date); m_unsetButton->setEnabled(true); } void Settings::BirthdayPage::disableCalendar() { m_dateInput->setEnabled(false); m_calendar->setEnabled(false); m_unsetButton->setEnabled(false); m_birthdayOfLabel->setText(i18n("<i>Select an item on the left to edit the birthday</i>")); } void Settings::BirthdayPage::discardChanges() { m_changedData.clear(); } void Settings::BirthdayPage::saveSettings() { QMapIterator<QString, QMap<QString, QDate>> changedCategory(m_changedData); while (changedCategory.hasNext()) { changedCategory.next(); DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(changedCategory.key()); QMapIterator<QString, QDate> changedItem(changedCategory.value()); while (changedItem.hasNext()) { changedItem.next(); category->setBirthDate(changedItem.key(), changedItem.value()); } } if (m_changedData.size() > 0) { MainWindow::DirtyIndicator::markDirty(); m_changedData.clear(); } } void Settings::BirthdayPage::removeDate() { m_dateInput->setText(QString()); m_calendar->setSelectedDate(QDate::currentDate()); setDate(QDate()); } diff --git a/Settings/CategoriesGroupsWidget.cpp b/Settings/CategoriesGroupsWidget.cpp index fbbb4e61..07d35d02 100644 --- a/Settings/CategoriesGroupsWidget.cpp +++ b/Settings/CategoriesGroupsWidget.cpp @@ -1,116 +1,117 @@ /* Copyright (C) 2014 Tobias Leupold <tobias.leupold@web.de> 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 "CategoriesGroupsWidget.h" // Qt includes #include <QDropEvent> // Local includes -#include "DB/Category.h" #include "TagGroupsPage.h" +#include <DB/Category.h> + Settings::CategoriesGroupsWidget::CategoriesGroupsWidget(QWidget *parent) : QTreeWidget(parent) { setDragEnabled(true); setAcceptDrops(true); m_tagGroupsPage = dynamic_cast<TagGroupsPage *>(parentWidget()); m_oldTarget = nullptr; } Settings::CategoriesGroupsWidget::~CategoriesGroupsWidget() { } void Settings::CategoriesGroupsWidget::mousePressEvent(QMouseEvent *event) { m_draggedItem = itemAt(event->pos()); if (m_draggedItem != nullptr) { m_backgroundNoTarget = m_draggedItem->background(0); if (m_draggedItem->parent() != nullptr) { m_draggedItemCategory = m_tagGroupsPage->getCategory(m_draggedItem); } } else { m_draggedItemCategory = QString(); } QTreeWidget::mousePressEvent(event); } void Settings::CategoriesGroupsWidget::dragMoveEvent(QDragMoveEvent *event) { QTreeWidgetItem *target = itemAt(event->pos()); if (target == nullptr) { // We don't have a target, so we don't allow a drop. event->setDropAction(Qt::IgnoreAction); } else if (target->parent() == nullptr) { // The target is a category. It has to be the same one as dragged group's category, if (target->text(0) != m_draggedItemCategory) { event->setDropAction(Qt::IgnoreAction); } else { updateHighlight(target); event->setDropAction(Qt::MoveAction); event->accept(); } } else { // The target is another group. It has to be in the same category as the dragged group. QTreeWidgetItem *parent = target->parent(); ; while (parent->parent() != nullptr) { parent = parent->parent(); } if (parent->text(0) != m_draggedItemCategory) { event->setDropAction(Qt::IgnoreAction); } else { updateHighlight(target); event->setDropAction(Qt::MoveAction); event->accept(); } } } void Settings::CategoriesGroupsWidget::updateHighlight(QTreeWidgetItem *target) { if (target == m_oldTarget) { return; } if (m_oldTarget != nullptr) { m_oldTarget->setBackground(0, m_backgroundNoTarget); } target->setBackground(0, *(new QBrush(Qt::lightGray))); m_oldTarget = target; } void Settings::CategoriesGroupsWidget::dropEvent(QDropEvent *event) { QTreeWidgetItem *target = itemAt(event->pos()); target->setBackground(0, m_backgroundNoTarget); if (m_draggedItem != target) { m_tagGroupsPage->processDrop(m_draggedItem, target); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/CategoryItem.cpp b/Settings/CategoryItem.cpp index 28ca95eb..6872511c 100644 --- a/Settings/CategoryItem.cpp +++ b/Settings/CategoryItem.cpp @@ -1,189 +1,190 @@ /* Copyright (C) 2003-2014 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 "CategoryItem.h" // Qt includes #include <QDir> // KDE includes // Local includes +#include "SettingsData.h" + #include <DB/CategoryCollection.h> #include <DB/ImageDB.h> #include <DB/MemberMap.h> #include <MainWindow/DirtyIndicator.h> #include <MainWindow/Window.h> -#include <Settings/SettingsData.h> Settings::CategoryItem::CategoryItem(const QString &category, const QString &icon, DB::Category::ViewType type, int thumbnailSize, QListWidget *parent, bool positionable) : QListWidgetItem(category, parent) , m_categoryOrig(category) , m_iconOrig(icon) , m_positionable(positionable) , m_positionableOrig(positionable) , m_category(category) , m_icon(icon) , m_type(type) , m_typeOrig(type) , m_thumbnailSize(thumbnailSize) , m_thumbnailSizeOrig(thumbnailSize) { setFlags(flags() | Qt::ItemIsEditable); } void Settings::CategoryItem::setLabel(const QString &label) { setText(label); m_category = label; } void Settings::CategoryItem::submit(DB::MemberMap *memberMap) { if (m_categoryOrig.isNull()) { // New Item DB::ImageDB::instance()->categoryCollection()->addCategory(m_category, m_icon, m_type, m_thumbnailSize, true); MainWindow::DirtyIndicator::markDirty(); } else { DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_categoryOrig); if (m_category != m_categoryOrig) { renameCategory(memberMap); } if (m_positionable != m_positionableOrig) { category->setPositionable(m_positionable); } if (m_icon != m_iconOrig) { category->setIconName(m_icon); } if (m_type != m_typeOrig) { category->setViewType(m_type); } if (m_thumbnailSize != m_thumbnailSizeOrig) { category->setThumbnailSize(m_thumbnailSize); } } m_categoryOrig = m_category; m_iconOrig = m_icon; m_typeOrig = m_type; m_thumbnailSizeOrig = m_thumbnailSize; m_positionableOrig = m_positionable; } void Settings::CategoryItem::removeFromDatabase() { if (!m_categoryOrig.isNull()) { // The database knows about the item. DB::ImageDB::instance()->categoryCollection()->removeCategory(m_categoryOrig); } } bool Settings::CategoryItem::positionable() const { return m_positionable; } void Settings::CategoryItem::setPositionable(bool positionable) { m_positionable = positionable; } QString Settings::CategoryItem::icon() const { return m_icon; } int Settings::CategoryItem::thumbnailSize() const { return m_thumbnailSize; } DB::Category::ViewType Settings::CategoryItem::viewType() const { return m_type; } void Settings::CategoryItem::setIcon(const QString &icon) { m_icon = icon; } void Settings::CategoryItem::setThumbnailSize(int size) { m_thumbnailSize = size; } void Settings::CategoryItem::setViewType(DB::Category::ViewType type) { m_type = type; } void Settings::CategoryItem::renameCategory(DB::MemberMap *memberMap) { QDir dir(QString::fromLatin1("%1/CategoryImages").arg(Settings::SettingsData::instance()->imageDirectory())); const QStringList files = dir.entryList(QStringList() << QString::fromLatin1("%1*").arg(m_categoryOrig)); for (QStringList::ConstIterator fileNameIt = files.begin(); fileNameIt != files.end(); ++fileNameIt) { QString newName = m_category + (*fileNameIt).mid(m_categoryOrig.length()); dir.rename(*fileNameIt, newName); } // update category names for privacy-lock settings: Settings::SettingsData *settings = Settings::SettingsData::instance(); DB::ImageSearchInfo info = settings->currentLock(); const bool exclude = settings->lockExcludes(); info.renameCategory(m_categoryOrig, m_category); settings->setCurrentLock(info, exclude); DB::ImageDB::instance()->categoryCollection()->rename(m_categoryOrig, m_category); memberMap->renameCategory(m_categoryOrig, m_category); m_categoryOrig = m_category; } QString Settings::CategoryItem::originalName() const { return m_categoryOrig; } void Settings::CategoryItem::markAsNewCategory() { m_categoryOrig = QString(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/CategoryItem.h b/Settings/CategoryItem.h index 5547a261..1a52ca2c 100644 --- a/Settings/CategoryItem.h +++ b/Settings/CategoryItem.h @@ -1,86 +1,86 @@ /* Copyright (C) 2003-2014 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. */ #ifndef SETTINGS_CATEGORYITEM_H #define SETTINGS_CATEGORYITEM_H // Qt includes #include <QListWidgetItem> // Local includes -#include "DB/Category.h" +#include <DB/Category.h> namespace DB { // Local classes class MemberMap; } namespace Settings { class CategoryItem : public QObject, public QListWidgetItem { Q_OBJECT public: CategoryItem( const QString &category, const QString &icon, DB::Category::ViewType type, int thumbnailSize, QListWidget *parent, bool positionable = false); void setLabel(const QString &label); void setPositionable(bool positionable); void submit(DB::MemberMap *memberMap); void removeFromDatabase(); bool positionable() const; int thumbnailSize() const; DB::Category::ViewType viewType() const; void setThumbnailSize(int size); void setViewType(DB::Category::ViewType type); QString icon() const; void setIcon(const QString &icon); QString originalName() const; void markAsNewCategory(); protected: void renameCategory(DB::MemberMap *memberMap); private: // Variables QString m_categoryOrig; QString m_iconOrig; bool m_positionable; bool m_positionableOrig; QString m_category; QString m_text; QString m_icon; DB::Category::ViewType m_type; DB::Category::ViewType m_typeOrig; int m_thumbnailSize; int m_thumbnailSizeOrig; }; } #endif // SETTINGS_CATEGORYITEM_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/CategoryPage.cpp b/Settings/CategoryPage.cpp index 1fc402d1..79a165e8 100644 --- a/Settings/CategoryPage.cpp +++ b/Settings/CategoryPage.cpp @@ -1,564 +1,565 @@ /* Copyright (C) 2003-2014 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 "CategoryPage.h" // Qt includes #include <QCheckBox> #include <QComboBox> #include <QGridLayout> #include <QGroupBox> #include <QHBoxLayout> #include <QLabel> #include <QLocale> #include <QPushButton> #include <QSpinBox> #include <QVBoxLayout> // KDE includes #include <KIconButton> #include <KLocalizedString> #include <KMessageBox> // Local includes #include "CategoryItem.h" -#include "DB/CategoryCollection.h" -#include "DB/ImageDB.h" -#include "DB/MemberMap.h" -#include "MainWindow/DirtyIndicator.h" -#include "MainWindow/Window.h" #include "SettingsDialog.h" #include "UntaggedGroupBox.h" +#include <DB/CategoryCollection.h> +#include <DB/ImageDB.h> +#include <DB/MemberMap.h> +#include <MainWindow/DirtyIndicator.h> +#include <MainWindow/Window.h> + Settings::CategoryPage::CategoryPage(QWidget *parent) : QWidget(parent) { QVBoxLayout *mainLayout = new QVBoxLayout(this); // The category settings QGroupBox *categoryGroupBox = new QGroupBox; mainLayout->addWidget(categoryGroupBox); categoryGroupBox->setTitle(i18n("Category Settings")); QHBoxLayout *categoryLayout = new QHBoxLayout(categoryGroupBox); // Category list QVBoxLayout *categorySideLayout = new QVBoxLayout; categoryLayout->addLayout(categorySideLayout); m_categoriesListWidget = new QListWidget; connect(m_categoriesListWidget, &QListWidget::itemClicked, this, &CategoryPage::editCategory); connect(m_categoriesListWidget, &QListWidget::itemSelectionChanged, this, &CategoryPage::editSelectedCategory); connect(m_categoriesListWidget, &QListWidget::itemChanged, this, &CategoryPage::categoryNameChanged); // This is needed to fix some odd behavior if the "New" button is double clicked connect(m_categoriesListWidget, &QListWidget::itemDoubleClicked, this, &CategoryPage::categoryDoubleClicked); connect(m_categoriesListWidget->itemDelegate(), SIGNAL(closeEditor(QWidget *, QAbstractItemDelegate::EndEditHint)), this, SLOT(listWidgetEditEnd(QWidget *, QAbstractItemDelegate::EndEditHint))); categorySideLayout->addWidget(m_categoriesListWidget); // New, Delete, and buttons QHBoxLayout *newDeleteRenameLayout = new QHBoxLayout; categorySideLayout->addLayout(newDeleteRenameLayout); m_newCategoryButton = new QPushButton(i18n("New")); connect(m_newCategoryButton, &QPushButton::clicked, this, &CategoryPage::newCategory); newDeleteRenameLayout->addWidget(m_newCategoryButton); m_delItem = new QPushButton(i18n("Delete")); connect(m_delItem, &QPushButton::clicked, this, &CategoryPage::deleteCurrentCategory); newDeleteRenameLayout->addWidget(m_delItem); m_renameItem = new QPushButton(i18n("Rename")); connect(m_renameItem, &QPushButton::clicked, this, &CategoryPage::renameCurrentCategory); newDeleteRenameLayout->addWidget(m_renameItem); // Category settings QVBoxLayout *rightSideLayout = new QVBoxLayout; categoryLayout->addLayout(rightSideLayout); // Header m_categoryLabel = new QLabel; m_categoryLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); rightSideLayout->addWidget(m_categoryLabel); // Pending rename label m_renameLabel = new QLabel; m_renameLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); rightSideLayout->addWidget(m_renameLabel); QDialog *parentDialog = qobject_cast<QDialog *>(parent); connect(parentDialog, &QDialog::rejected, m_renameLabel, &QLabel::clear); // Some space looks better here :-) QLabel *spacer = new QLabel; rightSideLayout->addWidget(spacer); // Here we start with the actual settings QGridLayout *settingsLayout = new QGridLayout; rightSideLayout->addLayout(settingsLayout); int row = 0; // Positionable m_positionableLabel = new QLabel(i18n("Positionable tags:")); settingsLayout->addWidget(m_positionableLabel, row, 0); m_positionable = new QCheckBox(i18n("Tags in this category can be associated with areas within images")); settingsLayout->addWidget(m_positionable, row, 1); connect(m_positionable, &QCheckBox::clicked, this, &CategoryPage::positionableChanged); row++; // Icon m_iconLabel = new QLabel(i18n("Icon:")); settingsLayout->addWidget(m_iconLabel, row, 0); m_icon = new KIconButton; settingsLayout->addWidget(m_icon, row, 1); m_icon->setIconSize(32); m_icon->setIcon(QString::fromUtf8("personsIcon")); connect(m_icon, &KIconButton::iconChanged, this, &CategoryPage::iconChanged); row++; // Thumbnail size m_thumbnailSizeInCategoryLabel = new QLabel(i18n("Thumbnail size:")); settingsLayout->addWidget(m_thumbnailSizeInCategoryLabel, row, 0); m_thumbnailSizeInCategory = new QSpinBox; m_thumbnailSizeInCategory->setRange(32, 512); m_thumbnailSizeInCategory->setSingleStep(32); settingsLayout->addWidget(m_thumbnailSizeInCategory, row, 1); connect(m_thumbnailSizeInCategory, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &CategoryPage::thumbnailSizeChanged); row++; // Preferred View m_preferredViewLabel = new QLabel(i18n("Preferred view:")); settingsLayout->addWidget(m_preferredViewLabel, row, 0); m_preferredView = new QComboBox; settingsLayout->addWidget(m_preferredView, row, 1); m_preferredView->addItems(QStringList() << i18n("List View") << i18n("List View with Custom Thumbnails") << i18n("Icon View") << i18n("Icon View with Custom Thumbnails")); connect(m_preferredView, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &CategoryPage::preferredViewChanged); rightSideLayout->addStretch(); // Info about the database not being saved QHBoxLayout *dbNotSavedLayout = new QHBoxLayout; mainLayout->addLayout(dbNotSavedLayout); m_dbNotSavedLabel = new QLabel(i18n("<font color='red'>" "The database has unsaved changes. As long as those are " "not saved,<br/>the names of categories can't be changed " "and new ones can't be added." "</font>")); m_dbNotSavedLabel->setWordWrap(true); dbNotSavedLayout->addWidget(m_dbNotSavedLabel); m_saveDbNowButton = new QPushButton(i18n("Save the DB now")); m_saveDbNowButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum); connect(m_saveDbNowButton, &QPushButton::clicked, this, &CategoryPage::saveDbNow); dbNotSavedLayout->addWidget(m_saveDbNowButton); resetInterface(); // Untagged images m_untaggedBox = new UntaggedGroupBox; mainLayout->addWidget(m_untaggedBox); m_currentCategory = 0; // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = false; m_categoryNamesChanged = false; } void Settings::CategoryPage::resetInterface() { enableDisable(false); m_categoriesListWidget->setItemSelected(m_categoriesListWidget->currentItem(), false); resetCategoryLabel(); m_renameLabel->hide(); } void Settings::CategoryPage::editSelectedCategory() { editCategory(m_categoriesListWidget->currentItem()); } void Settings::CategoryPage::editCategory(QListWidgetItem *i) { if (i == 0) { return; } m_categoryNameBeforeEdit = i->text(); Settings::CategoryItem *item = static_cast<Settings::CategoryItem *>(i); m_currentCategory = item; m_categoryLabel->setText(i18n("Settings for category <b>%1</b>", item->originalName())); if (m_currentCategory->originalName() != m_categoryNameBeforeEdit) { m_renameLabel->setText(i18n("<i>Pending change: rename to \"%1\"</i>", m_categoryNameBeforeEdit)); m_renameLabel->show(); } else { m_renameLabel->clear(); m_renameLabel->hide(); } m_positionable->setChecked(item->positionable()); m_icon->setIcon(item->icon()); m_thumbnailSizeInCategory->setValue(item->thumbnailSize()); m_preferredView->setCurrentIndex(static_cast<int>(item->viewType())); enableDisable(true); if (item->originalName() == DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name()) { m_delItem->setEnabled(false); m_positionableLabel->setEnabled(false); m_positionable->setEnabled(false); m_thumbnailSizeInCategoryLabel->setEnabled(false); m_thumbnailSizeInCategory->setEnabled(false); m_preferredViewLabel->setEnabled(false); m_preferredView->setEnabled(false); } } void Settings::CategoryPage::categoryNameChanged(QListWidgetItem *item) { QString newCategoryName = item->text().simplified(); m_categoriesListWidget->blockSignals(true); item->setText(QString()); m_categoriesListWidget->blockSignals(false); // Now let's check if the new name is valid :-) // If it's empty, we're done here. The new name can't be empty. if (newCategoryName.isEmpty()) { resetCategory(item); return; } // We don't want to have special category names. // We do have to search both for the localized version and the C locale version, because a user // could start KPA e. g. with a German locale and create a "Folder" category (which would not // be caught by i18n("Folder")), and then start KPA with the C locale, which would produce a // doubled "Folder" category. if (newCategoryName == i18n("Folder") || newCategoryName == QString::fromUtf8("Folder") || newCategoryName == i18n("Media Type") || newCategoryName == QString::fromUtf8("Media Type")) { resetCategory(item); KMessageBox::sorry(this, i18n("<p>Can't change the name of category \"%1\" to \"%2\":</p>" "<p>\"%2\" is a special category name which is reserved and can't " "be used for a normal category.</p>", m_currentCategory->text(), newCategoryName), i18n("Invalid category name")); return; } // Let's see if we already have a category with this name. if (m_categoriesListWidget->findItems(newCategoryName, Qt::MatchExactly).size() > 0) { resetCategory(item); KMessageBox::sorry(this, i18n("<p>Can't change the name of category \"%1\" to \"%2\":</p>" "<p>A category with this name already exists.</p>", m_currentCategory->text(), newCategoryName), i18n("Invalid category name")); return; } // Let's see if we have any pending name changes that would cause collisions. for (int i = 0; i < m_categoriesListWidget->count(); i++) { Settings::CategoryItem *cat = static_cast<Settings::CategoryItem *>(m_categoriesListWidget->item(i)); if (cat == m_currentCategory) { continue; } if (newCategoryName == cat->originalName()) { resetCategory(item); KMessageBox::sorry(this, i18n("<p>Can't change the name of category \"%1\" to \"%2\":</p>" "<p>There's a pending rename action on the category \"%2\". " "Please save this change first.</p>", m_currentCategory->text(), newCategoryName), i18n("Unsaved pending renaming action")); return; } } m_categoriesListWidget->blockSignals(true); item->setText(newCategoryName); m_categoriesListWidget->blockSignals(false); emit categoryChangesPending(); m_untaggedBox->categoryRenamed(m_categoryNameBeforeEdit, newCategoryName); m_currentCategory->setLabel(newCategoryName); editCategory(m_currentCategory); m_categoryNamesChanged = true; } void Settings::CategoryPage::resetCategory(QListWidgetItem *item) { m_categoriesListWidget->blockSignals(true); item->setText(m_categoryNameBeforeEdit); m_categoriesListWidget->blockSignals(false); } void Settings::CategoryPage::positionableChanged(bool positionable) { if (!m_currentCategory) { return; } if (!positionable) { int answer = KMessageBox::questionYesNo(this, i18n("<p>Do you really want to make \"%1\" " "non-positionable?</p>" "<p>All areas linked against this category " "will be deleted!</p>", m_currentCategory->text())); if (answer == KMessageBox::No) { m_positionable->setCheckState(Qt::Checked); return; } } m_currentCategory->setPositionable(positionable); } void Settings::CategoryPage::iconChanged(const QString &icon) { if (m_currentCategory) { m_currentCategory->setIcon(icon); } } void Settings::CategoryPage::thumbnailSizeChanged(int size) { if (m_currentCategory) { m_currentCategory->setThumbnailSize(size); } } void Settings::CategoryPage::preferredViewChanged(int i) { if (m_currentCategory) { m_currentCategory->setViewType(static_cast<DB::Category::ViewType>(i)); } } void Settings::CategoryPage::newCategory() { // This is needed to fix some odd behavior if the "New" button is double clicked if (m_editorOpen) { return; } else { m_editorOpen = true; } // Here starts the real function QString newCategory = i18n("New category"); QString checkedCategory = newCategory; int i = 1; while (m_categoriesListWidget->findItems(checkedCategory, Qt::MatchExactly).size() > 0) { i++; checkedCategory = QString::fromUtf8("%1 %2").arg(newCategory).arg(i); } m_categoriesListWidget->blockSignals(true); m_currentCategory = new Settings::CategoryItem(checkedCategory, QString(), DB::Category::TreeView, 64, m_categoriesListWidget); m_currentCategory->markAsNewCategory(); emit categoryChangesPending(); m_currentCategory->setLabel(checkedCategory); m_currentCategory->setSelected(true); m_categoriesListWidget->blockSignals(false); m_positionable->setChecked(false); m_icon->setIcon(QIcon()); m_thumbnailSizeInCategory->setValue(64); enableDisable(true); editCategory(m_currentCategory); m_categoriesListWidget->editItem(m_currentCategory); } void Settings::CategoryPage::deleteCurrentCategory() { int answer = KMessageBox::questionYesNo(this, i18n("<p>Really delete category \"%1\"?</p>", m_currentCategory->text())); if (answer == KMessageBox::No) { return; } m_untaggedBox->categoryDeleted(m_currentCategory->text()); m_deletedCategories.append(m_currentCategory); m_categoriesListWidget->takeItem(m_categoriesListWidget->row(m_currentCategory)); m_currentCategory = 0; m_positionable->setChecked(false); m_icon->setIcon(QIcon()); m_thumbnailSizeInCategory->setValue(64); enableDisable(false); resetCategoryLabel(); editCategory(m_categoriesListWidget->currentItem()); emit categoryChangesPending(); } void Settings::CategoryPage::renameCurrentCategory() { // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = true; m_categoriesListWidget->editItem(m_currentCategory); } void Settings::CategoryPage::enableDisable(bool b) { m_delItem->setEnabled(b); m_positionableLabel->setEnabled(b); m_positionable->setEnabled(b); m_icon->setEnabled(b); m_iconLabel->setEnabled(b); m_thumbnailSizeInCategoryLabel->setEnabled(b); m_thumbnailSizeInCategory->setEnabled(b); m_preferredViewLabel->setEnabled(b); m_preferredView->setEnabled(b); m_categoriesListWidget->blockSignals(true); if (MainWindow::Window::theMainWindow()->dbIsDirty()) { m_dbNotSavedLabel->show(); m_saveDbNowButton->show(); m_renameItem->setEnabled(false); m_newCategoryButton->setEnabled(false); for (int i = 0; i < m_categoriesListWidget->count(); i++) { QListWidgetItem *currentItem = m_categoriesListWidget->item(i); currentItem->setFlags(currentItem->flags() & ~Qt::ItemIsEditable); } } else { m_dbNotSavedLabel->hide(); m_saveDbNowButton->hide(); m_renameItem->setEnabled(b); m_newCategoryButton->setEnabled(true); for (int i = 0; i < m_categoriesListWidget->count(); i++) { QListWidgetItem *currentItem = m_categoriesListWidget->item(i); currentItem->setFlags(currentItem->flags() | Qt::ItemIsEditable); } } m_categoriesListWidget->blockSignals(false); } void Settings::CategoryPage::saveSettings(Settings::SettingsData *opt, DB::MemberMap *memberMap) { // Delete items Q_FOREACH (CategoryItem *item, m_deletedCategories) { item->removeFromDatabase(); } // Created or Modified items for (int i = 0; i < m_categoriesListWidget->count(); ++i) { CategoryItem *item = static_cast<CategoryItem *>(m_categoriesListWidget->item(i)); item->submit(memberMap); } DB::ImageDB::instance()->memberMap() = *memberMap; m_untaggedBox->saveSettings(opt); if (m_categoryNamesChanged) { // Probably, one or more category names have been edited. Save the database so that // all thumbnails are referenced with the correct name. MainWindow::Window::theMainWindow()->slotSave(); m_categoryNamesChanged = false; } } void Settings::CategoryPage::loadSettings(Settings::SettingsData *opt) { m_categoriesListWidget->blockSignals(true); m_categoriesListWidget->clear(); QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH (const DB::CategoryPtr category, categories) { if (category->type() == DB::Category::PlainCategory || category->type() == DB::Category::TokensCategory) { Settings::CategoryItem *item = new CategoryItem(category->name(), category->iconName(), category->viewType(), category->thumbnailSize(), m_categoriesListWidget, category->positionable()); Q_UNUSED(item); } } m_categoriesListWidget->blockSignals(false); m_untaggedBox->loadSettings(opt); } void Settings::CategoryPage::categoryDoubleClicked(QListWidgetItem *) { // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = true; } void Settings::CategoryPage::listWidgetEditEnd(QWidget *, QAbstractItemDelegate::EndEditHint) { // This is needed to fix some odd behavior if the "New" button is double clicked m_editorOpen = false; } void Settings::CategoryPage::resetCategoryLabel() { m_categoryLabel->setText(i18n("<i>Choose a category to edit it</i>")); } void Settings::CategoryPage::saveDbNow() { MainWindow::Window::theMainWindow()->slotSave(); resetInterface(); enableDisable(false); } void Settings::CategoryPage::resetCategoryNamesChanged() { m_categoryNamesChanged = false; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/DatabaseBackendPage.cpp b/Settings/DatabaseBackendPage.cpp index 4d686943..8a56ab93 100644 --- a/Settings/DatabaseBackendPage.cpp +++ b/Settings/DatabaseBackendPage.cpp @@ -1,104 +1,107 @@ /* Copyright (C) 2003-2010 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 "DatabaseBackendPage.h" -#include "MainWindow/DirtyIndicator.h" + #include "SettingsData.h" + +#include <MainWindow/DirtyIndicator.h> + #include <KLocalizedString> #include <QCheckBox> #include <QHBoxLayout> #include <QLabel> #include <QSpinBox> #include <QVBoxLayout> Settings::DatabaseBackendPage::DatabaseBackendPage(QWidget *parent) : QWidget(parent) { QVBoxLayout *topLayout = new QVBoxLayout(this); // Compressed index.xml m_compressedIndexXML = new QCheckBox(i18n("Choose speed over readability for index.xml file"), this); topLayout->addWidget(m_compressedIndexXML); connect(m_compressedIndexXML, &QCheckBox::clicked, this, &DatabaseBackendPage::markDirty); m_compressBackup = new QCheckBox(i18n("Compress backup file"), this); topLayout->addWidget(m_compressBackup); // Auto save QLabel *label = new QLabel(i18n("Auto save every:"), this); m_autosave = new QSpinBox; m_autosave->setRange(1, 120); m_autosave->setSuffix(i18n("min.")); QHBoxLayout *lay = new QHBoxLayout; topLayout->addLayout(lay); lay->addWidget(label); lay->addWidget(m_autosave); lay->addStretch(1); // Backup lay = new QHBoxLayout; topLayout->addLayout(lay); QLabel *backupLabel = new QLabel(i18n("Number of backups to keep:"), this); lay->addWidget(backupLabel); m_backupCount = new QSpinBox; m_backupCount->setRange(-1, 100); m_backupCount->setSpecialValueText(i18n("Infinite")); lay->addWidget(m_backupCount); lay->addStretch(1); topLayout->addStretch(1); QString txt; txt = i18n("<p>KPhotoAlbum is capable of backing up the index.xml file by keeping copies named index.xml~1~ index.xml~2~ etc. " "and you can use the spinbox to specify the number of backup files to keep. " "KPhotoAlbum will delete the oldest backup file when it reaches " "the maximum number of backup files.</p>" "<p>The index.xml file may grow substantially if you have many images, and in that case it is useful to ask KPhotoAlbum to zip " "the backup files to preserve disk space.</p>"); backupLabel->setWhatsThis(txt); m_backupCount->setWhatsThis(txt); m_compressBackup->setWhatsThis(txt); txt = i18n("<p>KPhotoAlbum is using a single index.xml file as its <i>data base</i>. With lots of images it may take " "a long time to read this file. You may cut down this time to approximately half, by checking this check box. " "The disadvantage is that the index.xml file is less readable by human eyes.</p>"); m_compressedIndexXML->setWhatsThis(txt); } void Settings::DatabaseBackendPage::loadSettings(Settings::SettingsData *opt) { m_compressedIndexXML->setChecked(opt->useCompressedIndexXML()); m_autosave->setValue(opt->autoSave()); m_backupCount->setValue(opt->backupCount()); m_compressBackup->setChecked(opt->compressBackup()); } void Settings::DatabaseBackendPage::saveSettings(Settings::SettingsData *opt) { opt->setBackupCount(m_backupCount->value()); opt->setCompressBackup(m_compressBackup->isChecked()); opt->setUseCompressedIndexXML(m_compressedIndexXML->isChecked()); opt->setAutoSave(m_autosave->value()); } void Settings::DatabaseBackendPage::markDirty() { MainWindow::DirtyIndicator::markDirty(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/ExifPage.cpp b/Settings/ExifPage.cpp index 519327ba..efbc1895 100644 --- a/Settings/ExifPage.cpp +++ b/Settings/ExifPage.cpp @@ -1,73 +1,75 @@ /* Copyright (C) 2003-2010 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 "ExifPage.h" + #include "SettingsData.h" + +#include <Exif/Info.h> +#include <Exif/TreeView.h> + #include <KComboBox> #include <KLocalizedString> #include <QHBoxLayout> #include <QLabel> #include <QTextCodec> #include <QVBoxLayout> -#include "Exif/Info.h" -#include "Exif/TreeView.h" - Settings::ExifPage::ExifPage(QWidget *parent) : QWidget(parent) { QVBoxLayout *vlay = new QVBoxLayout(this); QHBoxLayout *hlay1 = new QHBoxLayout(); QHBoxLayout *hlay2 = new QHBoxLayout(); vlay->addLayout(hlay1); vlay->addLayout(hlay2); m_exifForViewer = new Exif::TreeView(i18n("Exif/IPTC info to show in the viewer"), this); hlay1->addWidget(m_exifForViewer); m_exifForDialog = new Exif::TreeView(i18n("Exif/IPTC info to show in the Exif dialog"), this); hlay1->addWidget(m_exifForDialog); QLabel *iptcCharsetLabel = new QLabel(i18n("Character set for image metadata:"), this); m_iptcCharset = new KComboBox(this); QStringList charsets; QList<QByteArray> charsetsBA = QTextCodec::availableCodecs(); for (QList<QByteArray>::const_iterator it = charsetsBA.constBegin(); it != charsetsBA.constEnd(); ++it) charsets << QString::fromLatin1(*it); m_iptcCharset->insertItems(m_iptcCharset->count(), charsets); hlay2->addStretch(1); hlay2->addWidget(iptcCharsetLabel); hlay2->addWidget(m_iptcCharset); } void Settings::ExifPage::saveSettings(Settings::SettingsData *opt) { opt->setExifForViewer(m_exifForViewer->selected()); opt->setExifForDialog(m_exifForDialog->selected()); opt->setIptcCharset(m_iptcCharset->currentText()); } void Settings::ExifPage::loadSettings(Settings::SettingsData *opt) { m_exifForViewer->reload(); m_exifForDialog->reload(); m_exifForViewer->setSelectedExif(Settings::SettingsData::instance()->exifForViewer()); m_exifForDialog->setSelectedExif(Settings::SettingsData::instance()->exifForDialog()); m_iptcCharset->setCurrentIndex(qMax(0, QTextCodec::availableCodecs().indexOf(opt->iptcCharset().toLatin1()))); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/FileVersionDetectionPage.cpp b/Settings/FileVersionDetectionPage.cpp index 13a6910f..5150f8f6 100644 --- a/Settings/FileVersionDetectionPage.cpp +++ b/Settings/FileVersionDetectionPage.cpp @@ -1,222 +1,224 @@ /* Copyright (C) 2003-2010 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 "FileVersionDetectionPage.h" + #include "SettingsData.h" + #include <KLocalizedString> #include <QCheckBox> #include <QGroupBox> #include <QLabel> #include <QLineEdit> #include <QVBoxLayout> Settings::FileVersionDetectionPage::FileVersionDetectionPage(QWidget *parent) : QWidget(parent) { QVBoxLayout *topLayout = new QVBoxLayout(this); QString txt; // General file searching { QGroupBox *generalBox = new QGroupBox(i18n("New File Searches"), this); topLayout->addWidget(generalBox); QVBoxLayout *layout = new QVBoxLayout(generalBox); // Search for images on startup m_searchForImagesOnStart = new QCheckBox(i18n("Search for new images and videos on startup"), generalBox); layout->addWidget(m_searchForImagesOnStart); m_ignoreFileExtension = new QCheckBox(i18n("Ignore file extensions when searching for new images and videos"), generalBox); layout->addWidget(m_ignoreFileExtension); m_skipSymlinks = new QCheckBox(i18n("Skip symbolic links when searching for new images"), generalBox); layout->addWidget(m_skipSymlinks); m_skipRawIfOtherMatches = new QCheckBox(i18n("Do not read RAW files if a matching JPEG/TIFF file exists"), generalBox); layout->addWidget(m_skipRawIfOtherMatches); // Exclude directories from search QLabel *excludeDirectoriesLabel = new QLabel(i18n("Directories to exclude from new file search:"), generalBox); layout->addWidget(excludeDirectoriesLabel); m_excludeDirectories = new QLineEdit(generalBox); layout->addWidget(m_excludeDirectories); excludeDirectoriesLabel->setBuddy(m_excludeDirectories); txt = i18n("<p>KPhotoAlbum is capable of searching for new images and videos when started, this does, " "however, take some time, so instead you may wish to manually tell KPhotoAlbum to search for new images " "using <b>Maintenance->Rescan for new images</b></p>"); m_searchForImagesOnStart->setWhatsThis(txt); txt = i18n("<p>KPhotoAlbum will normally search new images and videos by their file extension. " "If this option is set, <em>all</em> files neither in the database nor in the block list " "will be checked by their Mime type, regardless of their extension. This will take " "significantly longer than finding files by extension!</p>"); m_ignoreFileExtension->setWhatsThis(txt); txt = i18n("<p>KPhotoAlbum attempts to read all image files whether actual files or symbolic links. If you " "wish to ignore symbolic links, check this option. This is useful if for some reason you have e.g. " "both the original files and symbolic links to these files within your image directory.</p>"); m_skipSymlinks->setWhatsThis(txt); txt = i18n("<p>KPhotoAlbum is capable of reading certain kinds of RAW images. " "Some cameras store both a RAW image and a matching JPEG or TIFF image. " "This causes duplicate images to be stored in KPhotoAlbum, which may be undesirable. " "If this option is checked, KPhotoAlbum will not read RAW files for which matching image files also exist.</p>"); m_skipRawIfOtherMatches->setWhatsThis(txt); txt = i18n("<p>Directories defined here (separated by comma <tt>,</tt>) are " "skipped when searching for new photos. Thumbnail directories of different " "tools should be configured here. E.g. <tt>xml,ThumbNails,.thumbs,.thumbnails</tt>.</p>"); excludeDirectoriesLabel->setWhatsThis(txt); } // Original/Modified File Support { QGroupBox *modifiedBox = new QGroupBox(i18n("File Version Detection Settings"), this); topLayout->addWidget(modifiedBox); QVBoxLayout *layout = new QVBoxLayout(modifiedBox); m_detectModifiedFiles = new QCheckBox(i18n("Try to detect multiple versions of files"), modifiedBox); layout->addWidget(m_detectModifiedFiles); QLabel *modifiedFileComponentLabel = new QLabel(i18n("File versions search regexp:"), modifiedBox); layout->addWidget(modifiedFileComponentLabel); m_modifiedFileComponent = new QLineEdit(modifiedBox); layout->addWidget(m_modifiedFileComponent); QLabel *originalFileComponentLabel = new QLabel(i18n("Original file replacement text:"), modifiedBox); layout->addWidget(originalFileComponentLabel); m_originalFileComponent = new QLineEdit(modifiedBox); layout->addWidget(m_originalFileComponent); m_moveOriginalContents = new QCheckBox(i18n("Move meta-data (i.e. delete tags from the original)"), modifiedBox); layout->addWidget(m_moveOriginalContents); m_autoStackNewFiles = new QCheckBox(i18n("Automatically stack new versions of images"), modifiedBox); layout->addWidget(m_autoStackNewFiles); txt = i18n("<p>When KPhotoAlbum searches for new files and finds a file that matches the " "<i>modified file search regexp</i> it is assumed that an original version of " "the image may exist. The regexp pattern will be replaced with the <i>original " "file replacement text</i> and if that file exists, all associated metadata (category " "information, ratings, etc) will be copied or moved from the original file to the new one.</p>"); m_detectModifiedFiles->setWhatsThis(txt); txt = i18n("<p>A perl regular expression that should match a modified file. " "<ul><li>A dot matches a single character (<tt>\\.</tt> matches a dot)</li> " " <li>You can use the quantifiers <tt>*</tt>,<tt>+</tt>,<tt>?</tt>, or you can " " match multiple occurrences of an expression by using curly brackets (e.g. " "<tt>e{0,1}</tt> matches 0 or 1 occurrences of the character \"e\").</li> " " <li>You can group parts of the expression using parenthesis.</li> " "</ul>Example: <tt>-modified\\.(jpg|tiff)</tt> </p>"); modifiedFileComponentLabel->setWhatsThis(txt); m_modifiedFileComponent->setWhatsThis(txt); txt = i18n("<p>A string that is used to replace the match from the <i>File versions search regexp</i>. " "This can be a semicolon (<tt>;</tt>) separated list. Each string is used to replace the match " "in the new file's name until an original file is found or we run out of options.</p>"); originalFileComponentLabel->setWhatsThis(txt); m_originalFileComponent->setWhatsThis(txt); txt = i18n("<p>The tagging is moved from the original file to the new file. This way " "only the latest version of an image is tagged.</p>"); m_moveOriginalContents->setWhatsThis(txt); txt = i18n("<p>If this option is set, new versions of an image are automatically stacked " "and placed to the top of the stack. This way the new image is shown when the " "stack is in collapsed state - the default state in KPhotoAlbum.</p>"); m_autoStackNewFiles->setWhatsThis(txt); } // Copy File Support { QGroupBox *copyBox = new QGroupBox(i18nc("Configure the feature to make a copy of a file first and then open the copied file with an external application", "Copy File and Open with an External Application"), this); topLayout->addWidget(copyBox); QVBoxLayout *layout = new QVBoxLayout(copyBox); QLabel *copyFileComponentLabel = new QLabel(i18n("Copy file search regexp:"), copyBox); layout->addWidget(copyFileComponentLabel); m_copyFileComponent = new QLineEdit(copyBox); layout->addWidget(m_copyFileComponent); QLabel *copyFileReplacementComponentLabel = new QLabel(i18n("Copy file replacement text:"), copyBox); layout->addWidget(copyFileReplacementComponentLabel); m_copyFileReplacementComponent = new QLineEdit(copyBox); layout->addWidget(m_copyFileReplacementComponent); txt = i18n("<p>KPhotoAlbum can make a copy of an image before opening it with an external application. This configuration defines how the new file is named.</p>" "<p>The regular expression defines the part of the original file name that is replaced with the <i>replacement text</i>. " "E.g. regexp <i>\"\\.(jpg|png)\"</i> and replacement text <i>\"-mod.\\1\"</i> would copy test.jpg to test-mod.jpg and open the new file in selected application.</p>"); copyFileComponentLabel->setWhatsThis(txt); m_copyFileComponent->setWhatsThis(txt); copyFileReplacementComponentLabel->setWhatsThis(txt); m_copyFileReplacementComponent->setWhatsThis(txt); } } Settings::FileVersionDetectionPage::~FileVersionDetectionPage() { delete m_searchForImagesOnStart; delete m_ignoreFileExtension; delete m_skipSymlinks; delete m_skipRawIfOtherMatches; delete m_excludeDirectories; delete m_detectModifiedFiles; delete m_modifiedFileComponent; delete m_originalFileComponent; delete m_moveOriginalContents; delete m_autoStackNewFiles; delete m_copyFileComponent; delete m_copyFileReplacementComponent; } void Settings::FileVersionDetectionPage::loadSettings(Settings::SettingsData *opt) { m_searchForImagesOnStart->setChecked(opt->searchForImagesOnStart()); m_ignoreFileExtension->setChecked(opt->ignoreFileExtension()); m_skipSymlinks->setChecked(opt->skipSymlinks()); m_skipRawIfOtherMatches->setChecked(opt->skipRawIfOtherMatches()); m_excludeDirectories->setText(opt->excludeDirectories()); m_detectModifiedFiles->setChecked(opt->detectModifiedFiles()); m_modifiedFileComponent->setText(opt->modifiedFileComponent()); m_originalFileComponent->setText(opt->originalFileComponent()); m_moveOriginalContents->setChecked(opt->moveOriginalContents()); m_autoStackNewFiles->setChecked(opt->autoStackNewFiles()); m_copyFileComponent->setText(opt->copyFileComponent()); m_copyFileReplacementComponent->setText(opt->copyFileReplacementComponent()); } void Settings::FileVersionDetectionPage::saveSettings(Settings::SettingsData *opt) { opt->setSearchForImagesOnStart(m_searchForImagesOnStart->isChecked()); opt->setIgnoreFileExtension(m_ignoreFileExtension->isChecked()); opt->setSkipSymlinks(m_skipSymlinks->isChecked()); opt->setSkipRawIfOtherMatches(m_skipRawIfOtherMatches->isChecked()); opt->setExcludeDirectories(m_excludeDirectories->text()); opt->setDetectModifiedFiles(m_detectModifiedFiles->isChecked()); opt->setModifiedFileComponent(m_modifiedFileComponent->text()); opt->setOriginalFileComponent(m_originalFileComponent->text()); opt->setAutoStackNewFiles(m_autoStackNewFiles->isChecked()); opt->setCopyFileComponent(m_copyFileComponent->text()); opt->setCopyFileReplacementComponent(m_copyFileReplacementComponent->text()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/GeneralPage.cpp b/Settings/GeneralPage.cpp index 87b95417..29bf874e 100644 --- a/Settings/GeneralPage.cpp +++ b/Settings/GeneralPage.cpp @@ -1,315 +1,318 @@ /* Copyright (C) 2003-2010 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 "GeneralPage.h" -#include "DB/CategoryCollection.h" -#include "MainWindow/Window.h" + #include "SettingsData.h" + #include <DB/Category.h> +#include <DB/CategoryCollection.h> #include <DB/ImageDB.h> +#include <MainWindow/Window.h> + #include <KComboBox> #include <KLocalizedString> #include <QCheckBox> #include <QGroupBox> #include <QLabel> #include <QSpinBox> #include <QTextEdit> #include <QVBoxLayout> #include <QWidget> Settings::GeneralPage::GeneralPage(QWidget *parent) : QWidget(parent) { QVBoxLayout *lay1 = new QVBoxLayout(this); QGroupBox *box = new QGroupBox(i18n("Loading New Images"), this); lay1->addWidget(box); QGridLayout *lay = new QGridLayout(box); lay->setSpacing(6); int row = 0; // Thrust time stamps QLabel *timeStampLabel = new QLabel(i18n("Trust image dates:"), box); m_trustTimeStamps = new KComboBox(box); m_trustTimeStamps->addItems(QStringList() << i18nc("As in 'always trust image dates'", "Always") << i18nc("As in 'ask whether to trust image dates'", "Ask") << i18nc("As in 'never trust image dates'", "Never")); timeStampLabel->setBuddy(m_trustTimeStamps); lay->addWidget(timeStampLabel, row, 0); lay->addWidget(m_trustTimeStamps, row, 1, 1, 3); // Do Exif rotate row++; m_useEXIFRotate = new QCheckBox(i18n("Use Exif orientation information"), box); lay->addWidget(m_useEXIFRotate, row, 0, 1, 4); // Use Exif description row++; m_useEXIFComments = new QCheckBox(i18n("Use Exif description"), box); lay->addWidget(m_useEXIFComments, row, 0, 1, 4); connect(m_useEXIFComments, &QCheckBox::stateChanged, this, &GeneralPage::useEXIFCommentsChanged); m_stripEXIFComments = new QCheckBox(i18n("Strip out camera generated default descriptions"), box); connect(m_stripEXIFComments, &QCheckBox::stateChanged, this, &GeneralPage::stripEXIFCommentsChanged); lay->addWidget(m_stripEXIFComments, row, 1, 1, 4); row++; m_commentsToStrip = new QTextEdit(); m_commentsToStrip->setMaximumHeight(60); m_commentsToStrip->setEnabled(false); lay->addWidget(m_commentsToStrip, row, 1, 1, 4); // Use embedded thumbnail row++; m_useRawThumbnail = new QCheckBox(i18n("Use the embedded thumbnail in RAW file or halfsized RAW"), box); lay->addWidget(m_useRawThumbnail, row, 0); row++; QLabel *label = new QLabel(i18n("Required size for the thumbnail:"), box); m_useRawThumbnailWidth = new QSpinBox(box); m_useRawThumbnailWidth->setRange(100, 5000); m_useRawThumbnailWidth->setSingleStep(64); lay->addWidget(label, row, 0); lay->addWidget(m_useRawThumbnailWidth, row, 1); label = new QLabel(QString::fromLatin1("x"), box); m_useRawThumbnailHeight = new QSpinBox(box); m_useRawThumbnailHeight->setRange(100, 5000); m_useRawThumbnailHeight->setSingleStep(64); lay->addWidget(label, row, 2); lay->addWidget(m_useRawThumbnailHeight, row, 3); box = new QGroupBox(i18n("Histogram"), this); lay1->addWidget(box); lay = new QGridLayout(box); lay->setSpacing(6); row = 0; m_showHistogram = new QCheckBox(i18n("Show histogram"), box); lay->addWidget(m_showHistogram, row, 0); row++; connect(m_showHistogram, &QCheckBox::stateChanged, this, &GeneralPage::showHistogramChanged); m_histogramUseLinearScale = new QCheckBox(i18n("Use linear scale for histogram")); lay->addWidget(m_histogramUseLinearScale, row, 0); row++; label = new QLabel(i18n("Size of histogram columns in date bar:"), box); m_barWidth = new QSpinBox; m_barWidth->setRange(1, 100); m_barWidth->setSingleStep(1); lay->addWidget(label, row, 0); lay->addWidget(m_barWidth, row, 1); label = new QLabel(QString::fromLatin1("x"), box); m_barHeight = new QSpinBox; m_barHeight->setRange(15, 100); lay->addWidget(label, row, 2); lay->addWidget(m_barHeight, row, 3); box = new QGroupBox(i18n("Miscellaneous"), this); lay1->addWidget(box); lay = new QGridLayout(box); lay->setSpacing(6); row = 0; // Show splash screen m_showSplashScreen = new QCheckBox(i18n("Show splash screen"), box); lay->addWidget(m_showSplashScreen, row, 0); // Album Category row++; QLabel *albumCategoryLabel = new QLabel(i18n("Category for virtual albums:"), box); m_albumCategory = new QComboBox; lay->addWidget(albumCategoryLabel, row, 0); lay->addWidget(m_albumCategory, row, 1); QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH (const DB::CategoryPtr category, categories) { m_albumCategory->addItem(category->name()); } m_listenForAndroidDevicesOnStartup = new QCheckBox(i18n("Listen for Android devices on startup")); lay->addWidget(m_listenForAndroidDevicesOnStartup); lay1->addStretch(1); // Whats This QString txt; txt = i18n("<p>KPhotoAlbum will try to read the image date from Exif information in the image. " "If that fails it will try to get the date from the file's time stamp.</p>" "<p>However, this information will be wrong if the image was scanned in (you want the date the image " "was taken, not the date of the scan).</p>" "<p>If you only scan images, in contrast to sometimes using " "a digital camera, you should reply <b>no</b>. If you never scan images, you should reply <b>yes</b>, " "otherwise reply <b>ask</b>. This will allow you to decide whether the images are from " "the scanner or the camera, from session to session.</p>"); timeStampLabel->setWhatsThis(txt); m_trustTimeStamps->setWhatsThis(txt); txt = i18n("<p>JPEG images may contain information about rotation. " "If you have a reason for not using this information to get a default rotation of " "your images, uncheck this check box.</p>" "<p>Note: Your digital camera may not write this information into the images at all.</p>"); m_useEXIFRotate->setWhatsThis(txt); txt = i18n("<p>JPEG images may contain a description. " "Check this checkbox to specify if you want to use this as a " "default description for your images.</p>"); m_useEXIFComments->setWhatsThis(txt); txt = i18n("<p>KPhotoAlbum shares plugins with other imaging applications, some of which have the concept of albums. " "KPhotoAlbum does not have this concept; nevertheless, for certain plugins to function, KPhotoAlbum behaves " "to the plugin system as if it did.</p>" "<p>KPhotoAlbum does this by defining the current album to be the current view - that is, all the images the " "browser offers to display.</p>" "<p>In addition to the current album, KPhotoAlbum must also be able to give a list of all albums; " "the list of all albums is defined in the following way:" "<ul><li>When KPhotoAlbum's browser displays the content of a category, say all People, then each item in this category " "will look like an album to the plugin.</li>" "<li>Otherwise, the category you specify using this option will be used; e.g. if you specify People " "with this option, then KPhotoAlbum will act as if you had just chosen to display people and then invoke " "the plugin which needs to know about all albums.</li></ul></p>" "<p>Most users would probably want to specify Events here.</p>"); albumCategoryLabel->setWhatsThis(txt); m_albumCategory->setWhatsThis(txt); txt = i18n("Show the KPhotoAlbum splash screen on start up"); m_showSplashScreen->setWhatsThis(txt); txt = i18n("<p>KPhotoAlbum is capable of showing your images on android devices. KPhotoAlbum will automatically pair with the app from " "android. This, however, requires that KPhotoAlbum on your desktop is listening for multicast messages. " "Checking this checkbox will make KPhotoAlbum do so automatically on start up. " "Alternatively, you can click the connection icon in the status bar to start listening."); m_listenForAndroidDevicesOnStartup->setWhatsThis(txt); txt = i18n("<p>Some cameras automatically store generic comments in each image. " "These comments can be ignored automatically.</p>" "<p>Enter the comments that you want to ignore in the input field, one per line. " "Be sure to add the exact comment, including all whitespace.</p>"); m_stripEXIFComments->setWhatsThis(txt); m_commentsToStrip->setWhatsThis(txt); } void Settings::GeneralPage::loadSettings(Settings::SettingsData *opt) { m_trustTimeStamps->setCurrentIndex(opt->tTimeStamps()); m_useEXIFRotate->setChecked(opt->useEXIFRotate()); m_useEXIFComments->setChecked(opt->useEXIFComments()); m_stripEXIFComments->setChecked(opt->stripEXIFComments()); m_stripEXIFComments->setEnabled(opt->useEXIFComments()); QStringList commentsToStrip = opt->EXIFCommentsToStrip(); QString commentsToStripStr; for (int i = 0; i < commentsToStrip.size(); ++i) { if (commentsToStripStr.size() > 0) { commentsToStripStr += QString::fromLatin1("\n"); } commentsToStripStr += commentsToStrip.at(i); } m_commentsToStrip->setPlainText(commentsToStripStr); m_commentsToStrip->setEnabled(opt->stripEXIFComments()); m_useRawThumbnail->setChecked(opt->useRawThumbnail()); setUseRawThumbnailSize(QSize(opt->useRawThumbnailSize().width(), opt->useRawThumbnailSize().height())); m_barWidth->setValue(opt->histogramSize().width()); m_barHeight->setValue(opt->histogramSize().height()); m_showHistogram->setChecked(opt->showHistogram()); m_histogramUseLinearScale->setChecked(opt->histogramUseLinearScale()); m_showSplashScreen->setChecked(opt->showSplashScreen()); m_listenForAndroidDevicesOnStartup->setChecked(opt->listenForAndroidDevicesOnStartup()); DB::CategoryPtr cat = DB::ImageDB::instance()->categoryCollection()->categoryForName(opt->albumCategory()); if (!cat) cat = DB::ImageDB::instance()->categoryCollection()->categories()[0]; m_albumCategory->setEditText(cat->name()); } void Settings::GeneralPage::saveSettings(Settings::SettingsData *opt) { opt->setTTimeStamps((TimeStampTrust)m_trustTimeStamps->currentIndex()); opt->setUseEXIFRotate(m_useEXIFRotate->isChecked()); opt->setUseEXIFComments(m_useEXIFComments->isChecked()); opt->setStripEXIFComments(m_stripEXIFComments->isChecked()); QStringList commentsToStrip = m_commentsToStrip->toPlainText().split(QString::fromLatin1("\n")); // Put the processable list to opt opt->setEXIFCommentsToStrip(commentsToStrip); QString commentsToStripString; for (QString comment : commentsToStrip) { // separate comments with "-,-" and escape existing commas by doubling if (!comment.isEmpty()) commentsToStripString += comment.replace(QString::fromLatin1(","), QString::fromLatin1(",,")) + QString::fromLatin1("-,-"); } // Put the storable list to opt opt->setCommentsToStrip(commentsToStripString); opt->setUseRawThumbnail(m_useRawThumbnail->isChecked()); opt->setUseRawThumbnailSize(QSize(useRawThumbnailSize())); opt->setShowHistogram(m_showHistogram->isChecked()); opt->setHistogramUseLinearScale(m_histogramUseLinearScale->isChecked()); opt->setShowSplashScreen(m_showSplashScreen->isChecked()); opt->setListenForAndroidDevicesOnStartup(m_listenForAndroidDevicesOnStartup->isChecked()); QString name = m_albumCategory->currentText(); if (name.isNull()) { name = DB::ImageDB::instance()->categoryCollection()->categoryNames()[0]; } opt->setAlbumCategory(name); opt->setHistogramSize(QSize(m_barWidth->value(), m_barHeight->value())); } void Settings::GeneralPage::setUseRawThumbnailSize(const QSize &size) { m_useRawThumbnailWidth->setValue(size.width()); m_useRawThumbnailHeight->setValue(size.height()); } QSize Settings::GeneralPage::useRawThumbnailSize() { return QSize(m_useRawThumbnailWidth->value(), m_useRawThumbnailHeight->value()); } void Settings::GeneralPage::showHistogramChanged(int state) const { const bool checked = state == Qt::Checked; m_histogramUseLinearScale->setChecked(checked); m_barHeight->setEnabled(checked); m_barWidth->setEnabled(checked); MainWindow::Window::theMainWindow()->setHistogramVisibilty(checked); } void Settings::GeneralPage::useEXIFCommentsChanged(int state) { m_stripEXIFComments->setEnabled(state); m_commentsToStrip->setEnabled(state && m_stripEXIFComments->isChecked()); } void Settings::GeneralPage::stripEXIFCommentsChanged(int state) { m_commentsToStrip->setEnabled(state); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/PluginsPage.cpp b/Settings/PluginsPage.cpp index ec5aedbd..5445bf59 100644 --- a/Settings/PluginsPage.cpp +++ b/Settings/PluginsPage.cpp @@ -1,62 +1,62 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "PluginsPage.h" -#include <config-kpa-kipi.h> +#include <KLocalizedString> #include <QCheckBox> #include <QLabel> #include <QVBoxLayout> - -#include <KLocalizedString> +#include <config-kpa-kipi.h> #ifdef HASKIPI #include <KIPI/ConfigWidget> #include <KIPI/PluginLoader> #endif +#include "SettingsData.h" + #include <MainWindow/Window.h> -#include <Settings/SettingsData.h> Settings::PluginsPage::PluginsPage(QWidget *parent) : QWidget(parent) { // TODO: DEPENDENCY: the circular dependency on mainwindow is unfortunate. ::MainWindow::Window::theMainWindow()->loadKipiPlugins(); QVBoxLayout *lay1 = new QVBoxLayout(this); QLabel *label = new QLabel(i18n("Choose Plugins to load:"), this); lay1->addWidget(label); m_pluginConfig = KIPI::PluginLoader::instance()->configWidget(this); lay1->addWidget(m_pluginConfig); m_delayLoadingPlugins = new QCheckBox(i18n("Delay loading plugins until the plugin menu is opened"), this); lay1->addWidget(m_delayLoadingPlugins); } void Settings::PluginsPage::saveSettings(Settings::SettingsData *opt) { m_pluginConfig->apply(); opt->setDelayLoadingPlugins(m_delayLoadingPlugins->isChecked()); } void Settings::PluginsPage::loadSettings(Settings::SettingsData *opt) { m_delayLoadingPlugins->setChecked(opt->delayLoadingPlugins()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/SettingsData.cpp b/Settings/SettingsData.cpp index 465311f1..d2b24dc8 100644 --- a/Settings/SettingsData.cpp +++ b/Settings/SettingsData.cpp @@ -1,560 +1,558 @@ /* 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 <stdlib.h> - -#include <QApplication> -#include <QPixmapCache> -#include <QStringList> +#include <DB/CategoryCollection.h> +#include <DB/ImageDB.h> #include <KConfig> #include <KConfigGroup> #include <KLocalizedString> #include <KMessageBox> #include <KSharedConfig> - -#include "DB/CategoryCollection.h" -#include "DB/ImageDB.h" +#include <QApplication> +#include <QPixmapCache> +#include <QStringList> +#include <stdlib.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("Exif_JPEG_PICTURE-,-OLYMPUS DIGITAL CAMERA-,-JENOPTIK DIGITAL CAMERA-,-")).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, true) property_copy(commentsToStrip, setCommentsToStrip, QString, General, "" /* see constructor */) property_copy(searchForImagesOnStart, setSearchForImagesOnStart, bool, General, true) property_copy(ignoreFileExtension, setIgnoreFileExtension, bool, General, false) property_copy(skipSymlinks, setSkipSymlinks, bool, General, false) property_copy(skipRawIfOtherMatches, setSkipRawIfOtherMatches, bool, General, false) property_copy(useRawThumbnail, setUseRawThumbnail, bool, General, true) property_copy(useRawThumbnailSize, setUseRawThumbnailSize, QSize, General, QSize(1024, 768)) property_copy(useCompressedIndexXML, setUseCompressedIndexXML, bool, General, true) property_copy(compressBackup, setCompressBackup, bool, General, true) property_copy(showSplashScreen, setShowSplashScreen, bool, General, true) property_copy(showHistogram, setShowHistogram, bool, General, true) property_copy(autoSave, setAutoSave, int, General, 5) property_copy(backupCount, setBackupCount, int, General, 5) property_enum(tTimeStamps, setTTimeStamps, TimeStampTrust, General, Always) property_copy(excludeDirectories, setExcludeDirectories, QString, General, QString::fromLatin1("xml,ThumbNails,.thumbs")) property_copy(recentAndroidAddress, setRecentAndroidAddress, QString, General, QString()) property_copy(listenForAndroidDevicesOnStartup, setListenForAndroidDevicesOnStartup, bool, General, false) getValueFunc(QSize, histogramSize, General, QSize(15, 30)) getValueFunc(ViewSortType, viewSortType, General, (int)SortLastUse) getValueFunc(AnnotationDialog::MatchType, matchType, General, (int)AnnotationDialog::MatchFromWordStart) getValueFunc(bool, histogramUseLinearScale, General, false) void SettingsData::setHistogramUseLinearScale(const bool useLinearScale) { if (useLinearScale == histogramUseLinearScale()) return; setValue("General", "histogramUseLinearScale", useLinearScale); emit histogramScaleChanged(); } 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, true) property_copy(modifiedFileComponent, setModifiedFileComponent, QString, FileVersionDetection, "^(.*)-edited.([^.]+)$") property_copy(originalFileComponent, setOriginalFileComponent, QString, FileVersionDetection, "\\1.\\2") property_copy(moveOriginalContents, setMoveOriginalContents, bool, FileVersionDetection, false) property_copy(autoStackNewFiles, setAutoStackNewFiles, bool, FileVersionDetection, true) property_copy(copyFileComponent, setCopyFileComponent, QString, FileVersionDetection, "(.[^.]+)$") property_copy(copyFileReplacementComponent, setCopyFileReplacementComponent, QString, FileVersionDetection, "-edited\\1") //////////////////// //// Thumbnails //// //////////////////// property_copy(displayLabels, setDisplayLabels, bool, Thumbnails, true) property_copy(displayCategories, setDisplayCategories, bool, Thumbnails, false) property_copy(autoShowThumbnailView, setAutoShowThumbnailView, int, Thumbnails, 20) property_copy(showNewestThumbnailFirst, setShowNewestFirst, bool, Thumbnails, false) property_copy(thumbnailDisplayGrid, setThumbnailDisplayGrid, bool, Thumbnails, false) property_copy(previewSize, setPreviewSize, int, Thumbnails, 256) property_copy(thumbnailSpace, setThumbnailSpace, int, Thumbnails, 4) // not available via GUI, but should be consistent (and maybe confgurable for powerusers): property_copy(minimumThumbnailSize, setMinimumThumbnailSize, int, Thumbnails, 32) property_copy(maximumThumbnailSize, setMaximumThumbnailSize, int, Thumbnails, 4096) property_enum(thumbnailAspectRatio, setThumbnailAspectRatio, ThumbnailAspectRatio, Thumbnails, Aspect_3_2) property_ref(backgroundColor, setBackgroundColor, QString, Thumbnails, QColor(Qt::darkGray).name()) property_copy(incrementalThumbnails, setIncrementalThumbnails, bool, Thumbnails, true) // database specific so that changing it doesn't invalidate the thumbnail cache for other databases: getValueFunc_(int, thumbnailSize, groupForDatabase("Thumbnails"), "thumbSize", 256) void SettingsData::setThumbnailSize(int value) { // enforce limits: value = qBound(minimumThumbnailSize(), value, maximumThumbnailSize()); if (value != thumbnailSize()) emit thumbnailSizeChanged(value); setValue(groupForDatabase("Thumbnails"), "thumbSize", value); } int SettingsData::actualThumbnailSize() const { // this is database specific since it's a derived value of thumbnailSize int retval = value(groupForDatabase("Thumbnails"), "actualThumbSize", 0); // if no value has been set, use thumbnailSize if (retval == 0) retval = thumbnailSize(); return retval; } void SettingsData::setActualThumbnailSize(int value) { QPixmapCache::clear(); // enforce limits: value = qBound(minimumThumbnailSize(), value, thumbnailSize()); if (value != actualThumbnailSize()) { setValue(groupForDatabase("Thumbnails"), "actualThumbSize", value); emit actualThumbnailSizeChanged(value); } } //////////////// //// Viewer //// //////////////// property_ref(viewerSize, setViewerSize, QSize, Viewer, QSize(1024, 768)) property_ref(slideShowSize, setSlideShowSize, QSize, Viewer, QSize(1024, 768)) property_copy(launchViewerFullScreen, setLaunchViewerFullScreen, bool, Viewer, false) property_copy(launchSlideShowFullScreen, setLaunchSlideShowFullScreen, bool, Viewer, true) property_copy(showInfoBox, setShowInfoBox, bool, Viewer, true) property_copy(showLabel, setShowLabel, bool, Viewer, true) property_copy(showDescription, setShowDescription, bool, Viewer, true) property_copy(showDate, setShowDate, bool, Viewer, true) property_copy(showImageSize, setShowImageSize, bool, Viewer, true) property_copy(showRating, setShowRating, bool, Viewer, true) property_copy(showTime, setShowTime, bool, Viewer, true) property_copy(showFilename, setShowFilename, bool, Viewer, false) property_copy(showEXIF, setShowEXIF, bool, Viewer, true) property_copy(slideShowInterval, setSlideShowInterval, int, Viewer, 5) property_copy(viewerCacheSize, setViewerCacheSize, int, Viewer, 195) property_copy(infoBoxWidth, setInfoBoxWidth, int, Viewer, 400) property_copy(infoBoxHeight, setInfoBoxHeight, int, Viewer, 300) property_enum(infoBoxPosition, setInfoBoxPosition, Position, Viewer, Bottom) property_enum(viewerStandardSize, setViewerStandardSize, StandardViewSize, Viewer, FullSize) bool SettingsData::smoothScale() const { return _smoothScale; } void SettingsData::setSmoothScale(bool b) { _smoothScale = b; setValue("Viewer", "smoothScale", b); } //////////////////// //// Categories //// //////////////////// setValueFunc(setAlbumCategory, QString &, General, albumCategory) QString SettingsData::albumCategory() const { QString category = value("General", "albumCategory", STR("")); if (!DB::ImageDB::instance()->categoryCollection()->categoryNames().contains(category)) { category = DB::ImageDB::instance()->categoryCollection()->categoryNames()[0]; const_cast<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) /////////////////////// //// 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 a1af6856..2c03a80e 100644 --- a/Settings/SettingsDialog.cpp +++ b/Settings/SettingsDialog.cpp @@ -1,185 +1,186 @@ /* 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 "SettingsDialog.h" #include "config-kpa-kipi.h" -#include <QDialogButtonBox> -#include <QPushButton> -#include <QVBoxLayout> - -#include <KLocalizedString> -#include <KSharedConfig> +#include "SettingsDialog.h" #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> +#include <KLocalizedString> +#include <KSharedConfig> +#include <QDialogButtonBox> +#include <QPushButton> +#include <QVBoxLayout> + struct Data { Settings::SettingsPage page; 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[] = { { SettingsPage::GeneralPage, i18n("General"), "configure-shortcuts", m_generalPage }, { SettingsPage::FileVersionDetectionPage, i18n("File Searching & Versions"), "system-search", m_fileVersionDetectionPage }, { SettingsPage::ThumbnailsPage, i18n("Thumbnail View"), "view-preview", m_thumbnailsPage }, { SettingsPage::CategoryPage, i18n("Categories"), "edit-group", m_categoryPage }, { SettingsPage::BirthdayPage, i18n("Birthdays"), "view-calendar-birthday", m_birthdayPage }, { SettingsPage::TagGroupsPage, i18n("Tag Groups"), "view-group", m_tagGroupsPage }, { SettingsPage::ViewerPage, i18n("Viewer"), "document-preview", m_viewerPage }, #ifdef HASKIPI { SettingsPage::PluginsPage, i18n("Plugins"), "plugins", m_pluginsPage }, #endif { SettingsPage::ExifPage, i18n("Exif/IPTC Information"), "document-properties", m_exifPage }, { SettingsPage::DatabaseBackendPage, i18n("Database Backend"), "document-save", m_databaseBackendPage }, { SettingsPage::GeneralPage, 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); m_pages[data[i].page] = 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(i18nc("@title:window", "Settings")); connect(m_categoryPage, &Settings::CategoryPage::categoryChangesPending, m_tagGroupsPage, &Settings::TagGroupsPage::categoryChangesPending); connect(this, &SettingsDialog::currentPageChanged, m_tagGroupsPage, &Settings::TagGroupsPage::slotPageChange); connect(this, &SettingsDialog::currentPageChanged, m_birthdayPage, &Settings::BirthdayPage::pageChange); // slot is protected -> use old style connect: connect(this, SIGNAL(rejected()), m_categoryPage, SLOT(resetCategoryLabel())); } void Settings::SettingsDialog::show() { Settings::SettingsData *opt = Settings::SettingsData::instance(); m_generalPage->loadSettings(opt); m_fileVersionDetectionPage->loadSettings(opt); m_thumbnailsPage->loadSettings(opt); m_tagGroupsPage->loadSettings(); m_databaseBackendPage->loadSettings(opt); m_viewerPage->loadSettings(opt); #ifdef HASKIPI m_pluginsPage->loadSettings(opt); #endif m_categoryPage->loadSettings(opt); m_exifPage->loadSettings(opt); m_categoryPage->enableDisable(false); m_birthdayPage->reload(); m_categoryPage->resetCategoryNamesChanged(); QDialog::show(); } void Settings::SettingsDialog::activatePage(Settings::SettingsPage pageId) { auto page = m_pages.value(pageId, nullptr); if (page) setCurrentPage(page); } // 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::keyPressEvent(QKeyEvent *) { // This prevents the dialog to be closed if the ENTER key is pressed anywhere } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/TagGroupsPage.cpp b/Settings/TagGroupsPage.cpp index 2bd1b55c..1008f86e 100644 --- a/Settings/TagGroupsPage.cpp +++ b/Settings/TagGroupsPage.cpp @@ -1,813 +1,814 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "TagGroupsPage.h" // Qt includes #include <QAction> #include <QFont> #include <QGridLayout> #include <QHeaderView> #include <QInputDialog> #include <QLabel> #include <QListWidget> #include <QLocale> #include <QMenu> #include <QTreeWidget> #include <QTreeWidgetItem> // KDE includes #include <KLocalizedString> #include <KMessageBox> // Local includes #include "CategoriesGroupsWidget.h" -#include "DB/CategoryCollection.h" -#include "MainWindow/DirtyIndicator.h" -#include "Settings/SettingsData.h" +#include "SettingsData.h" + +#include <DB/CategoryCollection.h> +#include <MainWindow/DirtyIndicator.h> Settings::TagGroupsPage::TagGroupsPage(QWidget *parent) : QWidget(parent) { QGridLayout *layout = new QGridLayout(this); // The category and group tree layout->addWidget(new QLabel(i18nc("@label", "Categories and groups:")), 0, 0); m_categoryTreeWidget = new CategoriesGroupsWidget(this); m_categoryTreeWidget->header()->hide(); m_categoryTreeWidget->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_categoryTreeWidget, 1, 0); connect(m_categoryTreeWidget, &CategoriesGroupsWidget::customContextMenuRequested, this, &TagGroupsPage::showTreeContextMenu); connect(m_categoryTreeWidget, &CategoriesGroupsWidget::itemClicked, this, &TagGroupsPage::slotGroupSelected); // The member list m_selectGroupToAddTags = i18nc("@label/rich", "<strong>Select a group on the left side to add tags to it</strong>"); m_tagsInGroupLabel = new QLabel(m_selectGroupToAddTags); layout->addWidget(m_tagsInGroupLabel, 0, 1); m_membersListWidget = new QListWidget; m_membersListWidget->setEnabled(false); m_membersListWidget->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_membersListWidget, 1, 1); connect(m_membersListWidget, &QListWidget::itemChanged, this, &TagGroupsPage::checkItemSelection); connect(m_membersListWidget, &QListWidget::customContextMenuRequested, this, &TagGroupsPage::showMembersContextMenu); // The "pending rename actions" label m_pendingChangesLabel = new QLabel(i18nc("@label/rich", "<strong>There are pending changes on the categories page.<nl> " "Please save the changes before working on tag groups.</strong>")); m_pendingChangesLabel->hide(); layout->addWidget(m_pendingChangesLabel, 2, 0, 1, 2); QDialog *parentDialog = qobject_cast<QDialog *>(parent); connect(parentDialog, &QDialog::rejected, this, &TagGroupsPage::discardChanges); // Context menu actions m_newGroupAction = new QAction(i18nc("@action:inmenu", "Add group ..."), this); connect(m_newGroupAction, &QAction::triggered, this, &TagGroupsPage::slotAddGroup); m_renameAction = new QAction(this); connect(m_renameAction, &QAction::triggered, this, &TagGroupsPage::slotRenameGroup); m_deleteAction = new QAction(this); connect(m_deleteAction, &QAction::triggered, this, &TagGroupsPage::slotDeleteGroup); m_deleteMemberAction = new QAction(this); connect(m_deleteMemberAction, &QAction::triggered, this, &TagGroupsPage::slotDeleteMember); m_renameMemberAction = new QAction(this); connect(m_renameMemberAction, &QAction::triggered, this, &TagGroupsPage::slotRenameMember); m_memberMap = DB::ImageDB::instance()->memberMap(); connect(DB::ImageDB::instance()->categoryCollection(), SIGNAL(itemRemoved(DB::Category *, QString)), &m_memberMap, SLOT(deleteItem(DB::Category *, QString))); connect(DB::ImageDB::instance()->categoryCollection(), SIGNAL(itemRenamed(DB::Category *, QString, QString)), &m_memberMap, SLOT(renameItem(DB::Category *, QString, QString))); connect(DB::ImageDB::instance()->categoryCollection(), SIGNAL(categoryRemoved(QString)), &m_memberMap, SLOT(deleteCategory(QString))); m_dataChanged = false; } void Settings::TagGroupsPage::updateCategoryTree() { // Store all expanded items so that they can be expanded after reload QList<QPair<QString, QString>> expandedItems = QList<QPair<QString, QString>>(); QTreeWidgetItemIterator it(m_categoryTreeWidget); while (*it) { if ((*it)->isExpanded()) { if ((*it)->parent() == nullptr) { expandedItems.append(QPair<QString, QString>((*it)->text(0), QString())); } else { expandedItems.append( QPair<QString, QString>((*it)->text(0), (*it)->parent()->text(0))); } } ++it; } m_categoryTreeWidget->clear(); // Create a tree view of all groups and their sub-groups QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH (const DB::CategoryPtr category, categories) { if (category->isSpecialCategory()) { continue; } // Add the real categories as top-level items QTreeWidgetItem *topLevelItem = new QTreeWidgetItem; topLevelItem->setText(0, category->name()); topLevelItem->setFlags(topLevelItem->flags() & Qt::ItemIsEnabled); QFont font = topLevelItem->font(0); font.setWeight(QFont::Bold); topLevelItem->setFont(0, font); m_categoryTreeWidget->addTopLevelItem(topLevelItem); // Build a map with all members for each group QMap<QString, QStringList> membersForGroup; QStringList allGroups = m_memberMap.groups(category->name()); foreach (const QString &group, allGroups) { // FIXME: Why does the member map return an empty category?! if (group.isEmpty()) { continue; } QStringList allMembers = m_memberMap.members(category->name(), group, false); foreach (const QString &member, allMembers) { membersForGroup[group] << member; } // We add an empty member placeholder if the group currently has no members. // Otherwise, it won't be added. if (!membersForGroup.contains(group)) { membersForGroup[group] = QStringList(); } } // Add all groups (their sub-groups will be added recursively) addSubCategories(topLevelItem, membersForGroup, allGroups); } // Order the items alphabetically m_categoryTreeWidget->sortItems(0, Qt::AscendingOrder); // Re-expand all previously expanded items QTreeWidgetItemIterator it2(m_categoryTreeWidget); while (*it2) { if ((*it2)->parent() == nullptr) { if (expandedItems.contains(QPair<QString, QString>((*it2)->text(0), QString()))) { (*it2)->setExpanded(true); } } else { if (expandedItems.contains(QPair<QString, QString>((*it2)->text(0), (*it2)->parent()->text(0)))) { (*it2)->setExpanded(true); } } ++it2; } } void Settings::TagGroupsPage::addSubCategories(QTreeWidgetItem *superCategory, QMap<QString, QStringList> &membersForGroup, QStringList &allGroups) { // Process all group members QMap<QString, QStringList>::iterator memIt1; for (memIt1 = membersForGroup.begin(); memIt1 != membersForGroup.end(); ++memIt1) { bool isSubGroup = false; // Search for a membership in another group for the current group QMap<QString, QStringList>::iterator memIt2; for (memIt2 = membersForGroup.begin(); memIt2 != membersForGroup.end(); ++memIt2) { if (memIt2.value().contains(memIt1.key())) { isSubGroup = true; break; } } // Add the group if it's not member of another group if (!isSubGroup) { QTreeWidgetItem *group = new QTreeWidgetItem; group->setText(0, memIt1.key()); superCategory->addChild(group); // Search the member list for other groups QMap<QString, QStringList> subGroups; foreach (const QString &groupName, allGroups) { if (membersForGroup[memIt1.key()].contains(groupName)) { subGroups[groupName] = membersForGroup[groupName]; } } // If the list contains other groups, add them recursively if (subGroups.count() > 0) { addSubCategories(group, subGroups, allGroups); } } } } QString Settings::TagGroupsPage::getCategory(QTreeWidgetItem *currentItem) { while (currentItem->parent() != nullptr) { currentItem = currentItem->parent(); } return currentItem->text(0); } void Settings::TagGroupsPage::showTreeContextMenu(QPoint point) { QTreeWidgetItem *currentItem = m_categoryTreeWidget->currentItem(); if (currentItem == nullptr) { return; } m_currentSubCategory = currentItem->text(0); if (currentItem->parent() == nullptr) { // It's a top-level, "real" category m_currentSuperCategory.clear(); } else { // It's a normal sub-category that belongs to another one m_currentSuperCategory = currentItem->parent()->text(0); } m_currentCategory = getCategory(currentItem); QMenu *menu = new QMenu; menu->addAction(m_newGroupAction); // "Real" top-level categories have to processed on the category page. if (!m_currentSuperCategory.isEmpty()) { menu->addSeparator(); m_renameAction->setText(i18nc("@action:inmenu", "Rename group \"%1\"", m_currentSubCategory)); menu->addAction(m_renameAction); m_deleteAction->setText(i18nc("@action:inmenu", "Delete group \"%1\"", m_currentSubCategory)); menu->addAction(m_deleteAction); } menu->exec(m_categoryTreeWidget->mapToGlobal(point)); delete menu; } void Settings::TagGroupsPage::categoryChanged(const QString &name) { if (name.isEmpty()) { return; } m_membersListWidget->blockSignals(true); m_membersListWidget->clear(); QStringList list = getCategoryObject(name)->items(); list += m_memberMap.groups(name); QStringList alreadyAdded; Q_FOREACH (const QString &member, list) { if (member.isEmpty()) { // This can happen if we add group that currently has no members. continue; } if (!alreadyAdded.contains(member)) { alreadyAdded << member; if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && !Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (name == Settings::SettingsData::instance()->untaggedCategory()) { if (member == Settings::SettingsData::instance()->untaggedTag()) { continue; } } } QListWidgetItem *newItem = new QListWidgetItem(member, m_membersListWidget); newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); newItem->setCheckState(Qt::Unchecked); } } m_currentGroup.clear(); m_membersListWidget->clearSelection(); m_membersListWidget->sortItems(); m_membersListWidget->setEnabled(false); m_membersListWidget->blockSignals(false); } void Settings::TagGroupsPage::slotGroupSelected(QTreeWidgetItem *item) { // When something else than a "real" category has been selected before, // we have to save it's members. if (!m_currentGroup.isEmpty()) { saveOldGroup(); } if (item->parent() == nullptr) { // A "real" category has been selected, not a group m_currentCategory.clear(); m_currentGroup.clear(); m_membersListWidget->setEnabled(false); categoryChanged(item->text(0)); m_tagsInGroupLabel->setText(m_selectGroupToAddTags); return; } // Let's see if the category changed QString itemCategory = getCategory(item); if (m_currentCategory != itemCategory) { m_currentCategory = itemCategory; categoryChanged(m_currentCategory); } m_currentGroup = item->text(0); selectMembers(m_currentGroup); m_tagsInGroupLabel->setText(i18nc("@label", "Tags in group \"%1\" of category \"%2\"", m_currentGroup, m_currentCategory)); } void Settings::TagGroupsPage::slotAddGroup() { bool ok; DB::CategoryPtr category = getCategoryObject(m_currentCategory); QStringList groups = m_memberMap.groups(m_currentCategory); QStringList tags; for (QString tag : category->items()) { if (!groups.contains(tag)) { tags << tag; } } //// reject existing group names: //KStringListValidator validator(groups); //QString newSubCategory = KInputDialog::getText(i18nc("@title:window","New Group"), // i18nc("@label:textbox","Group name:"), // QString() /*value*/, // &ok, // this /*parent*/, // &validator, // QString() /*mask*/, // QString() /*WhatsThis*/, // tags /*completion*/ // ); // FIXME: KF5-port: QInputDialog does not accept a validator, // and KInputDialog was removed in KF5. -> Reimplement input validation using other stuff QString newSubCategory = QInputDialog::getText(this, i18nc("@title:window", "New Group"), i18nc("@label:textbox", "Group name:"), QLineEdit::Normal, QString(), &ok); if (groups.contains(newSubCategory)) return; // only a workaround until GUI-support for validation is restored if (!ok) { return; } // Let's see if we already have this group if (groups.contains(newSubCategory)) { // (with the validator working correctly, we should not get to this point) KMessageBox::sorry(this, i18nc("@info", "<p>The group \"%1\" already exists.</p>", newSubCategory), i18nc("@title:window", "Cannot add group")); return; } // Add the group as a new tag to the respective category MainWindow::DirtyIndicator::suppressMarkDirty(true); category->addItem(newSubCategory); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap<CategoryEdit, QString> categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Add] = newSubCategory; m_categoryChanges.append(categoryChange); m_dataChanged = true; // Add the group m_memberMap.addGroup(m_currentCategory, newSubCategory); // Display the new group categoryChanged(m_currentCategory); // Display the new item QTreeWidgetItem *parentItem = m_categoryTreeWidget->currentItem(); addNewSubItem(newSubCategory, parentItem); // Check if we also have to update some other group (in case this is not a top-level group) if (!m_currentSuperCategory.isEmpty()) { m_memberMap.addMemberToGroup(m_currentCategory, parentItem->text(0), newSubCategory); slotGroupSelected(parentItem); } m_dataChanged = true; } void Settings::TagGroupsPage::addNewSubItem(QString &name, QTreeWidgetItem *parentItem) { QTreeWidgetItem *newItem = new QTreeWidgetItem; newItem->setText(0, name); parentItem->addChild(newItem); if (!parentItem->isExpanded()) { parentItem->setExpanded(true); } } QTreeWidgetItem *Settings::TagGroupsPage::findCategoryItem(QString category) { QTreeWidgetItem *categoryItem = nullptr; for (int i = 0; i < m_categoryTreeWidget->topLevelItemCount(); ++i) { categoryItem = m_categoryTreeWidget->topLevelItem(i); if (categoryItem->text(0) == category) { break; } } return categoryItem; } void Settings::TagGroupsPage::checkItemSelection(QListWidgetItem *) { m_dataChanged = true; saveOldGroup(); updateCategoryTree(); } void Settings::TagGroupsPage::slotRenameGroup() { bool ok; DB::CategoryPtr category = getCategoryObject(m_currentCategory); QStringList groups = m_memberMap.groups(m_currentCategory); QStringList tags; for (QString tag : category->items()) { if (!groups.contains(tag)) { tags << tag; } } // FIXME: reject existing group names QString newSubCategoryName = QInputDialog::getText(this, i18nc("@title:window", "Rename Group"), i18nc("@label:textbox", "New group name:"), QLineEdit::Normal, m_currentSubCategory, &ok); if (!ok || m_currentSubCategory == newSubCategoryName) { return; } if (groups.contains(newSubCategoryName)) { // (with the validator working correctly, we should not get to this point) KMessageBox::sorry(this, xi18nc("@info", "<para>Cannot rename group \"%1\" to \"%2\": " "\"%2\" already exists in category \"%3\"</para>", m_currentSubCategory, newSubCategoryName, m_currentCategory), i18nc("@title:window", "Rename Group")); return; } QTreeWidgetItem *selectedGroup = m_categoryTreeWidget->currentItem(); saveOldGroup(); // Update the group m_memberMap.renameGroup(m_currentCategory, m_currentSubCategory, newSubCategoryName); // Update the tag in the respective category MainWindow::DirtyIndicator::suppressMarkDirty(true); category->renameItem(m_currentSubCategory, newSubCategoryName); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap<CategoryEdit, QString> categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Rename] = m_currentSubCategory; categoryChange[CategoryEdit::NewName] = newSubCategoryName; m_categoryChanges.append(categoryChange); m_dataChanged = true; // Search for all possible sub-category items in this category that have to be renamed QTreeWidgetItem *categoryItem = findCategoryItem(m_currentCategory); for (int i = 0; i < categoryItem->childCount(); ++i) { renameAllSubCategories(categoryItem->child(i), m_currentSubCategory, newSubCategoryName); } // Update the displayed items categoryChanged(m_currentCategory); slotGroupSelected(selectedGroup); m_dataChanged = true; } void Settings::TagGroupsPage::renameAllSubCategories(QTreeWidgetItem *categoryItem, QString oldName, QString newName) { // Probably, it item itself has to be renamed if (categoryItem->text(0) == oldName) { categoryItem->setText(0, newName); } // Also check all sub-categories recursively for (int i = 0; i < categoryItem->childCount(); ++i) { renameAllSubCategories(categoryItem->child(i), oldName, newName); } } void Settings::TagGroupsPage::slotDeleteGroup() { QTreeWidgetItem *currentItem = m_categoryTreeWidget->currentItem(); QString message; QString title; if (currentItem->childCount() > 0) { message = xi18nc("@info", "<para>Really delete group \"%1\"?</para>" "<para>Sub-categories of this group will be moved to the super category of \"%1\" (\"%2\").<nl/> " "All other memberships of the sub-categories will stay intact.</para>", m_currentSubCategory, m_currentSuperCategory); } else { message = xi18nc("@info", "<para>Really delete group \"%1\"?</para>", m_currentSubCategory); } int res = KMessageBox::warningContinueCancel(this, message, i18nc("@title:window", "Delete Group"), KGuiItem(i18n("&Delete"), QString::fromUtf8("editdelete"))); if (res == KMessageBox::Cancel) { return; } // Delete the group m_memberMap.deleteGroup(m_currentCategory, m_currentSubCategory); // Delete the tag MainWindow::DirtyIndicator::suppressMarkDirty(true); getCategoryObject(m_currentCategory)->removeItem(m_currentSubCategory); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap<CategoryEdit, QString> categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Remove] = m_currentSubCategory; m_categoryChanges.append(categoryChange); m_dataChanged = true; slotPageChange(); m_dataChanged = true; } void Settings::TagGroupsPage::saveOldGroup() { QStringList list; for (int i = 0; i < m_membersListWidget->count(); ++i) { QListWidgetItem *item = m_membersListWidget->item(i); if (item->checkState() == Qt::Checked) { list << item->text(); } } m_memberMap.setMembers(m_currentCategory, m_currentGroup, list); } void Settings::TagGroupsPage::selectMembers(const QString &group) { m_membersListWidget->blockSignals(true); m_membersListWidget->setEnabled(false); m_currentGroup = group; QStringList memberList = m_memberMap.members(m_currentCategory, group, false); for (int i = 0; i < m_membersListWidget->count(); ++i) { QListWidgetItem *item = m_membersListWidget->item(i); item->setCheckState(Qt::Unchecked); if (!m_memberMap.canAddMemberToGroup(m_currentCategory, group, item->text())) { item->setFlags(item->flags() & ~Qt::ItemIsSelectable & ~Qt::ItemIsEnabled); } else { item->setFlags(item->flags() | Qt::ItemIsSelectable | Qt::ItemIsEnabled); if (memberList.contains(item->text())) { item->setCheckState(Qt::Checked); } } } m_membersListWidget->setEnabled(true); m_membersListWidget->blockSignals(false); } void Settings::TagGroupsPage::slotPageChange() { m_tagsInGroupLabel->setText(m_selectGroupToAddTags); m_membersListWidget->setEnabled(false); m_membersListWidget->clear(); m_currentCategory.clear(); updateCategoryTree(); } void Settings::TagGroupsPage::saveSettings() { saveOldGroup(); slotPageChange(); DB::ImageDB::instance()->memberMap() = m_memberMap; m_categoryChanges.clear(); if (m_dataChanged) { m_dataChanged = false; MainWindow::DirtyIndicator::markDirty(); } m_categoryTreeWidget->setEnabled(true); m_membersListWidget->setEnabled(true); m_pendingChangesLabel->hide(); } void Settings::TagGroupsPage::discardChanges() { m_memberMap = DB::ImageDB::instance()->memberMap(); slotPageChange(); m_dataChanged = false; // Revert all changes to the "real" category objects MainWindow::DirtyIndicator::suppressMarkDirty(true); for (int i = m_categoryChanges.size() - 1; i >= 0; i--) { DB::CategoryPtr category = getCategoryObject(m_categoryChanges.at(i)[CategoryEdit::Category]); if (m_categoryChanges.at(i).contains(CategoryEdit::Add)) { // Remove added tags category->removeItem(m_categoryChanges.at(i)[CategoryEdit::Add]); } else if (m_categoryChanges.at(i).contains(CategoryEdit::Remove)) { // Add removed tags category->addItem(m_categoryChanges.at(i)[CategoryEdit::Add]); } else if (m_categoryChanges.at(i).contains(CategoryEdit::Rename)) { // Re-rename tags to their old name category->renameItem(m_categoryChanges.at(i)[CategoryEdit::NewName], m_categoryChanges.at(i)[Rename]); } } MainWindow::DirtyIndicator::suppressMarkDirty(false); m_categoryChanges.clear(); m_categoryTreeWidget->setEnabled(true); m_membersListWidget->setEnabled(true); m_pendingChangesLabel->hide(); } void Settings::TagGroupsPage::loadSettings() { categoryChanged(m_currentCategory); updateCategoryTree(); } void Settings::TagGroupsPage::categoryChangesPending() { m_categoryTreeWidget->setEnabled(false); m_membersListWidget->setEnabled(false); m_pendingChangesLabel->show(); } DB::MemberMap *Settings::TagGroupsPage::memberMap() { return &m_memberMap; } void Settings::TagGroupsPage::processDrop(QTreeWidgetItem *draggedItem, QTreeWidgetItem *targetItem) { if (targetItem->parent() != nullptr) { // Dropped on a group // Select the group m_categoryTreeWidget->setCurrentItem(targetItem); slotGroupSelected(targetItem); // Check the dragged group on the member side to make it a sub-group of the target group m_membersListWidget->findItems(draggedItem->text(0), Qt::MatchExactly)[0]->setCheckState(Qt::Checked); } else { // Dropped on a top-level category // Check if it's already a direct child of the category. // If so, we don't need to do anything. QTreeWidgetItem *parent = draggedItem->parent(); if (parent->parent() == nullptr) { return; } // Select the former super group m_categoryTreeWidget->setCurrentItem(parent); slotGroupSelected(parent); // Deselect the dragged group (this will bring it to the top level) m_membersListWidget->findItems(draggedItem->text(0), Qt::MatchExactly)[0]->setCheckState(Qt::Unchecked); } } void Settings::TagGroupsPage::showMembersContextMenu(QPoint point) { if (m_membersListWidget->currentItem() == nullptr) { return; } QMenu *menu = new QMenu; m_renameMemberAction->setText(i18nc("@action:inmenu", "Rename \"%1\"", m_membersListWidget->currentItem()->text())); menu->addAction(m_renameMemberAction); m_deleteMemberAction->setText(i18nc("@action:inmenu", "Delete \"%1\"", m_membersListWidget->currentItem()->text())); menu->addAction(m_deleteMemberAction); menu->exec(m_membersListWidget->mapToGlobal(point)); delete menu; } void Settings::TagGroupsPage::slotRenameMember() { bool ok; QString newTagName = QInputDialog::getText(this, i18nc("@title:window", "New Tag Name"), i18nc("@label:textbox", "Tag name:"), QLineEdit::Normal, m_membersListWidget->currentItem()->text(), &ok); if (!ok || newTagName == m_membersListWidget->currentItem()->text()) { return; } // Update the tag name in the database MainWindow::DirtyIndicator::suppressMarkDirty(true); getCategoryObject(m_currentCategory)->renameItem(m_membersListWidget->currentItem()->text(), newTagName); MainWindow::DirtyIndicator::suppressMarkDirty(false); QMap<CategoryEdit, QString> categoryChange; categoryChange[CategoryEdit::Category] = m_currentCategory; categoryChange[CategoryEdit::Rename] = m_membersListWidget->currentItem()->text(); categoryChange[CategoryEdit::NewName] = newTagName; m_categoryChanges.append(categoryChange); // Update the displayed tag name m_membersListWidget->currentItem()->setText(newTagName); // Re-order the tags, as their alphabetial order may have changed m_membersListWidget->sortItems(); } void Settings::TagGroupsPage::slotDeleteMember() { QString memberToDelete = m_membersListWidget->currentItem()->text(); if (m_memberMap.groups(m_currentCategory).contains(memberToDelete)) { // The item to delete is a group // Find the tag in the tree view and select it ... QTreeWidgetItemIterator it(m_categoryTreeWidget); while (*it) { if ((*it)->text(0) == memberToDelete && getCategory((*it)) == m_currentCategory) { m_categoryTreeWidget->setCurrentItem((*it)); m_currentSubCategory = (*it)->text(0); m_currentSuperCategory = (*it)->parent()->text(0); break; } ++it; } // ... then delete it like it had been requested by the TreeWidget's context menu slotDeleteGroup(); } else { // The item to delete is a normal tag int res = KMessageBox::warningContinueCancel(this, xi18nc("@info", "<para>Do you really want to delete \"%1\"?</para>" "<para>Deleting the item will remove any information " "about it from any image containing the item.</para>", memberToDelete), i18nc("@title:window", "Really delete %1?", memberToDelete), KGuiItem(i18n("&Delete"), QString::fromUtf8("editdelete"))); if (res != KMessageBox::Continue) { return; } // Delete the tag as if it had been deleted from the annotation dialog. getCategoryObject(m_currentCategory)->removeItem(memberToDelete); slotPageChange(); } } DB::CategoryPtr Settings::TagGroupsPage::getCategoryObject(QString category) const { return DB::ImageDB::instance()->categoryCollection()->categoryForName(category); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/ThumbnailsPage.cpp b/Settings/ThumbnailsPage.cpp index 2f7c51f1..22d24484 100644 --- a/Settings/ThumbnailsPage.cpp +++ b/Settings/ThumbnailsPage.cpp @@ -1,193 +1,195 @@ /* Copyright (C) 2003-2010 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 "ThumbnailsPage.h" + #include "SettingsData.h" + #include <KColorButton> #include <KComboBox> #include <KLocalizedString> #include <QCheckBox> #include <QGridLayout> #include <QLabel> #include <QSpinBox> Settings::ThumbnailsPage::ThumbnailsPage(QWidget *parent) : QWidget(parent) { QGridLayout *lay = new QGridLayout(this); lay->setSpacing(6); int row = 0; // Preview size QLabel *previewSizeLabel = new QLabel(i18n("Tooltip preview image size:")); m_previewSize = new QSpinBox; m_previewSize->setRange(0, 2000); m_previewSize->setSingleStep(10); m_previewSize->setSpecialValueText(i18n("No Image Preview")); lay->addWidget(previewSizeLabel, row, 0); lay->addWidget(m_previewSize, row, 1); // Thumbnail size ++row; QLabel *thumbnailSizeLabel = new QLabel(i18n("Thumbnail image size:")); m_thumbnailSize = new QSpinBox; // range set from settings on load m_thumbnailSize->setSingleStep(16); lay->addWidget(thumbnailSizeLabel, row, 0); lay->addWidget(m_thumbnailSize, row, 1); // incremental Thumbnail building ++row; m_incrementalThumbnails = new QCheckBox(i18n("Build thumbnails on demand")); lay->addWidget(m_incrementalThumbnails, row, 0, 1, 2); // Thumbnail aspect ratio ++row; QLabel *thumbnailAspectRatioLabel = new QLabel(i18n("Thumbnail table cells aspect ratio:")); m_thumbnailAspectRatio = new KComboBox(this); m_thumbnailAspectRatio->addItems(QStringList() << i18n("1:1") << i18n("4:3") << i18n("3:2") << i18n("16:9") << i18n("3:4") << i18n("2:3") << i18n("9:16")); lay->addWidget(thumbnailAspectRatioLabel, row, 0); lay->addWidget(m_thumbnailAspectRatio, row, 1); // Space around cells ++row; QLabel *thumbnailSpaceLabel = new QLabel(i18n("Space around cells:")); m_thumbnailSpace = new QSpinBox; m_thumbnailSpace->setRange(0, 20); lay->addWidget(thumbnailSpaceLabel, row, 0); lay->addWidget(m_thumbnailSpace, row, 1); // Background color ++row; QLabel *backgroundColorLabel = new QLabel(i18n("Background color:")); m_backgroundColor = new KColorButton; lay->addWidget(backgroundColorLabel, row, 0); lay->addWidget(m_backgroundColor, row, 1); // Display grid lines in the thumbnail view ++row; m_thumbnailDisplayGrid = new QCheckBox(i18n("Display grid around thumbnails")); lay->addWidget(m_thumbnailDisplayGrid, row, 0, 1, 2); // Display Labels ++row; m_displayLabels = new QCheckBox(i18n("Display labels in thumbnail view")); lay->addWidget(m_displayLabels, row, 0, 1, 2); // Display Categories ++row; m_displayCategories = new QCheckBox(i18n("Display categories in thumbnail view")); lay->addWidget(m_displayCategories, row, 0, 1, 2); // Auto Show Thumbnail view ++row; QLabel *autoShowLabel = new QLabel(i18n("Threshold for automatic thumbnail view: "), this); m_autoShowThumbnailView = new QSpinBox; m_autoShowThumbnailView->setRange(0, 10000); m_autoShowThumbnailView->setSingleStep(10); m_autoShowThumbnailView->setSpecialValueText(i18nc("Describing: 'ThumbnailView will not be automatically shown'", "Disabled")); lay->addWidget(autoShowLabel, row, 0); lay->addWidget(m_autoShowThumbnailView, row, 1); lay->setColumnStretch(1, 1); lay->setRowStretch(++row, 1); // Whats This QString txt; txt = i18n("<p>If you select <b>Settings -> Show Tooltips</b> in the thumbnail view, then you will see a small tool tip window " "displaying information about the thumbnails. This window includes a small preview image. " "This option configures the image size.</p>"); previewSizeLabel->setWhatsThis(txt); m_previewSize->setWhatsThis(txt); txt = i18n("<p>Thumbnail image size. Changing the thumbnail size here triggers a rebuild of the thumbnail database.</p>"); thumbnailSizeLabel->setWhatsThis(txt); m_thumbnailSize->setWhatsThis(txt); txt = i18n("<p>If this is set, thumbnails are built on demand. As you browse your image database, " "only those thumbnails that are needed are actually built. " "This means that when you change the thumbnail size, KPhotoAlbum will remain responsive " "even if you have lots of images.</p>" "<p>If this is not set, KPhotoAlbum will always build the thumbnails for all images as soon as possible. " "This means that when new images are found, KPhotoAlbum will immediately build thumbnails " "for them and you won't have a delay later while browsing.</p>"); m_incrementalThumbnails->setWhatsThis(txt); txt = i18n("<p>Choose what aspect ratio the cells holding thumbnails should have.</p>"); m_thumbnailAspectRatio->setWhatsThis(txt); txt = i18n("<p>How thick the cell padding should be.</p>"); thumbnailSpaceLabel->setWhatsThis(txt); txt = i18n("<p>Background color to use in the thumbnail viewer</p>"); backgroundColorLabel->setWhatsThis(txt); m_backgroundColor->setWhatsThis(txt); txt = i18n("<p>If you want to see grid around your thumbnail images, " "select this option.</p>"); m_thumbnailDisplayGrid->setWhatsThis(txt); txt = i18n("<p>Checking this option will show the base name for the file under " "thumbnails in the thumbnail view.</p>"); m_displayLabels->setWhatsThis(txt); txt = i18n("<p>Checking this option will show the Categories for the file under " "thumbnails in the thumbnail view</p>"); m_displayCategories->setWhatsThis(txt); txt = i18n("<p>When you are browsing, and the count gets below the value specified here, " "the thumbnails will be shown automatically. The alternative is to continue showing the " "browser until you press <i>Show Images</i></p>"); m_autoShowThumbnailView->setWhatsThis(txt); autoShowLabel->setWhatsThis(txt); } void Settings::ThumbnailsPage::loadSettings(Settings::SettingsData *opt) { m_previewSize->setValue(opt->previewSize()); m_thumbnailSize->setMinimum(opt->minimumThumbnailSize()); m_thumbnailSize->setMaximum(opt->maximumThumbnailSize()); m_thumbnailSize->setValue(opt->thumbnailSize()); m_backgroundColor->setColor(QColor(opt->backgroundColor())); m_thumbnailDisplayGrid->setChecked(opt->thumbnailDisplayGrid()); m_thumbnailAspectRatio->setCurrentIndex(opt->thumbnailAspectRatio()); m_thumbnailSpace->setValue(opt->thumbnailSpace()); m_displayLabels->setChecked(opt->displayLabels()); m_displayCategories->setChecked(opt->displayCategories()); m_autoShowThumbnailView->setValue(opt->autoShowThumbnailView()); m_incrementalThumbnails->setChecked(opt->incrementalThumbnails()); } void Settings::ThumbnailsPage::saveSettings(Settings::SettingsData *opt) { opt->setPreviewSize(m_previewSize->value()); opt->setThumbnailSize(m_thumbnailSize->value()); // ensure that the user actually sees the thumbnail size change: opt->setActualThumbnailSize(m_thumbnailSize->value()); opt->setThumbnailAspectRatio((ThumbnailAspectRatio)m_thumbnailAspectRatio->currentIndex()); opt->setBackgroundColor(m_backgroundColor->color().name()); opt->setThumbnailDisplayGrid(m_thumbnailDisplayGrid->isChecked()); opt->setThumbnailSpace(m_thumbnailSpace->value()); opt->setDisplayLabels(m_displayLabels->isChecked()); opt->setDisplayCategories(m_displayCategories->isChecked()); opt->setAutoShowThumbnailView(m_autoShowThumbnailView->value()); opt->setIncrementalThumbnails(m_incrementalThumbnails->isChecked()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/UntaggedGroupBox.cpp b/Settings/UntaggedGroupBox.cpp index 78f5557a..f8863741 100644 --- a/Settings/UntaggedGroupBox.cpp +++ b/Settings/UntaggedGroupBox.cpp @@ -1,156 +1,159 @@ /* Copyright (C) 2003-2010 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 "UntaggedGroupBox.h" -#include "DB/CategoryCollection.h" + #include "SettingsData.h" + +#include <DB/CategoryCollection.h> #include <DB/ImageDB.h> + #include <KLocalizedString> #include <QCheckBox> #include <QComboBox> #include <QGridLayout> #include <QLabel> #include <QMessageBox> Settings::UntaggedGroupBox::UntaggedGroupBox(QWidget *parent) : QGroupBox(i18n("Untagged Images"), parent) { setWhatsThis(i18n("If a tag is selected here, it will be added to new (untagged) images " "automatically, so that they can be easily found. It will be removed as " "soon as the image has been annotated.")); QGridLayout *grid = new QGridLayout(this); int row = -1; QLabel *label = new QLabel(i18n("Category:")); grid->addWidget(label, ++row, 0); m_category = new QComboBox; grid->addWidget(m_category, row, 1); connect(m_category, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &UntaggedGroupBox::populateTagsCombo); label = new QLabel(i18n("Tag:")); grid->addWidget(label, ++row, 0); m_tag = new QComboBox; grid->addWidget(m_tag, row, 1); m_tag->setEditable(true); m_showUntaggedImagesTag = new QCheckBox(i18n("Show the untagged images tag as a normal tag")); grid->addWidget(m_showUntaggedImagesTag, ++row, 0, 1, 2); grid->setColumnStretch(1, 1); } void Settings::UntaggedGroupBox::populateCategoryComboBox() { m_category->clear(); m_category->addItem(i18n("None Selected")); Q_FOREACH (DB::CategoryPtr category, DB::ImageDB::instance()->categoryCollection()->categories()) { if (!category->isSpecialCategory()) m_category->addItem(category->name(), category->name()); } } void Settings::UntaggedGroupBox::populateTagsCombo() { m_tag->clear(); const QString currentCategory = m_category->itemData(m_category->currentIndex()).value<QString>(); if (currentCategory.isEmpty()) m_tag->setEnabled(false); else { m_tag->setEnabled(true); const QStringList items = DB::ImageDB::instance()->categoryCollection()->categoryForName(currentCategory)->items(); m_tag->addItems(items); } } void Settings::UntaggedGroupBox::loadSettings(Settings::SettingsData *opt) { populateCategoryComboBox(); const QString category = opt->untaggedCategory(); const QString tag = opt->untaggedTag(); int categoryIndex = m_category->findData(category); if (categoryIndex == -1) categoryIndex = 0; m_category->setCurrentIndex(categoryIndex); populateTagsCombo(); if (categoryIndex != 0) { int tagIndex = m_tag->findText(tag); if (tagIndex == -1) { m_tag->addItem(tag); tagIndex = m_tag->findText(tag); Q_ASSERT(tagIndex != -1); } m_tag->setCurrentIndex(tagIndex); } m_showUntaggedImagesTag->setChecked(opt->untaggedImagesTagVisible()); } void Settings::UntaggedGroupBox::saveSettings(Settings::SettingsData *opt) { const QString category = m_category->itemData(m_category->currentIndex()).value<QString>(); QString untaggedTag = m_tag->currentText().simplified(); if (!category.isEmpty()) { // Add a new tag if the entered one is not in the DB yet DB::CategoryPtr categoryPointer = DB::ImageDB::instance()->categoryCollection()->categoryForName(category); if (!categoryPointer->items().contains(untaggedTag)) { categoryPointer->addItem(untaggedTag); QMessageBox::information(this, i18n("New tag added"), i18n("<p>The new tag \"%1\" has been added to the category \"%2\" and will be used " "for untagged images now.</p>" "<p>Please save now, so that this tag will be stored in the database. " "Otherwise, it will be lost, and you will get an error about this tag being " "not present on the next start.</p>", untaggedTag, category)); } opt->setUntaggedCategory(category); opt->setUntaggedTag(untaggedTag); } else { // If no untagged images tag is selected, remove the setting by using an empty string opt->setUntaggedCategory(QString()); opt->setUntaggedTag(QString()); } opt->setUntaggedImagesTagVisible(m_showUntaggedImagesTag->isChecked()); } void Settings::UntaggedGroupBox::categoryDeleted(QString categoryName) { if (categoryName == m_category->itemData(m_category->currentIndex()).value<QString>()) { m_category->setCurrentIndex(0); } m_category->removeItem(m_category->findText(categoryName)); } void Settings::UntaggedGroupBox::categoryRenamed(QString oldCategoryName, QString newCategoryName) { const int index = m_category->findText(oldCategoryName); m_category->setItemText(index, newCategoryName); m_category->setItemData(index, newCategoryName); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/ViewerPage.cpp b/Settings/ViewerPage.cpp index a32310ea..18c23fed 100644 --- a/Settings/ViewerPage.cpp +++ b/Settings/ViewerPage.cpp @@ -1,113 +1,115 @@ /* Copyright (C) 2003-2010 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 "ViewerPage.h" + #include "SettingsData.h" #include "ViewerSizeConfig.h" + #include <KComboBox> #include <KLocalizedString> #include <QGridLayout> #include <QLabel> #include <QSpinBox> #include <QVBoxLayout> Settings::ViewerPage::ViewerPage(QWidget *parent) : QWidget(parent) { QVBoxLayout *lay1 = new QVBoxLayout(this); m_slideShowSetup = new ViewerSizeConfig(i18n("Running Slide Show From Thumbnail View"), this); lay1->addWidget(m_slideShowSetup); m_viewImageSetup = new ViewerSizeConfig(i18n("Viewing Images and Videos From Thumbnail View"), this); lay1->addWidget(m_viewImageSetup); QGridLayout *glay = new QGridLayout; lay1->addLayout(glay); QLabel *label = new QLabel(i18n("Slideshow interval:"), this); glay->addWidget(label, 0, 0); m_slideShowInterval = new QSpinBox; m_slideShowInterval->setRange(1, INT_MAX); glay->addWidget(m_slideShowInterval, 0, 1); m_slideShowInterval->setSuffix(i18n(" sec")); label->setBuddy(m_slideShowInterval); label = new QLabel(i18n("Image cache:"), this); glay->addWidget(label, 1, 0); m_cacheSize = new QSpinBox; m_cacheSize->setRange(0, 16384); m_cacheSize->setSingleStep(10); m_cacheSize->setSuffix(i18n(" Mbytes")); glay->addWidget(m_cacheSize, 1, 1); label->setBuddy(m_cacheSize); QString txt; QLabel *standardSizeLabel = new QLabel(i18n("Standard size in viewer:"), this); m_viewerStandardSize = new KComboBox(this); m_viewerStandardSize->addItems(QStringList() << i18n("Full Viewer Size") << i18n("Natural Image Size") << i18n("Natural Image Size If Possible")); glay->addWidget(standardSizeLabel, 2, 0); glay->addWidget(m_viewerStandardSize, 2, 1); standardSizeLabel->setBuddy(m_viewerStandardSize); txt = i18n("<p>Set the standard size for images to be displayed in the viewer.</p> " "<p><b>Full Viewer Size</b> indicates that the image will be stretched or shrunk to fill the viewer window.</p> " "<p><b>Natural Image Size</b> indicates that the image will be displayed pixel for pixel.</p> " "<p><b>Natural Image Size If Possible</b> indicates that the image will be displayed pixel for pixel if it would fit the window, " "otherwise it will be shrunk to fit the viewer.</p>"); m_viewerStandardSize->setWhatsThis(txt); QLabel *scalingLabel = new QLabel(i18n("Scaling algorithm:"), this); m_smoothScale = new KComboBox(this); m_smoothScale->addItems(QStringList() << i18n("Fastest") << i18n("Best")); scalingLabel->setBuddy(m_smoothScale); glay->addWidget(scalingLabel, 3, 0); glay->addWidget(m_smoothScale, 3, 1); txt = i18n("<p>When displaying images, KPhotoAlbum normally performs smooth scaling of the image. " "If this option is not set, KPhotoAlbum will use a faster but less smooth scaling method.</p>"); scalingLabel->setWhatsThis(txt); m_smoothScale->setWhatsThis(txt); } void Settings::ViewerPage::loadSettings(Settings::SettingsData *opt) { m_viewImageSetup->setLaunchFullScreen(opt->launchViewerFullScreen()); m_viewImageSetup->setSize(opt->viewerSize()); m_slideShowSetup->setLaunchFullScreen(opt->launchSlideShowFullScreen()); m_slideShowSetup->setSize(opt->slideShowSize()); m_slideShowInterval->setValue(opt->slideShowInterval()); m_cacheSize->setValue(opt->viewerCacheSize()); m_smoothScale->setCurrentIndex(opt->smoothScale()); m_viewerStandardSize->setCurrentIndex(opt->viewerStandardSize()); } void Settings::ViewerPage::saveSettings(Settings::SettingsData *opt) { opt->setLaunchViewerFullScreen(m_viewImageSetup->launchFullScreen()); opt->setViewerSize(m_viewImageSetup->size()); opt->setSlideShowInterval(m_slideShowInterval->value()); opt->setViewerCacheSize(m_cacheSize->value()); opt->setSmoothScale(m_smoothScale->currentIndex()); opt->setViewerStandardSize((StandardViewSize)m_viewerStandardSize->currentIndex()); opt->setSlideShowSize(m_slideShowSetup->size()); opt->setLaunchSlideShowFullScreen(m_slideShowSetup->launchFullScreen()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Settings/ViewerSizeConfig.cpp b/Settings/ViewerSizeConfig.cpp index 72ed8d1d..df37c109 100644 --- a/Settings/ViewerSizeConfig.cpp +++ b/Settings/ViewerSizeConfig.cpp @@ -1,79 +1,80 @@ /* 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 <KLocalizedString> #include <qcheckbox.h> #include <qlabel.h> #include <qlayout.h> #include <qspinbox.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(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/CellGeometry.cpp b/ThumbnailView/CellGeometry.cpp index 351dfdd3..52354479 100644 --- a/ThumbnailView/CellGeometry.cpp +++ b/ThumbnailView/CellGeometry.cpp @@ -1,157 +1,158 @@ /* Copyright (C) 2003-2010 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 "CellGeometry.h" -#include <KLocalizedString> - -#include "Settings/SettingsData.h" #include "ThumbnailModel.h" #include "ThumbnailWidget.h" +#include <Settings/SettingsData.h> + +#include <KLocalizedString> + using Utilities::StringSet; ThumbnailView::CellGeometry::CellGeometry(ThumbnailFactory *factory) : ThumbnailComponent(factory) , m_cacheInitialized(false) { } /** * Return desired size of the pixmap */ QSize ThumbnailView::CellGeometry::preferredIconSize() { int width = Settings::SettingsData::instance()->actualThumbnailSize(); int height = width * Settings::SettingsData::instance()->getThumbnailAspectRatio(); return QSize(width, height); } /** * Return base size of the pixmap. * I.e. the unscaled thumbnail size, as it is set in the settings page. */ QSize ThumbnailView::CellGeometry::baseIconSize() { int width = Settings::SettingsData::instance()->thumbnailSize(); int height = width * Settings::SettingsData::instance()->getThumbnailAspectRatio(); return QSize(width, height); } /** * Return the geometry for the icon in the cell. The coordinates are relative to the cell. */ QRect ThumbnailView::CellGeometry::iconGeometry(const QPixmap &pixmap) const { const QSize cellSize = preferredIconSize(); const int space = Settings::SettingsData::instance()->thumbnailSpace() + 5; /* 5 pixels for 3d effect */ int width = cellSize.width() - space; int xoff = space / 2 + qMax(0, (width - pixmap.width()) / 2); int yoff = space / 2 + cellSize.height() - pixmap.height(); return QRect(QPoint(xoff, yoff), pixmap.size()); } /** * return the number of categories with valies in for the given image. */ static int noOfCategoriesForImage(const DB::FileName &image) { int catsInText = 0; QStringList grps = image.info()->availableCategories(); for (QStringList::const_iterator it = grps.constBegin(); it != grps.constEnd(); ++it) { QString category = *it; if (category != i18n("Folder") && category != i18n("Media Type")) { StringSet items = image.info()->itemsOfCategory(category); if (!items.empty()) { catsInText++; } } } return catsInText; } /** * Return the height of the text under the thumbnails. */ int ThumbnailView::CellGeometry::textHeight() const { if (!m_cacheInitialized) const_cast<CellGeometry *>(this)->flushCache(); return m_textHeight; } QSize ThumbnailView::CellGeometry::cellSize() const { if (!m_cacheInitialized) const_cast<CellGeometry *>(this)->flushCache(); return m_cellSize; } QRect ThumbnailView::CellGeometry::cellTextGeometry() const { if (!m_cacheInitialized) const_cast<CellGeometry *>(this)->flushCache(); return m_cellTextGeometry; } void ThumbnailView::CellGeometry::flushCache() { m_cacheInitialized = true; calculateTextHeight(); calculateCellSize(); calculateCellTextGeometry(); } void ThumbnailView::CellGeometry::calculateTextHeight() { m_textHeight = 0; const int charHeight = QFontMetrics(widget()->font()).height(); if (Settings::SettingsData::instance()->displayLabels()) m_textHeight += charHeight + 2; if (Settings::SettingsData::instance()->displayCategories()) { int maxCatsInText = 0; Q_FOREACH (const DB::FileName &fileName, model()->imageList(ViewOrder)) { maxCatsInText = qMax(noOfCategoriesForImage(fileName), maxCatsInText); } m_textHeight += charHeight * maxCatsInText + 5; } } void ThumbnailView::CellGeometry::calculateCellSize() { const QSize iconSize = preferredIconSize(); const int height = iconSize.height() + 2 + m_textHeight; const int space = Settings::SettingsData::instance()->thumbnailSpace() + 5; /* 5 pixels for 3d effect */ m_cellSize = QSize(iconSize.width() + space, height + space); } void ThumbnailView::CellGeometry::calculateCellTextGeometry() { if (!Settings::SettingsData::instance()->displayLabels() && !Settings::SettingsData::instance()->displayCategories()) m_cellTextGeometry = QRect(); else { const int h = m_textHeight; m_cellTextGeometry = QRect(1, m_cellSize.height() - h - 1, m_cellSize.width() - 2, h); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/CellGeometry.h b/ThumbnailView/CellGeometry.h index 41fb901c..7b31bbfd 100644 --- a/ThumbnailView/CellGeometry.h +++ b/ThumbnailView/CellGeometry.h @@ -1,64 +1,65 @@ /* Copyright (C) 2003-2010 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. */ #ifndef CELLGEOMETRY_H #define CELLGEOMETRY_H #include "ThumbnailComponent.h" + #include <QRect> #include <QSize> class QPixmap; class QRect; class QSize; namespace DB { class Id; } namespace ThumbnailView { class ThumbnailFactory; class CellGeometry : public ThumbnailComponent { public: void flushCache(); explicit CellGeometry(ThumbnailFactory *factory); QSize cellSize() const; static QSize preferredIconSize(); static QSize baseIconSize(); QRect iconGeometry(const QPixmap &pixmap) const; int textHeight() const; QRect cellTextGeometry() const; void calculateCellSize(); private: void calculateTextHeight(); void calculateCellTextGeometry(); bool m_cacheInitialized; int m_textHeight; QSize m_cellSize; QRect m_cellTextGeometry; }; } #endif /* CELLGEOMETRY_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/Delegate.cpp b/ThumbnailView/Delegate.cpp index 8c0dc332..d5d72c29 100644 --- a/ThumbnailView/Delegate.cpp +++ b/ThumbnailView/Delegate.cpp @@ -1,280 +1,283 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "Delegate.h" + #include "CellGeometry.h" -#include "Settings/SettingsData.h" #include "ThumbnailModel.h" #include "ThumbnailWidget.h" + +#include <Settings/SettingsData.h> + #include <KLocalizedString> #include <QPainter> ThumbnailView::Delegate::Delegate(ThumbnailFactory *factory, QObject *parent) : QStyledItemDelegate(parent) , ThumbnailComponent(factory) { } void ThumbnailView::Delegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { paintCellBackground(painter, option.rect); if (widget()->isGridResizing()) return; if (index.data(Qt::DecorationRole).value<QPixmap>().isNull()) return; paintCellPixmap(painter, option, index); paintCellText(painter, option, index); } void ThumbnailView::Delegate::paintCellBackground(QPainter *painter, const QRect &rect) const { painter->fillRect(rect, QColor(Settings::SettingsData::instance()->backgroundColor())); if (widget()->isGridResizing() || Settings::SettingsData::instance()->thumbnailDisplayGrid()) { painter->setPen(contrastColor(Settings::SettingsData::instance()->backgroundColor())); // left and right of frame painter->drawLine(rect.right(), rect.top(), rect.right(), rect.bottom()); // bottom line painter->drawLine(rect.left(), rect.bottom(), rect.right(), rect.bottom()); } } void ThumbnailView::Delegate::paintCellPixmap(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { const QPixmap pixmap = index.data(Qt::DecorationRole).value<QPixmap>(); const QRect pixmapRect = cellGeometryInfo()->iconGeometry(pixmap).translated(option.rect.topLeft()); paintBoundingRect(painter, pixmapRect, index); painter->drawPixmap(pixmapRect, pixmap); paintVideoInfo(painter, pixmapRect, index); paintDropIndicator(painter, option.rect, index); paintStackedIndicator(painter, pixmapRect, index); // Paint transparent pixels over the widget for selection. const QItemSelectionModel *selectionModel = widget()->selectionModel(); if (selectionModel->isSelected(index)) painter->fillRect(option.rect, QColor(58, 98, 134, 127)); else if (selectionModel->hasSelection() && selectionModel->currentIndex() == index) painter->fillRect(option.rect, QColor(58, 98, 134, 127)); } void ThumbnailView::Delegate::paintVideoInfo(QPainter *painter, const QRect &pixmapRect, const QModelIndex &index) const { DB::ImageInfoPtr imageInfo = model()->imageAt(index.row()).info(); if (!imageInfo || imageInfo->mediaType() != DB::Video) return; const QString text = videoLengthText(imageInfo); const QRect metricsRect = painter->fontMetrics().boundingRect(text); const int margin = 3; const QRect textRect = QRect(pixmapRect.right() - metricsRect.width() - 2 * margin, pixmapRect.bottom() - metricsRect.height() - margin, metricsRect.width() + margin, metricsRect.height()); const QRect backgroundRect = textRect.adjusted(-margin, -margin, margin, margin); if (backgroundRect.width() > pixmapRect.width() / 2) { // Don't show the time if the box would fill more than half the thumbnail return; } painter->save(); painter->fillRect(backgroundRect, QBrush(QColor(0, 0, 0, 128))); painter->setPen(Qt::white); painter->drawText(textRect, Qt::TextDontClip, text); painter->restore(); } void ThumbnailView::Delegate::paintCellText(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { // Optimization based on result from KCacheGrind if (!Settings::SettingsData::instance()->displayLabels() && !Settings::SettingsData::instance()->displayCategories()) return; DB::FileName fileName = model()->imageAt(index.row()); if (fileName.isNull()) return; QString title = index.data(Qt::DisplayRole).value<QString>(); QRect rect = cellGeometryInfo()->cellTextGeometry(); painter->setPen(contrastColor(Settings::SettingsData::instance()->backgroundColor())); //Qt::TextWordWrap just in case, if the text's width is wider than the cell's width painter->drawText(rect.translated(option.rect.topLeft()), Qt::AlignCenter | Qt::TextWordWrap, title); } QSize ThumbnailView::Delegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const { return cellGeometryInfo()->cellSize(); } /** This will paint the pixels around the thumbnail, which gives it a 3D effect, and also which indicates current image and selection state. The colors are fetched from looking at the Gwenview. I tried to see if I could figure out from the code how it was drawn, but failed at doing so. */ void ThumbnailView::Delegate::paintBoundingRect(QPainter *painter, const QRect &pixmapRect, const QModelIndex &index) const { QRect rect = pixmapRect; rect.adjust(-5, -5, 4, 4); for (int i = 4; i >= 0; --i) { QColor color; if (widget()->selectionModel()->isSelected(index)) { static QColor selectionColors[] = { QColor(58, 98, 134), QColor(96, 161, 221), QColor(93, 165, 228), QColor(132, 186, 237), QColor(62, 95, 128) }; color = selectionColors[i]; } #if 0 // This code doesn't work very well with the QListView, for some odd reason, it often leaves a highlighted thumbnail behind // 9 Aug. 2010 11:33 -- Jesper K. Pedersen else if ( widget()->indexUnderCursor() == index ) { static QColor hoverColors[] = { QColor(46,99,152), QColor(121,136,151), QColor(121,136,151), QColor(126,145,163), QColor(109,126,142)}; color = hoverColors[i]; } #endif else { // Originally I just painted the outline using drawRect, but that turned out to be a huge bottleneck. // The code was therefore converted to fillRect, which was much faster. // This code was complicted from that, as I previously drew the // rects from insite out, but with fillRect that doesn't work, // and I thefore had to rewrite the code to draw the rects from // outside in. // I now had to calculate the destination color myself, rather // than rely on drawing with a transparent color on top of the // background. // 12 Aug. 2010 17:38 -- Jesper K. Pedersen const QColor foreground = Qt::black; const QColor backround = QColor(Settings::SettingsData::instance()->backgroundColor()); double alpha = (0.5 - 0.1 * i); double inverseAlpha = 1 - alpha; color = QColor(int(foreground.red() * alpha + backround.red() * inverseAlpha), int(foreground.green() * alpha + backround.green() * inverseAlpha), int(foreground.blue() * alpha + backround.blue() * inverseAlpha)); } QPen pen(color); painter->setPen(pen); painter->fillRect(rect, QBrush(color)); rect.adjust(1, 1, -1, -1); } } static DB::StackID getStackId(const DB::FileName &fileName) { return fileName.info()->stackId(); } void ThumbnailView::Delegate::paintStackedIndicator(QPainter *painter, const QRect &pixmapRect, const QModelIndex &index) const { DB::ImageInfoPtr imageInfo = model()->imageAt(index.row()).info(); if (!imageInfo || !imageInfo->isStacked()) return; const QRect cellRect = widget()->visualRect(index); // Calculate the three points for the bottom most/right most lines int leftX = cellRect.left(); int rightX = cellRect.right() + 5; // 5 for the 3D effect if (isFirst(index.row())) leftX = pixmapRect.left() + pixmapRect.width() / 2; if (isLast(index.row())) rightX = pixmapRect.right(); QPoint bottomLeftPoint(leftX, pixmapRect.bottom()); QPoint bottomRightPoint(rightX, pixmapRect.bottom()); QPoint topPoint = isLast(index.row()) ? QPoint(rightX, pixmapRect.top() + pixmapRect.height() / 2) : QPoint(); // Paint the lines. painter->save(); for (int i = 0; i < 8; ++i) { painter->setPen(QPen(i % 2 == 0 ? Qt::black : Qt::white)); painter->drawLine(bottomLeftPoint, bottomRightPoint); if (topPoint != QPoint()) { painter->drawLine(bottomRightPoint, topPoint); topPoint -= QPoint(1, 1); } bottomLeftPoint -= QPoint(isFirst(index.row()) ? 1 : 0, 1); bottomRightPoint -= QPoint(isLast(index.row()) ? 1 : 0, 1); } painter->restore(); } bool ThumbnailView::Delegate::isFirst(int row) const { const DB::StackID curId = getStackId(model()->imageAt(row)); return !model()->isItemInExpandedStack(curId) || row == 0 || getStackId(model()->imageAt(row - 1)) != curId; } bool ThumbnailView::Delegate::isLast(int row) const { const DB::StackID curId = getStackId(model()->imageAt(row)); return !model()->isItemInExpandedStack(curId) || row == model()->imageCount() - 1 || getStackId(model()->imageAt(row + 1)) != curId; } QString ThumbnailView::Delegate::videoLengthText(const DB::ImageInfoPtr &imageInfo) const { const int length = imageInfo->videoLength(); if (length < 0) return i18nc("No video length could be determined, so we just display 'video' instead of the video length.", "video"); const int hours = length / 60 / 60; const int minutes = (length / 60) % 60; const int secs = length % 60; QString res; if (hours > 0) res = QString::number(hours) + QLatin1String(":"); if (minutes < 10 && hours > 0) res += QLatin1String("0"); res += QString::number(minutes); res += QLatin1String(":"); if (secs < 10) res += QLatin1String("0"); res += QString::number(secs); return res; } void ThumbnailView::Delegate::paintDropIndicator(QPainter *painter, const QRect &rect, const QModelIndex &index) const { const DB::FileName fileName = model()->imageAt(index.row()); if (model()->leftDropItem() == fileName) painter->fillRect(rect.left(), rect.top(), 3, rect.height(), QBrush(Qt::red)); else if (model()->rightDropItem() == fileName) painter->fillRect(rect.right() - 2, rect.top(), 3, rect.height(), QBrush(Qt::red)); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/Delegate.h b/ThumbnailView/Delegate.h index 307ff688..67e5e948 100644 --- a/ThumbnailView/Delegate.h +++ b/ThumbnailView/Delegate.h @@ -1,52 +1,54 @@ /* Copyright (C) 2003-2010 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. */ #ifndef DELEGATE_H #define DELEGATE_H #include "ThumbnailComponent.h" + #include <DB/ImageInfoPtr.h> + #include <QStyledItemDelegate> namespace ThumbnailView { class Delegate : public QStyledItemDelegate, private ThumbnailComponent { public: explicit Delegate(ThumbnailFactory *factory, QObject *parent); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; private: void paintCellBackground(QPainter *painter, const QRect &rect) const; void paintCellPixmap(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; void paintVideoInfo(QPainter *painter, const QRect &pixmapRect, const QModelIndex &index) const; void paintCellText(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; void paintBoundingRect(QPainter *painter, const QRect &pixmapRect, const QModelIndex &index) const; void paintStackedIndicator(QPainter *painter, const QRect &rect, const QModelIndex &index) const; void paintDropIndicator(QPainter *painter, const QRect &rect, const QModelIndex &index) const; bool isFirst(int row) const; bool isLast(int row) const; QString videoLengthText(const DB::ImageInfoPtr &imageInfo) const; }; } #endif /* DELEGATE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/FilterWidget.cpp b/ThumbnailView/FilterWidget.cpp index 03bd8786..45eb946b 100644 --- a/ThumbnailView/FilterWidget.cpp +++ b/ThumbnailView/FilterWidget.cpp @@ -1,81 +1,82 @@ /* Copyright (C) 2019 The KPhotoAlbum Development Team 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 "FilterWidget.h" + #include <KLocalizedString> #include <KRatingWidget> #include <QHBoxLayout> #include <QLabel> ThumbnailView::FilterWidget::FilterWidget(QWidget *parent) : KToolBar(parent) { m_toggleFilter = addAction( QIcon::fromTheme(QLatin1String("view-filter")), i18nc("The action enables/disables filtering of images in the thumbnail view.", "Toggle filter")); m_toggleFilter->setCheckable(true); m_toggleFilter->setToolTip(xi18n("Press <shortcut>Escape</shortcut> to clear filter.")); connect(m_toggleFilter, &QAction::toggled, this, &FilterWidget::filterToggled); m_rating = new KRatingWidget; addWidget(m_rating); m_label = new QLabel; resetLabelText(); addWidget(m_label); // Note(jzarl): new style connect seems to be confused by overloaded signal in KRatingWidget // -> fall back to old-style connect(m_rating, SIGNAL(ratingChanged(int)), this, SLOT(slotRatingChanged(int))); } void ThumbnailView::FilterWidget::setFilter(const DB::ImageSearchInfo &filter) { // prevent ratingChanged signal when the filter has changed blockSignals(true); m_rating->setRating(qMax(static_cast<short int>(0), filter.rating())); if (filter.isNull()) { m_toggleFilter->setChecked(false); resetLabelText(); } else { m_toggleFilter->setChecked(true); m_label->setText(i18nc("The label gives a textual description of the active filter", "Filter: %1", filter.toString())); } blockSignals(false); } void ThumbnailView::FilterWidget::setEnabled(bool enabled) { m_toggleFilter->setEnabled(enabled); m_rating->setEnabled(enabled); if (enabled) resetLabelText(); else m_label->clear(); } void ThumbnailView::FilterWidget::slotRatingChanged(int rating) { Q_ASSERT(-1 <= rating && rating <= 10); emit ratingChanged((short)rating); } void ThumbnailView::FilterWidget::resetLabelText() { m_label->setText(xi18n("Tip: Use <shortcut>Alt+Shift+<placeholder>A-Z</placeholder></shortcut> to toggle a filter for that token.")); } diff --git a/ThumbnailView/FilterWidget.h b/ThumbnailView/FilterWidget.h index ab27afc3..4e3f9b66 100644 --- a/ThumbnailView/FilterWidget.h +++ b/ThumbnailView/FilterWidget.h @@ -1,70 +1,70 @@ /* Copyright (C) 2019 The KPhotoAlbum Development Team 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/>. */ #ifndef FILTERWIDGET_H #define FILTERWIDGET_H -#include <KToolBar> - #include <DB/ImageSearchInfo.h> +#include <KToolBar> + class KRatingWidget; class QAction; class QLabel; class QWidget; namespace ThumbnailView { /** * @brief The FilterWidget class provides a KToolBar widget to interact with the thumbnail filter. * You can use it to set the rating filter, and it gives some visual feedback when the filter changes. * * \image html FilterWidget.png "FilterWidget when no filter is set" */ class FilterWidget : public KToolBar { Q_OBJECT public: explicit FilterWidget(QWidget *parent = nullptr); signals: void filterToggled(bool enabled); void ratingChanged(short rating); public slots: void setFilter(const DB::ImageSearchInfo &filter); /** * @brief setEnabled enables or disables the filter controls. * If the ThumbnailView is not active, setEnable should be set to \c false. * @param enabled */ void setEnabled(bool enabled); protected slots: void slotRatingChanged(int rating); void resetLabelText(); private: QAction *m_toggleFilter; KRatingWidget *m_rating; QLabel *m_label; }; } #endif // FILTERWIDGET_H diff --git a/ThumbnailView/GridResizeInteraction.cpp b/ThumbnailView/GridResizeInteraction.cpp index 47594e43..f0998667 100644 --- a/ThumbnailView/GridResizeInteraction.cpp +++ b/ThumbnailView/GridResizeInteraction.cpp @@ -1,96 +1,99 @@ /* Copyright (C) 2003-2010 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 "GridResizeInteraction.h" + #include "CellGeometry.h" -#include "ImageManager/ThumbnailBuilder.h" -#include "ImageManager/ThumbnailCache.h" -#include "ImageManager/enums.h" -#include "MainWindow/Window.h" -#include "Settings/SettingsData.h" #include "ThumbnailModel.h" #include "ThumbnailWidget.h" + +#include <ImageManager/ThumbnailBuilder.h> +#include <ImageManager/ThumbnailCache.h> +#include <ImageManager/enums.h> +#include <MainWindow/Window.h> +#include <Settings/SettingsData.h> + #include <KLocalizedString> #include <KSharedConfig> #include <QScrollBar> ThumbnailView::GridResizeInteraction::GridResizeInteraction(ThumbnailFactory *factory) : ThumbnailComponent(factory) { } bool ThumbnailView::GridResizeInteraction::mousePressEvent(QMouseEvent *event) { m_resizing = true; m_mousePressPos = event->pos(); enterGridResizingMode(); return true; } bool ThumbnailView::GridResizeInteraction::mouseMoveEvent(QMouseEvent *event) { // no need to query this more than once (can't be changed in the GUI): static int _minimum_ = Settings::SettingsData::instance()->minimumThumbnailSize(); QPoint dist = event->pos() - m_mousePressPos; setCellSize(qMax(_minimum_, m_origWidth + dist.x() / 5)); return true; } bool ThumbnailView::GridResizeInteraction::mouseReleaseEvent(QMouseEvent *) { leaveGridResizingMode(); m_resizing = false; return true; } void ThumbnailView::GridResizeInteraction::setCellSize(int size) { const int baseSize = Settings::SettingsData::instance()->thumbnailSize(); // snap to base size: if (qAbs(size - baseSize) < 10) size = baseSize; model()->beginResetModel(); Settings::SettingsData::instance()->setActualThumbnailSize(size); cellGeometryInfo()->calculateCellSize(); model()->endResetModel(); } bool ThumbnailView::GridResizeInteraction::isResizingGrid() { return m_resizing; } void ThumbnailView::GridResizeInteraction::leaveGridResizingMode() { KSharedConfig::openConfig()->sync(); model()->beginResetModel(); cellGeometryInfo()->flushCache(); model()->endResetModel(); model()->updateVisibleRowInfo(); widget()->setCurrentIndex(model()->index(m_currentRow, 0)); } void ThumbnailView::GridResizeInteraction::enterGridResizingMode() { m_origWidth = widget()->cellWidth(); ImageManager::ThumbnailBuilder::instance()->cancelRequests(); m_currentRow = widget()->currentIndex().row(); widget()->verticalScrollBar()->setValue(0); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/GridResizeInteraction.h b/ThumbnailView/GridResizeInteraction.h index 3fd59e9d..3e3a80e1 100644 --- a/ThumbnailView/GridResizeInteraction.h +++ b/ThumbnailView/GridResizeInteraction.h @@ -1,60 +1,61 @@ /* Copyright (C) 2003-2010 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. */ #ifndef GRIDRESIZEINTERACTION_H #define GRIDRESIZEINTERACTION_H #include "MouseInteraction.h" #include "ThumbnailComponent.h" + #include <QMouseEvent> namespace ThumbnailView { class ThumbnailWidget; class GridResizeInteraction : public MouseInteraction, private ThumbnailComponent { public: explicit GridResizeInteraction(ThumbnailFactory *factory); bool mousePressEvent(QMouseEvent *) override; bool mouseMoveEvent(QMouseEvent *) override; bool mouseReleaseEvent(QMouseEvent *) override; bool isResizingGrid() override; void enterGridResizingMode(); void leaveGridResizingMode(); private: void setCellSize(int size); /** * The position the mouse was pressed down, in view port coordinates */ QPoint m_mousePressPos; /** * This variable contains the size of a cell prior to the beginning of * resizing the grid. */ int m_origWidth; bool m_resizing; int m_currentRow; }; } #endif /* GRIDRESIZEINTERACTION_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/GridResizeSlider.cpp b/ThumbnailView/GridResizeSlider.cpp index 3213d7b3..b257d634 100644 --- a/ThumbnailView/GridResizeSlider.cpp +++ b/ThumbnailView/GridResizeSlider.cpp @@ -1,189 +1,189 @@ /* 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 <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 "Logging.h" #include "ThumbnailModel.h" #include "ThumbnailWidget.h" +#include <ImageManager/ThumbnailBuilder.h> +#include <MainWindow/Window.h> +#include <Settings/SettingsData.h> + 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) { qCDebug(ThumbnailViewLog) << "Mouse pressed"; enterGridResizingMode(); QSlider::mousePressEvent(event); } void ThumbnailView::GridResizeSlider::mouseReleaseEvent(QMouseEvent *event) { qCDebug(ThumbnailViewLog) << "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); qCDebug(ThumbnailViewLog) << "(Re)starting timer"; if (!m_resizing) { enterGridResizingMode(); } QSlider::wheelEvent(event); } void ThumbnailView::GridResizeSlider::enterGridResizingMode() { if (m_resizing) return; //already resizing m_resizing = true; qCDebug(ThumbnailViewLog) << "Entering grid resizing mode"; ImageManager::ThumbnailBuilder::instance()->cancelRequests(); emit isResizing(true); } void ThumbnailView::GridResizeSlider::leaveGridResizingMode() { if (!m_resizing) return; //not resizing m_resizing = false; qCDebug(ThumbnailViewLog) << "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(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/GridResizeSlider.h b/ThumbnailView/GridResizeSlider.h index 26f5b527..76508131 100644 --- a/ThumbnailView/GridResizeSlider.h +++ b/ThumbnailView/GridResizeSlider.h @@ -1,73 +1,73 @@ /* Copyright (C) 2015 Johannes Zarl <johannes@zarl.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. */ #ifndef GRIDRESIZESLIDER_H #define GRIDRESIZESLIDER_H -#include <QSlider> - #include "ThumbnailComponent.h" + +#include <QSlider> class QTimer; namespace ThumbnailView { class ThumbnailWidget; /** * @brief The GridResizeSlider class * The GridResizeSlider is a QSlider that ties into the ThumbnailView infrastructure, * as well as into the SettingsData on thumbnail size. * * Moving the slider changes the actual thumbnail size, and changing the actual * thumbnail size in the SettingsData reflects on the slider. * Changes in SettingsData::thumbnailSize() are reflected in the maximum slider value. */ class GridResizeSlider : public QSlider, private ThumbnailComponent { Q_OBJECT public: explicit GridResizeSlider(ThumbnailFactory *factory); ~GridResizeSlider() override; public slots: void increaseThumbnailSize(); void decreaseThumbnailSize(); signals: void isResizing(bool); protected: void mousePressEvent(QMouseEvent *) override; void mouseReleaseEvent(QMouseEvent *) override; void wheelEvent(QWheelEvent *) override; private slots: void enterGridResizingMode(); void leaveGridResizingMode(); void setCellSize(int size); void setMaximum(int size); private: bool m_resizing; QTimer *m_timer; void calculateNewThumbnailSize(int perRowDifference); }; } #endif /* GRIDRESIZESLIDER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/KeyboardEventHandler.cpp b/ThumbnailView/KeyboardEventHandler.cpp index cf7eeac9..661387cd 100644 --- a/ThumbnailView/KeyboardEventHandler.cpp +++ b/ThumbnailView/KeyboardEventHandler.cpp @@ -1,129 +1,129 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "KeyboardEventHandler.h" -#include <DB/CategoryCollection.h> -#include <DB/ImageDB.h> -#include <MainWindow/DirtyIndicator.h> -#include <Settings/SettingsData.h> - #include "CellGeometry.h" #include "ThumbnailModel.h" #include "ThumbnailWidget.h" #include "VideoThumbnailCycler.h" #include "enums.h" +#include <DB/CategoryCollection.h> +#include <DB/ImageDB.h> +#include <MainWindow/DirtyIndicator.h> +#include <Settings/SettingsData.h> + ThumbnailView::KeyboardEventHandler::KeyboardEventHandler(ThumbnailFactory *factory) : ThumbnailComponent(factory) { } bool ThumbnailView::KeyboardEventHandler::keyPressEvent(QKeyEvent *event) { if (event->modifiers() == Qt::NoModifier && event->key() == Qt::Key_Escape) { if (model()->isFiltered()) { model()->clearFilter(); return true; } } // tokens if (event->key() >= Qt::Key_A && event->key() <= Qt::Key_Z) { const QString token = event->text().toUpper().left(1); if (event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::ShiftModifier) { // toggle tokens bool mustRemoveToken = false; bool hadHit = false; const DB::FileNameList selection = widget()->selection(event->modifiers() == Qt::NoModifier ? NoExpandCollapsedStacks : IncludeAllStacks); DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); Q_FOREACH (const DB::FileName &fileName, selection) { DB::ImageInfoPtr info = fileName.info(); if (!hadHit) { mustRemoveToken = info->hasCategoryInfo(tokensCategory->name(), token); hadHit = true; } if (mustRemoveToken) info->removeCategoryInfo(tokensCategory->name(), token); else info->addCategoryInfo(tokensCategory->name(), token); model()->updateCell(fileName); } tokensCategory->addItem(token); MainWindow::DirtyIndicator::markDirty(); return true; } if (event->modifiers() == (Qt::AltModifier | Qt::ShiftModifier)) { // filter view const QString tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name(); model()->toggleCategoryFilter(tokensCategory, token); return true; } } // rating if (event->key() >= Qt::Key_0 && event->key() <= Qt::Key_5) { bool ok; const short rating = 2 * event->text().left(1).toShort(&ok, 10); if (ok) { if (event->modifiers() == Qt::NoModifier || event->modifiers() == Qt::ShiftModifier) { // set rating const DB::FileNameList selection = widget()->selection(event->modifiers() == Qt::NoModifier ? NoExpandCollapsedStacks : IncludeAllStacks); Q_FOREACH (const DB::FileName &fileName, selection) { DB::ImageInfoPtr info = fileName.info(); info->setRating(rating); } MainWindow::DirtyIndicator::markDirty(); return true; } } } if (event->key() == Qt::Key_Control && widget()->isItemUnderCursorSelected()) VideoThumbnailCycler::instance()->stopCycle(); if (event->key() == Qt::Key_Return) { emit showSelection(); return true; } return false; } /** Handle key release event. \return true if the event should propagate */ bool ThumbnailView::KeyboardEventHandler::keyReleaseEvent(QKeyEvent *event) { if (widget()->m_wheelResizing && event->key() == Qt::Key_Control) { widget()->m_gridResizeInteraction.leaveGridResizingMode(); widget()->m_wheelResizing = false; return false; // Don't propagate the event - I'm not sure why. } if (event->key() == Qt::Key_Control) VideoThumbnailCycler::instance()->setActive(widget()->mediaIdUnderCursor()); return true; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/KeyboardEventHandler.h b/ThumbnailView/KeyboardEventHandler.h index 3f7dde97..d86153f4 100644 --- a/ThumbnailView/KeyboardEventHandler.h +++ b/ThumbnailView/KeyboardEventHandler.h @@ -1,60 +1,60 @@ /* Copyright (C) 2003-2010 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. */ #ifndef KEYBOARDEVENTHANDLER_H #define KEYBOARDEVENTHANDLER_H -#include <QObject> - #include "ThumbnailComponent.h" #include "enums.h" +#include <QObject> + class QKeyEvent; class ThumbnailFactory; namespace ThumbnailView { /** * @brief The KeyboardEventHandler class handles keyboard input for the thumbnail widget. * * Specifically, the following keyboard interactions are handled: * - Setting and unsetting tokens on images (a-z) * - Setting the rating for images (1-5) * - Stopping video thumbnail cycling when Control is pressed * - Showing the Viewer when Enter is pressed. * - Applying filters for tokens and ratings. * - Clearing the current filter */ class KeyboardEventHandler : public QObject, public ThumbnailComponent { Q_OBJECT public: explicit KeyboardEventHandler(ThumbnailFactory *factory); bool keyPressEvent(QKeyEvent *event); bool keyReleaseEvent(QKeyEvent *); signals: void showSelection(); private: }; } #endif /* KEYBOARDEVENTHANDLER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/MouseTrackingInteraction.cpp b/ThumbnailView/MouseTrackingInteraction.cpp index 9207a972..271061b9 100644 --- a/ThumbnailView/MouseTrackingInteraction.cpp +++ b/ThumbnailView/MouseTrackingInteraction.cpp @@ -1,69 +1,72 @@ /* Copyright (C) 2003-2010 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 "MouseTrackingInteraction.h" + #include "ThumbnailModel.h" #include "ThumbnailWidget.h" #include "VideoThumbnailCycler.h" + #include <DB/FileName.h> + #include <QMouseEvent> ThumbnailView::MouseTrackingInteraction::MouseTrackingInteraction(ThumbnailFactory *factory) : ThumbnailComponent(factory) , m_cursorWasAtStackIcon(false) { } bool ThumbnailView::MouseTrackingInteraction::mouseMoveEvent(QMouseEvent *event) { updateStackingIndication(event); handleCursorOverNewIcon(); if ((event->modifiers() & Qt::ControlModifier) != 0 && widget()->isItemUnderCursorSelected()) VideoThumbnailCycler::instance()->stopCycle(); else VideoThumbnailCycler::instance()->setActive(widget()->mediaIdUnderCursor()); return false; } void ThumbnailView::MouseTrackingInteraction::updateStackingIndication(QMouseEvent *event) { bool interestingArea = widget()->isMouseOverStackIndicator(event->pos()); if (interestingArea && !m_cursorWasAtStackIcon) { widget()->setCursor(Qt::PointingHandCursor); m_cursorWasAtStackIcon = true; } else if (!interestingArea && m_cursorWasAtStackIcon) { widget()->unsetCursor(); m_cursorWasAtStackIcon = false; } } void ThumbnailView::MouseTrackingInteraction::handleCursorOverNewIcon() { static DB::FileName lastFileNameUnderCursor; const DB::FileName fileName = widget()->mediaIdUnderCursor(); if (fileName != lastFileNameUnderCursor) { if (!fileName.isNull() && !lastFileNameUnderCursor.isNull()) { emit fileIdUnderCursorChanged(fileName); model()->updateCell(lastFileNameUnderCursor); model()->updateCell(fileName); } lastFileNameUnderCursor = fileName; } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/MouseTrackingInteraction.h b/ThumbnailView/MouseTrackingInteraction.h index e747368e..9af76bef 100644 --- a/ThumbnailView/MouseTrackingInteraction.h +++ b/ThumbnailView/MouseTrackingInteraction.h @@ -1,55 +1,56 @@ /* Copyright (C) 2003-2010 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. */ #ifndef MOUSETRACKINGINTERACTION_H #define MOUSETRACKINGINTERACTION_H #include "MouseInteraction.h" #include "ThumbnailComponent.h" + #include <QMouseEvent> namespace DB { class FileName; } namespace ThumbnailView { class MouseTrackingInteraction : public QObject, public MouseInteraction, private ThumbnailComponent { Q_OBJECT public: explicit MouseTrackingInteraction(ThumbnailFactory *factory); bool mouseMoveEvent(QMouseEvent *) override; signals: void fileIdUnderCursorChanged(const DB::FileName &id); private: void updateStackingIndication(QMouseEvent *event); void handleCursorOverNewIcon(); private: bool m_cursorWasAtStackIcon; }; } #endif /* MOUSETRACKINGINTERACTION_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/SelectionInteraction.cpp b/ThumbnailView/SelectionInteraction.cpp index 6d9ef83d..ab43e18b 100644 --- a/ThumbnailView/SelectionInteraction.cpp +++ b/ThumbnailView/SelectionInteraction.cpp @@ -1,83 +1,82 @@ /* 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 "CellGeometry.h" +#include "ThumbnailFactory.h" +#include "ThumbnailModel.h" +#include "ThumbnailWidget.h" + +#include <DB/FileNameList.h> +#include <MainWindow/Window.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; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/SelectionInteraction.h b/ThumbnailView/SelectionInteraction.h index 6771e19d..8b048c54 100644 --- a/ThumbnailView/SelectionInteraction.h +++ b/ThumbnailView/SelectionInteraction.h @@ -1,67 +1,67 @@ /* Copyright (C) 2003-2010 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. */ #ifndef SELECTIONINTERACTION_H #define SELECTIONINTERACTION_H -#include <QObject> - -#include <DB/FileName.h> - #include "MouseInteraction.h" #include "ThumbnailComponent.h" #include "enums.h" +#include <DB/FileName.h> + +#include <QObject> + class QMouseEvent; namespace ThumbnailView { class ThumbnailFactory; class SelectionInteraction : public QObject, public MouseInteraction, private ThumbnailComponent { Q_OBJECT public: explicit SelectionInteraction(ThumbnailFactory *factory); bool mousePressEvent(QMouseEvent *) override; bool mouseMoveEvent(QMouseEvent *) override; bool isDragging() const; protected: void startDrag(); private: /** * This variable contains the position the mouse was pressed down. * The point is in contents coordinates. */ QPoint m_mousePressPos; /** * Did the mouse interaction start with the mouse on top of an icon. */ bool m_isMouseDragOperation; // PENDING(blackie) this instance variable is unused! DB::FileNameSet m_originalSelectionBeforeDragStart; bool m_dragInProgress; }; } #endif /* SELECTIONINTERACTION_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/SelectionMaintainer.cpp b/ThumbnailView/SelectionMaintainer.cpp index 496515ff..c2b35d99 100644 --- a/ThumbnailView/SelectionMaintainer.cpp +++ b/ThumbnailView/SelectionMaintainer.cpp @@ -1,66 +1,67 @@ /* Copyright (C) 2003-2011 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 "SelectionMaintainer.h" + #include <DB/FileNameList.h> ThumbnailView::SelectionMaintainer::SelectionMaintainer(ThumbnailWidget *widget, ThumbnailModel *model) : m_widget(widget) , m_model(model) , m_enabled(true) { m_currentItem = widget->currentItem(); m_currentRow = widget->currentIndex().row(); m_selectedItems = widget->selection(NoExpandCollapsedStacks); if (m_selectedItems.isEmpty()) m_firstRow = -1; else m_firstRow = m_model->indexOf(m_selectedItems.at(0)); } ThumbnailView::SelectionMaintainer::~SelectionMaintainer() { if (!m_enabled) return; // We need to set the current item before we set the selection m_widget->setCurrentItem(m_currentItem); // If the previous current item was deleted, then set the last item of the selection current // This, however, need to be an actualt item, some of the previous selected items might have been deleted. if (m_widget->currentItem().isNull()) { for (int i = m_selectedItems.size() - 1; i >= 0; --i) { m_widget->setCurrentItem(m_selectedItems.at(i)); if (!m_widget->currentItem().isNull()) break; } } // Now set the selection m_widget->select(m_selectedItems); // If no item is current at this point, it means that all the items of the selection // had been deleted, so make the item just before the previous selection start the current. if (m_widget->currentItem().isNull()) m_widget->setCurrentIndex(m_model->index(m_firstRow)); } void ThumbnailView::SelectionMaintainer::disable() { m_enabled = false; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/SelectionMaintainer.h b/ThumbnailView/SelectionMaintainer.h index 2b004532..81ad8874 100644 --- a/ThumbnailView/SelectionMaintainer.h +++ b/ThumbnailView/SelectionMaintainer.h @@ -1,48 +1,49 @@ /* Copyright (C) 2003-2011 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. */ #ifndef SELECTIONMAINTAINER_H #define SELECTIONMAINTAINER_H #include "ThumbnailModel.h" #include "ThumbnailWidget.h" + #include <DB/FileNameList.h> namespace ThumbnailView { class SelectionMaintainer { public: SelectionMaintainer(ThumbnailWidget *widget, ThumbnailModel *model); ~SelectionMaintainer(); void disable(); private: ThumbnailWidget *m_widget; ThumbnailModel *m_model; DB::FileName m_currentItem; int m_currentRow; DB::FileNameList m_selectedItems; int m_firstRow; bool m_enabled; }; } #endif // SELECTIONMAINTAINER_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailComponent.cpp b/ThumbnailView/ThumbnailComponent.cpp index 0028452b..3ae41bd0 100644 --- a/ThumbnailView/ThumbnailComponent.cpp +++ b/ThumbnailView/ThumbnailComponent.cpp @@ -1,56 +1,57 @@ /* Copyright (C) 2003-2010 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 "ThumbnailComponent.h" + #include "ThumbnailFactory.h" ThumbnailView::ThumbnailComponent::ThumbnailComponent(ThumbnailFactory *factory) : m_factory(factory) { } ThumbnailView::ThumbnailModel *ThumbnailView::ThumbnailComponent::model() { return m_factory->model(); } ThumbnailView::CellGeometry *ThumbnailView::ThumbnailComponent::cellGeometryInfo() { return m_factory->cellGeometry(); } ThumbnailView::ThumbnailWidget *ThumbnailView::ThumbnailComponent::widget() { return m_factory->widget(); } const ThumbnailView::ThumbnailModel *ThumbnailView::ThumbnailComponent::model() const { return m_factory->model(); } const ThumbnailView::CellGeometry *ThumbnailView::ThumbnailComponent::cellGeometryInfo() const { return m_factory->cellGeometry(); } const ThumbnailView::ThumbnailWidget *ThumbnailView::ThumbnailComponent::widget() const { return m_factory->widget(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailDND.cpp b/ThumbnailView/ThumbnailDND.cpp index da1112f1..6b9184ac 100644 --- a/ThumbnailView/ThumbnailDND.cpp +++ b/ThumbnailView/ThumbnailDND.cpp @@ -1,149 +1,149 @@ /* Copyright (C) 2003-2010 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 "ThumbnailDND.h" -#include <QMimeData> -#include <QTimer> - -#include <KLocalizedString> -#include <KMessageBox> - #include "ThumbnailModel.h" #include "ThumbnailWidget.h" + #include <Browser/BrowserWidget.h> #include <DB/ImageDB.h> +#include <KLocalizedString> +#include <KMessageBox> +#include <QMimeData> +#include <QTimer> + ThumbnailView::ThumbnailDND::ThumbnailDND(ThumbnailFactory *factory) : ThumbnailComponent(factory) { } void ThumbnailView::ThumbnailDND::contentsDragMoveEvent(QDragMoveEvent *event) { if (event->mimeData()->hasUrls() && widget()->m_selectionInteraction.isDragging()) event->accept(); else { event->ignore(); return; } removeDropIndications(); const DB::FileName fileName = widget()->mediaIdUnderCursor(); if (fileName.isNull()) { // cursor not in drop zone (e.g. empty space right/below of the thumbnails) return; } const QRect rect = widget()->visualRect(widget()->indexUnderCursor()); if ((event->pos().y() < 10)) widget()->scrollTo(widget()->indexUnderCursor(), QAbstractItemView::PositionAtCenter); if ((event->pos().y() > widget()->viewport()->visibleRegion().cbegin()->height() - 10)) widget()->scrollTo(widget()->indexUnderCursor(), QAbstractItemView::PositionAtCenter); bool left = (event->pos().x() - rect.x() < rect.width() / 2); if (left) { model()->setLeftDropItem(fileName); const int index = model()->indexOf(fileName) - 1; if (index != -1) model()->setRightDropItem(model()->imageAt(index)); } else { model()->setRightDropItem(fileName); const int index = model()->indexOf(fileName) + 1; if (index != model()->imageCount()) model()->setLeftDropItem(model()->imageAt(index)); } model()->updateCell(model()->leftDropItem()); model()->updateCell(model()->rightDropItem()); } void ThumbnailView::ThumbnailDND::contentsDragLeaveEvent(QDragLeaveEvent *) { removeDropIndications(); } void ThumbnailView::ThumbnailDND::contentsDropEvent(QDropEvent *event) { if (model()->leftDropItem().isNull() && model()->rightDropItem().isNull()) { // drop outside drop zone removeDropIndications(); event->ignore(); } else { QTimer::singleShot(0, this, SLOT(realDropEvent())); } } /** * Do the real work for the drop event. * We can't bring up the dialog in the contentsDropEvent, as Qt is still in drag and drop mode with a different cursor etc. * That's why we use a QTimer to get this call back executed. */ void ThumbnailView::ThumbnailDND::realDropEvent() { QString msg = i18n("<p><b>Really reorder thumbnails?</b></p>" "<p>By dragging images around in the thumbnail viewer, you actually reorder them. " "This is very useful where you do not know the exact date for the images. On the other hand, " "if the images have valid timestamps, you should use " "<b>Maintenance -> Sort All By Date and Time</b> or " "<b>View -> Sort Selected By Date and Time</b>.</p>"); if (KMessageBox::questionYesNo(widget(), msg, i18n("Reorder Thumbnails"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QString::fromLatin1("reorder_images")) == KMessageBox::Yes) { // expand selection so that stacks are always selected as a whole: const DB::FileNameList selected = widget()->selection(IncludeAllStacks); // protect against self drop if (selected.indexOf(model()->leftDropItem()) == -1 && selected.indexOf(model()->rightDropItem()) == -1) { if (model()->rightDropItem().isNull()) { // We dropped onto the first image. DB::ImageDB::instance()->reorder(model()->leftDropItem(), selected, false); } else DB::ImageDB::instance()->reorder(model()->rightDropItem(), selected, true); Browser::BrowserWidget::instance()->reload(); } } removeDropIndications(); } void ThumbnailView::ThumbnailDND::removeDropIndications() { const DB::FileName left = model()->leftDropItem(); const DB::FileName right = model()->rightDropItem(); model()->setLeftDropItem(DB::FileName()); model()->setRightDropItem(DB::FileName()); if (!left.isNull()) model()->updateCell(left); if (!right.isNull()) model()->updateCell(right); } void ThumbnailView::ThumbnailDND::contentsDragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls() && widget()->m_selectionInteraction.isDragging()) event->accept(); else event->ignore(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailDND.h b/ThumbnailView/ThumbnailDND.h index 58dd2f17..f5bbca0d 100644 --- a/ThumbnailView/ThumbnailDND.h +++ b/ThumbnailView/ThumbnailDND.h @@ -1,54 +1,54 @@ /* Copyright (C) 2003-2010 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. */ #ifndef THUMBNAILDND_H #define THUMBNAILDND_H -#include <QObject> - #include "ThumbnailComponent.h" +#include <QObject> + class QDragEnterEvent; class QDropEvent; class QDragLeaveEvent; class QDragMoveEvent; namespace ThumbnailView { class ThumbnailDND : public QObject, private ThumbnailComponent { Q_OBJECT public: explicit ThumbnailDND(ThumbnailFactory *factory); void contentsDragMoveEvent(QDragMoveEvent *event); void contentsDragLeaveEvent(QDragLeaveEvent *); void contentsDropEvent(QDropEvent *event); void contentsDragEnterEvent(QDragEnterEvent *event); private slots: void realDropEvent(); private: void removeDropIndications(); }; } #endif /* THUMBNAILDND_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailFacade.cpp b/ThumbnailView/ThumbnailFacade.cpp index 4b655954..6d885c9c 100644 --- a/ThumbnailView/ThumbnailFacade.cpp +++ b/ThumbnailView/ThumbnailFacade.cpp @@ -1,192 +1,193 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ThumbnailFacade.h" -#include "ImageManager/ThumbnailCache.h" -#include <BackgroundJobs/HandleVideoThumbnailRequestJob.h> #include "CellGeometry.h" #include "GridResizeSlider.h" -#include "Settings/SettingsData.h" #include "ThumbnailModel.h" #include "ThumbnailToolTip.h" #include "ThumbnailWidget.h" +#include <BackgroundJobs/HandleVideoThumbnailRequestJob.h> +#include <ImageManager/ThumbnailCache.h> +#include <Settings/SettingsData.h> + ThumbnailView::ThumbnailFacade *ThumbnailView::ThumbnailFacade::s_instance = nullptr; ThumbnailView::ThumbnailFacade::ThumbnailFacade() : m_cellGeometry(nullptr) , m_model(nullptr) , m_widget(nullptr) , m_toolTip(nullptr) { // To avoid one of the components references one of the other before it has been initialized, we first construct them all with null. m_cellGeometry = new CellGeometry(this); m_model = new ThumbnailModel(this); m_widget = new ThumbnailWidget(this); m_toolTip = new ThumbnailToolTip(m_widget); connect(m_widget, &ThumbnailWidget::showImage, this, &ThumbnailFacade::showImage); connect(m_widget, &ThumbnailWidget::showSelection, this, &ThumbnailFacade::showSelection); connect(m_widget, &ThumbnailWidget::fileIdUnderCursorChanged, this, &ThumbnailFacade::fileIdUnderCursorChanged); connect(m_widget, &ThumbnailWidget::currentDateChanged, this, &ThumbnailFacade::currentDateChanged); connect(m_widget, &ThumbnailWidget::selectionCountChanged, this, &ThumbnailFacade::selectionChanged); connect(m_model, &ThumbnailModel::collapseAllStacksEnabled, this, &ThumbnailFacade::collapseAllStacksEnabled); connect(m_model, &ThumbnailModel::expandAllStacksEnabled, this, &ThumbnailFacade::expandAllStacksEnabled); s_instance = this; } QWidget *ThumbnailView::ThumbnailFacade::gui() { return m_widget; } void ThumbnailView::ThumbnailFacade::gotoDate(const DB::ImageDate &date, bool b) { m_widget->gotoDate(date, b); } void ThumbnailView::ThumbnailFacade::setCurrentItem(const DB::FileName &fileName) { widget()->setCurrentItem(fileName); } void ThumbnailView::ThumbnailFacade::reload(SelectionUpdateMethod method) { m_widget->reload(method); } DB::FileNameList ThumbnailView::ThumbnailFacade::selection(ThumbnailView::SelectionMode mode) const { return m_widget->selection(mode); } DB::FileNameList ThumbnailView::ThumbnailFacade::imageList(Order order) const { return m_model->imageList(order); } DB::FileName ThumbnailView::ThumbnailFacade::mediaIdUnderCursor() const { return m_widget->mediaIdUnderCursor(); } DB::FileName ThumbnailView::ThumbnailFacade::currentItem() const { return m_model->imageAt(m_widget->currentIndex().row()); } void ThumbnailView::ThumbnailFacade::setImageList(const DB::FileNameList &list) { m_model->setImageList(list); } void ThumbnailView::ThumbnailFacade::setSortDirection(SortDirection direction) { m_model->setSortDirection(direction); } QSlider *ThumbnailView::ThumbnailFacade::createResizeSlider() { return new GridResizeSlider(this); } ThumbnailView::FilterWidget *ThumbnailView::ThumbnailFacade::createFilterWidget(QWidget *parent) { return model()->createFilterWidget(parent); } void ThumbnailView::ThumbnailFacade::selectAll() { m_widget->selectAll(); } void ThumbnailView::ThumbnailFacade::clearSelection() { m_widget->clearSelection(); } void ThumbnailView::ThumbnailFacade::showToolTipsOnImages(bool on) { m_toolTip->setActive(on); } void ThumbnailView::ThumbnailFacade::toggleStackExpansion(const DB::FileName &fileName) { m_model->toggleStackExpansion(fileName); } void ThumbnailView::ThumbnailFacade::collapseAllStacks() { m_model->collapseAllStacks(); } void ThumbnailView::ThumbnailFacade::expandAllStacks() { m_model->expandAllStacks(); } void ThumbnailView::ThumbnailFacade::updateDisplayModel() { m_model->updateDisplayModel(); } void ThumbnailView::ThumbnailFacade::changeSingleSelection(const DB::FileName &fileName) { m_widget->changeSingleSelection(fileName); } ThumbnailView::ThumbnailModel *ThumbnailView::ThumbnailFacade::model() { Q_ASSERT(m_model); return m_model; } ThumbnailView::CellGeometry *ThumbnailView::ThumbnailFacade::cellGeometry() { Q_ASSERT(m_cellGeometry); return m_cellGeometry; } ThumbnailView::ThumbnailWidget *ThumbnailView::ThumbnailFacade::widget() { Q_ASSERT(m_widget); return m_widget; } ThumbnailView::ThumbnailFacade *ThumbnailView::ThumbnailFacade::instance() { Q_ASSERT(s_instance); return s_instance; } void ThumbnailView::ThumbnailFacade::slotRecreateThumbnail() { Q_FOREACH (const DB::FileName &fileName, widget()->selection(NoExpandCollapsedStacks)) { ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); BackgroundJobs::HandleVideoThumbnailRequestJob::removeFullScaleFrame(fileName); m_model->updateCell(fileName); } } void ThumbnailView::ThumbnailFacade::clearFilter() { Q_ASSERT(m_model); m_model->clearFilter(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailFacade.h b/ThumbnailView/ThumbnailFacade.h index 0a99cc41..d6114082 100644 --- a/ThumbnailView/ThumbnailFacade.h +++ b/ThumbnailView/ThumbnailFacade.h @@ -1,103 +1,104 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef THUMBNAILFACADE_H #define THUMBNAILFACADE_H #include "ThumbnailFactory.h" #include "ThumbnailWidget.h" + #include <DB/FileNameList.h> class QSlider; namespace ThumbnailView { class ThumbnailModel; class CellGeometry; class FilterWidget; class ThumbnailPainter; class ThumbnailToolTip; class ThumbnailFacade : public QObject, public ThumbnailFactory { Q_OBJECT public: static ThumbnailFacade *instance(); ThumbnailFacade(); QWidget *gui(); void setCurrentItem(const DB::FileName &fileName); void reload(SelectionUpdateMethod method); DB::FileNameList selection(ThumbnailView::SelectionMode mode = ExpandCollapsedStacks) const; DB::FileNameList imageList(Order) const; DB::FileName mediaIdUnderCursor() const; DB::FileName currentItem() const; void setImageList(const DB::FileNameList &list); void setSortDirection(SortDirection); /** * @brief createResizeSlider returns a QSlider that can be used to resize the thumbnail grid. * @return a (horizontal) QSlider */ QSlider *createResizeSlider(); /** * @brief createFilterWidget that is connected to the ThumbnailModel. * It will reflect changes in the filter and can be used to set the filter. * @param parent * @return a new FilterWidget with the given parent. */ FilterWidget *createFilterWidget(QWidget *parent = nullptr); public slots: void gotoDate(const DB::ImageDate &date, bool includeRanges); void selectAll(); void clearSelection(); void showToolTipsOnImages(bool b); void toggleStackExpansion(const DB::FileName &id); void collapseAllStacks(); void expandAllStacks(); void updateDisplayModel(); void changeSingleSelection(const DB::FileName &fileName); void slotRecreateThumbnail(); void clearFilter(); signals: void showImage(const DB::FileName &id); void showSelection(); void fileIdUnderCursorChanged(const DB::FileName &id); void currentDateChanged(const QDateTime &); void selectionChanged(int numberOfItemsSelected); void collapseAllStacksEnabled(bool enabled); void expandAllStacksEnabled(bool enabled); private: ThumbnailModel *model() override; CellGeometry *cellGeometry() override; ThumbnailWidget *widget() override; private: static ThumbnailFacade *s_instance; CellGeometry *m_cellGeometry; ThumbnailModel *m_model; ThumbnailWidget *m_widget; ThumbnailPainter *m_painter; ThumbnailToolTip *m_toolTip; }; } #endif /* THUMBNAILFACADE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailModel.cpp b/ThumbnailView/ThumbnailModel.cpp index f13c3aae..eaa81fd7 100644 --- a/ThumbnailView/ThumbnailModel.cpp +++ b/ThumbnailView/ThumbnailModel.cpp @@ -1,548 +1,547 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "ThumbnailModel.h" -#include <QIcon> -#include <QLoggingCategory> - -#include <KLocalizedString> +#include "CellGeometry.h" +#include "FilterWidget.h" +#include "Logging.h" +#include "SelectionMaintainer.h" +#include "ThumbnailRequest.h" +#include "ThumbnailWidget.h" #include <DB/FileName.h> #include <DB/ImageDB.h> #include <ImageManager/AsyncLoader.h> #include <ImageManager/ThumbnailCache.h> #include <Settings/SettingsData.h> #include <Utilities/FileUtil.h> -#include "CellGeometry.h" -#include "FilterWidget.h" -#include "Logging.h" -#include "SelectionMaintainer.h" -#include "ThumbnailRequest.h" -#include "ThumbnailWidget.h" +#include <KLocalizedString> +#include <QIcon> +#include <QLoggingCategory> ThumbnailView::ThumbnailModel::ThumbnailModel(ThumbnailFactory *factory) : ThumbnailComponent(factory) , m_sortDirection(Settings::SettingsData::instance()->showNewestThumbnailFirst() ? NewestFirst : OldestFirst) , m_firstVisibleRow(-1) , m_lastVisibleRow(-1) { connect(DB::ImageDB::instance(), SIGNAL(imagesDeleted(DB::FileNameList)), this, SLOT(imagesDeletedFromDB(DB::FileNameList))); m_ImagePlaceholder = QIcon::fromTheme(QLatin1String("image-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize()); m_VideoPlaceholder = QIcon::fromTheme(QLatin1String("video-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize()); m_filter.setSearchMode(0); connect(this, &ThumbnailModel::filterChanged, this, &ThumbnailModel::updateDisplayModel); } static bool stackOrderComparator(const DB::FileName &a, const DB::FileName &b) { return a.info()->stackOrder() < b.info()->stackOrder(); } void ThumbnailView::ThumbnailModel::updateDisplayModel() { beginResetModel(); ImageManager::AsyncLoader::instance()->stop(model(), ImageManager::StopOnlyNonPriorityLoads); // Note, this can be simplified, if we make the database backend already // return things in the right order. Then we only need one pass while now // we need to go through the list two times. /* Extract all stacks we have first. Different stackid's might be * intermingled in the result so we need to know this ahead before * creating the display list. */ typedef QList<DB::FileName> StackList; typedef QMap<DB::StackID, StackList> StackMap; StackMap stackContents; Q_FOREACH (const DB::FileName &fileName, m_imageList) { DB::ImageInfoPtr imageInfo = fileName.info(); if (imageInfo && imageInfo->isStacked()) { DB::StackID stackid = imageInfo->stackId(); stackContents[stackid].append(fileName); } } /* * All stacks need to be ordered in their stack order. We don't rely that * the images actually came in the order necessary. */ for (StackMap::iterator it = stackContents.begin(); it != stackContents.end(); ++it) { std::stable_sort(it->begin(), it->end(), stackOrderComparator); } /* Build the final list to be displayed. That is basically the sequence * we got from the original, but the stacks shown with all images together * in the right sequence or collapsed showing only the top image. */ m_displayList = DB::FileNameList(); QSet<DB::StackID> alreadyShownStacks; Q_FOREACH (const DB::FileName &fileName, m_imageList) { DB::ImageInfoPtr imageInfo = fileName.info(); if (!m_filter.match(imageInfo)) continue; if (imageInfo && imageInfo->isStacked()) { DB::StackID stackid = imageInfo->stackId(); if (alreadyShownStacks.contains(stackid)) continue; StackMap::iterator found = stackContents.find(stackid); Q_ASSERT(found != stackContents.end()); const StackList &orderedStack = *found; if (m_expandedStacks.contains(stackid)) { Q_FOREACH (const DB::FileName &fileName, orderedStack) { m_displayList.append(fileName); } } else { m_displayList.append(orderedStack.at(0)); } alreadyShownStacks.insert(stackid); } else { m_displayList.append(fileName); } } if (m_sortDirection != OldestFirst) m_displayList = m_displayList.reversed(); updateIndexCache(); emit collapseAllStacksEnabled(m_expandedStacks.size() > 0); emit expandAllStacksEnabled(m_allStacks.size() != model()->m_expandedStacks.size()); endResetModel(); } void ThumbnailView::ThumbnailModel::toggleStackExpansion(const DB::FileName &fileName) { DB::ImageInfoPtr imageInfo = fileName.info(); if (imageInfo) { DB::StackID stackid = imageInfo->stackId(); model()->beginResetModel(); if (m_expandedStacks.contains(stackid)) m_expandedStacks.remove(stackid); else m_expandedStacks.insert(stackid); updateDisplayModel(); model()->endResetModel(); } } void ThumbnailView::ThumbnailModel::collapseAllStacks() { m_expandedStacks.clear(); updateDisplayModel(); } void ThumbnailView::ThumbnailModel::expandAllStacks() { m_expandedStacks = m_allStacks; updateDisplayModel(); } void ThumbnailView::ThumbnailModel::setImageList(const DB::FileNameList &items) { m_imageList = items; m_allStacks.clear(); Q_FOREACH (const DB::FileName &fileName, items) { DB::ImageInfoPtr info = fileName.info(); if (info && info->isStacked()) m_allStacks << info->stackId(); } updateDisplayModel(); preloadThumbnails(); } // TODO(hzeller) figure out if this should return the m_imageList or m_displayList. DB::FileNameList ThumbnailView::ThumbnailModel::imageList(Order order) const { if (order == SortedOrder && m_sortDirection == NewestFirst) return m_displayList.reversed(); else return m_displayList; } void ThumbnailView::ThumbnailModel::imagesDeletedFromDB(const DB::FileNameList &list) { SelectionMaintainer dummy(widget(), model()); Q_FOREACH (const DB::FileName &fileName, list) { m_displayList.removeAll(fileName); m_imageList.removeAll(fileName); } updateDisplayModel(); } int ThumbnailView::ThumbnailModel::indexOf(const DB::FileName &fileName) { Q_ASSERT(!fileName.isNull()); if (!m_fileNameToIndex.contains(fileName)) m_fileNameToIndex.insert(fileName, m_displayList.indexOf(fileName)); return m_fileNameToIndex[fileName]; } int ThumbnailView::ThumbnailModel::indexOf(const DB::FileName &fileName) const { Q_ASSERT(!fileName.isNull()); if (!m_fileNameToIndex.contains(fileName)) return -1; return m_fileNameToIndex[fileName]; } void ThumbnailView::ThumbnailModel::updateIndexCache() { m_fileNameToIndex.clear(); int index = 0; Q_FOREACH (const DB::FileName &fileName, m_displayList) { m_fileNameToIndex[fileName] = index; ++index; } } DB::FileName ThumbnailView::ThumbnailModel::rightDropItem() const { return m_rightDrop; } void ThumbnailView::ThumbnailModel::setRightDropItem(const DB::FileName &item) { m_rightDrop = item; } DB::FileName ThumbnailView::ThumbnailModel::leftDropItem() const { return m_leftDrop; } void ThumbnailView::ThumbnailModel::setLeftDropItem(const DB::FileName &item) { m_leftDrop = item; } void ThumbnailView::ThumbnailModel::setSortDirection(SortDirection direction) { if (direction == m_sortDirection) return; Settings::SettingsData::instance()->setShowNewestFirst(direction == NewestFirst); m_displayList = m_displayList.reversed(); updateIndexCache(); m_sortDirection = direction; } bool ThumbnailView::ThumbnailModel::isItemInExpandedStack(const DB::StackID &id) const { return m_expandedStacks.contains(id); } int ThumbnailView::ThumbnailModel::imageCount() const { return m_displayList.size(); } void ThumbnailView::ThumbnailModel::setOverrideImage(const DB::FileName &fileName, const QPixmap &pixmap) { if (pixmap.isNull()) m_overrideFileName = DB::FileName(); else { m_overrideFileName = fileName; m_overrideImage = pixmap; } emit dataChanged(fileNameToIndex(fileName), fileNameToIndex(fileName)); } DB::FileName ThumbnailView::ThumbnailModel::imageAt(int index) const { Q_ASSERT(index >= 0 && index < imageCount()); return m_displayList.at(index); } int ThumbnailView::ThumbnailModel::rowCount(const QModelIndex &) const { return imageCount(); } QVariant ThumbnailView::ThumbnailModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= m_displayList.size()) return QVariant(); if (role == Qt::DecorationRole) { const DB::FileName fileName = m_displayList.at(index.row()); return pixmap(fileName); } if (role == Qt::DisplayRole) return thumbnailText(index); return QVariant(); } void ThumbnailView::ThumbnailModel::requestThumbnail(const DB::FileName &fileName, const ImageManager::Priority priority) { DB::ImageInfoPtr imageInfo = fileName.info(); if (!imageInfo) return; // request the thumbnail in the size that is set in the settings, not in the current grid size: const QSize cellSize = cellGeometryInfo()->baseIconSize(); const int angle = imageInfo->angle(); const int row = indexOf(fileName); ThumbnailRequest *request = new ThumbnailRequest(row, fileName, cellSize, angle, this); request->setPriority(priority); ImageManager::AsyncLoader::instance()->load(request); } void ThumbnailView::ThumbnailModel::pixmapLoaded(ImageManager::ImageRequest *request, const QImage & /*image*/) { const DB::FileName fileName = request->databaseFileName(); const QSize fullSize = request->fullSize(); // As a result of the image being loaded, we emit the dataChanged signal, which in turn asks the delegate to paint the cell // The delegate now fetches the newly loaded image from the cache. DB::ImageInfoPtr imageInfo = fileName.info(); // TODO(hzeller): figure out, why the size is set here. We do an implicit // write here to the database. if (fullSize.isValid() && imageInfo) { imageInfo->setSize(fullSize); } emit dataChanged(fileNameToIndex(fileName), fileNameToIndex(fileName)); } QString ThumbnailView::ThumbnailModel::thumbnailText(const QModelIndex &index) const { const DB::FileName fileName = imageAt(index.row()); QString text; const QSize cellSize = cellGeometryInfo()->preferredIconSize(); const int thumbnailHeight = cellSize.height() - 2 * Settings::SettingsData::instance()->thumbnailSpace(); const int thumbnailWidth = cellSize.width(); // no subtracting here const int maxCharacters = thumbnailHeight / QFontMetrics(widget()->font()).maxWidth() * 2; if (Settings::SettingsData::instance()->displayLabels()) { QString line = fileName.info()->label(); if (QFontMetrics(widget()->font()).width(line) > thumbnailWidth) { line = line.left(maxCharacters); line += QString::fromLatin1(" ..."); } text += line + QString::fromLatin1("\n"); } if (Settings::SettingsData::instance()->displayCategories()) { QStringList grps = fileName.info()->availableCategories(); for (QStringList::const_iterator it = grps.constBegin(); it != grps.constEnd(); ++it) { QString category = *it; if (category != i18n("Folder") && category != i18n("Media Type")) { Utilities::StringSet items = fileName.info()->itemsOfCategory(category); if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && !Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (category == Settings::SettingsData::instance()->untaggedCategory()) { if (items.contains(Settings::SettingsData::instance()->untaggedTag())) { items.remove(Settings::SettingsData::instance()->untaggedTag()); } } } if (!items.empty()) { QString line; bool first = true; for (Utilities::StringSet::const_iterator it2 = items.begin(); it2 != items.end(); ++it2) { QString item = *it2; if (first) first = false; else line += QString::fromLatin1(", "); line += item; } if (QFontMetrics(widget()->font()).width(line) > thumbnailWidth) { line = line.left(maxCharacters); line += QString::fromLatin1(" ..."); } text += line + QString::fromLatin1("\n"); } } } } if (text.isEmpty()) text = QString::fromLatin1(""); return text.trimmed(); } void ThumbnailView::ThumbnailModel::updateCell(int row) { updateCell(index(row, 0)); } void ThumbnailView::ThumbnailModel::updateCell(const QModelIndex &index) { emit dataChanged(index, index); } void ThumbnailView::ThumbnailModel::updateCell(const DB::FileName &fileName) { updateCell(indexOf(fileName)); } QModelIndex ThumbnailView::ThumbnailModel::fileNameToIndex(const DB::FileName &fileName) const { if (fileName.isNull()) return QModelIndex(); else return index(indexOf(fileName), 0); } QPixmap ThumbnailView::ThumbnailModel::pixmap(const DB::FileName &fileName) const { if (m_overrideFileName == fileName) return m_overrideImage; const DB::ImageInfoPtr imageInfo = fileName.info(); if (imageInfo == DB::ImageInfoPtr(nullptr)) return QPixmap(); if (ImageManager::ThumbnailCache::instance()->contains(fileName)) { // the cached thumbnail needs to be scaled to the actual thumbnail size: return ImageManager::ThumbnailCache::instance()->lookup(fileName).scaled(cellGeometryInfo()->preferredIconSize(), Qt::KeepAspectRatio); } const_cast<ThumbnailView::ThumbnailModel *>(this)->requestThumbnail(fileName, ImageManager::ThumbnailVisible); if (imageInfo->isVideo()) return m_VideoPlaceholder; else return m_ImagePlaceholder; } bool ThumbnailView::ThumbnailModel::isFiltered() const { return !m_filter.isNull(); } ThumbnailView::FilterWidget *ThumbnailView::ThumbnailModel::createFilterWidget(QWidget *parent) { auto widget = new FilterWidget(parent); connect(this, &ThumbnailModel::filterChanged, widget, &FilterWidget::setFilter); connect(widget, &FilterWidget::ratingChanged, this, &ThumbnailModel::filterByRating); connect(widget, &FilterWidget::filterToggled, this, &ThumbnailModel::toggleFilter); return widget; } bool ThumbnailView::ThumbnailModel::thumbnailStillNeeded(int row) const { return (row >= m_firstVisibleRow && row <= m_lastVisibleRow); } void ThumbnailView::ThumbnailModel::updateVisibleRowInfo() { m_firstVisibleRow = widget()->indexAt(QPoint(0, 0)).row(); const int columns = widget()->width() / cellGeometryInfo()->cellSize().width(); const int rows = widget()->height() / cellGeometryInfo()->cellSize().height(); m_lastVisibleRow = qMin(m_firstVisibleRow + columns * (rows + 1), rowCount(QModelIndex())); // the cellGeometry has changed -> update placeholders m_ImagePlaceholder = QIcon::fromTheme(QLatin1String("image-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize()); m_VideoPlaceholder = QIcon::fromTheme(QLatin1String("video-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize()); } void ThumbnailView::ThumbnailModel::toggleFilter(bool enable) { if (!enable) clearFilter(); else if (m_filter.isNull()) { std::swap(m_filter, m_previousFilter); emit filterChanged(m_filter); } } void ThumbnailView::ThumbnailModel::clearFilter() { if (!m_filter.isNull()) { qCDebug(ThumbnailViewLog) << "Filter cleared."; m_previousFilter = m_filter; m_filter = DB::ImageSearchInfo(); emit filterChanged(m_filter); } } void ThumbnailView::ThumbnailModel::filterByRating(short rating) { Q_ASSERT(-1 <= rating && rating <= 10); qCDebug(ThumbnailViewLog) << "Filter set: rating(" << rating << ")"; m_filter.setRating(rating); emit filterChanged(m_filter); } void ThumbnailView::ThumbnailModel::toggleRatingFilter(short rating) { if (m_filter.rating() == rating) { filterByRating(rating); } else { filterByRating(-1); qCDebug(ThumbnailViewLog) << "Filter removed: rating"; m_filter.setRating(-1); m_filter.checkIfNull(); emit filterChanged(m_filter); } } void ThumbnailView::ThumbnailModel::filterByCategory(const QString &category, const QString &tag) { qCDebug(ThumbnailViewLog) << "Filter added: category(" << category << "," << tag << ")"; m_filter.addAnd(category, tag); emit filterChanged(m_filter); } void ThumbnailView::ThumbnailModel::toggleCategoryFilter(const QString &category, const QString &tag) { auto tags = m_filter.categoryMatchText(category).split(QString::fromLatin1("&"), QString::SkipEmptyParts); for (const auto &existingTag : tags) { if (tag == existingTag.trimmed()) { qCDebug(ThumbnailViewLog) << "Filter removed: category(" << category << "," << tag << ")"; tags.removeAll(existingTag); m_filter.setCategoryMatchText(category, tags.join(QString::fromLatin1(" & "))); m_filter.checkIfNull(); emit filterChanged(m_filter); return; } } filterByCategory(category, tag); } void ThumbnailView::ThumbnailModel::preloadThumbnails() { // FIXME: it would make a lot of sense to merge preloadThumbnails() with pixmap() // and maybe also move the caching stuff into the ImageManager Q_FOREACH (const DB::FileName &fileName, m_displayList) { if (fileName.isNull()) continue; if (ImageManager::ThumbnailCache::instance()->contains(fileName)) continue; const_cast<ThumbnailView::ThumbnailModel *>(this)->requestThumbnail(fileName, ImageManager::ThumbnailInvisible); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailModel.h b/ThumbnailView/ThumbnailModel.h index 0537a5f6..b29349c7 100644 --- a/ThumbnailView/ThumbnailModel.h +++ b/ThumbnailView/ThumbnailModel.h @@ -1,204 +1,205 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef THUMBNAILMODEL_H #define THUMBNAILMODEL_H -#include <QAbstractListModel> -#include <QPixmap> +#include "ThumbnailComponent.h" +#include "enums.h" #include <DB/FileNameList.h> #include <DB/ImageInfo.h> #include <DB/ImageSearchInfo.h> #include <ImageManager/ImageClientInterface.h> #include <ImageManager/enums.h> -#include <ThumbnailView/ThumbnailComponent.h> -#include <ThumbnailView/enums.h> + +#include <QAbstractListModel> +#include <QPixmap> namespace ThumbnailView { class ThumbnailFactory; class FilterWidget; class ThumbnailModel : public QAbstractListModel, public ImageManager::ImageClientInterface, private ThumbnailComponent { Q_OBJECT public: explicit ThumbnailModel(ThumbnailFactory *factory); // -------------------------------------------------- QAbstractListModel using QAbstractListModel::beginResetModel; using QAbstractListModel::endResetModel; int rowCount(const QModelIndex &) const override; QVariant data(const QModelIndex &, int) const override; QString thumbnailText(const QModelIndex &index) const; void updateCell(int row); void updateCell(const QModelIndex &index); void updateCell(const DB::FileName &id); // -------------------------------------------------- ImageClient API void pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) override; bool thumbnailStillNeeded(int row) const; //-------------------------------------------------- Drag and Drop of items DB::FileName rightDropItem() const; void setRightDropItem(const DB::FileName &item); DB::FileName leftDropItem() const; void setLeftDropItem(const DB::FileName &item); //-------------------------------------------------- Stack void toggleStackExpansion(const DB::FileName &id); void collapseAllStacks(); void expandAllStacks(); bool isItemInExpandedStack(const DB::StackID &id) const; //-------------------------------------------------- Position Information DB::FileName imageAt(int index) const; int indexOf(const DB::FileName &fileName) const; int indexOf(const DB::FileName &fileName); QModelIndex fileNameToIndex(const DB::FileName &fileName) const; //-------------------------------------------------- Images void setImageList(const DB::FileNameList &list); DB::FileNameList imageList(Order) const; int imageCount() const; void setOverrideImage(const DB::FileName &fileName, const QPixmap &pixmap); //-------------------------------------------------- Misc. void updateDisplayModel(); void updateIndexCache(); void setSortDirection(SortDirection); QPixmap pixmap(const DB::FileName &fileName) const; /** * @brief isFiltered * @return \c true, if the filter is currently active, \c false otherwise. */ bool isFiltered() const; FilterWidget *createFilterWidget(QWidget *parent = nullptr); public slots: void updateVisibleRowInfo(); void toggleFilter(bool enable); /** * @brief clearFilter clears the filter so that all images in the current view are displayed. */ void clearFilter(); /** * @brief filterByRating sets the filter to only show images with the given rating. * @param rating a number between 0 and 10 (or -1 to disable) */ void filterByRating(short rating); /** * @brief toggleRatingFilter sets the filter to only show images with the given rating, * if no rating filter is active. If the rating filter is already set to the given rating, * clear the rating filter. * @param rating a number between 0 and 10 */ void toggleRatingFilter(short rating); /** * @brief filterByCategory sets the filter to only show images with the given tag. * Calling this method again for the same category will overwrite the previous filter * for that category. * @param category * @param tag * * @see DB::ImageSearchinfo::setCategoryMatchText() */ void filterByCategory(const QString &category, const QString &tag); /** * @brief toggleCategoryFilter is similar to filterByCategory(), except resets the * category filter if called again with the same value. * @param category * @param tag */ void toggleCategoryFilter(const QString &category, const QString &tag); signals: void collapseAllStacksEnabled(bool enabled); void expandAllStacksEnabled(bool enabled); void selectionChanged(int numberOfItemsSelected); void filterChanged(const DB::ImageSearchInfo &filter); private: // Methods void requestThumbnail(const DB::FileName &mediaId, const ImageManager::Priority priority); void preloadThumbnails(); private slots: void imagesDeletedFromDB(const DB::FileNameList &); private: // Instance variables. /** * The list of images shown. The difference between m_imageList and * m_displayList is that m_imageList contains all the images given to us, * while m_displayList only includes those that currently should be * shown, ie. it exclude images from stacks that are collapsed and thus * not visible. */ DB::FileNameList m_displayList; /** The input list for images. See documentation for m_displayList */ DB::FileNameList m_imageList; /** * File which should have drop indication point drawn on its left side */ DB::FileName m_leftDrop; /** * File which should have drop indication point drawn on its right side */ DB::FileName m_rightDrop; SortDirection m_sortDirection; /** * All the stacks that should be shown expanded */ QSet<DB::StackID> m_expandedStacks; /** @short Store stack IDs for all images in current list * * Used by expandAllStacks. */ QSet<DB::StackID> m_allStacks; /** * A map mapping from Id to its index in m_displayList. */ QMap<DB::FileName, int> m_fileNameToIndex; int m_firstVisibleRow; int m_lastVisibleRow; DB::FileName m_overrideFileName; QPixmap m_overrideImage; // placeholder pixmaps to be displayed before thumbnails are loaded: QPixmap m_ImagePlaceholder; QPixmap m_VideoPlaceholder; DB::ImageSearchInfo m_filter; DB::ImageSearchInfo m_previousFilter; }; } #endif /* THUMBNAILMODEL_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailRequest.cpp b/ThumbnailView/ThumbnailRequest.cpp index daec19fc..c3a6f51c 100644 --- a/ThumbnailView/ThumbnailRequest.cpp +++ b/ThumbnailView/ThumbnailRequest.cpp @@ -1,33 +1,34 @@ /* Copyright (C) 2003-2010 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 "ThumbnailRequest.h" + #include "ThumbnailModel.h" ThumbnailView::ThumbnailRequest::ThumbnailRequest(int row, const DB::FileName &fileName, const QSize &size, int angle, ThumbnailModel *client) : ImageManager::ImageRequest(fileName, size, angle, client) , m_thumbnailModel(client) , m_row(row) { setIsThumbnailRequest(true); } bool ThumbnailView::ThumbnailRequest::stillNeeded() const { return m_thumbnailModel->thumbnailStillNeeded(m_row); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailRequest.h b/ThumbnailView/ThumbnailRequest.h index cace4f1a..13b0736b 100644 --- a/ThumbnailView/ThumbnailRequest.h +++ b/ThumbnailView/ThumbnailRequest.h @@ -1,41 +1,41 @@ /* Copyright (C) 2003-2010 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. */ #ifndef THUMBNAILREQUEST_H #define THUMBNAILREQUEST_H -#include "ImageManager/ImageRequest.h" +#include <ImageManager/ImageRequest.h> namespace ThumbnailView { class ThumbnailModel; class ThumbnailRequest : public ImageManager::ImageRequest { public: ThumbnailRequest(int row, const DB::FileName &fileName, const QSize &size, int angle, ThumbnailModel *client); bool stillNeeded() const override; private: const ThumbnailModel *const m_thumbnailModel; int m_row; }; } #endif /* THUMBNAILREQUEST_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailToolTip.cpp b/ThumbnailView/ThumbnailToolTip.cpp index a8e662aa..bac7eb82 100644 --- a/ThumbnailView/ThumbnailToolTip.cpp +++ b/ThumbnailView/ThumbnailToolTip.cpp @@ -1,126 +1,127 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "ThumbnailWidget.h" + +#include <DB/ImageDB.h> +#include <DB/ImageInfo.h> +#include <Settings/SettingsData.h> +#include <Utilities/FileUtil.h> + #include <QApplication> #include <QCursor> #include <QDesktopWidget> #include <QScreen> -#include "DB/ImageDB.h" -#include "DB/ImageInfo.h" -#include "Settings/SettingsData.h" -#include "ThumbnailWidget.h" -#include "Utilities/FileUtil.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()); // TODO: remove this version check once we don't care about Ubuntu 18.04 LTS anymore #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) QScreen *screen = qApp->screenAt(QCursor::pos()); if (!screen) return; QRect geom = screen->geometry(); #else QRect geom = qApp->desktop()->screenGeometry(QCursor::pos()); #endif // 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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailToolTip.h b/ThumbnailView/ThumbnailToolTip.h index af7cd992..d982889f 100644 --- a/ThumbnailView/ThumbnailToolTip.h +++ b/ThumbnailView/ThumbnailToolTip.h @@ -1,61 +1,62 @@ /* Copyright (C) 2003-2010 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. */ #ifndef THUMBNAILTOOLTIP_H #define THUMBNAILTOOLTIP_H -#include "ImageManager/ImageClientInterface.h" -#include "Utilities/ToolTip.h" #include <DB/FileName.h> +#include <ImageManager/ImageClientInterface.h> +#include <Utilities/ToolTip.h> + #include <QEvent> #include <qlabel.h> #include <qtimer.h> namespace DB { class ImageInfo; } namespace ThumbnailView { class ThumbnailWidget; class ThumbnailToolTip : public Utilities::ToolTip { Q_OBJECT public: explicit ThumbnailToolTip(ThumbnailWidget *view); virtual void setActive(bool); private slots: void requestToolTip(); private: bool eventFilter(QObject *, QEvent *e) override; void placeWindow() override; private: ThumbnailWidget *m_view; bool m_widthInverse; bool m_heightInverse; }; } #endif /* THUMBNAILTOOLTIP_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailWidget.cpp b/ThumbnailView/ThumbnailWidget.cpp index 8820b94f..470d4da8 100644 --- a/ThumbnailView/ThumbnailWidget.cpp +++ b/ThumbnailView/ThumbnailWidget.cpp @@ -1,429 +1,430 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "CellGeometry.h" #include "Delegate.h" #include "KeyboardEventHandler.h" +#include "SelectionMaintainer.h" #include "ThumbnailDND.h" #include "ThumbnailFactory.h" #include "ThumbnailModel.h" + +#include <Browser/BrowserWidget.h> +#include <DB/ImageDB.h> +#include <DB/ImageInfoPtr.h> +#include <Settings/SettingsData.h> + +#include <KLocalizedString> #include <QScrollBar> #include <QTimer> #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 "SelectionMaintainer.h" -#include "Settings/SettingsData.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, 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()); } QColor ThumbnailView::contrastColor(const QColor &color) { if (color.red() < 127 && color.green() < 127 && color.blue() < 127) return Qt::white; else return Qt::black; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/ThumbnailWidget.h b/ThumbnailView/ThumbnailWidget.h index b957e306..205f5513 100644 --- a/ThumbnailView/ThumbnailWidget.h +++ b/ThumbnailView/ThumbnailWidget.h @@ -1,157 +1,158 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef THUMBNAILVIEW_THUMBNAILWIDGET_H #define THUMBNAILVIEW_THUMBNAILWIDGET_H #include "GridResizeInteraction.h" #include "MouseTrackingInteraction.h" #include "SelectionInteraction.h" #include "ThumbnailComponent.h" -#include "ThumbnailView/enums.h" #include "VideoThumbnailCycler.h" +#include "enums.h" + #include <QListView> #include <QScopedPointer> class QTimer; class QDateTime; namespace DB { class ImageDate; class Id; class FileNameList; } namespace ThumbnailView { class ThumbnailPainter; class CellGeometry; class ThumbnailModel; class ThumbnailFactory; class KeyboardEventHandler; class ThumbnailDND; class ThumbnailWidget : public QListView, private ThumbnailComponent { Q_OBJECT public: explicit ThumbnailWidget(ThumbnailFactory *factory); void reload(SelectionUpdateMethod method); DB::FileName mediaIdUnderCursor() const; QModelIndex indexUnderCursor() const; bool isMouseOverStackIndicator(const QPoint &point); bool isGridResizing() const; void setCurrentItem(const DB::FileName &fileName); DB::FileName currentItem() const; void changeSingleSelection(const DB::FileName &fileName); // Misc int cellWidth() const; void showEvent(QShowEvent *) override; DB::FileNameList selection(ThumbnailView::SelectionMode mode) const; bool isSelected(const DB::FileName &id) const; void select(const DB::FileNameList &); bool isItemUnderCursorSelected() const; public slots: void gotoDate(const DB::ImageDate &date, bool includeRanges); /** * @brief setExternallyResizing * Used by the GridResizeSlider to indicate that the grid is being resized. * @param state true, if the grid is being resized by an external widget, false if not */ void setExternallyResizing(bool state); signals: void showImage(const DB::FileName &id); void showSelection(); void fileIdUnderCursorChanged(const DB::FileName &id); void currentDateChanged(const QDateTime &); void selectionCountChanged(int numberOfItemsSelected); protected: // event handlers void keyPressEvent(QKeyEvent *) override; void keyReleaseEvent(QKeyEvent *) override; void mousePressEvent(QMouseEvent *) override; void mouseMoveEvent(QMouseEvent *) override; void mouseReleaseEvent(QMouseEvent *) override; void mouseDoubleClickEvent(QMouseEvent *) override; void wheelEvent(QWheelEvent *) override; // Drag and drop void dragEnterEvent(QDragEnterEvent *event) override; void dragMoveEvent(QDragMoveEvent *) override; void dragLeaveEvent(QDragLeaveEvent *) override; void dropEvent(QDropEvent *) override; private slots: void emitDateChange(); void scheduleDateChangeSignal(); void emitSelectionChangedSignal(); private: friend class GridResizeInteraction; inline ThumbnailModel *model() { return ThumbnailComponent::model(); } inline const ThumbnailModel *model() const { return ThumbnailComponent::model(); } void updatePalette(); void setupDateChangeTimer(); /** * When the user selects a date on the date bar the thumbnail view will * position itself accordingly. As a consequence, the thumbnail view * is telling the date bar which date it moved to. This is all fine * except for the fact that the date selected in the date bar, may be * for an image which is in the middle of a line, while the date * emitted from the thumbnail view is for the top most image in * the view (that is the first image on the line), which results in a * different cell being selected in the date bar, than what the user * selected. * Therefore we need this variable to disable the emission of the date * change while setting the date. */ bool m_isSettingDate; GridResizeInteraction m_gridResizeInteraction; bool m_wheelResizing; bool m_externallyResizing; SelectionInteraction m_selectionInteraction; MouseTrackingInteraction m_mouseTrackingHandler; MouseInteraction *m_mouseHandler; ThumbnailDND *m_dndHandler; bool m_pressOnStackIndicator; QTimer *m_dateChangedTimer; friend class SelectionInteraction; friend class KeyboardEventHandler; friend class ThumbnailDND; friend class ThumbnailModel; KeyboardEventHandler *m_keyboardHandler; QScopedPointer<VideoThumbnailCycler> m_videoThumbnailCycler; }; QColor contrastColor(const QColor &color); } #endif /* THUMBNAILVIEW_THUMBNAILWIDGET_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/VideoThumbnailCycler.cpp b/ThumbnailView/VideoThumbnailCycler.cpp index 3a02fad7..39cdddb4 100644 --- a/ThumbnailView/VideoThumbnailCycler.cpp +++ b/ThumbnailView/VideoThumbnailCycler.cpp @@ -1,96 +1,99 @@ /* Copyright 2012 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 "VideoThumbnailCycler.h" + +#include "CellGeometry.h" #include "ThumbnailModel.h" + #include <DB/ImageInfo.h> #include <DB/ImageInfoPtr.h> #include <ImageManager/VideoThumbnails.h> -#include <QTimer> -#include <ThumbnailView/CellGeometry.h> #include <Utilities/VideoUtil.h> +#include <QTimer> + ThumbnailView::VideoThumbnailCycler *ThumbnailView::VideoThumbnailCycler::s_instance = nullptr; ThumbnailView::VideoThumbnailCycler::VideoThumbnailCycler(ThumbnailModel *model, QObject *parent) : QObject(parent) , m_thumbnails(new ImageManager::VideoThumbnails(this)) , m_model(model) { m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, m_thumbnails, &ImageManager::VideoThumbnails::requestNext); connect(m_thumbnails, &ImageManager::VideoThumbnails::frameLoaded, this, &VideoThumbnailCycler::gotFrame); Q_ASSERT(!s_instance); s_instance = this; } ThumbnailView::VideoThumbnailCycler *ThumbnailView::VideoThumbnailCycler::instance() { Q_ASSERT(s_instance); return s_instance; } void ThumbnailView::VideoThumbnailCycler::setActive(const DB::FileName &fileName) { if (m_fileName == fileName) return; stopCycle(); m_fileName = fileName; if (!m_fileName.isNull() && isVideo(m_fileName)) startCycle(); } void ThumbnailView::VideoThumbnailCycler::gotFrame(const QImage &image) { QImage img = image.scaled(ThumbnailView::CellGeometry::preferredIconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation); m_model->setOverrideImage(m_fileName, QPixmap::fromImage(img)); } void ThumbnailView::VideoThumbnailCycler::resetPreviousThumbail() { if (m_fileName.isNull() || !isVideo(m_fileName)) return; m_model->setOverrideImage(m_fileName, QPixmap()); } bool ThumbnailView::VideoThumbnailCycler::isVideo(const DB::FileName &fileName) const { if (!fileName.isNull()) return Utilities::isVideo(fileName); else return false; } void ThumbnailView::VideoThumbnailCycler::startCycle() { m_thumbnails->setVideoFile(m_fileName); m_timer->start(500); m_thumbnails->requestNext(); // We want it to cycle right away. } void ThumbnailView::VideoThumbnailCycler::stopCycle() { resetPreviousThumbail(); m_fileName = DB::FileName(); m_timer->stop(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/ThumbnailView/VideoThumbnailCycler.h b/ThumbnailView/VideoThumbnailCycler.h index 474739a4..2688a43b 100644 --- a/ThumbnailView/VideoThumbnailCycler.h +++ b/ThumbnailView/VideoThumbnailCycler.h @@ -1,73 +1,74 @@ /* Copyright 2012 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/>. */ #ifndef VIDEOTHUMBNAILCYCLER_H #define VIDEOTHUMBNAILCYCLER_H #include <DB/FileName.h> + #include <QObject> class QTimer; class QImage; namespace DB { class FileName; } namespace ImageManager { class VideoThumbnails; } namespace ThumbnailView { class ThumbnailModel; /** \brief Class which is responsible for cycling the video thumbnails in the thumbnail viewer \see \ref videothumbnails */ class VideoThumbnailCycler : public QObject { Q_OBJECT public: explicit VideoThumbnailCycler(ThumbnailModel *model, QObject *parent = nullptr); static VideoThumbnailCycler *instance(); void setActive(const DB::FileName &id); void stopCycle(); private slots: void gotFrame(const QImage &image); private: void resetPreviousThumbail(); bool isVideo(const DB::FileName &fileName) const; void startCycle(); static VideoThumbnailCycler *s_instance; DB::FileName m_fileName; QTimer *m_timer; ImageManager::VideoThumbnails *m_thumbnails; ThumbnailModel *m_model; }; } #endif // VIDEOTHUMBNAILCYCLER_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/DeleteFiles.cpp b/Utilities/DeleteFiles.cpp index 2636f53f..45523ab6 100644 --- a/Utilities/DeleteFiles.cpp +++ b/Utilities/DeleteFiles.cpp @@ -1,98 +1,97 @@ /* Copyright 2012 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 "DeleteFiles.h" #include "ShowBusyCursor.h" #include <DB/ImageDB.h> #include <ImageManager/ThumbnailCache.h> #include <MainWindow/DirtyIndicator.h> #include <MainWindow/Window.h> #include <KIO/CopyJob> #include <KIO/DeleteJob> #include <KJob> #include <KLocalizedString> #include <KMessageBox> - #include <QUrl> namespace Utilities { DeleteFiles *DeleteFiles::s_instance; bool DeleteFiles::deleteFiles(const DB::FileNameList &files, DeleteMethod method) { if (!s_instance) s_instance = new DeleteFiles; return s_instance->deleteFilesPrivate(files, method); } void DeleteFiles::slotKIOJobCompleted(KJob *job) { if (job->error()) KMessageBox::error(MainWindow::Window::theMainWindow(), job->errorString(), i18n("Error Deleting Files")); } bool DeleteFiles::deleteFilesPrivate(const DB::FileNameList &files, DeleteMethod method) { Utilities::ShowBusyCursor dummy; DB::FileNameList filenamesToRemove; QList<QUrl> filesToDelete; Q_FOREACH (const DB::FileName &fileName, files) { if (DB::ImageInfo::imageOnDisk(fileName)) { if (method == DeleteFromDisk || method == MoveToTrash) { filesToDelete.append(QUrl::fromLocalFile(fileName.absolute())); filenamesToRemove.append(fileName); } else { filenamesToRemove.append(fileName); } } else filenamesToRemove.append(fileName); } ImageManager::ThumbnailCache::instance()->removeThumbnails(files); if (method == DeleteFromDisk || method == MoveToTrash) { KJob *job; if (method == MoveToTrash) job = KIO::trash(filesToDelete); else job = KIO::del(filesToDelete); connect(job, SIGNAL(result(KJob *)), this, SLOT(slotKIOJobCompleted(KJob *))); } if (!filenamesToRemove.isEmpty()) { if (method == MoveToTrash || method == DeleteFromDisk) DB::ImageDB::instance()->deleteList(filenamesToRemove); else DB::ImageDB::instance()->addToBlockList(filenamesToRemove); MainWindow::DirtyIndicator::markDirty(); return true; } else return false; } } // namespace Utilities // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/DeleteFiles.h b/Utilities/DeleteFiles.h index 8e07074d..5b3dc044 100644 --- a/Utilities/DeleteFiles.h +++ b/Utilities/DeleteFiles.h @@ -1,53 +1,53 @@ /* Copyright 2012 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/>. */ #ifndef UTILITIES_DELETEFILES_H #define UTILITIES_DELETEFILES_H -#include <QObject> +#include <DB/FileNameList.h> -#include "DB/FileNameList.h" +#include <QObject> class KJob; namespace Utilities { enum DeleteMethod { DeleteFromDisk, MoveToTrash, BlockFromDatabase }; class DeleteFiles : public QObject { Q_OBJECT public: static bool deleteFiles(const DB::FileNameList &files, DeleteMethod method); private slots: void slotKIOJobCompleted(KJob *); private: static DeleteFiles *s_instance; bool deleteFilesPrivate(const DB::FileNameList &files, DeleteMethod method); }; } // namespace Utilities #endif // UTILITIES_DELETEFILES_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/DemoUtil.cpp b/Utilities/DemoUtil.cpp index f484abc4..dcb1b3e1 100644 --- a/Utilities/DemoUtil.cpp +++ b/Utilities/DemoUtil.cpp @@ -1,127 +1,126 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "DemoUtil.h" + #include "FileUtil.h" #include "Logging.h" #include <MainWindow/Window.h> +#include <KIO/DeleteJob> #include <KJob> #include <KJobWidgets> #include <KLocalizedString> #include <KMessageBox> - -#include <KIO/DeleteJob> - #include <QDir> #include <QDirIterator> #include <QFileInfo> #include <QStandardPaths> #include <QUrl> namespace { void copyList(const QStringList &from, const QString &directoryTo) { for (QStringList::ConstIterator it = from.constBegin(); it != from.constEnd(); ++it) { const QString destFile = directoryTo + QString::fromLatin1("/") + QFileInfo(*it).fileName(); if (!QFileInfo(destFile).exists()) { const bool ok = Utilities::copyOrOverwrite(*it, destFile); if (!ok) { KMessageBox::error(nullptr, i18n("Unable to copy '%1' to '%2'.", *it, destFile), i18n("Error Running Demo")); exit(-1); } } } } } // vi:expandtab:tabstop=4 shiftwidth=4: QString Utilities::setupDemo() { const QString demoDir = QString::fromLatin1("%1/kphotoalbum-demo-%2").arg(QDir::tempPath()).arg(QString::fromLocal8Bit(qgetenv("LOGNAME"))); QFileInfo fi(demoDir); if (!fi.exists()) { bool ok = QDir().mkdir(demoDir); if (!ok) { KMessageBox::error(nullptr, i18n("Unable to create directory '%1' needed for demo.", demoDir), i18n("Error Running Demo")); exit(-1); } } // index.xml const QString demoDB = QStandardPaths::locate(QStandardPaths::DataLocation, QString::fromLatin1("demo/index.xml")); if (demoDB.isEmpty()) { qCDebug(UtilitiesLog) << "No demo database in standard locations:" << QStandardPaths::standardLocations(QStandardPaths::DataLocation); exit(-1); } const QString configFile = demoDir + QString::fromLatin1("/index.xml"); copyOrOverwrite(demoDB, configFile); // Images const QStringList kpaDemoDirs = QStandardPaths::locateAll( QStandardPaths::DataLocation, QString::fromLatin1("demo"), QStandardPaths::LocateDirectory); QStringList images; Q_FOREACH (const QString &dir, kpaDemoDirs) { QDirIterator it(dir, QStringList() << QStringLiteral("*.jpg") << QStringLiteral("*.avi")); while (it.hasNext()) { images.append(it.next()); } } copyList(images, demoDir); // CategoryImages QString catDir = demoDir + QString::fromLatin1("/CategoryImages"); fi = QFileInfo(catDir); if (!fi.exists()) { bool ok = QDir().mkdir(catDir); if (!ok) { KMessageBox::error(nullptr, i18n("Unable to create directory '%1' needed for demo.", catDir), i18n("Error Running Demo")); exit(-1); } } const QStringList kpaDemoCatDirs = QStandardPaths::locateAll( QStandardPaths::DataLocation, QString::fromLatin1("demo/CategoryImages"), QStandardPaths::LocateDirectory); QStringList catImages; Q_FOREACH (const QString &dir, kpaDemoCatDirs) { QDirIterator it(dir, QStringList() << QStringLiteral("*.jpg")); while (it.hasNext()) { catImages.append(it.next()); } } copyList(catImages, catDir); return configFile; } void Utilities::deleteDemo() { QString dir = QString::fromLatin1("%1/kphotoalbum-demo-%2").arg(QDir::tempPath()).arg(QString::fromLocal8Bit(qgetenv("LOGNAME"))); QUrl demoUrl = QUrl::fromLocalFile(dir); KJob *delDemoJob = KIO::del(demoUrl); KJobWidgets::setWindow(delDemoJob, MainWindow::Window::theMainWindow()); delDemoJob->exec(); } diff --git a/Utilities/DescriptionUtil.cpp b/Utilities/DescriptionUtil.cpp index 01ade48b..7a39be80 100644 --- a/Utilities/DescriptionUtil.cpp +++ b/Utilities/DescriptionUtil.cpp @@ -1,281 +1,281 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "DescriptionUtil.h" + #include "Logging.h" #include <DB/CategoryCollection.h> #include <DB/ImageDB.h> #include <Exif/Info.h> #include <Settings/SettingsData.h> #include <KLocalizedString> - #include <QDate> #include <QList> #include <QTextCodec> #include <QUrl> /** * Add a line label + info text to the result text if info is not empty. * If the result already contains something, a HTML newline is added first. * To be used in createInfoText(). */ static void AddNonEmptyInfo(const QString &label, const QString &infoText, QString *result) { if (infoText.isEmpty()) return; if (!result->isEmpty()) *result += QString::fromLatin1("<br/>"); result->append(label).append(infoText); } /** * Given an ImageInfoPtr this function will create an HTML blob about the * image. The blob is used in the viewer and in the tool tip box from the * thumbnail view. * * As the HTML text is created, the parameter linkMap is filled with * information about hyperlinks. The map maps from an index to a pair of * (categoryName, categoryItem). This linkMap is used when the user selects * one of the hyberlinks. */ QString Utilities::createInfoText(DB::ImageInfoPtr info, QMap<int, QPair<QString, QString>> *linkMap) { Q_ASSERT(info); QString result; if (Settings::SettingsData::instance()->showFilename()) { AddNonEmptyInfo(i18n("<b>File Name: </b> "), info->fileName().relative(), &result); } if (Settings::SettingsData::instance()->showDate()) { AddNonEmptyInfo(i18n("<b>Date: </b> "), info->date().toString(Settings::SettingsData::instance()->showTime() ? true : false), &result); } /* XXX */ if (Settings::SettingsData::instance()->showImageSize() && info->mediaType() == DB::Image) { const QSize imageSize = info->size(); // Do not add -1 x -1 text if (imageSize.width() >= 0 && imageSize.height() >= 0) { const double megapix = imageSize.width() * imageSize.height() / 1000000.0; QString infoText = i18nc("width x height", "%1x%2", QString::number(imageSize.width()), QString::number(imageSize.height())); if (megapix > 0.05) { infoText += i18nc("short for: x megapixels", " (%1MP)", QString::number(megapix, 'f', 1)); } const double aspect = (double)imageSize.width() / (double)imageSize.height(); // 0.995 - 1.005 can still be considered quadratic if (aspect > 1.005) infoText += i18nc("aspect ratio", " (%1:1)", QLocale::system().toString(aspect, 'f', 2)); else if (aspect >= 0.995) infoText += i18nc("aspect ratio", " (1:1)"); else infoText += i18nc("aspect ratio", " (1:%1)", QLocale::system().toString(1.0 / aspect, 'f', 2)); AddNonEmptyInfo(i18n("<b>Image Size: </b> "), infoText, &result); } } if (Settings::SettingsData::instance()->showRating()) { if (info->rating() != -1) { if (!result.isEmpty()) result += QString::fromLatin1("<br/>"); QUrl rating; rating.setScheme(QString::fromLatin1("kratingwidget")); // we don't use the host part, but if we don't set it, we can't use port: rating.setHost(QString::fromLatin1("int")); rating.setPort(qMin(qMax(static_cast<short int>(0), info->rating()), static_cast<short int>(10))); result += QString::fromLatin1("<img src=\"%1\"/>").arg(rating.toString(QUrl::None)); } } QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); int link = 0; Q_FOREACH (const DB::CategoryPtr category, categories) { const QString categoryName = category->name(); if (category->doShow()) { StringSet items = info->itemsOfCategory(categoryName); if (Settings::SettingsData::instance()->hasUntaggedCategoryFeatureConfigured() && !Settings::SettingsData::instance()->untaggedImagesTagVisible()) { if (categoryName == Settings::SettingsData::instance()->untaggedCategory()) { if (items.contains(Settings::SettingsData::instance()->untaggedTag())) { items.remove(Settings::SettingsData::instance()->untaggedTag()); } } } if (!items.empty()) { QString title = QString::fromUtf8("<b>%1: </b> ").arg(category->name()); QString infoText; bool first = true; Q_FOREACH (const QString &item, items) { if (first) first = false; else infoText += QString::fromLatin1(", "); if (linkMap) { ++link; (*linkMap)[link] = QPair<QString, QString>(categoryName, item); infoText += QString::fromLatin1("<a href=\"%1\">%2</a>").arg(link).arg(item); infoText += formatAge(category, item, info); } else infoText += item; } AddNonEmptyInfo(title, infoText, &result); } } } if (Settings::SettingsData::instance()->showLabel()) { AddNonEmptyInfo(i18n("<b>Label: </b> "), info->label(), &result); } if (Settings::SettingsData::instance()->showDescription() && !info->description().trimmed().isEmpty()) { AddNonEmptyInfo(i18n("<b>Description: </b> "), info->description(), &result); } QString exifText; if (Settings::SettingsData::instance()->showEXIF()) { typedef QMap<QString, QStringList> ExifMap; typedef ExifMap::const_iterator ExifMapIterator; ExifMap exifMap = Exif::Info::instance()->infoForViewer(info->fileName(), Settings::SettingsData::instance()->iptcCharset()); for (ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt) { if (exifIt.key().startsWith(QString::fromLatin1("Exif."))) for (QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt) { QString exifName = exifIt.key().split(QChar::fromLatin1('.')).last(); AddNonEmptyInfo(QString::fromLatin1("<b>%1: </b> ").arg(exifName), *valuesIt, &exifText); } } QString iptcText; for (ExifMapIterator exifIt = exifMap.constBegin(); exifIt != exifMap.constEnd(); ++exifIt) { if (!exifIt.key().startsWith(QString::fromLatin1("Exif."))) for (QStringList::const_iterator valuesIt = exifIt.value().constBegin(); valuesIt != exifIt.value().constEnd(); ++valuesIt) { QString iptcName = exifIt.key().split(QChar::fromLatin1('.')).last(); AddNonEmptyInfo(QString::fromLatin1("<b>%1: </b> ").arg(iptcName), *valuesIt, &iptcText); } } if (!iptcText.isEmpty()) { if (exifText.isEmpty()) exifText = iptcText; else exifText += QString::fromLatin1("<hr>") + iptcText; } } if (!result.isEmpty() && !exifText.isEmpty()) result += QString::fromLatin1("<hr>"); result += exifText; return result; } using DateSpec = QPair<int, char>; DateSpec dateDiff(const QDate &birthDate, const QDate &imageDate) { const int bday = birthDate.day(); const int iday = imageDate.day(); const int bmonth = birthDate.month(); const int imonth = imageDate.month(); const int byear = birthDate.year(); const int iyear = imageDate.year(); // Image before birth const int diff = birthDate.daysTo(imageDate); if (diff < 0) return qMakePair(0, 'I'); if (diff < 31) return qMakePair(diff, 'D'); int months = (iyear - byear) * 12; months += (imonth - bmonth); months += (iday >= bday) ? 0 : -1; if (months < 24) return qMakePair(months, 'M'); else return qMakePair(months / 12, 'Y'); } QString formatDate(const DateSpec &date) { if (date.second == 'I') return {}; else if (date.second == 'D') return i18np("1 day", "%1 days", date.first); else if (date.second == 'M') return i18np("1 month", "%1 months", date.first); else return i18np("1 year", "%1 years", date.first); } void test() { Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1971, 7, 11))) == QString::fromLatin1("0 days")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1971, 8, 10))) == QString::fromLatin1("30 days")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1971, 8, 11))) == QString::fromLatin1("1 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1971, 8, 12))) == QString::fromLatin1("1 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1971, 9, 10))) == QString::fromLatin1("1 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1971, 9, 11))) == QString::fromLatin1("2 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1972, 6, 10))) == QString::fromLatin1("10 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1972, 6, 11))) == QString::fromLatin1("11 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1972, 6, 12))) == QString::fromLatin1("11 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1972, 7, 10))) == QString::fromLatin1("11 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1972, 7, 11))) == QString::fromLatin1("12 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1972, 7, 12))) == QString::fromLatin1("12 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1972, 12, 11))) == QString::fromLatin1("17 month")); Q_ASSERT(formatDate(dateDiff(QDate(1971, 7, 11), QDate(1973, 7, 11))) == QString::fromLatin1("2 years")); } QString Utilities::formatAge(DB::CategoryPtr category, const QString &item, DB::ImageInfoPtr info) { // test(); // I wish I could get my act together to set up a test suite. const QDate birthDate = category->birthDate(item); const QDate start = info->date().start().date(); const QDate end = info->date().end().date(); if (birthDate.isNull() || start.isNull()) return {}; if (start == end) return QString::fromUtf8(" (%1)").arg(formatDate(dateDiff(birthDate, start))); else { DateSpec lower = dateDiff(birthDate, start); DateSpec upper = dateDiff(birthDate, end); if (lower == upper) return QString::fromUtf8(" (%1)").arg(formatDate(lower)); else if (lower.second == 'I') return QString::fromUtf8(" (< %1)").arg(formatDate(upper)); else { if (lower.second == upper.second) return QString::fromUtf8(" (%1-%2)").arg(lower.first).arg(formatDate(upper)); else return QString::fromUtf8(" (%1-%2)").arg(formatDate(lower)).arg(formatDate(upper)); } } } diff --git a/Utilities/DescriptionUtil.h b/Utilities/DescriptionUtil.h index 471385d2..e602e35e 100644 --- a/Utilities/DescriptionUtil.h +++ b/Utilities/DescriptionUtil.h @@ -1,36 +1,36 @@ /* Copyright (C) 2003-2010 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. */ #ifndef TEXTUTIL_H #define TEXTUTIL_H -#include "DB/CategoryPtr.h" -#include "DB/ImageInfoPtr.h" +#include <DB/CategoryPtr.h> +#include <DB/ImageInfoPtr.h> #include <QMap> #include <QPair> #include <QString> namespace Utilities { QString createInfoText(DB::ImageInfoPtr info, QMap<int, QPair<QString, QString>> *); QString formatAge(DB::CategoryPtr category, const QString &item, DB::ImageInfoPtr info); } #endif /* TEXTUTIL_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/FastJpeg.cpp b/Utilities/FastJpeg.cpp index ee7262c9..e457763d 100644 --- a/Utilities/FastJpeg.cpp +++ b/Utilities/FastJpeg.cpp @@ -1,206 +1,205 @@ /* Copyright (C) 2003-2010 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 "FastJpeg.h" #include "JpeglibWithFix.h" #include "Logging.h" -#include "DB/FileName.h" +#include <DB/FileName.h> #include <QFileInfo> #include <QImageReader> #include <QVector> - #include <csetjmp> extern "C" { #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> } struct myjpeg_error_mgr : public jpeg_error_mgr { jmp_buf setjmp_buffer; }; extern "C" { static void myjpeg_error_exit(j_common_ptr cinfo) { auto *myerr = (myjpeg_error_mgr *)cinfo->err; char buffer[JMSG_LENGTH_MAX]; (*cinfo->err->format_message)(cinfo, buffer); //kWarning() << buffer; longjmp(myerr->setjmp_buffer, 1); } } namespace Utilities { static bool loadJPEGInternal(QImage *img, FILE *inputFile, QSize *fullSize, int dim, const char *membuf, size_t membuf_size); } bool Utilities::loadJPEG(QImage *img, const DB::FileName &imageFile, QSize *fullSize, int dim, char *membuf, size_t membufSize) { bool ok; struct stat statbuf; if (stat(QFile::encodeName(imageFile.absolute()).constData(), &statbuf) == -1) return false; if (!membuf || statbuf.st_size > (int)membufSize) { qCDebug(UtilitiesLog) << "loadJPEG (slow path) " << imageFile.relative() << " " << statbuf.st_size << " " << membufSize; FILE *inputFile = fopen(QFile::encodeName(imageFile.absolute()).constData(), "rb"); if (!inputFile) return false; ok = loadJPEGInternal(img, inputFile, fullSize, dim, NULL, 0); fclose(inputFile); } else { // May be more than is needed, but less likely to fragment memory this // way. int inputFD = open(QFile::encodeName(imageFile.absolute()).constData(), O_RDONLY); if (inputFD == -1) { return false; } unsigned bytesLeft = statbuf.st_size; unsigned offset = 0; while (bytesLeft > 0) { int bytes = read(inputFD, membuf + offset, bytesLeft); if (bytes <= 0) { (void)close(inputFD); return false; } offset += bytes; bytesLeft -= bytes; } ok = loadJPEGInternal(img, NULL, fullSize, dim, membuf, statbuf.st_size); (void)close(inputFD); } return ok; } bool Utilities::loadJPEG(QImage *img, const DB::FileName &imageFile, QSize *fullSize, int dim) { return loadJPEG(img, imageFile, fullSize, dim, NULL, 0); } bool Utilities::loadJPEG(QImage *img, const QByteArray &data, QSize *fullSize, int dim) { return loadJPEGInternal(img, nullptr, fullSize, dim, data.data(), data.size()); } bool Utilities::loadJPEGInternal(QImage *img, FILE *inputFile, QSize *fullSize, int dim, const char *membuf, size_t membuf_size) { struct jpeg_decompress_struct cinfo; struct myjpeg_error_mgr jerr; // JPEG error handling - thanks to Marcus Meissner cinfo.err = jpeg_std_error(&jerr); cinfo.err->error_exit = myjpeg_error_exit; if (setjmp(jerr.setjmp_buffer)) { jpeg_destroy_decompress(&cinfo); return false; } jpeg_create_decompress(&cinfo); if (inputFile) jpeg_stdio_src(&cinfo, inputFile); else jpeg_mem_src(&cinfo, (unsigned char *)membuf, membuf_size); jpeg_read_header(&cinfo, TRUE); *fullSize = QSize(cinfo.image_width, cinfo.image_height); int imgSize = qMax(cinfo.image_width, cinfo.image_height); //libjpeg supports a sort of scale-while-decoding which speeds up decoding int scale = 1; if (dim != -1) { while (dim * scale * 2 <= imgSize) { scale *= 2; } if (scale > 8) scale = 8; } cinfo.scale_num = 1; cinfo.scale_denom = scale; // Create QImage jpeg_start_decompress(&cinfo); switch (cinfo.output_components) { case 3: case 4: *img = QImage( cinfo.output_width, cinfo.output_height, QImage::Format_RGB32); if (img->isNull()) return false; break; case 1: // B&W image *img = QImage( cinfo.output_width, cinfo.output_height, QImage::Format_Indexed8); if (img->isNull()) return false; img->setColorCount(256); for (int i = 0; i < 256; i++) img->setColor(i, qRgb(i, i, i)); break; default: return false; } QVector<uchar *> linesVector; linesVector.reserve(img->height()); for (int i = 0; i < img->height(); ++i) linesVector.push_back(img->scanLine(i)); uchar **lines = linesVector.data(); while (cinfo.output_scanline < cinfo.output_height) jpeg_read_scanlines(&cinfo, lines + cinfo.output_scanline, cinfo.output_height); jpeg_finish_decompress(&cinfo); // Expand 24->32 bpp if (cinfo.output_components == 3) { for (uint j = 0; j < cinfo.output_height; j++) { uchar *in = img->scanLine(j) + cinfo.output_width * 3; QRgb *out = reinterpret_cast<QRgb *>(img->scanLine(j)); for (uint i = cinfo.output_width; i--;) { in -= 3; out[i] = qRgb(in[0], in[1], in[2]); } } } /*int newMax = qMax(cinfo.output_width, cinfo.output_height); int newx = size_*cinfo.output_width / newMax; int newy = size_*cinfo.output_height / newMax;*/ jpeg_destroy_decompress(&cinfo); //image = img.smoothScale(newx,newy); return true; } bool Utilities::isJPEG(const DB::FileName &fileName) { QString format = QString::fromLocal8Bit(QImageReader::imageFormat(fileName.absolute())); return format == QString::fromLocal8Bit("jpeg"); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/ImageUtil.h b/Utilities/ImageUtil.h index cb5bca44..8a8f9d15 100644 --- a/Utilities/ImageUtil.h +++ b/Utilities/ImageUtil.h @@ -1,47 +1,47 @@ /* Copyright (C) 2003-2010 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. */ #ifndef IMAGE_UTIL_H #define IMAGE_UTIL_H -#include "DB/FileName.h" +#include <DB/FileName.h> #include <QImage> namespace Utilities { /** * @brief scaleImage returns the scaled image, honoring the settings for smooth scaling. * @param image * @param size * @param mode aspect ratio mode * @return a scaled image */ QImage scaleImage(const QImage &image, const QSize &size, Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio); /** * @brief saveImage saves a QImage to a FileName, making sure that the directory exists. * @param fileName * @param image * @param format the storage format for QImage::save(), usually "JPEG" */ void saveImage(const DB::FileName &fileName, const QImage &image, const char *format); } #endif /* IMAGE_UTIL_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/List.cpp b/Utilities/List.cpp index 12c90073..837a3946 100644 --- a/Utilities/List.cpp +++ b/Utilities/List.cpp @@ -1,101 +1,103 @@ /* Copyright (C) 2006-2010 Tuomas Suutari <thsuut@utu.fi> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program (see the file COPYING); if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "List.h" -#include "DB/RawId.h" + #include <DB/FileName.h> +#include <DB/RawId.h> + #include <QList> #include <QStringList> #include <QTime> #include <algorithm> // std::swap #include <stdlib.h> // rand template <class T> QList<T> Utilities::mergeListsUniqly(const QList<T> &l1, const QList<T> &l2) { QList<T> r = l1; Q_FOREACH (const T &x, l2) if (!r.contains(x)) r.append(x); return r; } namespace { template <class T> class AutoDeletedArray { public: AutoDeletedArray(uint size) : m_ptr(new T[size]) { } operator T *() const { return m_ptr; } ~AutoDeletedArray() { delete[] m_ptr; } private: T *m_ptr; }; } template <class T> QList<T> Utilities::shuffleList(const QList<T> &list) { static bool init = false; if (!init) { QTime midnight(0, 0, 0); srand(midnight.secsTo(QTime::currentTime())); init = true; } // Take pointers from input list to an array for shuffling uint N = list.size(); AutoDeletedArray<const T *> deck(N); const T **p = deck; for (typename QList<T>::const_iterator i = list.begin(); i != list.end(); ++i) { *p = &(*i); ++p; } // Shuffle the array of pointers for (uint i = 0; i < N; i++) { uint r = i + static_cast<uint>(static_cast<double>(N - i) * rand() / static_cast<double>(RAND_MAX)); std::swap(deck[r], deck[i]); } // Create new list from the array QList<T> result; const T **const onePastLast = deck + N; for (p = deck; p != onePastLast; ++p) result.push_back(**p); return result; } #define INSTANTIATE_MERGELISTSUNIQLY(T) \ template QList<T> Utilities::mergeListsUniqly(const QList<T> &l1, const QList<T> &l2) #define INSTANTIATE_SHUFFLELIST(T) \ template QList<T> Utilities::shuffleList(const QList<T> &list) INSTANTIATE_MERGELISTSUNIQLY(DB::RawId); INSTANTIATE_MERGELISTSUNIQLY(QString); INSTANTIATE_SHUFFLELIST(DB::FileName); // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/Process.cpp b/Utilities/Process.cpp index dc17af42..a5125bd2 100644 --- a/Utilities/Process.cpp +++ b/Utilities/Process.cpp @@ -1,59 +1,59 @@ /* 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 <QTextStream> - #include "Process.h" +#include <QTextStream> + /** \class Utilities::Process \brief QProcess subclass which collects stdout and print stderr */ Utilities::Process::Process(QObject *parent) : QProcess(parent) { connect(this, SIGNAL(readyReadStandardError()), this, SLOT(readStandardError())); connect(this, SIGNAL(readyReadStandardOutput()), this, SLOT(readStandardOutput())); } QString Utilities::Process::stdOut() const { return m_stdout; } QString Utilities::Process::stdErr() const { return m_stderr; } void Utilities::Process::readStandardError() { setReadChannel(QProcess::StandardError); QTextStream stream(this); m_stderr.append(stream.readAll()); } void Utilities::Process::readStandardOutput() { setReadChannel(QProcess::StandardOutput); QTextStream stream(this); m_stdout.append(stream.readAll()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/ShowBusyCursor.cpp b/Utilities/ShowBusyCursor.cpp index ec364a81..e0cdbe4b 100644 --- a/Utilities/ShowBusyCursor.cpp +++ b/Utilities/ShowBusyCursor.cpp @@ -1,41 +1,42 @@ /* Copyright (C) 2003-2010 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 "ShowBusyCursor.h" + #include <qapplication.h> #include <qcursor.h> Utilities::ShowBusyCursor::ShowBusyCursor(Qt::CursorShape shape) { qApp->setOverrideCursor(QCursor(shape)); m_active = true; } Utilities::ShowBusyCursor::~ShowBusyCursor() { stop(); } void Utilities::ShowBusyCursor::stop() { if (m_active) { qApp->restoreOverrideCursor(); m_active = false; } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/ToolTip.cpp b/Utilities/ToolTip.cpp index b507f1a3..754e39fc 100644 --- a/Utilities/ToolTip.cpp +++ b/Utilities/ToolTip.cpp @@ -1,99 +1,102 @@ /* Copyright 2012 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 "ToolTip.h" -#include "DB/ImageDB.h" -#include "ImageManager/AsyncLoader.h" -#include "ImageManager/ImageRequest.h" -#include "Settings/SettingsData.h" -#include "Utilities/DescriptionUtil.h" + +#include "DescriptionUtil.h" + +#include <DB/ImageDB.h> +#include <ImageManager/AsyncLoader.h> +#include <ImageManager/ImageRequest.h> +#include <Settings/SettingsData.h> + #include <QTemporaryFile> namespace Utilities { ToolTip::ToolTip(QWidget *parent, Qt::WindowFlags f) : QLabel(parent, f) , m_tmpFileForThumbnailView(nullptr) { setAlignment(Qt::AlignLeft | Qt::AlignTop); setLineWidth(1); setMargin(1); setWindowOpacity(0.8); setAutoFillBackground(true); QPalette p = palette(); p.setColor(QPalette::Background, QColor(0, 0, 0, 170)); // r,g,b,A p.setColor(QPalette::WindowText, Qt::white); setPalette(p); } void ToolTip::requestImage(const DB::FileName &fileName) { int size = Settings::SettingsData::instance()->previewSize(); DB::ImageInfoPtr info = DB::ImageDB::instance()->info(fileName); if (size != 0) { ImageManager::ImageRequest *request = new ImageManager::ImageRequest(fileName, QSize(size, size), info->angle(), this); request->setPriority(ImageManager::Viewer); ImageManager::AsyncLoader::instance()->load(request); } else renderToolTip(); } void ToolTip::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) { const DB::FileName fileName = request->databaseFileName(); delete m_tmpFileForThumbnailView; m_tmpFileForThumbnailView = new QTemporaryFile(this); m_tmpFileForThumbnailView->open(); image.save(m_tmpFileForThumbnailView, "PNG"); if (fileName == m_currentFileName) renderToolTip(); } void ToolTip::requestToolTip(const DB::FileName &fileName) { if (fileName.isNull() || fileName == m_currentFileName) return; m_currentFileName = fileName; requestImage(fileName); } void ToolTip::renderToolTip() { const int size = Settings::SettingsData::instance()->previewSize(); if (size != 0) { setText(QString::fromLatin1("<table cols=\"2\" cellpadding=\"10\"><tr><td><img src=\"%1\"></td><td>%2</td></tr>") .arg(m_tmpFileForThumbnailView->fileName()) .arg(Utilities::createInfoText(DB::ImageDB::instance()->info(m_currentFileName), nullptr))); } else setText(QString::fromLatin1("<p>%1</p>").arg(Utilities::createInfoText(DB::ImageDB::instance()->info(m_currentFileName), nullptr))); setWordWrap(true); resize(sizeHint()); // m_view->setFocus(); show(); placeWindow(); } } // namespace Utilities // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/ToolTip.h b/Utilities/ToolTip.h index bba03fb2..081ad653 100644 --- a/Utilities/ToolTip.h +++ b/Utilities/ToolTip.h @@ -1,52 +1,53 @@ /* Copyright 2012 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/>. */ #ifndef UTILITIES_TOOLTIP_H #define UTILITIES_TOOLTIP_H -#include "DB/FileName.h" -#include "ImageManager/ImageClientInterface.h" +#include <DB/FileName.h> +#include <ImageManager/ImageClientInterface.h> + #include <QLabel> class QTemporaryFile; namespace Utilities { class ToolTip : public QLabel, public ImageManager::ImageClientInterface { Q_OBJECT public: explicit ToolTip(QWidget *parent = nullptr, Qt::WindowFlags f = 0); void pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) override; void requestToolTip(const DB::FileName &fileName); protected: virtual void placeWindow() = 0; private: void renderToolTip(); void requestImage(const DB::FileName &fileName); DB::FileName m_currentFileName; QTemporaryFile *m_tmpFileForThumbnailView; }; } // namespace Utilities #endif // UTILITIES_TOOLTIP_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/UniqFilenameMapper.cpp b/Utilities/UniqFilenameMapper.cpp index e69fec6f..36b86600 100644 --- a/Utilities/UniqFilenameMapper.cpp +++ b/Utilities/UniqFilenameMapper.cpp @@ -1,72 +1,72 @@ /* Copyright (C) 2003-2010 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 "Utilities/UniqFilenameMapper.h" +#include "UniqFilenameMapper.h" #include <QFileInfo> Utilities::UniqFilenameMapper::UniqFilenameMapper() { /* nop */ } Utilities::UniqFilenameMapper::UniqFilenameMapper(const QString &target) : m_targetDirectory(target) { /* nop */ } void Utilities::UniqFilenameMapper::reset() { m_uniqFiles.clear(); m_origToUniq.clear(); } bool Utilities::UniqFilenameMapper::fileClashes(const QString &file) { return m_uniqFiles.contains(file) || (!m_targetDirectory.isNull() && QFileInfo(file).exists()); } QString Utilities::UniqFilenameMapper::uniqNameFor(const DB::FileName &filename) { if (m_origToUniq.contains(filename)) return m_origToUniq[filename]; const QString extension = QFileInfo(filename.absolute()).completeSuffix(); QString base = QFileInfo(filename.absolute()).baseName(); if (!m_targetDirectory.isNull()) { base = QString::fromUtf8("%1/%2") .arg(m_targetDirectory) .arg(base); } QString uniqFile; int i = 0; do { uniqFile = (i == 0) ? QString::fromUtf8("%1.%2").arg(base).arg(extension) : QString::fromUtf8("%1-%2.%3").arg(base).arg(i).arg(extension); ++i; } while (fileClashes(uniqFile)); m_origToUniq.insert(filename, uniqFile); m_uniqFiles.insert(uniqFile); return uniqFile; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/UniqFilenameMapper.h b/Utilities/UniqFilenameMapper.h index 30c28bf9..a5e397c9 100644 --- a/Utilities/UniqFilenameMapper.h +++ b/Utilities/UniqFilenameMapper.h @@ -1,74 +1,75 @@ /* Copyright (C) 2008-2010 Henner Zeller <h.zeller@acm.org> based on Utilities::createUniqNameMap() by <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. */ #ifndef UTILITIES_UNIQ_FILENAME_MAPPER_H #define UTILITIES_UNIQ_FILENAME_MAPPER_H #include <DB/FileName.h> + #include <QMap> #include <QSet> #include <QString> namespace Utilities { /** * The UniqFilenameMapper creates flat filenames from arbitrary input filenames * so that there are no conflicts if they're written in one directory together. * The resulting names do not contain any path unless a targetDirectory is * given in the constructor. * * Example: * uniqNameFor("cd1/def.jpg") -> def.jpg * uniqNameFor("cd1/abc/file.jpg") -> file.jpg * uniqNameFor("cd3/file.jpg") -> file-1.jpg * uniqNameFor("cd1/abc/file.jpg") -> file.jpg // file from above. */ class UniqFilenameMapper { public: UniqFilenameMapper(); // Create a UniqFilenameMapper that returns filenames with the // targetDirectory prepended. // The UniqFilenameMapper makes sure, that generated filenames do not // previously exist in the targetDirectory. explicit UniqFilenameMapper(const QString &targetDirectory); // Create a unique, flat filename for the target directory. If this method // has been called before with the same argument, the unique name that has // been created before is returned (see example above). QString uniqNameFor(const DB::FileName &filename); // Reset all mappings. void reset(); private: UniqFilenameMapper(const UniqFilenameMapper &); // don't copy. bool fileClashes(const QString &file); const QString m_targetDirectory; typedef QMap<DB::FileName, QString> FileNameMap; FileNameMap m_origToUniq; QSet<QString> m_uniqFiles; }; } #endif /* UTILITIES_UNIQ_FILENAME_MAPPER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Utilities/VideoUtil.h b/Utilities/VideoUtil.h index b7e01e23..0a807a3a 100644 --- a/Utilities/VideoUtil.h +++ b/Utilities/VideoUtil.h @@ -1,34 +1,34 @@ /* Copyright (C) 2003-2010 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. */ #ifndef VIDEO_UTIL_H #define VIDEO_UTIL_H -#include "DB/FileName.h" +#include <DB/FileName.h> #include <QSet> #include <QString> namespace Utilities { const QSet<QString> &supportedVideoExtensions(); bool isVideo(const DB::FileName &fileName); } #endif /* VIDEO_UTIL_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/AbstractDisplay.cpp b/Viewer/AbstractDisplay.cpp index 0d173172..b81e265d 100644 --- a/Viewer/AbstractDisplay.cpp +++ b/Viewer/AbstractDisplay.cpp @@ -1,29 +1,30 @@ /* 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 <DB/ImageInfo.h> #include <Settings/SettingsData.h> Viewer::AbstractDisplay::AbstractDisplay(QWidget *parent) : QWidget(parent) , m_info(nullptr) { } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/AbstractDisplay.h b/Viewer/AbstractDisplay.h index 31c80a94..e9a1ac5c 100644 --- a/Viewer/AbstractDisplay.h +++ b/Viewer/AbstractDisplay.h @@ -1,49 +1,50 @@ /* Copyright (C) 2003-2010 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. */ #ifndef ABSTRACTDISPLAY_H #define ABSTRACTDISPLAY_H #include <DB/ImageInfoPtr.h> + #include <qwidget.h> namespace Viewer { class AbstractDisplay : public QWidget { Q_OBJECT public: explicit AbstractDisplay(QWidget *parent); virtual bool setImage(DB::ImageInfoPtr info, bool forward) = 0; public slots: virtual void zoomIn() = 0; virtual void zoomOut() = 0; virtual void zoomFull() = 0; virtual void zoomPixelForPixel() = 0; protected: DB::ImageInfoPtr m_info; }; } #endif /* ABSTRACTDISPLAY_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/CategoryImageConfig.cpp b/Viewer/CategoryImageConfig.cpp index eb27bf40..d1f8769e 100644 --- a/Viewer/CategoryImageConfig.cpp +++ b/Viewer/CategoryImageConfig.cpp @@ -1,191 +1,190 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 <DB/CategoryCollection.h> +#include <DB/ImageDB.h> +#include <DB/ImageInfo.h> +#include <DB/MemberMap.h> +#include <Settings/SettingsData.h> +#include <Utilities/FileUtil.h> + +#include <KComboBox> +#include <KLocalizedString> #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/FileUtil.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(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/CategoryImageConfig.h b/Viewer/CategoryImageConfig.h index 0f7914cd..b74c3c42 100644 --- a/Viewer/CategoryImageConfig.h +++ b/Viewer/CategoryImageConfig.h @@ -1,70 +1,70 @@ /* Copyright (C) 2003-2010 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. */ #ifndef CATEGORYIMAGECONFIG_H #define CATEGORYIMAGECONFIG_H +#include <DB/ImageInfoPtr.h> + #include <QDialog> #include <QImage> #include <QLabel> -#include <DB/ImageInfoPtr.h> - class QComboBox; class QLabel; namespace DB { class ImageInfo; } namespace Viewer { class CategoryImageConfig : public QDialog { Q_OBJECT public: static CategoryImageConfig *instance(); void setCurrentImage(const QImage &image, const DB::ImageInfoPtr &info); void show(); protected slots: void groupChanged(); void memberChanged(); void slotSet(); protected: QString currentGroup(); private: static CategoryImageConfig *s_instance; CategoryImageConfig(); QComboBox *m_group; QStringList m_categoryNames; QComboBox *m_member; QLabel *m_current; QImage m_image; QLabel *m_imageLabel; DB::ImageInfoPtr m_info; }; } #endif /* CATEGORYIMAGECONFIG_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ImageDisplay.cpp b/Viewer/ImageDisplay.cpp index a30b7c30..8f4f9cec 100644 --- a/Viewer/ImageDisplay.cpp +++ b/Viewer/ImageDisplay.cpp @@ -1,758 +1,757 @@ /* 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 "Logging.h" +#include "ViewHandler.h" + +#include <DB/ImageDB.h> +#include <ImageManager/AsyncLoader.h> +#include <Settings/SettingsData.h> +#include <KLocalizedString> +#include <KMessageBox> #include <QApplication> #include <QCursor> #include <QMouseEvent> #include <QPaintEvent> #include <QPainter> #include <QResizeEvent> #include <QTimer> - -#include <KLocalizedString> -#include <KMessageBox> - -#include "DB/ImageDB.h" -#include "ImageManager/AsyncLoader.h" -#include "Settings/SettingsData.h" -#include "Viewer/ViewHandler.h" - #include <cmath> /** 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 above might very likely be simplified. Back in the old days it needed to be that complex to allow drawing on images. To propagate the cache, we need to know which direction the images are viewed in, which is the job of the instance variable _forward. */ Viewer::ImageDisplay::ImageDisplay(QWidget *parent) : AbstractDisplay(parent) , m_reloadImageInProgress(false) , m_forward(true) , m_curIndex(0) , m_busy(false) , m_cursorHiding(true) { m_viewHandler = new ViewHandler(this); setMouseTracking(true); m_cursorTimer = new QTimer(this); m_cursorTimer->setSingleShot(true); connect(m_cursorTimer, &QTimer::timeout, this, &ImageDisplay::hideCursor); showCursor(); } /** * If mouse cursor hiding is enabled, hide the cursor right now */ void Viewer::ImageDisplay::hideCursor() { if (m_cursorHiding) setCursor(Qt::BlankCursor); } /** * If mouse cursor hiding is enabled, show normal cursor and start a timer that will hide it later */ void Viewer::ImageDisplay::showCursor() { if (m_cursorHiding) { unsetCursor(); m_cursorTimer->start(1500); } } /** * Prevent hideCursor() and showCursor() from altering cursor state */ void Viewer::ImageDisplay::disableCursorHiding() { m_cursorHiding = false; } /** * Enable automatic mouse cursor hiding */ void Viewer::ImageDisplay::enableCursorHiding() { m_cursorHiding = true; } void Viewer::ImageDisplay::mousePressEvent(QMouseEvent *event) { // disable cursor hiding till button release disableCursorHiding(); QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers()); double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size()); bool block = m_viewHandler->mousePressEvent(&e, event->pos(), ratio); if (!block) QWidget::mousePressEvent(event); update(); } void Viewer::ImageDisplay::mouseMoveEvent(QMouseEvent *event) { // just reset the timer showCursor(); QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers()); double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size()); bool block = m_viewHandler->mouseMoveEvent(&e, event->pos(), ratio); if (!block) QWidget::mouseMoveEvent(event); update(); } void Viewer::ImageDisplay::mouseReleaseEvent(QMouseEvent *event) { // enable cursor hiding and reset timer enableCursorHiding(); showCursor(); m_cache.remove(m_curIndex); QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers()); double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size()); bool block = m_viewHandler->mouseReleaseEvent(&e, event->pos(), ratio); if (!block) { QWidget::mouseReleaseEvent(event); } emit possibleChange(); update(); } bool Viewer::ImageDisplay::setImage(DB::ImageInfoPtr info, bool forward) { qCDebug(ViewerLog) << "setImage(" << info->fileName().relative() << "," << forward << ")"; m_info = info; m_loadedImage = QImage(); // Find the index of the current image m_curIndex = 0; Q_FOREACH (const DB::FileName &filename, m_imageList) { if (filename == info->fileName()) break; ++m_curIndex; } if (m_cache.contains(m_curIndex) && m_cache[m_curIndex].angle == info->angle()) { const ViewPreloadInfo &found = m_cache[m_curIndex]; m_loadedImage = found.img; updateZoomPoints(Settings::SettingsData::instance()->viewerStandardSize(), found.img.size()); cropAndScale(); info->setSize(found.size); emit imageReady(); } else { requestImage(info, true); busy(); } m_forward = forward; updatePreload(); return true; } void Viewer::ImageDisplay::resizeEvent(QResizeEvent *event) { ImageManager::AsyncLoader::instance()->stop(this, ImageManager::StopOnlyNonPriorityLoads); m_cache.clear(); if (m_info) { cropAndScale(); if (event->size().width() > 1.5 * this->m_loadedImage.size().width() || event->size().height() > 1.5 * this->m_loadedImage.size().height()) potentiallyLoadFullSize(); // Only do if we scale much bigger. } updatePreload(); } void Viewer::ImageDisplay::paintEvent(QPaintEvent *) { int x = (width() - m_croppedAndScaledImg.width()) / 2; int y = (height() - m_croppedAndScaledImg.height()) / 2; QPainter painter(this); painter.fillRect(0, 0, width(), height(), Qt::black); painter.drawImage(x, y, m_croppedAndScaledImg); } QPoint Viewer::ImageDisplay::offset(int logicalWidth, int logicalHeight, int physicalWidth, int physicalHeight, double *ratio) { double rat = sizeRatio(QSize(logicalWidth, logicalHeight), QSize(physicalWidth, physicalHeight)); int ox = (int)(physicalWidth - logicalWidth * rat) / 2; int oy = (int)(physicalHeight - logicalHeight * rat) / 2; if (ratio) *ratio = rat; return QPoint(ox, oy); } void Viewer::ImageDisplay::zoom(QPoint p1, QPoint p2) { qCDebug(ViewerLog, "zoom(%d,%d, %d,%d)", p1.x(), p1.y(), p2.x(), p2.y()); m_cache.remove(m_curIndex); normalize(p1, p2); double ratio; QPoint off = offset((p2 - p1).x(), (p2 - p1).y(), width(), height(), &ratio); off = off / ratio; p1.setX(p1.x() - off.x()); p1.setY(p1.y() - off.y()); p2.setX(p2.x() + off.x()); p2.setY(p2.y() + off.y()); m_zStart = p1; m_zEnd = p2; potentiallyLoadFullSize(); cropAndScale(); } QPoint Viewer::ImageDisplay::mapPos(QPoint p) { QPoint off = offset(qAbs(m_zEnd.x() - m_zStart.x()), qAbs(m_zEnd.y() - m_zStart.y()), width(), height(), 0); p -= off; int x = (int)(m_zStart.x() + (m_zEnd.x() - m_zStart.x()) * ((double)p.x() / (width() - 2 * off.x()))); int y = (int)(m_zStart.y() + (m_zEnd.y() - m_zStart.y()) * ((double)p.y() / (height() - 2 * off.y()))); return QPoint(x, y); } void Viewer::ImageDisplay::xformPainter(QPainter *p) { QPoint off = offset(qAbs(m_zEnd.x() - m_zStart.x()), qAbs(m_zEnd.y() - m_zStart.y()), width(), height(), 0); double s = (width() - 2 * off.x()) / qAbs((double)m_zEnd.x() - m_zStart.x()); p->scale(s, s); p->translate(-m_zStart.x(), -m_zStart.y()); } void Viewer::ImageDisplay::zoomIn() { qCDebug(ViewerLog, "zoomIn()"); QPoint size = (m_zEnd - m_zStart); QPoint p1 = m_zStart + size * (0.2 / 2); QPoint p2 = m_zEnd - size * (0.2 / 2); zoom(p1, p2); } void Viewer::ImageDisplay::zoomOut() { qCDebug(ViewerLog, "zoomOut()"); QPoint size = (m_zEnd - m_zStart); //Bug 150971, Qt tries to render bigger and bigger images (10000x10000), hence running out of memory. if ((size.x() * size.y() > 25 * 1024 * 1024)) return; QPoint p1 = m_zStart - size * (0.25 / 2); QPoint p2 = m_zEnd + size * (0.25 / 2); zoom(p1, p2); } void Viewer::ImageDisplay::zoomFull() { qCDebug(ViewerLog, "zoomFull()"); m_zStart = QPoint(0, 0); m_zEnd = QPoint(m_loadedImage.width(), m_loadedImage.height()); zoom(QPoint(0, 0), QPoint(m_loadedImage.width(), m_loadedImage.height())); } void Viewer::ImageDisplay::normalize(QPoint &p1, QPoint &p2) { int minx = qMin(p1.x(), p2.x()); int miny = qMin(p1.y(), p2.y()); int maxx = qMax(p1.x(), p2.x()); int maxy = qMax(p1.y(), p2.y()); p1 = QPoint(minx, miny); p2 = QPoint(maxx, maxy); } void Viewer::ImageDisplay::pan(const QPoint &point) { m_zStart += point; m_zEnd += point; cropAndScale(); } void Viewer::ImageDisplay::cropAndScale() { if (m_loadedImage.isNull()) { return; } if (m_zStart != QPoint(0, 0) || m_zEnd != QPoint(m_loadedImage.width(), m_loadedImage.height())) { qCDebug(ViewerLog) << "cropAndScale(): using cropped image" << m_zStart << "-" << m_zEnd; m_croppedAndScaledImg = m_loadedImage.copy(m_zStart.x(), m_zStart.y(), m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()); } else { qCDebug(ViewerLog) << "cropAndScale(): using full image."; m_croppedAndScaledImg = m_loadedImage; } updateZoomCaption(); if (!m_croppedAndScaledImg.isNull()) // I don't know how this can happen, but it seems not to be dangerous. { qCDebug(ViewerLog) << "cropAndScale(): scaling image to" << width() << "x" << height(); m_croppedAndScaledImg = m_croppedAndScaledImg.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); } else { qCDebug(ViewerLog) << "cropAndScale(): image is null."; } update(); emit viewGeometryChanged(m_croppedAndScaledImg.size(), QRect(m_zStart, m_zEnd), sizeRatio(m_loadedImage.size(), m_info->size())); } void Viewer::ImageDisplay::filterNone() { cropAndScale(); update(); } bool Viewer::ImageDisplay::filterMono() { m_croppedAndScaledImg = m_croppedAndScaledImg.convertToFormat(m_croppedAndScaledImg.Format_Mono); update(); return true; } // I can't believe there isn't a standard conversion for this??? -- WH bool Viewer::ImageDisplay::filterBW() { if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error(this, i18n("Insufficient color depth for this filter")); return false; } for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int gray = qGray(pixel); int alpha = qAlpha(pixel); m_croppedAndScaledImg.setPixel(x, y, qRgba(gray, gray, gray, alpha)); } } update(); return true; } bool Viewer::ImageDisplay::filterContrastStretch() { int redMin, redMax, greenMin, greenMax, blueMin, blueMax; redMin = greenMin = blueMin = 255; redMax = greenMax = blueMax = 0; if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error(this, i18n("Insufficient color depth for this filter")); return false; } // Look for minimum and maximum intensities within each color channel for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int red = qRed(pixel); int green = qGreen(pixel); int blue = qBlue(pixel); redMin = redMin < red ? redMin : red; redMax = redMax > red ? redMax : red; greenMin = greenMin < green ? greenMin : green; greenMax = greenMax > green ? greenMax : green; blueMin = blueMin < blue ? blueMin : blue; blueMax = blueMax > blue ? blueMax : blue; } } // Calculate factor for stretching each color intensity throughout the // whole range float redFactor, greenFactor, blueFactor; redFactor = ((float)(255) / (float)(redMax - redMin)); greenFactor = ((float)(255) / (float)(greenMax - greenMin)); blueFactor = ((float)(255) / (float)(blueMax - blueMin)); // Perform the contrast stretching for (int y = 0; y < m_croppedAndScaledImg.height(); ++y) { for (int x = 0; x < m_croppedAndScaledImg.width(); ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); int red = qRed(pixel); int green = qGreen(pixel); int blue = qBlue(pixel); int alpha = qAlpha(pixel); red = (red - redMin) * redFactor; red = red < 255 ? red : 255; red = red > 0 ? red : 0; green = (green - greenMin) * greenFactor; green = green < 255 ? green : 255; green = green > 0 ? green : 0; blue = (blue - blueMin) * blueFactor; blue = blue < 255 ? blue : 255; blue = blue > 0 ? blue : 0; m_croppedAndScaledImg.setPixel(x, y, qRgba(red, green, blue, alpha)); } } update(); return true; } bool Viewer::ImageDisplay::filterHistogramEqualization() { int width, height; float R_histogram[256]; float G_histogram[256]; float B_histogram[256]; float d; if (m_croppedAndScaledImg.depth() < 32) { KMessageBox::error(this, i18n("Insufficient color depth for this filter")); return false; } memset(R_histogram, 0, sizeof(R_histogram)); memset(G_histogram, 0, sizeof(G_histogram)); memset(B_histogram, 0, sizeof(B_histogram)); width = m_croppedAndScaledImg.width(); height = m_croppedAndScaledImg.height(); d = 1.0 / width / height; // Populate histogram for each color channel for (int y = 0; y < height; ++y) { for (int x = 1; x < width; ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); R_histogram[qRed(pixel)] += d; G_histogram[qGreen(pixel)] += d; B_histogram[qBlue(pixel)] += d; } } // Transfer histogram table to cumulative distribution table float R_sum = 0.0; float G_sum = 0.0; float B_sum = 0.0; for (int i = 0; i < 256; ++i) { R_sum += R_histogram[i]; G_sum += G_histogram[i]; B_sum += B_histogram[i]; R_histogram[i] = R_sum * 255 + 0.5; G_histogram[i] = G_sum * 255 + 0.5; B_histogram[i] = B_sum * 255 + 0.5; } // Equalize the image for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int pixel = m_croppedAndScaledImg.pixel(x, y); m_croppedAndScaledImg.setPixel( x, y, qRgba(R_histogram[qRed(pixel)], G_histogram[qGreen(pixel)], B_histogram[qBlue(pixel)], qAlpha(pixel))); } } update(); return true; } void Viewer::ImageDisplay::updateZoomCaption() { const QSize imgSize = m_loadedImage.size(); // similar to sizeRatio(), but we take the _highest_ factor. double ratio = ((double)imgSize.width()) / (m_zEnd.x() - m_zStart.x()); if (ratio * (m_zEnd.y() - m_zStart.y()) < imgSize.height()) { ratio = ((double)imgSize.height()) / (m_zEnd.y() - m_zStart.y()); } emit setCaptionInfo((ratio > 1.05) ? ki18n("[ zoom x%1 ]").subs(ratio, 0, 'f', 1).toString() : QString()); } QImage Viewer::ImageDisplay::currentViewAsThumbnail() const { if (m_croppedAndScaledImg.isNull()) return QImage(); else return m_croppedAndScaledImg.scaled(512, 512, Qt::KeepAspectRatio, Qt::SmoothTransformation); } bool Viewer::ImageDisplay::isImageZoomed(const Settings::StandardViewSize type, const QSize &imgSize) { if (type == Settings::FullSize) return true; if (type == Settings::NaturalSizeIfFits) return !(imgSize.width() < width() && imgSize.height() < height()); return false; } void Viewer::ImageDisplay::pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) { const DB::FileName fileName = request->databaseFileName(); const QSize imgSize = request->size(); const QSize fullSize = request->fullSize(); const int angle = request->angle(); const bool loadedOK = request->loadedOK(); if (loadedOK && fileName == m_info->fileName()) { if (fullSize.isValid() && !m_info->size().isValid()) m_info->setSize(fullSize); if (!m_reloadImageInProgress) updateZoomPoints(Settings::SettingsData::instance()->viewerStandardSize(), image.size()); else { // See documentation for zoomPixelForPixel for details. // We just loaded a likely much larger image, so the zoom points // need to be scaled. Notice m_loadedImage is the size of the // old image. // when using raw images, the decoded image may be a preview // and have a size different from m_info->size(). Therefore, use fullSize here: double ratio = sizeRatio(m_loadedImage.size(), fullSize); qCDebug(ViewerLog) << "Old size:" << m_loadedImage.size() << "; new size:" << m_info->size(); qCDebug(ViewerLog) << "Req size:" << imgSize << "fullsize:" << fullSize; qCDebug(ViewerLog) << "pixmapLoaded(): Zoom region was" << m_zStart << "-" << m_zEnd; m_zStart *= ratio; m_zEnd *= ratio; qCDebug(ViewerLog) << "pixmapLoaded(): Zoom region changed to" << m_zStart << "-" << m_zEnd; 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) { qCWarning(ViewerLog, "Info was null for index %d!", i); return; } if (m_cache.contains(i)) { nextOnesInCache++; if (nextOnesInCache >= ceil(cacheSize / 2.0) && cacheFull) { // Ok enough images in cache return; } } else { requestImage(info); if (cacheFull) { // The cache was full, we need to delete an item from the cache. // First try to find an item from the direction we came from for (int j = (m_forward ? 0 : m_imageList.count() - 1); j != m_curIndex; j += (m_forward ? 1 : -1)) { if (m_cache.contains(j)) { m_cache.remove(j); return; } } // OK We found no item in the direction we came from (think of home/end keys) for (int j = (m_forward ? m_imageList.count() - 1 : 0); j != m_curIndex; j += (m_forward ? -1 : 1)) { if (m_cache.contains(j)) { m_cache.remove(j); return; } } Q_ASSERT(false); // We should never get here. } return; } } } int Viewer::ImageDisplay::indexOf(const DB::FileName &fileName) { int i = 0; Q_FOREACH (const DB::FileName &name, m_imageList) { if (name == fileName) break; ++i; } return i; } void Viewer::ImageDisplay::busy() { if (!m_busy) qApp->setOverrideCursor(Qt::WaitCursor); m_busy = true; } void Viewer::ImageDisplay::unbusy() { if (m_busy) qApp->restoreOverrideCursor(); m_busy = false; } void Viewer::ImageDisplay::zoomPixelForPixel() { qCDebug(ViewerLog, "zoomPixelForPixel()"); // This is rather tricky. // We want to zoom to a pixel level for the real image, which we might // or might not have loaded yet. // // First we ask for zoom points as they would look like had we had the // real image loaded now. (We need to ask for them, for the real image, // otherwise we would just zoom to the pixel level of the view size // image) updateZoomPoints(Settings::NaturalSize, m_info->size()); // The points now, however might not match the current visible image - // as this image might be be only view size large. We therefore need // to scale the coordinates. double ratio = sizeRatio(m_loadedImage.size(), m_info->size()); qCDebug(ViewerLog) << "zoomPixelForPixel(): Zoom region was" << m_zStart << "-" << m_zEnd; m_zStart /= ratio; m_zEnd /= ratio; qCDebug(ViewerLog) << "zoomPixelForPixel(): Zoom region changed to" << m_zStart << "-" << m_zEnd; cropAndScale(); potentiallyLoadFullSize(); } 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); qCDebug(ViewerLog) << "updateZoomPoints(): Zoom region reset to" << m_zStart << "-" << m_zEnd; } else { m_zStart = QPoint(-(width() - iw) / 2, -(height() - ih) / 2); m_zEnd = QPoint(iw + (width() - iw) / 2, ih + (height() - ih) / 2); qCDebug(ViewerLog) << "updateZoomPoints(): Zoom region set to" << m_zStart << "-" << m_zEnd; } } void Viewer::ImageDisplay::potentiallyLoadFullSize() { if (m_info->size() != m_loadedImage.size()) { qCDebug(ViewerLog) << "Loading full size image for " << m_info->fileName().relative(); ImageManager::ImageRequest *request = new ImageManager::ImageRequest(m_info->fileName(), QSize(-1, -1), m_info->angle(), this); request->setPriority(ImageManager::Viewer); ImageManager::AsyncLoader::instance()->load(request); busy(); m_reloadImageInProgress = true; } } /** * return the ratio of the two sizes. That is newSize/baseSize. */ double Viewer::ImageDisplay::sizeRatio(const QSize &baseSize, const QSize &newSize) const { double res = ((double)newSize.width()) / baseSize.width(); if (res * baseSize.height() > newSize.height()) { res = ((double)newSize.height()) / baseSize.height(); } return res; } void Viewer::ImageDisplay::requestImage(const DB::ImageInfoPtr &info, bool priority) { Settings::StandardViewSize viewSize = Settings::SettingsData::instance()->viewerStandardSize(); QSize s = size(); if (viewSize == Settings::NaturalSize) s = QSize(-1, -1); ImageManager::ImageRequest *request = new ImageManager::ImageRequest(info->fileName(), s, info->angle(), this); request->setUpScale(viewSize == Settings::FullSize); request->setPriority(priority ? ImageManager::Viewer : ImageManager::ViewerPreload); ImageManager::AsyncLoader::instance()->load(request); } void Viewer::ImageDisplay::hideEvent(QHideEvent *) { m_viewHandler->hideEvent(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ImageDisplay.h b/Viewer/ImageDisplay.h index da4829ca..e57ad1b6 100644 --- a/Viewer/ImageDisplay.h +++ b/Viewer/ImageDisplay.h @@ -1,147 +1,149 @@ /* Copyright (C) 2003-2010 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. */ #ifndef IMAGEDISPLAY_H #define IMAGEDISPLAY_H #include "AbstractDisplay.h" -#include "DB/ImageInfoPtr.h" -#include "ImageManager/ImageClientInterface.h" -#include "Settings/SettingsData.h" + #include <DB/FileNameList.h> +#include <DB/ImageInfoPtr.h> +#include <ImageManager/ImageClientInterface.h> +#include <Settings/SettingsData.h> + #include <QMouseEvent> #include <QPaintEvent> #include <QResizeEvent> #include <qimage.h> #include <qpixmap.h> class QTimer; namespace DB { class ImageInfo; } namespace Viewer { class ViewHandler; class ViewerWidget; struct ViewPreloadInfo { ViewPreloadInfo() {} ViewPreloadInfo(const QImage &img, const QSize &size, int angle) : img(img) , size(size) , angle(angle) { } QImage img; QSize size; int angle; }; class ImageDisplay : public Viewer::AbstractDisplay, public ImageManager::ImageClientInterface { Q_OBJECT public: explicit ImageDisplay(QWidget *parent); bool setImage(DB::ImageInfoPtr info, bool forward) override; QImage currentViewAsThumbnail() const; void pixmapLoaded(ImageManager::ImageRequest *request, const QImage &image) override; void setImageList(const DB::FileNameList &list); void filterNone(); void filterSelected(); bool filterMono(); bool filterBW(); bool filterContrastStretch(); bool filterHistogramEqualization(); public slots: void zoomIn() override; void zoomOut() override; void zoomFull() override; void zoomPixelForPixel() override; protected slots: void hideCursor(); void showCursor(); void disableCursorHiding(); void enableCursorHiding(); signals: void possibleChange(); void imageReady(); void setCaptionInfo(const QString &info); void viewGeometryChanged(QSize viewSize, QRect zoomWindow, double sizeRatio); protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void resizeEvent(QResizeEvent *event) override; void paintEvent(QPaintEvent *event) override; void hideEvent(QHideEvent *) override; QPoint mapPos(QPoint); QPoint offset(int logicalWidth, int logicalHeight, int physicalWidth, int physicalHeight, double *ratio); void xformPainter(QPainter *); void cropAndScale(); void updatePreload(); int indexOf(const DB::FileName &fileName); void requestImage(const DB::ImageInfoPtr &info, bool priority = false); /** display zoom factor in title of display window */ void updateZoomCaption(); friend class ViewHandler; void zoom(QPoint p1, QPoint p2); void normalize(QPoint &p1, QPoint &p2); void pan(const QPoint &); void busy(); void unbusy(); bool isImageZoomed(const Settings::StandardViewSize type, const QSize &imgSize); void updateZoomPoints(const Settings::StandardViewSize type, const QSize &imgSize); void potentiallyLoadFullSize(); double sizeRatio(const QSize &baseSize, const QSize &newSize) const; private: QImage m_loadedImage; QImage m_croppedAndScaledImg; ViewHandler *m_viewHandler; // zoom points in the coordinate system of the image. QPoint m_zStart; QPoint m_zEnd; QMap<int, ViewPreloadInfo> m_cache; DB::FileNameList m_imageList; QMap<QString, DB::ImageInfoPtr> m_loadMap; bool m_reloadImageInProgress; int m_forward; int m_curIndex; bool m_busy; ViewerWidget *m_viewer; QTimer *m_cursorTimer; bool m_cursorHiding; }; } #endif /* IMAGEDISPLAY_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/InfoBox.h b/Viewer/InfoBox.h index 119361c5..b2f7be99 100644 --- a/Viewer/InfoBox.h +++ b/Viewer/InfoBox.h @@ -1,106 +1,107 @@ /* Copyright (C) 2003-2014 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. */ #ifndef INFOBOX_H #define INFOBOX_H #include "config-kpa-kgeomap.h" // Qt includes #include <QMouseEvent> #include <QPointer> // KDE includes #include <QTextBrowser> // Local includes #include "InfoBoxResizer.h" #include "ViewerWidget.h" + #include <Settings/SettingsData.h> // Qt classes class QMenu; class QToolButton; namespace Map { // Local classes class MapView; } namespace Viewer { // Local classes class VisibleOptionsMenu; class InfoBox : public QTextBrowser { Q_OBJECT public: explicit InfoBox(ViewerWidget *parent); void setSource(const QUrl &source) override; void setInfo(const QString &text, const QMap<int, QPair<QString, QString>> &linkMap); void setSize(); protected: QVariant loadResource(int type, const QUrl &name) override; void mouseMoveEvent(QMouseEvent *) override; void mousePressEvent(QMouseEvent *) override; void mouseReleaseEvent(QMouseEvent *) override; void resizeEvent(QResizeEvent *) override; void contextMenuEvent(QContextMenuEvent *event) override; void updateCursor(const QPoint &pos); bool atBlackoutPos(bool left, bool right, bool top, bool bottom, Settings::Position windowPos) const; void showBrowser(); void possiblyStartResize(const QPoint &pos); void hackLinkColorForQt44(); protected slots: void jumpToContext(); void linkHovered(const QString &linkName); #ifdef HAVE_KGEOMAP void launchMapView(); void updateMapForCurrentImage(DB::FileName); #endif signals: void tagHovered(QPair<QString, QString> tagData); void noTagHovered(); private: // Variables QMap<int, QPair<QString, QString>> m_linkMap; ViewerWidget *m_viewer; QToolButton *m_jumpToContext; bool m_hoveringOverLink; InfoBoxResizer m_infoBoxResizer; VisibleOptionsMenu *m_menu; QList<QPixmap> m_ratingPixmap; #ifdef HAVE_KGEOMAP QToolButton *m_showOnMap; QPointer<Map::MapView> m_map; #endif }; } #endif // INFOBOX_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/InfoBoxResizer.cpp b/Viewer/InfoBoxResizer.cpp index 3955cc9c..9c8bad9e 100644 --- a/Viewer/InfoBoxResizer.cpp +++ b/Viewer/InfoBoxResizer.cpp @@ -1,62 +1,63 @@ /* Copyright (C) 2003-2010 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 "InfoBoxResizer.h" + #include "InfoBox.h" Viewer::InfoBoxResizer::InfoBoxResizer(Viewer::InfoBox *infoBox) : m_infoBox(infoBox) { } void Viewer::InfoBoxResizer::setPos(QPoint pos) { QRect rect = m_infoBox->geometry(); pos = m_infoBox->mapToParent(pos); if (m_left) rect.setLeft(pos.x()); if (m_right) rect.setRight(pos.x()); if (m_top) rect.setTop(pos.y()); if (m_bottom) rect.setBottom(pos.y()); if (rect.width() > 100 && rect.height() > 50) m_infoBox->setGeometry(rect); } void Viewer::InfoBoxResizer::setup(bool left, bool right, bool top, bool bottom) { m_left = left; m_right = right; m_top = top; m_bottom = bottom; m_active = true; } void Viewer::InfoBoxResizer::deactivate() { m_active = false; } bool Viewer::InfoBoxResizer::isActive() const { return m_active; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/SpeedDisplay.cpp b/Viewer/SpeedDisplay.cpp index 5b67e4aa..b2a6f29e 100644 --- a/Viewer/SpeedDisplay.cpp +++ b/Viewer/SpeedDisplay.cpp @@ -1,94 +1,93 @@ /* 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 <KLocalizedString> #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) { // FIXME(jzarl): if the user sets a different shortcut, this is inaccurate // -> dynamically update this text setText(i18nc("OSD for slideshow, num of seconds per image", "<p><center><font size=\"+4\">%1 s</font></center></p>", i / 1000.0)); go(); } void Viewer::SpeedDisplay::start() { // FIXME(jzarl): if the user sets a different shortcut, this is inaccurate // -> dynamically update this text setText(i18nc("OSD for slideshow", "<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(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/TaggedArea.cpp b/Viewer/TaggedArea.cpp index 69a6aeb6..0cbe250a 100644 --- a/Viewer/TaggedArea.cpp +++ b/Viewer/TaggedArea.cpp @@ -1,81 +1,82 @@ /* Copyright (C) 2014-2019 The KPhotoAlbum Development Team 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 "TaggedArea.h" + #include <KLocalizedString> Viewer::TaggedArea::TaggedArea(QWidget *parent) : QFrame(parent) { setFrameShape(QFrame::Box); setStyleSheet(QString::fromLatin1( "Viewer--TaggedArea { border: none; background-color: none; }" "Viewer--TaggedArea:hover, Viewer--TaggedArea[selected=\"true\"]{ border: 1px solid rgb(0,255,0,99); background-color: rgb(255,255,255,30); }" "Viewer--TaggedArea[highlighted=\"true\"]{ border: 1px solid rgb(255,128,0,99); background-color: rgb(255,255,255,30); }")); } Viewer::TaggedArea::~TaggedArea() { } void Viewer::TaggedArea::setTagInfo(QString category, QString localizedCategory, QString tag) { setToolTip(tag + QString::fromLatin1(" (") + localizedCategory + QString::fromLatin1(")")); m_tagInfo = QPair<QString, QString>(category, tag); } void Viewer::TaggedArea::setActualGeometry(QRect geometry) { m_actualGeometry = geometry; } QRect Viewer::TaggedArea::actualGeometry() const { return m_actualGeometry; } void Viewer::TaggedArea::setSelected(bool selected) { m_selected = selected; } bool Viewer::TaggedArea::selected() const { return m_selected; } void Viewer::TaggedArea::deselect() { setSelected(false); } void Viewer::TaggedArea::checkIsSelected(QPair<QString, QString> tagData) { m_selected = (tagData == m_tagInfo); } bool Viewer::TaggedArea::highlighted() const { return m_highlighted; } void Viewer::TaggedArea::setHighlighted(bool highlighted) { m_highlighted = highlighted; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/TextDisplay.cpp b/Viewer/TextDisplay.cpp index 268eeb20..94d55a76 100644 --- a/Viewer/TextDisplay.cpp +++ b/Viewer/TextDisplay.cpp @@ -1,56 +1,59 @@ /* 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 "DB/ImageDB.h" + #include "ImageDisplay.h" + +#include <DB/ImageDB.h> + #include <QLabel> #include <QVBoxLayout> #include <qlabel.h> #include <qlayout.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); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/TextDisplay.h b/Viewer/TextDisplay.h index e901bb79..cb7751ad 100644 --- a/Viewer/TextDisplay.h +++ b/Viewer/TextDisplay.h @@ -1,52 +1,53 @@ /* Copyright (C) 2007-2010 Jan Kundr�t <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. */ #ifndef VIEWER_TEXTDISPLAY_H #define VIEWER_TEXTDISPLAY_H #include "AbstractDisplay.h" -#include "DB/ImageInfoPtr.h" + +#include <DB/ImageInfoPtr.h> class QWidget; class QLabel; namespace Viewer { class TextDisplay : public Viewer::AbstractDisplay { Q_OBJECT public: explicit TextDisplay(QWidget *parent); bool setImage(DB::ImageInfoPtr info, bool forward) override; void setText(const QString text); public slots: /* zooming doesn't make sense for textual display */ void zoomIn() override {} void zoomOut() override {} void zoomFull() override {} void zoomPixelForPixel() override {} private: QLabel *m_text; }; } #endif /* VIEWER_TEXTDISPLAY_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/VideoDisplay.cpp b/Viewer/VideoDisplay.cpp index a981a61d..52661163 100644 --- a/Viewer/VideoDisplay.cpp +++ b/Viewer/VideoDisplay.cpp @@ -1,221 +1,223 @@ /* 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 <DB/ImageInfo.h> #include <DB/ImageInfoPtr.h> -#include <KLocalizedString> #include <MainWindow/FeatureDialog.h> + +#include <KLocalizedString> #include <QAction> #include <QResizeEvent> #include <kmessagebox.h> #include <ktoolbar.h> #include <ktoolinvocation.h> #include <kxmlguibuilder.h> #include <kxmlguiclient.h> #include <kxmlguifactory.h> #include <phonon/audiooutput.h> #include <phonon/mediaobject.h> #include <phonon/seekslider.h> #include <phonon/videowidget.h> #include <qglobal.h> #include <qlayout.h> #include <qtimer.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()); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/VideoDisplay.h b/Viewer/VideoDisplay.h index a6cb9793..2d50d92d 100644 --- a/Viewer/VideoDisplay.h +++ b/Viewer/VideoDisplay.h @@ -1,84 +1,85 @@ /* Copyright (C) 2003-2010 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. */ #ifndef VIEWER_VIDEODISPLAY_H #define VIEWER_VIDEODISPLAY_H #include "AbstractDisplay.h" + #include <QResizeEvent> #include <phonon/mediaobject.h> namespace Phonon { class VideoWidget; class SeekSlider; } namespace Viewer { class VideoDisplay : public Viewer::AbstractDisplay { Q_OBJECT public: explicit VideoDisplay(QWidget *parent); ~VideoDisplay() override; bool setImage(DB::ImageInfoPtr info, bool forward) override; bool isPaused() const; bool isPlaying() const; QImage screenShoot(); signals: void stopped(); public slots: void zoomIn() override; void zoomOut() override; void zoomFull() override; void zoomPixelForPixel() override; void stop(); void playPause(); void restart(); void seek(); private slots: void phononStateChanged(Phonon::State, Phonon::State); protected: void resize(double factor); void resizeEvent(QResizeEvent *) override; void setup(); void setVideoWidgetSize(); enum ZoomType { FullZoom, PixelForPixelZoom, FixedZoom }; private: Phonon::MediaObject *m_mediaObject; Phonon::VideoWidget *m_videoWidget; Phonon::SeekSlider *m_slider; ZoomType m_zoomType; double m_zoomFactor; }; } #endif /* VIEWER_VIDEODISPLAY_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/VideoShooter.cpp b/Viewer/VideoShooter.cpp index 8b2aed59..181ef690 100644 --- a/Viewer/VideoShooter.cpp +++ b/Viewer/VideoShooter.cpp @@ -1,91 +1,94 @@ /* Copyright (C) 2012 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 "VideoShooter.h" + #include "InfoBox.h" #include "VideoDisplay.h" #include "ViewerWidget.h" + #include <BackgroundJobs/HandleVideoThumbnailRequestJob.h> #include <DB/ImageInfo.h> #include <ImageManager/ThumbnailCache.h> + #include <QApplication> #include <QTimer> /** \class Viewer::VideoShooter \brief Utility class for making screenshots from a video While Phonon has an API for implementing a screenshot, it unfortunately doesn't work. We therefore need to make a screenshot using QPixmap::grabWindow (grabWidget doesn't work either). As grabWindow takes the pixels directly from the window, we need to make sure that it isn't covered with the infobox or the context menu. This class takes care of that, namely waiting for the context menu to disapear, hide the infobox, stop the video, and then shoot the snapshot */ Viewer::VideoShooter *Viewer::VideoShooter::s_instance = nullptr; Viewer::VideoShooter::VideoShooter() { } void Viewer::VideoShooter::go(const DB::ImageInfoPtr &info, Viewer::ViewerWidget *viewer) { if (!s_instance) s_instance = new VideoShooter; s_instance->start(info, viewer); } void Viewer::VideoShooter::start(const DB::ImageInfoPtr &info, ViewerWidget *viewer) { qApp->setOverrideCursor(QCursor(Qt::BusyCursor)); m_info = info; m_viewer = viewer; // Hide the info box m_infoboxVisible = m_viewer->m_infoBox->isVisible(); if (m_infoboxVisible) m_viewer->m_infoBox->hide(); // Stop playback m_wasPlaying = !m_viewer->m_videoDisplay->isPaused(); if (m_wasPlaying) m_viewer->m_videoDisplay->playPause(); // Wait a bit for the context menu to disapear QTimer::singleShot(200, this, SLOT(doShoot())); } void Viewer::VideoShooter::doShoot() { // Make the screenshot and save it const QImage image = m_viewer->m_videoDisplay->screenShoot(); const DB::FileName fileName = m_info->fileName(); ImageManager::ThumbnailCache::instance()->removeThumbnail(fileName); BackgroundJobs::HandleVideoThumbnailRequestJob::saveFullScaleFrame(fileName, image); // Show the infobox again if (m_infoboxVisible) m_viewer->m_infoBox->show(); // Restart the video if (m_wasPlaying) m_viewer->m_videoDisplay->playPause(); qApp->restoreOverrideCursor(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/VideoShooter.h b/Viewer/VideoShooter.h index d701dc7b..e2edabfa 100644 --- a/Viewer/VideoShooter.h +++ b/Viewer/VideoShooter.h @@ -1,51 +1,52 @@ /* Copyright (C) 2012 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. */ #ifndef VIDEOSHOOTER_H #define VIDEOSHOOTER_H #include <DB/ImageInfoPtr.h> + #include <QObject> namespace Viewer { class ViewerWidget; class VideoShooter : public QObject { Q_OBJECT public: static void go(const DB::ImageInfoPtr &info, Viewer::ViewerWidget *viewer); private slots: void start(const DB::ImageInfoPtr &info, ViewerWidget *); void doShoot(); private: static VideoShooter *s_instance; explicit VideoShooter(); ViewerWidget *m_viewer; bool m_infoboxVisible; DB::ImageInfoPtr m_info; bool m_wasPlaying; }; } #endif // VIDEOSHOOTER_H // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewHandler.cpp b/Viewer/ViewHandler.cpp index c9732a46..83fa4934 100644 --- a/Viewer/ViewHandler.cpp +++ b/Viewer/ViewHandler.cpp @@ -1,127 +1,128 @@ /* Copyright (C) 2003-2010 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 "Viewer/ViewHandler.h" +#include "ViewHandler.h" + #include <QMouseEvent> #include <QRubberBand> #include <qapplication.h> #include <qcursor.h> #include <qpainter.h> /** * \class Viewer::ViewHandler * \brief Mouse handler used during zooming and panning actions */ Viewer::ViewHandler::ViewHandler(Viewer::ImageDisplay *display) : QObject(display) , m_scale(false) , m_pan(false) , m_rubberBand(new QRubberBand(QRubberBand::Rectangle, display)) , m_display(display) { } bool Viewer::ViewHandler::mousePressEvent(QMouseEvent *e, const QPoint &unTranslatedPos, double /*scaleFactor*/) { m_pan = false; m_scale = false; if ((e->button() & Qt::LeftButton)) { if ((e->modifiers() & Qt::ControlModifier)) { m_pan = true; } else { m_scale = true; } } else if (e->button() & Qt::MidButton) { m_pan = true; } if (m_pan) { // panning m_last = unTranslatedPos; qApp->setOverrideCursor(Qt::SizeAllCursor); m_errorX = 0; m_errorY = 0; return true; } else if (m_scale) { // scaling m_start = e->pos(); m_untranslatedStart = unTranslatedPos; qApp->setOverrideCursor(Qt::CrossCursor); return true; } else { return true; } } bool Viewer::ViewHandler::mouseMoveEvent(QMouseEvent *, const QPoint &unTranslatedPos, double scaleFactor) { if (m_scale) { m_rubberBand->setGeometry(QRect(m_untranslatedStart, unTranslatedPos).normalized()); m_rubberBand->show(); return true; } else if (m_pan) { // This code need to be taking the error into account, consider this situation: // The user moves the mouse very slowly, only 1 pixel at a time, scale factor is 3 // Then translated delta would be 1/3 which every time would be // rounded down to 0, and the panning would never move any pixels. double deltaX = m_errorX + (m_last.x() - unTranslatedPos.x()) / scaleFactor; double deltaY = m_errorY + (m_last.y() - unTranslatedPos.y()) / scaleFactor; QPoint deltaPoint = QPoint((int)deltaX, (int)deltaY); m_errorX = deltaX - ((double)((int)deltaX)); m_errorY = deltaY - ((double)((int)deltaY)); m_display->pan(deltaPoint); m_last = unTranslatedPos; return true; } else return false; } bool Viewer::ViewHandler::mouseReleaseEvent(QMouseEvent *e, const QPoint & /*unTranslatedPos*/, double /*scaleFactor*/) { if (m_scale) { qApp->restoreOverrideCursor(); m_rubberBand->hide(); m_scale = false; if ((e->pos() - m_start).manhattanLength() > 1) { m_display->zoom(m_start, e->pos()); return true; } else return false; } else if (m_pan) { qApp->restoreOverrideCursor(); m_pan = false; return true; } else return false; } void Viewer::ViewHandler::hideEvent() { // In case the escape key is pressed while viewing or scaling, then we need to restore the override cursor // (As in that case we will not see a key release event) if (m_pan || m_scale) { qApp->restoreOverrideCursor(); m_pan = false; m_scale = false; } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewHandler.h b/Viewer/ViewHandler.h index 70ce1253..76d1859e 100644 --- a/Viewer/ViewHandler.h +++ b/Viewer/ViewHandler.h @@ -1,52 +1,53 @@ /* Copyright (C) 2003-2010 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. */ #ifndef VIEWHANDLER_H #define VIEWHANDLER_H #include "ImageDisplay.h" + #include <QMouseEvent> #include <qpoint.h> class ImageDisplay; class QRubberBand; namespace Viewer { class ViewHandler : public QObject { Q_OBJECT public: explicit ViewHandler(ImageDisplay *display); bool mousePressEvent(QMouseEvent *e, const QPoint &unTranslatedPos, double scaleFactor); bool mouseReleaseEvent(QMouseEvent *e, const QPoint &unTranslatedPos, double scaleFactor); bool mouseMoveEvent(QMouseEvent *e, const QPoint &unTranslatedPos, double scaleFactor); void hideEvent(); private: bool m_scale, m_pan; QPoint m_start, m_untranslatedStart, m_last; double m_errorX, m_errorY; QRubberBand *m_rubberBand; ImageDisplay *m_display; }; } #endif /* VIEWHANDLER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewerWidget.cpp b/Viewer/ViewerWidget.cpp index 8ef201c9..9db36cc9 100644 --- a/Viewer/ViewerWidget.cpp +++ b/Viewer/ViewerWidget.cpp @@ -1,1443 +1,1442 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team 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 "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" + +#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/DescriptionUtil.h> +#include <Utilities/VideoUtil.h> + +#include <KActionCollection> +#include <KIO/CopyJob> +#include <KIconLoader> +#include <KLocalizedString> #include <QAction> #include <QApplication> #include <QContextMenuEvent> #include <QDBusConnection> #include <QDBusMessage> #include <QDesktopWidget> #include <QEventLoop> #include <QFileDialog> #include <QFileInfo> #include <QKeyEvent> #include <QList> #include <QPushButton> #include <QResizeEvent> #include <QStackedWidget> #include <QTimeLine> #include <QTimer> #include <QWheelEvent> #include <qglobal.h> -#include <KActionCollection> -#include <KIO/CopyJob> -#include <KIconLoader> -#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/DescriptionUtil.h> -#include <Utilities/VideoUtil.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(); createInvokeExternalMenu(); createVideoMenu(); createCategoryImageMenu(); createFilterMenu(); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-edit-image-properties"), this, SLOT(editImage())); action->setText(i18nc("@action:inmenu", "Annotate...")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_1); m_popup->addAction(action); m_setStackHead = m_actions->addAction(QString::fromLatin1("viewer-set-stack-head"), this, SLOT(slotSetStackHead())); m_setStackHead->setText(i18nc("@action:inmenu", "Set as First Image in Stack")); m_actions->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4); m_popup->addAction(m_setStackHead); m_showExifViewer = m_actions->addAction(QString::fromLatin1("viewer-show-exif-viewer"), this, SLOT(showExifViewer())); m_showExifViewer->setText(i18nc("@action:inmenu", "Show Exif Viewer")); m_popup->addAction(m_showExifViewer); m_copyTo = m_actions->addAction(QString::fromLatin1("viewer-copy-to"), this, SLOT(copyTo())); m_copyTo->setText(i18nc("@action:inmenu", "Copy Image to...")); m_actions->setDefaultShortcut(m_copyTo, Qt::Key_F7); m_popup->addAction(m_copyTo); if (m_type == ViewerWindow) { action = m_actions->addAction(QString::fromLatin1("viewer-close"), this, SLOT(close())); action->setText(i18nc("@action:inmenu", "Close")); action->setShortcut(Qt::Key_Escape); m_actions->setShortcutsConfigurable(action, false); } m_popup->addAction(action); m_actions->readSettings(); Q_FOREACH (QAction *action, m_actions->actions()) { action->setShortcutContext(Qt::WindowShortcut); addAction(action); } } void Viewer::ViewerWidget::createShowContextMenu() { VisibleOptionsMenu *menu = new VisibleOptionsMenu(this, m_actions); connect(menu, &VisibleOptionsMenu::visibleOptionsChanged, this, &ViewerWidget::updateInfoBox); m_popup->addMenu(menu); } void Viewer::ViewerWidget::inhibitScreenSaver(bool inhibit) { QDBusMessage message; if (inhibit) { message = QDBusMessage::createMethodCall(QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("Inhibit")); message << QString(QString::fromLatin1("KPhotoAlbum")); message << QString(QString::fromLatin1("Giving a slideshow")); QDBusMessage reply = QDBusConnection::sessionBus().call(message); if (reply.type() == QDBusMessage::ReplyMessage) m_screenSaverCookie = reply.arguments().first().toInt(); } else { if (m_screenSaverCookie != -1) { message = QDBusMessage::createMethodCall(QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"), QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("UnInhibit")); message << (uint)m_screenSaverCookie; QDBusConnection::sessionBus().send(message); m_screenSaverCookie = -1; } } } void Viewer::ViewerWidget::createInvokeExternalMenu() { m_externalPopup = new MainWindow::ExternalPopup(m_popup); m_popup->addMenu(m_externalPopup); connect(m_externalPopup, &MainWindow::ExternalPopup::aboutToShow, this, &ViewerWidget::populateExternalPopup); } void Viewer::ViewerWidget::createRotateMenu() { m_rotateMenu = new QMenu(m_popup); m_rotateMenu->setTitle(i18nc("@title:inmenu", "Rotate")); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-rotate90"), this, SLOT(rotate90())); action->setText(i18nc("@action:inmenu", "Rotate clockwise")); action->setShortcut(Qt::Key_9); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-rotate180"), this, SLOT(rotate180())); action->setText(i18nc("@action:inmenu", "Flip Over")); action->setShortcut(Qt::Key_8); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-rotare270"), this, SLOT(rotate270())); // ^ this is a typo, isn't it?! action->setText(i18nc("@action:inmenu", "Rotate counterclockwise")); action->setShortcut(Qt::Key_7); m_actions->setShortcutsConfigurable(action, false); m_rotateMenu->addAction(action); m_popup->addMenu(m_rotateMenu); } void Viewer::ViewerWidget::createSkipMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@title:inmenu As in 'skip 2 images'", "Skip")); QAction *action = m_actions->addAction(QString::fromLatin1("viewer-home"), this, SLOT(showFirst())); action->setText(i18nc("@action:inmenu Go to first image", "First")); action->setShortcut(Qt::Key_Home); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-end"), this, SLOT(showLast())); action->setText(i18nc("@action:inmenu Go to last image", "Last")); action->setShortcut(Qt::Key_End); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next"), this, SLOT(showNext())); action->setText(i18nc("@action:inmenu", "Show Next")); action->setShortcuts(QList<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")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-100"), this, SLOT(showNext100())); action->setText(i18nc("@action:inmenu", "Skip 100 Forward")); m_actions->setDefaultShortcut(action, Qt::SHIFT + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-next-1000"), this, SLOT(showNext1000())); action->setText(i18nc("@action:inmenu", "Skip 1000 Forward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_PageDown); popup->addAction(action); m_forwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev"), this, SLOT(showPrev())); action->setText(i18nc("@action:inmenu", "Show Previous")); action->setShortcuts(QList<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")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-100"), this, SLOT(showPrev100())); action->setText(i18nc("@action:inmenu", "Skip 100 Backward")); m_actions->setDefaultShortcut(action, Qt::SHIFT + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-prev-1000"), this, SLOT(showPrev1000())); action->setText(i18nc("@action:inmenu", "Skip 1000 Backward")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_PageUp); popup->addAction(action); m_backwardActions.append(action); action = m_actions->addAction(QString::fromLatin1("viewer-delete-current"), this, SLOT(deleteCurrent())); action->setText(i18nc("@action:inmenu", "Delete Image")); m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-remove-current"), this, SLOT(removeCurrent())); action->setText(i18nc("@action:inmenu", "Remove Image from Display List")); action->setShortcut(Qt::Key_Delete); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); m_popup->addMenu(popup); } void Viewer::ViewerWidget::createZoomMenu() { QMenu *popup = new QMenu(m_popup); popup->setTitle(i18nc("@action:inmenu", "Zoom")); // PENDING(blackie) Only for image display? QAction *action = m_actions->addAction(QString::fromLatin1("viewer-zoom-in"), this, SLOT(zoomIn())); action->setText(i18nc("@action:inmenu", "Zoom In")); action->setShortcut(Qt::Key_Plus); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-out"), this, SLOT(zoomOut())); action->setText(i18nc("@action:inmenu", "Zoom Out")); action->setShortcut(Qt::Key_Minus); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-full"), this, SLOT(zoomFull())); action->setText(i18nc("@action:inmenu", "Full View")); action->setShortcut(Qt::Key_Period); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-zoom-pixel"), this, SLOT(zoomPixelForPixel())); action->setText(i18nc("@action:inmenu", "Pixel for Pixel View")); action->setShortcut(Qt::Key_Equal); m_actions->setShortcutsConfigurable(action, false); popup->addAction(action); action = m_actions->addAction(QString::fromLatin1("viewer-toggle-fullscreen"), this, SLOT(toggleFullScreen())); action->setText(i18nc("@action:inmenu", "Toggle Full Screen")); action->setShortcuts(QList<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_actions->setDefaultShortcut(m_startStopSlideShow, Qt::CTRL + Qt::Key_R); popup->addAction(m_startStopSlideShow); m_slideShowRunFaster = m_actions->addAction(QString::fromLatin1("viewer-run-faster"), this, SLOT(slotSlideShowFaster())); m_slideShowRunFaster->setText(i18nc("@action:inmenu", "Run Faster")); m_actions->setDefaultShortcut(m_slideShowRunFaster, Qt::CTRL + Qt::Key_Plus); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction(m_slideShowRunFaster); m_slideShowRunSlower = m_actions->addAction(QString::fromLatin1("viewer-run-slower"), this, SLOT(slotSlideShowSlower())); m_slideShowRunSlower->setText(i18nc("@action:inmenu", "Run Slower")); m_actions->setDefaultShortcut(m_slideShowRunSlower, Qt::CTRL + Qt::Key_Minus); // if you change this, please update the info in Viewer::SpeedDisplay popup->addAction(m_slideShowRunSlower); m_popup->addMenu(popup); } void Viewer::ViewerWidget::load(const DB::FileNameList &list, int index) { m_list = list; m_imageDisplay->setImageList(list); m_current = index; load(); bool on = (list.count() > 1); m_startStopSlideShow->setEnabled(on); m_slideShowRunFaster->setEnabled(on); m_slideShowRunSlower->setEnabled(on); } void Viewer::ViewerWidget::load() { const bool isReadable = QFileInfo(currentInfo()->fileName().absolute()).isReadable(); const bool isVideo = isReadable && Utilities::isVideo(currentInfo()->fileName()); if (isReadable) { if (isVideo) { m_display = m_videoDisplay; } else m_display = m_imageDisplay; } else { m_display = m_textDisplay; m_textDisplay->setText(i18n("File not available")); updateInfoBox(); } setCurrentWidget(m_display); m_infoBox->raise(); m_rotateMenu->setEnabled(!isVideo); m_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 setTaggedAreasFromImage(); } void Viewer::ViewerWidget::setCaptionWithDetail(const QString &detail) { setWindowTitle(i18nc("@title:window %1 is the filename, %2 its detail info", "%1 %2", currentInfo()->fileName().absolute(), detail)); } void Viewer::ViewerWidget::contextMenuEvent(QContextMenuEvent *e) { if (m_videoDisplay) { if (m_videoDisplay->isPaused()) m_playPause->setText(i18nc("@action:inmenu Start video playback", "Play")); else m_playPause->setText(i18nc("@action:inmenu Pause video playback", "Pause")); m_stop->setEnabled(m_videoDisplay->isPlaying()); } m_popup->exec(e->globalPos()); e->setAccepted(true); } void Viewer::ViewerWidget::showNextN(int n) { filterNone(); if (m_display == m_videoDisplay) { m_videoPlayerStoppedManually = true; m_videoDisplay->stop(); } if (m_current + n < (int)m_list.count()) { m_current += n; if (m_current >= (int)m_list.count()) m_current = (int)m_list.count() - 1; m_forward = true; load(); } } void Viewer::ViewerWidget::showNext() { showNextN(1); } void Viewer::ViewerWidget::removeCurrent() { removeOrDeleteCurrent(OnlyRemoveFromViewer); } void Viewer::ViewerWidget::deleteCurrent() { removeOrDeleteCurrent(RemoveImageFromDatabase); } void Viewer::ViewerWidget::removeOrDeleteCurrent(RemoveAction action) { const DB::FileName fileName = m_list[m_current]; if (action == RemoveImageFromDatabase) m_removed.append(fileName); m_list.removeAll(fileName); if (m_list.isEmpty()) close(); if (m_current == m_list.count()) showPrev(); else showNextN(0); } void Viewer::ViewerWidget::showNext10() { showNextN(10); } void Viewer::ViewerWidget::showNext100() { showNextN(100); } void Viewer::ViewerWidget::showNext1000() { showNextN(1000); } void Viewer::ViewerWidget::showPrevN(int n) { if (m_display == m_videoDisplay) m_videoDisplay->stop(); if (m_current > 0) { m_current -= n; if (m_current < 0) m_current = 0; m_forward = false; load(); } } void Viewer::ViewerWidget::showPrev() { showPrevN(1); } void Viewer::ViewerWidget::showPrev10() { showPrevN(10); } void Viewer::ViewerWidget::showPrev100() { showPrevN(100); } void Viewer::ViewerWidget::showPrev1000() { showPrevN(1000); } void Viewer::ViewerWidget::rotate90() { currentInfo()->rotate(90); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::rotate180() { currentInfo()->rotate(180); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::rotate270() { currentInfo()->rotate(270); load(); invalidateThumbnail(); MainWindow::DirtyIndicator::markDirty(); emit imageRotated(m_list[m_current]); } void Viewer::ViewerWidget::showFirst() { showPrevN(m_list.count()); } void Viewer::ViewerWidget::showLast() { showNextN(m_list.count()); } 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) { 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); m_actions->setShortcutsConfigurable(seek, false); menu->addAction(seek); } QAction *sep = new QAction(m_popup); sep->setSeparator(true); m_popup->addAction(sep); m_videoActions.append(sep); m_stop = m_actions->addAction(QString::fromLatin1("viewer-video-stop"), m_videoDisplay, SLOT(stop())); m_stop->setText(i18nc("@action:inmenu Stop video playback", "Stop")); m_popup->addAction(m_stop); m_videoActions.append(m_stop); m_playPause = m_actions->addAction(QString::fromLatin1("viewer-video-pause"), m_videoDisplay, SLOT(playPause())); // text set in contextMenuEvent() m_playPause->setShortcut(Qt::Key_P); m_actions->setShortcutsConfigurable(m_playPause, false); m_popup->addAction(m_playPause); m_videoActions.append(m_playPause); m_makeThumbnailImage = m_actions->addAction(QString::fromLatin1("make-thumbnail-image"), this, SLOT(makeThumbnailImage())); m_actions->setDefaultShortcut(m_makeThumbnailImage, Qt::ControlModifier + Qt::Key_S); m_makeThumbnailImage->setText(i18nc("@action:inmenu", "Use current frame in thumbnail view")); m_popup->addAction(m_makeThumbnailImage); m_videoActions.append(m_makeThumbnailImage); QAction *restart = m_actions->addAction(QString::fromLatin1("viewer-video-restart"), m_videoDisplay, SLOT(restart())); restart->setText(i18nc("@action:inmenu Restart video playback.", "Restart")); m_popup->addAction(restart); m_videoActions.append(restart); } void Viewer::ViewerWidget::createCategoryImageMenu() { m_categoryImagePopup = new MainWindow::CategoryImagePopup(m_popup); m_popup->addMenu(m_categoryImagePopup); connect(m_categoryImagePopup, &MainWindow::CategoryImagePopup::aboutToShow, this, &ViewerWidget::populateCategoryImagePopup); } void Viewer::ViewerWidget::createFilterMenu() { m_filterMenu = new QMenu(m_popup); m_filterMenu->setTitle(i18nc("@title:inmenu", "Filters")); m_filterNone = m_actions->addAction(QString::fromLatin1("filter-empty"), this, SLOT(filterNone())); m_filterNone->setText(i18nc("@action:inmenu", "Remove All Filters")); m_filterMenu->addAction(m_filterNone); m_filterBW = m_actions->addAction(QString::fromLatin1("filter-bw"), this, SLOT(filterBW())); m_filterBW->setText(i18nc("@action:inmenu", "Apply Grayscale Filter")); m_filterBW->setCheckable(true); m_filterMenu->addAction(m_filterBW); m_filterContrastStretch = m_actions->addAction(QString::fromLatin1("filter-cs"), this, SLOT(filterContrastStretch())); m_filterContrastStretch->setText(i18nc("@action:inmenu", "Apply Contrast Stretching Filter")); m_filterContrastStretch->setCheckable(true); m_filterMenu->addAction(m_filterContrastStretch); m_filterHistogramEqualization = m_actions->addAction(QString::fromLatin1("filter-he"), this, SLOT(filterHistogramEqualization())); m_filterHistogramEqualization->setText(i18nc("@action:inmenu", "Apply Histogram Equalization Filter")); m_filterHistogramEqualization->setCheckable(true); m_filterMenu->addAction(m_filterHistogramEqualization); m_filterMono = m_actions->addAction(QString::fromLatin1("filter-mono"), this, SLOT(filterMono())); m_filterMono->setText(i18nc("@action:inmenu", "Apply Monochrome Filter")); m_filterMono->setCheckable(true); m_filterMenu->addAction(m_filterMono); m_popup->addMenu(m_filterMenu); } void Viewer::ViewerWidget::test() { #ifdef TESTING QTimeLine *timeline = new QTimeLine; timeline->setStartFrame(_infoBox->y()); timeline->setEndFrame(height()); connect(timeline, &QTimeLine::frameChanged, this, &ViewerWidget::moveInfoBox); timeline->start(); #endif // TESTING } void Viewer::ViewerWidget::moveInfoBox(int y) { m_infoBox->move(m_infoBox->x(), y); } void Viewer::ViewerWidget::createVideoViewer() { m_videoDisplay = new VideoDisplay(this); addWidget(m_videoDisplay); connect(m_videoDisplay, &VideoDisplay::stopped, this, &ViewerWidget::videoStopped); } void Viewer::ViewerWidget::stopPlayback() { m_videoDisplay->stop(); } void Viewer::ViewerWidget::invalidateThumbnail() const { ImageManager::ThumbnailCache::instance()->removeThumbnail(currentInfo()->fileName()); } void Viewer::ViewerWidget::setTaggedAreasFromImage() { // Clean all areas we probably already have foreach (TaggedArea *area, findChildren<TaggedArea *>()) { area->deleteLater(); } QMap<QString, QMap<QString, QRect>> taggedAreas = currentInfo()->taggedAreas(); addTaggedAreas(taggedAreas, AreaType::Standard); } void Viewer::ViewerWidget::addAdditionalTaggedAreas(QMap<QString, QMap<QString, QRect>> taggedAreas) { addTaggedAreas(taggedAreas, AreaType::Highlighted); } void Viewer::ViewerWidget::addTaggedAreas(QMap<QString, QMap<QString, QRect>> taggedAreas, AreaType type) { 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->setHighlighted(type == AreaType::Highlighted); newArea->show(); connect(m_infoBox, &InfoBox::tagHovered, newArea, &TaggedArea::checkIsSelected); connect(m_infoBox, &InfoBox::noTagHovered, newArea, &TaggedArea::deselect); } } // 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(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/ViewerWidget.h b/Viewer/ViewerWidget.h index 7f497c77..4a1c8bbd 100644 --- a/Viewer/ViewerWidget.h +++ b/Viewer/ViewerWidget.h @@ -1,274 +1,274 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef VIEWER_H #define VIEWER_H +#include <DB/FileNameList.h> +#include <DB/ImageInfoPtr.h> + #include <QImage> #include <QMap> #include <QPixmap> #include <QPointer> #include <QStackedWidget> -#include <DB/FileNameList.h> -#include <DB/ImageInfoPtr.h> - class KActionCollection; class QAction; class QContextMenuEvent; class QKeyEvent; class QMenu; class QResizeEvent; class QStackedWidget; class QWheelEvent; namespace DB { class ImageInfo; class Id; } namespace MainWindow { class ExternalPopup; class CategoryImagePopup; } namespace Exif { class InfoDialog; } namespace Viewer { class AbstractDisplay; class ImageDisplay; class InfoBox; class SpeedDisplay; class TextDisplay; class VideoDisplay; class VideoShooter; class ViewerWidget : public QStackedWidget { Q_OBJECT public: enum UsageType { InlineViewer, ViewerWindow }; ViewerWidget(UsageType type = ViewerWindow, QMap<Qt::Key, QPair<QString, QString>> *macroStore = nullptr); ~ViewerWidget() override; static ViewerWidget *latest(); void load(const DB::FileNameList &list, int index = 0); void infoBoxMove(); bool showingFullScreen() const; void setShowFullScreen(bool on); void show(bool slideShow); KActionCollection *actions(); /** * @brief setTaggedAreasFromImage * Clear existing areas and set them based on the currentInfo(). */ void setTaggedAreasFromImage(); /** * @brief addAdditionalTaggedAreas adds additional areas and marks them as highlighted. * @param taggedAreas */ void addAdditionalTaggedAreas(QMap<QString, QMap<QString, QRect>> taggedAreas); public slots: bool close(bool alsoDelete = false); void updateInfoBox(); void test(); void moveInfoBox(int); void stopPlayback(); void remapAreas(QSize viewSize, QRect zoomWindow, double sizeRatio); void copyTo(); signals: void soughtTo(const DB::FileName &id); void imageRotated(const DB::FileName &id); protected: void contextMenuEvent(QContextMenuEvent *e) override; void resizeEvent(QResizeEvent *) override; void keyPressEvent(QKeyEvent *) override; void wheelEvent(QWheelEvent *event) override; void moveInfoBox(); enum class AreaType { Standard, Highlighted }; /** * @brief addTaggedAreas adds tagged areas to the viewer. * @param taggedAreas Map(category -> Map(tagname, area)) * @param type AreaType::Standard is for areas that are part of the Image; AreaType::Highlight is for additional areas */ void addTaggedAreas(QMap<QString, QMap<QString, QRect>> taggedAreas, AreaType type); void load(); void setupContextMenu(); void createShowContextMenu(); void createInvokeExternalMenu(); void createRotateMenu(); void createSkipMenu(); void createZoomMenu(); void createSlideShowMenu(); void createVideoMenu(); void createCategoryImageMenu(); void createFilterMenu(); void changeSlideShowInterval(int delta); void createVideoViewer(); void inhibitScreenSaver(bool inhibit); DB::ImageInfoPtr currentInfo() const; friend class InfoBox; private: void showNextN(int); void showPrevN(int); int find_tag_in_list(const QStringList &list, QString &namefound); void invalidateThumbnail() const; enum RemoveAction { RemoveImageFromDatabase, OnlyRemoveFromViewer }; void removeOrDeleteCurrent(RemoveAction); protected slots: void showNext(); void showNext10(); void showNext100(); void showNext1000(); void showPrev(); void showPrev10(); void showPrev100(); void showPrev1000(); void showFirst(); void showLast(); void deleteCurrent(); void removeCurrent(); void rotate90(); void rotate180(); void rotate270(); void toggleFullScreen(); void slotStartStopSlideShow(); void slotSlideShowNext(); void slotSlideShowNextFromTimer(); void slotSlideShowFaster(); void slotSlideShowSlower(); void editImage(); void filterNone(); void filterSelected(); void filterBW(); void filterContrastStretch(); void filterHistogramEqualization(); void filterMono(); void slotSetStackHead(); void updateCategoryConfig(); void populateExternalPopup(); void populateCategoryImagePopup(); void videoStopped(); void showExifViewer(); void zoomIn(); void zoomOut(); void zoomFull(); void zoomPixelForPixel(); void makeThumbnailImage(); /** Set the current window title (filename) and add the given detail */ void setCaptionWithDetail(const QString &detail); private: static ViewerWidget *s_latest; friend class VideoShooter; QList<QAction *> m_forwardActions; QList<QAction *> m_backwardActions; QAction *m_startStopSlideShow; QAction *m_slideShowRunFaster; QAction *m_slideShowRunSlower; QAction *m_setStackHead; QAction *m_filterNone; QAction *m_filterSelected; QAction *m_filterBW; QAction *m_filterContrastStretch; QAction *m_filterHistogramEqualization; QAction *m_filterMono; AbstractDisplay *m_display; ImageDisplay *m_imageDisplay; VideoDisplay *m_videoDisplay; TextDisplay *m_textDisplay; int m_screenSaverCookie; DB::FileNameList m_list; DB::FileNameList m_removed; int m_current; QRect m_textRect; QMenu *m_popup; QMenu *m_rotateMenu; QMenu *m_filterMenu; MainWindow::ExternalPopup *m_externalPopup; MainWindow::CategoryImagePopup *m_categoryImagePopup; int m_width; int m_height; QPixmap m_pixmap; QAction *m_delete; QAction *m_showExifViewer; QPointer<Exif::InfoDialog> m_exifViewer; QAction *m_copyTo; InfoBox *m_infoBox; QImage m_currentImage; bool m_showingFullScreen; int m_slideShowPause; SpeedDisplay *m_speedDisplay; KActionCollection *m_actions; bool m_forward; QTimer *m_slideShowTimer; bool m_isRunningSlideShow; QList<QAction *> m_videoActions; QAction *m_stop; QAction *m_playPause; QAction *m_makeThumbnailImage; bool m_videoPlayerStoppedManually; UsageType m_type; enum InputMode { InACategory, AlwaysStartWithCategory }; InputMode m_currentInputMode; QString m_currentInput; QString m_currentCategory; QString m_currentInputList; QString m_lastFound; QString m_lastCategory; QMap<Qt::Key, QPair<QString, QString>> *m_inputMacros; QMap<Qt::Key, QPair<QString, QString>> *m_myInputMacros; QString m_lastCopyToTarget; }; } #endif /* VIEWER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/Viewer/VisibleOptionsMenu.cpp b/Viewer/VisibleOptionsMenu.cpp index 79f94cf8..0eb1ca1e 100644 --- a/Viewer/VisibleOptionsMenu.cpp +++ b/Viewer/VisibleOptionsMenu.cpp @@ -1,174 +1,176 @@ /* Copyright (C) 2003-2010 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 "VisibleOptionsMenu.h" -#include "DB/Category.h" -#include "DB/CategoryCollection.h" -#include "DB/ImageDB.h" -#include "Settings/SettingsData.h" + +#include <DB/Category.h> +#include <DB/CategoryCollection.h> +#include <DB/ImageDB.h> +#include <Settings/SettingsData.h> + #include <KActionCollection> #include <KLocalizedString> #include <KToggleAction> #include <QCheckBox> #include <QList> Viewer::VisibleOptionsMenu::VisibleOptionsMenu(QWidget *parent, KActionCollection *actions) : QMenu(i18n("Show..."), parent) { setTearOffEnabled(true); setTitle(i18n("Show")); connect(this, &VisibleOptionsMenu::aboutToShow, this, &VisibleOptionsMenu::updateState); m_showInfoBox = actions->add<KToggleAction>(QString::fromLatin1("viewer-show-infobox")); m_showInfoBox->setText(i18n("Show Info Box")); actions->setDefaultShortcut(m_showInfoBox, Qt::CTRL + Qt::Key_I); m_showInfoBox->setChecked(Settings::SettingsData::instance()->showInfoBox()); connect(m_showInfoBox, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowInfoBox); addAction(m_showInfoBox); m_showLabel = actions->add<KToggleAction>(QString::fromLatin1("viewer-show-label")); m_showLabel->setText(i18n("Show Label")); connect(m_showLabel, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowLabel); addAction(m_showLabel); m_showDescription = actions->add<KToggleAction>(QString::fromLatin1("viewer-show-description")); m_showDescription->setText(i18n("Show Description")); connect(m_showDescription, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowDescription); addAction(m_showDescription); m_showDate = actions->add<KToggleAction>(QString::fromLatin1("viewer-show-date")); m_showDate->setText(i18n("Show Date")); connect(m_showDate, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowDate); addAction(m_showDate); m_showTime = actions->add<KToggleAction>(QString::fromLatin1("viewer-show-time")); m_showTime->setText(i18n("Show Time")); connect(m_showTime, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowTime); addAction(m_showTime); m_showTime->setVisible(m_showDate->isChecked()); m_showFileName = actions->add<KToggleAction>(QString::fromLatin1("viewer-show-filename")); m_showFileName->setText(i18n("Show Filename")); connect(m_showFileName, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowFilename); addAction(m_showFileName); m_showExif = actions->add<KToggleAction>(QString::fromLatin1("viewer-show-exif")); m_showExif->setText(i18n("Show Exif")); connect(m_showExif, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowEXIF); addAction(m_showExif); m_showImageSize = actions->add<KToggleAction>(QString::fromLatin1("viewer-show-imagesize")); m_showImageSize->setText(i18n("Show Image Size")); connect(m_showImageSize, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowImageSize); addAction(m_showImageSize); m_showRating = actions->add<KToggleAction>(QString::fromLatin1("viewer-show-rating")); m_showRating->setText(i18n("Show Rating")); connect(m_showRating, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowRating); addAction(m_showRating); QList<DB::CategoryPtr> categories = DB::ImageDB::instance()->categoryCollection()->categories(); for (QList<DB::CategoryPtr>::Iterator it = categories.begin(); it != categories.end(); ++it) { KToggleAction *taction = actions->add<KToggleAction>((*it)->name()); m_actionList.append(taction); taction->setText((*it)->name()); taction->setData((*it)->name()); addAction(taction); connect(taction, &KToggleAction::toggled, this, &VisibleOptionsMenu::toggleShowCategory); } } void Viewer::VisibleOptionsMenu::toggleShowCategory(bool b) { QAction *action = qobject_cast<QAction *>(sender()); DB::ImageDB::instance()->categoryCollection()->categoryForName(action->data().value<QString>())->setDoShow(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowLabel(bool b) { Settings::SettingsData::instance()->setShowLabel(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowDescription(bool b) { Settings::SettingsData::instance()->setShowDescription(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowDate(bool b) { Settings::SettingsData::instance()->setShowDate(b); m_showTime->setVisible(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowFilename(bool b) { Settings::SettingsData::instance()->setShowFilename(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowTime(bool b) { Settings::SettingsData::instance()->setShowTime(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowEXIF(bool b) { Settings::SettingsData::instance()->setShowEXIF(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowImageSize(bool b) { Settings::SettingsData::instance()->setShowImageSize(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowRating(bool b) { Settings::SettingsData::instance()->setShowRating(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::toggleShowInfoBox(bool b) { Settings::SettingsData::instance()->setShowInfoBox(b); emit visibleOptionsChanged(); } void Viewer::VisibleOptionsMenu::updateState() { m_showInfoBox->setChecked(Settings::SettingsData::instance()->showInfoBox()); m_showLabel->setChecked(Settings::SettingsData::instance()->showLabel()); m_showDescription->setChecked(Settings::SettingsData::instance()->showDescription()); m_showDate->setChecked(Settings::SettingsData::instance()->showDate()); m_showTime->setChecked(Settings::SettingsData::instance()->showTime()); m_showFileName->setChecked(Settings::SettingsData::instance()->showFilename()); m_showExif->setChecked(Settings::SettingsData::instance()->showEXIF()); m_showImageSize->setChecked(Settings::SettingsData::instance()->showImageSize()); m_showRating->setChecked(Settings::SettingsData::instance()->showRating()); Q_FOREACH (KToggleAction *action, m_actionList) { action->setChecked(DB::ImageDB::instance()->categoryCollection()->categoryForName(action->data().value<QString>())->doShow()); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/Database.h b/XMLDB/Database.h index 97f94448..184a5f58 100644 --- a/XMLDB/Database.h +++ b/XMLDB/Database.h @@ -1,131 +1,133 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef XMLDB_DATABASE_H #define XMLDB_DATABASE_H -#include "DB/Category.h" -#include "DB/CategoryCollection.h" -#include "DB/ImageDB.h" -#include "DB/ImageInfoList.h" -#include "DB/ImageSearchInfo.h" -#include "DB/MD5Map.h" -#include "DB/MemberMap.h" #include "FileReader.h" #include "XMLCategoryCollection.h" + +#include <DB/Category.h> +#include <DB/CategoryCollection.h> #include <DB/FileNameList.h> +#include <DB/ImageDB.h> +#include <DB/ImageInfoList.h> +#include <DB/ImageSearchInfo.h> +#include <DB/MD5Map.h> +#include <DB/MemberMap.h> + #include <qdom.h> #include <qstringlist.h> namespace DB { class ImageInfo; } namespace XMLDB { class Database : public DB::ImageDB { Q_OBJECT public: uint totalCount() const override; DB::FileNameList search( const DB::ImageSearchInfo &, bool requireOnDisk = false) const override; void renameCategory(const QString &oldName, const QString newName) override; QMap<QString, DB::CountWithRange> classify(const DB::ImageSearchInfo &info, const QString &category, DB::MediaType typemask, DB::ClassificationMode mode) override; DB::FileNameList images() override; void addImages(const DB::ImageInfoList &images, bool doUpdate) override; void commitDelayedImages() override; void clearDelayedImages() override; void renameImage(DB::ImageInfoPtr info, const DB::FileName &newName) override; void addToBlockList(const DB::FileNameList &list) override; bool isBlocking(const DB::FileName &fileName) override; void deleteList(const DB::FileNameList &list) override; DB::ImageInfoPtr info(const DB::FileName &fileName) const override; DB::MemberMap &memberMap() override; void save(const QString &fileName, bool isAutoSave) override; DB::MD5Map *md5Map() override; void sortAndMergeBackIn(const DB::FileNameList &idList) override; DB::CategoryCollection *categoryCollection() override; QExplicitlySharedDataPointer<DB::ImageDateCollection> rangeCollection() override; void reorder( const DB::FileName &item, const DB::FileNameList &cutList, bool after) override; static DB::ImageInfoPtr createImageInfo(const DB::FileName &fileName, ReaderPtr, Database *db = nullptr, const QMap<QString, QString> *newToOldCategory = nullptr); static void possibleLoadCompressedCategories(ReaderPtr reader, DB::ImageInfoPtr info, Database *db, const QMap<QString, QString> *newToOldCategory = nullptr); bool stack(const DB::FileNameList &items) override; void unstack(const DB::FileNameList &images) override; DB::FileNameList getStackFor(const DB::FileName &referenceId) const override; void copyData(const DB::FileName &from, const DB::FileName &to) override; static int fileVersion(); protected: DB::FileNameList searchPrivate( const DB::ImageSearchInfo &, bool requireOnDisk, bool onlyItemsMatchingRange) const; bool rangeInclude(DB::ImageInfoPtr info) const; DB::ImageInfoList takeImagesFromSelection(const DB::FileNameList &list); void insertList(const DB::FileName &id, const DB::ImageInfoList &list, bool after); static void readOptions(DB::ImageInfoPtr info, ReaderPtr reader, const QMap<QString, QString> *newToOldCategory = nullptr); protected slots: void renameItem(DB::Category *category, const QString &oldName, const QString &newName); void deleteItem(DB::Category *category, const QString &option); void lockDB(bool lock, bool exclude) override; private: friend class DB::ImageDB; friend class FileReader; friend class FileWriter; Database(const QString &configFile, DB::UIDelegate &delegate); void forceUpdate(const DB::ImageInfoList &); QString m_fileName; DB::ImageInfoList m_images; QSet<DB::FileName> m_blockList; DB::ImageInfoList m_missingTimes; XMLCategoryCollection m_categoryCollection; DB::MemberMap m_members; DB::MD5Map m_md5map; //QMap<QString, QString> m_settings; DB::StackID m_nextStackId; typedef QMap<DB::StackID, DB::FileNameList> StackMap; mutable StackMap m_stackMap; DB::ImageInfoList m_delayedUpdate; mutable QHash<const QString, DB::ImageInfoPtr> m_imageCache; mutable QHash<const QString, DB::ImageInfoPtr> m_delayedCache; // used for checking if any images are without image attribute from the database. static bool s_anyImageWithEmptySize; }; } #endif /* XMLDB_DATABASE_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/ElementWriter.cpp b/XMLDB/ElementWriter.cpp index b096ff8a..984729f0 100644 --- a/XMLDB/ElementWriter.cpp +++ b/XMLDB/ElementWriter.cpp @@ -1,45 +1,46 @@ /* Copyright (C) 2012 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 "ElementWriter.h" + #include <QXmlStreamWriter> XMLDB::ElementWriter::ElementWriter(QXmlStreamWriter &writer, const QString &elementName, bool writeAtOnce) : m_writer(writer) , m_elementName(elementName) , m_haveWrittenStartTag(writeAtOnce) { if (writeAtOnce) m_writer.writeStartElement(m_elementName); } void XMLDB::ElementWriter::writeStartElement() { if (m_haveWrittenStartTag) return; m_haveWrittenStartTag = true; m_writer.writeStartElement(m_elementName); } XMLDB::ElementWriter::~ElementWriter() { if (m_haveWrittenStartTag) m_writer.writeEndElement(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileReader.cpp b/XMLDB/FileReader.cpp index 36fb5ef1..33022f7f 100644 --- a/XMLDB/FileReader.cpp +++ b/XMLDB/FileReader.cpp @@ -1,547 +1,548 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Local includes #include "FileReader.h" + #include "CompressFileInfo.h" #include "Database.h" #include "Logging.h" #include "XMLCategory.h" #include <DB/MD5Map.h> #include <DB/UIDelegate.h> // KDE includes #include <KLocalizedString> // Qt includes #include <QFile> #include <QHash> #include <QLocale> #include <QRegExp> #include <QStandardPaths> #include <QTextCodec> #include <QTextStream> void XMLDB::FileReader::read(const QString &configFile) { static QString versionString = QString::fromUtf8("version"); static QString compressedString = QString::fromUtf8("compressed"); ReaderPtr reader = readConfigFile(configFile); ElementInfo info = reader->readNextStartOrStopElement(QString::fromUtf8("KPhotoAlbum")); if (!info.isStartToken) reader->complainStartElementExpected(QString::fromUtf8("KPhotoAlbum")); m_fileVersion = reader->attribute(versionString, QString::fromLatin1("1")).toInt(); if (m_fileVersion > Database::fileVersion()) { DB::UserFeedback ret = m_db->uiDelegate().warningContinueCancel( QString::fromLatin1("index.xml version %1 is newer than %2!").arg(m_fileVersion).arg(Database::fileVersion()), i18n("<p>The database file (index.xml) is from a newer version of KPhotoAlbum!</p>" "<p>Chances are you will be able to read this file, but when writing it back, " "information saved in the newer version will be lost</p>"), i18n("index.xml version mismatch"), QString::fromLatin1("checkDatabaseFileVersion")); if (ret != DB::UserFeedback::Confirm) exit(-1); } setUseCompressedFileFormat(reader->attribute(compressedString).toInt()); m_db->m_members.setLoading(true); loadCategories(reader); loadImages(reader); loadBlockList(reader); loadMemberGroups(reader); //loadSettings(reader); m_db->m_members.setLoading(false); checkIfImagesAreSorted(); checkIfAllImagesHaveSizeAttributes(); } void XMLDB::FileReader::createSpecialCategories() { // Setup the "Folder" category m_folderCategory = new XMLCategory(i18n("Folder"), QString::fromLatin1("folder"), DB::Category::TreeView, 32, false); m_folderCategory->setType(DB::Category::FolderCategory); // The folder category is not stored in the index.xml file, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if (m_db->m_categoryCollection.categoryForName(m_folderCategory->name())) m_db->m_categoryCollection.removeCategory(m_folderCategory->name()); m_db->m_categoryCollection.addCategory(m_folderCategory); dynamic_cast<XMLCategory *>(m_folderCategory.data())->setShouldSave(false); // Setup the "Tokens" category DB::CategoryPtr tokenCat; if (m_fileVersion >= 7) { tokenCat = m_db->m_categoryCollection.categoryForSpecial(DB::Category::TokensCategory); } else { // Before version 7, the "Tokens" category name wasn't stored to the settings. So ... // look for a literal "Tokens" category ... tokenCat = m_db->m_categoryCollection.categoryForName(QString::fromUtf8("Tokens")); if (!tokenCat) { // ... and a translated "Tokens" category if we don't have the literal one. tokenCat = m_db->m_categoryCollection.categoryForName(i18n("Tokens")); } if (tokenCat) { // in this case we need to give the tokens category its special meaning: m_db->m_categoryCollection.removeCategory(tokenCat->name()); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } } if (!tokenCat) { // Create a new "Tokens" category tokenCat = new XMLCategory(i18n("Tokens"), QString::fromUtf8("tag"), DB::Category::TreeView, 32, true); tokenCat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(tokenCat); } // KPhotoAlbum 2.2 did not write the tokens to the category section, // so unless we do this small trick they will not show up when importing. for (char ch = 'A'; ch <= 'Z'; ++ch) { tokenCat->addItem(QString::fromUtf8("%1").arg(QChar::fromLatin1(ch))); } // Setup the "Media Type" category DB::CategoryPtr mediaCat; mediaCat = new XMLCategory(i18n("Media Type"), QString::fromLatin1("view-categories"), DB::Category::TreeView, 32, false); mediaCat->addItem(i18n("Image")); mediaCat->addItem(i18n("Video")); mediaCat->setType(DB::Category::MediaTypeCategory); dynamic_cast<XMLCategory *>(mediaCat.data())->setShouldSave(false); // The media type is not stored in the media category, // but older versions of KPhotoAlbum stored a stub entry, which we need to remove first: if (m_db->m_categoryCollection.categoryForName(mediaCat->name())) m_db->m_categoryCollection.removeCategory(mediaCat->name()); m_db->m_categoryCollection.addCategory(mediaCat); } void XMLDB::FileReader::loadCategories(ReaderPtr reader) { static QString nameString = QString::fromUtf8("name"); static QString iconString = QString::fromUtf8("icon"); static QString viewTypeString = QString::fromUtf8("viewtype"); static QString showString = QString::fromUtf8("show"); static QString thumbnailSizeString = QString::fromUtf8("thumbnailsize"); static QString positionableString = QString::fromUtf8("positionable"); static QString metaString = QString::fromUtf8("meta"); static QString tokensString = QString::fromUtf8("tokens"); static QString valueString = QString::fromUtf8("value"); static QString idString = QString::fromUtf8("id"); static QString birthDateString = QString::fromUtf8("birthDate"); static QString categoriesString = QString::fromUtf8("Categories"); static QString categoryString = QString::fromUtf8("Category"); ElementInfo info = reader->readNextStartOrStopElement(categoriesString); if (!info.isStartToken) reader->complainStartElementExpected(categoriesString); while (reader->readNextStartOrStopElement(categoryString).isStartToken) { const QString categoryName = unescape(reader->attribute(nameString)); if (!categoryName.isNull()) { // Read Category info QString icon = reader->attribute(iconString); DB::Category::ViewType type = (DB::Category::ViewType)reader->attribute(viewTypeString, QString::fromLatin1("0")).toInt(); int thumbnailSize = reader->attribute(thumbnailSizeString, QString::fromLatin1("32")).toInt(); bool show = (bool)reader->attribute(showString, QString::fromLatin1("1")).toInt(); bool positionable = (bool)reader->attribute(positionableString, QString::fromLatin1("0")).toInt(); bool tokensCat = reader->attribute(metaString) == tokensString; DB::CategoryPtr cat = m_db->m_categoryCollection.categoryForName(categoryName); bool repairMode = false; if (cat) { DB::UserFeedback choice = m_db->uiDelegate().warningContinueCancel( QString::fromUtf8("Line %1, column %2: duplicate category '%3'") .arg(reader->lineNumber()) .arg(reader->columnNumber()) .arg(categoryName), i18n("<p>Line %1, column %2: duplicate category '%3'</p>" "<p>Choose continue to ignore the duplicate category and try an automatic repair, " "or choose cancel to quit.</p>", reader->lineNumber(), reader->columnNumber(), categoryName), i18n("Error in database file")); if (choice == DB::UserFeedback::Confirm) repairMode = true; else exit(-1); } else { cat = new XMLCategory(categoryName, icon, type, thumbnailSize, show, positionable); if (tokensCat) cat->setType(DB::Category::TokensCategory); m_db->m_categoryCollection.addCategory(cat); } // Read values QStringList items; while (reader->readNextStartOrStopElement(valueString).isStartToken) { QString value = reader->attribute(valueString); if (reader->hasAttribute(idString)) { int id = reader->attribute(idString).toInt(); static_cast<XMLCategory *>(cat.data())->setIdMapping(value, id); } if (reader->hasAttribute(birthDateString)) cat->setBirthDate(value, QDate::fromString(reader->attribute(birthDateString), Qt::ISODate)); items.append(value); reader->readEndElement(); } if (repairMode) { // merge with duplicate category qCInfo(XMLDBLog) << "Repairing category " << categoryName << ": merging items " << cat->items() << " with " << items; items.append(cat->items()); items.removeDuplicates(); } cat->setItems(items); } } createSpecialCategories(); if (m_fileVersion < 7) { m_db->uiDelegate().information( QString::fromLatin1("Standard category names are no longer used since index.xml " "version 7. Standard categories will be left untranslated from now on."), i18nc("Leave \"Folder\" and \"Media Type\" untranslated below, those will show up with " "these exact names. Thanks :-)", "<p><b>This version of KPhotoAlbum does not translate \"standard\" categories " "any more.</b></p>" "<p>This may mean that – if you use a locale other than English – some of your " "categories are now displayed in English.</p>" "<p>You can manually rename your categories any time and then save your database." "</p>" "<p>In some cases, you may get two additional empty categories, \"Folder\" and " "\"Media Type\". You can delete those.</p>"), i18n("Changed standard category names")); } } void XMLDB::FileReader::loadImages(ReaderPtr reader) { static QString fileString = QString::fromUtf8("file"); static QString imagesString = QString::fromUtf8("images"); static QString imageString = QString::fromUtf8("image"); ElementInfo info = reader->readNextStartOrStopElement(imagesString); if (!info.isStartToken) reader->complainStartElementExpected(imagesString); while (reader->readNextStartOrStopElement(imageString).isStartToken) { const QString fileNameStr = reader->attribute(fileString); if (fileNameStr.isNull()) { qCWarning(XMLDBLog, "Element did not contain a file attribute"); return; } const DB::FileName dbFileName = DB::FileName::fromRelativePath(fileNameStr); DB::ImageInfoPtr info = load(dbFileName, reader); if (m_db->md5Map()->containsFile(dbFileName)) { if (m_db->md5Map()->contains(info->MD5Sum())) { qCWarning(XMLDBLog) << "Merging duplicate entry for file" << dbFileName.relative(); DB::ImageInfoPtr existingInfo = m_db->info(dbFileName); existingInfo->merge(*info); } else { m_db->uiDelegate().error( QString::fromUtf8("Conflicting information for file '%1': duplicate entry with different MD5 sum! Bailing out...") .arg(dbFileName.relative()), i18n("<p>Line %1, column %2: duplicate entry for file '%3' with different MD5 sum.</p>" "<p>Manual repair required!</p>", reader->lineNumber(), reader->columnNumber(), dbFileName.relative()), i18n("Error in database file")); exit(-1); } } else { m_db->m_images.append(info); m_db->m_md5map.insert(info->MD5Sum(), dbFileName); } } } void XMLDB::FileReader::loadBlockList(ReaderPtr reader) { static QString fileString = QString::fromUtf8("file"); static QString blockListString = QString::fromUtf8("blocklist"); static QString blockString = QString::fromUtf8("block"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == blockListString) { reader->readNextStartOrStopElement(blockListString); while (reader->readNextStartOrStopElement(blockString).isStartToken) { QString fileName = reader->attribute(fileString); if (!fileName.isEmpty()) m_db->m_blockList.insert(DB::FileName::fromRelativePath(fileName)); reader->readEndElement(); } } } void XMLDB::FileReader::loadMemberGroups(ReaderPtr reader) { static QString categoryString = QString::fromUtf8("category"); static QString groupNameString = QString::fromUtf8("group-name"); static QString memberString = QString::fromUtf8("member"); static QString membersString = QString::fromUtf8("members"); static QString memberGroupsString = QString::fromUtf8("member-groups"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == memberGroupsString) { reader->readNextStartOrStopElement(memberGroupsString); while (reader->readNextStartOrStopElement(memberString).isStartToken) { QString category = reader->attribute(categoryString); QString group = reader->attribute(groupNameString); if (reader->hasAttribute(memberString)) { QString member = reader->attribute(memberString); m_db->m_members.addMemberToGroup(category, group, member); } else { QStringList members = reader->attribute(membersString).split(QString::fromLatin1(","), QString::SkipEmptyParts); Q_FOREACH (const QString &memberItem, members) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName(category); if (!catPtr) { // category was not declared in "Categories" qCWarning(XMLDBLog) << "File corruption in index.xml. Inserting missing category: " << category; catPtr = new XMLCategory(category, QString::fromUtf8("dialog-warning"), DB::Category::TreeView, 32, false); m_db->m_categoryCollection.addCategory(catPtr); } XMLCategory *cat = static_cast<XMLCategory *>(catPtr.data()); QString member = cat->nameForId(memberItem.toInt()); if (member.isNull()) continue; m_db->m_members.addMemberToGroup(category, group, member); } if (members.size() == 0) { // Groups are stored even if they are empty, so we also have to read them. // With no members, the above for loop will not be executed. m_db->m_members.addGroup(category, group); } } reader->readEndElement(); } } } /* void XMLDB::FileReader::loadSettings(ReaderPtr reader) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementInfo info = reader->peekNext(); if (info.isStartToken && info.tokenName == settingsString) { reader->readNextStartOrStopElement(settingString); while(reader->readNextStartOrStopElement(settingString).isStartToken) { if (reader->hasAttribute(keyString) && reader->hasAttribute(valueString)) { m_db->m_settings.insert(unescape(reader->attribute(keyString)), unescape(reader->attribute(valueString))); } else { qWarning() << "File corruption in index.xml. Setting either lacking a key or a " << "value attribute. Ignoring this entry."; } reader->readEndElement(); } } } */ void XMLDB::FileReader::checkIfImagesAreSorted() { if (m_db->uiDelegate().isDialogDisabled(QString::fromLatin1("checkWhetherImagesAreSorted"))) return; QDateTime last(QDate(1900, 1, 1)); bool wrongOrder = false; for (DB::ImageInfoListIterator it = m_db->m_images.begin(); !wrongOrder && it != m_db->m_images.end(); ++it) { if (last > (*it)->date().start() && (*it)->date().start().isValid()) wrongOrder = true; last = (*it)->date().start(); } if (wrongOrder) { m_db->uiDelegate().information( QString::fromLatin1("Database is not sorted by date."), i18n("<p>Your images/videos are not sorted, which means that navigating using the date bar " "will only work suboptimally.</p>" "<p>In the <b>Maintenance</b> menu, you can find <b>Display Images with Incomplete Dates</b> " "which you can use to find the images that are missing date information.</p>" "<p>You can then select the images that you have reason to believe have a correct date " "in either their Exif data or on the file, and execute <b>Maintenance->Read Exif Info</b> " "to reread the information.</p>" "<p>Finally, once all images have their dates set, you can execute " "<b>Maintenance->Sort All by Date & Time</b> to sort them in the database. </p>"), i18n("Images/Videos Are Not Sorted"), QString::fromLatin1("checkWhetherImagesAreSorted")); } } void XMLDB::FileReader::checkIfAllImagesHaveSizeAttributes() { QTime time; time.start(); if (m_db->uiDelegate().isDialogDisabled(QString::fromLatin1("checkWhetherAllImagesIncludesSize"))) return; if (m_db->s_anyImageWithEmptySize) { m_db->uiDelegate().information( QString::fromLatin1("Found image(s) without size information."), i18n("<p>Not all the images in the database have information about image sizes; this is needed to " "get the best result in the thumbnail view. To fix this, simply go to the <b>Maintenance</b> menu, " "and first choose <b>Remove All Thumbnails</b>, and after that choose <tt>Build Thumbnails</tt>.</p>" "<p>Not doing so will result in extra space around images in the thumbnail view - that is all - so " "there is no urgency in doing it.</p>"), i18n("Not All Images Have Size Information"), QString::fromLatin1("checkWhetherAllImagesIncludesSize")); } } DB::ImageInfoPtr XMLDB::FileReader::load(const DB::FileName &fileName, ReaderPtr reader) { DB::ImageInfoPtr info = XMLDB::Database::createImageInfo(fileName, reader, m_db); m_nextStackId = qMax(m_nextStackId, info->stackId() + 1); info->createFolderCategoryItem(m_folderCategory, m_db->m_members); return info; } XMLDB::ReaderPtr XMLDB::FileReader::readConfigFile(const QString &configFile) { ReaderPtr reader = ReaderPtr(new XmlReader(m_db->uiDelegate(), configFile)); QFile file(configFile); if (!file.exists()) { // Load a default setup QFile file(QStandardPaths::locate(QStandardPaths::DataLocation, QString::fromLatin1("default-setup"))); if (!file.open(QIODevice::ReadOnly)) { m_db->uiDelegate().information( QString::fromLatin1("default-setup not found in standard paths."), i18n("<p>KPhotoAlbum was unable to load a default setup, which indicates an installation error</p>" "<p>If you have installed KPhotoAlbum yourself, then you must remember to set the environment variable " "<b>KDEDIRS</b>, to point to the topmost installation directory.</p>" "<p>If you for example ran cmake with <b>-DCMAKE_INSTALL_PREFIX=/usr/local/kde</b>, then you must use the following " "environment variable setup (this example is for Bash and compatible shells):</p>" "<p><b>export KDEDIRS=/usr/local/kde</b></p>" "<p>In case you already have KDEDIRS set, simply append the string as if you where setting the <b>PATH</b> " "environment variable</p>"), i18n("No default setup file found")); } else { QTextStream stream(&file); stream.setCodec(QTextCodec::codecForName("UTF-8")); QString str = stream.readAll(); // Replace the default setup's category and tag names with localized ones str = str.replace(QString::fromUtf8("People"), i18n("People")); str = str.replace(QString::fromUtf8("Places"), i18n("Places")); str = str.replace(QString::fromUtf8("Events"), i18n("Events")); str = str.replace(QString::fromUtf8("untagged"), i18n("untagged")); str = str.replace(QRegExp(QString::fromLatin1("imageDirectory=\"[^\"]*\"")), QString::fromLatin1("")); str = str.replace(QRegExp(QString::fromLatin1("htmlBaseDir=\"[^\"]*\"")), QString::fromLatin1("")); str = str.replace(QRegExp(QString::fromLatin1("htmlBaseURL=\"[^\"]*\"")), QString::fromLatin1("")); reader->addData(str); } } else { if (!file.open(QIODevice::ReadOnly)) { m_db->uiDelegate().error( QString::fromLatin1("Unable to open '%1' for reading").arg(configFile), i18n("Unable to open '%1' for reading", configFile), i18n("Error Running Demo")); exit(-1); } reader->addData(file.readAll()); #if 0 QString errMsg; int errLine; int errCol; if ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol )) { file.close(); // If parsing index.xml fails let's see if we could use a backup instead Utilities::checkForBackupFile( configFile, i18n( "line %1 column %2 in file %3: %4", errLine , errCol , configFile , errMsg ) ); if ( !file.open( QIODevice::ReadOnly ) || ( !doc.setContent( &file, false, &errMsg, &errLine, &errCol ) ) ) { KMessageBox::error( messageParent(), i18n( "Failed to recover the backup: %1", errMsg ) ); exit(-1); } } #endif } // Now read the content of the file. #if 0 QDomElement top = doc.documentElement(); if ( top.isNull() ) { KMessageBox::error( messageParent(), i18n("Error in file %1: No elements found", configFile ) ); exit(-1); } if ( top.tagName().toLower() != QString::fromLatin1( "kphotoalbum" ) && top.tagName().toLower() != QString::fromLatin1( "kimdaba" ) ) { // KimDaBa compatibility KMessageBox::error( messageParent(), i18n("Error in file %1: expected 'KPhotoAlbum' as top element but found '%2'", configFile , top.tagName() ) ); exit(-1); } #endif file.close(); return reader; } /** * @brief Unescape a string used as an XML attribute name. * * @see XMLDB::FileWriter::escape * * @param str the string to be unescaped * @return the unescaped string */ QString XMLDB::FileReader::unescape(const QString &str) { static bool hashUsesCompressedFormat = useCompressedFileFormat(); static QHash<QString, QString> s_cache; if (hashUsesCompressedFormat != useCompressedFileFormat()) s_cache.clear(); if (s_cache.contains(str)) return s_cache[str]; QString tmp(str); // Matches encoded characters in attribute names QRegExp rx(QString::fromLatin1("(_.)([0-9A-F]{2})")); int pos = 0; // Unencoding special characters if compressed XML is selected if (useCompressedFileFormat()) { while ((pos = rx.indexIn(tmp, pos)) != -1) { QString before = rx.cap(1) + rx.cap(2); QString after = QString::fromLatin1(QByteArray::fromHex(rx.cap(2).toLocal8Bit())); tmp.replace(pos, before.length(), after); pos += after.length(); } } else tmp.replace(QString::fromLatin1("_"), QString::fromLatin1(" ")); s_cache.insert(str, tmp); return tmp; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileReader.h b/XMLDB/FileReader.h index cd63fe85..e5675a78 100644 --- a/XMLDB/FileReader.h +++ b/XMLDB/FileReader.h @@ -1,73 +1,75 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef XMLDB_FILEREADER_H #define XMLDB_FILEREADER_H -#include "DB/ImageInfo.h" -#include "DB/ImageInfoPtr.h" #include "XmlReader.h" + +#include <DB/ImageInfo.h> +#include <DB/ImageInfoPtr.h> + #include <QSharedPointer> class QXmlStreamReader; namespace XMLDB { class Database; class FileReader { public: FileReader(Database *db) : m_db(db) , m_nextStackId(1) { } void read(const QString &configFile); static QString unescape(const QString &); DB::StackID nextStackId() const { return m_nextStackId; } protected: void loadCategories(ReaderPtr reader); void loadImages(ReaderPtr reader); void loadBlockList(ReaderPtr reader); void loadMemberGroups(ReaderPtr reader); //void loadSettings(ReaderPtr reader); DB::ImageInfoPtr load(const DB::FileName &filename, ReaderPtr reader); ReaderPtr readConfigFile(const QString &configFile); void createSpecialCategories(); void checkIfImagesAreSorted(); void checkIfAllImagesHaveSizeAttributes(); private: Database *const m_db; int m_fileVersion; DB::StackID m_nextStackId; // During profilation I found that it was rather expensive to look this up over and over again (once for each image) DB::CategoryPtr m_folderCategory; }; } #endif /* XMLDB_FILEREADER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileWriter.cpp b/XMLDB/FileWriter.cpp index 3ae82cc1..01305e5f 100644 --- a/XMLDB/FileWriter.cpp +++ b/XMLDB/FileWriter.cpp @@ -1,495 +1,494 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "FileWriter.h" #include "CompressFileInfo.h" #include "Database.h" #include "ElementWriter.h" #include "Logging.h" #include "NumberedBackup.h" #include "XMLCategory.h" #include <DB/UIDelegate.h> #include <MainWindow/Logging.h> #include <Settings/SettingsData.h> #include <Utilities/List.h> #include <KLocalizedString> - #include <QFile> #include <QFileInfo> #include <QXmlStreamWriter> // // // // +++++++++++++++++++++++++++++++ REMEMBER ++++++++++++++++++++++++++++++++ // // // // // Update XMLDB::Database::fileVersion every time you update the file format! // // // // // // // // // (sorry for the noise, but it is really important :-) using Utilities::StringSet; void XMLDB::FileWriter::save(const QString &fileName, bool isAutoSave) { setUseCompressedFileFormat(Settings::SettingsData::instance()->useCompressedIndexXML()); if (!isAutoSave) NumberedBackup(m_db->uiDelegate()).makeNumberedBackup(); // prepare XML document for saving: m_db->m_categoryCollection.initIdMap(); QFile out(fileName + QString::fromLatin1(".tmp")); if (!out.open(QIODevice::WriteOnly | QIODevice::Text)) { m_db->uiDelegate().sorry( QString::fromUtf8("Error saving to file '%1': %2").arg(out.fileName()).arg(out.errorString()), i18n("<p>Could not save the image database to XML.</p>" "File %1 could not be opened because of the following error: %2", out.fileName(), out.errorString()), i18n("Error while saving...")); return; } QTime t; if (TimingLog().isDebugEnabled()) t.start(); QXmlStreamWriter writer(&out); writer.setAutoFormatting(true); writer.writeStartDocument(); { ElementWriter dummy(writer, QString::fromLatin1("KPhotoAlbum")); writer.writeAttribute(QString::fromLatin1("version"), QString::number(Database::fileVersion())); writer.writeAttribute(QString::fromLatin1("compressed"), QString::number(useCompressedFileFormat())); saveCategories(writer); saveImages(writer); saveBlockList(writer); saveMemberGroups(writer); //saveSettings(writer); } writer.writeEndDocument(); qCDebug(TimingLog) << "XMLDB::FileWriter::save(): Saving took" << t.elapsed() << "ms"; // State: index.xml has previous DB version, index.xml.tmp has the current version. // original file can be safely deleted if ((!QFile::remove(fileName)) && QFile::exists(fileName)) { m_db->uiDelegate().sorry( QString::fromUtf8("Removal of file '%1' failed.").arg(fileName), i18n("<p>Failed to remove old version of image database.</p>" "<p>Please try again or replace the file %1 with file %2 manually!</p>", fileName, out.fileName()), i18n("Error while saving...")); return; } // State: index.xml doesn't exist, index.xml.tmp has the current version. if (!out.rename(fileName)) { m_db->uiDelegate().sorry( QString::fromUtf8("Renaming index.xml to '%1' failed.").arg(out.fileName()), i18n("<p>Failed to move temporary XML file to permanent location.</p>" "<p>Please try again or rename file %1 to %2 manually!</p>", out.fileName(), fileName), i18n("Error while saving...")); // State: index.xml.tmp has the current version. return; } // State: index.xml has the current version. } void XMLDB::FileWriter::saveCategories(QXmlStreamWriter &writer) { QStringList categories = DB::ImageDB::instance()->categoryCollection()->categoryNames(); ElementWriter dummy(writer, QString::fromLatin1("Categories")); DB::CategoryPtr tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory); for (QString name : categories) { DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(name); if (!shouldSaveCategory(name)) { continue; } ElementWriter dummy(writer, QString::fromUtf8("Category")); writer.writeAttribute(QString::fromUtf8("name"), name); writer.writeAttribute(QString::fromUtf8("icon"), category->iconName()); writer.writeAttribute(QString::fromUtf8("show"), QString::number(category->doShow())); writer.writeAttribute(QString::fromUtf8("viewtype"), QString::number(category->viewType())); writer.writeAttribute(QString::fromUtf8("thumbnailsize"), QString::number(category->thumbnailSize())); writer.writeAttribute(QString::fromUtf8("positionable"), QString::number(category->positionable())); if (category == tokensCategory) { writer.writeAttribute(QString::fromUtf8("meta"), QString::fromUtf8("tokens")); } // FIXME (l3u): // Correct me if I'm wrong, but we don't need this, as the tags used as groups are // added to the respective category anyway when they're created, so there's no need to // re-add them here. Apart from this, adding an empty group (one without members) does // add an empty tag ("") doing so. /* QStringList list = Utilities::mergeListsUniqly(category->items(), m_db->_members.groups(name)); */ Q_FOREACH (const QString &tagName, category->items()) { ElementWriter dummy(writer, QString::fromLatin1("value")); writer.writeAttribute(QString::fromLatin1("value"), tagName); writer.writeAttribute(QString::fromLatin1("id"), QString::number(static_cast<XMLCategory *>(category.data())->idForName(tagName))); QDate birthDate = category->birthDate(tagName); if (!birthDate.isNull()) writer.writeAttribute(QString::fromUtf8("birthDate"), birthDate.toString(Qt::ISODate)); } } } void XMLDB::FileWriter::saveImages(QXmlStreamWriter &writer) { DB::ImageInfoList list = m_db->m_images; // Copy files from clipboard to end of overview, so we don't loose them Q_FOREACH (const DB::ImageInfoPtr &infoPtr, m_db->m_clipboard) { list.append(infoPtr); } { ElementWriter dummy(writer, QString::fromLatin1("images")); Q_FOREACH (const DB::ImageInfoPtr &infoPtr, list) { save(writer, infoPtr); } } } void XMLDB::FileWriter::saveBlockList(QXmlStreamWriter &writer) { ElementWriter dummy(writer, QString::fromLatin1("blocklist")); QList<DB::FileName> blockList = m_db->m_blockList.toList(); // sort blocklist to get diffable files std::sort(blockList.begin(), blockList.end()); Q_FOREACH (const DB::FileName &block, blockList) { ElementWriter dummy(writer, QString::fromLatin1("block")); writer.writeAttribute(QString::fromLatin1("file"), block.relative()); } } void XMLDB::FileWriter::saveMemberGroups(QXmlStreamWriter &writer) { if (m_db->m_members.isEmpty()) return; ElementWriter dummy(writer, QString::fromLatin1("member-groups")); for (QMap<QString, QMap<QString, StringSet>>::ConstIterator memberMapIt = m_db->m_members.memberMap().constBegin(); memberMapIt != m_db->m_members.memberMap().constEnd(); ++memberMapIt) { const QString categoryName = memberMapIt.key(); // FIXME (l3u): This can happen when an empty sub-category (group) is present. // Would be fine to fix the reason why this happens in the first place. if (categoryName.isEmpty()) { continue; } if (!shouldSaveCategory(categoryName)) continue; QMap<QString, StringSet> groupMap = memberMapIt.value(); for (QMap<QString, StringSet>::ConstIterator groupMapIt = groupMap.constBegin(); groupMapIt != groupMap.constEnd(); ++groupMapIt) { // FIXME (l3u): This can happen when an empty sub-category (group) is present. // Would be fine to fix the reason why this happens in the first place. if (groupMapIt.key().isEmpty()) { continue; } if (useCompressedFileFormat()) { StringSet members = groupMapIt.value(); ElementWriter dummy(writer, QString::fromLatin1("member")); writer.writeAttribute(QString::fromLatin1("category"), categoryName); writer.writeAttribute(QString::fromLatin1("group-name"), groupMapIt.key()); QStringList idList; Q_FOREACH (const QString &member, members) { DB::CategoryPtr catPtr = m_db->m_categoryCollection.categoryForName(categoryName); XMLCategory *category = static_cast<XMLCategory *>(catPtr.data()); if (category->idForName(member) == 0) qCWarning(XMLDBLog) << "Member" << member << "in group" << categoryName << "->" << groupMapIt.key() << "has no id!"; idList.append(QString::number(category->idForName(member))); } std::sort(idList.begin(), idList.end()); writer.writeAttribute(QString::fromLatin1("members"), idList.join(QString::fromLatin1(","))); } else { QStringList members = groupMapIt.value().toList(); std::sort(members.begin(), members.end()); Q_FOREACH (const QString &member, members) { ElementWriter dummy(writer, QString::fromLatin1("member")); writer.writeAttribute(QString::fromLatin1("category"), memberMapIt.key()); writer.writeAttribute(QString::fromLatin1("group-name"), groupMapIt.key()); writer.writeAttribute(QString::fromLatin1("member"), member); } // Add an entry even if the group is empty // (this is not necessary for the compressed format) if (members.size() == 0) { ElementWriter dummy(writer, QString::fromLatin1("member")); writer.writeAttribute(QString::fromLatin1("category"), memberMapIt.key()); writer.writeAttribute(QString::fromLatin1("group-name"), groupMapIt.key()); } } } } } /* Perhaps, we may need this later ;-) void XMLDB::FileWriter::saveSettings(QXmlStreamWriter& writer) { static QString settingsString = QString::fromUtf8("settings"); static QString settingString = QString::fromUtf8("setting"); static QString keyString = QString::fromUtf8("key"); static QString valueString = QString::fromUtf8("value"); ElementWriter dummy(writer, settingsString); QMap<QString, QString> settings; // For testing settings.insert(QString::fromUtf8("tokensCategory"), QString::fromUtf8("Tokens")); settings.insert(QString::fromUtf8("untaggedCategory"), QString::fromUtf8("Events")); settings.insert(QString::fromUtf8("untaggedTag"), QString::fromUtf8("untagged")); QMapIterator<QString, QString> settingsIterator(settings); while (settingsIterator.hasNext()) { ElementWriter dummy(writer, settingString); settingsIterator.next(); writer.writeAttribute(keyString, escape(settingsIterator.key())); writer.writeAttribute(valueString, escape(settingsIterator.value())); } } */ void XMLDB::FileWriter::save(QXmlStreamWriter &writer, const DB::ImageInfoPtr &info) { ElementWriter dummy(writer, QString::fromLatin1("image")); writer.writeAttribute(QString::fromLatin1("file"), info->fileName().relative()); if (info->label() != QFileInfo(info->fileName().relative()).completeBaseName()) writer.writeAttribute(QString::fromLatin1("label"), info->label()); if (!info->description().isEmpty()) writer.writeAttribute(QString::fromLatin1("description"), info->description()); DB::ImageDate date = info->date(); QDateTime start = date.start(); QDateTime end = date.end(); writer.writeAttribute(QString::fromLatin1("startDate"), start.toString(Qt::ISODate)); if (start != end) writer.writeAttribute(QString::fromLatin1("endDate"), end.toString(Qt::ISODate)); if (info->angle() != 0) writer.writeAttribute(QString::fromLatin1("angle"), QString::number(info->angle())); writer.writeAttribute(QString::fromLatin1("md5sum"), info->MD5Sum().toHexString()); writer.writeAttribute(QString::fromLatin1("width"), QString::number(info->size().width())); writer.writeAttribute(QString::fromLatin1("height"), QString::number(info->size().height())); if (info->rating() != -1) { writer.writeAttribute(QString::fromLatin1("rating"), QString::number(info->rating())); } if (info->stackId()) { writer.writeAttribute(QString::fromLatin1("stackId"), QString::number(info->stackId())); writer.writeAttribute(QString::fromLatin1("stackOrder"), QString::number(info->stackOrder())); } if (info->isVideo()) writer.writeAttribute(QLatin1String("videoLength"), QString::number(info->videoLength())); if (useCompressedFileFormat()) writeCategoriesCompressed(writer, info); else writeCategories(writer, info); } QString XMLDB::FileWriter::areaToString(QRect area) const { QStringList areaString; areaString.append(QString::number(area.x())); areaString.append(QString::number(area.y())); areaString.append(QString::number(area.width())); areaString.append(QString::number(area.height())); return areaString.join(QString::fromLatin1(" ")); } void XMLDB::FileWriter::writeCategories(QXmlStreamWriter &writer, const DB::ImageInfoPtr &info) { ElementWriter topElm(writer, QString::fromLatin1("options"), false); QStringList grps = info->availableCategories(); Q_FOREACH (const QString &name, grps) { if (!shouldSaveCategory(name)) continue; ElementWriter categoryElm(writer, QString::fromLatin1("option"), false); QStringList items = info->itemsOfCategory(name).toList(); std::sort(items.begin(), items.end()); if (!items.isEmpty()) { topElm.writeStartElement(); categoryElm.writeStartElement(); writer.writeAttribute(QString::fromLatin1("name"), name); } Q_FOREACH (const QString &itemValue, items) { ElementWriter dummy(writer, QString::fromLatin1("value")); writer.writeAttribute(QString::fromLatin1("value"), itemValue); QRect area = info->areaForTag(name, itemValue); if (!area.isNull()) { writer.writeAttribute(QString::fromLatin1("area"), areaToString(area)); } } } } void XMLDB::FileWriter::writeCategoriesCompressed(QXmlStreamWriter &writer, const DB::ImageInfoPtr &info) { QMap<QString, QList<QPair<QString, QRect>>> positionedTags; QList<DB::CategoryPtr> categoryList = DB::ImageDB::instance()->categoryCollection()->categories(); Q_FOREACH (const DB::CategoryPtr &category, categoryList) { QString categoryName = category->name(); if (!shouldSaveCategory(categoryName)) continue; StringSet items = info->itemsOfCategory(categoryName); if (!items.empty()) { QStringList idList; Q_FOREACH (const QString &itemValue, items) { QRect area = info->areaForTag(categoryName, itemValue); if (area.isValid()) { // Positioned tags can't be stored in the "fast" format // so we have to handle them separately positionedTags[categoryName] << QPair<QString, QRect>(itemValue, area); } else { int id = static_cast<const XMLCategory *>(category.data())->idForName(itemValue); idList.append(QString::number(id)); } } // Possibly all ids of a category have area information, so only // write the category attribute if there are actually ids to write if (!idList.isEmpty()) { std::sort(idList.begin(), idList.end()); writer.writeAttribute(escape(categoryName), idList.join(QString::fromLatin1(","))); } } } // Add a "readable" sub-element for the positioned tags // FIXME: can this be merged with the code in writeCategories()? if (!positionedTags.isEmpty()) { ElementWriter topElm(writer, QString::fromLatin1("options"), false); topElm.writeStartElement(); QMapIterator<QString, QList<QPair<QString, QRect>>> categoryWithAreas(positionedTags); while (categoryWithAreas.hasNext()) { categoryWithAreas.next(); ElementWriter categoryElm(writer, QString::fromLatin1("option"), false); categoryElm.writeStartElement(); writer.writeAttribute(QString::fromLatin1("name"), categoryWithAreas.key()); QList<QPair<QString, QRect>> areas = categoryWithAreas.value(); std::sort(areas.begin(), areas.end(), [](QPair<QString, QRect> a, QPair<QString, QRect> b) { return a.first < b.first; }); Q_FOREACH (const auto &positionedTag, areas) { ElementWriter dummy(writer, QString::fromLatin1("value")); writer.writeAttribute(QString::fromLatin1("value"), positionedTag.first); writer.writeAttribute(QString::fromLatin1("area"), areaToString(positionedTag.second)); } } } } bool XMLDB::FileWriter::shouldSaveCategory(const QString &categoryName) const { // Profiling indicated that this function was a hotspot, so this cache improved saving speed with 25% static QHash<QString, bool> cache; if (cache.contains(categoryName)) return cache[categoryName]; // A few bugs has shown up, where an invalid category name has crashed KPA. It therefore checks for such invalid names here. if (!m_db->m_categoryCollection.categoryForName(categoryName)) { qCWarning(XMLDBLog, "Invalid category name: %s", qPrintable(categoryName)); cache.insert(categoryName, false); return false; } const bool shouldSave = dynamic_cast<XMLCategory *>(m_db->m_categoryCollection.categoryForName(categoryName).data())->shouldSave(); cache.insert(categoryName, shouldSave); return shouldSave; } /** * @brief Escape problematic characters in a string that forms an XML attribute name. * * N.B.: Attribute values do not need to be escaped! * @see XMLDB::FileReader::unescape * * @param str the string to be escaped * @return the escaped string */ QString XMLDB::FileWriter::escape(const QString &str) { static bool hashUsesCompressedFormat = useCompressedFileFormat(); static QHash<QString, QString> s_cache; if (hashUsesCompressedFormat != useCompressedFileFormat()) s_cache.clear(); if (s_cache.contains(str)) return s_cache[str]; QString tmp(str); // Regex to match characters that are not allowed to start XML attribute names const QRegExp rx(QString::fromLatin1("([^a-zA-Z0-9:_])")); int pos = 0; // Encoding special characters if compressed XML is selected if (useCompressedFileFormat()) { while ((pos = rx.indexIn(tmp, pos)) != -1) { QString before = rx.cap(1); QString after; after.sprintf("_.%0X", rx.cap(1).data()->toLatin1()); tmp.replace(pos, before.length(), after); pos += after.length(); } } else tmp.replace(QString::fromLatin1(" "), QString::fromLatin1("_")); s_cache.insert(str, tmp); return tmp; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/FileWriter.h b/XMLDB/FileWriter.h index 31cd57fb..722bd142 100644 --- a/XMLDB/FileWriter.h +++ b/XMLDB/FileWriter.h @@ -1,62 +1,62 @@ /* Copyright (C) 2003-2019 The KPhotoAlbum Development Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef XMLDB_FILEWRITER_H #define XMLDB_FILEWRITER_H +#include <DB/ImageInfoPtr.h> + #include <QRect> #include <QString> -#include <DB/ImageInfoPtr.h> - class QXmlStreamWriter; namespace XMLDB { class Database; class FileWriter { public: explicit FileWriter(Database *db) : m_db(db) { } void save(const QString &fileName, bool isAutoSave); static QString escape(const QString &); protected: void saveCategories(QXmlStreamWriter &); void saveImages(QXmlStreamWriter &); void saveBlockList(QXmlStreamWriter &); void saveMemberGroups(QXmlStreamWriter &); void save(QXmlStreamWriter &writer, const DB::ImageInfoPtr &info); void writeCategories(QXmlStreamWriter &, const DB::ImageInfoPtr &info); void writeCategoriesCompressed(QXmlStreamWriter &, const DB::ImageInfoPtr &info); bool shouldSaveCategory(const QString &categoryName) const; //void saveSettings(QXmlStreamWriter&); private: Database *const m_db; QString areaToString(QRect area) const; }; } #endif /* XMLDB_FILEWRITER_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLCategory.cpp b/XMLDB/XMLCategory.cpp index 3a759541..712cc689 100644 --- a/XMLDB/XMLCategory.cpp +++ b/XMLDB/XMLCategory.cpp @@ -1,221 +1,222 @@ /* 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" + +#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]; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLCategory.h b/XMLDB/XMLCategory.h index 0f982aef..373f14b8 100644 --- a/XMLDB/XMLCategory.h +++ b/XMLDB/XMLCategory.h @@ -1,91 +1,92 @@ /* Copyright (C) 2003-2010 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. */ #ifndef XMLCATEGORY_H #define XMLCATEGORY_H -#include "DB/Category.h" +#include <DB/Category.h> + #include <QMap> #include <qstringlist.h> namespace XMLDB { class XMLCategory : public DB::Category { Q_OBJECT public: XMLCategory(const QString &name, const QString &icon, ViewType type, int thumbnailSize, bool show, bool positionable = false); QString name() const override; void setName(const QString &name) override; void setPositionable(bool) override; bool positionable() const override; QString iconName() const override; void setIconName(const QString &name) override; void setViewType(ViewType type) override; ViewType viewType() const override; void setThumbnailSize(int) override; int thumbnailSize() const override; void setDoShow(bool b) override; bool doShow() const override; void setType(DB::Category::CategoryType t) override; CategoryType type() const override; bool isSpecialCategory() const override; void addOrReorderItems(const QStringList &items) override; void setItems(const QStringList &items) override; void removeItem(const QString &item) override; void renameItem(const QString &oldValue, const QString &newValue) override; void addItem(const QString &item) override; QStringList items() const override; int idForName(const QString &name) const; void initIdMap(); void setIdMapping(const QString &name, int id); QString nameForId(int id) const; bool shouldSave(); void setShouldSave(bool b); void setBirthDate(const QString &item, const QDate &birthDate) override; QDate birthDate(const QString &item) const override; private: QString m_name; QString m_icon; bool m_show; ViewType m_type; int m_thumbnailSize; bool m_positionable; CategoryType m_categoryType; QStringList m_items; QMap<QString, int> m_idMap; QMap<int, QString> m_nameMap; QMap<QString, QDate> m_birthDates; bool m_shouldSave; }; } #endif /* XMLCATEGORY_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLCategoryCollection.cpp b/XMLDB/XMLCategoryCollection.cpp index e5302c81..9d00d1a8 100644 --- a/XMLDB/XMLCategoryCollection.cpp +++ b/XMLDB/XMLCategoryCollection.cpp @@ -1,108 +1,109 @@ /* 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 "XMLCategory.h" + #include <DB/ImageDB.h> +#include <QList> + 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(); } } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLCategoryCollection.h b/XMLDB/XMLCategoryCollection.h index 7197bcae..1f2e3122 100644 --- a/XMLDB/XMLCategoryCollection.h +++ b/XMLDB/XMLCategoryCollection.h @@ -1,54 +1,55 @@ /* Copyright (C) 2003-2010 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. */ #ifndef XMLCATEGORYCOLLECTION_H #define XMLCATEGORYCOLLECTION_H -#include "DB/CategoryCollection.h" +#include <DB/CategoryCollection.h> + #include <QList> #include <QMap> namespace XMLDB { class XMLCategoryCollection : public DB::CategoryCollection { Q_OBJECT public: DB::CategoryPtr categoryForName(const QString &name) const override; void addCategory(DB::CategoryPtr); QStringList categoryNames() const override; QStringList categoryTexts() const override; void removeCategory(const QString &name) override; void rename(const QString &oldName, const QString &newName) override; QList<DB::CategoryPtr> categories() const override; void addCategory(const QString &text, const QString &icon, DB::Category::ViewType type, int thumbnailSize, bool show, bool positionable = false) override; DB::CategoryPtr categoryForSpecial(const DB::Category::CategoryType type) const override; void initIdMap(); private: QList<DB::CategoryPtr> m_categories; QMap<DB::Category::CategoryType, DB::CategoryPtr> m_specialCategories; }; } #endif /* XMLCATEGORYCOLLECTION_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLImageDateCollection.cpp b/XMLDB/XMLImageDateCollection.cpp index 7e0a125e..f0f877de 100644 --- a/XMLDB/XMLImageDateCollection.cpp +++ b/XMLDB/XMLImageDateCollection.cpp @@ -1,140 +1,141 @@ /* Copyright (C) 2003-2010 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 "XMLImageDateCollection.h" -#include "DB/FileNameList.h" -#include "DB/ImageDB.h" + +#include <DB/FileNameList.h> +#include <DB/ImageDB.h> void XMLDB::XMLImageDateCollection::add(const DB::ImageDate &date) { m_startIndex.insertMulti(date.start(), date); } void XMLDB::XMLImageDateCollection::buildIndex() { StartIndexMap::ConstIterator startSearch = m_startIndex.constBegin(); QDateTime biggestEnd = QDateTime(QDate(1900, 1, 1)); for (StartIndexMap::ConstIterator it = m_startIndex.constBegin(); it != m_startIndex.constEnd(); ++it) { // We want a monotonic mapping end-date -> smallest-in-start-index. // Since we go through the start index sorted, lowest first, we just // have to keep the last pointer as long as we find smaller end-dates. // This should be rare as it only occurs if there are images that // actually represent a range not just a point in time. if (it.value().end() >= biggestEnd) { biggestEnd = it.value().end(); startSearch = it; } m_endIndex.insert(it.value().end(), startSearch); } } /** Previously, counting the elements was done by going through all elements and count the matches for a particular range, this unfortunately had O(n) complexity multiplied by m ranges we would get O(mn). Henner Zeller rewrote it to its current state. The main idea now is to have all dates sorted so that it is possible to only look at the requested range. Since it is not points in time, we can't have just a simple sorted list. So we have two sorted maps, the m_startIndex and m_endIndex. m_startIndex is sorted by the start time of all ImageDates (which are in fact ranges) If we would just look for Images that start _after_ the query-range, we would miscount, because there might be Image ranges starting before the query time but whose end time reaches into the query range this is what the m_endIndex is for: it is sortd by end-date; here we look for everything that is >= our query start. its value() part is basically a pointer to the position in the m_startIndex where we actually have to start looking. The rest is simple: we determine the interesting start in m_startIndex using the m_endIndex and iterate through it until the elements in that sorted list have a start time that is larger than the query-end-range .. there will no more elements coming. The above uses the fact that a QMap::constIterator iterates the map in sorted order. **/ DB::ImageCount XMLDB::XMLImageDateCollection::count(const DB::ImageDate &range) { if (m_cache.contains(range)) return m_cache[range]; int exact = 0, rangeMatch = 0; // We start searching in ranges that overlap our start search range, i.e. // where the end-date is higher than our search start. EndIndexMap::Iterator endSearch = m_endIndex.lowerBound(range.start()); if (endSearch != m_endIndex.end()) { for (StartIndexMap::ConstIterator it = endSearch.value(); it != m_startIndex.constEnd() && it.key() < range.end(); ++it) { DB::ImageDate::MatchType tp = it.value().isIncludedIn(range); switch (tp) { case DB::ImageDate::ExactMatch: exact++; break; case DB::ImageDate::RangeMatch: rangeMatch++; break; case DB::ImageDate::DontMatch: break; } } } DB::ImageCount res(exact, rangeMatch); m_cache.insert(range, res); // TODO(hzeller) this might go now return res; } QDateTime XMLDB::XMLImageDateCollection::lowerLimit() const { if (!m_startIndex.empty()) { // skip null dates: for (StartIndexMap::ConstIterator it = m_startIndex.constBegin(); it != m_startIndex.constEnd(); ++it) { if (it.key().isValid()) return it.key(); } } return QDateTime(QDate(1900, 1, 1)); } QDateTime XMLDB::XMLImageDateCollection::upperLimit() const { if (!m_endIndex.empty()) { EndIndexMap::ConstIterator highest = m_endIndex.constEnd(); --highest; return highest.key(); } return QDateTime(QDate(2100, 1, 1)); } XMLDB::XMLImageDateCollection::XMLImageDateCollection(const DB::FileNameList &list) { Q_FOREACH (const DB::FileName &fileName, list) { add(fileName.info()->date()); } buildIndex(); } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XMLImageDateCollection.h b/XMLDB/XMLImageDateCollection.h index defe09f8..b3ec30c6 100644 --- a/XMLDB/XMLImageDateCollection.h +++ b/XMLDB/XMLImageDateCollection.h @@ -1,71 +1,72 @@ /* Copyright (C) 2003-2010 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. */ #ifndef XMLIMAGEDATECOLLECTION_H #define XMLIMAGEDATECOLLECTION_H -#include "DB/ImageDateCollection.h" +#include <DB/ImageDateCollection.h> + #include <QMap> namespace DB { class FileNameList; } namespace XMLDB { class XMLImageDateCollection : public DB::ImageDateCollection { public: explicit XMLImageDateCollection(const DB::FileNameList &); public: DB::ImageCount count(const DB::ImageDate &range) override; QDateTime lowerLimit() const override; QDateTime upperLimit() const override; private: typedef QMap<QDateTime, DB::ImageDate> StartIndexMap; typedef QMap<QDateTime, StartIndexMap::ConstIterator> EndIndexMap; void add(const DB::ImageDate &); // Build index, after all elements have been added. void buildIndex(); // Cache for past successful range lookups. QMap<DB::ImageDate, DB::ImageCount> m_cache; // Elements ordered by start time. // // Start index is sorted by start time of the ImageDate, mapping // to the actual ImageDate; this is a multimap. StartIndexMap m_startIndex; // Pointers to start index ordered by end time. // // This maps the end date to an iterator into the startIndex. The // iterator points to the lowest element in startIndex whose end-time // is greater or equal to the key-time. Thus is points to the start // where its worth looking. EndIndexMap m_endIndex; }; } #endif /* XMLIMAGEDATECOLLECTION_H */ // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/XMLDB/XmlReader.cpp b/XMLDB/XmlReader.cpp index 468c3153..d43c296f 100644 --- a/XMLDB/XmlReader.cpp +++ b/XMLDB/XmlReader.cpp @@ -1,126 +1,127 @@ /* Copyright (C) 2013-2019 The KPhotoAlbum Development Team 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 "XmlReader.h" #include <DB/UIDelegate.h> + #include <KLocalizedString> namespace XMLDB { XmlReader::XmlReader(DB::UIDelegate &ui, const QString &friendlyStreamName) : m_ui(ui) , m_streamName(friendlyStreamName) { } QString XmlReader::attribute(const QString &name, const QString &defaultValue) { QStringRef ref = attributes().value(name); if (ref.isNull()) return defaultValue; else return ref.toString(); } ElementInfo XmlReader::readNextStartOrStopElement(const QString &expectedStart) { if (m_peek.isValid) { m_peek.isValid = false; return m_peek; } TokenType type = readNextInternal(); if (hasError()) reportError(i18n("Error reading next element")); if (type != StartElement && type != EndElement) reportError(i18n("Expected to read a start or stop element, but read %1", tokenString())); const QString elementName = name().toString(); if (type == StartElement) { if (!expectedStart.isNull() && elementName != expectedStart) reportError(i18n("Expected to read %1, but read %2", expectedStart, elementName)); } return ElementInfo(type == StartElement, elementName); } void XmlReader::readEndElement(bool readNextElement) { if (readNextElement) readNextInternal(); if (tokenType() != EndElement) reportError(i18n("Expected to read an end element but read %1", tokenString())); } bool XmlReader::hasAttribute(const QString &name) { return attributes().hasAttribute(name); } ElementInfo XmlReader::peekNext() { if (m_peek.isValid) return m_peek; m_peek = readNextStartOrStopElement(QString()); return m_peek; } void XmlReader::complainStartElementExpected(const QString &name) { reportError(i18n("Expected to read start element '%1'", name)); } void XmlReader::reportError(const QString &text) { QString message = i18n( "<p>An error was encountered on line %1, column %2:<nl/>" "<message>%3</message></p>", lineNumber(), columnNumber(), text); if (hasError()) message += i18n("<p>Additional error information:<nl/><message>%1</message></p>", errorString()); message += xi18n("<p>Database path: <filename>%1</filename></p>", m_streamName); m_ui.error(QString::fromUtf8("XmlReader: error in line %1, column %2 (%3)") .arg(lineNumber()) .arg(columnNumber()) .arg(errorString()), message, i18n("Error while reading database file")); exit(-1); } QXmlStreamReader::TokenType XmlReader::readNextInternal() { forever { TokenType type = readNext(); if (type == Comment || type == StartDocument) continue; else if (type == Characters) { if (isWhitespace()) continue; } else return type; } } } // vi:expandtab:tabstop=4 shiftwidth=4: