diff --git a/app/advancedconfigpage.ui b/app/advancedconfigpage.ui index 6003484a..4c580be1 100644 --- a/app/advancedconfigpage.ui +++ b/app/advancedconfigpage.ui @@ -1,218 +1,218 @@ AdvancedConfigPage 0 0 482 300 Thumbnails: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + Low resource usage mode Qt::Horizontal 40 20 Enable this option if you do not have a lot of disk space. This will avoid storing thumbnails on disk and will prefer embedded thumbnails of lower quality that are faster to load, if available.<br/><br/><em>Be careful:</em> this will delete the folder named <filename>.thumbnails</filename> in your home folder, deleting all thumbnails previously generated by Gwenview and other applications. true Qt::Vertical QSizePolicy::Fixed 20 20 History: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Remember folders and URLs Qt::Horizontal 40 20 Qt::Vertical QSizePolicy::Fixed 20 20 Rendering intent: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter relativeRenderingIntentRadioButton Perceptual true Scale all colors equally to fit them within the active color profile's color gamut true Qt::Vertical QSizePolicy::Fixed 20 10 Relative Colorimetric true Squash colors that don't fit within the active color profile's color gamut, leaving other colors untouched true Qt::Vertical 20 20 diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index b764e774..431cf95a 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -1,1815 +1,1815 @@ /* 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 "mainwindow.h" #include #include "dialogguard.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_OSX #include #endif #include #include // KDE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Local #include "configdialog.h" #include "documentinfoprovider.h" #include "viewmainpage.h" #include "fileopscontextmanageritem.h" #include "folderviewcontextmanageritem.h" #include "fullscreencontent.h" #include "gvcore.h" #include "imageopscontextmanageritem.h" #include "infocontextmanageritem.h" #ifdef KIPI_FOUND #include "kipiexportaction.h" #include "kipiinterface.h" #endif #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE #include "semanticinfocontextmanageritem.h" #endif #include "preloader.h" #include "savebar.h" #include "sidebar.h" #include "splitter.h" #include "startmainpage.h" #include "thumbnailviewhelper.h" #include "browsemainpage.h" #include #include #include #include #include #include #include #include #include #ifdef HAVE_QTDBUS #include #endif #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 static const int BROWSE_PRELOAD_DELAY = 1000; static const int VIEW_PRELOAD_DELAY = 100; static const char* SESSION_CURRENT_PAGE_KEY = "Page"; static const char* SESSION_URL_KEY = "Url"; enum MainPageId { StartMainPageId, BrowseMainPageId, ViewMainPageId }; struct MainWindowState { bool mToolBarVisible; }; /* Layout of the main window looks like this: .-mCentralSplitter-----------------------------. |.-mSideBar--. .-mContentWidget---------------.| || | |.-mSaveBar-------------------.|| || | || ||| || | |'----------------------------'|| || | |.-mViewStackedWidget---------.|| || | || ||| || | || ||| || | || ||| || | || ||| || | |'----------------------------'|| |'-----------' '------------------------------'| '----------------------------------------------' */ struct MainWindow::Private { GvCore* mGvCore; MainWindow* q; QSplitter* mCentralSplitter; QWidget* mContentWidget; ViewMainPage* mViewMainPage; KUrlNavigator* mUrlNavigator; ThumbnailView* mThumbnailView; ThumbnailView* mActiveThumbnailView; DocumentInfoProvider* mDocumentInfoProvider; ThumbnailViewHelper* mThumbnailViewHelper; QPointer mThumbnailProvider; BrowseMainPage* mBrowseMainPage; StartMainPage* mStartMainPage; SideBar* mSideBar; QStackedWidget* mViewStackedWidget; FullScreenContent* mFullScreenContent; SaveBar* mSaveBar; bool mStartSlideShowWhenDirListerCompleted; SlideShow* mSlideShow; #ifdef HAVE_QTDBUS Mpris2Service* mMpris2Service; #endif Preloader* mPreloader; bool mPreloadDirectionIsForward; #ifdef KIPI_FOUND KIPIInterface* mKIPIInterface; #endif QActionGroup* mViewModeActionGroup; KRecentFilesAction* mFileOpenRecentAction; QAction * mBrowseAction; QAction * mViewAction; QAction * mGoUpAction; QAction * mGoToPreviousAction; QAction * mGoToNextAction; QAction * mGoToFirstAction; QAction * mGoToLastAction; KToggleAction* mToggleSideBarAction; QAction* mFullScreenAction; QAction * mToggleSlideShowAction; KToggleAction* mShowMenuBarAction; KToggleAction* mShowStatusBarAction; Purpose::Menu* mShareMenu; KToolBarPopupAction* mShareAction; #ifdef KIPI_FOUND KIPIExportAction* mKIPIExportAction; #endif SortedDirModel* mDirModel; DocumentOnlyProxyModel* mThumbnailBarModel; KLinkItemSelectionModel* mThumbnailBarSelectionModel; ContextManager* mContextManager; MainWindowState mStateBeforeFullScreen; QString mCaption; MainPageId mCurrentMainPageId; QDateTime mFullScreenLeftAt; KNotificationRestrictions* mNotificationRestrictions; void setupContextManager() { mContextManager = new ContextManager(mDirModel, q); connect(mContextManager, &ContextManager::selectionChanged, q, &MainWindow::slotSelectionChanged); connect(mContextManager, &ContextManager::currentDirUrlChanged, q, &MainWindow::slotCurrentDirUrlChanged); } void setupWidgets() { mFullScreenContent = new FullScreenContent(q, mGvCore); connect(mContextManager, &ContextManager::currentUrlChanged, mFullScreenContent, &FullScreenContent::setCurrentUrl); mCentralSplitter = new Splitter(Qt::Horizontal, q); q->setCentralWidget(mCentralSplitter); // Left side of splitter mSideBar = new SideBar(mCentralSplitter); // Right side of splitter mContentWidget = new QWidget(mCentralSplitter); mSaveBar = new SaveBar(mContentWidget, q->actionCollection()); connect(mContextManager, &ContextManager::currentUrlChanged, mSaveBar, &SaveBar::setCurrentUrl); mViewStackedWidget = new QStackedWidget(mContentWidget); QVBoxLayout* layout = new QVBoxLayout(mContentWidget); layout->addWidget(mSaveBar); layout->addWidget(mViewStackedWidget); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); //// mStartSlideShowWhenDirListerCompleted = false; mSlideShow = new SlideShow(q); connect(mContextManager, &ContextManager::currentUrlChanged, mSlideShow, &SlideShow::setCurrentUrl); setupThumbnailView(mViewStackedWidget); setupViewMainPage(mViewStackedWidget); setupStartMainPage(mViewStackedWidget); mViewStackedWidget->addWidget(mBrowseMainPage); mViewStackedWidget->addWidget(mViewMainPage); mViewStackedWidget->addWidget(mStartMainPage); mViewStackedWidget->setCurrentWidget(mBrowseMainPage); mCentralSplitter->setStretchFactor(0, 0); mCentralSplitter->setStretchFactor(1, 1); mCentralSplitter->setChildrenCollapsible(false); mThumbnailView->setFocus(); connect(mSaveBar, &SaveBar::requestSaveAll, mGvCore, &GvCore::saveAll); connect(mSaveBar, &SaveBar::goToUrl, q, &MainWindow::goToUrl); connect(mSlideShow, &SlideShow::goToUrl, q, &MainWindow::goToUrl); } void setupThumbnailView(QWidget* parent) { Q_ASSERT(mContextManager); mBrowseMainPage = new BrowseMainPage(parent, q->actionCollection(), mGvCore); mThumbnailView = mBrowseMainPage->thumbnailView(); mThumbnailView->viewport()->installEventFilter(q); mThumbnailView->setSelectionModel(mContextManager->selectionModel()); mUrlNavigator = mBrowseMainPage->urlNavigator(); mDocumentInfoProvider = new DocumentInfoProvider(mDirModel); mThumbnailView->setDocumentInfoProvider(mDocumentInfoProvider); mThumbnailViewHelper = new ThumbnailViewHelper(mDirModel, q->actionCollection()); connect(mContextManager, &ContextManager::currentDirUrlChanged, mThumbnailViewHelper, &ThumbnailViewHelper::setCurrentDirUrl); mThumbnailView->setThumbnailViewHelper(mThumbnailViewHelper); mThumbnailBarSelectionModel = new KLinkItemSelectionModel(mThumbnailBarModel, mContextManager->selectionModel(), q); // Connect thumbnail view connect(mThumbnailView, &ThumbnailView::indexActivated, q, &MainWindow::slotThumbnailViewIndexActivated); // Connect delegate QAbstractItemDelegate* delegate = mThumbnailView->itemDelegate(); connect(delegate, SIGNAL(saveDocumentRequested(QUrl)), mGvCore, SLOT(save(QUrl))); connect(delegate, SIGNAL(rotateDocumentLeftRequested(QUrl)), mGvCore, SLOT(rotateLeft(QUrl))); connect(delegate, SIGNAL(rotateDocumentRightRequested(QUrl)), mGvCore, SLOT(rotateRight(QUrl))); connect(delegate, SIGNAL(showDocumentInFullScreenRequested(QUrl)), q, SLOT(showDocumentInFullScreen(QUrl))); connect(delegate, SIGNAL(setDocumentRatingRequested(QUrl,int)), mGvCore, SLOT(setRating(QUrl,int))); // Connect url navigator connect(mUrlNavigator, &KUrlNavigator::urlChanged, q, &MainWindow::openDirUrl); } void setupViewMainPage(QWidget* parent) { mViewMainPage = new ViewMainPage(parent, mSlideShow, q->actionCollection(), mGvCore); connect(mViewMainPage, &ViewMainPage::captionUpdateRequested, q, &MainWindow::slotUpdateCaption); connect(mViewMainPage, &ViewMainPage::completed, q, &MainWindow::slotPartCompleted); connect(mViewMainPage, &ViewMainPage::previousImageRequested, q, &MainWindow::goToPrevious); connect(mViewMainPage, &ViewMainPage::nextImageRequested, q, &MainWindow::goToNext); connect(mViewMainPage, &ViewMainPage::openUrlRequested, q, &MainWindow::openUrl); connect(mViewMainPage, &ViewMainPage::openDirUrlRequested, q, &MainWindow::openDirUrl); setupThumbnailBar(mViewMainPage->thumbnailBar()); } void setupThumbnailBar(ThumbnailView* bar) { Q_ASSERT(mThumbnailBarModel); Q_ASSERT(mThumbnailBarSelectionModel); Q_ASSERT(mDocumentInfoProvider); Q_ASSERT(mThumbnailViewHelper); bar->setModel(mThumbnailBarModel); bar->setSelectionModel(mThumbnailBarSelectionModel); bar->setDocumentInfoProvider(mDocumentInfoProvider); bar->setThumbnailViewHelper(mThumbnailViewHelper); } void setupStartMainPage(QWidget* parent) { mStartMainPage = new StartMainPage(parent, mGvCore); connect(mStartMainPage, &StartMainPage::urlSelected, q, &MainWindow::slotStartMainPageUrlSelected); connect(mStartMainPage, &StartMainPage::recentFileRemoved, [this](const QUrl& url) { mFileOpenRecentAction->removeUrl(url); }); connect(mStartMainPage, &StartMainPage::recentFilesCleared, [this]() { mFileOpenRecentAction->clear(); }); } void installDisabledActionShortcutMonitor(QAction* action, const char* slot) { DisabledActionShortcutMonitor* monitor = new DisabledActionShortcutMonitor(action, q); connect(monitor, SIGNAL(activated()), q, slot); } void setupActions() { KActionCollection* actionCollection = q->actionCollection(); KActionCategory* file = new KActionCategory(i18nc("@title actions category", "File"), actionCollection); KActionCategory* view = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "View"), actionCollection); file->addAction(KStandardAction::Save, q, SLOT(saveCurrent())); file->addAction(KStandardAction::SaveAs, q, SLOT(saveCurrentAs())); file->addAction(KStandardAction::Open, q, SLOT(openFile())); mFileOpenRecentAction = KStandardAction::openRecent(q, SLOT(openUrl(QUrl)), q); connect(mFileOpenRecentAction, &KRecentFilesAction::recentListCleared, mGvCore, &GvCore::clearRecentFilesAndFolders); QAction * clearAction = mFileOpenRecentAction->menu()->findChild("clear_action"); if (clearAction) { clearAction->setText(i18nc("@action Open Recent menu", "Forget All Files && Folders")); } file->addAction("file_open_recent", mFileOpenRecentAction); file->addAction(KStandardAction::Print, q, SLOT(print())); file->addAction(KStandardAction::Quit, qApp, SLOT(closeAllWindows())); QAction * action = file->addAction("reload", q, SLOT(reload())); action->setText(i18nc("@action reload the currently viewed image", "Reload")); action->setIcon(QIcon::fromTheme("view-refresh")); actionCollection->setDefaultShortcuts(action, KStandardShortcut::reload()); QAction * replaceLocationAction = actionCollection->addAction(QStringLiteral("replace_location")); replaceLocationAction->setText(i18nc("@action:inmenu Navigation Bar", "Replace Location")); actionCollection->setDefaultShortcut(replaceLocationAction, Qt::CTRL + Qt::Key_L); connect(replaceLocationAction, &QAction::triggered, q, &MainWindow::replaceLocation); mBrowseAction = view->addAction("browse"); mBrowseAction->setText(i18nc("@action:intoolbar Switch to file list", "Browse")); mBrowseAction->setToolTip(i18nc("@info:tooltip", "Browse folders for images")); mBrowseAction->setCheckable(true); mBrowseAction->setIcon(QIcon::fromTheme("view-list-icons")); actionCollection->setDefaultShortcut(mBrowseAction, Qt::Key_Escape); connect(mViewMainPage, &ViewMainPage::goToBrowseModeRequested, mBrowseAction, &QAction::trigger); mViewAction = view->addAction("view"); mViewAction->setText(i18nc("@action:intoolbar Switch to image view", "View")); mViewAction->setToolTip(i18nc("@info:tooltip", "View selected images")); mViewAction->setIcon(QIcon::fromTheme("view-preview")); mViewAction->setCheckable(true); mViewModeActionGroup = new QActionGroup(q); mViewModeActionGroup->addAction(mBrowseAction); mViewModeActionGroup->addAction(mViewAction); connect(mViewModeActionGroup, &QActionGroup::triggered, q, &MainWindow::setActiveViewModeAction); mFullScreenAction = KStandardAction::fullScreen(q, &MainWindow::toggleFullScreen, q, actionCollection); QList shortcuts = mFullScreenAction->shortcuts(); shortcuts.append(QKeySequence(Qt::Key_F11)); actionCollection->setDefaultShortcuts(mFullScreenAction, shortcuts); connect(mViewMainPage, &ViewMainPage::toggleFullScreenRequested, mFullScreenAction, &QAction::trigger); QAction * leaveFullScreenAction = view->addAction("leave_fullscreen", q, SLOT(leaveFullScreen())); leaveFullScreenAction->setIcon(QIcon::fromTheme("view-restore")); leaveFullScreenAction->setPriority(QAction::LowPriority); leaveFullScreenAction->setText(i18nc("@action", "Leave Fullscreen Mode")); mGoToPreviousAction = view->addAction("go_previous", q, SLOT(goToPrevious())); mGoToPreviousAction->setPriority(QAction::LowPriority); mGoToPreviousAction->setIcon(QIcon::fromTheme("go-previous-view")); mGoToPreviousAction->setText(i18nc("@action Go to previous image", "Previous")); mGoToPreviousAction->setToolTip(i18nc("@info:tooltip", "Go to previous image")); actionCollection->setDefaultShortcut(mGoToPreviousAction, Qt::Key_Backspace); installDisabledActionShortcutMonitor(mGoToPreviousAction, SLOT(showFirstDocumentReached())); mGoToNextAction = view->addAction("go_next", q, SLOT(goToNext())); mGoToNextAction->setPriority(QAction::LowPriority); mGoToNextAction->setIcon(QIcon::fromTheme("go-next-view")); mGoToNextAction->setText(i18nc("@action Go to next image", "Next")); mGoToNextAction->setToolTip(i18nc("@info:tooltip", "Go to next image")); actionCollection->setDefaultShortcut(mGoToNextAction, Qt::Key_Space); installDisabledActionShortcutMonitor(mGoToNextAction, SLOT(showLastDocumentReached())); mGoToFirstAction = view->addAction("go_first", q, SLOT(goToFirst())); mGoToFirstAction->setPriority(QAction::LowPriority); mGoToFirstAction->setIcon(QIcon::fromTheme("go-first-view")); mGoToFirstAction->setText(i18nc("@action Go to first image", "First")); mGoToFirstAction->setToolTip(i18nc("@info:tooltip", "Go to first image")); actionCollection->setDefaultShortcut(mGoToFirstAction, Qt::Key_Home); mGoToLastAction = view->addAction("go_last", q, SLOT(goToLast())); mGoToLastAction->setPriority(QAction::LowPriority); mGoToLastAction->setIcon(QIcon::fromTheme("go-last-view")); mGoToLastAction->setText(i18nc("@action Go to last image", "Last")); mGoToLastAction->setToolTip(i18nc("@info:tooltip", "Go to last image")); actionCollection->setDefaultShortcut(mGoToLastAction, Qt::Key_End); mPreloadDirectionIsForward = true; mGoUpAction = view->addAction(KStandardAction::Up, q, SLOT(goUp())); action = view->addAction("go_start_page", q, SLOT(showStartMainPage())); action->setPriority(QAction::LowPriority); action->setIcon(QIcon::fromTheme("go-home")); action->setText(i18nc("@action", "Start Page")); action->setToolTip(i18nc("@info:tooltip", "Open the start page")); actionCollection->setDefaultShortcuts(action, KStandardShortcut::home()); mToggleSideBarAction = view->add("toggle_sidebar"); connect(mToggleSideBarAction, &KToggleAction::triggered, q, &MainWindow::toggleSideBar); mToggleSideBarAction->setIcon(QIcon::fromTheme("view-sidetree")); actionCollection->setDefaultShortcut(mToggleSideBarAction, Qt::Key_F4); mToggleSideBarAction->setText(i18nc("@action", "Sidebar")); connect(mBrowseMainPage->toggleSideBarButton(), &QAbstractButton::clicked, mToggleSideBarAction, &QAction::trigger); connect(mViewMainPage->toggleSideBarButton(), &QAbstractButton::clicked, mToggleSideBarAction, &QAction::trigger); mToggleSlideShowAction = view->addAction("toggle_slideshow", q, SLOT(toggleSlideShow())); q->updateSlideShowAction(); connect(mSlideShow, &SlideShow::stateChanged, q, &MainWindow::updateSlideShowAction); q->setStandardToolBarMenuEnabled(true); mShowMenuBarAction = static_cast(view->addAction(KStandardAction::ShowMenubar, q, SLOT(toggleMenuBar()))); mShowStatusBarAction = static_cast(view->addAction(KStandardAction::ShowStatusbar, q, SLOT(toggleStatusBar(bool)))); actionCollection->setDefaultShortcut(mShowStatusBarAction, Qt::Key_F3); view->addAction(KStandardAction::name(KStandardAction::KeyBindings), KStandardAction::keyBindings(q, &MainWindow::configureShortcuts, actionCollection)); view->addAction(KStandardAction::Preferences, q, SLOT(showConfigDialog())); view->addAction(KStandardAction::ConfigureToolbars, q, SLOT(configureToolbars())); #ifdef KIPI_FOUND mKIPIExportAction = new KIPIExportAction(q); #endif mShareAction = new KToolBarPopupAction(QIcon::fromTheme("document-share"), "Share", q); mShareAction->setDelayed(false); actionCollection->addAction("share", mShareAction); mShareMenu = new Purpose::Menu(q); mShareAction->setMenu(mShareMenu); } void setupUndoActions() { // There is no KUndoGroup similar to KUndoStack. This code basically // does the same as KUndoStack, but for the KUndoGroup actions. QUndoGroup* undoGroup = DocumentFactory::instance()->undoGroup(); QAction* action; KActionCollection* actionCollection = q->actionCollection(); KActionCategory* edit = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "Edit"), actionCollection); action = undoGroup->createRedoAction(actionCollection); action->setObjectName(KStandardAction::name(KStandardAction::Redo)); action->setIcon(QIcon::fromTheme("edit-redo")); action->setIconText(i18n("Redo")); actionCollection->setDefaultShortcuts(action, KStandardShortcut::redo()); edit->addAction(action->objectName(), action); action = undoGroup->createUndoAction(actionCollection); action->setObjectName(KStandardAction::name(KStandardAction::Undo)); action->setIcon(QIcon::fromTheme("edit-undo")); action->setIconText(i18n("Undo")); actionCollection->setDefaultShortcuts(action, KStandardShortcut::undo()); edit->addAction(action->objectName(), action); } void setupContextManagerItems() { Q_ASSERT(mContextManager); KActionCollection* actionCollection = q->actionCollection(); // Create context manager items FolderViewContextManagerItem* folderViewItem = new FolderViewContextManagerItem(mContextManager); connect(folderViewItem, &FolderViewContextManagerItem::urlChanged, q, &MainWindow::folderViewUrlChanged); InfoContextManagerItem* infoItem = new InfoContextManagerItem(mContextManager); #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE SemanticInfoContextManagerItem* semanticInfoItem = nullptr; semanticInfoItem = new SemanticInfoContextManagerItem(mContextManager, actionCollection, mViewMainPage); #endif ImageOpsContextManagerItem* imageOpsItem = new ImageOpsContextManagerItem(mContextManager, q); FileOpsContextManagerItem* fileOpsItem = new FileOpsContextManagerItem(mContextManager, mThumbnailView, actionCollection, q); // Fill sidebar SideBarPage* page; page = new SideBarPage(i18n("Folders")); page->setObjectName(QLatin1String("folders")); page->addWidget(folderViewItem->widget()); page->layout()->setContentsMargins(0, 0, 0, 0); mSideBar->addPage(page); page = new SideBarPage(i18n("Information")); page->setObjectName(QLatin1String("information")); page->addWidget(infoItem->widget()); #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE if (semanticInfoItem) { page->addWidget(semanticInfoItem->widget()); } #endif mSideBar->addPage(page); page = new SideBarPage(i18n("Operations")); page->setObjectName(QLatin1String("operations")); page->addWidget(imageOpsItem->widget()); page->addWidget(fileOpsItem->widget()); page->addStretch(); mSideBar->addPage(page); } void initDirModel() { mDirModel->setKindFilter( MimeTypeUtils::KIND_DIR | MimeTypeUtils::KIND_ARCHIVE | MimeTypeUtils::KIND_RASTER_IMAGE | MimeTypeUtils::KIND_SVG_IMAGE | MimeTypeUtils::KIND_VIDEO); connect(mDirModel, &QAbstractItemModel::rowsInserted, q, &MainWindow::slotDirModelNewItems); connect(mDirModel, &QAbstractItemModel::rowsRemoved, q, &MainWindow::updatePreviousNextActions); connect(mDirModel, &QAbstractItemModel::modelReset, q, &MainWindow::updatePreviousNextActions); connect(mDirModel->dirLister(), SIGNAL(completed()), q, SLOT(slotDirListerCompleted())); } void setupThumbnailBarModel() { mThumbnailBarModel = new DocumentOnlyProxyModel(q); mThumbnailBarModel->setSourceModel(mDirModel); } bool indexIsDirOrArchive(const QModelIndex& index) const { Q_ASSERT(index.isValid()); KFileItem item = mDirModel->itemForIndex(index); return ArchiveUtils::fileItemIsDirOrArchive(item); } void goTo(const QModelIndex& index) { if (!index.isValid()) { return; } mThumbnailView->setCurrentIndex(index); mThumbnailView->scrollTo(index); } void goTo(int offset) { mPreloadDirectionIsForward = offset > 0; QModelIndex index = mContextManager->selectionModel()->currentIndex(); index = mDirModel->index(index.row() + offset, 0); if (index.isValid() && !indexIsDirOrArchive(index)) { goTo(index); } } void goToFirstDocument() { QModelIndex index; for (int row = 0;; ++row) { index = mDirModel->index(row, 0); if (!index.isValid()) { return; } if (!indexIsDirOrArchive(index)) { break; } } goTo(index); } void goToLastDocument() { QModelIndex index = mDirModel->index(mDirModel->rowCount() - 1, 0); goTo(index); } void setupFullScreenContent() { mFullScreenContent->init(q->actionCollection(), mViewMainPage, mSlideShow); setupThumbnailBar(mFullScreenContent->thumbnailBar()); } inline void setActionEnabled(const char* name, bool enabled) { QAction* action = q->actionCollection()->action(name); if (action) { action->setEnabled(enabled); } else { qWarning() << "Action" << name << "not found"; } } void setActionsDisabledOnStartMainPageEnabled(bool enabled) { mBrowseAction->setEnabled(enabled); mViewAction->setEnabled(enabled); mToggleSideBarAction->setEnabled(enabled); mShowStatusBarAction->setEnabled(enabled); mFullScreenAction->setEnabled(enabled); mToggleSlideShowAction->setEnabled(enabled); setActionEnabled("reload", enabled); setActionEnabled("go_start_page", enabled); setActionEnabled("add_folder_to_places", enabled); } void updateActions() { bool isRasterImage = false; bool canSave = false; bool isModified = false; const QUrl url = mContextManager->currentUrl(); if (url.isValid()) { isRasterImage = mContextManager->currentUrlIsRasterImage(); canSave = isRasterImage; isModified = DocumentFactory::instance()->load(url)->isModified(); if (mCurrentMainPageId != ViewMainPageId && mContextManager->selectedFileItemList().count() != 1) { // Saving only makes sense if exactly one image is selected canSave = false; } } KActionCollection* actionCollection = q->actionCollection(); actionCollection->action("file_save")->setEnabled(canSave && isModified); actionCollection->action("file_save_as")->setEnabled(canSave); actionCollection->action("file_print")->setEnabled(isRasterImage); if (url.isEmpty()) { mShareAction->setEnabled(false); } else { mShareAction->setEnabled(true); mShareMenu->model()->setInputData(QJsonObject{ { QStringLiteral("mimeType"), MimeTypeUtils::urlMimeType(url) }, { QStringLiteral("urls"), QJsonArray{url.toString()} } }); mShareMenu->model()->setPluginType( QStringLiteral("Export") ); mShareMenu->reload(); } } bool sideBarVisibility() const { switch (mCurrentMainPageId) { case StartMainPageId: GV_WARN_AND_RETURN_VALUE(false, "Sidebar not implemented on start page"); break; case BrowseMainPageId: return GwenviewConfig::sideBarVisibleBrowseMode(); case ViewMainPageId: return q->isFullScreen() ? GwenviewConfig::sideBarVisibleViewModeFullScreen() : GwenviewConfig::sideBarVisibleViewMode(); } return false; } void saveSideBarVisibility(const bool visible) { switch (mCurrentMainPageId) { case StartMainPageId: GV_WARN_AND_RETURN("Sidebar not implemented on start page"); break; case BrowseMainPageId: GwenviewConfig::setSideBarVisibleBrowseMode(visible); break; case ViewMainPageId: q->isFullScreen() ? GwenviewConfig::setSideBarVisibleViewModeFullScreen(visible) : GwenviewConfig::setSideBarVisibleViewMode(visible); break; } } bool statusBarVisibility() const { switch (mCurrentMainPageId) { case StartMainPageId: GV_WARN_AND_RETURN_VALUE(false, "Statusbar not implemented on start page"); break; case BrowseMainPageId: return GwenviewConfig::statusBarVisibleBrowseMode(); case ViewMainPageId: return q->isFullScreen() ? GwenviewConfig::statusBarVisibleViewModeFullScreen() : GwenviewConfig::statusBarVisibleViewMode(); } return false; } void saveStatusBarVisibility(const bool visible) { switch (mCurrentMainPageId) { case StartMainPageId: GV_WARN_AND_RETURN("Statusbar not implemented on start page"); break; case BrowseMainPageId: GwenviewConfig::setStatusBarVisibleBrowseMode(visible); break; case ViewMainPageId: q->isFullScreen() ? GwenviewConfig::setStatusBarVisibleViewModeFullScreen(visible) : GwenviewConfig::setStatusBarVisibleViewMode(visible); break; } } void loadSplitterConfig() { const QList sizes = GwenviewConfig::sideBarSplitterSizes(); if (!sizes.isEmpty()) { mCentralSplitter->setSizes(sizes); } } void saveSplitterConfig() { if (mSideBar->isVisible()) { GwenviewConfig::setSideBarSplitterSizes(mCentralSplitter->sizes()); } } void setScreenSaverEnabled(bool enabled) { // Always delete mNotificationRestrictions, it does not hurt delete mNotificationRestrictions; if (!enabled) { mNotificationRestrictions = new KNotificationRestrictions(KNotificationRestrictions::ScreenSaver, q); } else { mNotificationRestrictions = nullptr; } } void assignThumbnailProviderToThumbnailView(ThumbnailView* thumbnailView) { GV_RETURN_IF_FAIL(thumbnailView); if (mActiveThumbnailView) { mActiveThumbnailView->setThumbnailProvider(nullptr); } thumbnailView->setThumbnailProvider(mThumbnailProvider); mActiveThumbnailView = thumbnailView; if (mActiveThumbnailView->isVisible()) { mThumbnailProvider->stop(); mActiveThumbnailView->generateThumbnailsForItems(); } } void autoAssignThumbnailProvider() { if (mCurrentMainPageId == ViewMainPageId) { if (q->windowState() & Qt::WindowFullScreen) { assignThumbnailProviderToThumbnailView(mFullScreenContent->thumbnailBar()); } else { assignThumbnailProviderToThumbnailView(mViewMainPage->thumbnailBar()); } } else if (mCurrentMainPageId == BrowseMainPageId) { assignThumbnailProviderToThumbnailView(mThumbnailView); } else if (mCurrentMainPageId == StartMainPageId) { assignThumbnailProviderToThumbnailView(mStartMainPage->recentFoldersView()); } } }; MainWindow::MainWindow() : KXmlGuiWindow(), d(new MainWindow::Private) { d->q = this; d->mCurrentMainPageId = StartMainPageId; d->mDirModel = new SortedDirModel(this); d->setupContextManager(); d->setupThumbnailBarModel(); d->mGvCore = new GvCore(this, d->mDirModel); d->mPreloader = new Preloader(this); d->mNotificationRestrictions = nullptr; d->mThumbnailProvider = new ThumbnailProvider(); d->mActiveThumbnailView = nullptr; d->initDirModel(); d->setupWidgets(); d->setupActions(); d->setupUndoActions(); d->setupContextManagerItems(); d->setupFullScreenContent(); d->updateActions(); updatePreviousNextActions(); d->mSaveBar->initActionDependentWidgets(); createGUI(); loadConfig(); connect(DocumentFactory::instance(), &DocumentFactory::modifiedDocumentListChanged, this, &MainWindow::slotModifiedDocumentListChanged); #ifdef HAVE_QTDBUS d->mMpris2Service = new Mpris2Service(d->mSlideShow, d->mContextManager, d->mToggleSlideShowAction, d->mFullScreenAction, d->mGoToPreviousAction, d->mGoToNextAction, this); #endif auto* ratingMenu = static_cast(guiFactory()->container("rating", this)); ratingMenu->setIcon(QIcon::fromTheme(QStringLiteral("rating-unrated"))); #ifdef GWENVIEW_SEMANTICINFO_BACKEND_NONE if (ratingMenu) { ratingMenu->menuAction()->setVisible(false); } #endif #ifdef KIPI_FOUND d->mKIPIInterface = new KIPIInterface(this); d->mKIPIExportAction->setKIPIInterface(d->mKIPIInterface); #else auto* pluginsMenu = static_cast(guiFactory()->container("plugins", this)); if (pluginsMenu) { pluginsMenu->menuAction()->setVisible(false); } #endif setAutoSaveSettings(); #ifdef Q_OS_OSX qApp->installEventFilter(this); #endif } MainWindow::~MainWindow() { - if (GwenviewConfig::deleteThumbnailCacheOnExit()) { + if (GwenviewConfig::lowResourceUsageMode()) { QDir dir(ThumbnailProvider::thumbnailBaseDir()); if (dir.exists()) { dir.removeRecursively(); } } delete d->mThumbnailProvider; delete d; } ContextManager* MainWindow::contextManager() const { return d->mContextManager; } ViewMainPage* MainWindow::viewMainPage() const { return d->mViewMainPage; } void MainWindow::setCaption(const QString& caption) { // Keep a trace of caption to use it in slotModifiedDocumentListChanged() d->mCaption = caption; KXmlGuiWindow::setCaption(caption); } void MainWindow::setCaption(const QString& caption, bool modified) { d->mCaption = caption; KXmlGuiWindow::setCaption(caption, modified); } void MainWindow::slotUpdateCaption(const QString& caption) { const QUrl url = d->mContextManager->currentUrl(); const QList list = DocumentFactory::instance()->modifiedDocumentList(); setCaption(caption, list.contains(url)); } void MainWindow::slotModifiedDocumentListChanged() { d->updateActions(); slotUpdateCaption(d->mCaption); } void MainWindow::setInitialUrl(const QUrl &_url) { Q_ASSERT(_url.isValid()); QUrl url = UrlUtils::fixUserEnteredUrl(_url); if (UrlUtils::urlIsDirectory(url)) { d->mBrowseAction->trigger(); openDirUrl(url); } else { openUrl(url); } } void MainWindow::startSlideShow() { d->mViewAction->trigger(); // We need to wait until we have listed all images in the dirlister to // start the slideshow because the SlideShow objects needs an image list to // work. d->mStartSlideShowWhenDirListerCompleted = true; } void MainWindow::setActiveViewModeAction(QAction* action) { if (action == d->mViewAction) { d->mCurrentMainPageId = ViewMainPageId; // Switching to view mode d->mViewStackedWidget->setCurrentWidget(d->mViewMainPage); openSelectedDocuments(); d->mPreloadDirectionIsForward = true; QTimer::singleShot(VIEW_PRELOAD_DELAY, this, &MainWindow::preloadNextUrl); } else { d->mCurrentMainPageId = BrowseMainPageId; // Switching to browse mode d->mViewStackedWidget->setCurrentWidget(d->mBrowseMainPage); if (!d->mViewMainPage->isEmpty() && KProtocolManager::supportsListing(d->mViewMainPage->url())) { // Reset the view to spare resources, but don't do it if we can't // browse the url, otherwise if the user starts Gwenview this way: // gwenview http://example.com/example.png // and switch to browse mode, switching back to view mode won't bring // his image back. d->mViewMainPage->reset(); } setCaption(QString()); } d->autoAssignThumbnailProvider(); toggleSideBar(d->sideBarVisibility()); toggleStatusBar(d->statusBarVisibility()); emit viewModeChanged(); } void MainWindow::slotThumbnailViewIndexActivated(const QModelIndex& index) { if (!index.isValid()) { return; } KFileItem item = d->mDirModel->itemForIndex(index); if (item.isDir()) { // Item is a dir, open it openDirUrl(item.url()); } else { QString protocol = ArchiveUtils::protocolForMimeType(item.mimetype()); if (!protocol.isEmpty()) { // Item is an archive, tweak url then open it QUrl url = item.url(); url.setScheme(protocol); openDirUrl(url); } else { // Item is a document, switch to view mode d->mViewAction->trigger(); } } } void MainWindow::openSelectedDocuments() { if (d->mCurrentMainPageId != ViewMainPageId) { return; } int count = 0; QList urls; const auto list = d->mContextManager->selectedFileItemList(); for (const auto &item : list) { if (!item.isNull() && !ArchiveUtils::fileItemIsDirOrArchive(item)) { urls << item.url(); ++count; if (count == ViewMainPage::MaxViewCount) { break; } } } if (urls.isEmpty()) { // Selection contains no fitting items // Switch back to browsing mode d->mBrowseAction->trigger(); d->mViewMainPage->reset(); return; } QUrl currentUrl = d->mContextManager->currentUrl(); if (currentUrl.isEmpty() || !urls.contains(currentUrl)) { // There is no current URL or it doesn't belong to selection // This can happen when user manually selects a group of items currentUrl = urls.first(); } d->mViewMainPage->openUrls(urls, currentUrl); } void MainWindow::goUp() { if (d->mCurrentMainPageId == BrowseMainPageId) { QUrl url = d->mContextManager->currentDirUrl(); url = KIO::upUrl(url); openDirUrl(url); } else { d->mBrowseAction->trigger(); } } void MainWindow::showStartMainPage() { d->mCurrentMainPageId = StartMainPageId; d->setActionsDisabledOnStartMainPageEnabled(false); d->saveSplitterConfig(); d->mSideBar->hide(); d->mViewStackedWidget->setCurrentWidget(d->mStartMainPage); d->updateActions(); updatePreviousNextActions(); d->mContextManager->setCurrentDirUrl(QUrl()); d->mContextManager->setCurrentUrl(QUrl()); d->autoAssignThumbnailProvider(); } void MainWindow::slotStartMainPageUrlSelected(const QUrl &url) { d->setActionsDisabledOnStartMainPageEnabled(true); if (d->mBrowseAction->isChecked()) { // Silently uncheck the action so that setInitialUrl() does the right thing SignalBlocker blocker(d->mBrowseAction); d->mBrowseAction->setChecked(false); } setInitialUrl(url); } void MainWindow::openDirUrl(const QUrl &url) { const QUrl currentUrl = d->mContextManager->currentDirUrl(); if (url == currentUrl) { return; } if (url.isParentOf(currentUrl)) { // Keep first child between url and currentUrl selected // If currentPath is "/home/user/photos/2008/event" // and wantedPath is "/home/user/photos" // pathToSelect should be "/home/user/photos/2008" // To anyone considering using QUrl::toLocalFile() instead of // QUrl::path() here. Please don't, using QUrl::path() is the right // thing to do here. const QString currentPath = QDir::cleanPath(currentUrl.adjusted(QUrl::StripTrailingSlash).path()); const QString wantedPath = QDir::cleanPath(url.adjusted(QUrl::StripTrailingSlash).path()); const QChar separator('/'); const int slashCount = wantedPath.count(separator); const QString pathToSelect = currentPath.section(separator, 0, slashCount + 1); QUrl urlToSelect = url; urlToSelect.setPath(pathToSelect); d->mContextManager->setUrlToSelect(urlToSelect); } d->mThumbnailProvider->stop(); d->mContextManager->setCurrentDirUrl(url); d->mGvCore->addUrlToRecentFolders(url); d->mViewMainPage->reset(); } void MainWindow::folderViewUrlChanged(const QUrl &url) { const QUrl currentUrl = d->mContextManager->currentDirUrl(); if (url == currentUrl) { switch (d->mCurrentMainPageId) { case ViewMainPageId: d->mBrowseAction->trigger(); break; case BrowseMainPageId: d->mViewAction->trigger(); break; case StartMainPageId: break; } } else { openDirUrl(url); } } void MainWindow::toggleSideBar(bool visible) { d->saveSplitterConfig(); d->mToggleSideBarAction->setChecked(visible); d->saveSideBarVisibility(visible); d->mSideBar->setVisible(visible); const QString text = QApplication::isRightToLeft() ? QString::fromUtf8(visible ? "▮→" : "▮←") : QString::fromUtf8(visible ? "▮←" : "▮→"); const QString toolTip = visible ? i18nc("@info:tooltip", "Hide sidebar") : i18nc("@info:tooltip", "Show sidebar"); const QList buttonList { d->mBrowseMainPage->toggleSideBarButton(), d->mViewMainPage->toggleSideBarButton() }; for (auto button : buttonList) { button->setText(text); button->setToolTip(toolTip); } } void MainWindow::toggleStatusBar(bool visible) { d->mShowStatusBarAction->setChecked(visible); d->saveStatusBarVisibility(visible); d->mViewMainPage->setStatusBarVisible(visible); d->mBrowseMainPage->setStatusBarVisible(visible); } void MainWindow::slotPartCompleted() { d->updateActions(); const QUrl url = d->mContextManager->currentUrl(); if (!url.isEmpty() && GwenviewConfig::historyEnabled()) { d->mFileOpenRecentAction->addUrl(url); d->mGvCore->addUrlToRecentFiles(url); } if (KProtocolManager::supportsListing(url)) { const QUrl dirUrl = d->mContextManager->currentDirUrl(); d->mGvCore->addUrlToRecentFolders(dirUrl); } } void MainWindow::slotSelectionChanged() { if (d->mCurrentMainPageId == ViewMainPageId) { // The user selected a new file in the thumbnail view, since the // document view is visible, let's show it openSelectedDocuments(); } else { // No document view, we need to load the document to set the undo group // of document factory to the correct QUndoStack QModelIndex index = d->mThumbnailView->currentIndex(); KFileItem item; if (index.isValid()) { item = d->mDirModel->itemForIndex(index); } QUndoGroup* undoGroup = DocumentFactory::instance()->undoGroup(); if (!item.isNull() && !ArchiveUtils::fileItemIsDirOrArchive(item)) { QUrl url = item.url(); Document::Ptr doc = DocumentFactory::instance()->load(url); undoGroup->addStack(doc->undoStack()); undoGroup->setActiveStack(doc->undoStack()); } else { undoGroup->setActiveStack(nullptr); } } // Update UI d->updateActions(); updatePreviousNextActions(); // Start preloading int preloadDelay = d->mCurrentMainPageId == ViewMainPageId ? VIEW_PRELOAD_DELAY : BROWSE_PRELOAD_DELAY; QTimer::singleShot(preloadDelay, this, &MainWindow::preloadNextUrl); } void MainWindow::slotCurrentDirUrlChanged(const QUrl &url) { if (url.isValid()) { d->mUrlNavigator->setLocationUrl(url); d->mGoUpAction->setEnabled(url.path() != "/"); } else { d->mGoUpAction->setEnabled(false); } } void MainWindow::slotDirModelNewItems() { if (d->mContextManager->selectionModel()->hasSelection()) { updatePreviousNextActions(); } } void MainWindow::slotDirListerCompleted() { if (d->mStartSlideShowWhenDirListerCompleted) { d->mStartSlideShowWhenDirListerCompleted = false; QTimer::singleShot(0, d->mToggleSlideShowAction, &QAction::trigger); } if (d->mContextManager->selectionModel()->hasSelection()) { updatePreviousNextActions(); } else { d->goToFirstDocument(); // Try to select the first directory in case there are no images to select if (!d->mContextManager->selectionModel()->hasSelection()) { const QModelIndex index = d->mThumbnailView->model()->index(0, 0); if (index.isValid()) { d->mThumbnailView->setCurrentIndex(index); } } } d->mThumbnailView->scrollToSelectedIndex(); d->mViewMainPage->thumbnailBar()->scrollToSelectedIndex(); d->mFullScreenContent->thumbnailBar()->scrollToSelectedIndex(); } void MainWindow::goToPrevious() { d->goTo(-1); } void MainWindow::goToNext() { d->goTo(1); } void MainWindow::goToFirst() { d->goToFirstDocument(); } void MainWindow::goToLast() { d->goToLastDocument(); } void MainWindow::goToUrl(const QUrl &url) { if (d->mCurrentMainPageId == ViewMainPageId) { d->mViewMainPage->openUrl(url); } QUrl dirUrl = url; dirUrl = dirUrl.adjusted(QUrl::RemoveFilename); if (dirUrl != d->mContextManager->currentDirUrl()) { d->mContextManager->setCurrentDirUrl(dirUrl); d->mGvCore->addUrlToRecentFolders(dirUrl); } d->mContextManager->setUrlToSelect(url); } void MainWindow::updatePreviousNextActions() { bool hasPrevious; bool hasNext; QModelIndex currentIndex = d->mContextManager->selectionModel()->currentIndex(); if (currentIndex.isValid() && !d->indexIsDirOrArchive(currentIndex)) { int row = currentIndex.row(); QModelIndex prevIndex = d->mDirModel->index(row - 1, 0); QModelIndex nextIndex = d->mDirModel->index(row + 1, 0); hasPrevious = prevIndex.isValid() && !d->indexIsDirOrArchive(prevIndex); hasNext = nextIndex.isValid() && !d->indexIsDirOrArchive(nextIndex); } else { hasPrevious = false; hasNext = false; } d->mGoToPreviousAction->setEnabled(hasPrevious); d->mGoToNextAction->setEnabled(hasNext); d->mGoToFirstAction->setEnabled(hasPrevious); d->mGoToLastAction->setEnabled(hasNext); } void MainWindow::leaveFullScreen() { if (d->mFullScreenAction->isChecked()) { d->mFullScreenAction->trigger(); } } void MainWindow::toggleFullScreen(bool checked) { setUpdatesEnabled(false); if (checked) { // Save MainWindow config now, this way if we quit while in // fullscreen, we are sure latest MainWindow changes are remembered. KConfigGroup saveConfigGroup = autoSaveConfigGroup(); if (!isFullScreen()) { // Save state if window manager did not already switch to fullscreen. saveMainWindowSettings(saveConfigGroup); d->mStateBeforeFullScreen.mToolBarVisible = toolBar()->isVisible(); } setAutoSaveSettings(saveConfigGroup, false); resetAutoSaveSettings(); // Go full screen KToggleFullScreenAction::setFullScreen(this, true); menuBar()->hide(); toolBar()->hide(); qApp->setProperty("KDE_COLOR_SCHEME_PATH", d->mGvCore->fullScreenPaletteName()); QApplication::setPalette(d->mGvCore->palette(GvCore::FullScreenPalette)); d->setScreenSaverEnabled(false); } else { setAutoSaveSettings(); // Back to normal qApp->setProperty("KDE_COLOR_SCHEME_PATH", QVariant()); QApplication::setPalette(d->mGvCore->palette(GvCore::NormalPalette)); d->mSlideShow->pause(); KToggleFullScreenAction::setFullScreen(this, false); menuBar()->setVisible(d->mShowMenuBarAction->isChecked()); toolBar()->setVisible(d->mStateBeforeFullScreen.mToolBarVisible); d->setScreenSaverEnabled(true); // See resizeEvent d->mFullScreenLeftAt = QDateTime::currentDateTime(); } d->mFullScreenContent->setFullScreenMode(checked); d->mBrowseMainPage->setFullScreenMode(checked); d->mViewMainPage->setFullScreenMode(checked); d->mSaveBar->setFullScreenMode(checked); toggleSideBar(d->sideBarVisibility()); toggleStatusBar(d->statusBarVisibility()); setUpdatesEnabled(true); d->autoAssignThumbnailProvider(); } void MainWindow::saveCurrent() { d->mGvCore->save(d->mContextManager->currentUrl()); } void MainWindow::saveCurrentAs() { d->mGvCore->saveAs(d->mContextManager->currentUrl()); } void MainWindow::reload() { if (d->mCurrentMainPageId == ViewMainPageId) { d->mViewMainPage->reload(); } else { d->mBrowseMainPage->reload(); } } void MainWindow::openFile() { QUrl dirUrl = d->mContextManager->currentDirUrl(); DialogGuard dialog(this); dialog->setDirectoryUrl(dirUrl); dialog->setWindowTitle(i18nc("@title:window", "Open Image")); const QStringList mimeFilter = MimeTypeUtils::imageMimeTypes(); dialog->setMimeTypeFilters(mimeFilter); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (!dialog->exec()) { return; } if (!dialog->selectedUrls().isEmpty()) { openUrl(dialog->selectedUrls().first()); } } void MainWindow::openUrl(const QUrl& url) { d->setActionsDisabledOnStartMainPageEnabled(true); d->mContextManager->setUrlToSelect(url); d->mViewAction->trigger(); } void MainWindow::showDocumentInFullScreen(const QUrl &url) { d->mContextManager->setUrlToSelect(url); d->mViewAction->trigger(); d->mFullScreenAction->trigger(); } void MainWindow::toggleSlideShow() { if (d->mSlideShow->isRunning()) { d->mSlideShow->pause(); } else { if (!d->mViewAction->isChecked()) { d->mViewAction->trigger(); } if (!d->mFullScreenAction->isChecked()) { d->mFullScreenAction->trigger(); } QList list; for (int pos = 0; pos < d->mDirModel->rowCount(); ++pos) { QModelIndex index = d->mDirModel->index(pos, 0); KFileItem item = d->mDirModel->itemForIndex(index); MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); switch (kind) { case MimeTypeUtils::KIND_SVG_IMAGE: case MimeTypeUtils::KIND_RASTER_IMAGE: case MimeTypeUtils::KIND_VIDEO: list << item.url(); break; default: break; } } d->mSlideShow->start(list); } updateSlideShowAction(); } void MainWindow::updateSlideShowAction() { if (d->mSlideShow->isRunning()) { d->mToggleSlideShowAction->setText(i18n("Pause Slideshow")); d->mToggleSlideShowAction->setIcon(QIcon::fromTheme("media-playback-pause")); } else { d->mToggleSlideShowAction->setText(d->mFullScreenAction->isChecked() ? i18n("Resume Slideshow") : i18n("Start Slideshow")); d->mToggleSlideShowAction->setIcon(QIcon::fromTheme("media-playback-start")); } } bool MainWindow::queryClose() { saveConfig(); QList list = DocumentFactory::instance()->modifiedDocumentList(); if (list.size() == 0) { return true; } KGuiItem yes(i18n("Save All Changes"), "document-save-all"); KGuiItem no(i18n("Discard Changes"), "delete"); QString msg = i18np("One image has been modified.", "%1 images have been modified.", list.size()) + '\n' + i18n("If you quit now, your changes will be lost."); int answer = KMessageBox::warningYesNoCancel( this, msg, QString() /* caption */, yes, no); switch (answer) { case KMessageBox::Yes: d->mGvCore->saveAll(); // We need to wait a bit because the DocumentFactory is notified about // saved documents through a queued connection. qApp->processEvents(); return DocumentFactory::instance()->modifiedDocumentList().isEmpty(); case KMessageBox::No: return true; default: // cancel return false; } } void MainWindow::showConfigDialog() { // Save first so changes like thumbnail zoom level are not lost when reloading config saveConfig(); DialogGuard dialog(this); connect(dialog.data(), &KConfigDialog::settingsChanged, this, &MainWindow::loadConfig); dialog->exec(); } void MainWindow::configureShortcuts() { guiFactory()->configureShortcuts(); guiFactory()->refreshActionProperties(); } void MainWindow::toggleMenuBar() { if (!d->mFullScreenAction->isChecked()) { if (!d->mShowMenuBarAction->isChecked()) { const QString accel = d->mShowMenuBarAction->shortcut().toString(); KMessageBox::information(this, i18n("This will hide the menu bar completely." " You can show it again by typing %1.", accel), i18n("Hide menu bar"), QLatin1String("HideMenuBarWarning")); } menuBar()->setVisible(d->mShowMenuBarAction->isChecked()); } } void MainWindow::loadConfig() { d->mDirModel->setBlackListedExtensions(GwenviewConfig::blackListedExtensions()); d->mDirModel->adjustKindFilter(MimeTypeUtils::KIND_VIDEO, GwenviewConfig::listVideos()); if (GwenviewConfig::historyEnabled()) { d->mFileOpenRecentAction->loadEntries(KConfigGroup(KSharedConfig::openConfig(), "Recent Files")); foreach(const QUrl& url, d->mFileOpenRecentAction->urls()) { d->mGvCore->addUrlToRecentFiles(url); } } else { d->mFileOpenRecentAction->clear(); } d->mFileOpenRecentAction->setVisible(GwenviewConfig::historyEnabled()); d->mStartMainPage->loadConfig(); d->mViewMainPage->loadConfig(); d->mBrowseMainPage->loadConfig(); d->mContextManager->loadConfig(); d->mSideBar->loadConfig(); d->loadSplitterConfig(); } void MainWindow::saveConfig() { d->mFileOpenRecentAction->saveEntries(KConfigGroup(KSharedConfig::openConfig(), "Recent Files")); d->mViewMainPage->saveConfig(); d->mBrowseMainPage->saveConfig(); d->mContextManager->saveConfig(); d->saveSplitterConfig(); GwenviewConfig::setFullScreenModeActive(isFullScreen()); } void MainWindow::print() { if (!d->mContextManager->currentUrlIsRasterImage()) { return; } Document::Ptr doc = DocumentFactory::instance()->load(d->mContextManager->currentUrl()); PrintHelper printHelper(this); printHelper.print(doc); } void MainWindow::preloadNextUrl() { static bool disablePreload = qgetenv("GV_MAX_UNREFERENCED_IMAGES") == "0"; if (disablePreload) { qDebug() << "Preloading disabled"; return; } QItemSelection selection = d->mContextManager->selectionModel()->selection(); if (selection.size() != 1) { return; } QModelIndexList indexList = selection.indexes(); if (indexList.isEmpty()) { return; } QModelIndex index = indexList.at(0); if (!index.isValid()) { return; } if (d->mCurrentMainPageId == ViewMainPageId) { // If we are in view mode, preload the next url, otherwise preload the // selected one int offset = d->mPreloadDirectionIsForward ? 1 : -1; index = d->mDirModel->sibling(index.row() + offset, index.column(), index); if (!index.isValid()) { return; } } KFileItem item = d->mDirModel->itemForIndex(index); if (!ArchiveUtils::fileItemIsDirOrArchive(item)) { QUrl url = item.url(); if (url.isLocalFile()) { QSize size = d->mViewStackedWidget->size(); d->mPreloader->preload(url, size); } } } QSize MainWindow::sizeHint() const { return KXmlGuiWindow::sizeHint().expandedTo(QSize(750, 500)); } void MainWindow::showEvent(QShowEvent *event) { // We need to delay initializing the action state until the menu bar has // been initialized, that's why it's done only in the showEvent() d->mShowMenuBarAction->setChecked(menuBar()->isVisible()); KXmlGuiWindow::showEvent(event); } void MainWindow::resizeEvent(QResizeEvent* event) { KXmlGuiWindow::resizeEvent(event); // This is a small hack to execute code after leaving fullscreen, and after // the window has been resized back to its former size. if (d->mFullScreenLeftAt.isValid() && d->mFullScreenLeftAt.secsTo(QDateTime::currentDateTime()) < 2) { if (d->mCurrentMainPageId == BrowseMainPageId) { d->mThumbnailView->scrollToSelectedIndex(); } d->mFullScreenLeftAt = QDateTime(); } } bool MainWindow::eventFilter(QObject *obj, QEvent *event) { Q_UNUSED(obj); Q_UNUSED(event); #ifdef Q_OS_OSX /** * handle Mac OS X file open events (only exist on OS X) */ if (event->type() == QEvent::FileOpen) { QFileOpenEvent *fileOpenEvent = static_cast(event); openUrl(fileOpenEvent->url()); return true; } #endif if (obj == d->mThumbnailView->viewport()) { switch(event->type()) { case QEvent::MouseButtonPress: case QEvent::MouseButtonDblClick: mouseButtonNavigate(static_cast(event)); break; default: ; } } return false; } void MainWindow::mousePressEvent(QMouseEvent *event) { mouseButtonNavigate(event); KXmlGuiWindow::mousePressEvent(event); } void MainWindow::mouseDoubleClickEvent(QMouseEvent *event) { mouseButtonNavigate(event); KXmlGuiWindow::mouseDoubleClickEvent(event); } void MainWindow::mouseButtonNavigate(QMouseEvent *event) { switch(event->button()) { case Qt::ForwardButton: if (d->mGoToNextAction->isEnabled()) { d->mGoToNextAction->trigger(); return; } break; case Qt::BackButton: if (d->mGoToPreviousAction->isEnabled()) { d->mGoToPreviousAction->trigger(); return; } break; default: ; } } void MainWindow::setDistractionFreeMode(bool value) { d->mFullScreenContent->setDistractionFreeMode(value); } void MainWindow::saveProperties(KConfigGroup& group) { group.writeEntry(SESSION_CURRENT_PAGE_KEY, int(d->mCurrentMainPageId)); group.writeEntry(SESSION_URL_KEY, d->mContextManager->currentUrl().toString()); } void MainWindow::readProperties(const KConfigGroup& group) { const QUrl url = group.readEntry(SESSION_URL_KEY, QUrl()); if (url.isValid()) { goToUrl(url); } MainPageId pageId = MainPageId(group.readEntry(SESSION_CURRENT_PAGE_KEY, int(StartMainPageId))); if (pageId == StartMainPageId) { showStartMainPage(); } else if (pageId == BrowseMainPageId) { d->mBrowseAction->trigger(); } else { d->mViewAction->trigger(); } } void MainWindow::showFirstDocumentReached() { if (d->mCurrentMainPageId != ViewMainPageId) { return; } HudButtonBox* dlg = new HudButtonBox; dlg->setText(i18n("You reached the first document, what do you want to do?")); dlg->addButton(i18n("Stay There")); dlg->addAction(d->mGoToLastAction, i18n("Go to the Last Document")); dlg->addAction(d->mBrowseAction, i18n("Go Back to the Document List")); dlg->addCountDown(15000); d->mViewMainPage->showMessageWidget(dlg, Qt::AlignCenter); } void MainWindow::showLastDocumentReached() { if (d->mCurrentMainPageId != ViewMainPageId) { return; } HudButtonBox* dlg = new HudButtonBox; dlg->setText(i18n("You reached the last document, what do you want to do?")); dlg->addButton(i18n("Stay There")); dlg->addAction(d->mGoToFirstAction, i18n("Go to the First Document")); dlg->addAction(d->mBrowseAction, i18n("Go Back to the Document List")); dlg->addCountDown(15000); d->mViewMainPage->showMessageWidget(dlg, Qt::AlignCenter); } void MainWindow::replaceLocation() { QLineEdit* lineEdit = d->mUrlNavigator->editor()->lineEdit(); // If the text field currently has focus and everything is selected, // pressing the keyboard shortcut returns the whole thing to breadcrumb mode if (d->mUrlNavigator->isUrlEditable() && lineEdit->hasFocus() && lineEdit->selectedText() == lineEdit->text() ) { d->mUrlNavigator->setUrlEditable(false); } else { d->mUrlNavigator->setUrlEditable(true); d->mUrlNavigator->setFocus(); lineEdit->selectAll(); } } } // namespace diff --git a/kconf_update/gwenview.upd b/kconf_update/gwenview.upd index b9918cfc..627c542b 100644 --- a/kconf_update/gwenview.upd +++ b/kconf_update/gwenview.upd @@ -1,29 +1,36 @@ Version=5 Id=SideBar_StatusBar_Rename File=gwenviewrc Group=SideBar-BrowseMode,SideBar Key=IsVisible,IsVisible BrowseMode RemoveGroup=SideBar-BrowseMode Group=SideBar-ViewMode,SideBar Key=IsVisible,IsVisible ViewMode RemoveGroup=SideBar-ViewMode Group=SideBar-FullScreenMode,SideBar Key=IsVisible,IsVisible ViewMode FullScreen RemoveGroup=SideBar-FullScreenMode Group=General,StatusBar Key=StatusBarIsVisible,IsVisible BrowseMode Key=StatusBarIsVisible,IsVisible ViewMode RemoveKey=StatusBarIsVisible Id=ImageView_AlphaBackgroundMode_Update File=gwenviewrc Options=overwrite Group=ImageView Script=gwenview-imageview-alphabackgroundmode-update.pl,perl + + +Id=DeleteThumbnailSetting_Rename +File=gwenviewrc + +Group=ThumbnailView +Key=DeleteThumbnailCacheOnExit,LowResourceUsageMode diff --git a/lib/gwenviewconfig.kcfg b/lib/gwenviewconfig.kcfg index 4fc56b5f..140aec7e 100644 --- a/lib/gwenviewconfig.kcfg +++ b/lib/gwenviewconfig.kcfg @@ -1,342 +1,342 @@ lib/sorting.h lib/zoommode.h lib/thumbnailactions.h lib/mousewheelbehavior.h lib/documentview/documentview.h lib/documentview/rasterimageview.h lib/print/printoptionspage.h lib/renderingintent.h General.Name,General.ImageSize,Exif.Photo.ExposureTime,Exif.Photo.Flash true true false 100 true 0.5 The percentage of memory used by Gwenview before it warns the user and suggest saving changes. new A list of filename extensions Gwenview should not try to load. We exclude *.new as well because this is the extension used for temporary files by KSaveFile. false Horizontal 1 false false information ThumbnailActions::AllButtons General.Name,Exif.Image.DateTime true false AbstractImageView::AlphaBackgroundNone #ffffff MouseWheelBehavior::Scroll false true 350, 100 DocumentView::SoftwareAnimation ZoomMode::Autofit Defines what happens when going to image B after having zoomed in on an area of image A. If set to Autofit, image B is zoomed out to fit the screen. If set to KeepSame, all images share the same zoom and position: image B is set to the same zoom parameters as image A (and if these are changed, image A will then be displayed with the updated zoom and position). If set to Individual, all images remember their own zoom and position: image B is initially set to the same zoom parameters as image A, but will then remember its own zoom and position (if these are changed, image A will NOT be displayed with the updated zoom and position). RenderingIntent::Perceptual Defines how colors are rendered when your display uses an ICC color profile and an image has colors that do not fit within the profile's color gamut. "Perceptual" will scale the colors of the entire image so that they all fit within the display's capabilities. "Relative" will squash only the colors that cannot be displayed, and leave the other colors alone. 128 3./2. - + false Sorting::Name false 1 true Qt::AlignHCenter | Qt::AlignVCenter PrintOptionsPage::ScaleToPage false 15.0 10.0 PrintOptionsPage::Centimeters true false true false false 5.0 24 false false -1 0 0 true true false diff --git a/lib/thumbnailprovider/thumbnailgenerator.cpp b/lib/thumbnailprovider/thumbnailgenerator.cpp index de2061d1..c4c2ee0f 100644 --- a/lib/thumbnailprovider/thumbnailgenerator.cpp +++ b/lib/thumbnailprovider/thumbnailgenerator.cpp @@ -1,308 +1,308 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2012 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 "thumbnailgenerator.h" // Local #include "jpegcontent.h" #include "gwenviewconfig.h" #include "exiv2imageloader.h" // KDE #include #ifdef KDCRAW_FOUND #include #endif // Qt #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 //------------------------------------------------------------------------ // // ThumbnailContext // //------------------------------------------------------------------------ bool ThumbnailContext::load(const QString &pixPath, int pixelSize) { mImage = QImage(); mNeedCaching = true; QImage originalImage; QSize originalSize; QByteArray formatHint = pixPath.section(QLatin1Char('.'), -1).toLocal8Bit().toLower(); QImageReader reader(pixPath); JpegContent content; QByteArray format; QByteArray data; QBuffer buffer; int previewRatio = 1; #ifdef KDCRAW_FOUND // raw images deserve special treatment if (KDcrawIface::KDcraw::rawFilesList().contains(QString::fromLatin1(formatHint))) { // use KDCraw to extract the preview bool ret = KDcrawIface::KDcraw::loadEmbeddedPreview(data, pixPath); // We need QImage. Loading JpegContent from QImage - exif lost // Loading QImage from JpegContent - unimplemented, would go with loadFromData if (!ret) { // if the embedded preview loading failed, load half preview instead. That's slower... if (!KDcrawIface::KDcraw::loadHalfPreview(data, pixPath)) { qWarning() << "unable to get preview for " << pixPath.toUtf8().constData(); return false; } previewRatio = 2; } // And we need JpegContent too because of EXIF (orientation!). if (!content.loadFromData(data)) { qWarning() << "unable to load preview for " << pixPath.toUtf8().constData(); return false; } buffer.setBuffer(&data); buffer.open(QIODevice::ReadOnly); reader.setDevice(&buffer); reader.setFormat(formatHint); } else { #else { #endif if (!reader.canRead()) { reader.setDecideFormatFromContent(true); // Set filename again, otherwise QImageReader won't restart from scratch reader.setFileName(pixPath); } if (reader.format() == "jpeg" && GwenviewConfig::applyExifOrientation()) { content.load(pixPath); } } // If there's jpeg content (from jpg or raw files), try to load an embedded thumbnail, if available. if (!content.rawData().isEmpty()) { QImage thumbnail = content.thumbnail(); // If the user does not care about the generated thumbnails (by deleting them on exit), use ANY // embedded thumnail, even if it's too small. if (!thumbnail.isNull() && - (GwenviewConfig::deleteThumbnailCacheOnExit() || qMax(thumbnail.width(), thumbnail.height()) >= pixelSize) + (GwenviewConfig::lowResourceUsageMode() || qMax(thumbnail.width(), thumbnail.height()) >= pixelSize) ) { mImage = std::move(thumbnail); mOriginalWidth = content.size().width(); mOriginalHeight = content.size().height(); return true; } } // Generate thumbnail from full image originalSize = reader.size(); if (originalSize.isValid() && reader.supportsOption(QImageIOHandler::ScaledSize) && qMax(originalSize.width(), originalSize.height()) >= pixelSize) { QSizeF scaledSize = originalSize; scaledSize.scale(pixelSize, pixelSize, Qt::KeepAspectRatio); if (!scaledSize.isEmpty()) { reader.setScaledSize(scaledSize.toSize()); } } // Rotate if necessary if (GwenviewConfig::applyExifOrientation()) { reader.setAutoTransform(true); } // format() is empty after QImageReader::read() is called format = reader.format(); if (!reader.read(&originalImage)) { return false; } if (!originalSize.isValid()) { originalSize = originalImage.size(); } mOriginalWidth = originalSize.width() * previewRatio; mOriginalHeight = originalSize.height() * previewRatio; if (qMax(mOriginalWidth, mOriginalHeight) <= pixelSize) { mImage = originalImage; mNeedCaching = format != "png"; } else { mImage = originalImage.scaled(pixelSize, pixelSize, Qt::KeepAspectRatio); } if (reader.autoTransform() && (reader.transformation() & QImageIOHandler::TransformationRotate90)) { qSwap(mOriginalWidth, mOriginalHeight); } return true; } //------------------------------------------------------------------------ // // ThumbnailGenerator // //------------------------------------------------------------------------ ThumbnailGenerator::ThumbnailGenerator() : mCancel(false) {} void ThumbnailGenerator::load( const QString& originalUri, time_t originalTime, KIO::filesize_t originalFileSize, const QString& originalMimeType, const QString& pixPath, const QString& thumbnailPath, ThumbnailGroup::Enum group) { QMutexLocker lock(&mMutex); Q_ASSERT(mPixPath.isNull()); mOriginalUri = originalUri; mOriginalTime = originalTime; mOriginalFileSize = originalFileSize; mOriginalMimeType = originalMimeType; mPixPath = pixPath; mThumbnailPath = thumbnailPath; mThumbnailGroup = group; if (!isRunning()) start(); mCond.wakeOne(); } QString ThumbnailGenerator::originalUri() const { return mOriginalUri; } time_t ThumbnailGenerator::originalTime() const { return mOriginalTime; } KIO::filesize_t ThumbnailGenerator::originalFileSize() const { return mOriginalFileSize; } QString ThumbnailGenerator::originalMimeType() const { return mOriginalMimeType; } bool ThumbnailGenerator::testCancel() { QMutexLocker lock(&mMutex); return mCancel; } void ThumbnailGenerator::cancel() { QMutexLocker lock(&mMutex); mCancel = true; mCond.wakeOne(); } void ThumbnailGenerator::run() { LOG(""); while (!testCancel()) { QString pixPath; int pixelSize; { QMutexLocker lock(&mMutex); // empty mPixPath means nothing to do LOG("Waiting for mPixPath"); if (mPixPath.isNull()) { LOG("mPixPath.isNull"); mCond.wait(&mMutex); } } if (testCancel()) { return; } { QMutexLocker lock(&mMutex); pixPath = mPixPath; pixelSize = ThumbnailGroup::pixelSize(mThumbnailGroup); } Q_ASSERT(!pixPath.isNull()); LOG("Loading" << pixPath); ThumbnailContext context; bool ok = context.load(pixPath, pixelSize); { QMutexLocker lock(&mMutex); if (ok) { mImage = context.mImage; mOriginalWidth = context.mOriginalWidth; mOriginalHeight = context.mOriginalHeight; if (context.mNeedCaching && mThumbnailGroup <= ThumbnailGroup::Large) { cacheThumbnail(); } } else { // avoid emitting the thumb from the previous successful run mImage = QImage(); qWarning() << "Could not generate thumbnail for file" << mOriginalUri; } mPixPath.clear(); // done, ready for next } if (testCancel()) { return; } { QSize size(mOriginalWidth, mOriginalHeight); LOG("emitting done signal, size=" << size); QMutexLocker lock(&mMutex); emit done(mImage, size); LOG("Done"); } } LOG("Ending thread"); } void ThumbnailGenerator::cacheThumbnail() { mImage.setText(QStringLiteral("Thumb::URI") , mOriginalUri); mImage.setText(QStringLiteral("Thumb::MTime") , QString::number(mOriginalTime)); mImage.setText(QStringLiteral("Thumb::Size") , QString::number(mOriginalFileSize)); mImage.setText(QStringLiteral("Thumb::Mimetype") , mOriginalMimeType); mImage.setText(QStringLiteral("Thumb::Image::Width") , QString::number(mOriginalWidth)); mImage.setText(QStringLiteral("Thumb::Image::Height"), QString::number(mOriginalHeight)); mImage.setText(QStringLiteral("Software") , QStringLiteral("Gwenview")); emit thumbnailReadyToBeCached(mThumbnailPath, mImage); } } // namespace diff --git a/tests/auto/thumbnailprovidertest.cpp b/tests/auto/thumbnailprovidertest.cpp index cb895c54..fdbdac73 100644 --- a/tests/auto/thumbnailprovidertest.cpp +++ b/tests/auto/thumbnailprovidertest.cpp @@ -1,284 +1,284 @@ /* 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 "thumbnailprovidertest.h" // Qt #include #include #include #include // KDE #include #include #include #include // Local #include "../lib/thumbnailprovider/thumbnailprovider.h" #include "testutils.h" #include "gwenviewconfig.h" // libc #include #include using namespace Gwenview; QTEST_MAIN(ThumbnailProviderTest) SandBox::SandBox() : mPath(QDir::currentPath() + "/sandbox") {} void SandBox::initDir() { KIO::Job* job; QDir dir(mPath); if (dir.exists()) { QUrl sandBoxUrl("file://" + mPath); job = KIO::del(sandBoxUrl); QVERIFY2(job->exec(), "Couldn't delete sandbox"); } dir.mkpath("."); } void SandBox::fill() { initDir(); createTestImage("red.png", 300, 200, Qt::red); createTestImage("blue.png", 200, 300, Qt::blue); createTestImage("small.png", 50, 50, Qt::green); copyTestImage("orient6.jpg", 128, 256); copyTestImage("orient6-small.jpg", 32, 64); } void SandBox::copyTestImage(const QString& testFileName, int width, int height) { QUrl testPath = urlForTestFile(testFileName); QUrl testDest = QUrl("file://" + mPath + '/' + testFileName); KIO::Job* job = KIO::copy(testPath, testDest); QVERIFY2(job->exec(), "Couldn't copy test image"); mSizeHash.insert(testFileName, QSize(width, height)); } static QImage createColoredImage(int width, int height, const QColor& color) { QImage image(width, height, QImage::Format_RGB32); QPainter painter(&image); painter.fillRect(image.rect(), color); return image; } void SandBox::createTestImage(const QString& name, int width, int height, const QColor& color) { QImage image = createColoredImage(width, height, color); image.save(mPath + '/' + name, "png"); mSizeHash.insert(name, QSize(width, height)); } void ThumbnailProviderTest::initTestCase() { qRegisterMetaType("KFileItem"); } void ThumbnailProviderTest::init() { ThumbnailProvider::setThumbnailBaseDir(mSandBox.mPath + "/thumbnails/"); mSandBox.fill(); } static void syncRun(ThumbnailProvider *provider) { QEventLoop loop; QObject::connect(provider, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); } void ThumbnailProviderTest::testLoadLocal() { QDir dir(mSandBox.mPath); // Create a list of items which will be thumbnailed KFileItemList list; Q_FOREACH(const QFileInfo & info, dir.entryInfoList(QDir::Files)) { QUrl url("file://" + info.absoluteFilePath()); KFileItem item(url); list << item; } // Generate the thumbnails ThumbnailProvider provider; provider.setThumbnailGroup(ThumbnailGroup::Normal); provider.appendItems(list); QSignalSpy spy(&provider, SIGNAL(thumbnailLoaded(KFileItem,QPixmap,QSize,qulonglong))); syncRun(&provider); while (!ThumbnailProvider::isThumbnailWriterEmpty()) { QTest::qWait(100); } // Check we generated the correct number of thumbnails QDir thumbnailDir = ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Normal); // There should be one file less because small.png is a png and is too // small to have a thumbnail QStringList entryList = thumbnailDir.entryList(QStringList("*.png")); QCOMPARE(entryList.count(), mSandBox.mSizeHash.size() - 1); // Check thumbnail keys QHash > thumbnailHash; Q_FOREACH(const QString& name, entryList) { QImage thumb; QVERIFY(thumb.load(thumbnailDir.filePath(name))); QUrl url(thumb.text("Thumb::URI")); KFileItem item = list.findByUrl(url); QVERIFY(!item.isNull()); QSize originalSize = mSandBox.mSizeHash.value(item.url().fileName()); uint mtime = item.time(KFileItem::ModificationTime).toTime_t(); if (mtime == uint(-1)) { // This happens from time to time on build.kde.org, but I haven't // been able to reproduce it locally, so let's try to gather more // information. qWarning() << "mtime == -1 for url" << url << ". This should not happen!"; qWarning() << "errno:" << errno << "message:" << strerror(errno); qWarning() << "QFile::exists(" << url.toLocalFile() << "):" << QFile::exists(url.toLocalFile()); qWarning() << "Recalculating mtime" << item.time(KFileItem::ModificationTime).toTime_t(); QFAIL("Invalid time for test KFileItem"); } QCOMPARE(thumb.text("Thumb::Image::Width"), QString::number(originalSize.width())); QCOMPARE(thumb.text("Thumb::Image::Height"), QString::number(originalSize.height())); QCOMPARE(thumb.text("Thumb::Mimetype"), item.mimetype()); QCOMPARE(thumb.text("Thumb::Size"), QString::number(item.size())); QCOMPARE(thumb.text("Thumb::MTime"), QString::number(mtime)); } // Check what was in the thumbnailLoaded() signals QCOMPARE(spy.count(), mSandBox.mSizeHash.size()); QSignalSpy::ConstIterator it = spy.constBegin(), end = spy.constEnd(); for (; it != end; ++it) { const QVariantList args = *it; const KFileItem item = qvariant_cast(args.at(0)); const QSize size = args.at(2).toSize(); const QSize expectedSize = mSandBox.mSizeHash.value(item.url().fileName()); QCOMPARE(size, expectedSize); } } void ThumbnailProviderTest::testUseEmbeddedOrNot() { QImage expectedThumbnail; QPixmap thumbnailPix; SandBox sandBox; sandBox.initDir(); // This image is red (0xfe0000) and 256x128 but contains a white 128x64 thumbnail sandBox.copyTestImage("embedded-thumbnail.jpg", 256, 128); KFileItemList list; QUrl url("file://" + QDir(sandBox.mPath).absoluteFilePath("embedded-thumbnail.jpg")); list << KFileItem(url); // Loading a normal thumbnail should bring the white one { ThumbnailProvider provider; provider.setThumbnailGroup(ThumbnailGroup::Normal); provider.appendItems(list); QSignalSpy spy(&provider, SIGNAL(thumbnailLoaded(KFileItem,QPixmap,QSize,qulonglong))); syncRun(&provider); QCOMPARE(spy.count(), 1); expectedThumbnail = createColoredImage(128, 64, Qt::white); thumbnailPix = qvariant_cast(spy.at(0).at(1)); QVERIFY(TestUtils::imageCompare(expectedThumbnail, thumbnailPix.toImage())); } // Loading a large thumbnail should bring the red one, unless thumbnails are deleted on exit, // which should bring the white one { ThumbnailProvider provider; provider.setThumbnailGroup(ThumbnailGroup::Large); provider.appendItems(list); QSignalSpy spy(&provider, SIGNAL(thumbnailLoaded(KFileItem,QPixmap,QSize,qulonglong))); syncRun(&provider); QCOMPARE(spy.count(), 1); - if (GwenviewConfig::deleteThumbnailCacheOnExit()) { + if (GwenviewConfig::lowResourceUsageMode()) { expectedThumbnail = createColoredImage(128, 64, Qt::white); } else { expectedThumbnail = createColoredImage(256, 128, QColor(254, 0, 0)); } thumbnailPix = qvariant_cast(spy.at(0).at(1)); QVERIFY(TestUtils::imageCompare(expectedThumbnail, thumbnailPix.toImage())); } } void ThumbnailProviderTest::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"); KFileItemList list; KFileItem item(url); list << item; ThumbnailProvider provider; provider.setThumbnailGroup(ThumbnailGroup::Normal); provider.appendItems(list); syncRun(&provider); while (!ThumbnailProvider::isThumbnailWriterEmpty()) { QTest::qWait(100); } QDir thumbnailDir = ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Normal); QStringList entryList = thumbnailDir.entryList(QStringList("*.png")); QCOMPARE(entryList.count(), 1); } void ThumbnailProviderTest::testRemoveItemsWhileGenerating() { QDir dir(mSandBox.mPath); // Create a list of items which will be thumbnailed KFileItemList list; Q_FOREACH(const QFileInfo & info, dir.entryInfoList(QDir::Files)) { QUrl url("file://" + info.absoluteFilePath()); KFileItem item(url); list << item; } // Start generating thumbnails for items ThumbnailProvider provider; provider.setThumbnailGroup(ThumbnailGroup::Normal); provider.appendItems(list); QEventLoop loop; connect(&provider, SIGNAL(finished()), &loop, SLOT(quit())); // Remove items, it should not crash provider.removeItems(list); loop.exec(); }