578 lines
22 KiB
C++

// vim: set tabstop=4 shiftwidth=4 expandtab:
/*
Gwenview: an image viewer
Copyright 2008 Aurélien Gâteau <agateau@kde.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
*/
// Self
#include "browsemainpage.h"
// Qt
#include <QActionGroup>
#include <QDropEvent>
#include <QMenu>
#include <QVBoxLayout>
// KF
#include <KActionCategory>
#include <KActionCollection>
#include <KActionMenu>
#include <KDirModel>
#include <KFileItem>
#include <KFilePlacesModel>
#include <KIconLoader>
#include <KLocalizedString>
#include <KUrlMimeData>
#include <KUrlNavigator>
// Local
#include <fileoperations.h>
#include <filtercontroller.h>
#include <gvcore.h>
#include <lib/archiveutils.h>
#include <lib/document/documentfactory.h>
#include <lib/gvdebug.h>
#include <lib/gwenviewconfig.h>
#include <lib/semanticinfo/abstractsemanticinfobackend.h>
#include <lib/semanticinfo/sorteddirmodel.h>
#include <lib/semanticinfo/tagmodel.h>
#include <lib/sorting.h>
#include <lib/thumbnailview/previewitemdelegate.h>
#include <lib/thumbnailview/thumbnailview.h>
#include <mimetypeutils.h>
#include <ui_browsemainpage.h>
#ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
#include "lib/semanticinfo/semanticinfodirmodel.h"
#endif
namespace Gwenview
{
inline Sorting::Enum sortingFromSortAction(const QAction *action)
{
Q_ASSERT(action);
return Sorting::Enum(action->data().toInt());
}
struct BrowseMainPagePrivate : public Ui_BrowseMainPage {
BrowseMainPage *q = nullptr;
GvCore *mGvCore = nullptr;
KFilePlacesModel *mFilePlacesModel = nullptr;
KUrlNavigator *mUrlNavigator = nullptr;
SortedDirModel *mDirModel = nullptr;
int mDocumentCountImages;
int mDocumentCountVideos;
KFileItemList *mSelectedMediaItems = nullptr;
KActionCollection *mActionCollection = nullptr;
FilterController *mFilterController = nullptr;
QActionGroup *mSortAction = nullptr;
KToggleAction *mSortDescendingAction = nullptr;
QActionGroup *mThumbnailDetailsActionGroup = nullptr;
PreviewItemDelegate *mDelegate = nullptr;
void setupWidgets()
{
setupUi(q);
q->layout()->setContentsMargins(0, 0, 0, 0);
int margins = q->style()->pixelMetric(QStyle::PM_ToolBarItemMargin) + q->style()->pixelMetric(QStyle::PM_ToolBarFrameWidth);
mStatusBarContainer->layout()->setContentsMargins(margins, margins, margins, margins);
mStatusBarContainer->layout()->setSpacing(q->style()->pixelMetric(QStyle::PM_ToolBarItemSpacing));
// mThumbnailView
mThumbnailView->setModel(mDirModel);
mDelegate = new PreviewItemDelegate(mThumbnailView);
mThumbnailView->setItemDelegate(mDelegate);
mThumbnailView->setSelectionMode(QAbstractItemView::ExtendedSelection);
// mUrlNavigator (use stupid layouting code because KUrlNavigator ctor
// can't be used directly from Designer)
mFilePlacesModel = new KFilePlacesModel(q);
mUrlNavigator = new KUrlNavigator(mFilePlacesModel, QUrl(), mUrlNavigatorContainer);
mUrlNavigatorContainer->setAutoFillBackground(true);
mUrlNavigatorContainer->setBackgroundRole(QPalette::Mid);
auto layout = new QVBoxLayout(mUrlNavigatorContainer);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(mUrlNavigator);
QObject::connect(mUrlNavigator, SIGNAL(urlsDropped(QUrl, QDropEvent *)), q, SLOT(slotUrlsDropped(QUrl, QDropEvent *)));
// FullScreen Toolbar
mFullScreenToolBar->setVisible(false);
mFullScreenToolBar2->setVisible(false);
mFullScreenToolBar->setAutoFillBackground(true);
mFullScreenToolBar2->setAutoFillBackground(true);
mFullScreenToolBar->setBackgroundRole(QPalette::Mid);
mFullScreenToolBar2->setBackgroundRole(QPalette::Mid);
// Thumbnail slider
QObject::connect(mThumbnailSlider, &ZoomSlider::valueChanged, mThumbnailView, &ThumbnailView::setThumbnailWidth);
QObject::connect(mThumbnailView, &ThumbnailView::thumbnailWidthChanged, mThumbnailSlider, &ZoomSlider::setValue);
// Document count label
QMargins labelMargins = mDocumentCountLabel->contentsMargins();
labelMargins.setLeft(15);
labelMargins.setRight(15);
mDocumentCountLabel->setContentsMargins(labelMargins);
}
QAction *thumbnailDetailAction(const QString &text, PreviewItemDelegate::ThumbnailDetail detail)
{
auto action = new QAction(q);
action->setText(text);
action->setCheckable(true);
action->setChecked(GwenviewConfig::thumbnailDetails() & detail);
action->setData(QVariant(detail));
mThumbnailDetailsActionGroup->addAction(action);
QObject::connect(action, SIGNAL(triggered(bool)), q, SLOT(updateThumbnailDetails()));
return action;
}
void setupActions(KActionCollection *actionCollection)
{
auto view = new KActionCategory(i18nc("@title actions category - means actions changing smth in interface", "View"), actionCollection);
QAction *action = view->addAction("edit_location", q, SLOT(editLocation()));
action->setText(i18nc("@action:inmenu Navigation Bar", "Edit Location"));
actionCollection->setDefaultShortcut(action, Qt::Key_F6);
auto sortActionMenu = view->add<KActionMenu>("sort_by");
sortActionMenu->setText(i18nc("@action:inmenu", "Sort By"));
sortActionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-sort")));
sortActionMenu->setPopupMode(QToolButton::InstantPopup);
mSortAction = new QActionGroup(actionCollection);
action = new QAction(i18nc("@addAction:inmenu", "Name"), mSortAction);
action->setCheckable(true);
action->setData(QVariant(Sorting::Name));
action = new QAction(i18nc("@addAction:inmenu", "Date"), mSortAction);
action->setCheckable(true);
action->setData(QVariant(Sorting::Date));
action = new QAction(i18nc("@addAction:inmenu", "Size"), mSortAction);
action->setCheckable(true);
action->setData(QVariant(Sorting::Size));
#ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
action = new QAction(i18nc("@addAction:inmenu", "Rating"), mSortAction);
action->setCheckable(true);
action->setData(QVariant(Sorting::Rating));
#endif
QObject::connect(mSortAction, SIGNAL(triggered(QAction *)), q, SLOT(updateSortOrder()));
mSortDescendingAction = view->add<KToggleAction>(QStringLiteral("sort_desc"));
mSortDescendingAction->setText(i18nc("@action:inmenu Sort", "Descending"));
QObject::connect(mSortDescendingAction, SIGNAL(toggled(bool)), q, SLOT(updateSortOrder()));
for (auto action : mSortAction->actions()) {
sortActionMenu->addAction(action);
}
sortActionMenu->addSeparator();
sortActionMenu->addAction(mSortDescendingAction);
mThumbnailDetailsActionGroup = new QActionGroup(q);
mThumbnailDetailsActionGroup->setExclusive(false);
auto thumbnailDetailsAction = view->add<KActionMenu>(QStringLiteral("thumbnail_details"));
thumbnailDetailsAction->setText(i18nc("@action:inmenu", "Thumbnail Details"));
thumbnailDetailsAction->addAction(thumbnailDetailAction(i18nc("@action:inmenu", "Filename"), PreviewItemDelegate::FileNameDetail));
thumbnailDetailsAction->addAction(thumbnailDetailAction(i18nc("@action:inmenu", "Date"), PreviewItemDelegate::DateDetail));
thumbnailDetailsAction->addAction(thumbnailDetailAction(i18nc("@action:inmenu", "Image Size"), PreviewItemDelegate::ImageSizeDetail));
thumbnailDetailsAction->addAction(thumbnailDetailAction(i18nc("@action:inmenu", "File Size"), PreviewItemDelegate::FileSizeDetail));
#ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
thumbnailDetailsAction->addAction(thumbnailDetailAction(i18nc("@action:inmenu", "Rating"), PreviewItemDelegate::RatingDetail));
#endif
auto file = new KActionCategory(i18nc("@title actions category", "File"), actionCollection);
action = file->addAction(QStringLiteral("add_folder_to_places"), q, SLOT(addFolderToPlaces()));
action->setText(i18nc("@action:inmenu", "Add Folder to Places"));
action->setIcon(QIcon::fromTheme(QStringLiteral("bookmark-new")));
}
void setupFilterController()
{
auto menu = new QMenu(mAddFilterButton);
mFilterController = new FilterController(mFilterFrame, mDirModel);
const auto actionList = mFilterController->actionList();
for (QAction *action : actionList) {
menu->addAction(action);
}
mAddFilterButton->setMenu(menu);
}
void setupFullScreenToolBar()
{
mFullScreenToolBar->setIconDimensions(KIconLoader::SizeMedium);
mFullScreenToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
mFullScreenToolBar->addAction(mActionCollection->action(QStringLiteral("browse")));
mFullScreenToolBar->addAction(mActionCollection->action(QStringLiteral("view")));
mFullScreenToolBar2->setIconDimensions(KIconLoader::SizeMedium);
mFullScreenToolBar2->setToolButtonStyle(Qt::ToolButtonIconOnly);
mFullScreenToolBar2->addAction(mActionCollection->action(QStringLiteral("leave_fullscreen")));
}
void updateSelectedMediaItems(const QItemSelection &selected, const QItemSelection &deselected)
{
for (auto index : selected.indexes()) {
KFileItem item = mDirModel->itemForIndex(index);
if (!ArchiveUtils::fileItemIsDirOrArchive(item)) {
mSelectedMediaItems->append(item);
}
}
for (auto index : deselected.indexes()) {
KFileItem item = mDirModel->itemForIndex(index);
mSelectedMediaItems->removeOne(item);
}
}
void updateDocumentCountLabel()
{
if (mSelectedMediaItems->count() > 1) {
KIO::filesize_t totalSize = 0;
for (const auto &item : *mSelectedMediaItems) {
totalSize += item.size();
}
const QString text = i18nc("@info:status %1 number of selected documents, %2 total number of documents, %3 total filesize of selected documents",
"Selected %1 of %2 (%3)",
mSelectedMediaItems->count(),
mDocumentCountImages + mDocumentCountVideos,
KIO::convertSize(totalSize));
mDocumentCountLabel->setText(text);
} else {
const QString imageText = i18ncp("@info:status Image files", "%1 image", "%1 images", mDocumentCountImages);
const QString videoText = i18ncp("@info:status Video files", "%1 video", "%1 videos", mDocumentCountVideos);
QString labelText;
if (mDocumentCountImages > 0 && mDocumentCountVideos == 0) {
labelText = imageText;
} else if (mDocumentCountImages == 0 && mDocumentCountVideos > 0) {
labelText = videoText;
} else {
labelText = i18nc("@info:status images, videos", "%1, %2", imageText, videoText);
}
mDocumentCountLabel->setText(labelText);
}
}
void documentCountsForIndexRange(const QModelIndex &parent, int start, int end, int &imageCountOut, int &videoCountOut)
{
imageCountOut = 0;
videoCountOut = 0;
for (int row = start; row <= end; ++row) {
QModelIndex index = mDirModel->index(row, 0, parent);
KFileItem item = mDirModel->itemForIndex(index);
if (!ArchiveUtils::fileItemIsDirOrArchive(item)) {
MimeTypeUtils::Kind kind = MimeTypeUtils::mimeTypeKind(item.mimetype());
if (kind == MimeTypeUtils::KIND_RASTER_IMAGE || kind == MimeTypeUtils::KIND_SVG_IMAGE) {
imageCountOut++;
} else if (kind == MimeTypeUtils::KIND_VIDEO) {
videoCountOut++;
}
}
}
}
void updateContextBarActions()
{
PreviewItemDelegate::ContextBarActions actions;
switch (GwenviewConfig::thumbnailActions()) {
case ThumbnailActions::None:
actions = PreviewItemDelegate::NoAction;
break;
case ThumbnailActions::ShowSelectionButtonOnly:
actions = PreviewItemDelegate::SelectionAction;
break;
case ThumbnailActions::AllButtons:
default:
actions = PreviewItemDelegate::SelectionAction | PreviewItemDelegate::RotateAction;
if (!q->window()->isFullScreen()) {
actions |= PreviewItemDelegate::FullScreenAction;
}
break;
}
mDelegate->setContextBarActions(actions);
}
void applyPalette(bool fullScreenmode)
{
q->setPalette(mGvCore->palette(fullScreenmode ? GvCore::FullScreenPalette : GvCore::NormalPalette));
mThumbnailView->setPalette(mGvCore->palette(fullScreenmode ? GvCore::FullScreenViewPalette : GvCore::NormalViewPalette));
}
};
BrowseMainPage::BrowseMainPage(QWidget *parent, KActionCollection *actionCollection, GvCore *gvCore)
: QWidget(parent)
, d(new BrowseMainPagePrivate)
{
d->q = this;
d->mGvCore = gvCore;
d->mDirModel = gvCore->sortedDirModel();
d->mDocumentCountImages = 0;
d->mDocumentCountVideos = 0;
d->mSelectedMediaItems = new KFileItemList;
d->mActionCollection = actionCollection;
d->setupWidgets();
d->setupActions(actionCollection);
d->setupFilterController();
loadConfig();
updateSortOrder();
updateThumbnailDetails();
// Set up connections for document count
connect(d->mDirModel, &SortedDirModel::rowsInserted, this, &BrowseMainPage::slotDirModelRowsInserted);
connect(d->mDirModel, &SortedDirModel::rowsAboutToBeRemoved, this, &BrowseMainPage::slotDirModelRowsAboutToBeRemoved);
connect(d->mDirModel, &SortedDirModel::modelReset, this, &BrowseMainPage::slotDirModelReset);
connect(thumbnailView(), &ThumbnailView::selectionChangedSignal, this, &BrowseMainPage::slotSelectionChanged);
connect(qApp, &QApplication::paletteChanged, this, [this]() {
d->applyPalette(window()->isFullScreen());
});
installEventFilter(this);
}
BrowseMainPage::~BrowseMainPage()
{
d->mSelectedMediaItems->clear();
delete d->mSelectedMediaItems;
delete d;
}
void BrowseMainPage::loadConfig()
{
d->applyPalette(window()->isFullScreen());
d->mUrlNavigator->setUrlEditable(GwenviewConfig::urlNavigatorIsEditable());
d->mUrlNavigator->setShowFullPath(GwenviewConfig::urlNavigatorShowFullPath());
d->mThumbnailSlider->setValue(GwenviewConfig::thumbnailSize());
d->mThumbnailSlider->updateToolTip();
// If GwenviewConfig::thumbnailSize() returns the current value of
// mThumbnailSlider, it won't emit valueChanged() and the thumbnail view
// won't be updated. That's why we do it ourself.
d->mThumbnailView->setThumbnailAspectRatio(GwenviewConfig::thumbnailAspectRatio());
d->mThumbnailView->setThumbnailWidth(GwenviewConfig::thumbnailSize());
const auto actionsList = d->mSortAction->actions();
for (QAction *action : actionsList) {
if (sortingFromSortAction(action) == GwenviewConfig::sorting()) {
action->setChecked(true);
break;
}
}
d->mSortDescendingAction->setChecked(GwenviewConfig::sortDescending());
d->updateContextBarActions();
}
void BrowseMainPage::saveConfig() const
{
GwenviewConfig::setUrlNavigatorIsEditable(d->mUrlNavigator->isUrlEditable());
GwenviewConfig::setUrlNavigatorShowFullPath(d->mUrlNavigator->showFullPath());
GwenviewConfig::setThumbnailSize(d->mThumbnailSlider->value());
GwenviewConfig::setSorting(sortingFromSortAction(d->mSortAction->checkedAction()));
GwenviewConfig::setSortDescending(d->mSortDescendingAction->isChecked());
GwenviewConfig::setThumbnailDetails(d->mDelegate->thumbnailDetails());
}
bool BrowseMainPage::eventFilter(QObject *watched, QEvent *event)
{
// Leave fullscreen when not viewing an image
if (window()->isFullScreen() && event->type() == QEvent::ShortcutOverride) {
const QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Escape) {
d->mActionCollection->action(QStringLiteral("leave_fullscreen"))->trigger();
event->accept();
}
}
return QWidget::eventFilter(watched, event);
}
void BrowseMainPage::mousePressEvent(QMouseEvent *event)
{
switch (event->button()) {
case Qt::ForwardButton:
case Qt::BackButton:
return;
default:
QWidget::mousePressEvent(event);
}
}
ThumbnailView *BrowseMainPage::thumbnailView() const
{
return d->mThumbnailView;
}
KUrlNavigator *BrowseMainPage::urlNavigator() const
{
return d->mUrlNavigator;
}
void BrowseMainPage::reload()
{
const QModelIndexList list = d->mThumbnailView->selectionModel()->selectedIndexes();
for (const QModelIndex &index : list) {
d->mThumbnailView->reloadThumbnail(index);
}
d->mDirModel->reload();
}
void BrowseMainPage::editLocation()
{
d->mUrlNavigator->setUrlEditable(true);
d->mUrlNavigator->setFocus();
}
void BrowseMainPage::addFolderToPlaces()
{
QUrl url = d->mUrlNavigator->locationUrl();
QString text = url.adjusted(QUrl::StripTrailingSlash).fileName();
if (text.isEmpty()) {
text = url.toDisplayString();
}
d->mFilePlacesModel->addPlace(text, url);
}
void BrowseMainPage::slotDirModelRowsInserted(const QModelIndex &parent, int start, int end)
{
int imageCount;
int videoCount;
d->documentCountsForIndexRange(parent, start, end, imageCount, videoCount);
if (imageCount > 0 || videoCount > 0) {
d->mDocumentCountImages += imageCount;
d->mDocumentCountVideos += videoCount;
d->updateDocumentCountLabel();
}
}
void BrowseMainPage::slotDirModelRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
{
int imageCount;
int videoCount;
d->documentCountsForIndexRange(parent, start, end, imageCount, videoCount);
if (imageCount > 0 || videoCount > 0) {
d->mDocumentCountImages -= imageCount;
d->mDocumentCountVideos -= videoCount;
d->updateDocumentCountLabel();
}
}
void BrowseMainPage::slotDirModelReset()
{
d->mDocumentCountImages = 0;
d->mDocumentCountVideos = 0;
d->mSelectedMediaItems->clear();
d->updateDocumentCountLabel();
}
void BrowseMainPage::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
d->updateSelectedMediaItems(selected, deselected);
d->updateDocumentCountLabel();
}
void BrowseMainPage::updateSortOrder()
{
const QAction *action = d->mSortAction->checkedAction();
GV_RETURN_IF_FAIL(action);
const Qt::SortOrder order = d->mSortDescendingAction->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder;
KDirModel::ModelColumns column = KDirModel::Name;
int sortRole = Qt::DisplayRole;
// Map Sorting::Enum to model columns and sorting roles
switch (sortingFromSortAction(action)) {
case Sorting::Name:
column = KDirModel::Name;
break;
case Sorting::Size:
column = KDirModel::Size;
break;
case Sorting::Date:
column = KDirModel::ModifiedTime;
break;
#ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
case Sorting::Rating:
column = KDirModel::Name;
sortRole = SemanticInfoDirModel::RatingRole;
break;
#endif
}
d->mDirModel->setSortRole(sortRole);
d->mDirModel->sort(column, order);
d->mGvCore->setTrackFileManagerSorting(false);
}
void BrowseMainPage::updateThumbnailDetails()
{
PreviewItemDelegate::ThumbnailDetails details = {};
const auto actionList = d->mThumbnailDetailsActionGroup->actions();
for (const QAction *action : actionList) {
if (action->isChecked()) {
details |= PreviewItemDelegate::ThumbnailDetail(action->data().toInt());
}
}
d->mDelegate->setThumbnailDetails(details);
}
void BrowseMainPage::setFullScreenMode(bool fullScreen)
{
d->applyPalette(fullScreen);
d->mUrlNavigatorContainer->setContentsMargins(fullScreen ? 6 : 0, 0, 0, 0);
d->updateContextBarActions();
d->mFullScreenToolBar->setVisible(fullScreen);
d->mFullScreenToolBar2->setVisible(fullScreen);
if (fullScreen && d->mFullScreenToolBar->actions().isEmpty()) {
d->setupFullScreenToolBar();
}
}
void BrowseMainPage::setStatusBarVisible(bool visible)
{
d->mStatusBarContainer->setVisible(visible);
}
void BrowseMainPage::slotUrlsDropped(const QUrl &destUrl, QDropEvent *event)
{
const QList<QUrl> urlList = KUrlMimeData::urlsFromMimeData(event->mimeData());
if (urlList.isEmpty()) {
return;
}
event->acceptProposedAction();
// We can't call FileOperations::showMenuForDroppedUrls() directly because
// we need the slot to return so that the drop event is accepted. Otherwise
// the drop cursor is still visible when the menu is shown.
QMetaObject::invokeMethod(
this,
[this, urlList, destUrl]() {
showMenuForDroppedUrls(urlList, destUrl);
},
Qt::QueuedConnection);
}
void BrowseMainPage::showMenuForDroppedUrls(const QList<QUrl> &urlList, const QUrl &destUrl)
{
FileOperations::showMenuForDroppedUrls(d->mUrlNavigator, urlList, destUrl);
}
QToolButton *BrowseMainPage::toggleSideBarButton() const
{
return d->mToggleSideBarButton;
}
} // namespace
#include "moc_browsemainpage.cpp"