diff --git a/app/gvcore.cpp b/app/gvcore.cpp index 11bf03dc..3c4c2556 100644 --- a/app/gvcore.cpp +++ b/app/gvcore.cpp @@ -1,383 +1,386 @@ // 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" // Qt #include #include // KDE #include #include #include #include #include #include #include #include // Local #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; HistoryModel* mRecentUrlsModel; QPalette mPalettes[4]; bool showSaveAsDialog(const KUrl& url, KUrl* outUrl, QByteArray* format) { KFileDialog dialog(url, QString(), mMainWindow); dialog.setOperationMode(KFileDialog::Saving); dialog.setSelection(url.fileName()); dialog.setMimeFilter( KImageIO::mimeTypes(KImageIO::Writing), // List MimeTypeUtils::urlMimeType(url) // Default ); // Show dialog do { if (!dialog.exec()) { return false; } const QString mimeType = dialog.currentMimeFilter(); if (mimeType.isEmpty()) { KMessageBox::sorry( mMainWindow, i18nc("@info", "No image format selected.") ); continue; } const QStringList typeList = KImageIO::typeForMime(mimeType); if (typeList.count() > 0) { *format = typeList[0].toAscii(); break; } KMessageBox::sorry( mMainWindow, i18nc("@info", "Gwenview cannot save images as %1.", mimeType) ); } while (true); *outUrl = dialog.selectedUrl(); return true; } void setupPalettes() { QPalette pal; int value = GwenviewConfig::viewBackgroundValue(); QColor fgColor = value > 128 ? Qt::black : Qt::white; // Normal mPalettes[GvCore::NormalPalette] = KGlobalSettings::createApplicationPalette(); pal = mPalettes[GvCore::NormalPalette]; pal.setColor(QPalette::Base, QColor::fromHsv(0, 0, value)); pal.setColor(QPalette::Text, fgColor); mPalettes[GvCore::NormalViewPalette] = pal; // Fullscreen KSharedConfigPtr config; QString name = GwenviewConfig::fullScreenColorScheme(); if (name.isEmpty()) { // Default color scheme QString path = KStandardDirs::locate("data", "gwenview/color-schemes/fullscreen.colors"); config = KSharedConfig::openConfig(path); } else if (name.contains('/')) { // Full path to a .colors file config = KSharedConfig::openConfig(name); } else { // Standard KDE color scheme config = KSharedConfig::openConfig(QString("color-schemes/%1.colors").arg(name), KConfig::FullConfig, "data"); } mPalettes[GvCore::FullScreenPalette] = KGlobalSettings::createApplicationPalette(config); pal = mPalettes[GvCore::FullScreenPalette]; QString path = KStandardDirs::locate("data", "gwenview/images/background.png"); QPixmap bgTexture(path); pal.setBrush(QPalette::Base, bgTexture); mPalettes[GvCore::FullScreenViewPalette] = pal; } }; GvCore::GvCore(MainWindow* mainWindow, SortedDirModel* dirModel) : QObject(mainWindow) , d(new GvCorePrivate) { d->q = this; d->mMainWindow = mainWindow; d->mDirModel = dirModel; d->mRecentFoldersModel = 0; d->mRecentUrlsModel = 0; 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), KStandardDirs::locateLocal("appdata", "recentfolders/")); } return d->mRecentFoldersModel; } QAbstractItemModel* GvCore::recentUrlsModel() const { if (!d->mRecentUrlsModel) { d->mRecentUrlsModel = new HistoryModel(const_cast(this), KStandardDirs::locateLocal("appdata", "recenturls/")); } return d->mRecentUrlsModel; } AbstractSemanticInfoBackEnd* GvCore::semanticInfoBackEnd() const { return d->mDirModel->semanticInfoBackEnd(); } -void GvCore::addUrlToRecentFolders(const KUrl& url) +void GvCore::addUrlToRecentFolders(KUrl url) { if (!GwenviewConfig::historyEnabled()) { return; } if (!url.isValid()) { return; } + if (url.path() != "") { // This check is a workaraound for bug #312060 + url.adjustPath(KUrl::AddTrailingSlash); + } recentFoldersModel(); d->mRecentFoldersModel->addUrl(url); } void GvCore::addUrlToRecentUrls(const KUrl& url) { if (!GwenviewConfig::historyEnabled()) { return; } recentUrlsModel(); d->mRecentUrlsModel->addUrl(url); } void GvCore::saveAll() { SaveAllHelper helper(d->mMainWindow); helper.save(); } void GvCore::save(const KUrl& url) { Document::Ptr doc = DocumentFactory::instance()->load(url); QByteArray format = doc->format(); const QStringList availableTypes = KImageIO::types(KImageIO::Writing); if (availableTypes.contains(QString(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 KUrl& url) { QByteArray format; KUrl saveAsUrl; if (!d->showSaveAsDialog(url, &saveAsUrl, &format)) { return; } // Check for overwrite if (KIO::NetAccess::exists(saveAsUrl, KIO::NetAccess::DestinationSide, d->mMainWindow)) { int answer = KMessageBox::warningContinueCancel( d->mMainWindow, i18nc("@info", "A file named %1 already exists.\n" "Are you sure you want to overwrite it?", saveAsUrl.fileName()), QString(), KStandardGuiItem::overwrite()); if (answer == KMessageBox::Cancel) { return; } } // Start save Document::Ptr doc = DocumentFactory::instance()->load(url); KJob* job = doc->save(saveAsUrl, format.data()); if (!job) { const QString name = saveAsUrl.fileName().isEmpty() ? saveAsUrl.pathOrUrl() : saveAsUrl.fileName(); const QString msg = i18nc("@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 KUrl& url, Orientation orientation) { TransformImageOperation* op = new TransformImageOperation(orientation); Document::Ptr doc = DocumentFactory::instance()->load(url); op->applyToDocument(doc); } void GvCore::slotSaveResult(KJob* _job) { SaveJob* job = static_cast(_job); KUrl oldUrl = job->oldUrl(); KUrl newUrl = job->newUrl(); if (job->error()) { QString name = newUrl.fileName().isEmpty() ? newUrl.pathOrUrl() : newUrl.fileName(); QString msg = i18nc("@info", "Saving %1 failed:
%2", name, job->errorString()); int result = KMessageBox::warningContinueCancel( d->mMainWindow, msg, QString() /* caption */, KStandardGuiItem::saveAs()); if (result == KMessageBox::Continue) { saveAs(newUrl); } return; } if (oldUrl != newUrl) { d->mMainWindow->goToUrl(newUrl); ViewMainPage* page = d->mMainWindow->viewMainPage(); if (page->isVisible()) { MessageBubble* bubble = new MessageBubble(); bubble->setText(i18n("You are now viewing the new document.")); KGuiItem item = KStandardGuiItem::back(); item.setText(i18n("Go back to the original")); GraphicsHudButton* 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 KUrl& url) { applyTransform(url, ROT_270); } void GvCore::rotateRight(const KUrl& url) { applyTransform(url, ROT_90); } void GvCore::setRating(const KUrl& url, int rating) { QModelIndex index = d->mDirModel->indexForUrl(url); if (!index.isValid()) { kWarning() << "invalid index!"; return; } d->mDirModel->setData(index, rating, SemanticInfoDirModel::RatingRole); } bool GvCore::ensureDocumentIsEditable(const KUrl& url) { // FIXME: Replace with a CheckEditableJob? // This way we can factorize the error message Document::Ptr doc = DocumentFactory::instance()->load(url); doc->startLoadingFullImage(); doc->waitUntilLoaded(); if (doc->isEditable()) { return true; } KMessageBox::sorry( QApplication::activeWindow(), i18nc("@info", "Gwenview cannot edit this kind of image.") ); return false; } static void clearModel(QAbstractItemModel* model) { model->removeRows(0, model->rowCount()); } void GvCore::slotConfigChanged() { if (!GwenviewConfig::historyEnabled()) { clearModel(recentFoldersModel()); clearModel(recentUrlsModel()); } d->setupPalettes(); } QPalette GvCore::palette(GvCore::PaletteType type) const { return d->mPalettes[type]; } } // namespace diff --git a/app/gvcore.h b/app/gvcore.h index cb24fbc1..34ad238b 100644 --- a/app/gvcore.h +++ b/app/gvcore.h @@ -1,93 +1,93 @@ // 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. */ #ifndef GVCORE_H #define GVCORE_H // Qt #include // KDE // Local class KJob; class KUrl; class QAbstractItemModel; class QPalette; namespace Gwenview { class AbstractSemanticInfoBackEnd; class MainWindow; class SortedDirModel; class GvCorePrivate; class GvCore : public QObject { Q_OBJECT public: GvCore(MainWindow* mainWindow, SortedDirModel*); ~GvCore(); enum PaletteType { NormalPalette = 0, NormalViewPalette, FullScreenPalette, FullScreenViewPalette }; QAbstractItemModel* recentFoldersModel() const; QAbstractItemModel* recentUrlsModel() const; AbstractSemanticInfoBackEnd* semanticInfoBackEnd() const; - void addUrlToRecentFolders(const KUrl&); + void addUrlToRecentFolders(KUrl); void addUrlToRecentUrls(const KUrl& url); /** * Checks if the document referenced by url is editable, shows a sorry * dialog if it's not. * @return true if editable, false if not */ static bool ensureDocumentIsEditable(const KUrl& url); QPalette palette(PaletteType type) const; public Q_SLOTS: void saveAll(); void save(const KUrl&); void saveAs(const KUrl&); void rotateLeft(const KUrl&); void rotateRight(const KUrl&); void setRating(const KUrl&, int); private Q_SLOTS: void slotConfigChanged(); void slotSaveResult(KJob*); private: GvCorePrivate* const d; }; } // namespace #endif /* GVCORE_H */ diff --git a/app/startmainpage.cpp b/app/startmainpage.cpp index 1141c9e3..1c2baa6f 100644 --- a/app/startmainpage.cpp +++ b/app/startmainpage.cpp @@ -1,336 +1,337 @@ // 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 "startmainpage.moc" #include // Qt #include #include #include #include // KDE #include #include #include #include // Local #include #include #include #include #include #include #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE #include #endif #ifdef GWENVIEW_SEMANTICINFO_BACKEND_NEPOMUK #include #include #endif namespace Gwenview { class HistoryThumbnailViewHelper : public AbstractThumbnailViewHelper { public: HistoryThumbnailViewHelper(QObject* parent) : AbstractThumbnailViewHelper(parent) {} virtual void showContextMenu(QWidget*) { } virtual void showMenuForUrlDroppedOnViewport(QWidget*, const KUrl::List&) { } virtual void showMenuForUrlDroppedOnDir(QWidget*, const KUrl::List&, const KUrl&) { } }; /** * Inherit from QStyledItemDelegate to match KFilePlacesViewDelegate sizeHint * height. */ class HistoryViewDelegate : public QStyledItemDelegate { public: HistoryViewDelegate(QObject* parent = 0) : QStyledItemDelegate(parent) {} virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QSize sh = QStyledItemDelegate::sizeHint(option, index); int iconSize = static_cast(parent())->iconSize().height(); // Copied from KFilePlacesViewDelegate::sizeHint() int height = option.fontMetrics.height() / 2 + qMax(iconSize, option.fontMetrics.height()); sh.setHeight(qMax(sh.height(), height)); return sh; } }; struct StartMainPagePrivate : public Ui_StartMainPage { StartMainPage* q; GvCore* mGvCore; KFilePlacesModel* mBookmarksModel; bool mSearchUiInitialized; void setupSearchUi() { #ifdef GWENVIEW_SEMANTICINFO_BACKEND_NEPOMUK if (Nepomuk::ResourceManager::instance()->init() == 0) { mTagView->setModel(TagModel::createAllTagsModel(mTagView, mGvCore->semanticInfoBackEnd())); mTagView->show(); mTagLabel->hide(); } else { mTagView->hide(); mTagLabel->show(); } #else mTagView->hide(); mTagLabel->hide(); #endif } void updateHistoryTab() { mHistoryWidget->setVisible(GwenviewConfig::historyEnabled()); mHistoryDisabledLabel->setVisible(!GwenviewConfig::historyEnabled()); } }; static void initViewPalette(QAbstractItemView* view, const QColor& fgColor) { QWidget* viewport = view->viewport(); QPalette palette = viewport->palette(); palette.setColor(viewport->backgroundRole(), Qt::transparent); palette.setColor(QPalette::WindowText, fgColor); palette.setColor(QPalette::Text, fgColor); // QListView uses QStyledItemDelegate, which uses the view palette for // foreground color, while KFilePlacesView uses the viewport palette. viewport->setPalette(palette); view->setPalette(palette); } static bool styleIsGtkBased() { const char* name = QApplication::style()->metaObject()->className(); return qstrcmp(name, "QGtkStyle") == 0; } StartMainPage::StartMainPage(QWidget* parent, GvCore* gvCore) : QFrame(parent) , d(new StartMainPagePrivate) { d->q = this; d->mGvCore = gvCore; d->mSearchUiInitialized = false; d->setupUi(this); if (styleIsGtkBased()) { // Gtk-based styles do not apply the correct background color on tabs. // As a workaround, use the Plastique style instead. QStyle* fix = new QPlastiqueStyle(); fix->setParent(this); d->mHistoryWidget->tabBar()->setStyle(fix); d->mPlacesTagsWidget->tabBar()->setStyle(fix); } setFrameStyle(QFrame::NoFrame); // Bookmark view d->mBookmarksModel = new KFilePlacesModel(this); d->mBookmarksView->setModel(d->mBookmarksModel); d->mBookmarksView->setAutoResizeItemsEnabled(false); connect(d->mBookmarksView, SIGNAL(urlChanged(KUrl)), SIGNAL(urlSelected(KUrl))); // Tag view connect(d->mTagView, SIGNAL(clicked(QModelIndex)), SLOT(slotTagViewClicked(QModelIndex))); // Recent folder view connect(d->mRecentFoldersView, SIGNAL(indexActivated(QModelIndex)), SLOT(slotListViewActivated(QModelIndex))); connect(d->mRecentFoldersView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(showRecentFoldersViewContextMenu(QPoint))); // Url bag view d->mRecentUrlsView->setItemDelegate(new HistoryViewDelegate(d->mRecentUrlsView)); connect(d->mRecentUrlsView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(showRecentFoldersViewContextMenu(QPoint))); if (KGlobalSettings::singleClick()) { if (KGlobalSettings::changeCursorOverIcon()) { d->mRecentUrlsView->setCursor(Qt::PointingHandCursor); } connect(d->mRecentUrlsView, SIGNAL(clicked(QModelIndex)), SLOT(slotListViewActivated(QModelIndex))); } else { connect(d->mRecentUrlsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(slotListViewActivated(QModelIndex))); } d->updateHistoryTab(); connect(GwenviewConfig::self(), SIGNAL(configChanged()), SLOT(loadConfig())); } StartMainPage::~StartMainPage() { delete d; } void StartMainPage::slotTagViewClicked(const QModelIndex& index) { #ifdef GWENVIEW_SEMANTICINFO_BACKEND_NEPOMUK if (!index.isValid()) { return; } // FIXME: Check label encoding Nepomuk::Tag tagr(index.data().toString()); KUrl url = KUrl(tagr.resourceUri()).url(); emit urlSelected(url); #endif } void StartMainPage::applyPalette(const QPalette& newPalette) { QColor fgColor = newPalette.text().color(); QPalette pal = palette(); pal.setBrush(backgroundRole(), newPalette.base()); pal.setBrush(QPalette::Button, newPalette.base()); pal.setBrush(QPalette::WindowText, fgColor); pal.setBrush(QPalette::ButtonText, fgColor); pal.setBrush(QPalette::Text, fgColor); setPalette(pal); initViewPalette(d->mBookmarksView, fgColor); initViewPalette(d->mTagView, fgColor); initViewPalette(d->mRecentFoldersView, fgColor); initViewPalette(d->mRecentUrlsView, fgColor); } void StartMainPage::slotListViewActivated(const QModelIndex& index) { if (!index.isValid()) { return; } QVariant data = index.data(KFilePlacesModel::UrlRole); KUrl url = data.toUrl(); // Prevent dir lister error if (!url.isValid()) { kError() << "Tried to open an invalid url"; return; } emit urlSelected(url); } void StartMainPage::showEvent(QShowEvent* event) { if (GwenviewConfig::historyEnabled()) { if (!d->mRecentFoldersView->model()) { d->mRecentFoldersView->setThumbnailViewHelper(new HistoryThumbnailViewHelper(d->mRecentFoldersView)); d->mRecentFoldersView->setModel(d->mGvCore->recentFoldersModel()); PreviewItemDelegate* delegate = new PreviewItemDelegate(d->mRecentFoldersView); delegate->setContextBarActions(PreviewItemDelegate::NoAction); delegate->setTextElideMode(Qt::ElideLeft); d->mRecentFoldersView->setItemDelegate(delegate); d->mRecentFoldersView->setThumbnailWidth(128); + d->mRecentFoldersView->setCreateThumbnailsForRemoteUrls(false); } if (!d->mRecentUrlsView->model()) { d->mRecentUrlsView->setModel(d->mGvCore->recentUrlsModel()); } } if (!d->mSearchUiInitialized) { d->mSearchUiInitialized = true; d->setupSearchUi(); } QFrame::showEvent(event); } void StartMainPage::showRecentFoldersViewContextMenu(const QPoint& pos) { QAbstractItemView* view = qobject_cast(sender()); KUrl url; QModelIndex index = view->indexAt(pos); if (index.isValid()) { QVariant data = index.data(KFilePlacesModel::UrlRole); url = data.toUrl(); } // Create menu QMenu menu(this); bool fromRecentUrls = view == d->mRecentUrlsView; QAction* addToPlacesAction = fromRecentUrls ? 0 : menu.addAction(KIcon("bookmark-new"), i18n("Add to Places")); QAction* removeAction = menu.addAction(KIcon("edit-delete"), fromRecentUrls ? i18n("Forget this URL") : i18n("Forget this Folder")); menu.addSeparator(); QAction* clearAction = menu.addAction(KIcon("edit-delete-all"), i18n("Forget All")); if (!index.isValid()) { if (addToPlacesAction) { addToPlacesAction->setEnabled(false); } removeAction->setEnabled(false); } // Handle menu QAction* action = menu.exec(view->mapToGlobal(pos)); if (!action) { return; } if (action == addToPlacesAction) { QString text = url.fileName(); if (text.isEmpty()) { text = url.pathOrUrl(); } d->mBookmarksModel->addPlace(text, url); } else if (action == removeAction) { view->model()->removeRow(index.row()); } else if (action == clearAction) { view->model()->removeRows(0, view->model()->rowCount()); } } void StartMainPage::loadConfig() { d->updateHistoryTab(); applyPalette(d->mGvCore->palette(GvCore::NormalViewPalette)); } } // namespace diff --git a/lib/historymodel.cpp b/lib/historymodel.cpp index 07ed1eff..baa7dbed 100644 --- a/lib/historymodel.cpp +++ b/lib/historymodel.cpp @@ -1,251 +1,252 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 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, Cambridge, MA 02110-1301, USA. */ // Self #include "historymodel.moc" // Qt #include #include #include // KDE #include #include #include #include #include #include #include #include #include #include #include #include // Local #include namespace Gwenview { struct HistoryItem : public QStandardItem { void save() const { KConfig config(mConfigPath, KConfig::SimpleConfig); KConfigGroup group(&config, "general"); group.writeEntry("url", mUrl); group.writeEntry("dateTime", mDateTime.toString(Qt::ISODate)); config.sync(); } static HistoryItem* create(const KUrl& url, const QDateTime& dateTime, const QString& storageDir) { if (!KStandardDirs::makeDir(storageDir, 0600)) { kError() << "Could not create history dir" << storageDir; return 0; } KTemporaryFile file; file.setAutoRemove(false); file.setPrefix(storageDir); file.setSuffix("rc"); if (!file.open()) { kError() << "Could not create history file"; return 0; } HistoryItem* item = new HistoryItem(url, dateTime, file.fileName()); item->save(); return item; } static HistoryItem* load(const QString& fileName) { KConfig config(fileName, KConfig::SimpleConfig); KConfigGroup group(&config, "general"); KUrl url(group.readEntry("url")); if (!url.isValid()) { kError() << "Invalid url" << url; return 0; } QDateTime dateTime = QDateTime::fromString(group.readEntry("dateTime"), Qt::ISODate); if (!dateTime.isValid()) { kError() << "Invalid dateTime" << dateTime; return 0; } if (UrlUtils::urlIsFastLocalFile(url)) { if (!QFile::exists(url.path())) { kDebug() << "Skipping" << url.path() << "from recent folders. It does not exist anymore"; return 0; } } return new HistoryItem(url, dateTime, fileName); } KUrl url() const { return mUrl; } QDateTime dateTime() const { return mDateTime; } void setDateTime(const QDateTime& dateTime) { if (mDateTime != dateTime) { mDateTime = dateTime; save(); } } void unlink() { QFile::remove(mConfigPath); } private: KUrl mUrl; QDateTime mDateTime; QString mConfigPath; HistoryItem(const KUrl& url, const QDateTime& dateTime, const QString& configPath) : mUrl(url) , mDateTime(dateTime) , mConfigPath(configPath) { mUrl.cleanPath(); - mUrl.adjustPath(KUrl::RemoveTrailingSlash); - setText(mUrl.pathOrUrl()); + KUrl urlForView = mUrl; + urlForView.adjustPath(KUrl::RemoveTrailingSlash); + setText(urlForView.pathOrUrl()); QString iconName = KMimeType::iconNameForUrl(mUrl); setIcon(KIcon(iconName)); setData(QVariant(mUrl), KFilePlacesModel::UrlRole); KFileItem fileItem(KFileItem::Unknown, KFileItem::Unknown, mUrl); setData(QVariant(fileItem), KDirModel::FileItemRole); QString date = KGlobal::locale()->formatDateTime(mDateTime, KLocale::FancyLongDate); setData(QVariant(i18n("Last visited: %1", date)), Qt::ToolTipRole); } bool operator<(const QStandardItem& other) const { return mDateTime > static_cast(&other)->mDateTime; } }; struct HistoryModelPrivate { HistoryModel* q; QString mStorageDir; int mMaxCount; QMap mHistoryItemForUrl; void load() { QDir dir(mStorageDir); if (!dir.exists()) { return; } Q_FOREACH(const QString & name, dir.entryList(QStringList() << "*rc")) { HistoryItem* item = HistoryItem::load(dir.filePath(name)); if (!item) { continue; } HistoryItem* existingItem = mHistoryItemForUrl.value(item->url()); if (existingItem) { // We already know this url(!) update existing item dateTime // and get rid of duplicate if (existingItem->dateTime() < item->dateTime()) { existingItem->setDateTime(item->dateTime()); } item->unlink(); delete item; } else { mHistoryItemForUrl.insert(item->url(), item); q->appendRow(item); } } q->sort(0); } void garbageCollect() { while (q->rowCount() > mMaxCount) { HistoryItem* item = static_cast(q->takeRow(q->rowCount() - 1).at(0)); mHistoryItemForUrl.remove(item->url()); item->unlink(); delete item; } } }; HistoryModel::HistoryModel(QObject* parent, const QString& storageDir, int maxCount) : QStandardItemModel(parent) , d(new HistoryModelPrivate) { d->q = this; d->mStorageDir = storageDir; d->mMaxCount = maxCount; d->load(); } HistoryModel::~HistoryModel() { delete d; } void HistoryModel::addUrl(const KUrl& url, const QDateTime& _dateTime) { QDateTime dateTime = _dateTime.isValid() ? _dateTime : QDateTime::currentDateTime(); HistoryItem* historyItem = d->mHistoryItemForUrl.value(url); if (historyItem) { historyItem->setDateTime(dateTime); sort(0); } else { historyItem = HistoryItem::create(url, dateTime, d->mStorageDir); if (!historyItem) { kError() << "Could not save history for url" << url; return; } appendRow(historyItem); sort(0); d->garbageCollect(); } } bool HistoryModel::removeRows(int start, int count, const QModelIndex& parent) { Q_ASSERT(!parent.isValid()); for (int row = start + count - 1; row >= start ; --row) { HistoryItem* historyItem = static_cast(item(row, 0)); Q_ASSERT(historyItem); d->mHistoryItemForUrl.remove(historyItem->url()); historyItem->unlink(); } return QStandardItemModel::removeRows(start, count, parent); } } // namespace diff --git a/lib/thumbnailview/thumbnailview.cpp b/lib/thumbnailview/thumbnailview.cpp index 8ae8c36b..3958bcf9 100644 --- a/lib/thumbnailview/thumbnailview.cpp +++ b/lib/thumbnailview/thumbnailview.cpp @@ -1,957 +1,977 @@ /* 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 "thumbnailview.moc" // Std #include // Qt #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include // Local #include "abstractdocumentinfoprovider.h" #include "abstractthumbnailviewhelper.h" #include "archiveutils.h" #include "dragpixmapgenerator.h" #include "mimetypeutils.h" #include "thumbnailloadjob.h" +#include "urlutils.h" namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) kDebug() << x #else #define LOG(x) ; #endif /** How many msec to wait before starting to smooth thumbnails */ const int SMOOTH_DELAY = 500; const int WHEEL_ZOOM_MULTIPLIER = 4; static KFileItem fileItemForIndex(const QModelIndex& index) { if (!index.isValid()) { LOG("Invalid index"); return KFileItem(); } QVariant data = index.data(KDirModel::FileItemRole); return qvariant_cast(data); } static KUrl urlForIndex(const QModelIndex& index) { KFileItem item = fileItemForIndex(index); return item.isNull() ? KUrl() : item.url(); } struct Thumbnail { Thumbnail(const QPersistentModelIndex& index_, const KDateTime& mtime) : mIndex(index_) , mModificationTime(mtime) , mRough(true) , mWaitingForThumbnail(true) {} Thumbnail() : mRough(true) , mWaitingForThumbnail(true) {} /** * Init the thumbnail based on a icon */ void initAsIcon(const QPixmap& pix) { mGroupPix = pix; int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large); mFullSize = QSize(largeGroupSize, largeGroupSize); } bool isGroupPixAdaptedForSize(int size) const { if (mWaitingForThumbnail) { return false; } if (mGroupPix.isNull()) { return false; } const int groupSize = qMax(mGroupPix.width(), mGroupPix.height()); if (groupSize >= size) { return true; } // groupSize is less than size, but this may be because the full image // is the same size as groupSize return groupSize == qMax(mFullSize.width(), mFullSize.height()); } void prepareForRefresh(const KDateTime& mtime) { mModificationTime = mtime; mGroupPix = QPixmap(); mAdjustedPix = QPixmap(); mFullSize = QSize(); mRealFullSize = QSize(); mRough = true; mWaitingForThumbnail = true; } QPersistentModelIndex mIndex; KDateTime mModificationTime; /// The pix loaded from .thumbnails/{large,normal} QPixmap mGroupPix; /// Scaled version of mGroupPix, adjusted to ThumbnailView::thumbnailSize QPixmap mAdjustedPix; /// Size of the full image QSize mFullSize; /// Real size of the full image, invalid unless the thumbnail /// represents a raster image (not an icon) QSize mRealFullSize; /// Whether mAdjustedPix represents has been scaled using fast or smooth //transformation bool mRough; /// Set to true if mGroupPix should be replaced with a real thumbnail bool mWaitingForThumbnail; }; typedef QHash ThumbnailForUrl; typedef QQueue UrlQueue; typedef QSet PersistentModelIndexSet; struct ThumbnailViewPrivate { ThumbnailView* q; ThumbnailView::ThumbnailScaleMode mScaleMode; QSize mThumbnailSize; qreal mThumbnailAspectRatio; AbstractDocumentInfoProvider* mDocumentInfoProvider; AbstractThumbnailViewHelper* mThumbnailViewHelper; ThumbnailForUrl mThumbnailForUrl; QTimer mScheduledThumbnailGenerationTimer; UrlQueue mSmoothThumbnailQueue; QTimer mSmoothThumbnailTimer; QPixmap mWaitingThumbnail; QPointer mThumbnailLoadJob; PersistentModelIndexSet mBusyIndexSet; KPixmapSequence mBusySequence; QTimeLine* mBusyAnimationTimeLine; + bool mCreateThumbnailsForRemoteUrls; + void setupBusyAnimation() { mBusySequence = KPixmapSequence("process-working", 22); mBusyAnimationTimeLine = new QTimeLine(100 * mBusySequence.frameCount(), q); mBusyAnimationTimeLine->setCurveShape(QTimeLine::LinearCurve); mBusyAnimationTimeLine->setEndFrame(mBusySequence.frameCount() - 1); mBusyAnimationTimeLine->setLoopCount(0); QObject::connect(mBusyAnimationTimeLine, SIGNAL(frameChanged(int)), q, SLOT(updateBusyIndexes())); } void scheduleThumbnailGenerationForVisibleItems() { if (mThumbnailLoadJob) { mThumbnailLoadJob->removeItems(mThumbnailLoadJob->pendingItems()); } mSmoothThumbnailQueue.clear(); mScheduledThumbnailGenerationTimer.start(); } void updateThumbnailForModifiedDocument(const QModelIndex& index) { Q_ASSERT(mDocumentInfoProvider); KFileItem item = fileItemForIndex(index); KUrl url = item.url(); ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width()); QPixmap pix; QSize fullSize; mDocumentInfoProvider->thumbnailForDocument(url, group, &pix, &fullSize); mThumbnailForUrl[url] = Thumbnail(QPersistentModelIndex(index), KDateTime::currentLocalDateTime()); q->setThumbnail(item, pix, fullSize); } void generateThumbnailsForItems(const KFileItemList& list) { ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width()); if (!mThumbnailLoadJob) { mThumbnailLoadJob = new ThumbnailLoadJob(list, group); QObject::connect(mThumbnailLoadJob, SIGNAL(thumbnailLoaded(KFileItem,QPixmap,QSize)), q, SLOT(setThumbnail(KFileItem,QPixmap,QSize))); QObject::connect(mThumbnailLoadJob, SIGNAL(thumbnailLoadingFailed(KFileItem)), q, SLOT(setBrokenThumbnail(KFileItem))); mThumbnailLoadJob->start(); } else { mThumbnailLoadJob->setThumbnailGroup(group); Q_FOREACH(const KFileItem & item, list) { mThumbnailLoadJob->appendItem(item); } } } void roughAdjustThumbnail(Thumbnail* thumbnail) { const QPixmap& mGroupPix = thumbnail->mGroupPix; const int groupSize = qMax(mGroupPix.width(), mGroupPix.height()); const int fullSize = qMax(thumbnail->mFullSize.width(), thumbnail->mFullSize.height()); if (fullSize == groupSize && mGroupPix.height() <= mThumbnailSize.height() && mGroupPix.width() <= mThumbnailSize.width()) { thumbnail->mAdjustedPix = mGroupPix; thumbnail->mRough = false; } else { thumbnail->mAdjustedPix = scale(mGroupPix, Qt::FastTransformation); thumbnail->mRough = true; } } void initDragPixmap(QDrag* drag, const QModelIndexList& indexes) { const int thumbCount = qMin(indexes.count(), int(DragPixmapGenerator::MaxCount)); QList lst; for (int row = 0; row < thumbCount; ++row) { const KUrl url = urlForIndex(indexes[row]); lst << mThumbnailForUrl.value(url).mAdjustedPix; } DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate(lst, indexes.count()); drag->setPixmap(dragPixmap.pix); drag->setHotSpot(dragPixmap.hotSpot); } QPixmap scale(const QPixmap& pix, Qt::TransformationMode transformationMode) { switch (mScaleMode) { case ThumbnailView::ScaleToFit: return pix.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode); break; case ThumbnailView::ScaleToSquare: { int minSize = qMin(pix.width(), pix.height()); QPixmap pix2 = pix.copy((pix.width() - minSize) / 2, (pix.height() - minSize) / 2, minSize, minSize); return pix2.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode); } case ThumbnailView::ScaleToHeight: return pix.scaledToHeight(mThumbnailSize.height(), transformationMode); break; case ThumbnailView::ScaleToWidth: return pix.scaledToWidth(mThumbnailSize.width(), transformationMode); break; } // Keep compiler happy Q_ASSERT(0); return QPixmap(); } }; ThumbnailView::ThumbnailView(QWidget* parent) : QListView(parent) , d(new ThumbnailViewPrivate) { d->q = this; d->mScaleMode = ScaleToFit; d->mThumbnailViewHelper = 0; d->mDocumentInfoProvider = 0; // Init to some stupid value so that the first call to setThumbnailSize() // is not ignored (do not use 0 in case someone try to divide by // mThumbnailSize...) d->mThumbnailSize = QSize(1, 1); d->mThumbnailAspectRatio = 1; + d->mCreateThumbnailsForRemoteUrls = true; setFrameShape(QFrame::NoFrame); setViewMode(QListView::IconMode); setResizeMode(QListView::Adjust); setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(true); setUniformItemSizes(true); setEditTriggers(QAbstractItemView::EditKeyPressed); d->setupBusyAnimation(); setVerticalScrollMode(ScrollPerPixel); setHorizontalScrollMode(ScrollPerPixel); d->mScheduledThumbnailGenerationTimer.setSingleShot(true); d->mScheduledThumbnailGenerationTimer.setInterval(500); connect(&d->mScheduledThumbnailGenerationTimer, SIGNAL(timeout()), SLOT(generateThumbnailsForVisibleItems())); d->mSmoothThumbnailTimer.setSingleShot(true); connect(&d->mSmoothThumbnailTimer, SIGNAL(timeout()), SLOT(smoothNextThumbnail())); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, SIGNAL(customContextMenuRequested(QPoint)), SLOT(showContextMenu())); if (KGlobalSettings::singleClick()) { connect(this, SIGNAL(clicked(QModelIndex)), SLOT(emitIndexActivatedIfNoModifiers(QModelIndex))); } else { connect(this, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitIndexActivatedIfNoModifiers(QModelIndex))); } } ThumbnailView::~ThumbnailView() { delete d->mThumbnailLoadJob; delete d; } ThumbnailView::ThumbnailScaleMode ThumbnailView::thumbnailScaleMode() const { return d->mScaleMode; } void ThumbnailView::setThumbnailScaleMode(ThumbnailScaleMode mode) { d->mScaleMode = mode; setUniformItemSizes(mode == ScaleToFit || mode == ScaleToSquare); } void ThumbnailView::setModel(QAbstractItemModel* newModel) { if (model()) { disconnect(model(), 0, this, 0); } QListView::setModel(newModel); connect(model(), SIGNAL(rowsRemoved(QModelIndex,int,int)), SIGNAL(rowsRemovedSignal(QModelIndex,int,int))); } void ThumbnailView::updateThumbnailSize() { QSize value = d->mThumbnailSize; // mWaitingThumbnail int waitingThumbnailSize; if (value.width() > 64) { waitingThumbnailSize = 48; } else { waitingThumbnailSize = 32; } QPixmap icon = DesktopIcon("chronometer", waitingThumbnailSize); QPixmap pix(value); pix.fill(Qt::transparent); QPainter painter(&pix); painter.setOpacity(0.5); painter.drawPixmap((value.width() - icon.width()) / 2, (value.height() - icon.height()) / 2, icon); painter.end(); d->mWaitingThumbnail = pix; // Stop smoothing d->mSmoothThumbnailTimer.stop(); d->mSmoothThumbnailQueue.clear(); // Clear adjustedPixes ThumbnailForUrl::iterator it = d->mThumbnailForUrl.begin(), end = d->mThumbnailForUrl.end(); for (; it != end; ++it) { it.value().mAdjustedPix = QPixmap(); } thumbnailSizeChanged(value); thumbnailWidthChanged(value.width()); if (d->mScaleMode != ScaleToFit) { scheduleDelayedItemsLayout(); } d->scheduleThumbnailGenerationForVisibleItems(); } void ThumbnailView::setThumbnailWidth(int width) { if(d->mThumbnailSize.width() == width) { return; } int height = round((qreal)width / d->mThumbnailAspectRatio); d->mThumbnailSize = QSize(width, height); updateThumbnailSize(); } void ThumbnailView::setThumbnailAspectRatio(qreal ratio) { if(d->mThumbnailAspectRatio == ratio) { return; } d->mThumbnailAspectRatio = ratio; int width = d->mThumbnailSize.width(); int height = round((qreal)width / d->mThumbnailAspectRatio); d->mThumbnailSize = QSize(width, height); updateThumbnailSize(); } qreal ThumbnailView::thumbnailAspectRatio() const { return d->mThumbnailAspectRatio; } QSize ThumbnailView::thumbnailSize() const { return d->mThumbnailSize; } void ThumbnailView::setThumbnailViewHelper(AbstractThumbnailViewHelper* helper) { d->mThumbnailViewHelper = helper; } AbstractThumbnailViewHelper* ThumbnailView::thumbnailViewHelper() const { return d->mThumbnailViewHelper; } void ThumbnailView::setDocumentInfoProvider(AbstractDocumentInfoProvider* provider) { d->mDocumentInfoProvider = provider; if (provider) { connect(provider, SIGNAL(busyStateChanged(QModelIndex,bool)), SLOT(updateThumbnailBusyState(QModelIndex,bool))); connect(provider, SIGNAL(documentChanged(QModelIndex)), SLOT(updateThumbnail(QModelIndex))); } } AbstractDocumentInfoProvider* ThumbnailView::documentInfoProvider() const { return d->mDocumentInfoProvider; } void ThumbnailView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { QListView::rowsAboutToBeRemoved(parent, start, end); // Remove references to removed items KFileItemList itemList; for (int pos = start; pos <= end; ++pos) { QModelIndex index = model()->index(pos, 0, parent); KFileItem item = fileItemForIndex(index); if (item.isNull()) { kDebug() << "Skipping invalid item!" << index.data().toString(); continue; } QUrl url = item.url(); d->mThumbnailForUrl.remove(url); d->mSmoothThumbnailQueue.removeAll(url); itemList.append(item); } if (d->mThumbnailLoadJob) { d->mThumbnailLoadJob->removeItems(itemList); } // Update current index if it is among the deleted rows const int row = currentIndex().row(); if (start <= row && row <= end) { QModelIndex index; if (end < model()->rowCount() - 1) { index = model()->index(end + 1, 0); } else if (start > 0) { index = model()->index(start - 1, 0); } setCurrentIndex(index); } // Removing rows might make new images visible, make sure their thumbnail // is generated d->mScheduledThumbnailGenerationTimer.start(); } void ThumbnailView::rowsInserted(const QModelIndex& parent, int start, int end) { QListView::rowsInserted(parent, start, end); d->mScheduledThumbnailGenerationTimer.start(); rowsInsertedSignal(parent, start, end); } void ThumbnailView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { QListView::dataChanged(topLeft, bottomRight); bool thumbnailsNeedRefresh = false; for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { QModelIndex index = model()->index(row, 0); KFileItem item = fileItemForIndex(index); if (item.isNull()) { kWarning() << "Invalid item for index" << index << ". This should not happen!"; continue; } ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(item.url()); if (it != d->mThumbnailForUrl.end()) { // All thumbnail views are connected to the model, so // ThumbnailView::dataChanged() is called for all of them. As a // result this method will also be called for views which are not // currently visible, and do not yet have a thumbnail for the // modified url. KDateTime mtime = item.time(KFileItem::ModificationTime); if (it->mModificationTime != mtime) { // dataChanged() is called when the file changes but also when // the model fetched additional data such as semantic info. To // avoid needless refreshes, we only trigger a refresh if the // modification time changes. thumbnailsNeedRefresh = true; it->prepareForRefresh(mtime); } } } if (thumbnailsNeedRefresh) { d->mScheduledThumbnailGenerationTimer.start(); } } void ThumbnailView::showContextMenu() { d->mThumbnailViewHelper->showContextMenu(this); } void ThumbnailView::emitIndexActivatedIfNoModifiers(const QModelIndex& index) { if (QApplication::keyboardModifiers() == Qt::NoModifier) { emit indexActivated(index); } } void ThumbnailView::setThumbnail(const KFileItem& item, const QPixmap& pixmap, const QSize& size) { ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url()); if (it == d->mThumbnailForUrl.end()) { return; } Thumbnail& thumbnail = it.value(); thumbnail.mGroupPix = pixmap; thumbnail.mAdjustedPix = QPixmap(); int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large); thumbnail.mFullSize = size.isValid() ? size : QSize(largeGroupSize, largeGroupSize); thumbnail.mRealFullSize = size; thumbnail.mWaitingForThumbnail = false; update(thumbnail.mIndex); if (d->mScaleMode != ScaleToFit) { scheduleDelayedItemsLayout(); } } void ThumbnailView::setBrokenThumbnail(const KFileItem& item) { ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url()); if (it == d->mThumbnailForUrl.end()) { return; } Thumbnail& thumbnail = it.value(); MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); if (kind == MimeTypeUtils::KIND_VIDEO) { // Special case for videos because our kde install may come without // support for video thumbnails so we show the mimetype icon instead of // a broken image icon ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(d->mThumbnailSize.height()); QPixmap pix = item.pixmap(ThumbnailGroup::pixelSize(group)); thumbnail.initAsIcon(pix); } else if (kind == MimeTypeUtils::KIND_DIR) { // Special case for folders because ThumbnailLoadJob does not return a // thumbnail if there is no images thumbnail.mWaitingForThumbnail = false; return; } else { thumbnail.initAsIcon(DesktopIcon("image-missing", 48)); thumbnail.mFullSize = thumbnail.mGroupPix.size(); } update(thumbnail.mIndex); } QPixmap ThumbnailView::thumbnailForIndex(const QModelIndex& index, QSize* fullSize) { KFileItem item = fileItemForIndex(index); if (item.isNull()) { LOG("Invalid item"); if (fullSize) { *fullSize = QSize(); } return QPixmap(); } KUrl url = item.url(); // Find or create Thumbnail instance ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime)); it = d->mThumbnailForUrl.insert(url, thumbnail); } Thumbnail& thumbnail = it.value(); // If dir or archive, generate a thumbnail from fileitem pixmap MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); if (kind == MimeTypeUtils::KIND_ARCHIVE || kind == MimeTypeUtils::KIND_DIR) { int groupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::fromPixelSize(d->mThumbnailSize.height())); if (thumbnail.mGroupPix.isNull() || thumbnail.mGroupPix.height() < groupSize) { QPixmap pix = item.pixmap(groupSize); thumbnail.initAsIcon(pix); if (kind == MimeTypeUtils::KIND_ARCHIVE) { // No thumbnails for archives thumbnail.mWaitingForThumbnail = false; + } else if (!d->mCreateThumbnailsForRemoteUrls && !UrlUtils::urlIsFastLocalFile(url)) { + // If we don't want thumbnails for remote urls, use + // "folder-remote" icon for remote folders, so that they do + // not look like regular folders + thumbnail.mWaitingForThumbnail = false; + thumbnail.initAsIcon(DesktopIcon("folder-remote", groupSize)); } else { // set mWaitingForThumbnail to true (necessary in the case // 'thumbnail' already existed before, but with a too small // mGroupPix) thumbnail.mWaitingForThumbnail = true; } } } if (thumbnail.mGroupPix.isNull()) { if (fullSize) { *fullSize = QSize(); } return d->mWaitingThumbnail; } // Adjust thumbnail if (thumbnail.mAdjustedPix.isNull()) { d->roughAdjustThumbnail(&thumbnail); } if (thumbnail.mRough && !d->mSmoothThumbnailQueue.contains(url)) { d->mSmoothThumbnailQueue.enqueue(url); if (!d->mSmoothThumbnailTimer.isActive()) { d->mSmoothThumbnailTimer.start(SMOOTH_DELAY); } } if (fullSize) { *fullSize = thumbnail.mRealFullSize; } return thumbnail.mAdjustedPix; } bool ThumbnailView::isModified(const QModelIndex& index) const { if (!d->mDocumentInfoProvider) { return false; } KUrl url = urlForIndex(index); return d->mDocumentInfoProvider->isModified(url); } bool ThumbnailView::isBusy(const QModelIndex& index) const { if (!d->mDocumentInfoProvider) { return false; } KUrl url = urlForIndex(index); return d->mDocumentInfoProvider->isBusy(url); } void ThumbnailView::startDrag(Qt::DropActions supportedActions) { QModelIndexList indexes = selectionModel()->selectedIndexes(); if (indexes.isEmpty()) { return; } QDrag* drag = new QDrag(this); drag->setMimeData(model()->mimeData(indexes)); d->initDragPixmap(drag, indexes); drag->exec(supportedActions, Qt::CopyAction); } void ThumbnailView::dragEnterEvent(QDragEnterEvent* event) { QAbstractItemView::dragEnterEvent(event); if (event->mimeData()->hasUrls()) { event->acceptProposedAction(); } } void ThumbnailView::dragMoveEvent(QDragMoveEvent* event) { // Necessary, otherwise we don't reach dropEvent() QAbstractItemView::dragMoveEvent(event); event->acceptProposedAction(); } void ThumbnailView::dropEvent(QDropEvent* event) { const KUrl::List urlList = KUrl::List::fromMimeData(event->mimeData()); if (urlList.isEmpty()) { return; } QModelIndex destIndex = indexAt(event->pos()); if (destIndex.isValid()) { KFileItem item = fileItemForIndex(destIndex); if (item.isDir()) { KUrl destUrl = item.url(); d->mThumbnailViewHelper->showMenuForUrlDroppedOnDir(this, urlList, destUrl); return; } } d->mThumbnailViewHelper->showMenuForUrlDroppedOnViewport(this, urlList); event->acceptProposedAction(); } void ThumbnailView::keyPressEvent(QKeyEvent* event) { QListView::keyPressEvent(event); if (event->key() == Qt::Key_Return) { const QModelIndex index = selectionModel()->currentIndex(); if (index.isValid() && selectionModel()->selectedIndexes().count() == 1) { emit indexActivated(index); } } } void ThumbnailView::resizeEvent(QResizeEvent* event) { QListView::resizeEvent(event); d->scheduleThumbnailGenerationForVisibleItems(); } void ThumbnailView::showEvent(QShowEvent* event) { QListView::showEvent(event); d->scheduleThumbnailGenerationForVisibleItems(); QTimer::singleShot(0, this, SLOT(scrollToSelectedIndex())); } void ThumbnailView::wheelEvent(QWheelEvent* event) { // If we don't adjust the single step, the wheel scroll exactly one item up // and down, giving the impression that the items do not move but only // their label changes. // For some reason it is necessary to set the step here: setting it in // setThumbnailSize() does not work //verticalScrollBar()->setSingleStep(d->mThumbnailSize / 5); if (event->modifiers() == Qt::ControlModifier) { int width = d->mThumbnailSize.width() + (event->delta() > 0 ? 1 : -1) * WHEEL_ZOOM_MULTIPLIER; width = qMax(int(MinThumbnailSize), qMin(width, int(MaxThumbnailSize))); setThumbnailWidth(width); } else { QListView::wheelEvent(event); } } void ThumbnailView::scrollToSelectedIndex() { QModelIndexList list = selectedIndexes(); if (list.count() >= 1) { scrollTo(list.first(), PositionAtCenter); } } void ThumbnailView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { QListView::selectionChanged(selected, deselected); emit selectionChangedSignal(selected, deselected); } void ThumbnailView::scrollContentsBy(int dx, int dy) { QListView::scrollContentsBy(dx, dy); d->scheduleThumbnailGenerationForVisibleItems(); } void ThumbnailView::generateThumbnailsForVisibleItems() { if (!isVisible() || !model()) { return; } const QRect visibleRect = viewport()->rect(); const int visibleSurface = visibleRect.width() * visibleRect.height(); const QPoint origin = visibleRect.center(); // distance => item QMultiMap itemMap; for (int row = 0; row < model()->rowCount(); ++row) { QModelIndex index = model()->index(row, 0); KFileItem item = fileItemForIndex(index); QUrl url = item.url(); + // Filter out remote items if necessary + if (!d->mCreateThumbnailsForRemoteUrls && !url.isLocalFile()) { + continue; + } + // Filter out archives MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); if (kind == MimeTypeUtils::KIND_ARCHIVE) { continue; } // Immediately update modified items if (d->mDocumentInfoProvider && d->mDocumentInfoProvider->isModified(url)) { d->updateThumbnailForModifiedDocument(index); continue; } // Filter out items which already have a thumbnail ThumbnailForUrl::ConstIterator it = d->mThumbnailForUrl.constFind(url); if (it != d->mThumbnailForUrl.constEnd() && it.value().isGroupPixAdaptedForSize(d->mThumbnailSize.height())) { continue; } // Compute distance int distance; const QRect itemRect = visualRect(index); const qreal itemSurface = itemRect.width() * itemRect.height(); const QRect visibleItemRect = visibleRect.intersected(itemRect); qreal visibleItemFract = 0; if (itemSurface > 0) { visibleItemFract = visibleItemRect.width() * visibleItemRect.height() / itemSurface; } if (visibleItemFract > 0.7) { // Item is visible, order thumbnails from left to right, top to bottom // Distance is computed so that it is between 0 and visibleSurface distance = itemRect.top() * visibleRect.width() + itemRect.left(); // Make sure directory thumbnails are generated after image thumbnails: // Distance is between visibleSurface and 2 * visibleSurface if (kind == MimeTypeUtils::KIND_DIR) { distance = distance + visibleSurface; } } else { // Item is not visible, order thumbnails according to distance // Start at 2 * visibleSurface to ensure invisible thumbnails are // generated *after* visible thumbnails distance = 2 * visibleSurface + (itemRect.center() - origin).manhattanLength(); } // Add the item to our map itemMap.insert(distance, item); // Insert the thumbnail in mThumbnailForUrl, so that // setThumbnail() can find the item to update if (it == d->mThumbnailForUrl.constEnd()) { Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime)); d->mThumbnailForUrl.insert(url, thumbnail); } } if (!itemMap.isEmpty()) { d->generateThumbnailsForItems(itemMap.values()); } } void ThumbnailView::updateThumbnail(const QModelIndex& index) { KFileItem item = fileItemForIndex(index); KUrl url = item.url(); if (d->mDocumentInfoProvider && d->mDocumentInfoProvider->isModified(url)) { d->updateThumbnailForModifiedDocument(index); } else { KFileItemList list; list << item; d->generateThumbnailsForItems(list); } } void ThumbnailView::updateThumbnailBusyState(const QModelIndex& _index, bool busy) { QPersistentModelIndex index(_index); if (busy && !d->mBusyIndexSet.contains(index)) { d->mBusyIndexSet << index; update(index); if (d->mBusyAnimationTimeLine->state() != QTimeLine::Running) { d->mBusyAnimationTimeLine->start(); } } else if (!busy && d->mBusyIndexSet.remove(index)) { update(index); if (d->mBusyIndexSet.isEmpty()) { d->mBusyAnimationTimeLine->stop(); } } } void ThumbnailView::updateBusyIndexes() { Q_FOREACH(const QPersistentModelIndex & index, d->mBusyIndexSet) { update(index); } } QPixmap ThumbnailView::busySequenceCurrentPixmap() const { return d->mBusySequence.frameAt(d->mBusyAnimationTimeLine->currentFrame()); } void ThumbnailView::smoothNextThumbnail() { if (d->mSmoothThumbnailQueue.isEmpty()) { return; } if (d->mThumbnailLoadJob) { // give mThumbnailLoadJob priority over smoothing d->mSmoothThumbnailTimer.start(SMOOTH_DELAY); return; } KUrl url = d->mSmoothThumbnailQueue.dequeue(); ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { kWarning() << url << " not in mThumbnailForUrl. This should not happen!"; return; } Thumbnail& thumbnail = it.value(); thumbnail.mAdjustedPix = d->scale(thumbnail.mGroupPix, Qt::SmoothTransformation); thumbnail.mRough = false; if (thumbnail.mIndex.isValid()) { update(thumbnail.mIndex); } else { kWarning() << "index for" << url << "is invalid. This should not happen!"; } if (!d->mSmoothThumbnailQueue.isEmpty()) { d->mSmoothThumbnailTimer.start(0); } } void ThumbnailView::reloadThumbnail(const QModelIndex& index) { KUrl url = urlForIndex(index); if (!url.isValid()) { kWarning() << "Invalid url for index" << index; return; } ThumbnailLoadJob::deleteImageThumbnail(url); ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { return; } d->mThumbnailForUrl.erase(it); generateThumbnailsForVisibleItems(); } +void ThumbnailView::setCreateThumbnailsForRemoteUrls(bool createRemoteThumbs) +{ + d->mCreateThumbnailsForRemoteUrls = createRemoteThumbs; +} + } // namespace diff --git a/lib/thumbnailview/thumbnailview.h b/lib/thumbnailview/thumbnailview.h index 02ef4415..37168226 100644 --- a/lib/thumbnailview/thumbnailview.h +++ b/lib/thumbnailview/thumbnailview.h @@ -1,209 +1,211 @@ /* 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. */ #ifndef THUMBNAILVIEW_H #define THUMBNAILVIEW_H #include // Qt #include // KDE #include class KFileItem; class QDragEnterEvent; class QDragMoveEvent; class QDropEvent; class QPixmap; namespace Gwenview { class AbstractDocumentInfoProvider; class AbstractThumbnailViewHelper; struct ThumbnailViewPrivate; class GWENVIEWLIB_EXPORT ThumbnailView : public QListView { Q_OBJECT public: enum { MinThumbnailSize = 48, MaxThumbnailSize = 256 }; enum ThumbnailScaleMode { ScaleToSquare, ScaleToHeight, ScaleToWidth, ScaleToFit }; ThumbnailView(QWidget* parent); ~ThumbnailView(); void setThumbnailViewHelper(AbstractThumbnailViewHelper* helper); AbstractThumbnailViewHelper* thumbnailViewHelper() const; void setDocumentInfoProvider(AbstractDocumentInfoProvider* provider); AbstractDocumentInfoProvider* documentInfoProvider() const; ThumbnailScaleMode thumbnailScaleMode() const; void setThumbnailScaleMode(ThumbnailScaleMode); /** * Returns the thumbnail size. */ QSize thumbnailSize() const; /** * Returns the aspect ratio of the thumbnail. */ qreal thumbnailAspectRatio() const; QPixmap thumbnailForIndex(const QModelIndex&, QSize* fullSize = 0); /** * Returns true if the document pointed by the index has been modified * inside Gwenview. */ bool isModified(const QModelIndex&) const; /** * Returns true if the document pointed by the index is currently busy * (loading, saving, rotating...) */ bool isBusy(const QModelIndex& index) const; virtual void setModel(QAbstractItemModel* model); /** * Publish this method so that delegates can call it. */ using QListView::scheduleDelayedItemsLayout; /** * Returns the current pixmap to paint when drawing a busy index. */ QPixmap busySequenceCurrentPixmap() const; void reloadThumbnail(const QModelIndex&); void updateThumbnailSize(); + void setCreateThumbnailsForRemoteUrls(bool createRemoteThumbs); + Q_SIGNALS: /** * It seems we can't use the 'activated()' signal for now because it does * not know about KDE single vs doubleclick settings. The indexActivated() * signal replaces it. */ void indexActivated(const QModelIndex&); void urlListDropped(const KUrl::List& lst, const KUrl& destination); void thumbnailSizeChanged(const QSize&); void thumbnailWidthChanged(int); /** * Emitted whenever selectionChanged() is called. * This signal is suffixed with "Signal" because * QAbstractItemView::selectionChanged() is a slot. */ void selectionChangedSignal(const QItemSelection&, const QItemSelection&); /** * Forward some signals from model, so that the delegate can use them */ void rowsRemovedSignal(const QModelIndex& parent, int start, int end); void rowsInsertedSignal(const QModelIndex& parent, int start, int end); public Q_SLOTS: /** * Sets the thumbnail's width, in pixels. Keeps aspect ratio unchanged. */ void setThumbnailWidth(int width); /** * Sets the thumbnail's aspect ratio. Keeps width unchanged. */ void setThumbnailAspectRatio(qreal ratio); void scrollToSelectedIndex(); protected: virtual void dragEnterEvent(QDragEnterEvent*); virtual void dragMoveEvent(QDragMoveEvent*); virtual void dropEvent(QDropEvent*); virtual void keyPressEvent(QKeyEvent*); virtual void resizeEvent(QResizeEvent*); virtual void scrollContentsBy(int dx, int dy); virtual void showEvent(QShowEvent*); virtual void wheelEvent(QWheelEvent*); virtual void startDrag(Qt::DropActions); protected Q_SLOTS: virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); virtual void rowsInserted(const QModelIndex& parent, int start, int end); virtual void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected); virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); private Q_SLOTS: void showContextMenu(); void emitIndexActivatedIfNoModifiers(const QModelIndex&); void setThumbnail(const KFileItem&, const QPixmap&, const QSize&); void setBrokenThumbnail(const KFileItem&); /** * Generate thumbnail for @a index. */ void updateThumbnail(const QModelIndex& index); /** * Tells the view the busy state of the document pointed by the index has changed. */ void updateThumbnailBusyState(const QModelIndex& index, bool); /* * Cause a repaint of all busy indexes */ void updateBusyIndexes(); void generateThumbnailsForVisibleItems(); void smoothNextThumbnail(); private: friend struct ThumbnailViewPrivate; ThumbnailViewPrivate * const d; }; } // namespace #endif /* THUMBNAILVIEW_H */