diff --git a/app/gvcore.cpp b/app/gvcore.cpp index 2b15a020..8a3b20cb 100644 --- a/app/gvcore.cpp +++ b/app/gvcore.cpp @@ -1,532 +1,532 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "gvcore.h" #include "dialogguard.h" // Qt #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include // Local #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Gwenview { struct GvCorePrivate { GvCore* q; MainWindow* mMainWindow; SortedDirModel* mDirModel; HistoryModel* mRecentFoldersModel; RecentFilesModel* mRecentFilesModel; QPalette mPalettes[4]; QString mFullScreenPaletteName; int configFileJPEGQualityValue; bool showSaveAsDialog(const QUrl &url, QUrl* outUrl, QByteArray* format) { // Build the JPEG quality chooser custom widget QWidget* JPEGQualityChooserWidget = new QWidget; JPEGQualityChooserWidget->setVisible(false); // shown only for JPEGs QLabel* JPEGQualityChooserLabel = new QLabel; JPEGQualityChooserLabel->setText(i18n("JPEG quality:")); JPEGQualityChooserLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); QSpinBox* JPEGQualityChooserSpinBox = new QSpinBox; JPEGQualityChooserSpinBox->setMinimum(1); JPEGQualityChooserSpinBox->setMaximum(100); JPEGQualityChooserSpinBox->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); JPEGQualityChooserSpinBox->setSuffix(i18nc("Spinbox suffix; percentage 1 - 100", "%")); configFileJPEGQualityValue = GwenviewConfig::jPEGQuality(); JPEGQualityChooserSpinBox->setValue(configFileJPEGQualityValue); // Temporarily change JPEG quality value QObject::connect(JPEGQualityChooserSpinBox, QOverload::of(&QSpinBox::valueChanged), JPEGQualityChooserSpinBox, [=](int value) { GwenviewConfig::setJPEGQuality(value); }); QSpacerItem* horizontalSpacer = new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Fixed); QHBoxLayout* JPEGQualityChooserLayout = new QHBoxLayout(JPEGQualityChooserWidget); JPEGQualityChooserLayout->setContentsMargins(0,0,0,0); JPEGQualityChooserLayout->addWidget(JPEGQualityChooserLabel); JPEGQualityChooserLayout->addWidget(JPEGQualityChooserSpinBox); JPEGQualityChooserLayout->addItem(horizontalSpacer); // Set up the dialog DialogGuard dialog(mMainWindow); KFileWidget* fileWidget = dialog->fileWidget(); dialog->setCustomWidget(JPEGQualityChooserWidget); dialog->setOperationMode(KFileWidget::Saving); dialog->setWindowTitle(i18nc("@title:window", "Save Image")); // Temporary workaround for selectUrl() not setting the // initial directory to url (removed in D4193) dialog->setUrl(url.adjusted(QUrl::RemoveFilename)); fileWidget->setSelectedUrl(url); QStringList supportedMimetypes; for (const QByteArray &mimeName : QImageWriter::supportedMimeTypes()) { supportedMimetypes.append(QString::fromLocal8Bit(mimeName)); } fileWidget->setMimeFilter(supportedMimetypes, MimeTypeUtils::urlMimeType(url)); // Only show the JPEG quality chooser when saving a JPEG image QObject::connect(fileWidget, &KFileWidget::filterChanged, JPEGQualityChooserWidget, [=](const QString &filter) { - JPEGQualityChooserWidget->setVisible(filter.contains(QStringLiteral("jpeg"))); + JPEGQualityChooserWidget->setVisible(filter.contains(QLatin1String("jpeg"))); }); // Show dialog do { if (!dialog->exec()) { return false; } QList files = fileWidget->selectedUrls(); if (files.isEmpty()) { return false; } QString filename = files.first().fileName(); const QMimeType mimeType = QMimeDatabase().mimeTypeForFile(filename, QMimeDatabase::MatchExtension); if (mimeType.isValid()) { *format = mimeType.preferredSuffix().toLocal8Bit(); break; } KMessageBox::sorry( mMainWindow, i18nc("@info", "Gwenview cannot save images as %1.", QFileInfo(filename).suffix()) ); } while (true); *outUrl = fileWidget->selectedUrls().first(); return true; } void setupPalettes() { QPalette pal; int value = GwenviewConfig::viewBackgroundValue(); QColor fgColor = value > 128 ? Qt::black : Qt::white; // Normal KSharedConfigPtr config = KSharedConfig::openConfig(); mPalettes[GvCore::NormalPalette] = KColorScheme::createApplicationPalette(config); pal = mPalettes[GvCore::NormalPalette]; pal.setColor(QPalette::Base, QColor::fromHsv(0, 0, value)); pal.setColor(QPalette::Text, fgColor); mPalettes[GvCore::NormalViewPalette] = pal; // Fullscreen QString name = GwenviewConfig::fullScreenColorScheme(); if (name.isEmpty()) { // Default color scheme mFullScreenPaletteName = QStandardPaths::locate(QStandardPaths::AppDataLocation, "color-schemes/fullscreen.colors"); config = KSharedConfig::openConfig(mFullScreenPaletteName); } else if (name.contains('/')) { // Full path to a .colors file mFullScreenPaletteName = name; config = KSharedConfig::openConfig(mFullScreenPaletteName); } else { // Standard KDE color scheme mFullScreenPaletteName = QStringLiteral("color-schemes/%1.colors").arg(name); config = KSharedConfig::openConfig(mFullScreenPaletteName, KConfig::FullConfig, QStandardPaths::AppDataLocation); } mPalettes[GvCore::FullScreenPalette] = KColorScheme::createApplicationPalette(config); // If we are using the default palette, adjust it to match the system color scheme if (name.isEmpty()) { adjustDefaultFullScreenPalette(); } // FullScreenView has textured background pal = mPalettes[GvCore::FullScreenPalette]; QString path = QStandardPaths::locate(QStandardPaths::AppDataLocation, "images/background.png"); QPixmap bgTexture(path); pal.setBrush(QPalette::Base, bgTexture); mPalettes[GvCore::FullScreenViewPalette] = pal; } void adjustDefaultFullScreenPalette() { // The Fullscreen palette by default does not use the system color scheme, and therefore uses an 'accent' color // of blue. So for every color group/role combination that uses the accent color, we use a muted version of the // Normal palette. We also use the normal HighlightedText color so it properly contrasts with Highlight. const QPalette normalPal = mPalettes[GvCore::NormalPalette]; QPalette fullscreenPal = mPalettes[GvCore::FullScreenPalette]; // Colors from the normal palette (source of the system theme's accent color) const QColor normalToolTipBase = normalPal.color(QPalette::Normal, QPalette::ToolTipBase); const QColor normalToolTipText = normalPal.color(QPalette::Normal, QPalette::ToolTipText); const QColor normalHighlight = normalPal.color(QPalette::Normal, QPalette::Highlight); const QColor normalHighlightedText = normalPal.color(QPalette::Normal, QPalette::HighlightedText); const QColor normalLink = normalPal.color(QPalette::Normal, QPalette::Link); const QColor normalActiveToolTipBase = normalPal.color(QPalette::Active, QPalette::ToolTipBase); const QColor normalActiveToolTipText = normalPal.color(QPalette::Active, QPalette::ToolTipText); const QColor normalActiveHighlight = normalPal.color(QPalette::Active, QPalette::Highlight); const QColor normalActiveHighlightedText = normalPal.color(QPalette::Active, QPalette::HighlightedText); const QColor normalActiveLink = normalPal.color(QPalette::Active, QPalette::Link); const QColor normalDisabledToolTipBase = normalPal.color(QPalette::Disabled, QPalette::ToolTipBase); const QColor normalDisabledToolTipText = normalPal.color(QPalette::Disabled, QPalette::ToolTipText); // Note: Disabled Highlight missing as they do not use the accent color const QColor normalDisabledLink = normalPal.color(QPalette::Disabled, QPalette::Link); const QColor normalInactiveToolTipBase = normalPal.color(QPalette::Inactive, QPalette::ToolTipBase); const QColor normalInactiveToolTipText = normalPal.color(QPalette::Inactive, QPalette::ToolTipText); const QColor normalInactiveHighlight = normalPal.color(QPalette::Inactive, QPalette::Highlight); const QColor normalInactiveHighlightedText = normalPal.color(QPalette::Inactive, QPalette::HighlightedText); const QColor normalInactiveLink = normalPal.color(QPalette::Inactive, QPalette::Link); // Colors of the fullscreen palette which we will be modifying QColor fullScreenToolTipBase = fullscreenPal.color(QPalette::Normal, QPalette::ToolTipBase); QColor fullScreenToolTipText = fullscreenPal.color(QPalette::Normal, QPalette::ToolTipText); QColor fullScreenHighlight = fullscreenPal.color(QPalette::Normal, QPalette::Highlight); QColor fullScreenLink = fullscreenPal.color(QPalette::Normal, QPalette::Link); QColor fullScreenActiveToolTipBase = fullscreenPal.color(QPalette::Active, QPalette::ToolTipBase); QColor fullScreenActiveToolTipText = fullscreenPal.color(QPalette::Active, QPalette::ToolTipText); QColor fullScreenActiveHighlight = fullscreenPal.color(QPalette::Active, QPalette::Highlight); QColor fullScreenActiveLink = fullscreenPal.color(QPalette::Active, QPalette::Link); QColor fullScreenDisabledToolTipBase = fullscreenPal.color(QPalette::Disabled, QPalette::ToolTipBase); QColor fullScreenDisabledToolTipText = fullscreenPal.color(QPalette::Disabled, QPalette::ToolTipText); QColor fullScreenDisabledLink = fullscreenPal.color(QPalette::Disabled, QPalette::Link); QColor fullScreenInactiveToolTipBase = fullscreenPal.color(QPalette::Inactive, QPalette::ToolTipBase); QColor fullScreenInactiveToolTipText = fullscreenPal.color(QPalette::Inactive, QPalette::ToolTipText); QColor fullScreenInactiveHighlight = fullscreenPal.color(QPalette::Inactive, QPalette::Highlight); QColor fullScreenInactiveLink = fullscreenPal.color(QPalette::Inactive, QPalette::Link); // Adjust the value of the normal color so it's not too dark/bright, and apply to the respective fullscreen color fullScreenToolTipBase .setHsv(normalToolTipBase.hue(), normalToolTipBase.saturation(), (127 + 2 * normalToolTipBase.value()) / 3); fullScreenToolTipText .setHsv(normalToolTipText.hue(), normalToolTipText.saturation(), (127 + 2 * normalToolTipText.value()) / 3); fullScreenHighlight .setHsv(normalHighlight.hue(), normalHighlight.saturation(), (127 + 2 * normalHighlight.value()) / 3); fullScreenLink .setHsv(normalLink.hue(), normalLink.saturation(), (127 + 2 * normalLink.value()) / 3); fullScreenActiveToolTipBase .setHsv(normalActiveToolTipBase.hue(), normalActiveToolTipBase.saturation(), (127 + 2 * normalActiveToolTipBase.value()) / 3); fullScreenActiveToolTipText .setHsv(normalActiveToolTipText.hue(), normalActiveToolTipText.saturation(), (127 + 2 * normalActiveToolTipText.value()) / 3); fullScreenActiveHighlight .setHsv(normalActiveHighlight.hue(), normalActiveHighlight.saturation(), (127 + 2 * normalActiveHighlight.value()) / 3); fullScreenActiveLink .setHsv(normalActiveLink.hue(), normalActiveLink.saturation(), (127 + 2 * normalActiveLink.value()) / 3); fullScreenDisabledToolTipBase.setHsv(normalDisabledToolTipBase.hue(), normalDisabledToolTipBase.saturation(), (127 + 2 * normalDisabledToolTipBase.value()) / 3); fullScreenDisabledToolTipText.setHsv(normalDisabledToolTipText.hue(), normalDisabledToolTipText.saturation(), (127 + 2 * normalDisabledToolTipText.value()) / 3); fullScreenDisabledLink .setHsv(normalDisabledLink.hue(), normalDisabledLink.saturation(), (127 + 2 * normalDisabledLink.value()) / 3); fullScreenInactiveToolTipBase.setHsv(normalInactiveToolTipBase.hue(), normalInactiveToolTipBase.saturation(), (127 + 2 * normalInactiveToolTipBase.value()) / 3); fullScreenInactiveToolTipText.setHsv(normalInactiveToolTipText.hue(), normalInactiveToolTipText.saturation(), (127 + 2 * normalInactiveToolTipText.value()) / 3); fullScreenInactiveHighlight .setHsv(normalInactiveHighlight.hue(), normalInactiveHighlight.saturation(), (127 + 2 * normalInactiveHighlight.value()) / 3); fullScreenInactiveLink .setHsv(normalInactiveLink.hue(), normalInactiveLink.saturation(), (127 + 2 * normalInactiveLink.value()) / 3); // Apply the modified colors to the fullscreen palette fullscreenPal.setColor(QPalette::Normal, QPalette::ToolTipBase, fullScreenToolTipBase); fullscreenPal.setColor(QPalette::Normal, QPalette::ToolTipText, fullScreenToolTipText); fullscreenPal.setColor(QPalette::Normal, QPalette::Highlight, fullScreenHighlight); fullscreenPal.setColor(QPalette::Normal, QPalette::Link, fullScreenLink); fullscreenPal.setColor(QPalette::Active, QPalette::ToolTipBase, fullScreenActiveToolTipBase); fullscreenPal.setColor(QPalette::Active, QPalette::ToolTipText, fullScreenActiveToolTipText); fullscreenPal.setColor(QPalette::Active, QPalette::Highlight, fullScreenActiveHighlight); fullscreenPal.setColor(QPalette::Active, QPalette::Link, fullScreenActiveLink); fullscreenPal.setColor(QPalette::Disabled, QPalette::ToolTipBase, fullScreenDisabledToolTipBase); fullscreenPal.setColor(QPalette::Disabled, QPalette::ToolTipText, fullScreenDisabledToolTipText); fullscreenPal.setColor(QPalette::Disabled, QPalette::Link, fullScreenDisabledLink); fullscreenPal.setColor(QPalette::Inactive, QPalette::ToolTipBase, fullScreenInactiveToolTipBase); fullscreenPal.setColor(QPalette::Inactive, QPalette::ToolTipText, fullScreenInactiveToolTipText); fullscreenPal.setColor(QPalette::Inactive, QPalette::Highlight, fullScreenInactiveHighlight); fullscreenPal.setColor(QPalette::Inactive, QPalette::Link, fullScreenInactiveLink); // Since we use an adjusted version of the normal highlight color, we need to use the normal version of the // text color so it contrasts fullscreenPal.setColor(QPalette::Normal, QPalette::HighlightedText, normalHighlightedText); fullscreenPal.setColor(QPalette::Active, QPalette::HighlightedText, normalActiveHighlightedText); fullscreenPal.setColor(QPalette::Inactive, QPalette::HighlightedText, normalInactiveHighlightedText); mPalettes[GvCore::FullScreenPalette] = fullscreenPal; } }; GvCore::GvCore(MainWindow* mainWindow, SortedDirModel* dirModel) : QObject(mainWindow) , d(new GvCorePrivate) { d->q = this; d->mMainWindow = mainWindow; d->mDirModel = dirModel; d->mRecentFoldersModel = nullptr; d->mRecentFilesModel = nullptr; d->setupPalettes(); connect(GwenviewConfig::self(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); } GvCore::~GvCore() { delete d; } QAbstractItemModel* GvCore::recentFoldersModel() const { if (!d->mRecentFoldersModel) { d->mRecentFoldersModel = new HistoryModel(const_cast(this), QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/recentfolders/"); } return d->mRecentFoldersModel; } QAbstractItemModel* GvCore::recentFilesModel() const { if (!d->mRecentFilesModel) { d->mRecentFilesModel = new RecentFilesModel(const_cast(this)); } return d->mRecentFilesModel; } AbstractSemanticInfoBackEnd* GvCore::semanticInfoBackEnd() const { return d->mDirModel->semanticInfoBackEnd(); } SortedDirModel* GvCore::sortedDirModel() const { return d->mDirModel; } void GvCore::addUrlToRecentFolders(QUrl url) { if (!GwenviewConfig::historyEnabled()) { return; } if (!url.isValid()) { return; } // For "sftp://localhost", "/" is a different path than "" (bug #312060) if (!url.path().isEmpty() && !url.path().endsWith('/')) { url.setPath(url.path() + '/'); } recentFoldersModel(); d->mRecentFoldersModel->addUrl(url); } void GvCore::addUrlToRecentFiles(const QUrl &url) { if (!GwenviewConfig::historyEnabled()) { return; } recentFilesModel(); d->mRecentFilesModel->addUrl(url); } void GvCore::saveAll() { SaveAllHelper helper(d->mMainWindow); helper.save(); } void GvCore::save(const QUrl &url) { Document::Ptr doc = DocumentFactory::instance()->load(url); QByteArray format = doc->format(); const QByteArrayList availableTypes = QImageWriter::supportedImageFormats(); if (availableTypes.contains(format)) { DocumentJob* job = doc->save(url, format); connect(job, SIGNAL(result(KJob*)), SLOT(slotSaveResult(KJob*))); } else { // We don't know how to save in 'format', ask the user for a format we can // write to. KGuiItem saveUsingAnotherFormat = KStandardGuiItem::saveAs(); saveUsingAnotherFormat.setText(i18n("Save using another format")); int result = KMessageBox::warningContinueCancel( d->mMainWindow, i18n("Gwenview cannot save images in '%1' format.", QString(format)), QString() /* caption */, saveUsingAnotherFormat ); if (result == KMessageBox::Continue) { saveAs(url); } } } void GvCore::saveAs(const QUrl &url) { QByteArray format; QUrl saveAsUrl; if (!d->showSaveAsDialog(url, &saveAsUrl, &format)) { return; } if (format == "jpg") { // Gwenview code assumes JPEG images have "jpeg" format, so if the // dialog returned the format "jpg", use "jpeg" instead // This does not affect the actual filename extension format = "jpeg"; } // Start save Document::Ptr doc = DocumentFactory::instance()->load(url); KJob* job = doc->save(saveAsUrl, format); if (!job) { const QString name = saveAsUrl.fileName().isEmpty() ? saveAsUrl.toDisplayString() : saveAsUrl.fileName(); const QString msg = xi18nc("@info", "Saving %1 failed:%2", name, doc->errorString()); KMessageBox::sorry(QApplication::activeWindow(), msg); } else { connect(job, SIGNAL(result(KJob*)), SLOT(slotSaveResult(KJob*))); } } static void applyTransform(const QUrl &url, Orientation orientation) { TransformImageOperation* op = new TransformImageOperation(orientation); Document::Ptr doc = DocumentFactory::instance()->load(url); op->applyToDocument(doc); } void GvCore::slotSaveResult(KJob* _job) { // Regardless of job result, reset JPEG config value if it was changed by // the Save As dialog if (GwenviewConfig::jPEGQuality() != d->configFileJPEGQualityValue) { GwenviewConfig::setJPEGQuality(d->configFileJPEGQualityValue); } SaveJob* job = static_cast(_job); QUrl oldUrl = job->oldUrl(); QUrl newUrl = job->newUrl(); if (job->error()) { QString name = newUrl.fileName().isEmpty() ? newUrl.toDisplayString() : newUrl.fileName(); const QString msg = xi18nc("@info", "Saving %1 failed:%2", name, kxi18n(qPrintable(job->errorString()))); int result = KMessageBox::warningContinueCancel( d->mMainWindow, msg, QString() /* caption */, KStandardGuiItem::saveAs()); if (result == KMessageBox::Continue) { saveAs(oldUrl); } return; } if (oldUrl != newUrl) { d->mMainWindow->goToUrl(newUrl); ViewMainPage* page = d->mMainWindow->viewMainPage(); if (page->isVisible()) { HudMessageBubble* bubble = new HudMessageBubble(); bubble->setText(i18n("You are now viewing the new document.")); KGuiItem item = KStandardGuiItem::back(); item.setText(i18n("Go back to the original")); HudButton* button = bubble->addButton(item); BinderRef::bind(button, SIGNAL(clicked()), d->mMainWindow, &MainWindow::goToUrl, oldUrl); connect(button, SIGNAL(clicked()), bubble, SLOT(deleteLater())); page->showMessageWidget(bubble); } } } void GvCore::rotateLeft(const QUrl &url) { applyTransform(url, ROT_270); } void GvCore::rotateRight(const QUrl &url) { applyTransform(url, ROT_90); } void GvCore::setRating(const QUrl &url, int rating) { QModelIndex index = d->mDirModel->indexForUrl(url); if (!index.isValid()) { qWarning() << "invalid index!"; return; } d->mDirModel->setData(index, rating, SemanticInfoDirModel::RatingRole); } static void clearModel(QAbstractItemModel* model) { model->removeRows(0, model->rowCount()); } void GvCore::clearRecentFilesAndFolders() { clearModel(recentFilesModel()); clearModel(recentFoldersModel()); } void GvCore::slotConfigChanged() { if (!GwenviewConfig::historyEnabled()) { clearRecentFilesAndFolders(); } d->setupPalettes(); } QPalette GvCore::palette(GvCore::PaletteType type) const { return d->mPalettes[type]; } QString GvCore::fullScreenPaletteName() const { return d->mFullScreenPaletteName; } } // namespace diff --git a/app/infocontextmanageritem.cpp b/app/infocontextmanageritem.cpp index 5a4295a5..aee1f802 100644 --- a/app/infocontextmanageritem.cpp +++ b/app/infocontextmanageritem.cpp @@ -1,384 +1,384 @@ /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "infocontextmanageritem.h" // Qt #include #include #include #include #include // KDE #include #include // Local #include "imagemetainfodialog.h" #include "sidebar.h" #include #include #include #include #include #include #include #include namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) qDebug() << x #else #define LOG(x) ; #endif /** * This widget is capable of showing multiple lines of key/value pairs. */ class KeyValueWidget : public QWidget { struct Row { Row(QWidget* parent) : keyLabel(new QLabel(parent)) , valueLabel(new QLabel(parent)) { initLabel(keyLabel); initLabel(valueLabel); QPalette pal = keyLabel->palette(); QColor color = pal.color(QPalette::WindowText); color.setAlphaF(0.65); pal.setColor(QPalette::WindowText, color); keyLabel->setPalette(pal); valueLabel->setContentsMargins(6, 0, 0, 6); } ~Row() { delete keyLabel; delete valueLabel; } int setLabelGeometries(int rowY, int labelWidth) { int labelHeight = keyLabel->heightForWidth(labelWidth); keyLabel->setGeometry(0, rowY, labelWidth, labelHeight); rowY += labelHeight; labelHeight = valueLabel->heightForWidth(labelWidth); valueLabel->setGeometry(0, rowY, labelWidth, labelHeight); rowY += labelHeight; return rowY; } int heightForWidth(int width) const { return keyLabel->heightForWidth(width) + valueLabel->heightForWidth(width); } static void initLabel(QLabel* label) { label->setWordWrap(true); label->show(); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse); } QLabel* keyLabel; QLabel* valueLabel; }; public: explicit KeyValueWidget(QWidget* parent = nullptr) : QWidget(parent) { QSizePolicy policy(QSizePolicy::Preferred, QSizePolicy::Fixed); policy.setHeightForWidth(true); setSizePolicy(policy); } QSize sizeHint() const override { int width = 150; int height = heightForWidth(width); return QSize(width, height); } int heightForWidth(int w) const override { int height = 0; for (Row* row : qAsConst(mRows)) { height += row->heightForWidth(w); } return height; } void clear() { qDeleteAll(mRows); mRows.clear(); updateGeometry(); } void addRow(const QString& key, const QString& value) { Row* row = new Row(this); row->keyLabel->setText(i18nc( "@item:intext %1 is a key, we append a colon to it. A value is displayed after", "%1:", key)); row->valueLabel->setText(value); mRows << row; } static bool rowsLessThan(const Row* row1, const Row* row2) { return row1->keyLabel->text() < row2->keyLabel->text(); } void finishAddRows() { std::sort(mRows.begin(), mRows.end(), KeyValueWidget::rowsLessThan); updateGeometry(); } void layoutRows() { // Layout labels manually: I tried to use a QVBoxLayout but for some // reason when using software animations the widget blinks when going // from one image to another int rowY = 0; const int labelWidth = width(); for (Row* row : qAsConst(mRows)) { rowY = row->setLabelGeometries(rowY, labelWidth); } } protected: void showEvent(QShowEvent* event) override { QWidget::showEvent(event); layoutRows(); } void resizeEvent(QResizeEvent* event) override { QWidget::resizeEvent(event); layoutRows(); } private: QVector mRows; }; struct InfoContextManagerItemPrivate { InfoContextManagerItem* q; SideBarGroup* mGroup; // One selection fields QScrollArea* mOneFileWidget; KeyValueWidget* mKeyValueWidget; Document::Ptr mDocument; // Multiple selection fields QLabel* mMultipleFilesLabel; QPointer mImageMetaInfoDialog; void updateMetaInfoDialog() { if (!mImageMetaInfoDialog) { return; } ImageMetaInfoModel* model = mDocument ? mDocument->metaInfo() : nullptr; mImageMetaInfoDialog->setMetaInfo(model, GwenviewConfig::preferredMetaInfoKeyList()); } void setupGroup() { mOneFileWidget = new QScrollArea(); mOneFileWidget->setFrameStyle(QFrame::NoFrame); mOneFileWidget->setWidgetResizable(true); mKeyValueWidget = new KeyValueWidget; QLabel* moreLabel = new QLabel(mOneFileWidget); moreLabel->setText(QStringLiteral("%1").arg(i18nc("@action show more image meta info", "More..."))); moreLabel->setAlignment(Qt::AlignRight); QWidget* content = new QWidget; QVBoxLayout* layout = new QVBoxLayout(content); layout->setMargin(2); layout->setSpacing(2); layout->addWidget(mKeyValueWidget); layout->addWidget(moreLabel); mOneFileWidget->setWidget(content); mMultipleFilesLabel = new QLabel(); mGroup = new SideBarGroup(i18nc("@title:group", "Meta Information")); q->setWidget(mGroup); mGroup->addWidget(mOneFileWidget); mGroup->addWidget(mMultipleFilesLabel); EventWatcher::install(mGroup, QEvent::Show, q, SLOT(updateSideBarContent())); QObject::connect(moreLabel, &QLabel::linkActivated, q, &InfoContextManagerItem::showMetaInfoDialog); } void forgetCurrentDocument() { if (mDocument) { QObject::disconnect(mDocument.data(), nullptr, q, nullptr); // "Garbage collect" document mDocument = nullptr; } } }; InfoContextManagerItem::InfoContextManagerItem(ContextManager* manager) : AbstractContextManagerItem(manager) , d(new InfoContextManagerItemPrivate) { d->q = this; d->setupGroup(); connect(contextManager(), &ContextManager::selectionChanged, this, &InfoContextManagerItem::updateSideBarContent); connect(contextManager(), &ContextManager::selectionDataChanged, this, &InfoContextManagerItem::updateSideBarContent); } InfoContextManagerItem::~InfoContextManagerItem() { delete d; } void InfoContextManagerItem::updateSideBarContent() { LOG("updateSideBarContent"); if (!d->mGroup->isVisible()) { LOG("updateSideBarContent: not visible, not updating"); return; } LOG("updateSideBarContent: really updating"); KFileItemList itemList = contextManager()->selectedFileItemList(); - if (itemList.count() == 0) { + if (itemList.isEmpty()) { d->forgetCurrentDocument(); d->mOneFileWidget->hide(); d->mMultipleFilesLabel->hide(); d->updateMetaInfoDialog(); return; } KFileItem item = itemList.first(); if (itemList.count() == 1 && !ArchiveUtils::fileItemIsDirOrArchive(item)) { fillOneFileGroup(item); } else { fillMultipleItemsGroup(itemList); } d->updateMetaInfoDialog(); } void InfoContextManagerItem::fillOneFileGroup(const KFileItem& item) { d->mOneFileWidget->show(); d->mMultipleFilesLabel->hide(); d->forgetCurrentDocument(); d->mDocument = DocumentFactory::instance()->load(item.url()); connect(d->mDocument.data(), &Document::metaInfoUpdated, this, &InfoContextManagerItem::updateOneFileInfo); d->updateMetaInfoDialog(); updateOneFileInfo(); } void InfoContextManagerItem::fillMultipleItemsGroup(const KFileItemList& itemList) { d->forgetCurrentDocument(); int folderCount = 0, fileCount = 0; for (const KFileItem & item : itemList) { if (item.isDir()) { folderCount++; } else { fileCount++; } } if (folderCount == 0) { d->mMultipleFilesLabel->setText(i18ncp("@label", "%1 file selected", "%1 files selected", fileCount)); } else if (fileCount == 0) { d->mMultipleFilesLabel->setText(i18ncp("@label", "%1 folder selected", "%1 folders selected", folderCount)); } else { d->mMultipleFilesLabel->setText(i18nc("@label. The two parameters are strings like '2 folders' and '1 file'.", "%1 and %2 selected", i18np("%1 folder", "%1 folders", folderCount), i18np("%1 file", "%1 files", fileCount))); } d->mOneFileWidget->hide(); d->mMultipleFilesLabel->show(); } void InfoContextManagerItem::updateOneFileInfo() { if (!d->mDocument) { return; } ImageMetaInfoModel* metaInfoModel = d->mDocument->metaInfo(); d->mKeyValueWidget->clear(); const QStringList preferredMetaInfoKeyList = GwenviewConfig::preferredMetaInfoKeyList(); for (const QString & key : preferredMetaInfoKeyList) { QString label; QString value; metaInfoModel->getInfoForKey(key, &label, &value); if (!label.isEmpty() && !value.isEmpty()) { d->mKeyValueWidget->addRow(label, value); } } d->mKeyValueWidget->finishAddRows(); d->mKeyValueWidget->layoutRows(); } void InfoContextManagerItem::showMetaInfoDialog() { if (!d->mImageMetaInfoDialog) { d->mImageMetaInfoDialog = new ImageMetaInfoDialog(d->mOneFileWidget); d->mImageMetaInfoDialog->setAttribute(Qt::WA_DeleteOnClose, true); connect(d->mImageMetaInfoDialog.data(), &ImageMetaInfoDialog::preferredMetaInfoKeyListChanged, this, &InfoContextManagerItem::slotPreferredMetaInfoKeyListChanged); } d->mImageMetaInfoDialog->setMetaInfo(d->mDocument ? d->mDocument->metaInfo() : nullptr, GwenviewConfig::preferredMetaInfoKeyList()); d->mImageMetaInfoDialog->show(); } void InfoContextManagerItem::slotPreferredMetaInfoKeyListChanged(const QStringList& list) { GwenviewConfig::setPreferredMetaInfoKeyList(list); GwenviewConfig::self()->save(); updateOneFileInfo(); } } // namespace diff --git a/importer/main.cpp b/importer/main.cpp index f84adc51..20fb169d 100644 --- a/importer/main.cpp +++ b/importer/main.cpp @@ -1,79 +1,79 @@ /* Gwenview: an image viewer Copyright 2000-2009 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Qt #include #include #include #include #include #include // KDE #include #include #include // Local #include #include "importdialog.h" int main(int argc, char *argv[]) { KLocalizedString::setApplicationDomain("gwenview"); QApplication app(argc, argv); app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); QScopedPointer aboutData( Gwenview::createAboutData( QStringLiteral("org.kde.gwenview"), /* component name */ i18n("Gwenview Importer") /* programName */ )); aboutData->setShortDescription(i18n("Photo Importer")); KAboutData::setApplicationData(*aboutData); QCommandLineParser parser; aboutData.data()->setupCommandLine(&parser); parser.addOption(QCommandLineOption(QStringLiteral("udi"), i18n("The device UDI, used to retrieve information about the device (name, icon...)"), i18n("Device UDI"))); parser.addPositionalArgument("folder", i18n("Source folder")); parser.process(app); aboutData.data()->processCommandLine(&parser); - if (parser.positionalArguments().count() == 0) { + if (parser.positionalArguments().isEmpty()) { qWarning() << i18n("Missing required source folder argument."); parser.showHelp(); } if (parser.positionalArguments().count() > 1) { qWarning() << i18n("Too many arguments."); parser.showHelp(); } QString urlString = parser.positionalArguments().constFirst(); QUrl url = QUrl::fromUserInput(urlString, QDir::currentPath(), QUrl::AssumeLocalFile); if (!url.isValid()) { qCritical() << i18n("Invalid source folder."); return 1; } QString deviceUdi = parser.isSet("udi") ? parser.value("udi") : QString(); Gwenview::ImportDialog* dialog = new Gwenview::ImportDialog(); dialog->show(); QMetaObject::invokeMethod(dialog, "setSourceUrl", Qt::QueuedConnection, Q_ARG(QUrl, url), Q_ARG(QString, deviceUdi)); return app.exec(); } diff --git a/lib/document/documentloadedimpl.cpp b/lib/document/documentloadedimpl.cpp index 126e7908..18945e0e 100644 --- a/lib/document/documentloadedimpl.cpp +++ b/lib/document/documentloadedimpl.cpp @@ -1,128 +1,128 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include "documentloadedimpl.h" // Qt #include #include #include #include #include #include // KDE // Local #include "documentjob.h" #include "gwenviewconfig.h" #include "imageutils.h" #include "savejob.h" namespace Gwenview { struct DocumentLoadedImplPrivate { QByteArray mRawData; bool mQuietInit; }; DocumentLoadedImpl::DocumentLoadedImpl(Document* document, const QByteArray& rawData, bool quietInit) : AbstractDocumentImpl(document) , d(new DocumentLoadedImplPrivate) { if (document->keepRawData()) { d->mRawData = rawData; } d->mQuietInit = quietInit; } DocumentLoadedImpl::~DocumentLoadedImpl() { delete d; } void DocumentLoadedImpl::init() { if (!d->mQuietInit) { emit imageRectUpdated(document()->image().rect()); emit loaded(); } } bool DocumentLoadedImpl::isEditable() const { return true; } Document::LoadingState DocumentLoadedImpl::loadingState() const { return Document::Loaded; } bool DocumentLoadedImpl::saveInternal(QIODevice* device, const QByteArray& format) { QImageWriter writer(device, format); // If we're saving a non-JPEG image as a JPEG, respect the quality setting - if (format == QStringLiteral("jpeg")) { + if (format == QByteArrayLiteral("jpeg")) { writer.setQuality(GwenviewConfig::jPEGQuality()); } bool ok = writer.write(document()->image()); if (ok) { setDocumentFormat(format); } else { setDocumentErrorString(writer.errorString()); } return ok; } DocumentJob* DocumentLoadedImpl::save(const QUrl &url, const QByteArray& format) { return new SaveJob(this, url, format); } AbstractDocumentEditor* DocumentLoadedImpl::editor() { return this; } void DocumentLoadedImpl::setImage(const QImage& image) { setDocumentImage(image); emit imageRectUpdated(image.rect()); } void DocumentLoadedImpl::applyTransformation(Orientation orientation) { QImage image = document()->image(); QTransform matrix = ImageUtils::transformMatrix(orientation); image = image.transformed(matrix); setDocumentImage(image); emit imageRectUpdated(image.rect()); } QByteArray DocumentLoadedImpl::rawData() const { return d->mRawData; } } // namespace diff --git a/lib/imagemetainfomodel.cpp b/lib/imagemetainfomodel.cpp index 699b5665..6c65197b 100644 --- a/lib/imagemetainfomodel.cpp +++ b/lib/imagemetainfomodel.cpp @@ -1,575 +1,575 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include "imagemetainfomodel.h" #include "config-gwenview.h" // Qt #include #include #include // KDE #include #include #include // Exiv2 #include // Local #ifdef HAVE_FITS #include "imageformats/fitsformat/fitsdata.h" #include "urlutils.h" #endif namespace Gwenview { enum GroupRow { GeneralGroup, ExifGroup, #ifdef HAVE_FITS FitsGroup, #endif IptcGroup, XmpGroup, NoGroupSpace, // second last entry NoGroup // last entry }; class MetaInfoGroup { public: enum { InvalidRow = -1 }; class Entry { public: Entry(const QString& key, const QString& label, const QString& value) : mKey(key), mLabel(label.trimmed()), mValue(value.trimmed()) {} QString key() const { return mKey; } QString label() const { return mLabel; } QString value() const { return mValue; } void setValue(const QString& value) { mValue = value.trimmed(); } void appendValue(const QString& value) { if (!mValue.isEmpty()) { mValue += QLatin1Char('\n'); } mValue += value.trimmed(); } private: QString mKey; QString mLabel; QString mValue; }; MetaInfoGroup(const QString& label) : mLabel(label) {} ~MetaInfoGroup() { qDeleteAll(mList); } void clear() { qDeleteAll(mList); mList.clear(); mRowForKey.clear(); } void addEntry(const QString& key, const QString& label, const QString& value) { addEntry(new Entry(key, label, value)); } void addEntry(Entry* entry) { mList << entry; mRowForKey[entry->key()] = mList.size() - 1; } void getInfoForKey(const QString& key, QString* label, QString* value) const { Entry* entry = getEntryForKey(key); if (entry) { *label = entry->label(); *value = entry->value(); } } QString getKeyAt(int row) const { Q_ASSERT(row < mList.size()); return mList[row]->key(); } QString getLabelForKeyAt(int row) const { Q_ASSERT(row < mList.size()); return mList[row]->label(); } QString getValueForKeyAt(int row) const { Q_ASSERT(row < mList.size()); return mList[row]->value(); } void setValueForKeyAt(int row, const QString& value) { Q_ASSERT(row < mList.size()); mList[row]->setValue(value); } int getRowForKey(const QString& key) const { return mRowForKey.value(key, InvalidRow); } int size() const { return mList.size(); } QString label() const { return mLabel; } const QList& entryList() const { return mList; } private: Entry* getEntryForKey(const QString& key) const { int row = getRowForKey(key); if (row == InvalidRow) { return nullptr; } return mList[row]; } QList mList; QHash mRowForKey; QString mLabel; }; struct ImageMetaInfoModelPrivate { QVector mMetaInfoGroupVector; ImageMetaInfoModel* q; void clearGroup(MetaInfoGroup* group, const QModelIndex& parent) { if (group->size() > 0) { q->beginRemoveRows(parent, 0, group->size() - 1); group->clear(); q->endRemoveRows(); } } void setGroupEntryValue(GroupRow groupRow, const QString& key, const QString& value) { MetaInfoGroup* group = mMetaInfoGroupVector[groupRow]; int entryRow = group->getRowForKey(key); if (entryRow == MetaInfoGroup::InvalidRow) { qWarning() << "No row for key" << key; return; } group->setValueForKeyAt(entryRow, value); QModelIndex groupIndex = q->index(groupRow, 0); QModelIndex entryIndex = q->index(entryRow, 1, groupIndex); emit q->dataChanged(entryIndex, entryIndex); } QVariant displayData(const QModelIndex& index) const { if (index.internalId() == NoGroup) { if (index.column() != 0) { return QVariant(); } QString label = mMetaInfoGroupVector[index.row()]->label(); return QVariant(label); } if (index.internalId() == NoGroupSpace) { return QString(); } MetaInfoGroup* group = mMetaInfoGroupVector[index.internalId()]; if (index.column() == 0) { return group->getLabelForKeyAt(index.row()); } else { return group->getValueForKeyAt(index.row()); } } void initGeneralGroup() { MetaInfoGroup* group = mMetaInfoGroupVector[GeneralGroup]; group->addEntry(QStringLiteral("General.Name"), i18nc("@item:intable Image file name", "Name"), QString()); group->addEntry(QStringLiteral("General.Size"), i18nc("@item:intable", "File Size"), QString()); group->addEntry(QStringLiteral("General.Time"), i18nc("@item:intable", "File Time"), QString()); group->addEntry(QStringLiteral("General.ImageSize"), i18nc("@item:intable", "Image Size"), QString()); group->addEntry(QStringLiteral("General.Comment"), i18nc("@item:intable", "Comment"), QString()); } template void fillExivGroup(const QModelIndex& parent, MetaInfoGroup* group, const Container& container) { // key aren't always unique (for example, "Iptc.Application2.Keywords" // may appear multiple times) so we can't know how many rows we will // insert before going through them. That's why we create a hash // before. typedef QHash EntryHash; EntryHash hash; Iterator it = container.begin(), end = container.end(); for (; it != end; ++it) { try { // Skip metadatum if its tag is an hex number if (it->tagName().substr(0, 2) == "0x") { continue; } QString key = QString::fromUtf8(it->key().c_str()); QString label = QString::fromLocal8Bit(it->tagLabel().c_str()); std::ostringstream stream; stream << *it; QString value = QString::fromLocal8Bit(stream.str().c_str()); EntryHash::iterator hashIt = hash.find(key); if (hashIt != hash.end()) { hashIt.value()->appendValue(value); } else { hash.insert(key, new MetaInfoGroup::Entry(key, label, value)); } } catch (const Exiv2::Error& error) { qWarning() << "Failed to read some meta info:" << error.what(); } } if (hash.isEmpty()) { return; } q->beginInsertRows(parent, 0, hash.size() - 1); for (MetaInfoGroup::Entry * entry : qAsConst(hash)) { group->addEntry(entry); } q->endInsertRows(); } }; ImageMetaInfoModel::ImageMetaInfoModel() : d(new ImageMetaInfoModelPrivate) { d->q = this; #ifdef HAVE_FITS d->mMetaInfoGroupVector.resize(5); #else d->mMetaInfoGroupVector.resize(4); #endif d->mMetaInfoGroupVector[GeneralGroup] = new MetaInfoGroup(i18nc("@title:group General info about the image", "General")); d->mMetaInfoGroupVector[ExifGroup] = new MetaInfoGroup(QStringLiteral("EXIF")); #ifdef HAVE_FITS d->mMetaInfoGroupVector[FitsGroup] = new MetaInfoGroup(QStringLiteral("FITS")); #endif d->mMetaInfoGroupVector[IptcGroup] = new MetaInfoGroup(QStringLiteral("IPTC")); d->mMetaInfoGroupVector[XmpGroup] = new MetaInfoGroup(QStringLiteral("XMP")); d->initGeneralGroup(); } ImageMetaInfoModel::~ImageMetaInfoModel() { qDeleteAll(d->mMetaInfoGroupVector); delete d; } void ImageMetaInfoModel::setUrl(const QUrl &url) { KFileItem item(url); const QString sizeString = KFormat().formatByteSize(item.size()); const QString timeString = QLocale().toString(item.time(KFileItem::ModificationTime), QLocale::LongFormat); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Name"), item.name()); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Size"), sizeString); d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Time"), timeString); #ifdef HAVE_FITS if (UrlUtils::urlIsFastLocalFile(url) && (url.fileName().endsWith(QLatin1String(".fit"), Qt::CaseInsensitive) || url.fileName().endsWith(QLatin1String(".fits"), Qt::CaseInsensitive))) { FITSData fitsLoader; MetaInfoGroup* group = d->mMetaInfoGroupVector[FitsGroup]; QFile file(url.toLocalFile()); if (!file.open(QIODevice::ReadOnly)) { return; } if (fitsLoader.loadFITS(file)) { QString recordList; int nkeys = 0; fitsLoader.getFITSRecord(recordList, nkeys); for (int i = 0; i < nkeys; i++) { QString record = recordList.mid(i * 80, 80); QString key; QString keyStr; QString value; - if (!record.contains(QLatin1String("="))) { + if (!record.contains(QLatin1Char('='))) { key = record.section(QLatin1Char(' '), 0, 0).simplified(); keyStr = key; value = record.section(QLatin1Char(' '), 1, -1).simplified(); } else { key = record.section(QLatin1Char('='), 0, 0).simplified(); if (record.contains(QLatin1Char('/'))) { keyStr = record.section(QLatin1Char('/'), -1, -1).simplified(); value = record.section(QLatin1Char('='), 1, -1).section(QLatin1Char('/'), 0, 0); } else { keyStr = key; value = record.section(QLatin1Char('='), 1, -1); } value.remove(QStringLiteral("\'")); value = value.simplified(); } if (value.isEmpty()) { continue; } // Check if the value is a number and make it more readable bool ok = false; float number = value.toFloat(&ok); if (ok) { value = QString::number(number); } group->addEntry(QStringLiteral("Fits.")+key, keyStr, value); } } } #endif } void ImageMetaInfoModel::setImageSize(const QSize& size) { QString imageSize; if (size.isValid()) { imageSize = i18nc( "@item:intable %1 is image width, %2 is image height", "%1x%2", size.width(), size.height()); double megaPixels = size.width() * size.height() / 1000000.; if (megaPixels > 0.1) { QString megaPixelsString = QString::number(megaPixels, 'f', 1); imageSize += QLatin1Char(' '); imageSize += i18nc( "@item:intable %1 is number of millions of pixels in image", "(%1MP)", megaPixelsString); } } else { imageSize = QLatin1Char('-'); } d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.ImageSize"), imageSize); } void ImageMetaInfoModel::setExiv2Image(const Exiv2::Image* image) { MetaInfoGroup* exifGroup = d->mMetaInfoGroupVector[ExifGroup]; MetaInfoGroup* iptcGroup = d->mMetaInfoGroupVector[IptcGroup]; MetaInfoGroup* xmpGroup = d->mMetaInfoGroupVector[XmpGroup]; QModelIndex exifIndex = index(ExifGroup, 0); QModelIndex iptcIndex = index(IptcGroup, 0); QModelIndex xmpIndex = index(XmpGroup, 0); d->clearGroup(exifGroup, exifIndex); d->clearGroup(iptcGroup, iptcIndex); d->clearGroup(xmpGroup, xmpIndex); if (!image) { return; } d->setGroupEntryValue(GeneralGroup, QStringLiteral("General.Comment"), QString::fromUtf8(image->comment().c_str())); if (image->checkMode(Exiv2::mdExif) & Exiv2::amRead) { const Exiv2::ExifData& exifData = image->exifData(); d->fillExivGroup(exifIndex, exifGroup, exifData); } if (image->checkMode(Exiv2::mdIptc) & Exiv2::amRead) { const Exiv2::IptcData& iptcData = image->iptcData(); d->fillExivGroup(iptcIndex, iptcGroup, iptcData); } if (image->checkMode(Exiv2::mdXmp) & Exiv2::amRead) { const Exiv2::XmpData& xmpData = image->xmpData(); d->fillExivGroup(xmpIndex, xmpGroup, xmpData); } } void ImageMetaInfoModel::getInfoForKey(const QString& key, QString* label, QString* value) const { MetaInfoGroup* group; if (key.startsWith(QLatin1String("General"))) { group = d->mMetaInfoGroupVector[GeneralGroup]; } else if (key.startsWith(QLatin1String("Exif"))) { group = d->mMetaInfoGroupVector[ExifGroup]; #ifdef HAVE_FITS } else if (key.startsWith(QLatin1String("Fits"))) { group = d->mMetaInfoGroupVector[FitsGroup]; #endif } else if (key.startsWith(QLatin1String("Iptc"))) { group = d->mMetaInfoGroupVector[IptcGroup]; } else if (key.startsWith(QLatin1String("Xmp"))) { group = d->mMetaInfoGroupVector[XmpGroup]; } else { qWarning() << "Unknown metainfo key" << key; return; } group->getInfoForKey(key, label, value); } QString ImageMetaInfoModel::getValueForKey(const QString& key) const { QString label, value; getInfoForKey(key, &label, &value); return value; } QString ImageMetaInfoModel::keyForIndex(const QModelIndex& index) const { if (index.internalId() == NoGroup) { return QString(); } MetaInfoGroup* group = d->mMetaInfoGroupVector[index.internalId()]; return group->getKeyAt(index.row()); } QModelIndex ImageMetaInfoModel::index(int row, int col, const QModelIndex& parent) const { if (col < 0 || col > 1) { return QModelIndex(); } if (!parent.isValid()) { // This is a group if (row < 0 || row >= d->mMetaInfoGroupVector.size()) { return QModelIndex(); } return createIndex(row, col, col == 0 ? NoGroup : NoGroupSpace); } else { // This is an entry int group = parent.row(); if (row < 0 || row >= d->mMetaInfoGroupVector[group]->size()) { return QModelIndex(); } return createIndex(row, col, group); } } QModelIndex ImageMetaInfoModel::parent(const QModelIndex& index) const { if (!index.isValid()) { return QModelIndex(); } if (index.internalId() == NoGroup || index.internalId() == NoGroupSpace) { return QModelIndex(); } else { return createIndex(index.internalId(), 0, NoGroup); } } int ImageMetaInfoModel::rowCount(const QModelIndex& parent) const { if (!parent.isValid()) { return d->mMetaInfoGroupVector.size(); } else if (parent.internalId() == NoGroup) { return d->mMetaInfoGroupVector[parent.row()]->size(); } else { return 0; } } int ImageMetaInfoModel::columnCount(const QModelIndex& /*parent*/) const { return 2; } QVariant ImageMetaInfoModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } switch (role) { case Qt::DisplayRole: return d->displayData(index); default: return QVariant(); } } QVariant ImageMetaInfoModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Vertical || role != Qt::DisplayRole) { return QVariant(); } QString caption; if (section == 0) { caption = i18nc("@title:column", "Property"); } else if (section == 1) { caption = i18nc("@title:column", "Value"); } else { qWarning() << "Unknown section" << section; } return QVariant(caption); } } // namespace diff --git a/lib/jpegcontent.cpp b/lib/jpegcontent.cpp index 772d01cc..270ad688 100644 --- a/lib/jpegcontent.cpp +++ b/lib/jpegcontent.cpp @@ -1,714 +1,714 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "jpegcontent.h" // System #include #include #include #include extern "C" { #include #include "transupp.h" } // Qt #include #include #include #include #include #include // KDE #include // Exiv2 #include // Local #include "jpegerrormanager.h" #include "iodevicejpegsourcemanager.h" #include "exiv2imageloader.h" #include "gwenviewconfig.h" #include "imageutils.h" namespace Gwenview { const int INMEM_DST_DELTA = 4096; //----------------------------------------------- // // In-memory data destination manager for libjpeg // //----------------------------------------------- struct inmem_dest_mgr : public jpeg_destination_mgr { QByteArray* mOutput; void dump() { qDebug() << "dest_mgr:\n"; qDebug() << "- next_output_byte: " << next_output_byte; qDebug() << "- free_in_buffer: " << free_in_buffer; qDebug() << "- output size: " << mOutput->size(); } }; void inmem_init_destination(j_compress_ptr cinfo) { inmem_dest_mgr* dest = (inmem_dest_mgr*)(cinfo->dest); if (dest->mOutput->size() == 0) { dest->mOutput->resize(INMEM_DST_DELTA); } dest->free_in_buffer = dest->mOutput->size(); dest->next_output_byte = (JOCTET*)(dest->mOutput->data()); } boolean inmem_empty_output_buffer(j_compress_ptr cinfo) { inmem_dest_mgr* dest = (inmem_dest_mgr*)(cinfo->dest); dest->mOutput->resize(dest->mOutput->size() + INMEM_DST_DELTA); dest->next_output_byte = (JOCTET*)(dest->mOutput->data() + dest->mOutput->size() - INMEM_DST_DELTA); dest->free_in_buffer = INMEM_DST_DELTA; return true; } void inmem_term_destination(j_compress_ptr cinfo) { inmem_dest_mgr* dest = (inmem_dest_mgr*)(cinfo->dest); int finalSize = dest->next_output_byte - (JOCTET*)(dest->mOutput->data()); Q_ASSERT(finalSize >= 0); dest->mOutput->resize(finalSize); } //--------------------- // // JpegContent::Private // //--------------------- struct JpegContent::Private { // JpegContent usually stores the image pixels as compressed JPEG data in // mRawData. However if the image is set with setImage() because the user // performed a lossy image manipulation, mRawData is cleared and the image // pixels are kept in mImage until updateRawDataFromImage() is called. QImage mImage; // Store the input file, keep it open readOnly. This allows the file to be memory mapped // (i.e. mRawData may point to mFile.map()) rather than completely read on load. Postpone // QFile::readAll() as long as possible (currently in save()). QFile mFile; QByteArray mRawData; QSize mSize; QString mComment; bool mPendingTransformation; QTransform mTransformMatrix; Exiv2::ExifData mExifData; QString mErrorString; Private() { mPendingTransformation = false; } void setupInmemDestination(j_compress_ptr cinfo, QByteArray* outputData) { Q_ASSERT(!cinfo->dest); inmem_dest_mgr* dest = (inmem_dest_mgr*) (*cinfo->mem->alloc_small)((j_common_ptr) cinfo, JPOOL_PERMANENT, sizeof(inmem_dest_mgr)); cinfo->dest = (struct jpeg_destination_mgr*)(dest); dest->init_destination = inmem_init_destination; dest->empty_output_buffer = inmem_empty_output_buffer; dest->term_destination = inmem_term_destination; dest->mOutput = outputData; } bool readSize() { struct jpeg_decompress_struct srcinfo; // Init JPEG structs JPEGErrorManager errorManager; // Initialize the JPEG decompression object srcinfo.err = &errorManager; jpeg_create_decompress(&srcinfo); if (setjmp(errorManager.jmp_buffer)) { qCritical() << "libjpeg fatal error\n"; return false; } // Specify data source for decompression QBuffer buffer(&mRawData); buffer.open(QIODevice::ReadOnly); IODeviceJpegSourceManager::setup(&srcinfo, &buffer); // Read the header jcopy_markers_setup(&srcinfo, JCOPYOPT_ALL); int result = jpeg_read_header(&srcinfo, true); if (result != JPEG_HEADER_OK) { qCritical() << "Could not read jpeg header\n"; jpeg_destroy_decompress(&srcinfo); return false; } mSize = QSize(srcinfo.image_width, srcinfo.image_height); jpeg_destroy_decompress(&srcinfo); return true; } bool updateRawDataFromImage() { QBuffer buffer; QImageWriter writer(&buffer, "jpeg"); writer.setQuality(GwenviewConfig::jPEGQuality()); if (!writer.write(mImage)) { mErrorString = writer.errorString(); return false; } mRawData = buffer.data(); mImage = QImage(); return true; } }; //------------ // // JpegContent // //------------ JpegContent::JpegContent() { d = new JpegContent::Private(); } JpegContent::~JpegContent() { delete d; } bool JpegContent::load(const QString& path) { if (d->mFile.isOpen()) { d->mFile.unmap(reinterpret_cast(d->mRawData.data())); d->mFile.close(); d->mRawData.clear(); } d->mFile.setFileName(path); if (!d->mFile.open(QIODevice::ReadOnly)) { qCritical() << "Could not open '" << path << "' for reading\n"; return false; } QByteArray rawData; uchar* mappedFile = d->mFile.map(0, d->mFile.size(), QFileDevice::MapPrivateOption); if (mappedFile == nullptr) { // process' mapping limit exceeded, file is sealed or filesystem doesn't support it, etc. qDebug() << "Could not mmap '" << path << "', falling back to QFile::readAll()\n"; rawData = d->mFile.readAll(); // all read in, no need to keep it open d->mFile.close(); } else { rawData = QByteArray::fromRawData(reinterpret_cast(mappedFile), d->mFile.size()); } return loadFromData(rawData); } bool JpegContent::loadFromData(const QByteArray& data) { std::unique_ptr image; Exiv2ImageLoader loader; if (!loader.load(data)) { qCritical() << "Could not load image with Exiv2, reported error:" << loader.errorMessage(); } image.reset(loader.popImage().release()); return loadFromData(data, image.get()); } bool JpegContent::loadFromData(const QByteArray& data, Exiv2::Image* exiv2Image) { d->mPendingTransformation = false; d->mTransformMatrix.reset(); d->mRawData = data; if (d->mRawData.size() == 0) { qCritical() << "No data\n"; return false; } if (!d->readSize()) return false; d->mExifData = exiv2Image->exifData(); d->mComment = QString::fromUtf8(exiv2Image->comment().c_str()); if (!GwenviewConfig::applyExifOrientation()) { return true; } // Adjust the size according to the orientation switch (orientation()) { case TRANSPOSE: case ROT_90: case TRANSVERSE: case ROT_270: d->mSize.transpose(); break; default: break; } return true; } QByteArray JpegContent::rawData() const { return d->mRawData; } Orientation JpegContent::orientation() const { Exiv2::ExifKey key("Exif.Image.Orientation"); Exiv2::ExifData::iterator it = d->mExifData.findKey(key); // We do the same checks as in libexiv2's src/crwimage.cpp: // http://dev.exiv2.org/projects/exiv2/repository/entry/trunk/src/crwimage.cpp?rev=2681#L1336 if (it == d->mExifData.end() || it->count() == 0 || it->typeId() != Exiv2::unsignedShort) { return NOT_AVAILABLE; } return Orientation(it->toLong()); } int JpegContent::dotsPerMeterX() const { return dotsPerMeter(QStringLiteral("XResolution")); } int JpegContent::dotsPerMeterY() const { return dotsPerMeter(QStringLiteral("YResolution")); } int JpegContent::dotsPerMeter(const QString& keyName) const { Exiv2::ExifKey keyResUnit("Exif.Image.ResolutionUnit"); Exiv2::ExifData::iterator it = d->mExifData.findKey(keyResUnit); if (it == d->mExifData.end()) { return 0; } int res = it->toLong(); - QString keyVal = QStringLiteral("Exif.Image.") + keyName; + QString keyVal = QLatin1String("Exif.Image.") + keyName; Exiv2::ExifKey keyResolution(keyVal.toLocal8Bit().data()); it = d->mExifData.findKey(keyResolution); if (it == d->mExifData.end()) { return 0; } // The unit for measuring XResolution and YResolution. The same unit is used for both XResolution and YResolution. // If the image resolution in unknown, 2 (inches) is designated. // Default = 2 // 2 = inches // 3 = centimeters // Other = reserved const float INCHESPERMETER = (100. / 2.54); switch (res) { case 3: // dots per cm return int(it->toLong() * 100); default: // dots per inch return int(it->toLong() * INCHESPERMETER); } return 0; } void JpegContent::resetOrientation() { Exiv2::ExifKey key("Exif.Image.Orientation"); Exiv2::ExifData::iterator it = d->mExifData.findKey(key); if (it == d->mExifData.end()) { return; } *it = uint16_t(NORMAL); } QSize JpegContent::size() const { return d->mSize; } QString JpegContent::comment() const { return d->mComment; } void JpegContent::setComment(const QString& comment) { d->mComment = comment; } static QTransform createRotMatrix(int angle) { QTransform matrix; matrix.rotate(angle); return matrix; } static QTransform createScaleMatrix(int dx, int dy) { QTransform matrix; matrix.scale(dx, dy); return matrix; } struct OrientationInfo { OrientationInfo() : orientation(NOT_AVAILABLE) , jxform(JXFORM_NONE) {} OrientationInfo(Orientation o, const QTransform &m, JXFORM_CODE j) : orientation(o), matrix(m), jxform(j) {} Orientation orientation; QTransform matrix; JXFORM_CODE jxform; }; typedef QList OrientationInfoList; static const OrientationInfoList& orientationInfoList() { static OrientationInfoList list; if (list.size() == 0) { QTransform rot90 = createRotMatrix(90); QTransform hflip = createScaleMatrix(-1, 1); QTransform vflip = createScaleMatrix(1, -1); list << OrientationInfo() << OrientationInfo(NORMAL, QTransform(), JXFORM_NONE) << OrientationInfo(HFLIP, hflip, JXFORM_FLIP_H) << OrientationInfo(ROT_180, createRotMatrix(180), JXFORM_ROT_180) << OrientationInfo(VFLIP, vflip, JXFORM_FLIP_V) << OrientationInfo(TRANSPOSE, hflip * rot90, JXFORM_TRANSPOSE) << OrientationInfo(ROT_90, rot90, JXFORM_ROT_90) << OrientationInfo(TRANSVERSE, vflip * rot90, JXFORM_TRANSVERSE) << OrientationInfo(ROT_270, createRotMatrix(270), JXFORM_ROT_270) ; } return list; } void JpegContent::transform(Orientation orientation) { if (orientation != NOT_AVAILABLE && orientation != NORMAL) { d->mPendingTransformation = true; OrientationInfoList::ConstIterator it(orientationInfoList().begin()), end(orientationInfoList().end()); for (; it != end; ++it) { if ((*it).orientation == orientation) { d->mTransformMatrix = (*it).matrix * d->mTransformMatrix; break; } } if (it == end) { qWarning() << "Could not find matrix for orientation\n"; } } } #if 0 static void dumpMatrix(const QTransform& matrix) { qDebug() << "matrix | " << matrix.m11() << ", " << matrix.m12() << " |\n"; qDebug() << " | " << matrix.m21() << ", " << matrix.m22() << " |\n"; qDebug() << " ( " << matrix.dx() << ", " << matrix.dy() << " )\n"; } #endif static bool matricesAreSame(const QTransform& m1, const QTransform& m2, double tolerance) { return fabs(m1.m11() - m2.m11()) < tolerance && fabs(m1.m12() - m2.m12()) < tolerance && fabs(m1.m21() - m2.m21()) < tolerance && fabs(m1.m22() - m2.m22()) < tolerance && fabs(m1.dx() - m2.dx()) < tolerance && fabs(m1.dy() - m2.dy()) < tolerance; } static JXFORM_CODE findJxform(const QTransform& matrix) { OrientationInfoList::ConstIterator it(orientationInfoList().begin()), end(orientationInfoList().end()); for (; it != end; ++it) { if (matricesAreSame((*it).matrix, matrix, 0.001)) { return (*it).jxform; } } qWarning() << "findJxform: failed\n"; return JXFORM_NONE; } void JpegContent::applyPendingTransformation() { if (d->mRawData.size() == 0) { qCritical() << "No data loaded\n"; return; } // The following code is inspired by jpegtran.c from the libjpeg // Init JPEG structs struct jpeg_decompress_struct srcinfo; struct jpeg_compress_struct dstinfo; jvirt_barray_ptr * src_coef_arrays; jvirt_barray_ptr * dst_coef_arrays; // Initialize the JPEG decompression object JPEGErrorManager srcErrorManager; srcinfo.err = &srcErrorManager; jpeg_create_decompress(&srcinfo); if (setjmp(srcErrorManager.jmp_buffer)) { qCritical() << "libjpeg error in src\n"; return; } // Initialize the JPEG compression object JPEGErrorManager dstErrorManager; dstinfo.err = &dstErrorManager; jpeg_create_compress(&dstinfo); if (setjmp(dstErrorManager.jmp_buffer)) { qCritical() << "libjpeg error in dst\n"; return; } // Specify data source for decompression QBuffer buffer(&d->mRawData); buffer.open(QIODevice::ReadOnly); IODeviceJpegSourceManager::setup(&srcinfo, &buffer); // Enable saving of extra markers that we want to copy jcopy_markers_setup(&srcinfo, JCOPYOPT_ALL); (void) jpeg_read_header(&srcinfo, true); // Init transformation jpeg_transform_info transformoption; memset(&transformoption, 0, sizeof(jpeg_transform_info)); transformoption.transform = findJxform(d->mTransformMatrix); jtransform_request_workspace(&srcinfo, &transformoption); /* Read source file as DCT coefficients */ src_coef_arrays = jpeg_read_coefficients(&srcinfo); /* Initialize destination compression parameters from source values */ jpeg_copy_critical_parameters(&srcinfo, &dstinfo); /* Adjust destination parameters if required by transform options; * also find out which set of coefficient arrays will hold the output. */ dst_coef_arrays = jtransform_adjust_parameters(&srcinfo, &dstinfo, src_coef_arrays, &transformoption); /* Specify data destination for compression */ QByteArray output; output.resize(d->mRawData.size()); d->setupInmemDestination(&dstinfo, &output); /* Start compressor (note no image data is actually written here) */ jpeg_write_coefficients(&dstinfo, dst_coef_arrays); /* Copy to the output file any extra markers that we want to preserve */ jcopy_markers_execute(&srcinfo, &dstinfo, JCOPYOPT_ALL); /* Execute image transformation, if any */ jtransform_execute_transformation(&srcinfo, &dstinfo, src_coef_arrays, &transformoption); /* Finish compression and release memory */ jpeg_finish_compress(&dstinfo); jpeg_destroy_compress(&dstinfo); (void) jpeg_finish_decompress(&srcinfo); jpeg_destroy_decompress(&srcinfo); // Set rawData to our new JPEG d->mRawData = output; } QImage JpegContent::thumbnail() const { QImage image; if (!d->mExifData.empty()) { #if(EXIV2_TEST_VERSION(0,17,91)) Exiv2::ExifThumbC thumb(d->mExifData); Exiv2::DataBuf thumbnail = thumb.copy(); #else Exiv2::DataBuf thumbnail = d->mExifData.copyThumbnail(); #endif image.loadFromData(thumbnail.pData_, thumbnail.size_); Exiv2::ExifData::iterator it = d->mExifData.findKey(Exiv2::ExifKey("Exif.Canon.ThumbnailImageValidArea")); // ensure ThumbnailImageValidArea actually specifies a rectangle, i.e. there must be 4 coordinates if (it != d->mExifData.end() && it->count() == 4) { QRect validArea(QPoint(it->toLong(0), it->toLong(2)), QPoint(it->toLong(1), it->toLong(3))); image = image.copy(validArea); } else { // Unfortunately, Sony does not provide an exif tag that specifies the valid area of the // embedded thumbnail. Need to derive it from the size of the preview image instead. it = d->mExifData.findKey(Exiv2::ExifKey("Exif.Sony1.PreviewImageSize")); if (it != d->mExifData.end() && it->count() == 2) { const long prevHeight = it->toLong(0); const long prevWidth = it->toLong(1); const double scale = prevWidth / image.width(); // the embedded thumb only needs to be cropped vertically const long validThumbAreaHeight = ceil(prevHeight / scale); const long totalHeightOfBlackArea = image.height() - validThumbAreaHeight; // black bars on top and bottom should be equal in height const long offsetFromTop = totalHeightOfBlackArea / 2; const QRect validArea(QPoint(0, offsetFromTop), QSize(image.width(), validThumbAreaHeight)); image = image.copy(validArea); } } Orientation o = orientation(); if (GwenviewConfig::applyExifOrientation() && o != NORMAL && o != NOT_AVAILABLE) { image = image.transformed(ImageUtils::transformMatrix(o)); } } return image; } void JpegContent::setThumbnail(const QImage& thumbnail) { if (d->mExifData.empty()) { return; } QByteArray array; QBuffer buffer(&array); buffer.open(QIODevice::WriteOnly); QImageWriter writer(&buffer, "JPEG"); if (!writer.write(thumbnail)) { qCritical() << "Could not write thumbnail\n"; return; } #if (EXIV2_TEST_VERSION(0,17,91)) Exiv2::ExifThumb thumb(d->mExifData); thumb.setJpegThumbnail((unsigned char*)array.data(), array.size()); #else d->mExifData.setJpegThumbnail((unsigned char*)array.data(), array.size()); #endif } bool JpegContent::save(const QString& path) { // we need to take ownership of the input file's data // if the input file is still open, data is still only mem-mapped if (d->mFile.isOpen()) { // backup the mmap() pointer auto* mappedFile = reinterpret_cast(d->mRawData.data()); // read the file to memory d->mRawData = d->mFile.readAll(); d->mFile.unmap(mappedFile); d->mFile.close(); } QFile file(path); if (!file.open(QIODevice::WriteOnly)) { d->mErrorString = i18nc("@info", "Could not open file for writing."); return false; } return save(&file); } bool JpegContent::save(QIODevice* device) { if (!d->mImage.isNull()) { if (!d->updateRawDataFromImage()) { return false; } } if (d->mRawData.size() == 0) { d->mErrorString = i18nc("@info", "No data to store."); return false; } if (d->mPendingTransformation) { applyPendingTransformation(); d->mPendingTransformation = false; } std::unique_ptr image; image.reset(Exiv2::ImageFactory::open((unsigned char*)d->mRawData.data(), d->mRawData.size()).release()); // Store Exif info image->setExifData(d->mExifData); image->setComment(d->mComment.toUtf8().toStdString()); image->writeMetadata(); // Update mRawData Exiv2::BasicIo& io = image->io(); d->mRawData.resize(io.size()); io.read((unsigned char*)d->mRawData.data(), io.size()); QDataStream stream(device); stream.writeRawData(d->mRawData.data(), d->mRawData.size()); // Make sure we are up to date loadFromData(d->mRawData); return true; } QString JpegContent::errorString() const { return d->mErrorString; } void JpegContent::setImage(const QImage& image) { d->mRawData.clear(); d->mImage = image; d->mSize = image.size(); d->mExifData["Exif.Photo.PixelXDimension"] = image.width(); d->mExifData["Exif.Photo.PixelYDimension"] = image.height(); resetOrientation(); d->mPendingTransformation = false; d->mTransformMatrix = QTransform(); } } // namespace diff --git a/lib/thumbnailview/previewitemdelegate.cpp b/lib/thumbnailview/previewitemdelegate.cpp index a699dd05..939d5442 100644 --- a/lib/thumbnailview/previewitemdelegate.cpp +++ b/lib/thumbnailview/previewitemdelegate.cpp @@ -1,985 +1,985 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "previewitemdelegate.h" #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE #include #endif // Local #include "archiveutils.h" #include "itemeditor.h" #include "paintutils.h" #include "thumbnailview.h" #include "timeutils.h" #include "tooltipwidget.h" #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE #include "../semanticinfo/semanticinfodirmodel.h" #endif // Define this to be able to fine tune the rendering of the selection // background through a config file //#define FINETUNE_SELECTION_BACKGROUND #ifdef FINETUNE_SELECTION_BACKGROUND #include #include #endif //#define DEBUG_DRAW_BORDER //#define DEBUG_DRAW_CURRENT namespace Gwenview { /** * Space between the item outer rect and the content, and between the * thumbnail and the caption */ const int ITEM_MARGIN = 5; /** How darker is the border line around selection */ const int SELECTION_BORDER_DARKNESS = 140; const int FOCUS_BORDER_DARKNESS = 200; /** Radius of the selection rounded corners, in pixels */ const int SELECTION_RADIUS = 5; /** Space between the item outer rect and the context bar */ const int CONTEXTBAR_MARGIN = 1; /** How dark is the shadow, 0 is invisible, 255 is as dark as possible */ const int SHADOW_STRENGTH = 128; /** How many pixels around the thumbnail are shadowed */ const int SHADOW_SIZE = 4; static KFileItem fileItemForIndex(const QModelIndex& index) { Q_ASSERT(index.isValid()); QVariant data = index.data(KDirModel::FileItemRole); return qvariant_cast(data); } static QUrl urlForIndex(const QModelIndex& index) { KFileItem item = fileItemForIndex(index); return item.url(); } struct PreviewItemDelegatePrivate { /** * Maps full text to elided text. */ mutable QHash mElidedTextCache; // Key is height * 1000 + width typedef QHash ShadowCache; mutable ShadowCache mShadowCache; PreviewItemDelegate* q; ThumbnailView* mView; QWidget* mContextBar; QToolButton* mSaveButton; QPixmap mSaveButtonPixmap; QToolButton* mToggleSelectionButton; QToolButton* mFullScreenButton; QToolButton* mRotateLeftButton; QToolButton* mRotateRightButton; #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE KRatingPainter mRatingPainter; #endif QPersistentModelIndex mIndexUnderCursor; QSize mThumbnailSize; PreviewItemDelegate::ThumbnailDetails mDetails; PreviewItemDelegate::ContextBarActions mContextBarActions; Qt::TextElideMode mTextElideMode; QPointer mToolTip; QScopedPointer mToolTipAnimation; void initSaveButtonPixmap() { if (!mSaveButtonPixmap.isNull()) { return; } // Necessary otherwise we won't see the save button itself mSaveButton->adjustSize(); mSaveButtonPixmap = QPixmap(mSaveButton->sizeHint()); mSaveButtonPixmap.fill(Qt::transparent); mSaveButton->render(&mSaveButtonPixmap, QPoint(), QRegion(), QWidget::DrawChildren); } void showContextBar(const QRect& rect, const QPixmap& thumbnailPix) { if (mContextBarActions == PreviewItemDelegate::NoAction) { return; } mContextBar->adjustSize(); // Center bar, except if only showing SelectionAction. const int posX = mContextBarActions == PreviewItemDelegate::SelectionAction ? 0 : (rect.width() - mContextBar->width()) / 2; const int thumbnailPixHeight = qRound(thumbnailPix.height() / thumbnailPix.devicePixelRatio()); const int posY = qMax(CONTEXTBAR_MARGIN, mThumbnailSize.height() - thumbnailPixHeight - mContextBar->height()); mContextBar->move(rect.topLeft() + QPoint(posX, posY)); mContextBar->show(); } void initToolTip() { mToolTip = new ToolTipWidget(mView->viewport()); mToolTip->setOpacity(0); mToolTip->show(); } bool hoverEventFilter(QHoverEvent* event) { QModelIndex index = mView->indexAt(event->pos()); if (index != mIndexUnderCursor) { updateHoverUi(index); } else { // Same index, nothing to do, but repaint anyway in case we are // over the rating row mView->update(mIndexUnderCursor); } return false; } void updateHoverUi(const QModelIndex& index) { QModelIndex oldIndex = mIndexUnderCursor; mIndexUnderCursor = index; mView->update(oldIndex); if (QApplication::style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, mView)) { mView->setCursor(mIndexUnderCursor.isValid() ? Qt::PointingHandCursor : Qt::ArrowCursor); } if (mIndexUnderCursor.isValid()) { updateToggleSelectionButton(); updateImageButtons(); const QRect rect = mView->visualRect(mIndexUnderCursor); const QPixmap thumbnailPix = mView->thumbnailForIndex(index); showContextBar(rect, thumbnailPix); if (mView->isModified(mIndexUnderCursor)) { showSaveButton(rect); } else { mSaveButton->hide(); } showToolTip(index); mView->update(mIndexUnderCursor); } else { mContextBar->hide(); mSaveButton->hide(); hideToolTip(); } } QRect ratingRectFromIndexRect(const QRect& rect) const { return QRect( rect.left(), rect.bottom() - ratingRowHeight() - ITEM_MARGIN, rect.width(), ratingRowHeight()); } #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE int ratingFromCursorPosition(const QRect& ratingRect) const { const QPoint pos = mView->viewport()->mapFromGlobal(QCursor::pos()); return mRatingPainter.ratingFromPosition(ratingRect, pos); } #endif bool mouseButtonEventFilter(QEvent::Type type) { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE const QRect rect = ratingRectFromIndexRect(mView->visualRect(mIndexUnderCursor)); const int rating = ratingFromCursorPosition(rect); if (rating == -1) { return false; } if (type == QEvent::MouseButtonRelease) { q->setDocumentRatingRequested(urlForIndex(mIndexUnderCursor) , rating); } return true; #else return false; #endif } QPoint saveButtonPosition(const QRect& itemRect) const { QSize buttonSize = mSaveButton->sizeHint(); int posX = itemRect.right() - buttonSize.width(); int posY = itemRect.top() + mThumbnailSize.height() + 2 * ITEM_MARGIN - buttonSize.height(); return QPoint(posX, posY); } void showSaveButton(const QRect& itemRect) const { mSaveButton->move(saveButtonPosition(itemRect)); mSaveButton->show(); } void drawBackground(QPainter* painter, const QRect& rect, const QColor& bgColor, const QColor& borderColor) const { int bgH, bgS, bgV; int borderH, borderS, borderV, borderMargin; #ifdef FINETUNE_SELECTION_BACKGROUND QSettings settings(QDir::homePath() + "/colors.ini", QSettings::IniFormat); bgH = settings.value("bg/h").toInt(); bgS = settings.value("bg/s").toInt(); bgV = settings.value("bg/v").toInt(); borderH = settings.value("border/h").toInt(); borderS = settings.value("border/s").toInt(); borderV = settings.value("border/v").toInt(); borderMargin = settings.value("border/margin").toInt(); #else bgH = 0; bgS = -20; bgV = 43; borderH = 0; borderS = -100; borderV = 60; borderMargin = 1; #endif painter->setRenderHint(QPainter::Antialiasing); QRectF rectF = QRectF(rect).adjusted(0.5, 0.5, -0.5, -0.5); QPainterPath path = PaintUtils::roundedRectangle(rectF, SELECTION_RADIUS); QLinearGradient gradient(rectF.topLeft(), rectF.bottomLeft()); gradient.setColorAt(0, PaintUtils::adjustedHsv(bgColor, bgH, bgS, bgV)); gradient.setColorAt(1, bgColor); painter->fillPath(path, gradient); painter->setPen(borderColor); painter->drawPath(path); painter->setPen(PaintUtils::adjustedHsv(borderColor, borderH, borderS, borderV)); rectF = rectF.adjusted(borderMargin, borderMargin, -borderMargin, -borderMargin); path = PaintUtils::roundedRectangle(rectF, SELECTION_RADIUS); painter->drawPath(path); } void drawShadow(QPainter* painter, const QRect& rect) const { const QPoint shadowOffset(-SHADOW_SIZE, -SHADOW_SIZE + 1); const auto dpr = painter->device()->devicePixelRatioF(); int key = qRound((rect.height() * 1000 + rect.width()) * dpr); ShadowCache::Iterator it = mShadowCache.find(key); if (it == mShadowCache.end()) { QSize size = QSize(rect.width() + 2 * SHADOW_SIZE, rect.height() + 2 * SHADOW_SIZE); QColor color(0, 0, 0, SHADOW_STRENGTH); QPixmap shadow = PaintUtils::generateFuzzyRect(size * dpr, color, qRound(SHADOW_SIZE * dpr)); shadow.setDevicePixelRatio(dpr); it = mShadowCache.insert(key, shadow); } painter->drawPixmap(rect.topLeft() + shadowOffset, it.value()); } void drawText(QPainter* painter, const QRect& rect, const QColor& fgColor, const QString& fullText) const { QFontMetrics fm = mView->fontMetrics(); // Elide text QString text; QHash::const_iterator it = mElidedTextCache.constFind(fullText); if (it == mElidedTextCache.constEnd()) { text = fm.elidedText(fullText, mTextElideMode, rect.width()); mElidedTextCache[fullText] = text; } else { text = it.value(); } // Compute x pos int posX; if (text.length() == fullText.length()) { // Not elided, center text posX = (rect.width() - fm.boundingRect(text).width()) / 2; } else { // Elided, left align posX = 0; } // Draw text painter->setPen(fgColor); painter->drawText(rect.left() + posX, rect.top() + fm.ascent(), text); } void drawRating(QPainter* painter, const QRect& rect, const QVariant& value) { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE const int rating = value.toInt(); const QRect ratingRect = ratingRectFromIndexRect(rect); const int hoverRating = ratingFromCursorPosition(ratingRect); mRatingPainter.paint(painter, ratingRect, rating, hoverRating); #endif } bool isTextElided(const QString& text) const { QHash::const_iterator it = mElidedTextCache.constFind(text); if (it == mElidedTextCache.constEnd()) { return false; } return it.value().length() < text.length(); } /** * Show a tooltip only if the item has been elided. * This function places the tooltip over the item text. */ void showToolTip(const QModelIndex& index) { if (mDetails == 0 || mDetails == PreviewItemDelegate::RatingDetail) { // No text to display return; } // Gather tip text QStringList textList; bool elided = false; if (mDetails & PreviewItemDelegate::FileNameDetail) { const QString text = index.data().toString(); elided |= isTextElided(text); textList << text; } // FIXME: Duplicated from drawText const KFileItem fileItem = fileItemForIndex(index); const bool isDirOrArchive = ArchiveUtils::fileItemIsDirOrArchive(fileItem); if (mDetails & PreviewItemDelegate::DateDetail) { if (!ArchiveUtils::fileItemIsDirOrArchive(fileItem)) { const QDateTime dt = TimeUtils::dateTimeForFileItem(fileItem); const QString text = QLocale().toString(dt, QLocale::ShortFormat); elided |= isTextElided(text); textList << text; } } if (!isDirOrArchive && (mDetails & PreviewItemDelegate::ImageSizeDetail)) { QSize fullSize; QPixmap thumbnailPix = mView->thumbnailForIndex(index, &fullSize); if (fullSize.isValid()) { const QString text = QStringLiteral("%1x%2").arg(fullSize.width()).arg(fullSize.height()); elided |= isTextElided(text); textList << text; } } if (!isDirOrArchive && (mDetails & PreviewItemDelegate::FileSizeDetail)) { const KIO::filesize_t size = fileItem.size(); if (size > 0) { const QString text = KIO::convertSize(size); elided |= isTextElided(text); textList << text; } } if (!elided) { hideToolTip(); return; } bool newTipLabel = !mToolTip; if (!mToolTip) { initToolTip(); } mToolTip->setText(textList.join(QLatin1Char('\n'))); QSize tipSize = mToolTip->sizeHint(); // Compute tip position QRect rect = mView->visualRect(index); const int textY = ITEM_MARGIN + mThumbnailSize.height() + ITEM_MARGIN; const int spacing = 1; QRect geometry( QPoint(rect.topLeft() + QPoint((rect.width() - tipSize.width()) / 2, textY + spacing)), tipSize ); if (geometry.left() < 0) { geometry.moveLeft(0); } else if (geometry.right() > mView->viewport()->width()) { geometry.moveRight(mView->viewport()->width()); } // Show tip QParallelAnimationGroup* anim = new QParallelAnimationGroup(); QPropertyAnimation* fadeIn = new QPropertyAnimation(mToolTip, "opacity"); fadeIn->setStartValue(mToolTip->opacity()); fadeIn->setEndValue(1.); anim->addAnimation(fadeIn); if (newTipLabel) { mToolTip->setGeometry(geometry); } else { QPropertyAnimation* move = new QPropertyAnimation(mToolTip, "geometry"); move->setStartValue(mToolTip->geometry()); move->setEndValue(geometry); anim->addAnimation(move); } mToolTipAnimation.reset(anim); mToolTipAnimation->start(); } void hideToolTip() { if (!mToolTip) { return; } QSequentialAnimationGroup* anim = new QSequentialAnimationGroup(); if (mToolTipAnimation->state() == QPropertyAnimation::Stopped) { anim->addPause(500); } QPropertyAnimation* fadeOut = new QPropertyAnimation(mToolTip, "opacity"); fadeOut->setStartValue(mToolTip->opacity()); fadeOut->setEndValue(0.); anim->addAnimation(fadeOut); mToolTipAnimation.reset(anim); mToolTipAnimation->start(); QObject::connect(anim, &QSequentialAnimationGroup::finished, mToolTip.data(), &ToolTipWidget::deleteLater); } int itemWidth() const { return mThumbnailSize.width() + 2 * ITEM_MARGIN; } int ratingRowHeight() const { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE return qMax(mView->fontMetrics().ascent(), int(KIconLoader::SizeSmall)); #else return 0; #endif } int itemHeight() const { const int lineHeight = mView->fontMetrics().height(); int textHeight = 0; if (mDetails & PreviewItemDelegate::FileNameDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::DateDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::ImageSizeDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::FileSizeDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::RatingDetail) { textHeight += ratingRowHeight(); } if (textHeight == 0) { // Keep at least one row of text, so that we can show folder names textHeight = lineHeight; } return mThumbnailSize.height() + textHeight + 3 * ITEM_MARGIN; } void selectIndexUnderCursorIfNoMultiSelection() { if (mView->selectionModel()->selectedIndexes().size() <= 1) { mView->setCurrentIndex(mIndexUnderCursor); } } void updateToggleSelectionButton() { mToggleSelectionButton->setIcon(QIcon::fromTheme( mView->selectionModel()->isSelected(mIndexUnderCursor) ? QStringLiteral("list-remove") : QStringLiteral("list-add") )); } void updateImageButtons() { const KFileItem item = fileItemForIndex(mIndexUnderCursor); const bool isImage = !ArchiveUtils::fileItemIsDirOrArchive(item); mFullScreenButton->setEnabled(isImage); mRotateLeftButton->setEnabled(isImage); mRotateRightButton->setEnabled(isImage); } void updateContextBar() { if (mContextBarActions == PreviewItemDelegate::NoAction) { mContextBar->hide(); return; } const int width = itemWidth(); const int buttonWidth = mRotateRightButton->sizeHint().width(); mFullScreenButton->setVisible(mContextBarActions & PreviewItemDelegate::FullScreenAction); bool rotate = mContextBarActions & PreviewItemDelegate::RotateAction; mRotateLeftButton->setVisible(rotate && width >= 3 * buttonWidth); mRotateRightButton->setVisible(rotate && width >= 4 * buttonWidth); mContextBar->adjustSize(); } void updateViewGridSize() { mView->setGridSize(QSize(itemWidth(), itemHeight())); } }; PreviewItemDelegate::PreviewItemDelegate(ThumbnailView* view) : QItemDelegate(view) , d(new PreviewItemDelegatePrivate) { d->q = this; d->mView = view; view->viewport()->installEventFilter(this); // Set this attribute so that the viewport receives QEvent::HoverMove and // QEvent::HoverLeave events. We use these events in the event filter // installed on the viewport. // Some styles set this attribute themselves (Oxygen and Skulpture do) but // others do not (Plastique, Cleanlooks...) view->viewport()->setAttribute(Qt::WA_Hover); d->mThumbnailSize = view->thumbnailSize(); d->mDetails = FileNameDetail; d->mContextBarActions = SelectionAction | FullScreenAction | RotateAction; d->mTextElideMode = Qt::ElideRight; connect(view, &ThumbnailView::rowsRemovedSignal, this, &PreviewItemDelegate::slotRowsChanged); connect(view, &ThumbnailView::rowsInsertedSignal, this, &PreviewItemDelegate::slotRowsChanged); connect(view, &ThumbnailView::selectionChangedSignal, [this]() { d->updateToggleSelectionButton(); }); #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE d->mRatingPainter.setAlignment(Qt::AlignHCenter | Qt::AlignBottom); d->mRatingPainter.setLayoutDirection(view->layoutDirection()); d->mRatingPainter.setMaxRating(10); #endif connect(view, &ThumbnailView::thumbnailSizeChanged, this, &PreviewItemDelegate::setThumbnailSize); // Button frame d->mContextBar = new QWidget(d->mView->viewport()); d->mContextBar->hide(); d->mToggleSelectionButton = new QToolButton; d->mToggleSelectionButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); connect(d->mToggleSelectionButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotToggleSelectionClicked); d->mFullScreenButton = new QToolButton; d->mFullScreenButton->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen"))); connect(d->mFullScreenButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotFullScreenClicked); d->mRotateLeftButton = new QToolButton; d->mRotateLeftButton->setIcon(QIcon::fromTheme(QStringLiteral("object-rotate-left"))); connect(d->mRotateLeftButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotRotateLeftClicked); d->mRotateRightButton = new QToolButton; d->mRotateRightButton->setIcon(QIcon::fromTheme(QStringLiteral("object-rotate-right"))); connect(d->mRotateRightButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotRotateRightClicked); QHBoxLayout* layout = new QHBoxLayout(d->mContextBar); layout->setMargin(2); layout->setSpacing(2); layout->addWidget(d->mToggleSelectionButton); layout->addWidget(d->mFullScreenButton); layout->addWidget(d->mRotateLeftButton); layout->addWidget(d->mRotateRightButton); // Save button d->mSaveButton = new QToolButton(d->mView->viewport()); d->mSaveButton->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); d->mSaveButton->hide(); connect(d->mSaveButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotSaveClicked); } PreviewItemDelegate::~PreviewItemDelegate() { delete d; } QSize PreviewItemDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const { return d->mView->gridSize(); } bool PreviewItemDelegate::eventFilter(QObject* object, QEvent* event) { if (object == d->mView->viewport()) { switch (event->type()) { case QEvent::ToolTip: return true; case QEvent::HoverMove: case QEvent::HoverLeave: return d->hoverEventFilter(static_cast(event)); case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: return d->mouseButtonEventFilter(event->type()); default: return false; } } else { // Necessary for the item editor to work correctly (especially closing // the editor with the Escape key) return QItemDelegate::eventFilter(object, event); } } void PreviewItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const { int thumbnailHeight = d->mThumbnailSize.height(); QSize fullSize; QPixmap thumbnailPix = d->mView->thumbnailForIndex(index, &fullSize); QSize thumbnailSize = thumbnailPix.size() / thumbnailPix.devicePixelRatio(); const KFileItem fileItem = fileItemForIndex(index); const bool opaque = !thumbnailPix.hasAlphaChannel(); const bool isDirOrArchive = ArchiveUtils::fileItemIsDirOrArchive(fileItem); QRect rect = option.rect; const bool selected = option.state & QStyle::State_Selected; const bool underMouse = option.state & QStyle::State_MouseOver; const bool hasFocus = option.state & QStyle::State_HasFocus; const QWidget* viewport = d->mView->viewport(); #ifdef DEBUG_DRAW_BORDER painter->setPen(Qt::red); painter->setBrush(Qt::NoBrush); painter->drawRect(rect); #endif // Select color group QPalette::ColorGroup cg; if ((option.state & QStyle::State_Enabled) && (option.state & QStyle::State_Active)) { cg = QPalette::Normal; } else if ((option.state & QStyle::State_Enabled)) { cg = QPalette::Inactive; } else { cg = QPalette::Disabled; } // Select colors QColor bgColor, borderColor, fgColor; fgColor = viewport->palette().color(viewport->foregroundRole()); if (selected || underMouse) { bgColor = option.palette.color(cg, QPalette::Highlight); if (hasFocus) { borderColor = bgColor.darker(FOCUS_BORDER_DARKNESS); } else { borderColor = bgColor.darker(SELECTION_BORDER_DARKNESS); } } else { bgColor = viewport->palette().color(viewport->backgroundRole()); if (hasFocus) { borderColor = fgColor; } else { borderColor = bgColor.lighter(200); } } // Compute thumbnailRect QRect thumbnailRect = QRect( rect.left() + (rect.width() - thumbnailSize.width()) / 2, rect.top() + (thumbnailHeight - thumbnailSize.height()) + ITEM_MARGIN, thumbnailSize.width(), thumbnailSize.height()); // Draw background const QRect backgroundRect = thumbnailRect.adjusted(-ITEM_MARGIN, -ITEM_MARGIN, ITEM_MARGIN, ITEM_MARGIN); if (selected) { d->drawBackground(painter, backgroundRect, bgColor, borderColor); } else if (underMouse) { painter->setOpacity(0.2); d->drawBackground(painter, backgroundRect, bgColor, borderColor); painter->setOpacity(1.); } else if (opaque) { d->drawShadow(painter, thumbnailRect); } // Draw thumbnail if (opaque) { painter->setPen(borderColor); painter->setRenderHint(QPainter::Antialiasing, false); QRect borderRect = thumbnailRect.adjusted(-1, -1, 0, 0); painter->drawRect(borderRect); } else if (hasFocus && !selected) { painter->setPen(option.palette.color(cg, QPalette::Highlight)); painter->setRenderHint(QPainter::Antialiasing, false); QLine underLine = QLine(thumbnailRect.bottomLeft(), thumbnailRect.bottomRight()); underLine.translate(0, 3); painter->drawLine(underLine); } painter->drawPixmap(thumbnailRect.left(), thumbnailRect.top(), thumbnailPix); // Draw modified indicator bool isModified = d->mView->isModified(index); if (isModified) { // Draws a pixmap of the save button frame, as an indicator that // the image has been modified QPoint framePosition = d->saveButtonPosition(rect); d->initSaveButtonPixmap(); painter->drawPixmap(framePosition, d->mSaveButtonPixmap); } // Draw busy indicator if (d->mView->isBusy(index)) { QPixmap pix = d->mView->busySequenceCurrentPixmap(); painter->drawPixmap( thumbnailRect.left() + (thumbnailRect.width() - pix.width()) / 2, thumbnailRect.top() + (thumbnailRect.height() - pix.height()) / 2, pix); } if (index == d->mIndexUnderCursor) { // Show bar again: if the thumbnail has changed, we may need to update // its position. Don't do it if we are over rotate buttons, though: it // would not be nice to move the button now, the user may want to // rotate the image one more time. // The button will get moved when the mouse leaves. if (!d->mRotateLeftButton->underMouse() && !d->mRotateRightButton->underMouse()) { d->showContextBar(rect, thumbnailPix); } if (isModified) { // If we just rotated the image with the buttons from the // button frame, we need to show the save button frame right now. d->showSaveButton(rect); } else { d->mSaveButton->hide(); } } QRect textRect( rect.left() + ITEM_MARGIN, rect.top() + 2 * ITEM_MARGIN + thumbnailHeight, rect.width() - 2 * ITEM_MARGIN, d->mView->fontMetrics().height()); if (isDirOrArchive || (d->mDetails & PreviewItemDelegate::FileNameDetail)) { d->drawText(painter, textRect, fgColor, index.data().toString()); textRect.moveTop(textRect.bottom()); } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::DateDetail)) { const QDateTime dt = TimeUtils::dateTimeForFileItem(fileItem); d->drawText(painter, textRect, fgColor, QLocale().toString(dt, QLocale::ShortFormat)); textRect.moveTop(textRect.bottom()); } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::ImageSizeDetail)) { if (fullSize.isValid()) { const QString text = QStringLiteral("%1x%2").arg(fullSize.width()).arg(fullSize.height()); d->drawText(painter, textRect, fgColor, text); textRect.moveTop(textRect.bottom()); } } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::FileSizeDetail)) { const KIO::filesize_t size = fileItem.size(); if (size > 0) { const QString st = KIO::convertSize(size); d->drawText(painter, textRect, fgColor, st); textRect.moveTop(textRect.bottom()); } } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::RatingDetail)) { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE d->drawRating(painter, rect, index.data(SemanticInfoDirModel::RatingRole)); #endif } #ifdef DEBUG_DRAW_CURRENT if (d->mView->currentIndex() == index) { painter->fillRect(rect.left(), rect.top(), 12, 12, Qt::red); } #endif } void PreviewItemDelegate::setThumbnailSize(const QSize& value) { d->mThumbnailSize = value; d->updateViewGridSize(); d->updateContextBar(); d->mElidedTextCache.clear(); } void PreviewItemDelegate::slotSaveClicked() { emit saveDocumentRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotRotateLeftClicked() { d->selectIndexUnderCursorIfNoMultiSelection(); emit rotateDocumentLeftRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotRotateRightClicked() { d->selectIndexUnderCursorIfNoMultiSelection(); emit rotateDocumentRightRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotFullScreenClicked() { emit showDocumentInFullScreenRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotToggleSelectionClicked() { d->mView->selectionModel()->select(d->mIndexUnderCursor, QItemSelectionModel::Toggle); } PreviewItemDelegate::ThumbnailDetails PreviewItemDelegate::thumbnailDetails() const { return d->mDetails; } void PreviewItemDelegate::setThumbnailDetails(PreviewItemDelegate::ThumbnailDetails details) { d->mDetails = details; d->updateViewGridSize(); d->mView->scheduleDelayedItemsLayout(); } PreviewItemDelegate::ContextBarActions PreviewItemDelegate::contextBarActions() const { return d->mContextBarActions; } void PreviewItemDelegate::setContextBarActions(PreviewItemDelegate::ContextBarActions actions) { d->mContextBarActions = actions; d->updateContextBar(); } Qt::TextElideMode PreviewItemDelegate::textElideMode() const { return d->mTextElideMode; } void PreviewItemDelegate::setTextElideMode(Qt::TextElideMode mode) { if (d->mTextElideMode == mode) { return; } d->mTextElideMode = mode; d->mElidedTextCache.clear(); d->mView->viewport()->update(); } void PreviewItemDelegate::slotRowsChanged() { // We need to update hover ui because the current index may have // disappeared: for example if the current image is removed with "del". QPoint pos = d->mView->viewport()->mapFromGlobal(QCursor::pos()); QModelIndex index = d->mView->indexAt(pos); d->updateHoverUi(index); } QWidget * PreviewItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& /*option*/, const QModelIndex& /*index*/) const { return new ItemEditor(parent); } void PreviewItemDelegate::setEditorData(QWidget* widget, const QModelIndex& index) const { ItemEditor* edit = qobject_cast(widget); if (!edit) { return; } edit->setText(index.data().toString()); } void PreviewItemDelegate::updateEditorGeometry(QWidget* widget, const QStyleOptionViewItem& option, const QModelIndex& index) const { ItemEditor* edit = qobject_cast(widget); if (!edit) { return; } QString text = index.data().toString(); - int textWidth = edit->fontMetrics().boundingRect(QStringLiteral(" ") + text + QStringLiteral(" ")).width(); + int textWidth = edit->fontMetrics().boundingRect(QLatin1String(" ") + text + QLatin1String(" ")).width(); QRect textRect( option.rect.left() + (option.rect.width() - textWidth) / 2, option.rect.top() + 2 * ITEM_MARGIN + d->mThumbnailSize.height(), textWidth, edit->sizeHint().height()); edit->setGeometry(textRect); } void PreviewItemDelegate::setModelData(QWidget* widget, QAbstractItemModel* model, const QModelIndex& index) const { ItemEditor* edit = qobject_cast(widget); if (!edit) { return; } if (index.data().toString() != edit->text()) { model->setData(index, edit->text(), Qt::EditRole); } } } // namespace diff --git a/tests/auto/documenttest.cpp b/tests/auto/documenttest.cpp index fa27e244..eb0e6a23 100644 --- a/tests/auto/documenttest.cpp +++ b/tests/auto/documenttest.cpp @@ -1,894 +1,894 @@ /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Qt #include #include #include // KDE #include #include #include #include #include // Local #include "../lib/abstractimageoperation.h" #include "../lib/document/abstractdocumenteditor.h" #include "../lib/document/documentjob.h" #include "../lib/document/documentfactory.h" #include "../lib/imagemetainfomodel.h" #include "../lib/imageutils.h" #include "../lib/transformimageoperation.h" #include "testutils.h" #include #include "documenttest.h" QTEST_MAIN(DocumentTest) using namespace Gwenview; static void waitUntilMetaInfoLoaded(Document::Ptr doc) { while (doc->loadingState() < Document::MetaInfoLoaded) { QTest::qWait(100); } } static bool waitUntilJobIsDone(DocumentJob* job) { JobWatcher watcher(job); watcher.wait(); return watcher.error() == KJob::NoError; } void DocumentTest::initTestCase() { qRegisterMetaType("QUrl"); } void DocumentTest::init() { DocumentFactory::instance()->clearCache(); } void DocumentTest::testLoad() { QFETCH(QString, fileName); QFETCH(QByteArray, expectedFormat); QFETCH(int, expectedKindInt); QFETCH(bool, expectedIsAnimated); QFETCH(QImage, expectedImage); QFETCH(int, maxHeight); // number of lines to test. -1 to test all lines MimeTypeUtils::Kind expectedKind = MimeTypeUtils::Kind(expectedKindInt); QUrl url = urlForTestFile(fileName); // testing RAW loading. For raw, QImage directly won't work -> load it using KDCRaw QByteArray mFormatHint = url.fileName().section('.', -1).toLocal8Bit().toLower(); if (KDcrawIface::KDcraw::rawFilesList().contains(QString(mFormatHint))) { if (!KDcrawIface::KDcraw::loadEmbeddedPreview(expectedImage, url.toLocalFile())) { QSKIP("Not running this test: failed to get expectedImage. Try running ./fetch_testing_raw.sh\ in the tests/data directory and then rerun the tests."); } } if (expectedKind != MimeTypeUtils::KIND_SVG_IMAGE) { if (expectedImage.isNull()) { QSKIP("Not running this test: QImage failed to load the test image"); } } Document::Ptr doc = DocumentFactory::instance()->load(url); QSignalSpy spy(doc.data(), SIGNAL(isAnimatedUpdated())); doc->waitUntilLoaded(); QCOMPARE(doc->loadingState(), Document::Loaded); QCOMPARE(doc->kind(), expectedKind); QCOMPARE(doc->isAnimated(), expectedIsAnimated); QCOMPARE(spy.count(), doc->isAnimated() ? 1 : 0); if (doc->kind() == MimeTypeUtils::KIND_RASTER_IMAGE) { QImage image = doc->image(); if (maxHeight > -1) { QRect poiRect(0, 0, image.width(), maxHeight); image = image.copy(poiRect); expectedImage = expectedImage.copy(poiRect); } QCOMPARE(image, expectedImage); QCOMPARE(QString(doc->format()), QString(expectedFormat)); } } static void testLoad_newRow( const char* fileName, const QByteArray& format, MimeTypeUtils::Kind kind = MimeTypeUtils::KIND_RASTER_IMAGE, bool isAnimated = false, int maxHeight = -1 ) { QTest::newRow(fileName) << fileName << QByteArray(format) << int(kind) << isAnimated << QImage(pathForTestFile(fileName), format) << maxHeight; } void DocumentTest::testLoad_data() { QTest::addColumn("fileName"); QTest::addColumn("expectedFormat"); QTest::addColumn("expectedKindInt"); QTest::addColumn("expectedIsAnimated"); QTest::addColumn("expectedImage"); QTest::addColumn("maxHeight"); testLoad_newRow("test.png", "png"); testLoad_newRow("160216_no_size_before_decoding.eps", "eps"); testLoad_newRow("160382_corrupted.jpeg", "jpeg", MimeTypeUtils::KIND_RASTER_IMAGE, false, 55); testLoad_newRow("1x10k.png", "png"); testLoad_newRow("1x10k.jpg", "jpeg"); testLoad_newRow("test.xcf", "xcf"); testLoad_newRow("188191_does_not_load.tga", "tga"); testLoad_newRow("289819_does_not_load.png", "png"); testLoad_newRow("png-with-jpeg-extension.jpg", "png"); testLoad_newRow("jpg-with-gif-extension.gif", "jpeg"); // RAW preview testLoad_newRow("CANON-EOS350D-02.CR2", "cr2", MimeTypeUtils::KIND_RASTER_IMAGE, false); testLoad_newRow("dsc_0093.nef", "nef", MimeTypeUtils::KIND_RASTER_IMAGE, false); // SVG testLoad_newRow("test.svg", "", MimeTypeUtils::KIND_SVG_IMAGE); // FIXME: Test svgz // Animated testLoad_newRow("4frames.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, true); testLoad_newRow("1frame.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, false); testLoad_newRow("185523_1frame_with_graphic_control_extension.gif", "gif", MimeTypeUtils::KIND_RASTER_IMAGE, false); } void DocumentTest::testLoadTwoPasses() { QUrl url = urlForTestFile("test.png"); QImage image; bool ok = image.load(url.toLocalFile()); QVERIFY2(ok, "Could not load 'test.png'"); Document::Ptr doc = DocumentFactory::instance()->load(url); waitUntilMetaInfoLoaded(doc); QVERIFY2(doc->image().isNull(), "Image shouldn't have been loaded at this time"); QCOMPARE(doc->format().data(), "png"); doc->waitUntilLoaded(); QCOMPARE(image, doc->image()); } void DocumentTest::testLoadEmpty() { QUrl url = urlForTestFile("empty.png"); Document::Ptr doc = DocumentFactory::instance()->load(url); while (doc->loadingState() <= Document::KindDetermined) { QTest::qWait(100); } QCOMPARE(doc->loadingState(), Document::LoadingFailed); } #define NEW_ROW(fileName) QTest::newRow(fileName) << fileName void DocumentTest::testLoadDownSampled_data() { QTest::addColumn("fileName"); NEW_ROW("orient6.jpg"); NEW_ROW("1x10k.jpg"); } #undef NEW_ROW void DocumentTest::testLoadDownSampled() { // Note: for now we only support down sampling on jpeg, do not use test.png // here QFETCH(QString, fileName); QUrl url = urlForTestFile(fileName); QImage image; bool ok = image.load(url.toLocalFile()); QVERIFY2(ok, "Could not load test image"); Document::Ptr doc = DocumentFactory::instance()->load(url); QSignalSpy downSampledImageReadySpy(doc.data(), SIGNAL(downSampledImageReady())); QSignalSpy loadingFailedSpy(doc.data(), SIGNAL(loadingFailed(QUrl))); QSignalSpy loadedSpy(doc.data(), SIGNAL(loaded(QUrl))); bool ready = doc->prepareDownSampledImageForZoom(0.2); QVERIFY2(!ready, "There should not be a down sampled image at this point"); - while (downSampledImageReadySpy.count() == 0 && loadingFailedSpy.count() == 0 && loadedSpy.count() == 0) { + while (downSampledImageReadySpy.isEmpty() && loadingFailedSpy.isEmpty() && loadedSpy.isEmpty()) { QTest::qWait(100); } QImage downSampledImage = doc->downSampledImageForZoom(0.2); QVERIFY2(!downSampledImage.isNull(), "Down sampled image should not be null"); QSize expectedSize = doc->size() / 2; if (expectedSize.isEmpty()) { expectedSize = image.size(); } QCOMPARE(downSampledImage.size(), expectedSize); } /** * Down sampling is not supported on png. We should get a complete image * instead. */ void DocumentTest::testLoadDownSampledPng() { QUrl url = urlForTestFile("test.png"); QImage image; bool ok = image.load(url.toLocalFile()); QVERIFY2(ok, "Could not load test image"); Document::Ptr doc = DocumentFactory::instance()->load(url); LoadingStateSpy stateSpy(doc); connect(doc.data(), &Document::loaded, &stateSpy, &LoadingStateSpy::readState); bool ready = doc->prepareDownSampledImageForZoom(0.2); QVERIFY2(!ready, "There should not be a down sampled image at this point"); doc->waitUntilLoaded(); QCOMPARE(stateSpy.mCallCount, 1); QCOMPARE(stateSpy.mState, Document::Loaded); } void DocumentTest::testLoadRemote() { QUrl url = setUpRemoteTestDir("test.png"); if (!url.isValid()) { QSKIP("Not running this test: failed to setup remote test dir."); } url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + '/' + "test.png"); QVERIFY2(KIO::stat(url, KIO::StatJob::SourceSide, 0)->exec(), "test url not found"); Document::Ptr doc = DocumentFactory::instance()->load(url); doc->waitUntilLoaded(); QImage image = doc->image(); QCOMPARE(image.width(), 150); QCOMPARE(image.height(), 100); } void DocumentTest::testLoadAnimated() { QUrl srcUrl = urlForTestFile("40frames.gif"); Document::Ptr doc = DocumentFactory::instance()->load(srcUrl); QSignalSpy spy(doc.data(), SIGNAL(imageRectUpdated(QRect))); doc->waitUntilLoaded(); QVERIFY(doc->isAnimated()); // Test we receive only one imageRectUpdated() until animation is started // (the imageRectUpdated() is triggered by the loading of the first image) QTest::qWait(1000); QCOMPARE(spy.count(), 1); // Test we now receive some imageRectUpdated() doc->startAnimation(); QTest::qWait(1000); int count = spy.count(); doc->stopAnimation(); QVERIFY2(count > 0, "No imageRectUpdated() signal received"); // Test we do not receive imageRectUpdated() anymore QTest::qWait(1000); QCOMPARE(count, spy.count()); // Start again, we should receive imageRectUpdated() again doc->startAnimation(); QTest::qWait(1000); QVERIFY2(spy.count() > count, "No imageRectUpdated() signal received after restarting"); } void DocumentTest::testPrepareDownSampledAfterFailure() { QUrl url = urlForTestFile("empty.png"); Document::Ptr doc = DocumentFactory::instance()->load(url); doc->waitUntilLoaded(); QCOMPARE(doc->loadingState(), Document::LoadingFailed); bool ready = doc->prepareDownSampledImageForZoom(0.25); QVERIFY2(!ready, "Down sampled image should not be ready"); } void DocumentTest::testSaveRemote() { QUrl dstUrl = setUpRemoteTestDir(); if (!dstUrl.isValid()) { QSKIP("Not running this test: failed to setup remote test dir."); } QUrl srcUrl = urlForTestFile("test.png"); Document::Ptr doc = DocumentFactory::instance()->load(srcUrl); doc->waitUntilLoaded(); dstUrl = dstUrl.adjusted(QUrl::StripTrailingSlash); dstUrl.setPath(dstUrl.path() + '/' + "testSaveRemote.png"); QVERIFY(waitUntilJobIsDone(doc->save(dstUrl, "png"))); } /** * Check that deleting a document while it is loading does not crash */ void DocumentTest::testDeleteWhileLoading() { { QUrl url = urlForTestFile("test.png"); QImage image; bool ok = image.load(url.toLocalFile()); QVERIFY2(ok, "Could not load 'test.png'"); Document::Ptr doc = DocumentFactory::instance()->load(url); } DocumentFactory::instance()->clearCache(); // Wait two seconds. If the test fails we will get a segfault while waiting QTest::qWait(2000); } void DocumentTest::testLoadRotated() { QUrl url = urlForTestFile("orient6.jpg"); QImage image; bool ok = image.load(url.toLocalFile()); QVERIFY2(ok, "Could not load 'orient6.jpg'"); QTransform matrix = ImageUtils::transformMatrix(ROT_90); image = image.transformed(matrix); Document::Ptr doc = DocumentFactory::instance()->load(url); doc->waitUntilLoaded(); QCOMPARE(image, doc->image()); // RAW preview on rotated image url = urlForTestFile("dsd_1838.nef"); if (!KDcrawIface::KDcraw::loadEmbeddedPreview(image, url.toLocalFile())) { QSKIP("Not running this test: failed to get image. Try running ./fetch_testing_raw.sh\ in the tests/data directory and then rerun the tests."); } matrix = ImageUtils::transformMatrix(ROT_270); image = image.transformed(matrix); doc = DocumentFactory::instance()->load(url); doc->waitUntilLoaded(); QCOMPARE(image, doc->image()); } /** * Checks that asking the DocumentFactory the same document twice in a row does * not load it twice */ void DocumentTest::testMultipleLoads() { QUrl url = urlForTestFile("orient6.jpg"); Document::Ptr doc1 = DocumentFactory::instance()->load(url); Document::Ptr doc2 = DocumentFactory::instance()->load(url); QCOMPARE(doc1.data(), doc2.data()); } void DocumentTest::testSaveAs() { QUrl url = urlForTestFile("orient6.jpg"); DocumentFactory* factory = DocumentFactory::instance(); Document::Ptr doc = factory->load(url); QSignalSpy savedSpy(doc.data(), SIGNAL(saved(QUrl,QUrl))); QSignalSpy modifiedDocumentListChangedSpy(factory, SIGNAL(modifiedDocumentListChanged())); QSignalSpy documentChangedSpy(factory, SIGNAL(documentChanged(QUrl))); doc->startLoadingFullImage(); QUrl destUrl = urlForTestOutputFile("result.png"); QVERIFY(waitUntilJobIsDone(doc->save(destUrl, "png"))); QCOMPARE(doc->format().data(), "png"); QCOMPARE(doc->url(), destUrl); QCOMPARE(doc->metaInfo()->getValueForKey("General.Name"), destUrl.fileName()); QVERIFY2(doc->loadingState() == Document::Loaded, "Document is supposed to finish loading before saving" ); QTest::qWait(100); // saved() is emitted asynchronously QCOMPARE(savedSpy.count(), 1); QVariantList args = savedSpy.takeFirst(); QCOMPARE(args.at(0).toUrl(), url); QCOMPARE(args.at(1).toUrl(), destUrl); QImage image("result.png", "png"); QCOMPARE(doc->image(), image); QVERIFY(!DocumentFactory::instance()->hasUrl(url)); QVERIFY(DocumentFactory::instance()->hasUrl(destUrl)); QCOMPARE(modifiedDocumentListChangedSpy.count(), 0); // No changes were made QCOMPARE(documentChangedSpy.count(), 1); args = documentChangedSpy.takeFirst(); QCOMPARE(args.at(0).toUrl(), destUrl); } void DocumentTest::testLosslessSave() { QUrl url1 = urlForTestFile("orient6.jpg"); Document::Ptr doc = DocumentFactory::instance()->load(url1); doc->startLoadingFullImage(); QUrl url2 = urlForTestOutputFile("orient1.jpg"); QVERIFY(waitUntilJobIsDone(doc->save(url2, "jpeg"))); QImage image1; QVERIFY(image1.load(url1.toLocalFile())); QImage image2; QVERIFY(image2.load(url2.toLocalFile())); QCOMPARE(image1, image2); } void DocumentTest::testLosslessRotate() { // Generate test image QImage image1(200, 96, QImage::Format_RGB32); { QPainter painter(&image1); QConicalGradient gradient(QPointF(100, 48), 100); gradient.setColorAt(0, Qt::white); gradient.setColorAt(1, Qt::blue); painter.fillRect(image1.rect(), gradient); } QUrl url1 = urlForTestOutputFile("lossless1.jpg"); QVERIFY(image1.save(url1.toLocalFile(), "jpeg")); // Load it as a Gwenview document Document::Ptr doc = DocumentFactory::instance()->load(url1); doc->waitUntilLoaded(); // Rotate one time QVERIFY(doc->editor()); doc->editor()->applyTransformation(ROT_90); // Save it QUrl url2 = urlForTestOutputFile("lossless2.jpg"); waitUntilJobIsDone(doc->save(url2, "jpeg")); // Load the saved image doc = DocumentFactory::instance()->load(url2); doc->waitUntilLoaded(); // Rotate the other way QVERIFY(doc->editor()); doc->editor()->applyTransformation(ROT_270); waitUntilJobIsDone(doc->save(url2, "jpeg")); // Compare the saved images QVERIFY(image1.load(url1.toLocalFile())); QImage image2; QVERIFY(image2.load(url2.toLocalFile())); QCOMPARE(image1, image2); } void DocumentTest::testModifyAndSaveAs() { QVariantList args; class TestOperation : public AbstractImageOperation { public: void redo() override { QImage image(10, 10, QImage::Format_ARGB32); image.fill(QColor(Qt::white).rgb()); document()->editor()->setImage(image); finish(true); } }; QUrl url = urlForTestFile("orient6.jpg"); DocumentFactory* factory = DocumentFactory::instance(); Document::Ptr doc = factory->load(url); QSignalSpy savedSpy(doc.data(), SIGNAL(saved(QUrl,QUrl))); QSignalSpy modifiedDocumentListChangedSpy(factory, SIGNAL(modifiedDocumentListChanged())); QSignalSpy documentChangedSpy(factory, SIGNAL(documentChanged(QUrl))); doc->waitUntilLoaded(); QVERIFY(!doc->isModified()); QCOMPARE(modifiedDocumentListChangedSpy.count(), 0); // Modify image QVERIFY(doc->editor()); TestOperation* op = new TestOperation; op->applyToDocument(doc); QTest::qWait(100); QVERIFY(doc->isModified()); QCOMPARE(modifiedDocumentListChangedSpy.count(), 1); modifiedDocumentListChangedSpy.clear(); QList lst = factory->modifiedDocumentList(); QCOMPARE(lst.count(), 1); QCOMPARE(lst.first(), url); QCOMPARE(documentChangedSpy.count(), 1); args = documentChangedSpy.takeFirst(); QCOMPARE(args.at(0).toUrl(), url); // Save it under a new name QUrl destUrl = urlForTestOutputFile("modify.png"); QVERIFY(waitUntilJobIsDone(doc->save(destUrl, "png"))); // Wait a bit because save() will clear the undo stack when back to the // event loop QTest::qWait(100); QVERIFY(!doc->isModified()); QVERIFY(!factory->hasUrl(url)); QVERIFY(factory->hasUrl(destUrl)); QCOMPARE(modifiedDocumentListChangedSpy.count(), 1); QVERIFY(DocumentFactory::instance()->modifiedDocumentList().isEmpty()); QCOMPARE(documentChangedSpy.count(), 2); QList modifiedUrls = QList() << url << destUrl; QVERIFY(modifiedUrls.contains(url)); QVERIFY(modifiedUrls.contains(destUrl)); } void DocumentTest::testMetaInfoJpeg() { QUrl url = urlForTestFile("orient6.jpg"); Document::Ptr doc = DocumentFactory::instance()->load(url); // We cleared the cache, so the document should not be loaded Q_ASSERT(doc->loadingState() <= Document::KindDetermined); // Wait until we receive the metaInfoUpdated() signal QSignalSpy metaInfoUpdatedSpy(doc.data(), SIGNAL(metaInfoUpdated())); - while (metaInfoUpdatedSpy.count() == 0) { + while (metaInfoUpdatedSpy.isEmpty()) { QTest::qWait(100); } // Extract an exif key QString value = doc->metaInfo()->getValueForKey("Exif.Image.Make"); QCOMPARE(value, QString::fromUtf8("Canon")); } void DocumentTest::testMetaInfoBmp() { QUrl url = urlForTestOutputFile("metadata.bmp"); const int width = 200; const int height = 100; QImage image(width, height, QImage::Format_ARGB32); image.fill(Qt::black); image.save(url.toLocalFile(), "BMP"); Document::Ptr doc = DocumentFactory::instance()->load(url); QSignalSpy metaInfoUpdatedSpy(doc.data(), SIGNAL(metaInfoUpdated())); waitUntilMetaInfoLoaded(doc); Q_ASSERT(metaInfoUpdatedSpy.count() >= 1); QString value = doc->metaInfo()->getValueForKey("General.ImageSize"); QString expectedValue = QStringLiteral("%1x%2").arg(width).arg(height); QCOMPARE(value, expectedValue); } void DocumentTest::testForgetModifiedDocument() { QSignalSpy spy(DocumentFactory::instance(), SIGNAL(modifiedDocumentListChanged())); DocumentFactory::instance()->forget(QUrl("file://does/not/exist.png")); QCOMPARE(spy.count(), 0); // Generate test image QImage image1(200, 96, QImage::Format_RGB32); { QPainter painter(&image1); QConicalGradient gradient(QPointF(100, 48), 100); gradient.setColorAt(0, Qt::white); gradient.setColorAt(1, Qt::blue); painter.fillRect(image1.rect(), gradient); } QUrl url = urlForTestOutputFile("testForgetModifiedDocument.png"); QVERIFY(image1.save(url.toLocalFile(), "png")); // Load it as a Gwenview document Document::Ptr doc = DocumentFactory::instance()->load(url); doc->waitUntilLoaded(); // Modify it TransformImageOperation* op = new TransformImageOperation(ROT_90); op->applyToDocument(doc); QTest::qWait(100); QCOMPARE(spy.count(), 1); QList lst = DocumentFactory::instance()->modifiedDocumentList(); QCOMPARE(lst.length(), 1); QCOMPARE(lst.first(), url); // Forget it DocumentFactory::instance()->forget(url); QCOMPARE(spy.count(), 2); lst = DocumentFactory::instance()->modifiedDocumentList(); QVERIFY(lst.isEmpty()); } void DocumentTest::testModifiedAndSavedSignals() { TransformImageOperation* op; QUrl url = urlForTestFile("orient6.jpg"); Document::Ptr doc = DocumentFactory::instance()->load(url); QSignalSpy modifiedSpy(doc.data(), SIGNAL(modified(QUrl))); QSignalSpy savedSpy(doc.data(), SIGNAL(saved(QUrl,QUrl))); doc->waitUntilLoaded(); QCOMPARE(modifiedSpy.count(), 0); QCOMPARE(savedSpy.count(), 0); op = new TransformImageOperation(ROT_90); op->applyToDocument(doc); QTest::qWait(100); QCOMPARE(modifiedSpy.count(), 1); op = new TransformImageOperation(ROT_90); op->applyToDocument(doc); QTest::qWait(100); QCOMPARE(modifiedSpy.count(), 2); doc->undoStack()->undo(); QTest::qWait(100); QCOMPARE(modifiedSpy.count(), 3); doc->undoStack()->undo(); QTest::qWait(100); QCOMPARE(savedSpy.count(), 1); } class TestJob : public DocumentJob { public: TestJob(QString* str, char ch) : mStr(str) , mCh(ch) {} protected: void doStart() override { *mStr += mCh; emitResult(); } private: QString* mStr; char mCh; }; void DocumentTest::testJobQueue() { QUrl url = urlForTestFile("orient6.jpg"); Document::Ptr doc = DocumentFactory::instance()->load(url); QSignalSpy spy(doc.data(), SIGNAL(busyChanged(QUrl,bool))); QString str; doc->enqueueJob(new TestJob(&str, 'a')); doc->enqueueJob(new TestJob(&str, 'b')); doc->enqueueJob(new TestJob(&str, 'c')); QVERIFY(doc->isBusy()); QEventLoop loop; connect(doc.data(), &Document::allTasksDone, &loop, &QEventLoop::quit); loop.exec(); QVERIFY(!doc->isBusy()); QCOMPARE(spy.count(), 2); QVariantList row = spy.takeFirst(); QCOMPARE(row.at(0).toUrl(), url); QVERIFY(row.at(1).toBool()); row = spy.takeFirst(); QCOMPARE(row.at(0).toUrl(), url); QVERIFY(!row.at(1).toBool()); QCOMPARE(str, QStringLiteral("abc")); } class TestCheckDocumentEditorJob : public DocumentJob { public: TestCheckDocumentEditorJob(int* hasEditor) : mHasEditor(hasEditor) { *mHasEditor = -1; } protected: void doStart() override { document()->waitUntilLoaded(); *mHasEditor = checkDocumentEditor() ? 1 : 0; emitResult(); } private: int* mHasEditor; }; class TestUiDelegate : public KJobUiDelegate { public: TestUiDelegate(bool* showErrorMessageCalled) : mShowErrorMessageCalled(showErrorMessageCalled) { setAutoErrorHandlingEnabled(true); *mShowErrorMessageCalled = false; } void showErrorMessage() override { //qDebug(); *mShowErrorMessageCalled = true; } private: bool* mShowErrorMessageCalled; }; /** * Test that an error is reported when a DocumentJob fails because there is no * document editor available */ void DocumentTest::testCheckDocumentEditor() { int hasEditor; bool showErrorMessageCalled; QEventLoop loop; Document::Ptr doc; TestCheckDocumentEditorJob* job; doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg")); job = new TestCheckDocumentEditorJob(&hasEditor); job->setUiDelegate(new TestUiDelegate(&showErrorMessageCalled)); doc->enqueueJob(job); connect(doc.data(), &Document::allTasksDone, &loop, &QEventLoop::quit); loop.exec(); QVERIFY(!showErrorMessageCalled); QCOMPARE(hasEditor, 1); doc = DocumentFactory::instance()->load(urlForTestFile("test.svg")); job = new TestCheckDocumentEditorJob(&hasEditor); job->setUiDelegate(new TestUiDelegate(&showErrorMessageCalled)); doc->enqueueJob(job); connect(doc.data(), &Document::allTasksDone, &loop, &QEventLoop::quit); loop.exec(); QVERIFY(showErrorMessageCalled); QCOMPARE(hasEditor, 0); } /** * An operation should only pushed to the document undo stack if it succeed */ void DocumentTest::testUndoStackPush() { class SuccessOperation : public AbstractImageOperation { protected: void redo() override { QMetaObject::invokeMethod(this, "finish", Qt::QueuedConnection, Q_ARG(bool, true)); } }; class FailureOperation : public AbstractImageOperation { protected: void redo() override { QMetaObject::invokeMethod(this, "finish", Qt::QueuedConnection, Q_ARG(bool, false)); } }; AbstractImageOperation* op; Document::Ptr doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg")); // A successful operation should be added to the undo stack op = new SuccessOperation; op->applyToDocument(doc); QTest::qWait(100); QVERIFY(!doc->undoStack()->isClean()); // Reset doc->undoStack()->undo(); QVERIFY(doc->undoStack()->isClean()); // A failed operation should not be added to the undo stack op = new FailureOperation; op->applyToDocument(doc); QTest::qWait(100); QVERIFY(doc->undoStack()->isClean()); } void DocumentTest::testUndoRedo() { class SuccessOperation : public AbstractImageOperation { public: int mRedoCount = 0; int mUndoCount = 0; protected: void redo() override { mRedoCount++; finish(true); } void undo() override { mUndoCount++; finish(true); } }; Document::Ptr doc = DocumentFactory::instance()->load(urlForTestFile("orient6.jpg")); QSignalSpy modifiedSpy(doc.data(), &Document::modified); QSignalSpy savedSpy(doc.data(), &Document::saved); SuccessOperation* op = new SuccessOperation; QCOMPARE(op->mRedoCount, 0); QCOMPARE(op->mUndoCount, 0); // Apply (redo) operation op->applyToDocument(doc); QVERIFY(modifiedSpy.wait()); QCOMPARE(op->mRedoCount, 1); QCOMPARE(op->mUndoCount, 0); QCOMPARE(doc->undoStack()->count(), 1); QVERIFY(!doc->undoStack()->isClean()); // Undo operation doc->undoStack()->undo(); QVERIFY(savedSpy.wait()); QCOMPARE(op->mRedoCount, 1); QCOMPARE(op->mUndoCount, 1); QCOMPARE(doc->undoStack()->count(), 1); QVERIFY(doc->undoStack()->isClean()); // Redo operation doc->undoStack()->redo(); QVERIFY(modifiedSpy.wait()); QCOMPARE(op->mRedoCount, 2); QCOMPARE(op->mUndoCount, 1); QCOMPARE(doc->undoStack()->count(), 1); QVERIFY(!doc->undoStack()->isClean()); // Undo operation again doc->undoStack()->undo(); QVERIFY(savedSpy.wait()); QCOMPARE(op->mRedoCount, 2); QCOMPARE(op->mUndoCount, 2); QCOMPARE(doc->undoStack()->count(), 1); QVERIFY(doc->undoStack()->isClean()); }